Compare commits
5 changed files with 9 additions and 182 deletions
53
Cargo.lock
generated
53
Cargo.lock
generated
|
|
@ -460,17 +460,6 @@ dependencies = [
|
||||||
"cpufeatures 0.2.17",
|
"cpufeatures 0.2.17",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "chacha20"
|
|
||||||
version = "0.10.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "6f8d983286843e49675a4b7a2d174efe136dc93a18d69130dd18198a6c167601"
|
|
||||||
dependencies = [
|
|
||||||
"cfg-if",
|
|
||||||
"cpufeatures 0.3.0",
|
|
||||||
"rand_core 0.10.1",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "chacha20poly1305"
|
name = "chacha20poly1305"
|
||||||
version = "0.10.1"
|
version = "0.10.1"
|
||||||
|
|
@ -478,7 +467,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "10cd79432192d1c0f4e1a0fef9527696cc039165d729fb41b3f4f4f354c2dc35"
|
checksum = "10cd79432192d1c0f4e1a0fef9527696cc039165d729fb41b3f4f4f354c2dc35"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"aead",
|
"aead",
|
||||||
"chacha20 0.9.1",
|
"chacha20",
|
||||||
"cipher 0.4.4",
|
"cipher 0.4.4",
|
||||||
"poly1305",
|
"poly1305",
|
||||||
"zeroize",
|
"zeroize",
|
||||||
|
|
@ -1204,7 +1193,6 @@ dependencies = [
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
"libc",
|
"libc",
|
||||||
"r-efi 6.0.0",
|
"r-efi 6.0.0",
|
||||||
"rand_core 0.10.1",
|
|
||||||
"wasip2",
|
"wasip2",
|
||||||
"wasip3",
|
"wasip3",
|
||||||
]
|
]
|
||||||
|
|
@ -2840,17 +2828,6 @@ dependencies = [
|
||||||
"rand_core 0.9.5",
|
"rand_core 0.9.5",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "rand"
|
|
||||||
version = "0.10.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "d2e8e8bcc7961af1fdac401278c6a831614941f6164ee3bf4ce61b7edb162207"
|
|
||||||
dependencies = [
|
|
||||||
"chacha20 0.10.0",
|
|
||||||
"getrandom 0.4.2",
|
|
||||||
"rand_core 0.10.1",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rand_chacha"
|
name = "rand_chacha"
|
||||||
version = "0.3.1"
|
version = "0.3.1"
|
||||||
|
|
@ -2889,12 +2866,6 @@ dependencies = [
|
||||||
"getrandom 0.3.4",
|
"getrandom 0.3.4",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "rand_core"
|
|
||||||
version = "0.10.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rand_xoshiro"
|
name = "rand_xoshiro"
|
||||||
version = "0.7.0"
|
version = "0.7.0"
|
||||||
|
|
@ -3043,27 +3014,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "0810a9f717d9828f475fe1f629f4c305c8464b7f496c3a854b58d29e65f4058e"
|
checksum = "0810a9f717d9828f475fe1f629f4c305c8464b7f496c3a854b58d29e65f4058e"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-trait",
|
"async-trait",
|
||||||
"bytes",
|
|
||||||
"chrono",
|
"chrono",
|
||||||
"futures",
|
"futures",
|
||||||
"http",
|
|
||||||
"http-body",
|
|
||||||
"http-body-util",
|
|
||||||
"pastey",
|
"pastey",
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
"rand 0.10.1",
|
|
||||||
"rmcp-macros",
|
"rmcp-macros",
|
||||||
"schemars",
|
"schemars",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"sse-stream",
|
|
||||||
"thiserror 2.0.18",
|
"thiserror 2.0.18",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tokio-stream",
|
|
||||||
"tokio-util",
|
"tokio-util",
|
||||||
"tower-service",
|
|
||||||
"tracing",
|
"tracing",
|
||||||
"uuid",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -3666,19 +3628,6 @@ dependencies = [
|
||||||
"der",
|
"der",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "sse-stream"
|
|
||||||
version = "0.2.3"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "f3962b63f038885f15bce2c6e02c0e7925c072f1ac86bb60fd44c5c6b762fb72"
|
|
||||||
dependencies = [
|
|
||||||
"bytes",
|
|
||||||
"futures-util",
|
|
||||||
"http-body",
|
|
||||||
"http-body-util",
|
|
||||||
"pin-project-lite",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "stable_deref_trait"
|
name = "stable_deref_trait"
|
||||||
version = "1.2.1"
|
version = "1.2.1"
|
||||||
|
|
|
||||||
|
|
@ -36,7 +36,6 @@ rmcp = { version = "1.7", default-features = false, features = [
|
||||||
"server",
|
"server",
|
||||||
"macros",
|
"macros",
|
||||||
"transport-io",
|
"transport-io",
|
||||||
"transport-streamable-http-server",
|
|
||||||
] }
|
] }
|
||||||
rusqlite = { version = "0.37" }
|
rusqlite = { version = "0.37" }
|
||||||
schemars = "1.0"
|
schemars = "1.0"
|
||||||
|
|
|
||||||
|
|
@ -40,18 +40,10 @@ enum Cmd {
|
||||||
#[arg(long, default_value_t = 1000)]
|
#[arg(long, default_value_t = 1000)]
|
||||||
poll_ms: u64,
|
poll_ms: u64,
|
||||||
},
|
},
|
||||||
/// Run the MCP server. Default is stdio — spawned by `claude` via
|
/// Run the MCP server on stdio. Spawned by `claude` via
|
||||||
/// `--mcp-config`; tools dispatch through `/run/hive/mcp.sock` back
|
/// `--mcp-config`; tools dispatch through `/run/hive/mcp.sock` back
|
||||||
/// into the hyperhive broker. Pass `--http <addr>` to instead run a
|
/// into the hyperhive broker.
|
||||||
/// long-lived streamable-http listener (persistent daemon) that
|
Mcp,
|
||||||
/// claude reconnects to each turn, avoiding the per-turn stdio
|
|
||||||
/// re-registration race.
|
|
||||||
Mcp {
|
|
||||||
/// Serve over streamable-http on this address (e.g.
|
|
||||||
/// `127.0.0.1:8790`) instead of stdio. Bind loopback only.
|
|
||||||
#[arg(long)]
|
|
||||||
http: Option<std::net::SocketAddr>,
|
|
||||||
},
|
|
||||||
/// Inject a wake-up event into this harness's inbox so the next
|
/// Inject a wake-up event into this harness's inbox so the next
|
||||||
/// turn fires with the given body. Intended for extra MCP servers
|
/// turn fires with the given body. Intended for extra MCP servers
|
||||||
/// / helpers (matrix bridge, scraper, webhook listener, etc.) that
|
/// / helpers (matrix bridge, scraper, webhook listener, etc.) that
|
||||||
|
|
@ -78,10 +70,7 @@ async fn main() -> Result<()> {
|
||||||
|
|
||||||
match cli.cmd {
|
match cli.cmd {
|
||||||
Cmd::Serve { poll_ms } => serve_main::<AgentSurface>(&cli.socket, poll_ms).await,
|
Cmd::Serve { poll_ms } => serve_main::<AgentSurface>(&cli.socket, poll_ms).await,
|
||||||
Cmd::Mcp { http } => match http {
|
Cmd::Mcp => mcp::serve_agent_stdio(cli.socket).await,
|
||||||
Some(addr) => mcp::serve_http(cli.socket, addr).await,
|
|
||||||
None => mcp::serve_agent_stdio(cli.socket).await,
|
|
||||||
},
|
|
||||||
Cmd::Wake { from, body } => wake::<AgentSurface>(&cli.socket, from, body).await,
|
Cmd::Wake { from, body } => wake::<AgentSurface>(&cli.socket, from, body).await,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1549,41 +1549,6 @@ pub async fn serve_agent_stdio(socket: PathBuf) -> Result<()> {
|
||||||
serve_stdio(socket).await
|
serve_stdio(socket).await
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Run the MCP server over HTTP (rmcp streamable-http transport) on `addr`.
|
|
||||||
///
|
|
||||||
/// Unlike [`serve_stdio`] — a fresh stdio child claude respawns every turn —
|
|
||||||
/// this is meant to run as a long-lived in-container daemon. claude reconnects
|
|
||||||
/// to the stable URL each turn instead of respawning and re-registering a stdio
|
|
||||||
/// subprocess, which removes the per-turn MCP registration race that can strand
|
|
||||||
/// an agent when the async `initialize`/`tools/list` loses to claude's first
|
|
||||||
/// tool call. `socket` is the hyperhive control socket every tool call dials
|
|
||||||
/// fresh (the handler holds only the path), so a host-side hive-c0re restart is
|
|
||||||
/// transparent — the next call just reconnects.
|
|
||||||
///
|
|
||||||
/// Binds loopback only in practice; the default `allowed_hosts`
|
|
||||||
/// (`localhost`/`127.0.0.1`/`::1`) rejects Host headers from anywhere else.
|
|
||||||
///
|
|
||||||
/// # Errors
|
|
||||||
///
|
|
||||||
/// Returns an error if the listener cannot bind `addr` or the HTTP server
|
|
||||||
/// exits with a fatal error.
|
|
||||||
pub async fn serve_http(socket: PathBuf, addr: std::net::SocketAddr) -> Result<()> {
|
|
||||||
use rmcp::transport::streamable_http_server::{
|
|
||||||
StreamableHttpServerConfig, StreamableHttpService, session::local::LocalSessionManager,
|
|
||||||
};
|
|
||||||
let session_manager = std::sync::Arc::new(LocalSessionManager::default());
|
|
||||||
let service = StreamableHttpService::new(
|
|
||||||
move || Ok(AgentServer::new(socket.clone())),
|
|
||||||
session_manager,
|
|
||||||
StreamableHttpServerConfig::default(),
|
|
||||||
);
|
|
||||||
let app = axum::Router::new().nest_service("/mcp", service);
|
|
||||||
let listener = tokio::net::TcpListener::bind(addr).await?;
|
|
||||||
tracing::info!(%addr, "serving hyperhive MCP over streamable-http at /mcp");
|
|
||||||
axum::serve(listener, app).await?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
// -----------------------------------------------------------------------------
|
// -----------------------------------------------------------------------------
|
||||||
// Privileged tool arg types (lifecycle, approvals, scheduling, diagnostics)
|
// Privileged tool arg types (lifecycle, approvals, scheduling, diagnostics)
|
||||||
// -----------------------------------------------------------------------------
|
// -----------------------------------------------------------------------------
|
||||||
|
|
@ -2212,27 +2177,14 @@ fn load_extra_mcp() -> std::collections::BTreeMap<String, ExtraMcpServer> {
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn render_claude_config(agent_binary: &str, socket: &std::path::Path) -> String {
|
pub fn render_claude_config(agent_binary: &str, socket: &std::path::Path) -> String {
|
||||||
let mut servers = serde_json::Map::new();
|
let mut servers = serde_json::Map::new();
|
||||||
// When the harness is configured to run the built-in server as a
|
servers.insert(
|
||||||
// persistent streamable-http daemon (loopback port in
|
SERVER_NAME.to_owned(),
|
||||||
// `HYPERHIVE_MCP_HTTP_PORT`), point claude at the stable URL instead of
|
serde_json::json!({
|
||||||
// respawning a fresh stdio child each turn. The URL survives the per-turn
|
|
||||||
// claude re-spawn, so there is no per-turn re-registration race for the
|
|
||||||
// hyperhive surface. Extra servers (matrix/bash) stay stdio bridges.
|
|
||||||
let hyperhive_entry = match std::env::var("HYPERHIVE_MCP_HTTP_PORT")
|
|
||||||
.ok()
|
|
||||||
.and_then(|p| p.trim().parse::<u16>().ok())
|
|
||||||
{
|
|
||||||
Some(port) => serde_json::json!({
|
|
||||||
"type": "http",
|
|
||||||
"url": format!("http://127.0.0.1:{port}/mcp"),
|
|
||||||
}),
|
|
||||||
None => serde_json::json!({
|
|
||||||
"command": agent_binary,
|
"command": agent_binary,
|
||||||
"args": ["--socket", socket.display().to_string(), "mcp"],
|
"args": ["--socket", socket.display().to_string(), "mcp"],
|
||||||
"env": {}
|
"env": {}
|
||||||
}),
|
}),
|
||||||
};
|
);
|
||||||
servers.insert(SERVER_NAME.to_owned(), hyperhive_entry);
|
|
||||||
// Auto-inject HYPERHIVE_STATE_DIR so extra MCP servers can resolve the
|
// Auto-inject HYPERHIVE_STATE_DIR so extra MCP servers can resolve the
|
||||||
// agent's durable state dir without the agent author hard-coding it.
|
// agent's durable state dir without the agent author hard-coding it.
|
||||||
// User-supplied env takes precedence — we only fill in the missing key.
|
// User-supplied env takes precedence — we only fill in the missing key.
|
||||||
|
|
|
||||||
|
|
@ -828,34 +828,6 @@ 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:<port>`
|
|
||||||
and `render_claude_config` points claude at the stable
|
|
||||||
`http://127.0.0.1:<port>/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 = {
|
config = {
|
||||||
warnings = lib.optional (config.hyperhive.allowedBashPatterns != [ ]) ''
|
warnings = lib.optional (config.hyperhive.allowedBashPatterns != [ ]) ''
|
||||||
hyperhive.allowedBashPatterns is deprecated and has no effect.
|
hyperhive.allowedBashPatterns is deprecated and has no effect.
|
||||||
|
|
@ -1737,33 +1709,6 @@ 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
|
# Re-fire the daemon when the matrix token appears (hive-c0re
|
||||||
# provisions it after agent containers come up). Without this
|
# provisions it after agent containers come up). Without this
|
||||||
# the daemon would exit 0 silently on first boot and the MCP
|
# the daemon would exit 0 silently on first boot and the MCP
|
||||||
|
|
@ -1857,13 +1802,6 @@ in
|
||||||
# bind-mounts and gateway upstream config stay in sync.
|
# bind-mounts and gateway upstream config stay in sync.
|
||||||
HIVE_WEB_SOCKET = "/run/hive-agent/${userName}/web.sock";
|
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 {
|
// lib.optionalAttrs config.hyperhive.gui.enable {
|
||||||
# Tells the harness which fixed VNC port weston bound, and (by
|
# Tells the harness which fixed VNC port weston bound, and (by
|
||||||
# its presence) that gui is enabled — the harness `/screen/ws`
|
# its presence) that gui is enabled — the harness `/screen/ws`
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue