diff --git a/flake.nix b/flake.nix index 8f831359..870a4b2e 100644 --- a/flake.nix +++ b/flake.nix @@ -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 diff --git a/nix/agent-modules/agent-service.nix b/nix/agent-modules/agent-service.nix index db5af618..b806ac33 100644 --- a/nix/agent-modules/agent-service.nix +++ b/nix/agent-modules/agent-service.nix @@ -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 {"": ""} 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}"; }; }; }; diff --git a/nix/agent-modules/bash-env.nix b/nix/agent-modules/bash-env.nix index 302ece25..ad649427 100644 --- a/nix/agent-modules/bash-env.nix +++ b/nix/agent-modules/bash-env.nix @@ -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 + ''; }; } diff --git a/nix/agent-modules/claude-settings.nix b/nix/agent-modules/claude-settings.nix index 371595db..67257ce5 100644 --- a/nix/agent-modules/claude-settings.nix +++ b/nix/agent-modules/claude-settings.nix @@ -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 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 ` (`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 diff --git a/nix/agent-modules/dashboard-links.nix b/nix/agent-modules/dashboard-links.nix index 9ba7a5b6..fa98ef94 100644 --- a/nix/agent-modules/dashboard-links.nix +++ b/nix/agent-modules/dashboard-links.nix @@ -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//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//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" + ''; + }; }; } diff --git a/nix/agent-modules/default.nix b/nix/agent-modules/default.nix index 281ebb49..fea45d18 100644 --- a/nix/agent-modules/default.nix +++ b/nix/agent-modules/default.nix @@ -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 diff --git a/nix/agent-modules/docs.nix b/nix/agent-modules/docs.nix index e717f0bb..0020b582 100644 --- a/nix/agent-modules/docs.nix +++ b/nix/agent-modules/docs.nix @@ -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}"; }; }; } diff --git a/nix/agent-modules/forge.nix b/nix/agent-modules/forge.nix index 98e18dd0..8e0a9da2 100644 --- a/nix/agent-modules/forge.nix +++ b/nix/agent-modules/forge.nix @@ -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 : 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//state`. TOKEN_FILE="$HYPERHIVE_STATE_DIR/forge-token" diff --git a/nix/agent-modules/frontend.nix b/nix/agent-modules/frontend.nix index 90a631cf..38bdc937 100644 --- a/nix/agent-modules/frontend.nix +++ b/nix/agent-modules/frontend.nix @@ -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 '' 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) + ); }; } diff --git a/nix/agent-modules/github.nix b/nix/agent-modules/github.nix index 1fe7a9bd..e68cfba1 100644 --- a/nix/agent-modules/github.nix +++ b/nix/agent-modules/github.nix @@ -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 diff --git a/nix/agent-modules/logs.nix b/nix/agent-modules/logs.nix index 22d9e1d3..31849f21 100644 --- a/nix/agent-modules/logs.nix +++ b/nix/agent-modules/logs.nix @@ -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; diff --git a/nix/agent-modules/matrix.nix b/nix/agent-modules/matrix.nix index 37dcaeb8..760ff7c8 100644 --- a/nix/agent-modules/matrix.nix +++ b/nix/agent-modules/matrix.nix @@ -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 `/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` + `/matrix-token` + + `services.hyperhive.agent.matrix.url` + `/matrix-token` + `/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..tokenFile basename must start with " + "every services.hyperhive.agent.matrixAccounts..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 diff --git a/nix/agent-modules/mcp.nix b/nix/agent-modules/mcp.nix index bbda68b2..0144ff8f 100644 --- a/nix/agent-modules/mcp.nix +++ b/nix/agent-modules/mcp.nix @@ -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/`: # `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 diff --git a/nix/agent-modules/otel.nix b/nix/agent-modules/otel.nix index 95a432cf..f06cd8e8 100644 --- a/nix/agent-modules/otel.nix +++ b/nix/agent-modules/otel.nix @@ -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. diff --git a/nix/agent-modules/packages.nix b/nix/agent-modules/packages.nix index cc14d826..762cd25b 100644 --- a/nix/agent-modules/packages.nix +++ b/nix/agent-modules/packages.nix @@ -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 = '' diff --git a/nix/agent-modules/queue.nix b/nix/agent-modules/queue.nix index 4d7c5837..ffcaaf2c 100644 --- a/nix/agent-modules/queue.nix +++ b/nix/agent-modules/queue.nix @@ -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 diff --git a/nix/agent-modules/renamed-options.nix b/nix/agent-modules/renamed-options.nix new file mode 100644 index 00000000..cbf6d554 --- /dev/null +++ b/nix/agent-modules/renamed-options.nix @@ -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.`, +# while the host tier has always been under `services.hyperhive.*`. The agent +# tier moved to `services.hyperhive.agent.`; 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" ] + ) + ]; +} diff --git a/nix/agent-modules/screen.nix b/nix/agent-modules/screen.nix index 88858e2d..5f581686 100644 --- a/nix/agent-modules/screen.nix +++ b/nix/agent-modules/screen.nix @@ -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 = [ ]; }; diff --git a/nix/agent-modules/user.nix b/nix/agent-modules/user.nix index ec42b296..19f66a11 100644 --- a/nix/agent-modules/user.nix +++ b/nix/agent-modules/user.nix @@ -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 = [ diff --git a/nix/agent-modules/weston-vnc.nix b/nix/agent-modules/weston-vnc.nix index 1167b8eb..05fedb92 100644 --- a/nix/agent-modules/weston-vnc.nix +++ b/nix/agent-modules/weston-vnc.nix @@ -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 ''; diff --git a/nix/templates/ruth.nix b/nix/templates/ruth.nix index 0902001f..89c19ddc 100644 --- a/nix/templates/ruth.nix +++ b/nix/templates/ruth.nix @@ -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; }