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 = agentPackages =
{ lib, pkgs, ... }: { lib, pkgs, ... }:
{ {
hyperhive.packages = lib.mapAttrs (_: lib.mkDefault) { services.hyperhive.agent.packages = lib.mapAttrs (_: lib.mkDefault) {
inherit (self.packages.${pkgs.stdenv.hostPlatform.system}) inherit (self.packages.${pkgs.stdenv.hostPlatform.system})
hive-agent hive-agent
hive-agent-mcp hive-agent-mcp

View file

@ -9,11 +9,11 @@
... ...
}: }:
let let
userName = config.hyperhive.user.name; userName = config.services.hyperhive.agent.user.name;
homeDir = "/home/${userName}"; homeDir = "/home/${userName}";
in in
{ {
options.hyperhive.model = lib.mkOption { options.services.hyperhive.agent.model = lib.mkOption {
type = lib.types.str; type = lib.types.str;
default = "haiku"; default = "haiku";
example = "sonnet"; 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; type = lib.types.listOf lib.types.str;
default = [ default = [
"haiku" "haiku"
@ -54,7 +54,7 @@ in
Configure hive-wide by setting a shared default (e.g. in your 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 `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 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. UI; this option only controls which choices the picker presents.
Values are the short model names that `claude --model` accepts: 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 [ type = lib.types.enum [
"low" "low"
"medium" "medium"
@ -87,7 +87,7 @@ in
''; '';
}; };
options.hyperhive.autoCompact = lib.mkOption { options.services.hyperhive.agent.autoCompact = lib.mkOption {
type = lib.types.bool; type = lib.types.bool;
default = true; default = true;
description = '' description = ''
@ -106,7 +106,7 @@ in
''; '';
}; };
options.hyperhive.useApiKey = lib.mkOption { options.services.hyperhive.agent.useApiKey = lib.mkOption {
type = lib.types.bool; type = lib.types.bool;
default = false; default = false;
description = '' description = ''
@ -121,14 +121,14 @@ in
`~/.claude/` as "needs login" for this agent (see `~/.claude/` as "needs login" for this agent (see
`hive_c0re::container_view`'s `needs_login` computation). `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, option changes what the harness believes about its own login state,
the other actually supplies the credentials `claude` reads. Neither the other actually supplies the credentials `claude` reads. Neither
is useful alone. is useful alone.
''; '';
}; };
options.hyperhive.backendEnvironmentFile = lib.mkOption { options.services.hyperhive.agent.backendEnvironmentFile = lib.mkOption {
type = lib.types.nullOr lib.types.str; type = lib.types.nullOr lib.types.str;
default = null; default = null;
example = "/agents/myagent/harness/openrouter.env"; example = "/agents/myagent/harness/openrouter.env";
@ -160,12 +160,12 @@ in
so a path that doesn't exist yet an operator setting this option 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 before creating the file, or a fresh host rebuild before state is
restored makes systemd skip it rather than refuse to start the 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. paired with.
''; '';
}; };
options.hyperhive.extraWebProxies = lib.mkOption { options.services.hyperhive.agent.extraWebProxies = lib.mkOption {
type = lib.types.attrsOf lib.types.str; type = lib.types.attrsOf lib.types.str;
default = { }; default = { };
example = lib.literalExpression ''{ "stats" = "http://127.0.0.1:3737"; }''; example = lib.literalExpression ''{ "stats" = "http://127.0.0.1:3737"; }'';
@ -192,21 +192,21 @@ in
config = { config = {
assertions = [ 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. # the harness to pass an invalid model flag to claude.
{ {
assertion = config.hyperhive.model != ""; assertion = config.services.hyperhive.agent.model != "";
message = "hyperhive.model must not be empty (set it to e.g. \"haiku\" or \"sonnet\")"; 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 # 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. # UI would offer no way back to the model the agent is actually running.
{ {
assertion = assertion =
config.hyperhive.availableModels == [ ] config.services.hyperhive.agent.availableModels == [ ]
|| builtins.elem config.hyperhive.model config.hyperhive.availableModels; || builtins.elem config.services.hyperhive.agent.model config.services.hyperhive.agent.availableModels;
message = message =
"hyperhive.model (\"${config.hyperhive.model}\") must be one of " "services.hyperhive.agent.model (\"${config.services.hyperhive.agent.model}\") must be one of "
+ "hyperhive.availableModels ([ ${lib.concatStringsSep " " config.hyperhive.availableModels} ]) " + "services.hyperhive.agent.availableModels ([ ${lib.concatStringsSep " " config.services.hyperhive.agent.availableModels} ]) "
+ " add it to the list or change the model."; + " add it to the list or change the model.";
} }
]; ];
@ -214,17 +214,17 @@ in
# HIVE_DEFAULT_MODEL seeds the initial model selection when no # HIVE_DEFAULT_MODEL seeds the initial model selection when no
# persisted model choice exists in the state dir. # persisted model choice exists in the state dir.
environment.variables = { 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 # 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. # frontend; an empty value falls back to the built-in default list.
HIVE_AVAILABLE_MODELS = lib.concatStringsSep "," config.hyperhive.availableModels; HIVE_AVAILABLE_MODELS = lib.concatStringsSep "," config.services.hyperhive.agent.availableModels;
# Per-agent baseline effort (see hyperhive.effortLevel). The # Per-agent baseline effort (see services.hyperhive.agent.effortLevel). The
# harness resolves operator-override-file → this env → "medium" # harness resolves operator-override-file → this env → "medium"
# and passes it to claude --effort at turn launch. # 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 # Zero watermark disables proactive compaction; the reactive path
# (compact-on-overflow) still fires when the session is truly full. # (compact-on-overflow) still fires when the session is truly full.
HIVE_COMPACT_WATERMARK_TOKENS = "0"; HIVE_COMPACT_WATERMARK_TOKENS = "0";
@ -253,8 +253,8 @@ in
environment = { environment = {
SHELL = "${pkgs.bashInteractive}/bin/bash"; SHELL = "${pkgs.bashInteractive}/bin/bash";
HOME = homeDir; HOME = homeDir;
HIVE_STATIC_DIR = "${config.hyperhive.frontend.mergedDist}"; HIVE_STATIC_DIR = "${config.services.hyperhive.agent.frontend.mergedDist}";
HIVE_ASSETS_DIR = "${config.hyperhive.packages.assets}/share/hyperhive"; HIVE_ASSETS_DIR = "${config.services.hyperhive.agent.packages.assets}/share/hyperhive";
# Unix-socket path for the harness web UI. All agents always bind # Unix-socket path for the harness web UI. All agents always bind
# here; there is no TCP fallback. Path matches # here; there is no TCP fallback. Path matches
# `hive_c0re::agent_sockets::socket_path_for(name)` so lifecycle # `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 # (see ./mcp.nix) via the same option. Always set — network
# isolation is unconditional, so a fixed per-container port is # isolation is unconditional, so a fixed per-container port is
# collision-free. # 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 # Tells the harness which fixed VNC port weston bound, and (by
# its presence) that gui is enabled — the harness `/screen/ws` # its presence) that gui is enabled — the harness `/screen/ws`
# relay reads this instead of a runtime marker file. The port is # relay reads this instead of a runtime marker file. The port is
# container-local + fixed (network isolation is unconditional), # container-local + fixed (network isolation is unconditional),
# so the same value for every gui agent is fine. See # so the same value for every gui agent is fine. See
# ./weston-vnc.nix::hyperhive.gui.vncPort. # ./weston-vnc.nix::services.hyperhive.agent.gui.vncPort.
HIVE_GUI_VNC_PORT = toString config.hyperhive.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 # 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`. # 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 # 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"; HIVE_USE_API_KEY = "1";
}; };
serviceConfig = { 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 # Pin the journal identity to the binary name (otherwise systemd
# derives SyslogIdentifier from the ExecStart basename). # derives SyslogIdentifier from the ExecStart basename).
SyslogIdentifier = binary; SyslogIdentifier = binary;
@ -321,10 +321,10 @@ in
User = userName; User = userName;
Group = userName; Group = userName;
} }
// lib.optionalAttrs (config.hyperhive.backendEnvironmentFile != null) { // lib.optionalAttrs (config.services.hyperhive.agent.backendEnvironmentFile != null) {
# See `hyperhive.backendEnvironmentFile`'s own description for # See `services.hyperhive.agent.backendEnvironmentFile`'s own description for
# the file shape and the leading-`-` rationale. # 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 # all contributions across modules into one file. Generic by
# design so future hooks don't need to rename this file or # design so future hooks don't need to rename this file or
# invent a parallel dispatcher. # invent a parallel dispatcher.
options.hyperhive._bashEnvFragments = lib.mkOption { options.services.hyperhive.agent._bashEnvFragments = lib.mkOption {
type = lib.types.lines; type = lib.types.lines;
default = ""; default = "";
internal = true; internal = true;
@ -27,11 +27,11 @@
unset, and the interactive bashrc hook is omitted zero cost unset, and the interactive bashrc hook is omitted zero cost
when no feature is on. Internal set indirectly via the when no feature is on. Internal set indirectly via the
per-feature options that own the gate (e.g. 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; type = lib.types.bool;
default = true; default = true;
example = false; example = false;
@ -45,7 +45,7 @@
carry no signal beyond the warning/error summary. carry no signal beyond the warning/error summary.
Implementation: contributes a `cargo` shell function to 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` Loaded via `BASH_ENV` for non-interactive shells (`bash -c`
what the claude `Bash` tool runs) and sourced from what the claude `Bash` tool runs) and sourced from
`programs.bash.interactiveShellInit` for interactive shells. `programs.bash.interactiveShellInit` for interactive shells.
@ -70,7 +70,7 @@
# back to the un-wrapped binary in PATH (the rust toolchain's cargo # back to the un-wrapped binary in PATH (the rust toolchain's cargo
# — either from `environment.systemPackages` or from whatever # — either from `environment.systemPackages` or from whatever
# `nix develop` shell the agent's working in). # `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 # Auto-injects --message-format short on cargo compile
# subcommands so per-crate progress lines don't flood # subcommands so per-crate progress lines don't flood
# claude's context. Bypassed when the caller already passes # 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 # Wiring is gated on at least one fragment being active so a
# fully feature-disabled agent has neither the file nor the # fully feature-disabled agent has neither the file nor the
# `BASH_ENV` / interactive sourcing — zero cost in that case. # `BASH_ENV` / interactive sourcing — zero cost in that case.
environment.etc."hyperhive/bash-env.sh" = lib.mkIf (config.hyperhive._bashEnvFragments != "") { environment.etc."hyperhive/bash-env.sh" =
text = config.hyperhive._bashEnvFragments; lib.mkIf (config.services.hyperhive.agent._bashEnvFragments != "")
}; {
text = config.services.hyperhive.agent._bashEnvFragments;
};
# Non-interactive bash invocations (claude's `Bash` tool runs # Non-interactive bash invocations (claude's `Bash` tool runs
# `bash -c`) source $BASH_ENV at startup — drops every active # `bash -c`) source $BASH_ENV at startup — drops every active
# feature hook's snippet into scope without touching # feature hook's snippet into scope without touching
# `/etc/profile` (login-only). # `/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"; BASH_ENV = "/etc/hyperhive/bash-env.sh";
}; };
@ -125,10 +127,12 @@
# hook surface as claude's non-interactive calls. Gated on at # hook surface as claude's non-interactive calls. Gated on at
# least one fragment being active so we don't write a no-op # least one fragment being active so we don't write a no-op
# source line into `/etc/bashrc` on fully-feature-disabled agents. # source line into `/etc/bashrc` on fully-feature-disabled agents.
programs.bash.interactiveShellInit = lib.mkIf (config.hyperhive._bashEnvFragments != "") '' programs.bash.interactiveShellInit =
if [ -r /etc/hyperhive/bash-env.sh ]; then lib.mkIf (config.services.hyperhive.agent._bashEnvFragments != "")
. /etc/hyperhive/bash-env.sh ''
fi if [ -r /etc/hyperhive/bash-env.sh ]; then
''; . /etc/hyperhive/bash-env.sh
fi
'';
}; };
} }

View file

@ -13,12 +13,12 @@
... ...
}: }:
let let
userName = config.hyperhive.user.name; userName = config.services.hyperhive.agent.user.name;
homeDir = "/home/${userName}"; homeDir = "/home/${userName}";
# Hive-wide OpenTelemetry config (host-driven; baked in per-agent by # Hive-wide OpenTelemetry config (host-driven; baked in per-agent by
# meta.rs `otel_config`). Options declared in `otel.nix`, which also # meta.rs `otel_config`). Options declared in `otel.nix`, which also
# exports the generic OTLP environment this container's producers read. # 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 # Hive display name, read from the per-agent option meta.rs renders
# (NOT from `environment.variables` — that carries the same name at # (NOT from `environment.variables` — that carries the same name at
# *runtime* only, so reading it here silently yielded "unknown" on # *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 # means the hive did not name itself; "unknown" is then an honest label
# rather than a guess. # rather than a guess.
hiveDisplayName = 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 # 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 # when the effective cap is unbounded ("infinity") or a RAM
# percentage — see `resource_limits::effective_memory_bytes`. # 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. # Base claude-code environment applied to every agent regardless of OTEL.
# Shipped via the managed settings `env` block so claude and `hivectl # Shipped via the managed settings `env` block so claude and `hivectl
# choom` both inherit them without a launch wrapper. # choom` both inherit them without a launch wrapper.
@ -99,13 +102,13 @@ in
# `MemoryMax=` (per-agent `resource-limits.json` override, else the # `MemoryMax=` (per-agent `resource-limits.json` override, else the
# hive-wide `services.hyperhive.agentMemoryMax`) — see # hive-wide `services.hyperhive.agentMemoryMax`) — see
# `resource_limits::effective_memory_bytes_from`. Not meant to be set # `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 # 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 real operator knob, and this only reflects the value baked in at
# the agent's *last rebuild* — `set-limits` still applies the # the agent's *last rebuild* — `set-limits` still applies the
# cgroup cap live via a drop-in reload, but this derived heap ceiling # cgroup cap live via a drop-in reload, but this derived heap ceiling
# needs a rebuild to pick up a new value. # 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; type = lib.types.nullOr lib.types.str;
default = null; default = null;
internal = true; 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; type = lib.types.nullOr lib.types.str;
default = null; default = null;
internal = true; internal = true;
@ -130,13 +133,13 @@ in
Human-readable swarm name, rendered per-agent by Human-readable swarm name, rendered per-agent by
`meta.rs::render_flake` from the host's `meta.rs::render_flake` from the host's
`services.hyperhive.swarm.name`. Same build-time/runtime split as `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. `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; type = lib.types.nullOr lib.types.ints.positive;
default = null; default = null;
internal = true; 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; type = lib.types.listOf lib.types.str;
default = [ default = [
"anthropics/claude-plugins-official" "anthropics/claude-plugins-official"
"${config.hyperhive.packages.claude-plugins}" "${config.services.hyperhive.agent.packages.claude-plugins}"
]; ];
defaultText = lib.literalMD '' defaultText = lib.literalMD ''
`[ "anthropics/claude-plugins-official" "''${hyperhive.packages.claude-plugins}" ]` `[ "anthropics/claude-plugins-official" "''${hyperhive.packages.claude-plugins}" ]`
@ -169,7 +172,7 @@ in
entry is passed to `claude plugin marketplace add <source>` entry is passed to `claude plugin marketplace add <source>`
(`owner/repo`, full git URL, or local path). Idempotent (`owner/repo`, full git URL, or local path). Idempotent
re-adding an existing marketplace is treated as success. 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`). reference a marketplace (e.g. `foo@claude-plugins-official`).
Rendered to `/etc/hyperhive/claude-marketplaces.json`. 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; type = lib.types.listOf lib.types.str;
default = [ default = [
"skill-creator@claude-plugins-official" "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; type = lib.types.bool;
default = false; default = false;
description = '' description = ''
@ -239,13 +242,13 @@ in
config = { config = {
environment.etc."hyperhive/claude-plugins.json".text = 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 = 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 = 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 # Hive-enforced claude settings. claude-code auto-discovers managed
# settings at this canonical Linux path (precedence #1, read-only, # settings at this canonical Linux path (precedence #1, read-only,
@ -263,7 +266,7 @@ in
# `hivectl choom`) so no launch wrapper is needed. # `hivectl choom`) so no launch wrapper is needed.
environment.etc."claude-code/managed-settings.json".source = environment.etc."claude-code/managed-settings.json".source =
let 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 # Merge base env (always) with OTEL env (when enabled). jq is always
# run — `baseClaudeEnv` contains per-agent values (e.g. # run — `baseClaudeEnv` contains per-agent values (e.g.
# CLAUDE_REMOTE_CONTROL_SESSION_NAME_PREFIX) that can't live in the # 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 ( type = lib.types.listOf (
lib.types.submodule { lib.types.submodule {
options = { options = {
@ -47,27 +47,29 @@
# read them without accessing the container's /etc/ from the host. # read them without accessing the container's /etc/ from the host.
# Best-effort oneshot (always exit 0): # Best-effort oneshot (always exit 0):
# docs/process/conventions.md::Best-effort oneshot services. # docs/process/conventions.md::Best-effort oneshot services.
systemd.services.hive-dashboard-links = lib.mkIf (config.hyperhive.dashboardLinks != [ ]) { systemd.services.hive-dashboard-links =
description = "write declarative dashboardLinks to agent state dir"; lib.mkIf (config.services.hyperhive.agent.dashboardLinks != [ ])
wantedBy = [ "multi-user.target" ]; {
serviceConfig = { description = "write declarative dashboardLinks to agent state dir";
Type = "oneshot"; wantedBy = [ "multi-user.target" ];
RemainAfterExit = true; serviceConfig = {
# Pin the journal identity (else it's the `script` store-path wrapper). Type = "oneshot";
SyslogIdentifier = "hive-dashboard-links"; RemainAfterExit = true;
}; # Pin the journal identity (else it's the `script` store-path wrapper).
environment.LINKS_JSON = builtins.toJSON config.hyperhive.dashboardLinks; SyslogIdentifier = "hive-dashboard-links";
script = '' };
# Sub-agents have their state dir bind-mounted at /agents/<name>/state. environment.LINKS_JSON = builtins.toJSON config.services.hyperhive.agent.dashboardLinks;
# Use a glob — exactly one match per container at runtime. script = ''
STATE_DIR=$(echo /agents/*/state) # Sub-agents have their state dir bind-mounted at /agents/<name>/state.
if [ ! -d "$STATE_DIR" ]; then # Use a glob — exactly one match per container at runtime.
echo "hive-dashboard-links: no state dir found at /agents/*/state; skipping" STATE_DIR=$(echo /agents/*/state)
exit 0 if [ ! -d "$STATE_DIR" ]; then
fi echo "hive-dashboard-links: no state dir found at /agents/*/state; skipping"
printf '%s' "$LINKS_JSON" > "$STATE_DIR/hyperhive-dashboard-links.json" exit 0
echo "hive-dashboard-links: wrote $(printf '%s' "$LINKS_JSON" | wc -c) bytes to $STATE_DIR/hyperhive-dashboard-links.json" 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. # they came from.
# #
# This is the core module: container plumbing (boot/nix/nixpkgs), # 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 # 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, pkgs,
lib, lib,
@ -35,6 +35,7 @@
./otel.nix ./otel.nix
./packages.nix ./packages.nix
./queue.nix ./queue.nix
./renamed-options.nix
./user.nix ./user.nix
./screen.nix ./screen.nix
./weston-vnc.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; type = lib.types.nullOr lib.types.path;
default = null; default = null;
example = lib.literalExpression "./icon.svg"; 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; type = lib.types.nullOr lib.types.str;
default = null; default = null;
example = "/nix/store/-claude-code-2.1.220"; example = "/nix/store/-claude-code-2.1.220";
@ -108,20 +109,22 @@
_module.args.flakeInputs = builtins.removeAttrs inputs [ "self" ]; _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); assertion =
message = "hyperhive.icon must point to an .svg file"; 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 # SVG lands at /etc/hyperhive/icon.svg; the harness serves it at
# GET /icon, 404ing when absent (client-side fallback, no # GET /icon, 404ing when absent (client-side fallback, no
# server-side default). Consumed by forge-avatar-sync (./forge.nix) # server-side default). Consumed by forge-avatar-sync (./forge.nix)
# and the matrix avatar sync (./matrix.nix) too. # and the matrix avatar sync (./matrix.nix) too.
environment.etc."hyperhive/icon.svg" = lib.mkIf (config.hyperhive.icon != null) { environment.etc."hyperhive/icon.svg" = lib.mkIf (config.services.hyperhive.agent.icon != null) {
source = config.hyperhive.icon; source = config.services.hyperhive.agent.icon;
}; };
boot.isNspawnContainer = true; boot.isNspawnContainer = true;
@ -213,11 +216,11 @@
# `hive-metric` (agent-emitted custom metrics CLI, # `hive-metric` (agent-emitted custom metrics CLI,
# docs/scheduler/observability.md). # docs/scheduler/observability.md).
environment.systemPackages = [ 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 pkgs.claude-code
else else
# Host-pinned claude: a symlink farm around a path the # Host-pinned claude: a symlink farm around a path the
@ -235,7 +238,7 @@
# see `services.hyperhive.c0re.claudeCodePackage`. # see `services.hyperhive.c0re.claudeCodePackage`.
pkgs.runCommandLocal "claude-code-pinned" { } '' pkgs.runCommandLocal "claude-code-pinned" { } ''
mkdir -p "$out/bin" 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 # host-level `services.hyperhive.c0re.contextWindowTokens` option — not
# set here. # set here.
environment.variables = { 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"; SHELL = "${pkgs.bashInteractive}/bin/bash";
# Route interactive-shell nix invocations through the host daemon. # Route interactive-shell nix invocations through the host daemon.
# Redundant with /etc/profile.d/nix-daemon.sh but ensures it's set # 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. # options and the `$HIVE_DOCS_DIR` wiring the harness reads.
{ {
pkgs, 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 make the hyperhive reference docs (the repo `docs/` tree, shipped
read-only as the standalone `hyperhive-docs` derivation) available read-only as the standalone `hyperhive-docs` derivation) available
in-container. When enabled the harness exposes the docs dir to claude in-container. When enabled the harness exposes the docs dir to claude
@ -19,13 +19,13 @@
`agent.nix`. `agent.nix`.
''; '';
options.hyperhive.docs.source = lib.mkOption { options.services.hyperhive.agent.docs.source = lib.mkOption {
type = lib.types.path; 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)"; defaultText = lib.literalMD "`hyperhive.packages.reference-docs` (built from the repo `docs/` tree)";
description = '' description = ''
Store path of the reference-docs tree exposed at `$HIVE_DOCS_DIR` 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` `reference-docs` package (the `nix/packages/reference-docs.nix`
build) so a standalone container build from a full checkout build) so a standalone container build from a full checkout
works unchanged. The generated meta flake overrides this with the works unchanged. The generated meta flake overrides this with the
@ -36,15 +36,15 @@
}; };
config = { 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 # The harness reads HIVE_DOCS_DIR and passes it to claude as
# `--add-dir` so the docs are readable, and appends a single # `--add-dir` so the docs are readable, and appends a single
# pointer sentence to the system prompt # pointer sentence to the system prompt
# (hive-agent::prompt::render) telling the agent the docs exist. # (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 # meta-flake input, or `pkgs.hyperhive-docs` for standalone
# builds). See hive-agent::turn. # 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 let
userName = config.hyperhive.user.name; userName = config.services.hyperhive.agent.user.name;
homeDir = "/home/${userName}"; homeDir = "/home/${userName}";
# Same 512×512 rasterization of the agent icon the matrix avatar # Same 512×512 rasterization of the agent icon the matrix avatar
# sync uses (./matrix.nix — identical derivation, same store path). # sync uses (./matrix.nix — identical derivation, same store path).
# Only forced when an icon is configured (the avatar-sync unit below # 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 ]; } '' 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 # git credential helper for the hive forge --- the exact shape
@ -39,7 +39,7 @@ let
''; '';
in in
{ {
options.hyperhive.forge.url = lib.mkOption { options.services.hyperhive.agent.forge.url = lib.mkOption {
type = lib.types.nullOr lib.types.str; type = lib.types.nullOr lib.types.str;
default = null; default = null;
example = "http://forge.internal:3000"; example = "http://forge.internal:3000";
@ -80,10 +80,10 @@ in
# is for, so reject it and name the option. # is for, so reject it and name the option.
{ {
assertion = assertion =
config.hyperhive.forge.url == null config.services.hyperhive.agent.forge.url == null
|| lib.hasPrefix "http://" config.hyperhive.forge.url || lib.hasPrefix "http://" config.services.hyperhive.agent.forge.url
|| lib.hasPrefix "https://" config.hyperhive.forge.url; || lib.hasPrefix "https://" config.services.hyperhive.agent.forge.url;
message = "hyperhive.forge.url must be an http:// or https:// URL, or null for no forge (got: \"${toString config.hyperhive.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 # hive-forge <verb>: CLI wrapping common Forgejo REST API operations
# (view, pr, issue, comment, assign, close, labels, branches, etc.). # (view, pr, issue, comment, assign, close, labels, branches, etc.).
# The per-bin split package — narrow closure, no hivectl/wireguard. # 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 # Wire the forge credential helper for `git push`, scoped to the forge
# this agent is configured for. # this agent is configured for.
@ -135,9 +135,9 @@ in
# works for any agent whose personal `~/.gitconfig` names the helper by # works for any agent whose personal `~/.gitconfig` names the helper by
# ABSOLUTE path — so this is masked exactly where it would be noticed, # ABSOLUTE path — so this is masked exactly where it would be noticed,
# and bites a fresh agent that has no such file. # 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 = '' text = ''
[credential "${lib.removeSuffix "/" config.hyperhive.forge.url}"] [credential "${lib.removeSuffix "/" config.services.hyperhive.agent.forge.url}"]
helper = hive-forge helper = hive-forge
username = ${userName} username = ${userName}
''; '';
@ -165,7 +165,7 @@ in
# and the agent's `forge-token` from under the second. # and the agent's `forge-token` from under the second.
}; };
serviceConfig = { 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"; SyslogIdentifier = "hive-forge-notify";
# `on-failure`, NOT `always`: an agent with no forge account is a # `on-failure`, NOT `always`: an agent with no forge account is a
# supported configuration, and the poller reports that by logging # supported configuration, and the poller reports that by logging
@ -183,7 +183,7 @@ in
# docs/process/conventions.md::Best-effort oneshot services. # docs/process/conventions.md::Best-effort oneshot services.
# Not generated at all when no forge is configured: an absent # Not generated at all when no forge is configured: an absent
# integration rather than one pointed at a guessed address. # 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)"; description = "configure tea CLI from hive-forge token (best-effort)";
wantedBy = [ "multi-user.target" ]; wantedBy = [ "multi-user.target" ];
after = [ "local-fs.target" ]; after = [ "local-fs.target" ];
@ -202,7 +202,7 @@ in
environment.AGENT_USER = userName; environment.AGENT_USER = userName;
script = '' script = ''
# No `set -e`: best-effort posture (see docs pointer above). # 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. # $HYPERHIVE_STATE_DIR is system-wide via the meta flake.
TOKEN_FILE="$HYPERHIVE_STATE_DIR/forge-token" TOKEN_FILE="$HYPERHIVE_STATE_DIR/forge-token"
if [ ! -f "$TOKEN_FILE" ]; then if [ ! -f "$TOKEN_FILE" ]; then
@ -266,13 +266,13 @@ in
# watches paths this unit has no business reacting to. # watches paths this unit has no business reacting to.
# The service reads `$HYPERHIVE_STATE_DIR/forge-token`; this is the same # The service reads `$HYPERHIVE_STATE_DIR/forge-token`; this is the same
# file, spelled the way `tea-login` above already spells it. # 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"; description = "trigger forge-avatar-sync when forge-token appears";
wantedBy = [ "multi-user.target" ]; wantedBy = [ "multi-user.target" ];
pathConfig.PathChanged = "/agents/${userName}/state/forge-token"; 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. # docs/process/conventions.md::Best-effort oneshot services.
# RemainAfterExit = false so the .path trigger above can re-fire # RemainAfterExit = false so the .path trigger above can re-fire
# this unit when the forge-token arrives after boot. The PNG is # 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 # and needs no librsvg at runtime — Forgejo's Go image library
# can't decode SVG, hence PNG. # can't decode SVG, hence PNG.
systemd.services.forge-avatar-sync = 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)"; description = "sync agent icon to Forgejo user avatar (best-effort)";
wantedBy = [ "multi-user.target" ]; wantedBy = [ "multi-user.target" ];
@ -298,7 +299,7 @@ in
pkgs.jq pkgs.jq
]; ];
script = '' 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 # $HYPERHIVE_STATE_DIR is set system-wide by the meta flake
# (systemd.globalEnvironment) to `/agents/<name>/state`. # (systemd.globalEnvironment) to `/agents/<name>/state`.
TOKEN_FILE="$HYPERHIVE_STATE_DIR/forge-token" 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; 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)"; defaultText = lib.literalMD "`hyperhive.packages.frontend` (the flake's frontend dist)";
description = '' description = ''
The shipped frontend dist (built by `nix/packages/frontend.nix`). 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; type = lib.types.package;
readOnly = true; readOnly = true;
description = '' description = ''
Computed: the merged static tree consumed by the harness via Computed: the merged static tree consumed by the harness via
`HIVE_STATIC_DIR`. Composed at evaluation time by copying `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. 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 ( type = lib.types.attrsOf (
lib.types.submodule ( lib.types.submodule (
{ name, ... }: { name, ... }:
@ -100,7 +100,7 @@
prior entry's target), the `mergedDist` build aborts with prior entry's target), the `mergedDist` build aborts with
`refusing to overwrite existing path '<target>' in the `refusing to overwrite existing path '<target>' in the
default dist`. To override a default file, fork the dist via 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. pure additions.
`target` must be a relative path inside the static dir. An `target` must be a relative path inside the static dir. An
@ -112,7 +112,7 @@
config = { config = {
assertions = [ 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 # $out during the mergedDist build. The option's strMatching
# type already rejects leading `/`, leading `.`, and the # type already rejects leading `/`, leading `.`, and the
# weirder characters; this assertion catches mid-path `..` # weirder characters; this assertion catches mid-path `..`
@ -122,10 +122,10 @@
# kind of mistake that's easy to make and hard to spot. # kind of mistake that's easy to make and hard to spot.
{ {
assertion = lib.all (entry: !(builtins.any (seg: seg == "..") (lib.splitString "/" entry.target))) ( 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 = '' message = ''
hyperhive.frontend.extraFiles: `target` must not contain services.hyperhive.agent.frontend.extraFiles: `target` must not contain
`..` path segments. `..` path segments.
''; '';
} }
@ -137,20 +137,22 @@
# filename collision with the default dist surfaces as a build # filename collision with the default dist surfaces as a build
# failure rather than a silent override (operator gets a clear # failure rather than a silent override (operator gets a clear
# nix error rather than a confusing 404 / silent dist swap). # nix error rather than a confusing 404 / silent dist swap).
hyperhive.frontend.mergedDist = pkgs.runCommand "hyperhive-agent-frontend-merged" { } ( services.hyperhive.agent.frontend.mergedDist =
'' pkgs.runCommand "hyperhive-agent-frontend-merged" { }
mkdir -p $out (
cp -r ${config.hyperhive.frontend.dist}/agent/. $out/ ''
chmod -R u+w $out mkdir -p $out
'' cp -r ${config.services.hyperhive.agent.frontend.dist}/agent/. $out/
+ lib.concatMapStrings (entry: '' chmod -R u+w $out
mkdir -p $(dirname $out/${entry.target}) ''
if [ -e $out/${entry.target} ]; then + lib.concatMapStrings (entry: ''
echo "hyperhive.frontend.extraFiles: refusing to overwrite existing path '${entry.target}' in the default dist" >&2 mkdir -p $(dirname $out/${entry.target})
exit 1 if [ -e $out/${entry.target} ]; then
fi echo "services.hyperhive.agent.frontend.extraFiles: refusing to overwrite existing path '${entry.target}' in the default dist" >&2
cp -r ${entry.source} $out/${entry.target} exit 1
'') (lib.attrValues config.hyperhive.frontend.extraFiles) 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 # credential helper, both reading the PAT from the agent's
# `github-token` state file at invocation, so a dashboard-pasted token # `github-token` state file at invocation, so a dashboard-pasted token
# takes effect with no rebuild. The token PATH is baked in at build # takes effect with no rebuild. The token PATH is baked in at build
@ -15,7 +15,7 @@
... ...
}: }:
let let
userName = config.hyperhive.user.name; userName = config.services.hyperhive.agent.user.name;
ghWrapper = pkgs.writeShellScriptBin "gh" '' ghWrapper = pkgs.writeShellScriptBin "gh" ''
if [ -r "/agents/${userName}/state/github-token" ]; then if [ -r "/agents/${userName}/state/github-token" ]; then
GH_TOKEN="$(cat "/agents/${userName}/state/github-token")" GH_TOKEN="$(cat "/agents/${userName}/state/github-token")"
@ -35,7 +35,7 @@ let
''; '';
in in
{ {
options.hyperhive.github.enable = lib.mkOption { options.services.hyperhive.agent.github.enable = lib.mkOption {
type = lib.types.bool; type = lib.types.bool;
default = true; default = true;
description = '' description = ''
@ -63,7 +63,7 @@ in
config = { config = {
# No bare pkgs.gh here — the wrapper *is* `gh` and hardcodes the # No bare pkgs.gh here — the wrapper *is* `gh` and hardcodes the
# real binary path, so it can't be shadowed. # 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 ghWrapper
gitCredHelper gitCredHelper
]; ];
@ -78,7 +78,7 @@ in
# entries in the harness modules) — a whole-set `environment.etc = {…}` # entries in the harness modules) — a whole-set `environment.etc = {…}`
# here would collide with them at the nix level ("attribute already # here would collide with them at the nix level ("attribute already
# defined"). # defined").
environment.etc."gitconfig" = lib.mkIf config.hyperhive.github.enable { environment.etc."gitconfig" = lib.mkIf config.services.hyperhive.agent.github.enable {
text = '' text = ''
[credential "https://github.com"] [credential "https://github.com"]
helper = hive-github helper = hive-github
@ -100,7 +100,7 @@ in
# extractor copies exactly one binary, so an agent that installs # extractor copies exactly one binary, so an agent that installs
# only the Forgejo poller has no github.com poller anywhere in its # only the Forgejo poller has no github.com poller anywhere in its
# closure — not merely an unstarted unit. # 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"; description = "github.com notification poller for this agent";
wantedBy = [ "multi-user.target" ]; wantedBy = [ "multi-user.target" ];
after = [ "network.target" ]; after = [ "network.target" ];
@ -113,7 +113,7 @@ in
# poller reads the agent's `github-token` from under it. # poller reads the agent's `github-token` from under it.
}; };
serviceConfig = { 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"; SyslogIdentifier = "hive-github-notify";
# `on-failure`, NOT `always`, for the same reason as the forge # `on-failure`, NOT `always`, for the same reason as the forge
# poller: this unit ships on every agent, but most agents have no # poller: this unit ships on every agent, but most agents have no

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -16,7 +16,7 @@
... ...
}: }:
let let
cfg = config.hyperhive.queue; cfg = config.services.hyperhive.agent.queue;
configured = cfg.natsUrl != null && cfg.tokenEndpoint != null; configured = cfg.natsUrl != null && cfg.tokenEndpoint != null;
# The two ids `hive_c0re::lifecycle::host_config` forwards under. Neither # The two ids `hive_c0re::lifecycle::host_config` forwards under. Neither
@ -32,7 +32,7 @@ let
credentialsDir = "/run/credentials/hive-agent.service"; credentialsDir = "/run/credentials/hive-agent.service";
in in
{ {
options.hyperhive.queue = { options.services.hyperhive.agent.queue = {
natsUrl = lib.mkOption { natsUrl = lib.mkOption {
type = lib.types.nullOr lib.types.str; type = lib.types.nullOr lib.types.str;
default = null; default = null;
@ -61,7 +61,7 @@ in
`client_credentials` token there and presents it to the queue, which `client_credentials` token there and presents it to the queue, which
authenticates it as the client named in the delivered credential. 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 the harness treats a half-set pair as a deployment bug rather than as
"no queue coordinates". "no queue coordinates".
''; '';
@ -79,7 +79,7 @@ in
description = '' description = ''
Path the agent's OIDC client id is delivered at, for a consumer 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 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. for the consumer this exists for.
''; '';
}; };
@ -90,7 +90,7 @@ in
default = "${credentialsDir}/${secretCredential}"; default = "${credentialsDir}/${secretCredential}";
description = '' description = ''
Path the agent's OIDC client secret is delivered at. Read-only for 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 🩸 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 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. # 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 # `hive-screen-mcp` stdio bridge as `extraMcpServers.screen` so claude
# gets five tools: `screenshot`, `type_text`, `key_press`, # gets five tools: `screenshot`, `type_text`, `key_press`,
# `mouse_move`, and `mouse_click`. # `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. # Register the screen MCP bridge so claude gets the screen tools.
hyperhive.extraMcpServers.screen = { services.hyperhive.agent.extraMcpServers.screen = {
command = "${config.hyperhive.packages.hive-screen-mcp}/bin/hive-screen-mcp"; command = "${config.services.hyperhive.agent.packages.hive-screen-mcp}/bin/hive-screen-mcp";
args = [ ]; 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 # declarations, passwordless sudo, and the first-boot migration that
# chowns the bind-mounted state dirs to the agent user. # chowns the bind-mounted state dirs to the agent user.
{ {
@ -8,7 +8,7 @@
... ...
}: }:
let let
userName = config.hyperhive.user.name; userName = config.services.hyperhive.agent.user.name;
homeDir = "/home/${userName}"; homeDir = "/home/${userName}";
in in
{ {
@ -20,7 +20,7 @@ in
# uniquely-named user matching its agent label. UID auto-assigned # uniquely-named user matching its agent label. UID auto-assigned
# by NixOS (the auto-allocation range for normal users); no hard- # by NixOS (the auto-allocation range for normal users); no hard-
# coded UID. # 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}$"; type = lib.types.strMatching "^[a-z_][a-z0-9_-]{0,30}$";
default = "agent"; default = "agent";
example = "iris"; example = "iris";
@ -33,11 +33,11 @@ in
Constraints match `useradd`'s NAME_REGEX: lowercase / `_` start, Constraints match `useradd`'s NAME_REGEX: lowercase / `_` start,
total length 31, no special characters. UID is auto-assigned 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; type = lib.types.nullOr lib.types.int;
default = null; default = null;
example = 1100; 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; type = lib.types.nullOr lib.types.int;
default = null; default = null;
example = 1100; example = 1100;
description = '' description = ''
Optional fixed GID for the per-agent unix group. `null` (default) 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 to the same value (the conventional Unix pattern for per-user
groups where uid == gid), but can be set independently. 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; type = lib.types.bool;
default = true; default = true;
example = false; example = false;
description = '' 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 (`NOPASSWD: ALL`). True by default so claude's `Bash` tool
keeps working for tools that expect root inside the container keeps working for tools that expect root inside the container
(`systemctl`, package managers in dev shells, etc.) the (`systemctl`, package managers in dev shells, etc.) the
@ -94,10 +94,12 @@ in
assertions = [ assertions = [
{ {
assertion = assertion =
config.hyperhive.user.uid == null config.services.hyperhive.agent.user.uid == null
|| (config.hyperhive.user.uid >= 1000 && config.hyperhive.user.uid < 60000); || (
config.services.hyperhive.agent.user.uid >= 1000 && config.services.hyperhive.agent.user.uid < 60000
);
message = '' 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 1000 clash with system accounts; values 60000 are reserved
by NixOS for dynamic allocation. Leave unset (null) to let by NixOS for dynamic allocation. Leave unset (null) to let
NixOS auto-assign. NixOS auto-assign.
@ -105,11 +107,13 @@ in
} }
{ {
assertion = assertion =
config.hyperhive.user.gid == null config.services.hyperhive.agent.user.gid == null
|| (config.hyperhive.user.gid >= 1000 && config.hyperhive.user.gid < 60000); || (
config.services.hyperhive.agent.user.gid >= 1000 && config.services.hyperhive.agent.user.gid < 60000
);
message = '' message = ''
hyperhive.user.gid must be in [1000, 60000) same range services.hyperhive.agent.user.gid must be in [1000, 60000) same range
constraint as hyperhive.user.uid. constraint as services.hyperhive.agent.user.uid.
''; '';
} }
]; ];
@ -123,29 +127,29 @@ in
home = homeDir; home = homeDir;
createHome = true; createHome = true;
group = userName; 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 # Matches /bin/bash on NixOS — the harness's claude shell-outs
# expect a POSIX shell at $SHELL; bashInteractive is already # expect a POSIX shell at $SHELL; bashInteractive is already
# the system default for the root user too. # the system default for the root user too.
shell = pkgs.bashInteractive; shell = pkgs.bashInteractive;
} }
// lib.optionalAttrs (config.hyperhive.user.uid != null) { // lib.optionalAttrs (config.services.hyperhive.agent.user.uid != null) {
uid = config.hyperhive.user.uid; uid = config.services.hyperhive.agent.user.uid;
}; };
users.groups.${userName} = users.groups.${userName} =
{ } { }
// lib.optionalAttrs (config.hyperhive.user.gid != null) { // lib.optionalAttrs (config.services.hyperhive.agent.user.gid != null) {
gid = config.hyperhive.user.gid; gid = config.services.hyperhive.agent.user.gid;
}; };
# `NOPASSWD: ALL` for the agent user. Lets claude's Bash tool # `NOPASSWD: ALL` for the agent user. Lets claude's Bash tool
# keep working with anything that expected root (systemctl, # keep working with anything that expected root (systemctl,
# nix-env, etc.) without prompting. Flip # 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 # the wheel-group membership and this sudoers entry; anything
# that needs root then fails loudly instead of silently # that needs root then fails loudly instead of silently
# succeeding. # succeeding.
security.sudo.extraRules = lib.mkIf config.hyperhive.user.passwordlessSudo [ security.sudo.extraRules = lib.mkIf config.services.hyperhive.agent.user.passwordlessSudo [
{ {
users = [ userName ]; users = [ userName ];
commands = [ commands = [

View file

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

View file

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