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
|
|
@ -23,6 +23,7 @@ clap.workspace = true
|
|||
hive-claude.workspace = true
|
||||
hive-agent-sock.workspace = true
|
||||
hive-core-agent-sock.workspace = true
|
||||
hive-extra-mcp.workspace = true
|
||||
hive-sh4re.workspace = true
|
||||
hive-sock-client.workspace = true
|
||||
libc.workspace = true
|
||||
|
|
|
|||
|
|
@ -184,7 +184,7 @@ pub fn allowed_mcp_tools(groups: &[hive_sh4re::permissions::ToolGroup]) -> Vec<S
|
|||
// pattern list to `mcp__<server>__<pattern>` so claude can call
|
||||
// them without per-tool operator approval. `["*"]` (the default)
|
||||
// expands to `mcp__<server>__*` — every tool from that server.
|
||||
for (server, spec) in load_extra_mcp() {
|
||||
for (server, spec) in hive_extra_mcp::load_extra_mcp() {
|
||||
if server == SERVER_NAME || !extra_server_enabled(&server, groups) {
|
||||
continue;
|
||||
}
|
||||
|
|
@ -241,70 +241,6 @@ pub fn builtin_tools_arg() -> String {
|
|||
tools.join(",")
|
||||
}
|
||||
|
||||
/// Where the NixOS module writes the per-agent extra-MCP spec (see
|
||||
/// `nix/agent-modules/mcp.nix`). Each entry becomes an additional
|
||||
/// `mcpServers.<key>` block in the rendered claude config + a
|
||||
/// `mcp__<key>__<tool>` pattern in `--allowedTools`.
|
||||
const EXTRA_MCP_PATH: &str = "/etc/hyperhive/extra-mcp.json";
|
||||
|
||||
/// An extra MCP server declared via `hyperhive.extraMcpServers`. Two
|
||||
/// transports: `Stdio` (claude spawns `command` fresh each turn, talks
|
||||
/// JSON-RPC over its stdin/stdout) and `Http` (claude points at a
|
||||
/// long-lived streamable-http `url` instead — no per-turn spawn, no
|
||||
/// re-registration race, same shape as the built-in hyperhive surface).
|
||||
/// Internally tagged on the nix-rendered `type` field; unrecognised
|
||||
/// fields for the inactive variant (e.g. `command` on an `Http` entry)
|
||||
/// are ignored by serde's default struct deserialization.
|
||||
#[derive(Debug, serde::Deserialize)]
|
||||
#[serde(tag = "type", rename_all = "snake_case")]
|
||||
enum ExtraMcpServer {
|
||||
Stdio {
|
||||
command: String,
|
||||
#[serde(default)]
|
||||
args: Vec<String>,
|
||||
#[serde(default)]
|
||||
env: std::collections::BTreeMap<String, String>,
|
||||
#[serde(default = "default_allowed_tools")]
|
||||
#[serde(rename = "allowedTools")]
|
||||
allowed_tools: Vec<String>,
|
||||
},
|
||||
Http {
|
||||
url: String,
|
||||
#[serde(default = "default_allowed_tools")]
|
||||
#[serde(rename = "allowedTools")]
|
||||
allowed_tools: Vec<String>,
|
||||
},
|
||||
}
|
||||
|
||||
impl ExtraMcpServer {
|
||||
fn allowed_tools(&self) -> &[String] {
|
||||
match self {
|
||||
Self::Stdio { allowed_tools, .. } | Self::Http { allowed_tools, .. } => allowed_tools,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn default_allowed_tools() -> Vec<String> {
|
||||
vec!["*".to_owned()]
|
||||
}
|
||||
|
||||
/// Read + parse the extra-MCP spec. Returns an empty map when
|
||||
/// the file is missing or unparsable (the agent has none configured,
|
||||
/// or the file is malformed — both cases degrade to "no extra servers").
|
||||
fn load_extra_mcp() -> std::collections::BTreeMap<String, ExtraMcpServer> {
|
||||
let Ok(raw) = std::fs::read_to_string(EXTRA_MCP_PATH) else {
|
||||
return std::collections::BTreeMap::new();
|
||||
};
|
||||
serde_json::from_str(&raw).unwrap_or_else(|e| {
|
||||
tracing::warn!(
|
||||
path = EXTRA_MCP_PATH,
|
||||
error = ?e,
|
||||
"extra-mcp spec parse failed; ignoring",
|
||||
);
|
||||
std::collections::BTreeMap::new()
|
||||
})
|
||||
}
|
||||
|
||||
/// Render the MCP config blob claude reads from `--mcp-config <path>`.
|
||||
/// The built-in `hyperhive` surface is an HTTP entry pointing at the
|
||||
/// persistent `hive-mcp-http` daemon (see [`DEFAULT_MCP_HTTP_PORT`]); there
|
||||
|
|
@ -364,7 +300,7 @@ fn build_mcp_servers() -> serde_json::Map<String, serde_json::Value> {
|
|||
// agent isn't entitled to must not even appear in the MCP config, or the
|
||||
// agent could call it directly (there is no later enforcement point).
|
||||
let groups = effective_tool_groups();
|
||||
for (name, spec) in load_extra_mcp() {
|
||||
for (name, spec) in hive_extra_mcp::load_extra_mcp() {
|
||||
if name == SERVER_NAME {
|
||||
tracing::warn!(
|
||||
"extra MCP server name `{SERVER_NAME}` collides with the built-in surface; ignoring",
|
||||
|
|
@ -378,20 +314,7 @@ fn build_mcp_servers() -> serde_json::Map<String, serde_json::Value> {
|
|||
);
|
||||
continue;
|
||||
}
|
||||
let entry = match spec {
|
||||
ExtraMcpServer::Stdio {
|
||||
command,
|
||||
args,
|
||||
mut env,
|
||||
..
|
||||
} => {
|
||||
env.entry("HYPERHIVE_STATE_DIR".to_owned())
|
||||
.or_insert_with(|| state_dir.display().to_string());
|
||||
serde_json::json!({ "command": command, "args": args, "env": env })
|
||||
}
|
||||
ExtraMcpServer::Http { url, .. } => serde_json::json!({ "type": "http", "url": url }),
|
||||
};
|
||||
servers.insert(name, entry);
|
||||
servers.insert(name, spec.to_json_entry(&state_dir));
|
||||
}
|
||||
servers
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue