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

@ -82,7 +82,7 @@
agentPackages =
{ lib, pkgs, ... }:
{
hyperhive.packages = lib.mapAttrs (_: lib.mkDefault) {
services.hyperhive.agent.packages = lib.mapAttrs (_: lib.mkDefault) {
inherit (self.packages.${pkgs.stdenv.hostPlatform.system})
hive-agent
hive-agent-mcp

View file

@ -9,11 +9,11 @@
...
}:
let
userName = config.hyperhive.user.name;
userName = config.services.hyperhive.agent.user.name;
homeDir = "/home/${userName}";
in
{
options.hyperhive.model = lib.mkOption {
options.services.hyperhive.agent.model = lib.mkOption {
type = lib.types.str;
default = "haiku";
example = "sonnet";
@ -34,7 +34,7 @@ in
'';
};
options.hyperhive.availableModels = lib.mkOption {
options.services.hyperhive.agent.availableModels = lib.mkOption {
type = lib.types.listOf lib.types.str;
default = [
"haiku"
@ -54,7 +54,7 @@ in
Configure hive-wide by setting a shared default (e.g. in your
`agent.nix` shared defaults) or per-agent to narrow the menu for example a
haiku-only agent can hide `opus` and `sonnet`. The *current* model is
still set by `hyperhive.model` and remains switchable at runtime via the
still set by `services.hyperhive.agent.model` and remains switchable at runtime via the
UI; this option only controls which choices the picker presents.
Values are the short model names that `claude --model` accepts:
@ -62,7 +62,7 @@ in
'';
};
options.hyperhive.effortLevel = lib.mkOption {
options.services.hyperhive.agent.effortLevel = lib.mkOption {
type = lib.types.enum [
"low"
"medium"
@ -87,7 +87,7 @@ in
'';
};
options.hyperhive.autoCompact = lib.mkOption {
options.services.hyperhive.agent.autoCompact = lib.mkOption {
type = lib.types.bool;
default = true;
description = ''
@ -106,7 +106,7 @@ in
'';
};
options.hyperhive.useApiKey = lib.mkOption {
options.services.hyperhive.agent.useApiKey = lib.mkOption {
type = lib.types.bool;
default = false;
description = ''
@ -121,14 +121,14 @@ in
`~/.claude/` as "needs login" for this agent (see
`hive_c0re::container_view`'s `needs_login` computation).
Set this AND `hyperhive.backendEnvironmentFile` together this
Set this AND `services.hyperhive.agent.backendEnvironmentFile` together this
option changes what the harness believes about its own login state,
the other actually supplies the credentials `claude` reads. Neither
is useful alone.
'';
};
options.hyperhive.backendEnvironmentFile = lib.mkOption {
options.services.hyperhive.agent.backendEnvironmentFile = lib.mkOption {
type = lib.types.nullOr lib.types.str;
default = null;
example = "/agents/myagent/harness/openrouter.env";
@ -160,12 +160,12 @@ in
so a path that doesn't exist yet an operator setting this option
before creating the file, or a fresh host rebuild before state is
restored makes systemd skip it rather than refuse to start the
harness. See `hyperhive.useApiKey`'s doc for the option this one is
harness. See `services.hyperhive.agent.useApiKey`'s doc for the option this one is
paired with.
'';
};
options.hyperhive.extraWebProxies = lib.mkOption {
options.services.hyperhive.agent.extraWebProxies = lib.mkOption {
type = lib.types.attrsOf lib.types.str;
default = { };
example = lib.literalExpression ''{ "stats" = "http://127.0.0.1:3737"; }'';
@ -192,21 +192,21 @@ in
config = {
assertions = [
# hyperhive.model must be a non-empty string — an empty value causes
# services.hyperhive.agent.model must be a non-empty string — an empty value causes
# the harness to pass an invalid model flag to claude.
{
assertion = config.hyperhive.model != "";
message = "hyperhive.model must not be empty (set it to e.g. \"haiku\" or \"sonnet\")";
assertion = config.services.hyperhive.agent.model != "";
message = "services.hyperhive.agent.model must not be empty (set it to e.g. \"haiku\" or \"sonnet\")";
}
# The current model must appear in the quick-picker menu, otherwise the
# UI would offer no way back to the model the agent is actually running.
{
assertion =
config.hyperhive.availableModels == [ ]
|| builtins.elem config.hyperhive.model config.hyperhive.availableModels;
config.services.hyperhive.agent.availableModels == [ ]
|| builtins.elem config.services.hyperhive.agent.model config.services.hyperhive.agent.availableModels;
message =
"hyperhive.model (\"${config.hyperhive.model}\") must be one of "
+ "hyperhive.availableModels ([ ${lib.concatStringsSep " " config.hyperhive.availableModels} ]) "
"services.hyperhive.agent.model (\"${config.services.hyperhive.agent.model}\") must be one of "
+ "services.hyperhive.agent.availableModels ([ ${lib.concatStringsSep " " config.services.hyperhive.agent.availableModels} ]) "
+ " add it to the list or change the model.";
}
];
@ -214,17 +214,17 @@ in
# HIVE_DEFAULT_MODEL seeds the initial model selection when no
# persisted model choice exists in the state dir.
environment.variables = {
HIVE_DEFAULT_MODEL = config.hyperhive.model;
HIVE_DEFAULT_MODEL = config.services.hyperhive.agent.model;
# Comma-separated menu for the per-agent UI model quick-picker
# (see hyperhive.availableModels). The harness surfaces it to the
# (see services.hyperhive.agent.availableModels). The harness surfaces it to the
# frontend; an empty value falls back to the built-in default list.
HIVE_AVAILABLE_MODELS = lib.concatStringsSep "," config.hyperhive.availableModels;
# Per-agent baseline effort (see hyperhive.effortLevel). The
HIVE_AVAILABLE_MODELS = lib.concatStringsSep "," config.services.hyperhive.agent.availableModels;
# Per-agent baseline effort (see services.hyperhive.agent.effortLevel). The
# harness resolves operator-override-file → this env → "medium"
# and passes it to claude --effort at turn launch.
HIVE_DEFAULT_EFFORT = config.hyperhive.effortLevel;
HIVE_DEFAULT_EFFORT = config.services.hyperhive.agent.effortLevel;
}
// lib.optionalAttrs (!config.hyperhive.autoCompact) {
// lib.optionalAttrs (!config.services.hyperhive.agent.autoCompact) {
# Zero watermark disables proactive compaction; the reactive path
# (compact-on-overflow) still fires when the session is truly full.
HIVE_COMPACT_WATERMARK_TOKENS = "0";
@ -253,8 +253,8 @@ in
environment = {
SHELL = "${pkgs.bashInteractive}/bin/bash";
HOME = homeDir;
HIVE_STATIC_DIR = "${config.hyperhive.frontend.mergedDist}";
HIVE_ASSETS_DIR = "${config.hyperhive.packages.assets}/share/hyperhive";
HIVE_STATIC_DIR = "${config.services.hyperhive.agent.frontend.mergedDist}";
HIVE_ASSETS_DIR = "${config.services.hyperhive.agent.packages.assets}/share/hyperhive";
# Unix-socket path for the harness web UI. All agents always bind
# here; there is no TCP fallback. Path matches
# `hive_c0re::agent_sockets::socket_path_for(name)` so lifecycle
@ -274,30 +274,30 @@ in
# (see ./mcp.nix) via the same option. Always set — network
# isolation is unconditional, so a fixed per-container port is
# collision-free.
HYPERHIVE_MCP_HTTP_PORT = toString config.hyperhive.mcp.httpPort;
HYPERHIVE_MCP_HTTP_PORT = toString config.services.hyperhive.agent.mcp.httpPort;
}
// lib.optionalAttrs config.hyperhive.gui.enable {
// lib.optionalAttrs config.services.hyperhive.agent.gui.enable {
# Tells the harness which fixed VNC port weston bound, and (by
# its presence) that gui is enabled — the harness `/screen/ws`
# relay reads this instead of a runtime marker file. The port is
# container-local + fixed (network isolation is unconditional),
# so the same value for every gui agent is fine. See
# ./weston-vnc.nix::hyperhive.gui.vncPort.
HIVE_GUI_VNC_PORT = toString config.hyperhive.gui.vncPort;
# ./weston-vnc.nix::services.hyperhive.agent.gui.vncPort.
HIVE_GUI_VNC_PORT = toString config.services.hyperhive.agent.gui.vncPort;
}
// lib.optionalAttrs (config.hyperhive.extraWebProxies != { }) {
// lib.optionalAttrs (config.services.hyperhive.agent.extraWebProxies != { }) {
# JSON object {"<path>": "<upstream>"} for the transparent
# reverse-proxies. See `hyperhive.extraWebProxies` option
# reverse-proxies. See `services.hyperhive.agent.extraWebProxies` option
# and `web_ui/proxy.rs::extra_proxy_service`.
HIVE_EXTRA_WEB_PROXIES = builtins.toJSON config.hyperhive.extraWebProxies;
HIVE_EXTRA_WEB_PROXIES = builtins.toJSON config.services.hyperhive.agent.extraWebProxies;
}
// lib.optionalAttrs config.hyperhive.useApiKey {
// lib.optionalAttrs config.services.hyperhive.agent.useApiKey {
# Tells the harness not to wait for a Claude OAuth session — see
# `hyperhive.useApiKey`'s own description for the full mechanism.
# `services.hyperhive.agent.useApiKey`'s own description for the full mechanism.
HIVE_USE_API_KEY = "1";
};
serviceConfig = {
ExecStart = "${config.hyperhive.packages.hive-agent}/bin/${binary}";
ExecStart = "${config.services.hyperhive.agent.packages.hive-agent}/bin/${binary}";
# Pin the journal identity to the binary name (otherwise systemd
# derives SyslogIdentifier from the ExecStart basename).
SyslogIdentifier = binary;
@ -321,10 +321,10 @@ in
User = userName;
Group = userName;
}
// lib.optionalAttrs (config.hyperhive.backendEnvironmentFile != null) {
# See `hyperhive.backendEnvironmentFile`'s own description for
// lib.optionalAttrs (config.services.hyperhive.agent.backendEnvironmentFile != null) {
# See `services.hyperhive.agent.backendEnvironmentFile`'s own description for
# the file shape and the leading-`-` rationale.
EnvironmentFile = "-${config.hyperhive.backendEnvironmentFile}";
EnvironmentFile = "-${config.services.hyperhive.agent.backendEnvironmentFile}";
};
};
};

View file

@ -16,7 +16,7 @@
# all contributions across modules into one file. Generic by
# design so future hooks don't need to rename this file or
# invent a parallel dispatcher.
options.hyperhive._bashEnvFragments = lib.mkOption {
options.services.hyperhive.agent._bashEnvFragments = lib.mkOption {
type = lib.types.lines;
default = "";
internal = true;
@ -27,11 +27,11 @@
unset, and the interactive bashrc hook is omitted zero cost
when no feature is on. Internal set indirectly via the
per-feature options that own the gate (e.g.
`hyperhive.cargo.shortMessages`).
`services.hyperhive.agent.cargo.shortMessages`).
'';
};
options.hyperhive.cargo.shortMessages = lib.mkOption {
options.services.hyperhive.agent.cargo.shortMessages = lib.mkOption {
type = lib.types.bool;
default = true;
example = false;
@ -45,7 +45,7 @@
carry no signal beyond the warning/error summary.
Implementation: contributes a `cargo` shell function to
`/etc/hyperhive/bash-env.sh` (see `hyperhive._bashEnvFragments`).
`/etc/hyperhive/bash-env.sh` (see `services.hyperhive.agent._bashEnvFragments`).
Loaded via `BASH_ENV` for non-interactive shells (`bash -c`
what the claude `Bash` tool runs) and sourced from
`programs.bash.interactiveShellInit` for interactive shells.
@ -70,7 +70,7 @@
# back to the un-wrapped binary in PATH (the rust toolchain's cargo
# — either from `environment.systemPackages` or from whatever
# `nix develop` shell the agent's working in).
hyperhive._bashEnvFragments = lib.mkIf config.hyperhive.cargo.shortMessages ''
services.hyperhive.agent._bashEnvFragments = lib.mkIf config.services.hyperhive.agent.cargo.shortMessages ''
# Auto-injects --message-format short on cargo compile
# subcommands so per-crate progress lines don't flood
# claude's context. Bypassed when the caller already passes
@ -108,15 +108,17 @@
# Wiring is gated on at least one fragment being active so a
# fully feature-disabled agent has neither the file nor the
# `BASH_ENV` / interactive sourcing — zero cost in that case.
environment.etc."hyperhive/bash-env.sh" = lib.mkIf (config.hyperhive._bashEnvFragments != "") {
text = config.hyperhive._bashEnvFragments;
};
environment.etc."hyperhive/bash-env.sh" =
lib.mkIf (config.services.hyperhive.agent._bashEnvFragments != "")
{
text = config.services.hyperhive.agent._bashEnvFragments;
};
# Non-interactive bash invocations (claude's `Bash` tool runs
# `bash -c`) source $BASH_ENV at startup — drops every active
# feature hook's snippet into scope without touching
# `/etc/profile` (login-only).
environment.variables = lib.mkIf (config.hyperhive._bashEnvFragments != "") {
environment.variables = lib.mkIf (config.services.hyperhive.agent._bashEnvFragments != "") {
BASH_ENV = "/etc/hyperhive/bash-env.sh";
};
@ -125,10 +127,12 @@
# hook surface as claude's non-interactive calls. Gated on at
# least one fragment being active so we don't write a no-op
# source line into `/etc/bashrc` on fully-feature-disabled agents.
programs.bash.interactiveShellInit = lib.mkIf (config.hyperhive._bashEnvFragments != "") ''
if [ -r /etc/hyperhive/bash-env.sh ]; then
. /etc/hyperhive/bash-env.sh
fi
'';
programs.bash.interactiveShellInit =
lib.mkIf (config.services.hyperhive.agent._bashEnvFragments != "")
''
if [ -r /etc/hyperhive/bash-env.sh ]; then
. /etc/hyperhive/bash-env.sh
fi
'';
};
}

View file

@ -13,12 +13,12 @@
...
}:
let
userName = config.hyperhive.user.name;
userName = config.services.hyperhive.agent.user.name;
homeDir = "/home/${userName}";
# Hive-wide OpenTelemetry config (host-driven; baked in per-agent by
# meta.rs `otel_config`). Options declared in `otel.nix`, which also
# exports the generic OTLP environment this container's producers read.
otelCfg = config.hyperhive.otel;
otelCfg = config.services.hyperhive.agent.otel;
# Hive display name, read from the per-agent option meta.rs renders
# (NOT from `environment.variables` — that carries the same name at
# *runtime* only, so reading it here silently yielded "unknown" on
@ -26,12 +26,15 @@ let
# means the hive did not name itself; "unknown" is then an honest label
# rather than a guess.
hiveDisplayName =
if config.hyperhive.hiveName == null then "unknown" else config.hyperhive.hiveName;
if config.services.hyperhive.agent.hiveName == null then
"unknown"
else
config.services.hyperhive.agent.hiveName;
# Effective per-agent MemoryMax=, in bytes, injected by meta.rs's
# per-agent flake render (`hyperhive.claudeMemoryMaxBytes`). `null`
# per-agent flake render (`services.hyperhive.agent.claudeMemoryMaxBytes`). `null`
# when the effective cap is unbounded ("infinity") or a RAM
# percentage — see `resource_limits::effective_memory_bytes`.
memoryMaxBytes = config.hyperhive.claudeMemoryMaxBytes;
memoryMaxBytes = config.services.hyperhive.agent.claudeMemoryMaxBytes;
# Base claude-code environment applied to every agent regardless of OTEL.
# Shipped via the managed settings `env` block so claude and `hivectl
# choom` both inherit them without a launch wrapper.
@ -99,13 +102,13 @@ in
# `MemoryMax=` (per-agent `resource-limits.json` override, else the
# hive-wide `services.hyperhive.agentMemoryMax`) — see
# `resource_limits::effective_memory_bytes_from`. Not meant to be set
# directly in an agent.nix, same convention as `hyperhive.otel.*`
# directly in an agent.nix, same convention as `services.hyperhive.agent.otel.*`
# above; the host option (or `hivectl agent <name> set-limits`) is
# the real operator knob, and this only reflects the value baked in at
# the agent's *last rebuild* — `set-limits` still applies the
# cgroup cap live via a drop-in reload, but this derived heap ceiling
# needs a rebuild to pick up a new value.
options.hyperhive.hiveName = lib.mkOption {
options.services.hyperhive.agent.hiveName = lib.mkOption {
type = lib.types.nullOr lib.types.str;
default = null;
internal = true;
@ -122,7 +125,7 @@ in
'';
};
options.hyperhive.swarmName = lib.mkOption {
options.services.hyperhive.agent.swarmName = lib.mkOption {
type = lib.types.nullOr lib.types.str;
default = null;
internal = true;
@ -130,13 +133,13 @@ in
Human-readable swarm name, rendered per-agent by
`meta.rs::render_flake` from the host's
`services.hyperhive.swarm.name`. Same build-time/runtime split as
`hyperhive.hiveName`.
`services.hyperhive.agent.hiveName`.
`null` means the hive is not part of a named swarm.
'';
};
options.hyperhive.claudeMemoryMaxBytes = lib.mkOption {
options.services.hyperhive.agent.claudeMemoryMaxBytes = lib.mkOption {
type = lib.types.nullOr lib.types.ints.positive;
default = null;
internal = true;
@ -150,11 +153,11 @@ in
'';
};
options.hyperhive.claudeMarketplaces = lib.mkOption {
options.services.hyperhive.agent.claudeMarketplaces = lib.mkOption {
type = lib.types.listOf lib.types.str;
default = [
"anthropics/claude-plugins-official"
"${config.hyperhive.packages.claude-plugins}"
"${config.services.hyperhive.agent.packages.claude-plugins}"
];
defaultText = lib.literalMD ''
`[ "anthropics/claude-plugins-official" "''${hyperhive.packages.claude-plugins}" ]`
@ -169,7 +172,7 @@ in
entry is passed to `claude plugin marketplace add <source>`
(`owner/repo`, full git URL, or local path). Idempotent
re-adding an existing marketplace is treated as success.
Required before `hyperhive.claudePlugins` entries that
Required before `services.hyperhive.agent.claudePlugins` entries that
reference a marketplace (e.g. `foo@claude-plugins-official`).
Rendered to `/etc/hyperhive/claude-marketplaces.json`.
@ -185,7 +188,7 @@ in
'';
};
options.hyperhive.claudePlugins = lib.mkOption {
options.services.hyperhive.agent.claudePlugins = lib.mkOption {
type = lib.types.listOf lib.types.str;
default = [
"skill-creator@claude-plugins-official"
@ -224,7 +227,7 @@ in
'';
};
options.hyperhive.claudePluginsAutoUpdate = lib.mkOption {
options.services.hyperhive.agent.claudePluginsAutoUpdate = lib.mkOption {
type = lib.types.bool;
default = false;
description = ''
@ -239,13 +242,13 @@ in
config = {
environment.etc."hyperhive/claude-plugins.json".text =
builtins.toJSON config.hyperhive.claudePlugins;
builtins.toJSON config.services.hyperhive.agent.claudePlugins;
environment.etc."hyperhive/claude-marketplaces.json".text =
builtins.toJSON config.hyperhive.claudeMarketplaces;
builtins.toJSON config.services.hyperhive.agent.claudeMarketplaces;
environment.etc."hyperhive/claude-plugins-auto-update.json".text =
builtins.toJSON config.hyperhive.claudePluginsAutoUpdate;
builtins.toJSON config.services.hyperhive.agent.claudePluginsAutoUpdate;
# Hive-enforced claude settings. claude-code auto-discovers managed
# settings at this canonical Linux path (precedence #1, read-only,
@ -263,7 +266,7 @@ in
# `hivectl choom`) so no launch wrapper is needed.
environment.etc."claude-code/managed-settings.json".source =
let
baseSettings = "${config.hyperhive.packages.assets}/share/hyperhive/prompts/claude-settings.json";
baseSettings = "${config.services.hyperhive.agent.packages.assets}/share/hyperhive/prompts/claude-settings.json";
# Merge base env (always) with OTEL env (when enabled). jq is always
# run — `baseClaudeEnv` contains per-agent values (e.g.
# CLAUDE_REMOTE_CONTROL_SESSION_NAME_PREFIX) that can't live in the

View file

@ -7,7 +7,7 @@
...
}:
{
options.hyperhive.dashboardLinks = lib.mkOption {
options.services.hyperhive.agent.dashboardLinks = lib.mkOption {
type = lib.types.listOf (
lib.types.submodule {
options = {
@ -47,27 +47,29 @@
# read them without accessing the container's /etc/ from the host.
# Best-effort oneshot (always exit 0):
# docs/process/conventions.md::Best-effort oneshot services.
systemd.services.hive-dashboard-links = lib.mkIf (config.hyperhive.dashboardLinks != [ ]) {
description = "write declarative dashboardLinks to agent state dir";
wantedBy = [ "multi-user.target" ];
serviceConfig = {
Type = "oneshot";
RemainAfterExit = true;
# Pin the journal identity (else it's the `script` store-path wrapper).
SyslogIdentifier = "hive-dashboard-links";
};
environment.LINKS_JSON = builtins.toJSON config.hyperhive.dashboardLinks;
script = ''
# Sub-agents have their state dir bind-mounted at /agents/<name>/state.
# Use a glob — exactly one match per container at runtime.
STATE_DIR=$(echo /agents/*/state)
if [ ! -d "$STATE_DIR" ]; then
echo "hive-dashboard-links: no state dir found at /agents/*/state; skipping"
exit 0
fi
printf '%s' "$LINKS_JSON" > "$STATE_DIR/hyperhive-dashboard-links.json"
echo "hive-dashboard-links: wrote $(printf '%s' "$LINKS_JSON" | wc -c) bytes to $STATE_DIR/hyperhive-dashboard-links.json"
'';
};
systemd.services.hive-dashboard-links =
lib.mkIf (config.services.hyperhive.agent.dashboardLinks != [ ])
{
description = "write declarative dashboardLinks to agent state dir";
wantedBy = [ "multi-user.target" ];
serviceConfig = {
Type = "oneshot";
RemainAfterExit = true;
# Pin the journal identity (else it's the `script` store-path wrapper).
SyslogIdentifier = "hive-dashboard-links";
};
environment.LINKS_JSON = builtins.toJSON config.services.hyperhive.agent.dashboardLinks;
script = ''
# Sub-agents have their state dir bind-mounted at /agents/<name>/state.
# Use a glob — exactly one match per container at runtime.
STATE_DIR=$(echo /agents/*/state)
if [ ! -d "$STATE_DIR" ]; then
echo "hive-dashboard-links: no state dir found at /agents/*/state; skipping"
exit 0
fi
printf '%s' "$LINKS_JSON" > "$STATE_DIR/hyperhive-dashboard-links.json"
echo "hive-dashboard-links: wrote $(printf '%s' "$LINKS_JSON" | wc -c) bytes to $STATE_DIR/hyperhive-dashboard-links.json"
'';
};
};
}

View file

@ -4,9 +4,9 @@
# they came from.
#
# This is the core module: container plumbing (boot/nix/nixpkgs),
# base tooling, and the cross-cutting `hyperhive.icon` option. Each
# 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 `hyperhive.*` options + config.
# declares its own `services.hyperhive.agent.*` options + config.
{
pkgs,
lib,
@ -35,6 +35,7 @@
./otel.nix
./packages.nix
./queue.nix
./renamed-options.nix
./user.nix
./screen.nix
./weston-vnc.nix
@ -48,7 +49,7 @@
'')
];
options.hyperhive.icon = lib.mkOption {
options.services.hyperhive.agent.icon = lib.mkOption {
type = lib.types.nullOr lib.types.path;
default = null;
example = lib.literalExpression "./icon.svg";
@ -65,7 +66,7 @@
'';
};
options.hyperhive.claudeCodePath = lib.mkOption {
options.services.hyperhive.agent.claudeCodePath = lib.mkOption {
type = lib.types.nullOr lib.types.str;
default = null;
example = "/nix/store/-claude-code-2.1.220";
@ -108,20 +109,22 @@
_module.args.flakeInputs = builtins.removeAttrs inputs [ "self" ];
'';
}
# hyperhive.icon must reference an SVG file when set.
# services.hyperhive.agent.icon must reference an SVG file when set.
{
assertion = config.hyperhive.icon == null || lib.hasSuffix ".svg" (toString config.hyperhive.icon);
message = "hyperhive.icon must point to an .svg file";
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 (hyperhive.icon). When configured, the
# 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.hyperhive.icon != null) {
source = config.hyperhive.icon;
environment.etc."hyperhive/icon.svg" = lib.mkIf (config.services.hyperhive.agent.icon != null) {
source = config.services.hyperhive.agent.icon;
};
boot.isNspawnContainer = true;
@ -213,11 +216,11 @@
# `hive-metric` (agent-emitted custom metrics CLI,
# docs/scheduler/observability.md).
environment.systemPackages = [
config.hyperhive.packages.hive-metric
config.services.hyperhive.agent.packages.hive-metric
]
++ [
(
if config.hyperhive.claudeCodePath == null then
if config.services.hyperhive.agent.claudeCodePath == null then
pkgs.claude-code
else
# Host-pinned claude: a symlink farm around a path the
@ -235,7 +238,7 @@
# see `services.hyperhive.c0re.claudeCodePackage`.
pkgs.runCommandLocal "claude-code-pinned" { } ''
mkdir -p "$out/bin"
ln -s ${config.hyperhive.claudeCodePath}/bin/claude "$out/bin/claude"
ln -s ${config.services.hyperhive.agent.claudeCodePath}/bin/claude "$out/bin/claude"
''
)
]
@ -262,7 +265,7 @@
# host-level `services.hyperhive.c0re.contextWindowTokens` option — not
# set here.
environment.variables = {
HIVE_ASSETS_DIR = "${config.hyperhive.packages.assets}/share/hyperhive";
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

View file

@ -1,4 +1,4 @@
# In-container hyperhive reference docs: the `hyperhive.docs.*`
# In-container hyperhive reference docs: the `services.hyperhive.agent.docs.*`
# options and the `$HIVE_DOCS_DIR` wiring the harness reads.
{
pkgs,
@ -7,7 +7,7 @@
...
}:
{
options.hyperhive.docs.enable = lib.mkEnableOption ''
options.services.hyperhive.agent.docs.enable = lib.mkEnableOption ''
make the hyperhive reference docs (the repo `docs/` tree, shipped
read-only as the standalone `hyperhive-docs` derivation) available
in-container. When enabled the harness exposes the docs dir to claude
@ -19,13 +19,13 @@
`agent.nix`.
'';
options.hyperhive.docs.source = lib.mkOption {
options.services.hyperhive.agent.docs.source = lib.mkOption {
type = lib.types.path;
default = config.hyperhive.packages.reference-docs;
default = config.services.hyperhive.agent.packages.reference-docs;
defaultText = lib.literalMD "`hyperhive.packages.reference-docs` (built from the repo `docs/` tree)";
description = ''
Store path of the reference-docs tree exposed at `$HIVE_DOCS_DIR`
when `hyperhive.docs.enable` is set. Defaults to the flake's
when `services.hyperhive.agent.docs.enable` is set. Defaults to the flake's
`reference-docs` package (the `nix/packages/reference-docs.nix`
build) so a standalone container build from a full checkout
works unchanged. The generated meta flake overrides this with the
@ -36,15 +36,15 @@
};
config = {
environment.variables = lib.mkIf config.hyperhive.docs.enable {
environment.variables = lib.mkIf config.services.hyperhive.agent.docs.enable {
# The harness reads HIVE_DOCS_DIR and passes it to claude as
# `--add-dir` so the docs are readable, and appends a single
# pointer sentence to the system prompt
# (hive-agent::prompt::render) telling the agent the docs exist.
# Source is `hyperhive.docs.source` (the narrow `hyperhive-docs`
# Source is `services.hyperhive.agent.docs.source` (the narrow `hyperhive-docs`
# meta-flake input, or `pkgs.hyperhive-docs` for standalone
# builds). See hive-agent::turn.
HIVE_DOCS_DIR = "${config.hyperhive.docs.source}";
HIVE_DOCS_DIR = "${config.services.hyperhive.agent.docs.source}";
};
};
}

View file

@ -8,14 +8,14 @@
...
}:
let
userName = config.hyperhive.user.name;
userName = config.services.hyperhive.agent.user.name;
homeDir = "/home/${userName}";
# Same 512×512 rasterization of the agent icon the matrix avatar
# sync uses (./matrix.nix — identical derivation, same store path).
# Only forced when an icon is configured (the avatar-sync unit below
# is gated on `hyperhive.icon != null`).
# is gated on `services.hyperhive.agent.icon != null`).
iconPng = pkgs.runCommand "hive-agent-icon.png" { nativeBuildInputs = [ pkgs.librsvg ]; } ''
rsvg-convert -f png -w 512 -h 512 ${config.hyperhive.icon} -o $out
rsvg-convert -f png -w 512 -h 512 ${config.services.hyperhive.agent.icon} -o $out
'';
# git credential helper for the hive forge --- the exact shape
@ -39,7 +39,7 @@ let
'';
in
{
options.hyperhive.forge.url = lib.mkOption {
options.services.hyperhive.agent.forge.url = lib.mkOption {
type = lib.types.nullOr lib.types.str;
default = null;
example = "http://forge.internal:3000";
@ -80,10 +80,10 @@ in
# is for, so reject it and name the option.
{
assertion =
config.hyperhive.forge.url == null
|| lib.hasPrefix "http://" config.hyperhive.forge.url
|| lib.hasPrefix "https://" config.hyperhive.forge.url;
message = "hyperhive.forge.url must be an http:// or https:// URL, or null for no forge (got: \"${toString config.hyperhive.forge.url}\")";
config.services.hyperhive.agent.forge.url == null
|| lib.hasPrefix "http://" config.services.hyperhive.agent.forge.url
|| lib.hasPrefix "https://" config.services.hyperhive.agent.forge.url;
message = "services.hyperhive.agent.forge.url must be an http:// or https:// URL, or null for no forge (got: \"${toString config.services.hyperhive.agent.forge.url}\")";
}
];
@ -95,9 +95,9 @@ in
# hive-forge <verb>: CLI wrapping common Forgejo REST API operations
# (view, pr, issue, comment, assign, close, labels, branches, etc.).
# The per-bin split package — narrow closure, no hivectl/wireguard.
config.hyperhive.packages.hive-forge
config.services.hyperhive.agent.packages.hive-forge
]
++ lib.optional (config.hyperhive.forge.url != null) gitCredHelper;
++ lib.optional (config.services.hyperhive.agent.forge.url != null) gitCredHelper;
# Wire the forge credential helper for `git push`, scoped to the forge
# this agent is configured for.
@ -135,9 +135,9 @@ in
# works for any agent whose personal `~/.gitconfig` names the helper by
# ABSOLUTE path — so this is masked exactly where it would be noticed,
# and bites a fresh agent that has no such file.
environment.etc."gitconfig" = lib.mkIf (config.hyperhive.forge.url != null) {
environment.etc."gitconfig" = lib.mkIf (config.services.hyperhive.agent.forge.url != null) {
text = ''
[credential "${lib.removeSuffix "/" config.hyperhive.forge.url}"]
[credential "${lib.removeSuffix "/" config.services.hyperhive.agent.forge.url}"]
helper = hive-forge
username = ${userName}
'';
@ -165,7 +165,7 @@ in
# and the agent's `forge-token` from under the second.
};
serviceConfig = {
ExecStart = "${config.hyperhive.packages.hive-forge-notify}/bin/hive-forge-notify";
ExecStart = "${config.services.hyperhive.agent.packages.hive-forge-notify}/bin/hive-forge-notify";
SyslogIdentifier = "hive-forge-notify";
# `on-failure`, NOT `always`: an agent with no forge account is a
# supported configuration, and the poller reports that by logging
@ -183,7 +183,7 @@ in
# docs/process/conventions.md::Best-effort oneshot services.
# Not generated at all when no forge is configured: an absent
# integration rather than one pointed at a guessed address.
systemd.services.tea-login = lib.mkIf (config.hyperhive.forge.url != null) {
systemd.services.tea-login = lib.mkIf (config.services.hyperhive.agent.forge.url != null) {
description = "configure tea CLI from hive-forge token (best-effort)";
wantedBy = [ "multi-user.target" ];
after = [ "local-fs.target" ];
@ -202,7 +202,7 @@ in
environment.AGENT_USER = userName;
script = ''
# No `set -e`: best-effort posture (see docs pointer above).
FORGE_URL=${lib.escapeShellArg config.hyperhive.forge.url}
FORGE_URL=${lib.escapeShellArg config.services.hyperhive.agent.forge.url}
# $HYPERHIVE_STATE_DIR is system-wide via the meta flake.
TOKEN_FILE="$HYPERHIVE_STATE_DIR/forge-token"
if [ ! -f "$TOKEN_FILE" ]; then
@ -266,13 +266,13 @@ in
# watches paths this unit has no business reacting to.
# The service reads `$HYPERHIVE_STATE_DIR/forge-token`; this is the same
# file, spelled the way `tea-login` above already spells it.
systemd.paths.forge-avatar-sync = lib.mkIf (config.hyperhive.icon != null) {
systemd.paths.forge-avatar-sync = lib.mkIf (config.services.hyperhive.agent.icon != null) {
description = "trigger forge-avatar-sync when forge-token appears";
wantedBy = [ "multi-user.target" ];
pathConfig.PathChanged = "/agents/${userName}/state/forge-token";
};
# One-shot: hyperhive.icon → Forgejo profile avatar. Shape contract:
# One-shot: services.hyperhive.agent.icon → Forgejo profile avatar. Shape contract:
# docs/process/conventions.md::Best-effort oneshot services.
# RemainAfterExit = false so the .path trigger above can re-fire
# this unit when the forge-token arrives after boot. The PNG is
@ -281,7 +281,8 @@ in
# and needs no librsvg at runtime — Forgejo's Go image library
# can't decode SVG, hence PNG.
systemd.services.forge-avatar-sync =
lib.mkIf (config.hyperhive.icon != null && config.hyperhive.forge.url != null)
lib.mkIf
(config.services.hyperhive.agent.icon != null && config.services.hyperhive.agent.forge.url != null)
{
description = "sync agent icon to Forgejo user avatar (best-effort)";
wantedBy = [ "multi-user.target" ];
@ -298,7 +299,7 @@ in
pkgs.jq
];
script = ''
FORGE_URL=${lib.escapeShellArg config.hyperhive.forge.url}
FORGE_URL=${lib.escapeShellArg config.services.hyperhive.agent.forge.url}
# $HYPERHIVE_STATE_DIR is set system-wide by the meta flake
# (systemd.globalEnvironment) to `/agents/<name>/state`.
TOKEN_FILE="$HYPERHIVE_STATE_DIR/forge-token"

View file

@ -8,9 +8,9 @@
...
}:
{
options.hyperhive.frontend.dist = lib.mkOption {
options.services.hyperhive.agent.frontend.dist = lib.mkOption {
type = lib.types.package;
default = config.hyperhive.packages.frontend;
default = config.services.hyperhive.agent.packages.frontend;
defaultText = lib.literalMD "`hyperhive.packages.frontend` (the flake's frontend dist)";
description = ''
The shipped frontend dist (built by `nix/packages/frontend.nix`).
@ -22,18 +22,18 @@
'';
};
options.hyperhive.frontend.mergedDist = lib.mkOption {
options.services.hyperhive.agent.frontend.mergedDist = lib.mkOption {
type = lib.types.package;
readOnly = true;
description = ''
Computed: the merged static tree consumed by the harness via
`HIVE_STATIC_DIR`. Composed at evaluation time by copying
`hyperhive.frontend.dist`'s `agent/` subdir as the base, then
`services.hyperhive.agent.frontend.dist`'s `agent/` subdir as the base, then
layering each `extraFiles` entry on top. Read-only do not set directly.
'';
};
options.hyperhive.frontend.extraFiles = lib.mkOption {
options.services.hyperhive.agent.frontend.extraFiles = lib.mkOption {
type = lib.types.attrsOf (
lib.types.submodule (
{ name, ... }:
@ -100,7 +100,7 @@
prior entry's target), the `mergedDist` build aborts with
`refusing to overwrite existing path '<target>' in the
default dist`. To override a default file, fork the dist via
`hyperhive.frontend.dist` instead `extraFiles` is for
`services.hyperhive.agent.frontend.dist` instead `extraFiles` is for
pure additions.
`target` must be a relative path inside the static dir. An
@ -112,7 +112,7 @@
config = {
assertions = [
# hyperhive.frontend.extraFiles[*].target is concatenated into
# services.hyperhive.agent.frontend.extraFiles[*].target is concatenated into
# $out during the mergedDist build. The option's strMatching
# type already rejects leading `/`, leading `.`, and the
# weirder characters; this assertion catches mid-path `..`
@ -122,10 +122,10 @@
# kind of mistake that's easy to make and hard to spot.
{
assertion = lib.all (entry: !(builtins.any (seg: seg == "..") (lib.splitString "/" entry.target))) (
lib.attrValues config.hyperhive.frontend.extraFiles
lib.attrValues config.services.hyperhive.agent.frontend.extraFiles
);
message = ''
hyperhive.frontend.extraFiles: `target` must not contain
services.hyperhive.agent.frontend.extraFiles: `target` must not contain
`..` path segments.
'';
}
@ -137,20 +137,22 @@
# filename collision with the default dist surfaces as a build
# failure rather than a silent override (operator gets a clear
# nix error rather than a confusing 404 / silent dist swap).
hyperhive.frontend.mergedDist = pkgs.runCommand "hyperhive-agent-frontend-merged" { } (
''
mkdir -p $out
cp -r ${config.hyperhive.frontend.dist}/agent/. $out/
chmod -R u+w $out
''
+ lib.concatMapStrings (entry: ''
mkdir -p $(dirname $out/${entry.target})
if [ -e $out/${entry.target} ]; then
echo "hyperhive.frontend.extraFiles: refusing to overwrite existing path '${entry.target}' in the default dist" >&2
exit 1
fi
cp -r ${entry.source} $out/${entry.target}
'') (lib.attrValues config.hyperhive.frontend.extraFiles)
);
services.hyperhive.agent.frontend.mergedDist =
pkgs.runCommand "hyperhive-agent-frontend-merged" { }
(
''
mkdir -p $out
cp -r ${config.services.hyperhive.agent.frontend.dist}/agent/. $out/
chmod -R u+w $out
''
+ lib.concatMapStrings (entry: ''
mkdir -p $(dirname $out/${entry.target})
if [ -e $out/${entry.target} ]; then
echo "services.hyperhive.agent.frontend.extraFiles: refusing to overwrite existing path '${entry.target}' in the default dist" >&2
exit 1
fi
cp -r ${entry.source} $out/${entry.target}
'') (lib.attrValues config.services.hyperhive.agent.frontend.extraFiles)
);
};
}

View file

@ -1,4 +1,4 @@
# GitHub integration (hyperhive.github.enable): a `gh` wrapper + a git
# GitHub integration (services.hyperhive.agent.github.enable): a `gh` wrapper + a git
# credential helper, both reading the PAT from the agent's
# `github-token` state file at invocation, so a dashboard-pasted token
# takes effect with no rebuild. The token PATH is baked in at build
@ -15,7 +15,7 @@
...
}:
let
userName = config.hyperhive.user.name;
userName = config.services.hyperhive.agent.user.name;
ghWrapper = pkgs.writeShellScriptBin "gh" ''
if [ -r "/agents/${userName}/state/github-token" ]; then
GH_TOKEN="$(cat "/agents/${userName}/state/github-token")"
@ -35,7 +35,7 @@ let
'';
in
{
options.hyperhive.github.enable = lib.mkOption {
options.services.hyperhive.agent.github.enable = lib.mkOption {
type = lib.types.bool;
default = true;
description = ''
@ -63,7 +63,7 @@ in
config = {
# No bare pkgs.gh here — the wrapper *is* `gh` and hardcodes the
# real binary path, so it can't be shadowed.
environment.systemPackages = lib.optionals config.hyperhive.github.enable [
environment.systemPackages = lib.optionals config.services.hyperhive.agent.github.enable [
ghWrapper
gitCredHelper
];
@ -78,7 +78,7 @@ in
# entries in the harness modules) — a whole-set `environment.etc = {…}`
# here would collide with them at the nix level ("attribute already
# defined").
environment.etc."gitconfig" = lib.mkIf config.hyperhive.github.enable {
environment.etc."gitconfig" = lib.mkIf config.services.hyperhive.agent.github.enable {
text = ''
[credential "https://github.com"]
helper = hive-github
@ -100,7 +100,7 @@ in
# extractor copies exactly one binary, so an agent that installs
# only the Forgejo poller has no github.com poller anywhere in its
# closure — not merely an unstarted unit.
systemd.services.hive-github-notify = lib.mkIf config.hyperhive.github.enable {
systemd.services.hive-github-notify = lib.mkIf config.services.hyperhive.agent.github.enable {
description = "github.com notification poller for this agent";
wantedBy = [ "multi-user.target" ];
after = [ "network.target" ];
@ -113,7 +113,7 @@ in
# poller reads the agent's `github-token` from under it.
};
serviceConfig = {
ExecStart = "${config.hyperhive.packages.hive-github-notify}/bin/hive-github-notify";
ExecStart = "${config.services.hyperhive.agent.packages.hive-github-notify}/bin/hive-github-notify";
SyslogIdentifier = "hive-github-notify";
# `on-failure`, NOT `always`, for the same reason as the forge
# poller: this unit ships on every agent, but most agents have no

View file

@ -21,8 +21,8 @@
...
}:
let
cfg = config.hyperhive.logs;
queueCfg = config.hyperhive.queue;
cfg = config.services.hyperhive.agent.logs;
queueCfg = config.services.hyperhive.agent.queue;
configured = cfg.queryUrl != null && queueCfg.tokenEndpoint != null;
# The four coordinates `swarm-logs` reads, all-or-none on its side. Bound
@ -46,7 +46,7 @@ let
# The whole point of the CLI is that its stdout pipes into `grep`.
swarmLogsConfigured = pkgs.symlinkJoin {
name = "swarm-logs-configured";
paths = [ config.hyperhive.packages.swarm-logs ];
paths = [ config.services.hyperhive.agent.packages.swarm-logs ];
nativeBuildInputs = [ pkgs.makeWrapper ];
postBuild = ''
wrapProgram $out/bin/swarm-logs ${
@ -58,7 +58,7 @@ let
};
in
{
options.hyperhive.logs = {
options.services.hyperhive.agent.logs = {
queryUrl = lib.mkOption {
type = lib.types.nullOr lib.types.str;
default = null;

View file

@ -1,5 +1,5 @@
# Per-agent matrix integration: the `hyperhive.matrix.*` +
# `hyperhive.matrixAccounts` options, the long-running
# Per-agent matrix integration: the `services.hyperhive.agent.matrix.*` +
# `services.hyperhive.agent.matrixAccounts` options, the long-running
# hive-matrix-daemon (serves its MCP tools directly over
# streamable-http), its token-arrival path trigger, and the
# auto-injected extraMcpServers entry.
@ -10,19 +10,19 @@
...
}:
let
userName = config.hyperhive.user.name;
# Rasterize the operator-set agent icon (`hyperhive.icon`, an SVG) to a
userName = config.services.hyperhive.agent.user.name;
# Rasterize the operator-set agent icon (`services.hyperhive.agent.icon`, an SVG) to a
# 512x512 PNG so the matrix daemon can upload it as each account's avatar
# over the live authenticated Client (see hive-matrix-mcp::client::sync_avatar).
# Only forced when an icon is configured — the `HIVE_ICON_PNG` daemon-env
# entry is gated on `hyperhive.icon != null`, so this binding stays lazy
# entry is gated on `services.hyperhive.agent.icon != null`, so this binding stays lazy
# when no icon is set.
iconPng = pkgs.runCommand "hive-agent-icon.png" { nativeBuildInputs = [ pkgs.librsvg ]; } ''
rsvg-convert -f png -w 512 -h 512 ${config.hyperhive.icon} -o $out
rsvg-convert -f png -w 512 -h 512 ${config.services.hyperhive.agent.icon} -o $out
'';
in
{
options.hyperhive.matrix.enable = lib.mkOption {
options.services.hyperhive.agent.matrix.enable = lib.mkOption {
type = lib.types.bool;
default = true;
description = ''
@ -31,7 +31,7 @@ in
- runs `hive-matrix-daemon` as a systemd unit that holds a
matrix-sdk Client + sync against the homeserver named by
`HIVE_MATRIX_URL` (see `hyperhive.matrix.url` there is no
`HIVE_MATRIX_URL` (see `services.hyperhive.agent.matrix.url` there is no
default, since an agent's own netns makes a loopback guess
wrong). The daemon auto-skips when that URL or
`<state>/matrix-token` is missing, and a `systemd.paths`
@ -41,7 +41,7 @@ in
send_reaction, send_reply, mark_read, list_rooms,
list_room_members, read_room) to claude via an auto-injected
`extraMcpServers.matrix` entry pointed at the daemon's own
streamable-http listener (`hyperhive.mcp.matrixHttpPort`) no
streamable-http listener (`services.hyperhive.agent.mcp.matrixHttpPort`) no
stdio bridge, no per-turn respawn, same shape as the built-in
hyperhive surface and `hive-bash-daemon`.
- wakes the agent on incoming room events via a short teaser
@ -50,13 +50,13 @@ in
unread server-side until `read_room` consumes it.
Set to `false` for agents that should NOT have matrix tools at
all (e.g. agents on a host without `hyperhive.matrix.enable` on
all (e.g. agents on a host without `services.hyperhive.agent.matrix.enable` on
the meta side). When token file is absent the daemon and MCP
both no-op cleanly anyway, so `false` is rarely necessary.
'';
};
options.hyperhive.matrix.url = lib.mkOption {
options.services.hyperhive.agent.matrix.url = lib.mkOption {
type = lib.types.nullOr lib.types.str;
default = null;
example = "https://matrix.darkest.space";
@ -82,7 +82,7 @@ in
'';
};
options.hyperhive.matrixAccounts = lib.mkOption {
options.services.hyperhive.agent.matrixAccounts = lib.mkOption {
type = lib.types.attrsOf (
lib.types.submodule {
options = {
@ -112,7 +112,7 @@ in
example = "https://matrix.example.org";
description = ''
Homeserver URL for this account. When null (the default),
the account falls back to `hyperhive.matrix.url`. Set it for
the account falls back to `services.hyperhive.agent.matrix.url`. Set it for
an account on a different homeserver than the agent's
default (e.g. an external public-matrix account).
'';
@ -140,7 +140,7 @@ in
The **hive-internal account is always present and is the primary**:
it is named `main`, synthesized by the daemon from
`hyperhive.matrix.url` + `<state>/matrix-token` +
`services.hyperhive.agent.matrix.url` + `<state>/matrix-token` +
`<state>/matrix-sdk-state`, and is the account a tool call acts as
when it omits `account`. You never declare it here --- this option
is only for the extras (e.g. an external public-matrix account).
@ -149,23 +149,23 @@ in
agent then has only `main`. When non-empty, the extras are
serialized to the daemon's `HIVE_MATRIX_ACCOUNTS` environment
variable and the daemon appends them after `main`. Requires
`hyperhive.matrix.enable` (there is no `main` to extend otherwise).
`services.hyperhive.agent.matrix.enable` (there is no `main` to extend otherwise).
'';
};
options.hyperhive.mcp.matrixHttpPort = lib.mkOption {
options.services.hyperhive.agent.mcp.matrixHttpPort = lib.mkOption {
type = lib.types.port;
default = 8792;
example = 8793;
description = ''
Loopback port `hive-matrix-daemon` serves its MCP tools
(`send_message`, `list_rooms`, `read_room`, ) on. Same shape as
`hyperhive.mcp.bashHttpPort`: HTTP is the *sole* transport (no
`services.hyperhive.agent.mcp.bashHttpPort`: HTTP is the *sole* transport (no
stdio bridge the daemon that owns the matrix-sdk `Client`
registry 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`). Safe as a
`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).
'';
@ -177,18 +177,20 @@ in
# `main` account they extend, which exists only when matrix is
# enabled.
{
assertion = config.hyperhive.matrixAccounts == { } || config.hyperhive.matrix.enable;
assertion =
config.services.hyperhive.agent.matrixAccounts == { }
|| config.services.hyperhive.agent.matrix.enable;
message =
"hyperhive.matrixAccounts requires hyperhive.matrix.enable = true "
"services.hyperhive.agent.matrixAccounts requires services.hyperhive.agent.matrix.enable = true "
+ "(the extras extend the hive-internal `main` account, which only "
+ "exists when matrix is enabled).";
}
# `main` is reserved for the synthesized hive-internal account; a
# declared extra by that name would silently collide with it.
{
assertion = !builtins.hasAttr "main" config.hyperhive.matrixAccounts;
assertion = !builtins.hasAttr "main" config.services.hyperhive.agent.matrixAccounts;
message =
"hyperhive.matrixAccounts cannot contain a key named \"main\" "
"services.hyperhive.agent.matrixAccounts cannot contain a key named \"main\" "
+ "--- that name is reserved for the hive-internal account.";
}
# Token files must land at the `matrix-token*` name the daemon
@ -199,17 +201,17 @@ in
# `matrix-catgirl-token`) is caught at build time, not silently.
{
assertion = lib.all (a: lib.hasPrefix "matrix-token" (baseNameOf a.tokenFile)) (
lib.attrValues config.hyperhive.matrixAccounts
lib.attrValues config.services.hyperhive.agent.matrixAccounts
);
message =
"every hyperhive.matrixAccounts.<name>.tokenFile basename must start with "
"every services.hyperhive.agent.matrixAccounts.<name>.tokenFile basename must start with "
+ "\"matrix-token\" so the daemon path-watcher glob "
+ "(matrix-token* in the agent's state dir) picks it up live. Offending: "
+ lib.concatStringsSep ", " (
lib.mapAttrsToList (n: a: "${n}=${baseNameOf a.tokenFile}") (
lib.filterAttrs (
_n: a: !lib.hasPrefix "matrix-token" (baseNameOf a.tokenFile)
) config.hyperhive.matrixAccounts
) config.services.hyperhive.agent.matrixAccounts
)
)
+ ".";
@ -220,10 +222,10 @@ in
# ./mcp.nix. `lib.mkDefault` so the operator's own agent.nix can
# override it. Points at the daemon's own persistent
# streamable-http listener — no stdio bridge, no per-turn spawn.
hyperhive.extraMcpServers = lib.mkIf config.hyperhive.matrix.enable {
services.hyperhive.agent.extraMcpServers = lib.mkIf config.services.hyperhive.agent.matrix.enable {
matrix = lib.mkDefault {
type = "http";
url = "http://127.0.0.1:${toString config.hyperhive.mcp.matrixHttpPort}/mcp";
url = "http://127.0.0.1:${toString config.services.hyperhive.agent.mcp.matrixHttpPort}/mcp";
allowedTools = [ "*" ];
};
};
@ -233,7 +235,7 @@ in
# signals on incoming room events via `/run/hive/mcp.sock`. See
# `docs/agent-lifecycle/persistence.md::Matrix per-agent daemon + token-arrival
# trigger` for the first-boot-ordering rationale.
systemd.services.hive-matrix-daemon = lib.mkIf config.hyperhive.matrix.enable {
systemd.services.hive-matrix-daemon = lib.mkIf config.services.hyperhive.agent.matrix.enable {
description = "long-running matrix-sdk Client + MCP daemon";
wantedBy = [ "multi-user.target" ];
before = [ "hive-agent.service" ];
@ -255,8 +257,8 @@ in
# token and no-ops. Nothing here falls back to loopback: that would
# be a value that evaluates fine and then addresses the agent's own
# netns instead of the homeserver.
// lib.optionalAttrs (config.hyperhive.matrix.url != null) {
HIVE_MATRIX_URL = config.hyperhive.matrix.url;
// lib.optionalAttrs (config.services.hyperhive.agent.matrix.url != null) {
HIVE_MATRIX_URL = config.services.hyperhive.agent.matrix.url;
}
# Multi-account: serialize the *extra* accounts to the JSON the
# daemon parses (`accounts::configured`). Only set when extras are
@ -265,7 +267,7 @@ in
# prepends it, so we emit extras only. Each entry is in the
# daemon's `AccountCfg` serde shape: name (the attr key) /
# token_file / state_dir / optional homeserver.
// lib.optionalAttrs (config.hyperhive.matrixAccounts != { }) {
// lib.optionalAttrs (config.services.hyperhive.agent.matrixAccounts != { }) {
HIVE_MATRIX_ACCOUNTS = builtins.toJSON (
lib.mapAttrsToList (
name: a:
@ -275,17 +277,17 @@ in
state_dir = a.sessionDir;
}
// lib.optionalAttrs (a.homeserver != null) { inherit (a) homeserver; }
) config.hyperhive.matrixAccounts
) config.services.hyperhive.agent.matrixAccounts
);
}
# Rasterized agent icon path for the daemon's avatar sync. Only set
# when an icon is configured; absent → the daemon skips avatar setting
# (hive-matrix-mcp::client::sync_avatar returns early on unset env).
// lib.optionalAttrs (config.hyperhive.icon != null) {
// lib.optionalAttrs (config.services.hyperhive.agent.icon != null) {
HIVE_ICON_PNG = "${iconPng}";
};
serviceConfig = {
ExecStart = "${config.hyperhive.packages.hive-matrix-daemon}/bin/hive-matrix-daemon --http 127.0.0.1:${toString config.hyperhive.mcp.matrixHttpPort}";
ExecStart = "${config.services.hyperhive.agent.packages.hive-matrix-daemon}/bin/hive-matrix-daemon --http 127.0.0.1:${toString config.services.hyperhive.agent.mcp.matrixHttpPort}";
SyslogIdentifier = "hive-matrix-daemon";
# `on-failure`, not `always`: the daemon deliberately exits 0
# (a clean, non-failure exit) when no token is provisioned yet
@ -308,7 +310,7 @@ in
# the daemon would exit 0 silently on first boot and the MCP
# would have no backend until next restart. See
# `docs/agent-lifecycle/persistence.md` (same section as above).
systemd.paths.hive-matrix-daemon = lib.mkIf config.hyperhive.matrix.enable {
systemd.paths.hive-matrix-daemon = lib.mkIf config.services.hyperhive.agent.matrix.enable {
description = "trigger hive-matrix-daemon when a matrix token appears";
wantedBy = [ "multi-user.target" ];
# `matrix-token*` (not just `matrix-token`) so a secondary

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

View file

@ -23,8 +23,8 @@
...
}:
let
cfg = config.hyperhive.otel;
userName = config.hyperhive.user.name;
cfg = config.services.hyperhive.agent.otel;
userName = config.services.hyperhive.agent.user.name;
# Hive/swarm display names, read from the per-agent options meta.rs
# renders (NOT from `environment.variables` — those carry the same
# names at *runtime* only, so reading them here silently yielded
@ -32,9 +32,15 @@ let
# answer). `null` means the hive did not name itself; "unknown" is then
# an honest label rather than a guess.
hiveDisplayName =
if config.hyperhive.hiveName == null then "unknown" else config.hyperhive.hiveName;
if config.services.hyperhive.agent.hiveName == null then
"unknown"
else
config.services.hyperhive.agent.hiveName;
swarmDisplayName =
if config.hyperhive.swarmName == null then "unknown" else config.hyperhive.swarmName;
if config.services.hyperhive.agent.swarmName == null then
"unknown"
else
config.services.hyperhive.agent.swarmName;
# Resource labels every producer in this container stamps on what it
# emits. `service.name` names the container's role, not one binary
@ -81,7 +87,7 @@ in
# the build-time implementation surface that injection writes into;
# they are not meant to be set directly in an agent.nix. Marked
# `internal` so the host option is the only documented operator knob.
options.hyperhive.otel = {
options.services.hyperhive.agent.otel = {
enable = lib.mkOption {
type = lib.types.bool;
default = false;
@ -197,7 +203,7 @@ in
"none"
]);
message = ''
hyperhive.otel.enable is on for agent ${userName}, but
services.hyperhive.agent.otel.enable is on for agent ${userName}, but
services.journald.storage is
"${config.services.journald.storage}" in this container.

View file

@ -5,7 +5,7 @@
# individual key still wins.
{ lib, ... }:
{
options.hyperhive.packages = lib.mkOption {
options.services.hyperhive.agent.packages = lib.mkOption {
type = lib.types.attrsOf lib.types.package;
internal = true;
description = ''

View file

@ -16,7 +16,7 @@
...
}:
let
cfg = config.hyperhive.queue;
cfg = config.services.hyperhive.agent.queue;
configured = cfg.natsUrl != null && cfg.tokenEndpoint != null;
# The two ids `hive_c0re::lifecycle::host_config` forwards under. Neither
@ -32,7 +32,7 @@ let
credentialsDir = "/run/credentials/hive-agent.service";
in
{
options.hyperhive.queue = {
options.services.hyperhive.agent.queue = {
natsUrl = lib.mkOption {
type = lib.types.nullOr lib.types.str;
default = null;
@ -61,7 +61,7 @@ in
`client_credentials` token there and presents it to the queue, which
authenticates it as the client named in the delivered credential.
Set together with {option}`hyperhive.queue.natsUrl` or not at all
Set together with {option}`services.hyperhive.agent.queue.natsUrl` or not at all
the harness treats a half-set pair as a deployment bug rather than as
"no queue coordinates".
'';
@ -79,7 +79,7 @@ in
description = ''
Path the agent's OIDC client id is delivered at, for a consumer
outside the harness unit. Read-only: it is a fact about where the
credential lands, not a knob see {option}`hyperhive.logs.queryUrl`
credential lands, not a knob see {option}`services.hyperhive.agent.logs.queryUrl`
for the consumer this exists for.
'';
};
@ -90,7 +90,7 @@ in
default = "${credentialsDir}/${secretCredential}";
description = ''
Path the agent's OIDC client secret is delivered at. Read-only for
the same reason as {option}`hyperhive.queue.clientIdFile`.
the same reason as {option}`services.hyperhive.agent.queue.clientIdFile`.
🩸 A PATH and never a value. The file is `0400` to the agent user and
is read at the moment of a token request; nothing in this tree puts

View file

@ -0,0 +1,213 @@
# Deprecation shims for the agent tier's old option namespace.
#
# Every per-agent option used to live at the top level, `hyperhive.<name>`,
# while the host tier has always been under `services.hyperhive.*`. The agent
# tier moved to `services.hyperhive.agent.<name>`; these entries keep an
# existing `agent.nix` evaluating (with a deprecation warning) against the old
# spelling.
#
# ⚠️ One entry per LEAF, not per namespace. A single entry for a parent path
# only covers its children when the parent is a submodule-typed option; every
# nested option here (`user`, `mcp`, `otel`, …) is a plain attrset of
# declarations instead, so each leaf needs its own line — the same reason
# `../host-modules/deploy.nix` renames the CI runner's knobs one by one.
{ lib, ... }:
{
imports = [
(lib.mkRenamedOptionModule
[ "hyperhive" "_bashEnvFragments" ]
[ "services" "hyperhive" "agent" "_bashEnvFragments" ]
)
(lib.mkRenamedOptionModule
[ "hyperhive" "allowedRecipients" ]
[ "services" "hyperhive" "agent" "allowedRecipients" ]
)
(lib.mkRenamedOptionModule
[ "hyperhive" "autoCompact" ]
[ "services" "hyperhive" "agent" "autoCompact" ]
)
(lib.mkRenamedOptionModule
[ "hyperhive" "availableModels" ]
[ "services" "hyperhive" "agent" "availableModels" ]
)
(lib.mkRenamedOptionModule
[ "hyperhive" "backendEnvironmentFile" ]
[ "services" "hyperhive" "agent" "backendEnvironmentFile" ]
)
(lib.mkRenamedOptionModule
[ "hyperhive" "cargo" "shortMessages" ]
[ "services" "hyperhive" "agent" "cargo" "shortMessages" ]
)
(lib.mkRenamedOptionModule
[ "hyperhive" "claudeCodePath" ]
[ "services" "hyperhive" "agent" "claudeCodePath" ]
)
(lib.mkRenamedOptionModule
[ "hyperhive" "claudeMarketplaces" ]
[ "services" "hyperhive" "agent" "claudeMarketplaces" ]
)
(lib.mkRenamedOptionModule
[ "hyperhive" "claudeMemoryMaxBytes" ]
[ "services" "hyperhive" "agent" "claudeMemoryMaxBytes" ]
)
(lib.mkRenamedOptionModule
[ "hyperhive" "claudePlugins" ]
[ "services" "hyperhive" "agent" "claudePlugins" ]
)
(lib.mkRenamedOptionModule
[ "hyperhive" "claudePluginsAutoUpdate" ]
[ "services" "hyperhive" "agent" "claudePluginsAutoUpdate" ]
)
(lib.mkRenamedOptionModule
[ "hyperhive" "dashboardLinks" ]
[ "services" "hyperhive" "agent" "dashboardLinks" ]
)
(lib.mkRenamedOptionModule
[ "hyperhive" "docs" "enable" ]
[ "services" "hyperhive" "agent" "docs" "enable" ]
)
(lib.mkRenamedOptionModule
[ "hyperhive" "docs" "source" ]
[ "services" "hyperhive" "agent" "docs" "source" ]
)
(lib.mkRenamedOptionModule
[ "hyperhive" "effortLevel" ]
[ "services" "hyperhive" "agent" "effortLevel" ]
)
(lib.mkRenamedOptionModule
[ "hyperhive" "extraMcpServers" ]
[ "services" "hyperhive" "agent" "extraMcpServers" ]
)
(lib.mkRenamedOptionModule
[ "hyperhive" "extraWebProxies" ]
[ "services" "hyperhive" "agent" "extraWebProxies" ]
)
(lib.mkRenamedOptionModule
[ "hyperhive" "forge" "url" ]
[ "services" "hyperhive" "agent" "forge" "url" ]
)
(lib.mkRenamedOptionModule
[ "hyperhive" "frontend" "dist" ]
[ "services" "hyperhive" "agent" "frontend" "dist" ]
)
(lib.mkRenamedOptionModule
[ "hyperhive" "frontend" "extraFiles" ]
[ "services" "hyperhive" "agent" "frontend" "extraFiles" ]
)
# `frontend.mergedDist` moved with the rest and deliberately has NO entry:
# it is `readOnly` and ./frontend.nix already defines it, and a rename
# module contributes a *definition* to its target — so a shim for it makes
# every evaluation fail with "set multiple times". Nothing could have set
# it anyway; a reader of the old path now gets "option does not exist",
# which says the same thing sooner. Same reasoning as the `hostUsersFile`
# exclusion in ../host-modules/deploy.nix.
(lib.mkRenamedOptionModule
[ "hyperhive" "github" "enable" ]
[ "services" "hyperhive" "agent" "github" "enable" ]
)
(lib.mkRenamedOptionModule
[ "hyperhive" "gui" "enable" ]
[ "services" "hyperhive" "agent" "gui" "enable" ]
)
(lib.mkRenamedOptionModule
[ "hyperhive" "gui" "vncPort" ]
[ "services" "hyperhive" "agent" "gui" "vncPort" ]
)
(lib.mkRenamedOptionModule [ "hyperhive" "hiveName" ] [ "services" "hyperhive" "agent" "hiveName" ])
(lib.mkRenamedOptionModule [ "hyperhive" "icon" ] [ "services" "hyperhive" "agent" "icon" ])
(lib.mkRenamedOptionModule
[ "hyperhive" "logs" "queryUrl" ]
[ "services" "hyperhive" "agent" "logs" "queryUrl" ]
)
(lib.mkRenamedOptionModule
[ "hyperhive" "matrix" "enable" ]
[ "services" "hyperhive" "agent" "matrix" "enable" ]
)
(lib.mkRenamedOptionModule
[ "hyperhive" "matrix" "url" ]
[ "services" "hyperhive" "agent" "matrix" "url" ]
)
(lib.mkRenamedOptionModule
[ "hyperhive" "matrixAccounts" ]
[ "services" "hyperhive" "agent" "matrixAccounts" ]
)
(lib.mkRenamedOptionModule
[ "hyperhive" "mcp" "bashHttpPort" ]
[ "services" "hyperhive" "agent" "mcp" "bashHttpPort" ]
)
(lib.mkRenamedOptionModule
[ "hyperhive" "mcp" "httpPort" ]
[ "services" "hyperhive" "agent" "mcp" "httpPort" ]
)
(lib.mkRenamedOptionModule
[ "hyperhive" "mcp" "matrixHttpPort" ]
[ "services" "hyperhive" "agent" "mcp" "matrixHttpPort" ]
)
(lib.mkRenamedOptionModule
[ "hyperhive" "mcp" "subagentHttpPort" ]
[ "services" "hyperhive" "agent" "mcp" "subagentHttpPort" ]
)
(lib.mkRenamedOptionModule [ "hyperhive" "model" ] [ "services" "hyperhive" "agent" "model" ])
(lib.mkRenamedOptionModule
[ "hyperhive" "otel" "debug" ]
[ "services" "hyperhive" "agent" "otel" "debug" ]
)
(lib.mkRenamedOptionModule
[ "hyperhive" "otel" "enable" ]
[ "services" "hyperhive" "agent" "otel" "enable" ]
)
(lib.mkRenamedOptionModule
[ "hyperhive" "otel" "endpoint" ]
[ "services" "hyperhive" "agent" "otel" "endpoint" ]
)
(lib.mkRenamedOptionModule
[ "hyperhive" "otel" "extraResourceAttributes" ]
[ "services" "hyperhive" "agent" "otel" "extraResourceAttributes" ]
)
(lib.mkRenamedOptionModule
[ "hyperhive" "otel" "metricIntervalMs" ]
[ "services" "hyperhive" "agent" "otel" "metricIntervalMs" ]
)
(lib.mkRenamedOptionModule
[ "hyperhive" "otel" "protocol" ]
[ "services" "hyperhive" "agent" "otel" "protocol" ]
)
(lib.mkRenamedOptionModule [ "hyperhive" "packages" ] [ "services" "hyperhive" "agent" "packages" ])
(lib.mkRenamedOptionModule
[ "hyperhive" "queue" "natsUrl" ]
[ "services" "hyperhive" "agent" "queue" "natsUrl" ]
)
(lib.mkRenamedOptionModule
[ "hyperhive" "queue" "tokenEndpoint" ]
[ "services" "hyperhive" "agent" "queue" "tokenEndpoint" ]
)
# `queue.clientIdFile` / `queue.clientSecretFile` are excluded for the
# `mergedDist` reason above: both are `readOnly`, so a shim's definition
# collides with the option's own and breaks every evaluation. They are
# derived paths no agent config could have set.
(lib.mkRenamedOptionModule
[ "hyperhive" "swarmName" ]
[ "services" "hyperhive" "agent" "swarmName" ]
)
(lib.mkRenamedOptionModule
[ "hyperhive" "useApiKey" ]
[ "services" "hyperhive" "agent" "useApiKey" ]
)
(lib.mkRenamedOptionModule
[ "hyperhive" "user" "gid" ]
[ "services" "hyperhive" "agent" "user" "gid" ]
)
(lib.mkRenamedOptionModule
[ "hyperhive" "user" "name" ]
[ "services" "hyperhive" "agent" "user" "name" ]
)
(lib.mkRenamedOptionModule
[ "hyperhive" "user" "passwordlessSudo" ]
[ "services" "hyperhive" "agent" "user" "passwordlessSudo" ]
)
(lib.mkRenamedOptionModule
[ "hyperhive" "user" "uid" ]
[ "services" "hyperhive" "agent" "user" "uid" ]
)
];
}

View file

@ -1,6 +1,6 @@
# Screen MCP — screenshot, keyboard, and mouse for GUI agents.
#
# Auto-activated when `hyperhive.gui.enable = true`. Wires the
# Auto-activated when `services.hyperhive.agent.gui.enable = true`. Wires the
# `hive-screen-mcp` stdio bridge as `extraMcpServers.screen` so claude
# gets five tools: `screenshot`, `type_text`, `key_press`,
# `mouse_move`, and `mouse_click`.
@ -17,10 +17,10 @@
...
}:
{
config = lib.mkIf config.hyperhive.gui.enable {
config = lib.mkIf config.services.hyperhive.agent.gui.enable {
# Register the screen MCP bridge so claude gets the screen tools.
hyperhive.extraMcpServers.screen = {
command = "${config.hyperhive.packages.hive-screen-mcp}/bin/hive-screen-mcp";
services.hyperhive.agent.extraMcpServers.screen = {
command = "${config.services.hyperhive.agent.packages.hive-screen-mcp}/bin/hive-screen-mcp";
args = [ ];
};

View file

@ -1,4 +1,4 @@
# Per-agent unix user: the `hyperhive.user.*` options, the user/group
# Per-agent unix user: the `services.hyperhive.agent.user.*` options, the user/group
# declarations, passwordless sudo, and the first-boot migration that
# chowns the bind-mounted state dirs to the agent user.
{
@ -8,7 +8,7 @@
...
}:
let
userName = config.hyperhive.user.name;
userName = config.services.hyperhive.agent.user.name;
homeDir = "/home/${userName}";
in
{
@ -20,7 +20,7 @@ in
# uniquely-named user matching its agent label. UID auto-assigned
# by NixOS (the auto-allocation range for normal users); no hard-
# coded UID.
options.hyperhive.user.name = lib.mkOption {
options.services.hyperhive.agent.user.name = lib.mkOption {
type = lib.types.strMatching "^[a-z_][a-z0-9_-]{0,30}$";
default = "agent";
example = "iris";
@ -33,11 +33,11 @@ in
Constraints match `useradd`'s NAME_REGEX: lowercase / `_` start,
total length 31, no special characters. UID is auto-assigned
by NixOS unless `hyperhive.user.uid` is explicitly set.
by NixOS unless `services.hyperhive.agent.user.uid` is explicitly set.
'';
};
options.hyperhive.user.uid = lib.mkOption {
options.services.hyperhive.agent.user.uid = lib.mkOption {
type = lib.types.nullOr lib.types.int;
default = null;
example = 1100;
@ -59,24 +59,24 @@ in
'';
};
options.hyperhive.user.gid = lib.mkOption {
options.services.hyperhive.agent.user.gid = lib.mkOption {
type = lib.types.nullOr lib.types.int;
default = null;
example = 1100;
description = ''
Optional fixed GID for the per-agent unix group. `null` (default)
lets NixOS auto-assign. Usually set alongside `hyperhive.user.uid`
lets NixOS auto-assign. Usually set alongside `services.hyperhive.agent.user.uid`
to the same value (the conventional Unix pattern for per-user
groups where uid == gid), but can be set independently.
'';
};
options.hyperhive.user.passwordlessSudo = lib.mkOption {
options.services.hyperhive.agent.user.passwordlessSudo = lib.mkOption {
type = lib.types.bool;
default = true;
example = false;
description = ''
Grant `${config.hyperhive.user.name}` passwordless sudo
Grant `${config.services.hyperhive.agent.user.name}` passwordless sudo
(`NOPASSWD: ALL`). True by default so claude's `Bash` tool
keeps working for tools that expect root inside the container
(`systemctl`, package managers in dev shells, etc.) the
@ -94,10 +94,12 @@ in
assertions = [
{
assertion =
config.hyperhive.user.uid == null
|| (config.hyperhive.user.uid >= 1000 && config.hyperhive.user.uid < 60000);
config.services.hyperhive.agent.user.uid == null
|| (
config.services.hyperhive.agent.user.uid >= 1000 && config.services.hyperhive.agent.user.uid < 60000
);
message = ''
hyperhive.user.uid must be in [1000, 60000) values below
services.hyperhive.agent.user.uid must be in [1000, 60000) values below
1000 clash with system accounts; values 60000 are reserved
by NixOS for dynamic allocation. Leave unset (null) to let
NixOS auto-assign.
@ -105,11 +107,13 @@ in
}
{
assertion =
config.hyperhive.user.gid == null
|| (config.hyperhive.user.gid >= 1000 && config.hyperhive.user.gid < 60000);
config.services.hyperhive.agent.user.gid == null
|| (
config.services.hyperhive.agent.user.gid >= 1000 && config.services.hyperhive.agent.user.gid < 60000
);
message = ''
hyperhive.user.gid must be in [1000, 60000) same range
constraint as hyperhive.user.uid.
services.hyperhive.agent.user.gid must be in [1000, 60000) same range
constraint as services.hyperhive.agent.user.uid.
'';
}
];
@ -123,29 +127,29 @@ in
home = homeDir;
createHome = true;
group = userName;
extraGroups = lib.optional config.hyperhive.user.passwordlessSudo "wheel";
extraGroups = lib.optional config.services.hyperhive.agent.user.passwordlessSudo "wheel";
# Matches /bin/bash on NixOS — the harness's claude shell-outs
# expect a POSIX shell at $SHELL; bashInteractive is already
# the system default for the root user too.
shell = pkgs.bashInteractive;
}
// lib.optionalAttrs (config.hyperhive.user.uid != null) {
uid = config.hyperhive.user.uid;
// lib.optionalAttrs (config.services.hyperhive.agent.user.uid != null) {
uid = config.services.hyperhive.agent.user.uid;
};
users.groups.${userName} =
{ }
// lib.optionalAttrs (config.hyperhive.user.gid != null) {
gid = config.hyperhive.user.gid;
// lib.optionalAttrs (config.services.hyperhive.agent.user.gid != null) {
gid = config.services.hyperhive.agent.user.gid;
};
# `NOPASSWD: ALL` for the agent user. Lets claude's Bash tool
# keep working with anything that expected root (systemctl,
# nix-env, etc.) without prompting. Flip
# `hyperhive.user.passwordlessSudo = false` to drop both
# `services.hyperhive.agent.user.passwordlessSudo = false` to drop both
# the wheel-group membership and this sudoers entry; anything
# that needs root then fails loudly instead of silently
# succeeding.
security.sudo.extraRules = lib.mkIf config.hyperhive.user.passwordlessSudo [
security.sudo.extraRules = lib.mkIf config.services.hyperhive.agent.user.passwordlessSudo [
{
users = [ userName ];
commands = [

View file

@ -8,8 +8,8 @@ let
# GUI processes run as the agent's own non-root user — the same user
# hive-agent runs as (declared + home-chowned by ./user.nix) — so
# weston, the wayland client, and the agent share one user session.
# `hyperhive.user.name` is set per-agent by the meta-flake renderer.
userName = config.hyperhive.user.name;
# `services.hyperhive.agent.user.name` is set per-agent by the meta-flake renderer.
userName = config.services.hyperhive.agent.user.name;
# Static weston config. `[core] idle-time=0` disables the 300s idle /
# lock screen; `[vnc] auth-method=none` + the `--disable-transport-
@ -31,7 +31,7 @@ let
in
{
# Optional Weston (Wayland compositor) with the VNC backend,
# surfaced as a per-agent `hyperhive.gui.enable` option. Imported
# surfaced as a per-agent `services.hyperhive.agent.gui.enable` option. Imported
# from ./default.nix so every sub-agent + the manager sees the
# option; only those that flip it on get the service.
#
@ -41,7 +41,7 @@ in
# Harness-side WebSocket relay shape: docs/web-ui/agent.md::Per-agent
# endpoints (`/screen` + `/screen/ws`).
options.hyperhive.gui.enable = lib.mkOption {
options.services.hyperhive.agent.gui.enable = lib.mkOption {
type = lib.types.bool;
default = false;
description = ''
@ -50,7 +50,7 @@ in
relay. Renders in software (pixman) no GPU, DRM, or VT
access, so no extra container capabilities are needed.
Weston binds a fixed VNC port (`hyperhive.gui.vncPort`) on the
Weston binds a fixed VNC port (`services.hyperhive.agent.gui.vncPort`) on the
container's own loopback. Network isolation is unconditional
(each agent has its own netns), so a fixed port can't collide
across containers no per-agent hashing needed. The harness
@ -60,7 +60,7 @@ in
loop instead of blocking `nixos-container update`.
Weston, the wayland client and the agent harness run as the
agent's own non-root user (`hyperhive.user.name`), sharing one
agent's own non-root user (`services.hyperhive.agent.user.name`), sharing one
session: a fixed `XDG_RUNTIME_DIR=/run/gui`, one wayland display,
and one D-Bus session bus at `/run/gui/bus` (gui-dbus.service),
so GUI clients need no private `dbus-run-session`.
@ -73,7 +73,7 @@ in
# is container-local and can't collide. Internal: the harness reads
# the value via the `HIVE_GUI_VNC_PORT` env var the harness service
# injects from this option, not directly.
options.hyperhive.gui.vncPort = lib.mkOption {
options.services.hyperhive.agent.gui.vncPort = lib.mkOption {
type = lib.types.port;
default = 5900;
internal = true;
@ -84,14 +84,14 @@ in
'';
};
config = lib.mkIf config.hyperhive.gui.enable {
config = lib.mkIf config.services.hyperhive.agent.gui.enable {
# The GUI must run non-root: weston + the wayland client share the
# agent's own user session. `user.name` is the agent name for every
# spawned agent; only a misconfigured root-named agent would trip this.
assertions = [
{
assertion = userName != "root";
message = "hyperhive.gui.enable requires a non-root hyperhive.user.name (the GUI runs as that user).";
message = "services.hyperhive.agent.gui.enable requires a non-root services.hyperhive.agent.user.name (the GUI runs as that user).";
}
];
@ -177,7 +177,7 @@ in
--config=${westonIni} \
--backend=vnc-backend.so \
--renderer=pixman \
--port=${toString config.hyperhive.gui.vncPort} \
--port=${toString config.services.hyperhive.agent.gui.vncPort} \
--socket=wayland-0 \
--disable-transport-layer-security
'';

View file

@ -10,5 +10,5 @@
# `$HIVE_DOCS_DIR/`, added via `claude --add-dir`). `mkDefault` so a
# manager's own `agent.nix` can still turn it off. Other agents default
# off.
hyperhive.docs.enable = lib.mkDefault true;
services.hyperhive.agent.docs.enable = lib.mkDefault true;
}