hyperhive/nix/agent-modules/agent-service.nix

252 lines
11 KiB
Nix

# 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_<KEY_UPPER>` 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/<name>/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:<path>` (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 {"<path>": "<upstream>"} 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;
};
};
};
}