feat(#2659): serve hive-matrix-mcp over persistent streamable-http, drop stdio bridge

This commit is contained in:
damocles 2026-07-23 20:20:51 +02:00 committed by mara
commit a66b7ab298
21 changed files with 411 additions and 856 deletions

View file

@ -1,7 +1,8 @@
# Per-agent matrix integration: the `hyperhive.matrix.*` +
# `hyperhive.matrixAccounts` options, the long-running
# hive-matrix-daemon, its token-arrival path trigger, and the
# auto-injected stdio MCP bridge entry.
# hive-matrix-daemon (serves its MCP tools directly over
# streamable-http), its token-arrival path trigger, and the
# auto-injected extraMcpServers entry.
{
pkgs,
lib,
@ -31,7 +32,7 @@ in
type = lib.types.bool;
default = true;
description = ''
Enable per-agent matrix integration via `hive-matrix-mcp`.
Enable per-agent matrix integration via `hive-matrix-daemon`.
When true (the default), the harness:
- runs `hive-matrix-daemon` as a systemd unit that holds a
@ -45,9 +46,10 @@ in
- exposes the matrix tool surface (send_message, send_dm,
send_reaction, send_reply, mark_read, list_rooms,
list_room_members, read_room) to claude via an auto-injected
`extraMcpServers.matrix` entry. Claude spawns the stdio
`hive-matrix-mcp` bridge per turn, which forwards each tool
call to the daemon over `/run/hive-matrix/socket`.
`extraMcpServers.matrix` entry pointed at the daemon's own
streamable-http listener (`hyperhive.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
Wake signal (`[matrix] <sender> in <room>: <first 100c>`)
to the hyperhive control socket; the full event stays
@ -146,6 +148,24 @@ in
'';
};
options.hyperhive.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
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
single fixed default across all agents (private per-container
network namespace see docs/network.md).
'';
};
config = {
assertions = [
# Extra matrix accounts only make sense alongside the hive-internal
@ -190,35 +210,30 @@ in
}
];
# Auto-inject the matrix stdio MCP bridge alongside the bash entry
# from ./mcp.nix. `lib.mkDefault` so the operator's own agent.nix
# can override the entry.
# Auto-inject the matrix MCP entry alongside the bash entry from
# ./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 {
matrix = lib.mkDefault {
command = "${config.hyperhive.packages.hive-matrix-mcp}/bin/hive-matrix-mcp";
args = [ ];
# Same socket path the hive-matrix-daemon service binds
# via its `RuntimeDirectory = "hive-matrix"`. Keeps the
# bridge + daemon in sync without baking the path into
# the Rust default — the env override wins for both.
env.HIVE_MATRIX_SOCKET = "/run/hive-matrix/socket";
type = "http";
url = "http://127.0.0.1:${toString config.hyperhive.mcp.matrixHttpPort}/mcp";
allowedTools = [ "*" ];
};
};
# Long-running matrix-sdk client + sync per agent. Holds the unix
# socket the stdio `hive-matrix-mcp` bridge connects to + emits
# hyperhive wake signals on incoming room events via
# `/run/hive/mcp.sock`. See
# Long-running matrix-sdk client + sync per agent. Serves the MCP
# tools directly over streamable-http + emits hyperhive wake
# signals on incoming room events via `/run/hive/mcp.sock`. See
# `docs/persistence.md::Matrix per-agent daemon + token-arrival
# trigger` for the socket-path / first-boot-ordering rationale.
# trigger` for the first-boot-ordering rationale.
systemd.services.hive-matrix-daemon = lib.mkIf config.hyperhive.matrix.enable {
description = "long-running matrix-sdk Client + MCP daemon socket";
description = "long-running matrix-sdk Client + MCP daemon";
wantedBy = [ "multi-user.target" ];
before = [ "hive-agent.service" ];
after = [ "network-online.target" ];
wants = [ "network-online.target" ];
environment = {
HIVE_MATRIX_SOCKET = "/run/hive-matrix/socket";
# In-agent todo socket the harness serves (loose-ends v2): the
# matrix sweep pushes unread-room + pending-invite todos here
# instead of firing wakes at hive-c0re's mcp.sock.
@ -263,25 +278,21 @@ in
HIVE_ICON_PNG = "${iconPng}";
};
serviceConfig = {
ExecStart = "${config.hyperhive.packages.hive-matrix-daemon}/bin/hive-matrix-daemon";
ExecStart = "${config.hyperhive.packages.hive-matrix-daemon}/bin/hive-matrix-daemon --http 127.0.0.1:${toString config.hyperhive.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
# (see the module doc above) — the `systemd.paths` watcher
# below re-fires it the moment hive-c0re provisions one,
# instead of `always` busy-looping every `RestartSec` until
# then. Once a token exists this is no different from
# `hive-bash-daemon`'s reasoning (a down window loses the MCP
# tools with no stdio fallback) — a genuine crash is a
# non-zero exit, which `on-failure` already restarts.
Restart = "on-failure";
RestartSec = 5;
User = userName;
Group = userName;
RuntimeDirectory = "hive-matrix";
# Keep /run/hive-matrix across restarts. With the default
# `RuntimeDirectoryPreserve=no`, a `switch-to-configuration`
# restart races the outgoing instance's stop-time cleanup
# (which deletes the dir) against the incoming instance's
# start (which creates it + binds the socket inside it). The
# cleanup can win and delete the dir out from under the fresh
# daemon, which then fails to mkdir under root-owned /run and
# exits — looping on Restart=on-failure until the next boot.
# `yes` stops systemd removing it on stop; it still creates it
# on first start, and it lives on tmpfs so it's gone at
# container reboot regardless. See hive-bash-daemon (./mcp.nix).
RuntimeDirectoryPreserve = "yes";
};
};

View file

@ -12,7 +12,7 @@
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-forge`, `hive-matrix-daemon`, `hive-matrix-mcp`,
`hive-forge`, `hive-matrix-daemon`,
`hive-metric`, `hive-screen-mcp`) plus the `assets`, `frontend` and
`reference-docs` trees. Wired by the flake's agent-base/ruth
nixosModules to `hyperhive.packages.<system>.*`; override an