hyperhive/hive-subagent-mcp/src/main.rs
atlas 34129d776c subagent: give each run its own signal URL, and drop the name argument
`goal_reached`/`need_help` took the session name as a tool argument, so
identity was an assertion by the caller and the only guard on it was
`occupancy()` — "does that name have a turn in flight", which two
concurrently running siblings both satisfy for each other. A subagent
could stop its sibling's run by naming it.

Identity moves into the URL. Each spawned run is minted an unguessable
token (`Uuid::new_v4`, the OS CSPRNG), the URL carrying it goes into that
one subagent's own `--mcp-config`, and the route resolves it back to a
session before dispatching to a handler bound to that session. Neither
tool takes a `name` any more: a subagent has no field in which to name a
sibling, and a sibling's name — which a brief may well mention — is not a
token.

One route with a path parameter, not a route per session: the `Router` is
built once at startup and subagents come and go for the daemon's whole
life. An unminted or revoked token gets a bare 404, the same answer either
way, so nothing enumerates. A run's token is revoked when the run ends
(`finish_turn`) or when a call never reached a spawn.

Two things fall out of that:

- the config file becomes one per session. A single shared path was
  already a race between two `start`s; with a per-session URL in it, the
  loser would read the winner's identity.
- `occupancy()` stops being the identity guard and is gone from the signal
  path entirely rather than kept "just in case" — a revoked token can't
  reach it, and it never answered the question it was standing in for.
  It still backs `status`, which is what it was always actually for.

Refs #4403
Refs #4413
2026-09-14 22:24:51 +02:00

64 lines
2.5 KiB
Rust

//! `hive-subagent-daemon` binary — spawns nested claude sessions on
//! request and serves the `start`/`continue`/`status`/`interrupt` MCP tool
//! surface directly over streamable-http on `--http <addr>` — no stdio
//! bridge, no separate bin claude has to respawn every turn. The same
//! address also carries the subagent-facing `goal_reached`/`need_help`
//! route, which is the only place the daemon can learn its own URL from.
use std::sync::Arc;
use anyhow::Result;
use clap::Parser;
#[derive(Parser)]
#[command(
name = "hive-subagent-daemon",
about = "claude-subagent runner + MCP daemon"
)]
struct Cli {
/// Serve the MCP tools over streamable-http on this address (e.g.
/// `127.0.0.1:8793`). Bind loopback only.
#[arg(long)]
http: std::net::SocketAddr,
}
#[tokio::main]
async fn main() -> Result<()> {
tracing_subscriber::fmt()
.with_env_filter(
tracing_subscriber::EnvFilter::try_from_env("RUST_LOG")
.unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("info")),
)
// This is a systemd-managed daemon — stdout always goes to journald,
// never a human terminal, and journald doesn't strip ANSI escapes:
// they land in victorialogs as raw byte-array spam otherwise.
.with_ansi(false)
.init();
let cli = Cli::parse();
let todo_socket = hive_subagent_mcp::paths::agent_socket();
tracing::info!(
http = %cli.http,
todo = %todo_socket.display(),
"hive-subagent-daemon starting"
);
// The one place the subagent-facing signal URLs can come from: the
// address this process was told to listen on. Anything else would be a
// guess at the deployment's own port assignment. This is only their
// common prefix — each session's actual URL is this plus a token minted
// for that session alone, which is what makes a signal's identity a
// property of the endpoint rather than of the payload.
let signal_base = format!("http://{}{}", cli.http, hive_subagent_mcp::mcp::SIGNAL_PATH);
let state = Arc::new(hive_subagent_mcp::session::State::new(
todo_socket,
signal_base,
));
// Serve the MCP tools over streamable-http forever. No background poll
// loop to start — unlike the bash daemon's task-file queue, `start`/
// `continue` spawn their subagent's background turn directly from the
// tool call itself, nothing to scan for.
hive_subagent_mcp::mcp::serve_http(cli.http, state).await
}