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:
parent
60393d0e32
commit
3662eda440
21 changed files with 531 additions and 290 deletions
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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}";
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
'';
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
'';
|
||||||
|
};
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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}";
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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 = ''
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
213
nix/agent-modules/renamed-options.nix
Normal file
213
nix/agent-modules/renamed-options.nix
Normal 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" ]
|
||||||
|
)
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
@ -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 = [ ];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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 = [
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
'';
|
'';
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue