harness: gate the bash mcp server behind the execution tool group
This commit is contained in:
parent
6c3a83ffa1
commit
3795ffc1c6
1 changed files with 67 additions and 1 deletions
|
|
@ -1840,6 +1840,60 @@ fn effective_tool_groups() -> Vec<hive_sh4re::ToolGroup> {
|
||||||
groups
|
groups
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Tool group an extra (out-of-process) MCP server is gated behind, if any.
|
||||||
|
///
|
||||||
|
/// Most `hyperhive.extraMcpServers` entries are ungated — available whenever
|
||||||
|
/// the operator declares them. The `bash` server is the exception: raw shell
|
||||||
|
/// execution is a privilege, so it is only exposed when the agent holds the
|
||||||
|
/// `Execution` tool group. Unlike the in-process hyperhive tools (gated at
|
||||||
|
/// dispatch) and the capability tools (re-checked server-side by hive-c0re),
|
||||||
|
/// an out-of-process server has **no** later enforcement point — once it is
|
||||||
|
/// in the claude MCP config the agent can call it. So this gate, applied at
|
||||||
|
/// config-render time, is the security boundary for those servers.
|
||||||
|
fn extra_server_required_group(server: &str) -> Option<hive_sh4re::ToolGroup> {
|
||||||
|
match server {
|
||||||
|
"bash" => Some(hive_sh4re::ToolGroup::Execution),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Whether an extra MCP server should be exposed to claude given the active
|
||||||
|
/// tool `groups`. A gated server (see [`extra_server_required_group`]) is
|
||||||
|
/// suppressed when the agent lacks its required group.
|
||||||
|
fn extra_server_enabled(server: &str, groups: &[hive_sh4re::ToolGroup]) -> bool {
|
||||||
|
extra_server_required_group(server).is_none_or(|required| groups.contains(&required))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod extra_server_gate_tests {
|
||||||
|
use super::{extra_server_enabled, extra_server_required_group};
|
||||||
|
use hive_sh4re::ToolGroup;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn bash_is_gated_behind_execution() {
|
||||||
|
assert_eq!(
|
||||||
|
extra_server_required_group("bash"),
|
||||||
|
Some(ToolGroup::Execution)
|
||||||
|
);
|
||||||
|
// Suppressed without Execution, even if other groups are present.
|
||||||
|
assert!(!extra_server_enabled(
|
||||||
|
"bash",
|
||||||
|
&[ToolGroup::Messaging, ToolGroup::Inbox]
|
||||||
|
));
|
||||||
|
// Available once Execution is granted.
|
||||||
|
assert!(extra_server_enabled("bash", &[ToolGroup::Execution]));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn other_servers_are_ungated() {
|
||||||
|
assert_eq!(extra_server_required_group("matrix"), None);
|
||||||
|
assert_eq!(extra_server_required_group("scraper"), None);
|
||||||
|
// An ungated server is available regardless of (even empty) groups.
|
||||||
|
assert!(extra_server_enabled("matrix", &[]));
|
||||||
|
assert!(extra_server_enabled("scraper", &[ToolGroup::Messaging]));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// MCP tools claude is allowed to call without prompting, derived from
|
/// MCP tools claude is allowed to call without prompting, derived from
|
||||||
/// the supplied tool groups. Adding a new `#[tool]` fn to a server impl
|
/// the supplied tool groups. Adding a new `#[tool]` fn to a server impl
|
||||||
/// requires updating the matching `ToolGroup::tools()` slice in hive-sh4re
|
/// requires updating the matching `ToolGroup::tools()` slice in hive-sh4re
|
||||||
|
|
@ -1864,7 +1918,7 @@ pub fn allowed_mcp_tools(groups: &[hive_sh4re::ToolGroup]) -> Vec<String> {
|
||||||
// them without per-tool operator approval. `["*"]` (the default)
|
// them without per-tool operator approval. `["*"]` (the default)
|
||||||
// expands to `mcp__<server>__*` — every tool from that server.
|
// expands to `mcp__<server>__*` — every tool from that server.
|
||||||
for (server, spec) in load_extra_mcp() {
|
for (server, spec) in load_extra_mcp() {
|
||||||
if server == SERVER_NAME {
|
if server == SERVER_NAME || !extra_server_enabled(&server, groups) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
for pat in spec.allowed_tools {
|
for pat in spec.allowed_tools {
|
||||||
|
|
@ -2040,6 +2094,11 @@ pub fn render_claude_config(agent_binary: &str, socket: &std::path::Path) -> Str
|
||||||
// agent's durable state dir without the agent author hard-coding it.
|
// agent's durable state dir without the agent author hard-coding it.
|
||||||
// User-supplied env takes precedence — we only fill in the missing key.
|
// User-supplied env takes precedence — we only fill in the missing key.
|
||||||
let state_dir = crate::paths::state_dir();
|
let state_dir = crate::paths::state_dir();
|
||||||
|
// Gate tool-group-restricted extra servers (e.g. `bash` → `Execution`).
|
||||||
|
// This is the security boundary for them: an out-of-process server the
|
||||||
|
// 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, mut spec) in load_extra_mcp() {
|
for (name, mut spec) in load_extra_mcp() {
|
||||||
if name == SERVER_NAME {
|
if name == SERVER_NAME {
|
||||||
tracing::warn!(
|
tracing::warn!(
|
||||||
|
|
@ -2047,6 +2106,13 @@ pub fn render_claude_config(agent_binary: &str, socket: &std::path::Path) -> Str
|
||||||
);
|
);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
if !extra_server_enabled(&name, &groups) {
|
||||||
|
tracing::info!(
|
||||||
|
server = %name,
|
||||||
|
"extra MCP server suppressed: agent lacks the required tool group"
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
spec.env
|
spec.env
|
||||||
.entry("HYPERHIVE_STATE_DIR".to_owned())
|
.entry("HYPERHIVE_STATE_DIR".to_owned())
|
||||||
.or_insert_with(|| state_dir.display().to_string());
|
.or_insert_with(|| state_dir.display().to_string());
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue