Watch
0
0
Fork
You've already forked hyperhive
0

Compare commits

...
Author SHA1 Message Date
atlas
6ea81aae65 hive-c0re: render the new agent option paths into generated agent flakes
meta.rs writes each agent's flake, and it still named the pre-move
`hyperhive.*` paths — so every agent rebuild would print a rename
deprecation warning about a line no human wrote and no operator could fix.
A warning nobody can act on trains everyone to ignore the ones that matter,
which is the whole value of the alias shims.

Repoints the FORWARDED_VAR_OPTIONS table and every other emitted option
assignment (otel.*, docs.source, claudeCodePath, github.enable, user.name,
claudeMemoryMaxBytes) to `services.hyperhive.agent.*`, with the test
expectations that pin the rendered text. The flake input named `hyperhive`
(`hyperhive.url`, `hyperhive.inputs.nixpkgs.follows`,
`hyperhive.nixosConfigurations.*`), hive-tier `services.hyperhive.*` paths,
and the `@hyperhive.local` git identity share the word and are untouched.

Also repoints the same option paths where they appear in comments, rustdoc
and runtime message strings across the other crates — a refusal message
naming `hyperhive.allowedRecipients` sends an operator to a path that will
stop existing. Prose under docs/ is deliberately not in this commit.

Refs #4473
2026-09-17 20:19:30 +02:00
atlas
3662eda440 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
2026-09-17 20:19:30 +02:00
51 changed files with 669 additions and 411 deletions

View file

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

View file

@ -150,7 +150,7 @@ impl AgentServer {
async fn send(&self, Parameters(args): Parameters<SendArgs>) -> String {
let log = format!("{args:?}");
let to = args.to.clone();
// Check per-agent allow-list (hyperhive.allowedRecipients). When no
// Check per-agent allow-list (services.hyperhive.agent.allowedRecipients). When no
// policy file is present (e.g. manager containers) the check is a no-op.
if let Err(refusal) = crate::send_allow::check_send_allowed(&to) {
return run_tool_envelope("send", log, async move { refusal }).await;

View file

@ -318,7 +318,7 @@ pub(super) struct MatrixRoomUnread {
}
/// Default port `hive-matrix-daemon` serves its MCP + status endpoints
/// on (`hyperhive.mcp.matrixHttpPort`'s nix default). Overridable via
/// on (`services.hyperhive.agent.mcp.matrixHttpPort`'s nix default). Overridable via
/// `HIVE_MATRIX_HTTP_PORT` for parity with the port options nix already
/// exposes; unset in practice since a single fixed port is safe (each
/// agent container is its own network namespace — see docs/networking/network.md).

View file

@ -49,7 +49,7 @@ pub fn check_send_allowed(to: &str) -> Result<(), String> {
return Ok(());
}
Err(format!(
"send refused: recipient '{to}' not in hyperhive.allowedRecipients \
"send refused: recipient '{to}' not in services.hyperhive.agent.allowedRecipients \
(configured in agent.nix). Allowed: {allow:?}. Your structural \
parent is always reachable — route through `send(to: \"{}\", …)` \
if you need to reach someone outside the allow-list.",

View file

@ -1,4 +1,4 @@
//! Shared spec for `hyperhive.extraMcpServers` entries: the on-disk JSON
//! Shared spec for `services.hyperhive.agent.extraMcpServers` entries: the on-disk JSON
//! shape the nix module (`nix/agent-modules/mcp.nix`) renders to
//! `/etc/hyperhive/extra-mcp.json`, plus the parsing and per-entry
//! JSON-rendering logic every consumer needs. Lives in `hive-agent-sock`
@ -16,7 +16,7 @@ use std::path::Path;
/// `--mcp-config`.
pub const EXTRA_MCP_PATH: &str = "/etc/hyperhive/extra-mcp.json";
/// An extra MCP server declared via `hyperhive.extraMcpServers`. Two
/// An extra MCP server declared via `services.hyperhive.agent.extraMcpServers`. Two
/// transports: `Stdio` (the consumer spawns `command` fresh each turn, talks
/// JSON-RPC over its stdin/stdout) and `Http` (point claude at a long-lived
/// streamable-http `url` instead — no per-turn spawn, no re-registration
@ -55,7 +55,7 @@ impl ExtraMcpServer {
}
/// Whether this entry is opted in to subagent sessions
/// (`hyperhive.extraMcpServers.<name>.availableToSubagents`, default
/// (`services.hyperhive.agent.extraMcpServers.<name>.availableToSubagents`, default
/// `false`). Subagents run `--strict-mcp-config` with no discovery, so
/// this flag is the only path an entry reaches a subagent's own
/// `--mcp-config` at all — see `hive-subagent-mcp`'s `mcp_config`

View file

@ -16,7 +16,7 @@
//! signals its own turn loop directly, so hive-c0re is not in either path —
//! no broker round-trip, no long-poll, no marker files.
//!
//! **`extra_mcp`**: the `hyperhive.extraMcpServers` spec + parsing, shared by
//! **`extra_mcp`**: the `services.hyperhive.agent.extraMcpServers` spec + parsing, shared by
//! `hive-agent` (the main session's `--mcp-config`) and `hive-subagent-mcp`
//! (the subagent-eligible subset) — not a socket protocol at all, but this
//! crate is the natural home since both consumers already depend on it.

View file

@ -385,7 +385,7 @@ impl Bus {
}
};
let (tx, _) = broadcast::channel(CHANNEL_CAPACITY);
// Priority: HIVE_DEFAULT_MODEL (from hyperhive.model in agent.nix) >
// Priority: HIVE_DEFAULT_MODEL (from services.hyperhive.agent.model in agent.nix) >
// persisted runtime override > compiled-in DEFAULT_MODEL.
// The nix config always wins on rebuild; the persisted file is kept
// for within-session tracking only (see persist_model / set_model).

View file

@ -189,7 +189,7 @@ pub(crate) fn write_api_key_mode(enabled: bool) {
pub const DEFAULT_MODEL: &str = "haiku";
/// Return the model declared in `HIVE_DEFAULT_MODEL` (set from
/// `hyperhive.model` in `agent.nix`), or `None` if the env var is absent /
/// `services.hyperhive.agent.model` in `agent.nix`), or `None` if the env var is absent /
/// empty. When `Some`, this takes precedence over any persisted runtime
/// override so that nix config changes always take effect on rebuild.
#[must_use]
@ -216,7 +216,7 @@ pub fn is_valid_effort(level: &str) -> bool {
}
/// Return the effort level declared in `HIVE_DEFAULT_EFFORT` (set from
/// `hyperhive.effortLevel` in `agent.nix`), or `None` if absent / empty.
/// `services.hyperhive.agent.effortLevel` in `agent.nix`), or `None` if absent / empty.
/// Mirrors [`configured_model`]'s env shape. Unlike model, the persisted
/// runtime override takes precedence over this baseline (see `Bus::new`):
/// the operator's effort pick sticks across harness restart, with the nix

View file

@ -119,7 +119,7 @@ impl LoginState {
/// Whether this agent is configured to authenticate to its backend with an
/// API key rather than a Claude OAuth session — set by
/// `hyperhive.useApiKey` via the `HIVE_USE_API_KEY` env var
/// `services.hyperhive.agent.useApiKey` via the `HIVE_USE_API_KEY` env var
/// (`nix/agent-modules/agent-service.nix`). Read fresh on every call rather
/// than cached: it's an env var, not a value worth a `OnceLock` for.
#[must_use]

View file

@ -1,7 +1,7 @@
//! Claude launch-config layer: resolves the agent's tool-group / capability
//! set into the `--allowedTools` / `--tools` argument strings and renders the
//! `--mcp-config` blob claude reads at spawn (built-in hyperhive server +
//! any `hyperhive.extraMcpServers`). Pure config-string generation consumed by
//! any `services.hyperhive.agent.extraMcpServers`). Pure config-string generation consumed by
//! [`crate::turn`] when it builds the claude command. It never touches the
//! running MCP server (a separate binary) — the `send` allow-list check that
//! server enforces lives alongside it in the `hive-agent-mcp` crate.
@ -12,7 +12,7 @@ pub const SERVER_NAME: &str = "hyperhive";
/// Default loopback port the built-in hyperhive MCP surface is served on
/// (streamable HTTP, via the persistent `hive-mcp-http` daemon). Overridable
/// via `hyperhive.mcp.httpPort`; **must match that option's default** in
/// via `services.hyperhive.agent.mcp.httpPort`; **must match that option's default** in
/// `nix/agent-modules/mcp.nix`. Safe as a single fixed value across all
/// agents because each container runs in its own private network namespace,
/// so `127.0.0.1:<port>` is per-container-private (no cross-agent collision).
@ -65,7 +65,7 @@ fn allowed_capability_tools() -> Vec<String> {
/// Tool group an extra (out-of-process) MCP server is gated behind, if any.
///
/// Most `hyperhive.extraMcpServers` entries are ungated — available whenever
/// Most `services.hyperhive.agent.extraMcpServers` entries are ungated — available whenever
/// the operator declares them. The `bash` server is the exception: raw shell
/// execution is a privilege, so it is only exposed when the agent holds the
/// `Execution` tool group. Unlike the in-process hyperhive tools (gated at
@ -135,7 +135,7 @@ pub fn allowed_mcp_tools(groups: &[hive_sh4re::permissions::ToolGroup]) -> Vec<S
.filter(|t| seen.insert(*t))
.map(|t| format!("mcp__{SERVER_NAME}__{t}"))
.collect();
// Extra MCP servers declared via `hyperhive.extraMcpServers` in
// Extra MCP servers declared via `services.hyperhive.agent.extraMcpServers` in
// the agent's NixOS config. Each entry maps its `allowedTools`
// pattern list to `mcp__<server>__<pattern>` so claude can call
// them without per-tool operator approval. `["*"]` (the default)
@ -177,7 +177,7 @@ pub fn allowed_tools_arg() -> String {
/// The built-in `hyperhive` surface is an HTTP entry pointing at the
/// persistent `hive-mcp-http` daemon (see [`DEFAULT_MCP_HTTP_PORT`]); there
/// is no per-turn stdio child for it. Merges in any extra MCP servers
/// declared via `hyperhive.extraMcpServers` — each one is either a
/// declared via `services.hyperhive.agent.extraMcpServers` — each one is either a
/// per-turn stdio bridge or another persistent HTTP entry, per its own
/// `type`.
#[must_use]

View file

@ -1,5 +1,5 @@
//! Boot-time `claude plugin install` driver. Reads the list declared
//! via the `hyperhive.claudePlugins` NixOS option (rendered to
//! via the `services.hyperhive.agent.claudePlugins` NixOS option (rendered to
//! `/etc/hyperhive/claude-plugins.json` by the harness module) and
//! shells out `claude plugin install <spec>` for each entry. Runs once
//! per harness boot before the turn loop; `claude plugin install`
@ -62,7 +62,7 @@ async fn add_marketplaces() {
}
}
/// Read the `hyperhive.claudePluginsAutoUpdate` flag written by the NixOS
/// Read the `services.hyperhive.agent.claudePluginsAutoUpdate` flag written by the NixOS
/// module. Defaults to `false` when the file is absent or unparseable.
async fn auto_update_enabled() -> bool {
match tokio::fs::read_to_string(AUTO_UPDATE_PATH).await {

View file

@ -40,7 +40,7 @@ pub fn render(
.replace("{operator_pronouns}", operator_pronouns)
.replace("{hive_identity}", &hive_identity)
.replace("{swarm_identity}", &swarm_identity);
// When the reference docs are mounted in-container (`hyperhive.docs.enable`
// When the reference docs are mounted in-container (`services.hyperhive.agent.docs.enable`
// wires `HIVE_DOCS_DIR` + `claude --add-dir`), append a single pointer
// sentence so the agent knows they exist. Additive: it doesn't replace the
// agent's own memory/project instructions. Absent env → no change.
@ -129,7 +129,7 @@ pub async fn write_system_prompt(_socket: &Path, label: &str) -> Result<PathBuf>
// verbatim (single-hive deployments see no diff).
let hive_name = crate::identity::hive_name();
let swarm_name = crate::identity::swarm_name();
// `hyperhive.docs.enable` sets HIVE_DOCS_DIR (and the harness passes it to
// `services.hyperhive.agent.docs.enable` sets HIVE_DOCS_DIR (and the harness passes it to
// claude via `--add-dir`); when present, render() appends a pointer line.
let docs_dir = std::env::var("HIVE_DOCS_DIR")
.ok()

View file

@ -122,7 +122,7 @@ impl TurnFiles {
/// Drop the MCP config blob claude reads from `--mcp-config <path>`.
/// The built-in hyperhive surface is served over HTTP by the persistent
/// `hive-mcp-http` daemon, so no per-turn stdio child is spawned; extra
/// servers declared via `hyperhive.extraMcpServers` are still stdio bridges.
/// servers declared via `services.hyperhive.agent.extraMcpServers` are still stdio bridges.
///
/// # Errors
///
@ -576,7 +576,7 @@ fn session_store() -> hive_claude::SessionStore {
/// tool allow-lists and the optional docs `--add-dir`.
fn claude_config(bus: &Bus, files: &TurnFiles) -> Config {
let mut add_dirs = Vec::new();
// hyperhive.docs.enable wires HIVE_DOCS_DIR to the in-container reference
// services.hyperhive.agent.docs.enable wires HIVE_DOCS_DIR to the in-container reference
// docs; expose it as an additional readable directory when set.
if let Some(docs_dir) = std::env::var_os("HIVE_DOCS_DIR")
&& !docs_dir.is_empty()

View file

@ -92,7 +92,7 @@ pub async fn serve(
.map(PathBuf::from)
.context(
"HIVE_STATIC_DIR env var not set — point it at the merged \
per-agent dist (see hyperhive.frontend.mergedDist in nix)",
per-agent dist (see services.hyperhive.agent.frontend.mergedDist in nix)",
)?;
if !static_dir.is_dir() {
anyhow::bail!(
@ -130,7 +130,7 @@ pub async fn serve(
.route("/api/stats", get(stats::api_stats))
.route("/screen/ws", get(screen::screen_ws))
.route("/icon", get(screen::serve_icon));
// Mount any `hyperhive.extraWebProxies` under `/extra/<name>/` before the
// Mount any `services.hyperhive.agent.extraWebProxies` under `/extra/<name>/` before the
// static fallback so declared proxies win over `ServeDir`.
let app = proxy::mount_extra_proxies(app)
// Anything else (`/`, `/stats`, `/screen`, `/static/*`)
@ -139,7 +139,7 @@ pub async fn serve(
// (so `/stats` → `dist/stats.html`, `/screen` → `dist/
// screen.html`). Per-agent `extraFiles` additions are
// already layered into this same directory (see
// hyperhive.frontend.mergedDist in nix).
// services.hyperhive.agent.frontend.mergedDist in nix).
.fallback_service(ServeDir::new(&static_dir))
.with_state(state);
// `HIVE_WEB_SOCKET` opt-in: when set + non-empty, bind a

View file

@ -1,4 +1,4 @@
//! Extra web proxies (`HIVE_EXTRA_WEB_PROXIES` / `hyperhive.extraWebProxies`).
//! Extra web proxies (`HIVE_EXTRA_WEB_PROXIES` / `services.hyperhive.agent.extraWebProxies`).
//!
//! Each declared entry mounts a transparent reverse-proxy at `/extra/<name>/`
//! in the per-agent web UI, forwarding every request (method, headers, body)

View file

@ -10,7 +10,7 @@ use tokio::io::{AsyncReadExt, AsyncWriteExt};
use super::AppState;
/// This agent's icon. Serves the operator-configured SVG from
/// `/etc/hyperhive/icon.svg` (set via the `hyperhive.icon` agent.nix
/// `/etc/hyperhive/icon.svg` (set via the `services.hyperhive.agent.icon` agent.nix
/// option) when present, otherwise **404** — there is no bundled
/// server-side default any more (that was `hive_sh4re::assets::
/// branding_svg`, now removed). Consumers fall back client-side: the

View file

@ -255,7 +255,7 @@ enum AgentLinkKind {
/// `http://<host>:3000<url>`.
Forge,
/// `url` is already a fully-qualified absolute URL — use as-is.
/// Agent-declared `hyperhive.dashboardLinks` extras arrive here.
/// Agent-declared `services.hyperhive.agent.dashboardLinks` extras arrive here.
External,
}
@ -366,7 +366,7 @@ fn agent_links(label: &str, gui_enabled: bool) -> Vec<AgentLink> {
});
}
// Agent-declared extras (`hyperhive.dashboardLinks` → the
// Agent-declared extras (`services.hyperhive.agent.dashboardLinks` → the
// `hive-dashboard-links` NixOS oneshot writes them to
// `{state_dir}/hyperhive-dashboard-links.json`). Shape on disk
// is `{label, icon, url}` with absolute URLs — those become

View file

@ -202,7 +202,7 @@ pub async fn build_all(hive: &crate::coordinator::HiveEnv) -> Vec<ContainerView>
// stale sentinel state is not meaningful when the harness isn't up.
//
// The first half doesn't apply to an api-key agent
// (`hyperhive.useApiKey`, stamped as `api_key_mode` in the
// (`services.hyperhive.agent.useApiKey`, stamped as `api_key_mode` in the
// consolidated state file by `hive_agent::harness_state::write_api_key_mode`):
// its `~/.claude/` is empty by design (no OAuth flow to complete),
// so an empty dir there means nothing — only the auth-failed

View file

@ -47,7 +47,7 @@ pub struct Coordinator {
pub hyperhive_flake: String,
/// URL of the narrow `docs/` source (no fragment). Inlined into the
/// meta `flake.nix` as `inputs.hyperhive-docs.url` and threaded to
/// each agent as `hyperhive.docs.source`. Its own store path
/// each agent as `services.hyperhive.agent.docs.source`. Its own store path
/// so doc edits don't re-hash `hyperhive_flake`.
pub hyperhive_docs_flake: String,
/// Store-path URL of the nixpkgs to wire into the meta flake as
@ -59,7 +59,7 @@ pub struct Coordinator {
/// `follows = "hyperhive/nixpkgs"` behaviour.
pub nixpkgs_flake: String,
/// Store path of the `claude-code` build every agent runs, written
/// into each per-agent flake as `hyperhive.claudeCodePath`. Set by
/// into each per-agent flake as `services.hyperhive.agent.claudeCodePath`. Set by
/// the NixOS module option `services.hyperhive.c0re.claudeCodePackage`
/// (which resolves the package and hands us its path). `None` = every
/// agent keeps the `claude-code` from its own nixpkgs.
@ -228,13 +228,13 @@ pub struct HiveEnv {
pub hyperhive_flake: String,
/// Store-path URL of the narrow `docs/` source, wired into the meta
/// flake as `inputs.hyperhive-docs.url` and threaded to each agent as
/// `hyperhive.docs.source`. Separate from `hyperhive_flake`
/// `services.hyperhive.agent.docs.source`. Separate from `hyperhive_flake`
/// so a doc edit only re-locks this input, not the whole source.
pub hyperhive_docs_flake: String,
pub nixpkgs_flake: String,
/// Store path of the `claude-code` agents run, or `None` for "each
/// agent keeps the one out of its own nixpkgs". Travels into the
/// container as `hyperhive.claudeCodePath` — a plain string, kept
/// container as `services.hyperhive.agent.claudeCodePath` — a plain string, kept
/// alive host-side by the module that resolved it.
pub claude_code_path: Option<String>,
pub dashboard_port: u16,

View file

@ -739,21 +739,27 @@ const FORWARDED_VAR_OPTIONS: &[(&str, &str)] = &[
// whose option says no queue coordinates and whose env says otherwise logs the
// partial-config error rather than half-connecting, which is the same
// failure this map's doc describes and the reason both halves are wired.
("HIVE_AGENT_NATS_URL", "hyperhive.queue.natsUrl"),
(
"HIVE_AGENT_NATS_URL",
"services.hyperhive.agent.queue.natsUrl",
),
(
"HIVE_AGENT_OIDC_TOKEN_ENDPOINT",
"hyperhive.queue.tokenEndpoint",
"services.hyperhive.agent.queue.tokenEndpoint",
),
// Read at build time to decide whether `swarm-logs` is installed at all —
// see `nix/agent-modules/logs.nix`. Both halves wired for this map's own
// reason: an agent whose option says no log store and whose env says
// otherwise has a binary that is not on PATH and coordinates that claim
// it is.
("HIVE_AGENT_LOGS_QUERY_URL", "hyperhive.logs.queryUrl"),
("HIVE_FORGE_URL", "hyperhive.forge.url"),
("HIVE_MATRIX_URL", "hyperhive.matrix.url"),
("HYPERHIVE_HIVE_NAME", "hyperhive.hiveName"),
("HYPERHIVE_SWARM_NAME", "hyperhive.swarmName"),
(
"HIVE_AGENT_LOGS_QUERY_URL",
"services.hyperhive.agent.logs.queryUrl",
),
("HIVE_FORGE_URL", "services.hyperhive.agent.forge.url"),
("HIVE_MATRIX_URL", "services.hyperhive.agent.matrix.url"),
("HYPERHIVE_HIVE_NAME", "services.hyperhive.agent.hiveName"),
("HYPERHIVE_SWARM_NAME", "services.hyperhive.agent.swarmName"),
];
/// Render the forwarded-var option assignments for one agent's module block.
@ -874,17 +880,17 @@ struct OtelConfig {
protocol: String,
extra_resource_attributes: Option<String>,
metric_interval_ms: Option<u64>,
/// `HYPERHIVE_OTEL_DEBUG=1` → `hyperhive.otel.debug = true` →
/// `HYPERHIVE_OTEL_DEBUG=1` → `services.hyperhive.agent.otel.debug = true` →
/// `CLAUDE_CODE_OTEL_DIAG_STDERR=1` in every agent's env.
debug: bool,
}
/// Read the hive-wide OTEL config from env, or `None` when OTEL is off.
/// Mirrors `hive_ca_source` — host state surfaced to the meta renderer
/// so it can bake build-time `hyperhive.otel.*` config into each agent
/// so it can bake build-time `services.hyperhive.agent.otel.*` config into each agent
/// (the per-agent options the harness modules consume). Returns `None`
/// when the endpoint signal is absent so the renderer emits no
/// `hyperhive.otel.*` lines and agents keep the disabled default.
/// `services.hyperhive.agent.otel.*` lines and agents keep the disabled default.
fn otel_config() -> Option<OtelConfig> {
let endpoint = std::env::var("HYPERHIVE_OTEL_ENDPOINT")
.ok()
@ -1101,9 +1107,9 @@ where
}
// Narrow `docs/` source as its own input so a doc edit only
// re-locks THIS input instead of re-hashing the whole `hyperhive`
// source. Threaded to each agent below as `hyperhive.docs.source`.
// source. Threaded to each agent below as `services.hyperhive.agent.docs.source`.
// Empty = hive-c0re not built with the option wired up (legacy);
// agents then keep the harness default (`hyperhive.packages.reference-docs`).
// agents then keep the harness default (`services.hyperhive.agent.packages.reference-docs`).
if !docs_flake.is_empty() {
// `flake = false`: the docs/ tree is a plain source (no flake.nix),
// so nix must treat it as raw source, not evaluate it as a flake.
@ -1169,11 +1175,11 @@ where
);
// Point the in-container docs dir (`$HIVE_DOCS_DIR`) at the narrow
// `hyperhive-docs` input instead of the harness default
// (`hyperhive.packages.reference-docs`, built from the now-docs-stripped source).
// (`services.hyperhive.agent.packages.reference-docs`, built from the now-docs-stripped source).
// `inputs."hyperhive-docs"` is reachable via the outputs `@inputs`
// capture. Emitted only when the input exists (docs_flake non-empty).
if !docs_flake.is_empty() {
out.push_str(" hyperhive.docs.source = inputs.\"hyperhive-docs\".outPath;\n");
out.push_str(" services.hyperhive.agent.docs.source = inputs.\"hyperhive-docs\".outPath;\n");
}
// The `claude-code` agents run, as a bare store path rather than a
// flake input: containers share the host's `/nix/store`, so the
@ -1185,7 +1191,10 @@ where
// spelled out here is text and references nothing.
// `None` = no override; agents keep their own nixpkgs' `claude-code`.
if let Some(path) = claude_code_path {
let _ = writeln!(out, " hyperhive.claudeCodePath = \"{path}\";");
let _ = writeln!(
out,
" services.hyperhive.agent.claudeCodePath = \"{path}\";"
);
}
// CA trust: embed the hive's trust anchors so each agent validates
// them at build time — the hive's own self-signed CA when active,
@ -1209,41 +1218,44 @@ where
);
}
// Hive-wide OTEL stats export (`services.hyperhive.otel.*`): inject the
// build-time `hyperhive.otel.*` config the harness modules consume (its
// build-time `services.hyperhive.agent.otel.*` config the harness modules consume (its
// otelEnv + otelExecStart wrapper + LoadCredential). Host-driven, so
// the same config lands on every agent; emitted only when enabled.
// Mirrors the CA-cert injection above — host state -> build-time agent
// module config.
if let Some(otel) = otel_config() {
let esc = |s: &str| s.replace('\\', "\\\\").replace('"', "\\\"");
out.push_str(" hyperhive.otel.enable = true;\n");
out.push_str(" services.hyperhive.agent.otel.enable = true;\n");
let _ = writeln!(
out,
" hyperhive.otel.endpoint = \"{}\";",
" services.hyperhive.agent.otel.endpoint = \"{}\";",
esc(&otel.endpoint)
);
let _ = writeln!(
out,
" hyperhive.otel.protocol = \"{}\";",
" services.hyperhive.agent.otel.protocol = \"{}\";",
esc(&otel.protocol)
);
if let Some(attrs) = &otel.extra_resource_attributes {
let _ = writeln!(
out,
" hyperhive.otel.extraResourceAttributes = \"{}\";",
" services.hyperhive.agent.otel.extraResourceAttributes = \"{}\";",
esc(attrs)
);
}
if let Some(ms) = otel.metric_interval_ms {
// Int option — emit a bare numeric literal (no quotes). `ms` is a
// parsed u64, so it can't inject anything into the rendered nix.
let _ = writeln!(out, " hyperhive.otel.metricIntervalMs = {ms};");
let _ = writeln!(
out,
" services.hyperhive.agent.otel.metricIntervalMs = {ms};"
);
}
if otel.debug {
out.push_str(" hyperhive.otel.debug = true;\n");
out.push_str(" services.hyperhive.agent.otel.debug = true;\n");
}
}
// Agent-facing service URLs (`hyperhive.forge.url`, `hyperhive.matrix.url`):
// Agent-facing service URLs (`services.hyperhive.agent.forge.url`, `services.hyperhive.agent.matrix.url`):
// emit the host's real values as build-time agent config, the same
// host-state -> agent-module shape as the otel block above.
//
@ -1271,21 +1283,21 @@ where
// operation the tests exercise directly.
push_forwarded_var_options(&mut out, &forwarded_env_vars());
// GitHub integration is on by default in every agent
// (`hyperhive.github.enable`); the host turns it off hive-wide via
// (`services.hyperhive.agent.github.enable`); the host turns it off hive-wide via
// `services.hyperhive.github.enable = false`, surfaced here as the
// `HYPERHIVE_GITHUB_DISABLED` env on hive-c0re's unit. Only the OFF
// override is propagated — the enabled default needs no per-agent line.
if std::env::var_os("HYPERHIVE_GITHUB_DISABLED").is_some() {
out.push_str(" hyperhive.github.enable = false;\n");
out.push_str(" services.hyperhive.agent.github.enable = false;\n");
}
out.push_str(
r#" # The harness service inside the container runs as a
# non-root unix user named after the agent (`damocles`,
# `iris`, `root`, …). UID auto-assigned by NixOS; the
# per-agent override here is what makes
# `hyperhive.user.name` match the agent's identity
# `services.hyperhive.agent.user.name` match the agent's identity
# instead of the harness default of `"agent"`.
hyperhive.user.name = name;
services.hyperhive.agent.user.name = name;
# Without this an agent container is called `nixos` and ships
# every log line under that: nixpkgs sets the hostname from the
# attr name in the `containers.<name>` submodule, and an
@ -1295,7 +1307,7 @@ where
# nspawn itself already call this container, so logs join to
# both without a prefix transform.
networking.hostName = "h-${name}";
hyperhive.claudeMemoryMaxBytes = memoryMaxBytes;
services.hyperhive.agent.claudeMemoryMaxBytes = memoryMaxBytes;
programs.git.config.user = {
name = name;
email = "${name}@hyperhive.local";
@ -1418,7 +1430,7 @@ where
// Effective `MemoryMax=` for this agent (per-agent override, else
// the hive-wide default), turned into a raw byte count so
// the agent's claude-settings module can derive a JSC heap ceiling
// (see `hyperhive.claudeMemoryMaxBytes`). `null` when the
// (see `services.hyperhive.agent.claudeMemoryMaxBytes`). `null` when the
// effective value is `"infinity"` or a RAM percentage — no
// byte count to derive, dependent env var stays unset, same as
// today's no-cap behavior.
@ -1759,7 +1771,7 @@ mod tests {
"old-style follows must not appear when flake args are set:\n{out}"
);
// the narrow docs source is its own non-flake input, and each
// agent's docs dir resolves from it rather than hyperhive.packages.reference-docs.
// agent's docs dir resolves from it rather than services.hyperhive.agent.packages.reference-docs.
assert!(
out.contains("hyperhive-docs.url = \"path:/nix/store/bbbb-hyperhive-docs-source\""),
"expected hyperhive-docs input url:\n{out}"
@ -1769,7 +1781,9 @@ mod tests {
"docs source is not a flake, must be flake = false:\n{out}"
);
assert!(
out.contains("hyperhive.docs.source = inputs.\"hyperhive-docs\".outPath;"),
out.contains(
"services.hyperhive.agent.docs.source = inputs.\"hyperhive-docs\".outPath;"
),
"expected per-agent docs source wired to the input:\n{out}"
);
}
@ -1777,7 +1791,7 @@ mod tests {
#[test]
fn render_flake_omits_docs_input_when_docs_flake_empty() {
// Legacy / not-wired-up: empty docs_flake emits no docs input and
// leaves each agent on the harness default (hyperhive.packages.reference-docs).
// leaves each agent on the harness default (services.hyperhive.agent.packages.reference-docs).
let out = render_flake(
"github:example/hyperhive",
"",
@ -1850,7 +1864,9 @@ mod tests {
&[sample_spec("alice", false, 9001)],
);
assert!(
out.contains("hyperhive.claudeCodePath = \"/nix/store/cccc-claude-code-2.1.220\";"),
out.contains(
"services.hyperhive.agent.claudeCodePath = \"/nix/store/cccc-claude-code-2.1.220\";"
),
"claude path assigned as a plain string literal:\n{out}"
);
assert!(
@ -2046,9 +2062,9 @@ mod tests {
);
assert_eq!(
out,
" hyperhive.forge.url = \"http://forge.example.test\";\n\
\x20 hyperhive.matrix.url = \"http://matrix.example.test\";\n\
\x20 hyperhive.hiveName = \"pr1ma\";\n",
" services.hyperhive.agent.forge.url = \"http://forge.example.test\";\n\
\x20 services.hyperhive.agent.matrix.url = \"http://matrix.example.test\";\n\
\x20 services.hyperhive.agent.hiveName = \"pr1ma\";\n",
"expected exactly the mapped options, in input order, indented for the module block"
);
}
@ -2079,8 +2095,8 @@ mod tests {
);
assert_eq!(
out,
" hyperhive.queue.natsUrl = \"nats://10.42.0.1:4222\";\n\
\x20 hyperhive.queue.tokenEndpoint = \"https://auth.t.local/api/oidc/token\";\n"
" services.hyperhive.agent.queue.natsUrl = \"nats://10.42.0.1:4222\";\n\
\x20 services.hyperhive.agent.queue.tokenEndpoint = \"https://auth.t.local/api/oidc/token\";\n"
);
}
@ -2184,11 +2200,11 @@ mod tests {
std::env::remove_var("HIVE_MATRIX_URL");
}
let forge_opt_at = out
.find("hyperhive.forge.url = \"http://forge.example.test\"")
.expect("hyperhive.forge.url must be rendered from HIVE_FORGE_URL");
.find("services.hyperhive.agent.forge.url = \"http://forge.example.test\"")
.expect("services.hyperhive.agent.forge.url must be rendered from HIVE_FORGE_URL");
let matrix_opt_at = out
.find("hyperhive.matrix.url = \"http://matrix.example.test\"")
.expect("hyperhive.matrix.url must be rendered from HIVE_MATRIX_URL");
.find("services.hyperhive.agent.matrix.url = \"http://matrix.example.test\"")
.expect("services.hyperhive.agent.matrix.url must be rendered from HIVE_MATRIX_URL");
let env_block_at = out
.find("environment.variables = {")
.expect("per-agent environment.variables block must exist");
@ -2332,8 +2348,8 @@ mod tests {
#[test]
fn render_flake_injects_otel_when_signalled() {
// services.hyperhive.otel.* -> HYPERHIVE_OTEL_* on hive-c0re's unit
// -> injected as build-time hyperhive.otel.* into every agent. With
// no endpoint signal, no hyperhive.otel lines are emitted (agents
// -> injected as build-time services.hyperhive.agent.otel.* into every agent. With
// no endpoint signal, no services.hyperhive.agent.otel lines are emitted (agents
// keep the the harness modules disabled default).
//
// Serialised against every other env-mutating test in the crate.
@ -2376,32 +2392,33 @@ mod tests {
let off = render();
assert!(
on_minimal.contains("hyperhive.otel.enable = true;"),
on_minimal.contains("services.hyperhive.agent.otel.enable = true;"),
"otel enable must be injected:\n{on_minimal}"
);
assert!(
on_minimal.contains("hyperhive.otel.endpoint = \"https://c.example/otel\";"),
on_minimal
.contains("services.hyperhive.agent.otel.endpoint = \"https://c.example/otel\";"),
"otel endpoint must be injected:\n{on_minimal}"
);
assert!(
on_minimal.contains("hyperhive.otel.protocol = \"grpc\";"),
on_minimal.contains("services.hyperhive.agent.otel.protocol = \"grpc\";"),
"otel protocol must be injected:\n{on_minimal}"
);
// Optional fields absent when unset.
assert!(
!on_minimal.contains("hyperhive.otel.extraResourceAttributes"),
!on_minimal.contains("services.hyperhive.agent.otel.extraResourceAttributes"),
"extraResourceAttributes must not appear when unset:\n{on_minimal}"
);
assert!(
on_full.contains(
"hyperhive.otel.extraResourceAttributes = \"deployment.environment=prod\";"
"services.hyperhive.agent.otel.extraResourceAttributes = \"deployment.environment=prod\";"
),
"extraResourceAttributes must be injected when set:\n{on_full}"
);
assert!(
!off.contains("hyperhive.otel"),
!off.contains("services.hyperhive.agent.otel"),
"no otel lines when disabled:\n{off}"
);
}
@ -2409,7 +2426,7 @@ mod tests {
#[test]
fn render_flake_injects_github_disable_only_when_signalled() {
// services.hyperhive.github.enable = false -> HYPERHIVE_GITHUB_DISABLED
// on hive-c0re's unit -> `hyperhive.github.enable = false` injected into
// on hive-c0re's unit -> `services.hyperhive.agent.github.enable = false` injected into
// every agent. On by default, so nothing is emitted unless disabled.
//
// Serialised against every other env-mutating test in the crate — the
@ -2441,11 +2458,11 @@ mod tests {
std::env::remove_var("HYPERHIVE_GITHUB_DISABLED");
}
assert!(
!on_default.contains("hyperhive.github.enable"),
!on_default.contains("services.hyperhive.agent.github.enable"),
"github.enable must not be emitted by default (agents keep the true default):\n{on_default}"
);
assert!(
disabled.contains("hyperhive.github.enable = false;"),
disabled.contains("services.hyperhive.agent.github.enable = false;"),
"github.enable = false must be injected when the host disables it:\n{disabled}"
);
}

View file

@ -403,7 +403,7 @@ pub async fn write_agent_matrix_token(
/// (running as root). Writes `<state>/github-token` 0600, chowned to the agent
/// user so the `gh` wrapper / git credential helper can read it from inside the
/// container. Single account per agent — no account suffix. The token value is
/// operator-supplied (for the agent's GitHub integration, `hyperhive.github.enable`).
/// operator-supplied (for the agent's GitHub integration, `services.hyperhive.agent.github.enable`).
///
/// # Errors
///

View file

@ -8,7 +8,7 @@
//! `<state>/matrix-sdk-state`) and the daemon-wide `HIVE_MATRIX_URL`.
//! Any **extra** accounts come from the `HIVE_MATRIX_ACCOUNTS` env var
//! (JSON, written by the nix harness module from
//! `hyperhive.matrixAccounts`) and are appended after `main`.
//! `services.hyperhive.agent.matrixAccounts`) and are appended after `main`.
//!
//! So a single-account agent (no `HIVE_MATRIX_ACCOUNTS`) gets exactly
//! `main` — zero config, same behaviour as before: omitting `account` on

View file

@ -271,7 +271,7 @@ async fn bring_up_account(
) -> Result<Option<(Client, SyncLoop)>> {
let Some(homeserver) = cfg.homeserver() else {
// No homeserver for this account: the hive has none to offer (no
// matrix vhost) or this agent's `hyperhive.matrix.url` is null. Same
// matrix vhost) or this agent's `services.hyperhive.agent.matrix.url` is null. Same
// no-op as a missing token — an absent integration, not a guess at
// one.
tracing::info!(

View file

@ -11,7 +11,7 @@
//!
//! Multi-account: every tool carries an optional `account` arg naming
//! which matrix account to act as (a `name` from
//! `hyperhive.matrixAccounts`); omitting it selects the agent's primary
//! `services.hyperhive.agent.matrixAccounts`); omitting it selects the agent's primary
//! account (or errors, listing the choices, when more than one account
//! is configured — see [`crate::accounts::Registry::resolve`]).
@ -49,7 +49,7 @@ struct SendMessageArgs {
/// Message body. Markdown is rendered to HTML by the daemon
/// (`text_markdown`); plain text passes through unchanged.
body: String,
/// Matrix account to act as (a `name` from `hyperhive.matrixAccounts`).
/// Matrix account to act as (a `name` from `services.hyperhive.agent.matrixAccounts`).
/// Omit to use the agent's primary account.
#[serde(default)]
account: Option<String>,
@ -62,7 +62,7 @@ struct SendDmArgs {
/// the recipient.
user_id: String,
body: String,
/// Matrix account to act as (a `name` from `hyperhive.matrixAccounts`).
/// Matrix account to act as (a `name` from `services.hyperhive.agent.matrixAccounts`).
/// Omit to use the agent's primary account.
#[serde(default)]
account: Option<String>,
@ -79,7 +79,7 @@ struct SendFileArgs {
/// Optional caption, sent as a follow-up text message in the room.
#[serde(default)]
caption: Option<String>,
/// Matrix account to act as (a `name` from `hyperhive.matrixAccounts`).
/// Matrix account to act as (a `name` from `services.hyperhive.agent.matrixAccounts`).
/// Omit to use the agent's primary account.
#[serde(default)]
account: Option<String>,
@ -90,7 +90,7 @@ struct OpenDmArgs {
/// Matrix user id (`@user:server`) to open a DM with. The DM room is
/// created if one doesn't already exist.
user_id: String,
/// Matrix account to act as (a `name` from `hyperhive.matrixAccounts`).
/// Matrix account to act as (a `name` from `services.hyperhive.agent.matrixAccounts`).
/// Omit to use the agent's primary account.
#[serde(default)]
account: Option<String>,
@ -104,7 +104,7 @@ struct SendReactionArgs {
/// Reaction key — usually an emoji (`👍`, `❤️`) but any string
/// works per matrix spec.
key: String,
/// Matrix account to act as (a `name` from `hyperhive.matrixAccounts`).
/// Matrix account to act as (a `name` from `services.hyperhive.agent.matrixAccounts`).
/// Omit to use the agent's primary account.
#[serde(default)]
account: Option<String>,
@ -115,7 +115,7 @@ struct SendReplyArgs {
room: String,
event_id: String,
body: String,
/// Matrix account to act as (a `name` from `hyperhive.matrixAccounts`).
/// Matrix account to act as (a `name` from `services.hyperhive.agent.matrixAccounts`).
/// Omit to use the agent's primary account.
#[serde(default)]
account: Option<String>,
@ -125,7 +125,7 @@ struct SendReplyArgs {
struct MarkReadArgs {
room: String,
event_id: String,
/// Matrix account to act as (a `name` from `hyperhive.matrixAccounts`).
/// Matrix account to act as (a `name` from `services.hyperhive.agent.matrixAccounts`).
/// Omit to use the agent's primary account.
#[serde(default)]
account: Option<String>,
@ -139,7 +139,7 @@ struct SendRedactArgs {
/// Optional human-readable reason recorded on the redaction event.
#[serde(default)]
reason: Option<String>,
/// Matrix account to act as (a `name` from `hyperhive.matrixAccounts`).
/// Matrix account to act as (a `name` from `services.hyperhive.agent.matrixAccounts`).
/// Omit to use the agent's primary account.
#[serde(default)]
account: Option<String>,
@ -147,7 +147,7 @@ struct SendRedactArgs {
#[derive(Debug, Deserialize, JsonSchema)]
struct ListRoomsArgs {
/// Matrix account to act as (a `name` from `hyperhive.matrixAccounts`).
/// Matrix account to act as (a `name` from `services.hyperhive.agent.matrixAccounts`).
/// Omit to use the agent's primary account.
#[serde(default)]
account: Option<String>,
@ -156,7 +156,7 @@ struct ListRoomsArgs {
#[derive(Debug, Deserialize, JsonSchema)]
struct ListRoomMembersArgs {
room: String,
/// Matrix account to act as (a `name` from `hyperhive.matrixAccounts`).
/// Matrix account to act as (a `name` from `services.hyperhive.agent.matrixAccounts`).
/// Omit to use the agent's primary account.
#[serde(default)]
account: Option<String>,
@ -179,7 +179,7 @@ struct ReadRoomArgs {
/// Mutually exclusive with `from`.
#[serde(default)]
until: Option<String>,
/// Matrix account to act as (a `name` from `hyperhive.matrixAccounts`).
/// Matrix account to act as (a `name` from `services.hyperhive.agent.matrixAccounts`).
/// Omit to use the agent's primary account.
#[serde(default)]
account: Option<String>,
@ -196,7 +196,7 @@ struct DownloadFileArgs {
/// named after the attachment; the returned `path` is where to read it.
#[serde(default)]
dest_path: Option<String>,
/// Matrix account to act as (a `name` from `hyperhive.matrixAccounts`).
/// Matrix account to act as (a `name` from `services.hyperhive.agent.matrixAccounts`).
/// Omit to use the agent's primary account.
#[serde(default)]
account: Option<String>,
@ -204,7 +204,7 @@ struct DownloadFileArgs {
#[derive(Debug, Deserialize, JsonSchema)]
struct ListInvitesArgs {
/// Matrix account to act as (a `name` from `hyperhive.matrixAccounts`).
/// Matrix account to act as (a `name` from `services.hyperhive.agent.matrixAccounts`).
/// Omit to use the agent's primary account.
#[serde(default)]
account: Option<String>,
@ -214,7 +214,7 @@ struct ListInvitesArgs {
struct JoinRoomArgs {
/// Matrix room id (`!abc:server`) or canonical alias (`#name:server`).
room: String,
/// Matrix account to act as (a `name` from `hyperhive.matrixAccounts`).
/// Matrix account to act as (a `name` from `services.hyperhive.agent.matrixAccounts`).
/// Omit to use the agent's primary account.
#[serde(default)]
account: Option<String>,
@ -228,7 +228,7 @@ struct ResolveInviteArgs {
/// What to do with the invite: `"accept"` (join the room) or
/// `"reject"` (decline and leave).
action: String,
/// Matrix account to act as (a `name` from `hyperhive.matrixAccounts`).
/// Matrix account to act as (a `name` from `services.hyperhive.agent.matrixAccounts`).
/// Omit to use the agent's primary account.
#[serde(default)]
account: Option<String>,
@ -241,7 +241,7 @@ struct InviteUserArgs {
room: String,
/// Matrix user id of the invitee (`@user:server`).
user_id: String,
/// Matrix account to act as (a `name` from `hyperhive.matrixAccounts`).
/// Matrix account to act as (a `name` from `services.hyperhive.agent.matrixAccounts`).
/// Omit to use the agent's primary account.
#[serde(default)]
account: Option<String>,

View file

@ -2,7 +2,7 @@
//!
//! All paths are overridable via env vars for dev / test scenarios and
//! to let the harness point the daemon at non-default locations when
//! the operator overrides `hyperhive.matrix.*` options.
//! the operator overrides `services.hyperhive.agent.matrix.*` options.
use std::path::PathBuf;

View file

@ -510,7 +510,7 @@ pub enum PrivRequest {
/// Write `github-token` into `AGENT_STATE_ROOT/<agent_name>/state/github-token`.
///
/// The operator-supplied GitHub personal access token (PAT) for the
/// agent's GitHub integration (`hyperhive.github.enable`). Same write
/// agent's GitHub integration (`services.hyperhive.agent.github.enable`). Same write
/// semantics as
/// `WriteAgentForgeToken` — validates `agent_name`, creates the state dir
/// if absent, writes the file 0600, and chowns it to the agent so the

View file

@ -2812,7 +2812,7 @@ fn validate_name_chars(name: &str) -> Result<()> {
/// [`validate_name_chars`] allows on purpose.
///
/// An agent name is an `Ident` and lowercase by design. An account name is an
/// attribute name in `hyperhive.matrixAccounts`, typed `attrsOf` with no
/// attribute name in `services.hyperhive.agent.matrixAccounts`, typed `attrsOf` with no
/// charset constraint, so `Ops_Relay9` is a key an operator may already have
/// written. `swarm_secret_client::path::checked_segment` accepts exactly this
/// set for the same name in the secret store — the two must agree, or a
@ -3502,7 +3502,7 @@ mod tests {
/// Ported from `hive-c0re`'s `credential.rs`, which used to build this path
/// itself. The controls are the load-bearing half: an account name is an
/// attrset key in `hyperhive.matrixAccounts`, so uppercase and underscore
/// attrset key in `services.hyperhive.agent.matrixAccounts`, so uppercase and underscore
/// are names an operator can already have written, and the secret store
/// accepts exactly this set for the same name. A validator narrower than
/// the store's reads a credential out and then refuses to land it.

View file

@ -1,7 +1,7 @@
//! `hive-screen-mcp` — stdio MCP bridge for GUI agents.
//!
//! Provides screenshot, keyboard-input, and mouse tools for agents running a
//! Weston Wayland compositor (`hyperhive.gui.enable = true`). Each tool:
//! Weston Wayland compositor (`services.hyperhive.agent.gui.enable = true`). Each tool:
//!
//! - `screenshot` → `grim` (always available with gui enabled)
//! - `type_text` → `wtype` (Wayland virtual-keyboard protocol, no daemon)
@ -18,7 +18,7 @@
//!
//! `WAYLAND_DISPLAY` and `XDG_RUNTIME_DIR` are injected globally by the
//! `weston-vnc` module. `HIVE_GUI_VNC_PORT` is set by the harness service
//! when `hyperhive.gui.enable = true` (defaults to 5900 if absent).
//! when `services.hyperhive.agent.gui.enable = true` (defaults to 5900 if absent).
use anyhow::Result;
use rmcp::{
@ -203,7 +203,7 @@ impl ScreenMcp {
description = "Take a screenshot of the agent's Wayland display and save it as a PNG \
file under `/tmp/`. Returns the path to the saved file — pass that path to the \
`Read` tool to view the image visually. Requires `grim` and a live \
`WAYLAND_DISPLAY` (provided automatically when `hyperhive.gui.enable = true`)."
`WAYLAND_DISPLAY` (provided automatically when `services.hyperhive.agent.gui.enable = true`)."
)]
async fn screenshot(&self) -> String {
let ms = std::time::SystemTime::now()
@ -222,7 +222,7 @@ impl ScreenMcp {
injection). Supports arbitrary Unicode. For special keys (Enter, Tab, Escape, \
function keys, arrow keys) or modifier combos, use `key_press` instead. \
Requires a live `WAYLAND_DISPLAY` (provided automatically when \
`hyperhive.gui.enable = true`)."
`services.hyperhive.agent.gui.enable = true`)."
)]
async fn type_text(&self, Parameters(args): Parameters<TypeTextArgs>) -> String {
cmd_result(run_cmd("wtype", &[&args.text]).await)
@ -236,7 +236,7 @@ impl ScreenMcp {
with `+`-separated parts where all but the last are modifiers (`ctrl+c`, \
`ctrl+shift+t`, `super+l`, `alt+F4`). Modifier names: `ctrl`, `shift`, `alt`, \
`super`. Requires a live `WAYLAND_DISPLAY` (provided automatically when \
`hyperhive.gui.enable = true`)."
`services.hyperhive.agent.gui.enable = true`)."
)]
async fn key_press(&self, Parameters(args): Parameters<KeyPressArgs>) -> String {
// Parse "mod1+mod2+key" into: -M mod1 -M mod2 -k key -m mod2 -m mod1

View file

@ -1,5 +1,5 @@
//! Builds a subagent's own filtered `--mcp-config`: only
//! `hyperhive.extraMcpServers` entries with `availableToSubagents = true`
//! `services.hyperhive.agent.extraMcpServers` entries with `availableToSubagents = true`
//! (see `hive_agent_sock::extra_mcp::ExtraMcpServer::available_to_subagents`) ever reach
//! a subagent's claude invocation, plus this daemon's own two-tool signal
//! surface. Everything else — the built-in hyperhive

View file

@ -814,7 +814,7 @@ fn validate_name(name: &str) -> anyhow::Result<()> {
.map_err(|e| anyhow::anyhow!("invalid subagent name {name:?}: {e}"))
}
/// Env var carrying the nix-configured `hyperhive.availableModels` as a
/// Env var carrying the nix-configured `services.hyperhive.agent.availableModels` as a
/// comma-separated list, forwarded onto this daemon's unit by
/// `nix/agent-modules/mcp.nix` the same way `HIVE_TOOL_GROUPS` is.
const AVAILABLE_MODELS_ENV: &str = "HIVE_AVAILABLE_MODELS";

View file

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

View file

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

View file

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

View file

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

View file

@ -4,9 +4,9 @@
# they came from.
#
# This is the core module: container plumbing (boot/nix/nixpkgs),
# base tooling, and the cross-cutting `hyperhive.icon` option. Each
# base tooling, and the cross-cutting `services.hyperhive.agent.icon` option. Each
# feature lives in its own sibling module (imported below) that
# declares its own `hyperhive.*` options + config.
# declares its own `services.hyperhive.agent.*` options + config.
{
pkgs,
lib,
@ -35,6 +35,7 @@
./otel.nix
./packages.nix
./queue.nix
./renamed-options.nix
./user.nix
./screen.nix
./weston-vnc.nix
@ -48,7 +49,7 @@
'')
];
options.hyperhive.icon = lib.mkOption {
options.services.hyperhive.agent.icon = lib.mkOption {
type = lib.types.nullOr lib.types.path;
default = null;
example = lib.literalExpression "./icon.svg";
@ -65,7 +66,7 @@
'';
};
options.hyperhive.claudeCodePath = lib.mkOption {
options.services.hyperhive.agent.claudeCodePath = lib.mkOption {
type = lib.types.nullOr lib.types.str;
default = null;
example = "/nix/store/…-claude-code-2.1.220";
@ -108,20 +109,22 @@
_module.args.flakeInputs = builtins.removeAttrs inputs [ "self" ];
'';
}
# hyperhive.icon must reference an SVG file when set.
# services.hyperhive.agent.icon must reference an SVG file when set.
{
assertion = config.hyperhive.icon == null || lib.hasSuffix ".svg" (toString config.hyperhive.icon);
message = "hyperhive.icon must point to an .svg file";
assertion =
config.services.hyperhive.agent.icon == null
|| lib.hasSuffix ".svg" (toString config.services.hyperhive.agent.icon);
message = "services.hyperhive.agent.icon must point to an .svg file";
}
];
# Operator-set per-agent icon (hyperhive.icon). When configured, the
# Operator-set per-agent icon (services.hyperhive.agent.icon). When configured, the
# SVG lands at /etc/hyperhive/icon.svg; the harness serves it at
# GET /icon, 404ing when absent (client-side fallback, no
# server-side default). Consumed by forge-avatar-sync (./forge.nix)
# and the matrix avatar sync (./matrix.nix) too.
environment.etc."hyperhive/icon.svg" = lib.mkIf (config.hyperhive.icon != null) {
source = config.hyperhive.icon;
environment.etc."hyperhive/icon.svg" = lib.mkIf (config.services.hyperhive.agent.icon != null) {
source = config.services.hyperhive.agent.icon;
};
boot.isNspawnContainer = true;
@ -213,11 +216,11 @@
# `hive-metric` (agent-emitted custom metrics CLI,
# docs/scheduler/observability.md).
environment.systemPackages = [
config.hyperhive.packages.hive-metric
config.services.hyperhive.agent.packages.hive-metric
]
++ [
(
if config.hyperhive.claudeCodePath == null then
if config.services.hyperhive.agent.claudeCodePath == null then
pkgs.claude-code
else
# Host-pinned claude: a symlink farm around a path the
@ -235,7 +238,7 @@
# see `services.hyperhive.c0re.claudeCodePackage`.
pkgs.runCommandLocal "claude-code-pinned" { } ''
mkdir -p "$out/bin"
ln -s ${config.hyperhive.claudeCodePath}/bin/claude "$out/bin/claude"
ln -s ${config.services.hyperhive.agent.claudeCodePath}/bin/claude "$out/bin/claude"
''
)
]
@ -262,7 +265,7 @@
# host-level `services.hyperhive.c0re.contextWindowTokens` option — not
# set here.
environment.variables = {
HIVE_ASSETS_DIR = "${config.hyperhive.packages.assets}/share/hyperhive";
HIVE_ASSETS_DIR = "${config.services.hyperhive.agent.packages.assets}/share/hyperhive";
SHELL = "${pkgs.bashInteractive}/bin/bash";
# Route interactive-shell nix invocations through the host daemon.
# Redundant with /etc/profile.d/nix-daemon.sh but ensures it's set

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -100,7 +100,7 @@ pub struct PutMatrixAccountRequest {
/// flight, with nowhere to reconstruct it from on a re-delivery.
///
/// Optional in token mode (omitted means "resolve to the agent's own
/// `hyperhive.matrix.url` on the hive side" — this route never needs to
/// `services.hyperhive.agent.matrix.url` on the hive side" — this route never needs to
/// know it itself for a blind store). **Required** in password mode:
/// logging in needs somewhere to log in against, and unlike token mode
/// there is no hive-side fallback to defer to.
@ -163,7 +163,7 @@ pub async fn put_matrix_account(
let secret_path = matrix::account_path(&agent, &account)
.map_err(|e| error_problem(StatusCode::BAD_REQUEST, &e.to_string()))?;
// `main` is the hive-internal account `nix/agent-modules/matrix.nix`
// synthesizes per agent from `hyperhive.matrix.url` — the schema there
// synthesizes per agent from `services.hyperhive.agent.matrix.url` — the schema there
// forbids declaring a key by that name for the same reason this route
// refuses to write one: an extra account literally named `main` would
// not overwrite the real one (it lands at a different token-file suffix)
@ -174,7 +174,7 @@ pub async fn put_matrix_account(
return Err(error_problem(
StatusCode::BAD_REQUEST,
"'main' is the hive-internal account, synthesized per agent from \
hyperhive.matrix.url — it cannot be set through this route.",
services.hyperhive.agent.matrix.url — it cannot be set through this route.",
));
}