hive-subagent-daemon and hive-bash-daemon are now delegated their cgroup (Delegate=memory pids, DelegateSubgroup=daemon). At start each daemon enables memory+pids below its unit and creates runs/ (subagent) or tasks/ (bash). Each subagent run gets runs/<name>-<n>/ and each bash task tasks/<id>/, both with memory.oom.group=1, and the daemon removes the directory when the run ends. An OOM kill now takes one whole run instead of one process out of the daemon's shared cgroup, and never the daemon itself. Subagent runs get oom_score_adj +1000; bash tasks and both daemons stay at 0; the harness stays at -500. The unit-level OOMScoreAdjust=500 on the subagent daemon goes: it was both the daemon's own score and a floor no run below it could be lowered past. Bash tasks enter their cgroup from the existing pre_exec hook, writing to a cgroup.procs fd opened by the parent. hive-claude builds its Command itself and exposes neither a pre_exec hook nor the child's pid, so a subagent's claude (and an ACP agent's process) is started as the daemon's own binary with the run's cgroup in its environment; main() enters the cgroup, raises oom_score_adj and execs the real program, the trampoline swarm-review-classifier already uses. Both enter before the program executes, so nothing it forks starts outside the run's cgroup. Without a delegated `daemon` leaf (tests, a dev shell) each daemon logs one warning and runs everything in its own cgroup, as before. The shared helper is hive-agent-sock's run_cgroup module, the crate both daemons already use for what in-container plugins share. Closes #5007
522 lines
26 KiB
Nix
522 lines
26 KiB
Nix
# The MCP tool surface: the built-in hyperhive server (persistent
|
|
# streamable-http daemon), the bash-task backend daemon (also a
|
|
# persistent streamable-http MCP server, auto-injected into
|
|
# `extraMcpServers`), the `extraMcpServers` option itself (stdio or http,
|
|
# per entry), and the send-recipient allowlist.
|
|
{
|
|
pkgs,
|
|
lib,
|
|
config,
|
|
...
|
|
}:
|
|
let
|
|
userName = config.services.hyperhive.agent.user.name;
|
|
# Set on the harness only for an ACP agent on the opencode preset
|
|
# (./agent-service.nix).
|
|
acpApiKeyEnv = config.systemd.services.hive-agent.environment.HIVE_ACP_API_KEY_ENV or null;
|
|
# The subagent daemon reads this agent's own queue credential from the
|
|
# store, to publish its subagents' terminals as the agent, and on the
|
|
# opencode preset the provider key too. Either way it is handed the same
|
|
# store identity ./queue-identity.nix hands the harness, under the same
|
|
# switch.
|
|
subagentReadsStore = config.services.hyperhive.agent.bao.addr != null;
|
|
in
|
|
{
|
|
options.services.hyperhive.agent.allowedRecipients = lib.mkOption {
|
|
type = lib.types.listOf lib.types.str;
|
|
default = [ ];
|
|
example = [
|
|
"alice"
|
|
"manager"
|
|
];
|
|
description = ''
|
|
Names this agent is allowed to `send` to via
|
|
`mcp__hyperhive__send`. Empty list (the default) means
|
|
unrestricted — the agent can message any peer or the
|
|
manager. Non-empty list constrains the
|
|
surface: only the listed names + the manager (always
|
|
allowed) get through; anything else returns an error
|
|
string to claude without touching the broker.
|
|
`operator` is refused either way: agents reach the operator
|
|
over matrix.
|
|
|
|
Useful for sandboxing untrusted sub-agents — set
|
|
`[ "manager" ]` to scope them to manager-only chatter.
|
|
The manager itself is always exempt; this option only
|
|
affects sub-agent `send`.
|
|
'';
|
|
};
|
|
|
|
options.services.hyperhive.agent.extraMcpServers = lib.mkOption {
|
|
type = lib.types.attrsOf (
|
|
lib.types.submodule {
|
|
options = {
|
|
type = lib.mkOption {
|
|
type = lib.types.enum [
|
|
"stdio"
|
|
"http"
|
|
];
|
|
default = "stdio";
|
|
description = ''
|
|
Transport for this MCP server. `"stdio"` (the default) spawns
|
|
`command` as a fresh child process every turn, talking
|
|
JSON-RPC over its stdin/stdout — existing entries need zero
|
|
changes to keep this behaviour. `"http"` points claude at a
|
|
long-lived streamable-http `url` instead: no per-turn spawn,
|
|
no re-registration race, same shape as the built-in
|
|
hyperhive surface (`hive-mcp-http`) — use this for a server
|
|
backed by an always-on daemon. `command`/`args`/`env` only
|
|
apply to `"stdio"`; `url` only to `"http"`.
|
|
'';
|
|
};
|
|
command = lib.mkOption {
|
|
type = lib.types.nullOr lib.types.str;
|
|
default = null;
|
|
description = ''
|
|
Absolute path to the MCP server binary. Use `''${pkgs.foo}/bin/foo`
|
|
or `/run/current-system/sw/bin/foo`. Required when
|
|
`type = "stdio"`; ignored (leave `null`) for `"http"`.
|
|
'';
|
|
};
|
|
args = lib.mkOption {
|
|
type = lib.types.listOf lib.types.str;
|
|
default = [ ];
|
|
description = "Args passed to the MCP server binary. `\"stdio\"` only.";
|
|
};
|
|
env = lib.mkOption {
|
|
type = lib.types.attrsOf lib.types.str;
|
|
default = { };
|
|
description = "Environment variables for the MCP server child process. `\"stdio\"` only.";
|
|
};
|
|
url = lib.mkOption {
|
|
type = lib.types.nullOr lib.types.str;
|
|
default = null;
|
|
description = ''
|
|
Streamable-http URL (e.g. `http://127.0.0.1:8791/mcp`) of the
|
|
always-on daemon serving this MCP surface. Required when
|
|
`type = "http"`; ignored (leave `null`) for `"stdio"`.
|
|
'';
|
|
};
|
|
allowedTools = lib.mkOption {
|
|
type = lib.types.listOf lib.types.str;
|
|
default = [ "*" ];
|
|
example = [
|
|
"send_message"
|
|
"join_room"
|
|
];
|
|
description = ''
|
|
Tool names this MCP server is auto-approved to call via
|
|
`--allowedTools`. Single entry `"*"` (the default) means
|
|
"every tool from this server" — convenient but trusting.
|
|
Tighten to a specific list when you only want a subset.
|
|
Names are bare (e.g. `send_message`); the harness prepends
|
|
`mcp__<server-key>__` at build time.
|
|
'';
|
|
};
|
|
availableToSubagents = lib.mkOption {
|
|
type = lib.types.bool;
|
|
default = false;
|
|
description = ''
|
|
Whether a claude subagent (`mcp__subagent__start`/`continue`,
|
|
served by `hive-subagent-daemon`) also gets this MCP server.
|
|
Default `false`: a subagent runs `--strict-mcp-config` with no
|
|
ambient MCP discovery, so this is the *only* path an
|
|
`extraMcpServers` entry reaches a subagent's own claude
|
|
invocation at all — leaving it unset keeps a subagent exactly
|
|
as narrow as before this option existed (its native built-in
|
|
tools only, zero MCP servers).
|
|
|
|
The automatically injected `bash` and `subagent` entries below stay at
|
|
this default — a subagent never gets hive-bash (it already has
|
|
claude's native `Bash` tool instead) or the ability to spawn
|
|
its own nested subagents, unless an operator opts a
|
|
*different* entry in explicitly. Set `true` on an entry (e.g.
|
|
a scraper or read-only lookup MCP) to hand it to subagents too.
|
|
'';
|
|
};
|
|
};
|
|
}
|
|
);
|
|
default = { };
|
|
example = lib.literalExpression ''
|
|
{
|
|
matrix = {
|
|
command = "/run/current-system/sw/bin/mcp-matrix";
|
|
args = [ "--config" "/state/matrix.toml" ];
|
|
env.MATRIX_HOMESERVER = "https://matrix.example.org";
|
|
allowedTools = [ "send_message" "join_room" ];
|
|
};
|
|
bash = {
|
|
type = "http";
|
|
url = "http://127.0.0.1:8791/mcp";
|
|
};
|
|
}
|
|
'';
|
|
description = ''
|
|
Extra MCP servers claude sees alongside the hyperhive tool surface.
|
|
Keys are the server names (claude addresses tools as
|
|
`mcp__<key>__<tool>`). Rendered to `/etc/hyperhive/extra-mcp.json`
|
|
at activation time; the harness reads that file at boot and merges
|
|
it into `--mcp-config` + `--allowedTools`. Take effect on the
|
|
agent's next harness restart (no operator approval needed beyond
|
|
whatever brought the new agent.nix into deployed/*).
|
|
|
|
The `bash` entry above is illustrative only — it's auto-injected
|
|
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.services.hyperhive.agent.mcp.httpPort = lib.mkOption {
|
|
type = lib.types.port;
|
|
default = 8790;
|
|
example = 8791;
|
|
description = ''
|
|
Loopback port the built-in hyperhive MCP surface is served on. HTTP
|
|
is the *sole* transport for the built-in surface: a
|
|
long-lived `hive-mcp-http` systemd unit runs
|
|
`hive-agent-mcp --http 127.0.0.1:<port>` and `render_claude_config`
|
|
points claude at the stable `http://127.0.0.1:<port>/mcp` URL. That
|
|
URL survives the per-turn claude re-spawn (and a host-side hive-c0re
|
|
restart — each tool call dials the control socket fresh), so there
|
|
is no per-turn MCP re-registration race (a resumed stdio child could
|
|
emit its first tool call before that turn's async
|
|
`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
|
|
`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
|
|
headers from anywhere else, so no auth token is required for a
|
|
container-local endpoint.
|
|
|
|
Failure-mode note: with no stdio fallback, if `hive-mcp-http` is
|
|
down claude hits a dead URL until the unit restarts (guarded by
|
|
`Restart=always`, `RestartSec=3`). Intended shape: no per-turn race
|
|
while up, a bounded self-healing gap while restarting.
|
|
|
|
Safe as a single fixed default across all agents: each container
|
|
runs in its own private network namespace (isolation is always-on —
|
|
see docs/networking/network.md), so `127.0.0.1:<port>` is per-container-private
|
|
and cannot collide across agents. Override only if a container-local
|
|
service already occupies this port.
|
|
|
|
Must match `mcp_config::DEFAULT_MCP_HTTP_PORT` (the harness always
|
|
exports `HYPERHIVE_MCP_HTTP_PORT`, so the const is only a fallback).
|
|
'';
|
|
};
|
|
|
|
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 `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 `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.services.hyperhive.agent.mcp.subagentHttpPort = lib.mkOption {
|
|
type = lib.types.port;
|
|
default = 8793;
|
|
example = 8794;
|
|
description = ''
|
|
Loopback port `hive-subagent-daemon` serves its MCP tools
|
|
(`start`/`continue`/`status`/`interrupt`) on. Independent daemon (own crate,
|
|
`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
|
|
`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.
|
|
'';
|
|
};
|
|
|
|
config = {
|
|
# Assert the transport-specific required field is actually set —
|
|
# `command`/`url` are both `nullOr` so the submodule schema stays
|
|
# backward-compatible for existing stdio entries, but a `null` in the
|
|
# field the chosen `type` actually needs is a config mistake, not a
|
|
# valid "unset".
|
|
assertions =
|
|
lib.mapAttrsToList (name: spec: {
|
|
assertion = spec.type != "stdio" || spec.command != null;
|
|
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 = "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 that agent having a
|
|
# services.hyperhive.agent.matrixAccounts entry at all.) `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.
|
|
services.hyperhive.agent.extraMcpServers.bash = lib.mkDefault {
|
|
type = "http";
|
|
url = "http://127.0.0.1:${toString config.services.hyperhive.agent.mcp.bashHttpPort}/mcp";
|
|
allowedTools = [ "*" ];
|
|
};
|
|
|
|
# Auto-inject the subagent MCP server — default-on for every agent for
|
|
# now (operator's call: "default on for now, should be a capability
|
|
# later" — unconditional, where `matrix.nix`'s sibling entry appears only
|
|
# for an agent that has a matrix account to serve).
|
|
# `lib.mkDefault` so an agent.nix can still override/disable
|
|
# the entry in the meantime.
|
|
services.hyperhive.agent.extraMcpServers.subagent = lib.mkDefault {
|
|
type = "http";
|
|
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.services.hyperhive.agent.extraMcpServers;
|
|
|
|
environment.etc."hyperhive/send-allow.json".text =
|
|
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
|
|
# `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" ];
|
|
before = [ "hive-agent.service" ];
|
|
# The daemon runs every bash task via `Command::new("bash")` and the
|
|
# commands themselves (hive-forge, git, jq, …) resolve from PATH.
|
|
# A standalone daemon has no inherited agent PATH, so without this
|
|
# `bash` itself isn't found (spawn fails with ENOENT, the task is
|
|
# marked done in 0s with no output / no .out/.err). Mirror the
|
|
# harness unit's PATH: NixOS appends `/bin` to each entry →
|
|
# /run/wrappers/bin (setuid sudo) + /run/current-system/sw/bin
|
|
# (bash, coreutils, hive-forge, …).
|
|
path = [
|
|
"/run/wrappers"
|
|
"/run/current-system/sw"
|
|
];
|
|
environment = {
|
|
# In-agent todo socket the harness serves (loose-ends v2): the
|
|
# runner pushes bash-task todos here (upsert while active, keyless
|
|
# 'done' on completion) instead of firing a c0re wake. Must match
|
|
# the harness's HIVE_AGENT_SOCKET (agent-service.nix).
|
|
HIVE_AGENT_SOCKET = "/run/hive-agent/${userName}/agent.sock";
|
|
RUST_LOG = "info";
|
|
# HYPERHIVE_HARNESS_DIR and HYPERHIVE_STATE_DIR are already
|
|
# injected via systemd.globalEnvironment by the meta flake
|
|
# (set to /agents/<name>/harness and /agents/<name>/state
|
|
# respectively). The daemon uses these to derive its task +
|
|
# loose-ends dir paths; `hive_agent_sock::paths::harness_dir`
|
|
# panics loudly if HYPERHIVE_HARNESS_DIR is unset rather than
|
|
# deriving a fallback, since every service here always gets it.
|
|
};
|
|
serviceConfig = {
|
|
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
|
|
# no stdio fallback — same reasoning as `hive-mcp-http` below.
|
|
Restart = "always";
|
|
RestartSec = 3;
|
|
# systemd's default `stop` tears the whole unit down when the kernel
|
|
# kills *any* process in its cgroup, so one OOM-killed task would
|
|
# take every running sibling task with it. `continue` scopes the
|
|
# loss to the process the kernel chose.
|
|
OOMPolicy = "continue";
|
|
# The daemon runs each task in its own `tasks/<id>/` cgroup with
|
|
# `memory.oom.group=1`, so an OOM kill takes one whole task. The
|
|
# subgroup name is `DAEMON_SUBGROUP` in hive-agent-sock's `run_cgroup`.
|
|
Delegate = "memory pids";
|
|
DelegateSubgroup = "daemon";
|
|
User = userName;
|
|
Group = userName;
|
|
};
|
|
};
|
|
|
|
# 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 `services.hyperhive.agent.mcp.subagentHttpPort`. The same port also
|
|
# serves a second, subagent-facing route (`/signal/mcp/<token>`:
|
|
# `goal_reached`/`need_help`) — not something an agent's own config points
|
|
# at: the daemon mints each subagent it spawns its own token and writes
|
|
# that one URL into that subagent's own `--mcp-config`, which is how a
|
|
# signal's identity comes from the endpoint instead of from a `name` the
|
|
# caller could have filled in with a sibling's. No task files —
|
|
# this daemon's only state is in-memory, live only as long as the process
|
|
# is (see `hive-subagent-mcp/src/session.rs`'s module doc); a restart
|
|
# stops whatever's running, the actual claude session survives
|
|
# independently.
|
|
systemd.services.hive-subagent-daemon = {
|
|
description = "subagent task runner + MCP daemon for hive-subagent";
|
|
wantedBy = [ "multi-user.target" ];
|
|
before = [ "hive-agent.service" ];
|
|
# The live certificate pair exists once ./bao.nix's unit has run.
|
|
after = lib.optional subagentReadsStore "hive-agent-bao-identity.service";
|
|
# A subagent task runs its own nested `claude` invocation (via
|
|
# `hive-claude`), which needs to resolve `claude` itself off PATH —
|
|
# same reasoning as `hive-bash-daemon`'s `path` above, even though
|
|
# this daemon never shells out to `bash -c` directly.
|
|
path = [
|
|
"/run/wrappers"
|
|
"/run/current-system/sw"
|
|
];
|
|
environment = {
|
|
# Same in-agent todo socket as `hive-bash-daemon` — both push task
|
|
# todos to the one harness socket. Must match the harness's
|
|
# HIVE_AGENT_SOCKET (agent-service.nix).
|
|
HIVE_AGENT_SOCKET = "/run/hive-agent/${userName}/agent.sock";
|
|
RUST_LOG = "info";
|
|
# This daemon resolves each subagent's `--tools` from the same
|
|
# tool groups the harness resolves its own session from, so that a
|
|
# subagent's built-in tools are its parent agent's and never wider.
|
|
# The meta renderer writes this var onto the harness unit alone, so
|
|
# forward it from there rather than re-deriving it: without it the
|
|
# daemon falls back to the default groups and an agent granted
|
|
# `web_tools` would spawn subagents that quietly lack web access.
|
|
# `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;
|
|
# The harness's runtime selection, forwarded the same way, so an ACP
|
|
# agent's subagents run on its ACP agent. All five are absent on a
|
|
# claude agent, which the daemon reads as claude.
|
|
HIVE_RUNTIME = config.systemd.services.hive-agent.environment.HIVE_RUNTIME or null;
|
|
HIVE_ACP_COMMAND = config.systemd.services.hive-agent.environment.HIVE_ACP_COMMAND or null;
|
|
HIVE_ACP_ARGS = config.systemd.services.hive-agent.environment.HIVE_ACP_ARGS or null;
|
|
HIVE_ACP_ENV = config.systemd.services.hive-agent.environment.HIVE_ACP_ENV or null;
|
|
HIVE_ACP_API_KEY_ENV = acpApiKeyEnv;
|
|
# 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. `null`
|
|
# for an empty list, which systemd drops: the daemon reads an absent
|
|
# var as "no restriction" (this is a safety rail, not a security
|
|
# boundary), matching the harness assertion that an empty list waives
|
|
# too.
|
|
HIVE_AVAILABLE_MODELS =
|
|
if config.services.hyperhive.agent.availableModels == [ ] then
|
|
null
|
|
else
|
|
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.
|
|
# A subagent's backgrounded bash child is reaped with the rest of
|
|
# its process tree at turn end, which orphans anything still
|
|
# running past the CLI's default 2-minute timeout with a silent
|
|
# truncated/empty log rather than a reported failure. Raising the
|
|
# default here (not in managed-settings, which the main agent's
|
|
# session also reads) scopes the longer timeout to subagent turns
|
|
# only — the operator's ruling was explicit that the main agent's
|
|
# environment stays as is.
|
|
BASH_DEFAULT_TIMEOUT_MS = "1800000"; # 30 minutes
|
|
}
|
|
// lib.optionalAttrs subagentReadsStore {
|
|
# Paths and a name only. `%d` is this unit's own credentials directory.
|
|
HIVE_AGENT_NAME = userName;
|
|
BAO_ADDR = config.services.hyperhive.agent.bao.addr;
|
|
BAO_CLIENT_CERT = config.services.hyperhive.agent.bao.clientCertFile;
|
|
BAO_CLIENT_KEY = config.services.hyperhive.agent.bao.clientKeyFile;
|
|
BAO_CACERT = "%d/hive-agent-bao-server-ca";
|
|
};
|
|
serviceConfig = {
|
|
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
|
|
# `start`/`continue`/`status`/`interrupt` with no stdio fallback.
|
|
Restart = "always";
|
|
RestartSec = 3;
|
|
# The daemon runs each subagent run in its own `runs/<name>-<n>/`
|
|
# cgroup with `memory.oom.group=1` and `oom_score_adj` +1000, so an
|
|
# OOM kill takes one whole run, ahead of a bash task (0) and
|
|
# `hive-agent` (-500). No `OOMScoreAdjust=` here: the daemon stays
|
|
# at 0, and a unit value would also be the floor no run could go
|
|
# below. The subgroup name is `DAEMON_SUBGROUP` in hive-agent-sock's
|
|
# `run_cgroup`.
|
|
Delegate = "memory pids";
|
|
DelegateSubgroup = "daemon";
|
|
# systemd's default is `stop`, which tears the whole unit down as
|
|
# soon as the kernel kills *any* process in its cgroup — so one
|
|
# over-large subagent takes the daemon and every sibling session
|
|
# with it, which is the blast radius that makes an OOM here
|
|
# expensive. `continue` scopes the loss to the process the kernel
|
|
# actually chose and leaves the daemon serving, which is also what
|
|
# lets it report the kill rather than vanish and be restarted.
|
|
OOMPolicy = "continue";
|
|
# `MemoryHigh=` is a drop-in from ./memory-cap.nix, sized at boot from
|
|
# the container's cap.
|
|
User = userName;
|
|
Group = userName;
|
|
}
|
|
// lib.optionalAttrs subagentReadsStore {
|
|
# Bare id: inherits the credential the container manager passed in,
|
|
# the same form ./queue-identity.nix uses.
|
|
LoadCredential = [ "hive-agent-bao-server-ca" ];
|
|
}
|
|
//
|
|
lib.optionalAttrs
|
|
(
|
|
(config.services.hyperhive.agent.runtime == "acp" || config.services.hyperhive.agent.useApiKey)
|
|
&& config.services.hyperhive.agent.backendEnvironmentFile != null
|
|
)
|
|
{
|
|
# A subagent authenticates with the same credentials as the
|
|
# harness: an ACP subagent to its provider, a claude subagent of
|
|
# an api-key agent with `ANTHROPIC_API_KEY`, as it has no OAuth
|
|
# session to fall back on.
|
|
EnvironmentFile = "-${config.services.hyperhive.agent.backendEnvironmentFile}";
|
|
};
|
|
};
|
|
|
|
# Persistent streamable-http MCP daemon for the built-in hyperhive
|
|
# surface — the *sole* transport for that surface; always
|
|
# wired. Long-lived so claude reconnects to the stable URL each turn
|
|
# instead of respawning + re-registering a stdio subprocess (the
|
|
# per-turn MCP registration race). It dials the control socket
|
|
# (`/run/hive/mcp.sock`, the harness binaries' default) fresh on every
|
|
# tool call, so a host-side hive-c0re restart is transparent.
|
|
# `before = hive-agent` so the URL is already listening by the time
|
|
# the harness renders the first turn's config; the harness/claude also
|
|
# reconnect on their own, so ordering is a latency nicety not a hard
|
|
# correctness dep.
|
|
systemd.services.hive-mcp-http = {
|
|
description = "persistent streamable-http MCP daemon for the hyperhive surface";
|
|
wantedBy = [ "multi-user.target" ];
|
|
before = [ "hive-agent.service" ];
|
|
environment.RUST_LOG = "info";
|
|
# In-agent todo socket the harness serves (loose-ends v2): the
|
|
# `get_loose_ends` handler dials it to merge this agent's local todos
|
|
# with the static loose-ends from hive-c0re.
|
|
environment.HIVE_AGENT_SOCKET = "/run/hive-agent/${userName}/agent.sock";
|
|
serviceConfig = {
|
|
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
|
|
# hyperhive-MCP loss with no stdio fallback and no per-turn
|
|
# self-heal (the URL just stays dead). `always` also covers any
|
|
# unforeseen clean-return path and restarts after a stray SIGTERM
|
|
# stops it out from under the harness.
|
|
Restart = "always";
|
|
RestartSec = 3;
|
|
User = userName;
|
|
Group = userName;
|
|
};
|
|
};
|
|
};
|
|
}
|