A `start` or `continue` naming a `model` now reads this agent's key in the `agent-inference` bucket and, when `subagent_models` is set, checks the model against that list instead of HIVE_AVAILABLE_MODELS (agent.nix `availableModels`). An unset field, an absent key or an absent bucket keeps today's check. A read that fails or takes over 2 s falls back to HIVE_AVAILABLE_MODELS with a warning, so a subagent start never waits on the queue. A call without a `model` is not checked and does not read. The read uses the agent's own queue credential through swarm_term's connect, and the one direct-get subject the agent is granted in that bucket. The refusal names the list and that it came from the swarm. Refs #5034 (S5)
68 lines
2.8 KiB
Rust
68 lines
2.8 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);
|
|
// The parent agent's runtime, from the same variables the harness reads.
|
|
let runtime = hive_runtime::RuntimeSpec::from_env()?;
|
|
let state = Arc::new(
|
|
hive_subagent_mcp::session::State::new(todo_socket, signal_base)
|
|
.on_runtime(runtime)
|
|
.publishing_to(hive_subagent_mcp::swarm_term::Publisher::spawn())
|
|
.reading_models_from(hive_subagent_mcp::swarm_models::Reader::spawn()),
|
|
);
|
|
|
|
// 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
|
|
}
|