# The hive-agent harness service itself, plus the per-agent knobs it # reads from its environment: model selection, effort level, # compaction watermark, and the extra reverse-proxies of the per-agent # web UI. { pkgs, lib, config, ... }: let userName = config.hyperhive.user.name; homeDir = "/home/${userName}"; in { options.hyperhive.model = lib.mkOption { type = lib.types.str; default = "haiku"; example = "sonnet"; description = '' Claude model for this agent. Sets the `HIVE_DEFAULT_MODEL` environment variable; the harness applies it at boot and it takes priority over any persisted runtime override. The operator can still switch the model at runtime via the per-agent web UI — that choice is tracked in the state dir for the current session but is reset by any rebuild that changes this option. Valid values are the short model names that `claude --model` accepts: `"haiku"`, `"sonnet"`, `"opus"` (or any future identifier). Context window sizes are looked up at runtime from the `HIVE_CONTEXT_WINDOW_TOKENS_` env vars injected by the meta flake; override sizes via `services.hyperhive.c0re.contextWindowTokens` on the host. ''; }; options.hyperhive.availableModels = lib.mkOption { type = lib.types.listOf lib.types.str; default = [ "haiku" "sonnet" "opus" ]; example = [ "sonnet" "opus" ]; description = '' Models offered in the per-agent web UI's model quick-picker. Rendered into the `HIVE_AVAILABLE_MODELS` environment variable (comma-separated) which the harness surfaces to the agent UI, so the picker lists exactly these models instead of a hardcoded set. 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 UI; this option only controls which choices the picker presents. Values are the short model names that `claude --model` accepts: `"haiku"`, `"sonnet"`, `"opus"` (or any future identifier). ''; }; options.hyperhive.effortLevel = lib.mkOption { type = lib.types.enum [ "low" "medium" "high" "xhigh" "max" ]; default = "medium"; example = "high"; description = '' Baseline claude effort level for this agent. Rendered into the `HIVE_DEFAULT_EFFORT` environment variable; the harness resolves effort as operator-override-file → this env → built-in `"medium"`, and passes the result to `claude --effort` at turn launch. Ascending scale: `"low"` (minimal thinking budget), `"medium"` (default — balanced), `"high"` (platform default), `"xhigh"` (recommended for autonomous coding on capable models), `"max"` (maximum thinking budget, highest cost). The operator can override at runtime per-agent via the web UI (applied on the next session); any rebuild that changes this option resets that override. ''; }; options.hyperhive.autoCompact = lib.mkOption { type = lib.types.bool; default = true; description = '' Enable proactive watermark-based compaction. When `true` (the default) the harness automatically runs a notes-checkpoint turn followed by `/compact` once the context window crosses 75% of the model's limit, keeping later turns from hitting the hard overflow path. Set to `false` to disable proactive compaction entirely (`HIVE_COMPACT_WATERMARK_TOKENS=0`); the reactive path (compact-on-overflow when the session is already past the limit) still applies. Disable for agents that run large-context models (sonnet/opus) where the heuristic fires too early and discards useful history before the session is actually close to the limit. ''; }; options.hyperhive.extraWebProxies = lib.mkOption { type = lib.types.attrsOf lib.types.str; default = { }; example = lib.literalExpression ''{ "stats" = "http://127.0.0.1:3737"; }''; description = '' Transparent reverse-proxies mounted under `/extra/` in the per-agent web UI. Each attribute name becomes the sub-path and the value is the upstream. E.g. `{ "stats" = "http://127.0.0.1:3737"; }` mounts a proxy at `/agent//extra/stats/` that forwards to port 3737 with the prefix stripped. All user-declared proxies live under `/extra/` so they can never conflict with native agent endpoints (`/api/*`, `/events/*`, etc.). The upstream value is either an `http(s)://` URL or a Unix domain socket, spelled `unix:` (e.g. `unix:/run/myapp/http.sock`) — for agents whose secondary web server only listens on a UDS. Intended for agents that run secondary web servers in the same container. Static assets served by the secondary app must use relative paths to resolve correctly under the sub-path prefix. Sets the `HIVE_EXTRA_WEB_PROXIES` environment variable (JSON object) on the harness service unit. ''; }; config = { assertions = [ # hyperhive.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\")"; } # 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; message = "hyperhive.model (\"${config.hyperhive.model}\") must be one of " + "hyperhive.availableModels ([ ${lib.concatStringsSep " " config.hyperhive.availableModels} ]) " + "— add it to the list or change the model."; } ]; # 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; # Comma-separated menu for the per-agent UI model quick-picker # (see hyperhive.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 # harness resolves operator-override-file → this env → "medium" # and passes it to claude --effort at turn launch. HIVE_DEFAULT_EFFORT = config.hyperhive.effortLevel; } // lib.optionalAttrs (!config.hyperhive.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"; }; # Harness systemd unit. Unit shape (PATH wrapper-dir trick, env vars, # RuntimeDirectory, User=, standalone-eval fallbacks): # docs/agent-hierarchy.md::Harness systemd unit shape. PATH /bin # auto-append behaviour: docs/gotchas.md::systemd.services.*.path # appends /bin to every entry. systemd.services.hive-agent = let binary = "hive-agent"; in { description = "${binary} harness"; wantedBy = [ "multi-user.target" ]; after = [ "network.target" ]; # `/run/wrappers` before `/run/current-system/sw` so setuid # `sudo` resolves first. Passing the bare prefixes (no trailing # `/bin`) is intentional — see docs pointer above. path = [ "/run/wrappers" "/run/current-system/sw" ]; environment = { SHELL = "${pkgs.bashInteractive}/bin/bash"; HOME = homeDir; HIVE_STATIC_DIR = "${config.hyperhive.frontend.mergedDist}"; HIVE_ASSETS_DIR = "${config.hyperhive.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 # bind-mounts and gateway upstream config stay in sync. HIVE_WEB_SOCKET = "/run/hive-agent/${userName}/web.sock"; # In-agent socket (loose-ends v2): the harness binds this and # serves the `hive-agent-sock` todo protocol to the in-container # producers (matrix daemon) + the MCP bridge (`get_loose_ends`). # Same per-agent runtime dir as the web socket so all agent-user # services in this container can reach it; purely in-container # (never bind-mounted to the host — unlike hive-c0re's mcp.sock). HIVE_AGENT_SOCKET = "/run/hive-agent/${userName}/agent.sock"; # Loopback URL of the persistent `hive-mcp-http` daemon that # `render_claude_config` points claude at for the built-in # surface (HTTP is the sole transport — no per-turn stdio child). # Kept in sync with the `hive-mcp-http` unit's `--http` port # (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; } // lib.optionalAttrs config.hyperhive.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; } // lib.optionalAttrs (config.hyperhive.extraWebProxies != { }) { # JSON object {"": ""} for the transparent # reverse-proxies. See `hyperhive.extraWebProxies` option # and `web_ui/proxy.rs::extra_proxy_service`. HIVE_EXTRA_WEB_PROXIES = builtins.toJSON config.hyperhive.extraWebProxies; }; serviceConfig = { ExecStart = "${config.hyperhive.packages.hive-agent}/bin/${binary}"; # Pin the journal identity to the binary name (otherwise systemd # derives SyslogIdentifier from the ExecStart basename). SyslogIdentifier = binary; Restart = "on-failure"; RestartSec = 2; # Per-service runtime dir owned by `User=` below; the harness # writes its regenerated claude-{mcp-config,settings,system-prompt} # files here (`paths::config_dir`). Separate from /run/hive, # which holds hive-c0re's mcp.sock. RuntimeDirectory = "hive-config"; User = userName; Group = userName; }; }; }; }