diff --git a/nix/templates/harness-base.nix b/nix/templates/harness-base.nix index f6c7c01a..eb921254 100644 --- a/nix/templates/harness-base.nix +++ b/nix/templates/harness-base.nix @@ -828,6 +828,34 @@ in ''; }; + options.hyperhive.mcp.httpPort = lib.mkOption { + type = lib.types.nullOr lib.types.port; + default = null; + example = 8790; + description = '' + Serve the built-in hyperhive MCP surface as a persistent + streamable-http daemon on this loopback port instead of the + default per-turn stdio child. + + When null (the default) claude spawns a fresh `hive … mcp` stdio + subprocess every turn — the shape that carries the per-turn MCP + re-registration race (a resumed turn can emit its first tool call + before that turn's async `initialize`/`tools-list` completes, + stranding the agent with `No such tool`). When set, a long-lived + `hive-mcp-http` systemd unit runs `hive mcp --http 127.0.0.1:` + and `render_claude_config` points claude at the stable + `http://127.0.0.1:/mcp` URL, which survives the per-turn + claude re-spawn (and a host-side hive-c0re restart — each tool call + dials the control socket fresh). Extra MCP servers (matrix/bash) + stay stdio bridges regardless. + + 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. + ''; + }; + config = { warnings = lib.optional (config.hyperhive.allowedBashPatterns != [ ]) '' hyperhive.allowedBashPatterns is deprecated and has no effect. @@ -1709,6 +1737,33 @@ in }; }; + # Persistent streamable-http MCP daemon for the built-in hyperhive + # surface. Only wired when `hyperhive.mcp.httpPort` is set; otherwise + # the surface stays the default per-turn stdio child (rendered by + # `render_claude_config`). 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 `hive` binary default) + # fresh on every tool call, so a host-side hive-c0re restart is + # transparent. `before = hive-ag3nt` 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 = lib.mkIf (config.hyperhive.mcp.httpPort != null) { + description = "persistent streamable-http MCP daemon for the hyperhive surface"; + wantedBy = [ "multi-user.target" ]; + before = [ "hive-ag3nt.service" ]; + environment.RUST_LOG = "info"; + serviceConfig = { + ExecStart = "${pkgs.hyperhive}/bin/hive mcp --http 127.0.0.1:${toString config.hyperhive.mcp.httpPort}"; + SyslogIdentifier = "hive-mcp-http"; + Restart = "on-failure"; + RestartSec = 3; + User = userName; + Group = userName; + }; + }; + # Re-fire the daemon when the matrix token appears (hive-c0re # provisions it after agent containers come up). Without this # the daemon would exit 0 silently on first boot and the MCP @@ -1802,6 +1857,13 @@ in # bind-mounts and gateway upstream config stay in sync. HIVE_WEB_SOCKET = "/run/hive-agent/${userName}/web.sock"; } + // lib.optionalAttrs (config.hyperhive.mcp.httpPort != null) { + # Presence tells `render_claude_config` to point claude at the + # persistent `hive-mcp-http` daemon's loopback URL instead of a + # per-turn stdio child. Kept in sync with the `hive-mcp-http` + # unit's `--http` port above via the same option. + 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`