feat(#2659): serve hive-bash-mcp over persistent streamable-http, drop stdio bridge
This commit is contained in:
parent
ecc2ebe682
commit
c4fcf7fbf1
26 changed files with 376 additions and 574 deletions
|
|
@ -158,10 +158,10 @@
|
|||
# unreachable from inside a container — wrapped with
|
||||
# `wireguard-tools` for `hivectl wg`). The daemon/harness/MCP bins
|
||||
# the harness execs (hive-agent{,-mcp}, hive-bash-daemon,
|
||||
# hive-matrix-daemon, hive-bash-mcp, hive-matrix-mcp) are wired via
|
||||
# their own ExecStart/command lines in the sibling modules — they
|
||||
# don't need to be on PATH too. Only these two are actually looked
|
||||
# up on PATH by claude/shell code inside the container:
|
||||
# hive-matrix-daemon, hive-matrix-mcp) are wired via their own
|
||||
# ExecStart/command lines in the sibling modules — they don't need
|
||||
# to be on PATH too. Only these two are actually looked up on PATH
|
||||
# by claude/shell code inside the container:
|
||||
# `hive-agent-wake` (external wake CLI, docs/turn-loop/mcp.md) and
|
||||
# `hive-metric` (agent-emitted custom metrics CLI,
|
||||
# docs/observability.md).
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
# The MCP tool surface: the built-in hyperhive server (persistent
|
||||
# streamable-http daemon), the bash-task backend daemon + its
|
||||
# auto-injected stdio bridge, the `extraMcpServers` option they hang
|
||||
# off, and the send-recipient allowlist.
|
||||
# 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,
|
||||
|
|
@ -42,19 +43,51 @@ in
|
|||
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.str;
|
||||
description = "Absolute path to the MCP server binary. Use `\${pkgs.foo}/bin/foo` or `/run/current-system/sw/bin/foo`.";
|
||||
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.";
|
||||
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.";
|
||||
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;
|
||||
|
|
@ -84,6 +117,10 @@ in
|
|||
env.MATRIX_HOMESERVER = "https://matrix.example.org";
|
||||
allowedTools = [ "send_message" "join_room" ];
|
||||
};
|
||||
bash = {
|
||||
type = "http";
|
||||
url = "http://127.0.0.1:8791/mcp";
|
||||
};
|
||||
}
|
||||
'';
|
||||
description = ''
|
||||
|
|
@ -112,8 +149,9 @@ in
|
|||
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). Extra MCP servers
|
||||
(matrix/bash) stay stdio bridges regardless.
|
||||
such tool` — the http endpoint eliminates that). `matrix` stays a
|
||||
stdio bridge; `bash` runs its own persistent http listener (see
|
||||
`hyperhive.mcp.bashHttpPort`).
|
||||
|
||||
Bound loopback-only; the rmcp streamable-http transport's default
|
||||
`allowed_hosts` (`localhost` / `127.0.0.1` / `::1`) rejects Host
|
||||
|
|
@ -136,15 +174,48 @@ in
|
|||
'';
|
||||
};
|
||||
|
||||
options.hyperhive.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`
|
||||
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`).
|
||||
Safe as a single fixed default across all agents (private
|
||||
per-container network namespace — see docs/network.md).
|
||||
'';
|
||||
};
|
||||
|
||||
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 = "hyperhive.extraMcpServers.${name}: type = \"stdio\" requires `command` to be set";
|
||||
}) config.hyperhive.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;
|
||||
|
||||
# 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.)
|
||||
# ./matrix.nix, gated on hyperhive.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 {
|
||||
command = "${config.hyperhive.packages.hive-bash-mcp}/bin/hive-bash-mcp";
|
||||
args = [ ];
|
||||
env.HIVE_BASH_SOCKET = "/run/hive-bash/socket";
|
||||
type = "http";
|
||||
url = "http://127.0.0.1:${toString config.hyperhive.mcp.bashHttpPort}/mcp";
|
||||
allowedTools = [ "*" ];
|
||||
};
|
||||
|
||||
|
|
@ -154,13 +225,13 @@ in
|
|||
builtins.toJSON config.hyperhive.allowedRecipients;
|
||||
|
||||
# Bash task runner daemon — long-running process that owns subprocess
|
||||
# monitoring + completion wake signals. Always enabled (every agent
|
||||
# needs bash tools). The stdio MCP bridge `hive-bash-mcp` connects
|
||||
# to this daemon's socket per turn.
|
||||
# Socket dir: /run/hive-bash/ — RuntimeDirectory keeps it on tmpfs.
|
||||
# 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.
|
||||
systemd.services.hive-bash-daemon = {
|
||||
description = "bash task runner daemon for hive-bash-mcp";
|
||||
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
|
||||
|
|
@ -174,7 +245,6 @@ in
|
|||
"/run/current-system/sw"
|
||||
];
|
||||
environment = {
|
||||
HIVE_BASH_SOCKET = "/run/hive-bash/socket";
|
||||
# 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
|
||||
|
|
@ -190,22 +260,15 @@ in
|
|||
# value but is less robust if the two vars ever diverge.
|
||||
};
|
||||
serviceConfig = {
|
||||
ExecStart = "${config.hyperhive.packages.hive-bash-daemon}/bin/hive-bash-daemon";
|
||||
ExecStart = "${config.hyperhive.packages.hive-bash-daemon}/bin/hive-bash-daemon --http 127.0.0.1:${toString config.hyperhive.mcp.bashHttpPort}";
|
||||
SyslogIdentifier = "hive-bash-daemon";
|
||||
Restart = "on-failure";
|
||||
# `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;
|
||||
User = userName;
|
||||
Group = userName;
|
||||
RuntimeDirectory = "hive-bash";
|
||||
# Keep /run/hive-bash across restarts. With the default
|
||||
# `RuntimeDirectoryPreserve=no`, a post-rebuild restart races
|
||||
# stop-time dir cleanup against the fresh daemon's socket-dir
|
||||
# creation; the daemon loses, fails `mkdir /run/hive-bash`
|
||||
# (Permission denied, non-root in /run), and loops on
|
||||
# Restart=on-failure until the next container boot — i.e. the
|
||||
# bash daemon "doesn't come up post-rebuild". Same shape as
|
||||
# hive-matrix-daemon (./matrix.nix).
|
||||
RuntimeDirectoryPreserve = "yes";
|
||||
};
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@
|
|||
description = ''
|
||||
hyperhive package outputs consumed by the harness modules: the
|
||||
per-binary daemon/CLI packages (`hive-agent`, `hive-agent-mcp`,
|
||||
`hive-agent-wake`, `hive-bash-daemon`, `hive-bash-mcp`,
|
||||
`hive-agent-wake`, `hive-bash-daemon`,
|
||||
`hive-forge`, `hive-matrix-daemon`, `hive-matrix-mcp`,
|
||||
`hive-metric`, `hive-screen-mcp`) plus the `assets`, `frontend` and
|
||||
`reference-docs` trees. Wired by the flake's agent-base/ruth
|
||||
|
|
|
|||
Loading…
Reference in a new issue