//! Builds a subagent's own filtered `--mcp-config`: only //! `hyperhive.extraMcpServers` entries with `availableToSubagents = true` //! (see `hive_agent_sock::extra_mcp::ExtraMcpServer::available_to_subagents`) ever reach //! a subagent's claude invocation, plus this daemon's own two-tool signal //! surface. Everything else — the built-in hyperhive //! surface (todos/messaging), the automatically injected `bash` and `subagent` //! entries — stays unreachable by construction: none of those default to //! opted in, and the built-in surface isn't an `extraMcpServers` entry at //! all, so there's no name for an operator to opt it in under even if they //! wanted to. //! //! The signal surface is the one server a subagent always gets. It is a //! *different route* on this daemon's listener from the one the parent //! uses, serving `goal_reached` and `need_help` and nothing else — so //! "a subagent can say it is done or stuck" never widens into "a subagent //! can spawn subagents", which is what handing it the parent's route would //! have meant. //! //! **One file per session, not one file.** The signal URL carries the //! session's own token (`State::mint_signal_url`), so the //! file it is written into has to be the session's own too: a single shared //! path is a race between two `start`s — whichever wrote last decides which //! token the other's `claude` reads at startup, which would hand a subagent //! a sibling's identity by accident. use std::path::PathBuf; /// Filename the rendered config lives at, under [`crate::paths::harness_dir`], /// per session. The name is a validated [`hive_types::Ident`] by the time it /// gets here (`crate::session::validate_name`), so it is a single safe path /// segment and never escapes the directory. fn config_file(name: &str) -> String { format!("subagent-mcp-config-{name}.json") } /// Name the signal surface appears under in a subagent's own MCP config, /// and therefore the prefix its tools are called by /// (`mcp__subagent_control__goal_reached`). const SIGNAL_SERVER: &str = "subagent_control"; /// Render a subagent's `--mcp-config` file, returning its path — or `None` /// when there is nothing to put in it (or the render/write fails), so /// [`hive_claude::Config::mcp_config`] stays unset and the subagent gets /// literally zero MCP servers. /// /// `signal_url` is `name`'s *own* `goal_reached`/`need_help` route — this /// daemon's signal path plus the token minted for this run, and the only /// place that token is ever written. It is always included when given, since /// a subagent that can't say it's done or stuck is exactly the one the turn /// cap has to stop on its behalf. `None` reproduces the pre-continuation /// shape — only the opted-in extras, and no file at all when none opt in. /// /// Re-rendered on every call (cheap: a filter plus a small /// file write) rather than cached once at daemon startup, so a config change /// takes effect on this subagent's next `start`/`continue` without needing /// the daemon itself restarted. #[must_use] pub fn build(name: &str, signal_url: Option<&str>) -> Option { let state_dir = crate::paths::state_dir(); let mut servers: serde_json::Map = hive_agent_sock::extra_mcp::load_extra_mcp() .into_iter() .filter(|(_, spec)| spec.available_to_subagents()) .map(|(name, spec)| (name, spec.to_json_entry(&state_dir))) .collect(); if let Some(url) = signal_url { servers.insert( SIGNAL_SERVER.to_owned(), serde_json::json!({ "type": "http", "url": url }), ); } if servers.is_empty() { return None; } let body = serde_json::to_string_pretty(&serde_json::json!({ "mcpServers": servers })) .unwrap_or_else(|_| "{}".into()); let dir = crate::paths::harness_dir(); if let Err(e) = std::fs::create_dir_all(&dir) { tracing::warn!( error = ?e, dir = %dir.display(), "subagent mcp-config: harness dir create failed; subagent gets zero extra MCP servers this turn", ); return None; } let path = dir.join(config_file(name)); if let Err(e) = std::fs::write(&path, body) { tracing::warn!( error = ?e, path = %path.display(), "subagent mcp-config: write failed; subagent gets zero extra MCP servers this turn", ); return None; } Some(path) }