subagents: add availableToSubagents opt-in toggle for extraMcpServers
This commit is contained in:
parent
33aa2bdbc8
commit
16eec3c314
14 changed files with 356 additions and 84 deletions
|
|
@ -10,5 +10,6 @@
|
|||
//! in-memory `name -> Cancel` map, live only as long as the process is.
|
||||
|
||||
pub mod mcp;
|
||||
pub mod mcp_config;
|
||||
pub mod paths;
|
||||
pub mod session;
|
||||
|
|
|
|||
56
hive-subagent-mcp/src/mcp_config.rs
Normal file
56
hive-subagent-mcp/src/mcp_config.rs
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
//! Builds a subagent's own filtered `--mcp-config`: only
|
||||
//! `hyperhive.extraMcpServers` entries with `availableToSubagents = true`
|
||||
//! (see `hive_extra_mcp::ExtraMcpServer::available_to_subagents`) ever reach
|
||||
//! a subagent's claude invocation. Everything else — the built-in hyperhive
|
||||
//! surface (todos/messaging), the auto-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.
|
||||
|
||||
use std::path::PathBuf;
|
||||
|
||||
/// Filename the rendered config lives at, under [`crate::paths::harness_dir`].
|
||||
const CONFIG_FILE: &str = "subagent-mcp-config.json";
|
||||
|
||||
/// Render the subagent-eligible extra-MCP servers to a `--mcp-config` file,
|
||||
/// returning its path — or `None` when no entry opts in (or the render/write
|
||||
/// fails), so [`hive_claude::Config::mcp_config`] stays unset and the
|
||||
/// subagent gets literally zero MCP servers, the same default as before this
|
||||
/// toggle existed. 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() -> Option<PathBuf> {
|
||||
let state_dir = crate::paths::state_dir();
|
||||
let servers: serde_json::Map<String, serde_json::Value> = hive_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 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);
|
||||
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)
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
//! The one per-agent path this daemon needs.
|
||||
//! The per-agent paths this daemon needs.
|
||||
|
||||
use std::path::PathBuf;
|
||||
|
||||
|
|
@ -13,3 +13,28 @@ pub fn agent_socket() -> PathBuf {
|
|||
PathBuf::from,
|
||||
)
|
||||
}
|
||||
|
||||
/// Durable state directory for the current agent. Same resolution as
|
||||
/// `hive-agent`'s own `paths::state_dir` — reads `HYPERHIVE_STATE_DIR`
|
||||
/// (always injected via `systemd.globalEnvironment` by the meta flake),
|
||||
/// falling back to the `HIVE_LABEL`-derived pattern for dev/test
|
||||
/// environments where the env var may not be set. Copied rather than shared
|
||||
/// with `hive-agent` (a binary crate with no lib target) — a five-line env
|
||||
/// read isn't worth a crate dependency to dedupe.
|
||||
#[must_use]
|
||||
pub fn state_dir() -> PathBuf {
|
||||
if let Some(p) = std::env::var_os("HYPERHIVE_STATE_DIR") {
|
||||
return PathBuf::from(p);
|
||||
}
|
||||
let label = std::env::var("HIVE_LABEL").unwrap_or_default();
|
||||
PathBuf::from(format!("/agents/{label}/state"))
|
||||
}
|
||||
|
||||
/// Harness-internal scratch dir this daemon writes its own generated
|
||||
/// subagent `--mcp-config` file into (see `crate::mcp_config`). Delegates to
|
||||
/// the shared resolver so this and `hive-agent`'s own harness dir always
|
||||
/// agree on the same path.
|
||||
#[must_use]
|
||||
pub fn harness_dir() -> PathBuf {
|
||||
hive_agent_sock::paths::harness_dir()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -185,8 +185,14 @@ fn subagent_otel_attrs(name: &str) -> String {
|
|||
/// instructions. `dir`, when given, becomes `Config::cwd` (e.g. a worktree
|
||||
/// the caller already prepared); `None` inherits this daemon's own working
|
||||
/// directory, same as before this field existed. Always
|
||||
/// `--dangerously-skip-permissions --strict-mcp-config` (no `--mcp-config`
|
||||
/// override — a safety property, not a knob).
|
||||
/// `--dangerously-skip-permissions --strict-mcp-config` — the safety
|
||||
/// property is `strict_mcp_config: true` with no ambient MCP discovery, not
|
||||
/// an unconditional absence of `--mcp-config`: a subagent gets exactly the
|
||||
/// `hyperhive.extraMcpServers` entries an operator has explicitly opted in
|
||||
/// via `availableToSubagents = true` (`crate::mcp_config::build`), nothing
|
||||
/// implicit and nothing more. With no entry opted in — the default — that
|
||||
/// resolves to `None` and the invocation is unchanged from before this
|
||||
/// toggle existed: zero MCP servers, full stop.
|
||||
fn build_config(
|
||||
name: &str,
|
||||
model: Option<String>,
|
||||
|
|
@ -201,6 +207,7 @@ fn build_config(
|
|||
Config {
|
||||
model,
|
||||
cwd: dir.map(PathBuf::from),
|
||||
mcp_config: crate::mcp_config::build(),
|
||||
strict_mcp_config: true,
|
||||
extra_args,
|
||||
env: vec![(
|
||||
|
|
|
|||
Loading…
Reference in a new issue