diff --git a/hive-agent/src/turn.rs b/hive-agent/src/turn.rs index aeadb1d2..8d05dfc2 100644 --- a/hive-agent/src/turn.rs +++ b/hive-agent/src/turn.rs @@ -344,8 +344,9 @@ fn acp_permission_policy() -> PermissionPolicy { /// Whether an ACP agent may run the tool call it asks about. Default-deny, /// allowing what a claude agent has: /// -/// - tools of the MCP servers the session was handed — which of those an -/// agent gets is already decided by its tool groups (`mcp_config`); +/// - `other`-kind calls to tools of the MCP servers the session was handed +/// — which of those an agent gets is already decided by its tool groups +/// (`mcp_config`); /// - the file tools, mirroring the claude built-ins /// (`hive_sh4re::permissions`): `read`, `edit`, `search`; /// - `fetch` with the `web_tools` group (`web`). @@ -353,10 +354,8 @@ fn acp_permission_policy() -> PermissionPolicy { /// Everything else is refused, including a built-in shell (`execute`) — /// shell goes through `mcp__bash__run` — and any kind this list doesn't name. fn acp_permits(ask: &PermissionAsk<'_>, web: bool) -> bool { - if ask.mcp_server.is_some() { - return true; - } match ask.kind { + "other" => ask.mcp_server.is_some(), "read" | "edit" | "search" => true, "fetch" => web, _ => false, @@ -891,6 +890,20 @@ mod tests { assert!(acp_permits(&mcp, false)); } + #[test] + fn an_mcp_looking_title_does_not_lift_execute_or_fetch() { + let execute = PermissionAsk { + kind: "execute", + mcp_server: Some("hyperhive"), + }; + assert!(!acp_permits(&execute, true)); + let fetch = PermissionAsk { + kind: "fetch", + mcp_server: Some("hyperhive"), + }; + assert!(!acp_permits(&fetch, false)); + } + #[test] fn acp_fetch_follows_the_web_tools_group() { assert!(acp_permits(&ask("fetch"), true)); diff --git a/hive-runtime/src/acp/mod.rs b/hive-runtime/src/acp/mod.rs index 73b60828..fbe29b88 100644 --- a/hive-runtime/src/acp/mod.rs +++ b/hive-runtime/src/acp/mod.rs @@ -34,9 +34,10 @@ pub struct PermissionAsk<'a> { /// The tool call's ACP `kind` (`read`, `edit`, `execute`, `fetch`, …; /// `other` when the agent gives none). pub kind: &'a str, - /// The MCP server, of those handed to the session, whose tool this is: - /// the tool call's title names it as `_`, - /// `__` or `mcp____`. + /// For a `kind` of `other`: the MCP server, of those handed to the + /// session, whose tool this is — the tool call's title names it as + /// `_`, `__` or `mcp____`. + /// `None` for every other kind. pub mcp_server: Option<&'a str>, } diff --git a/hive-runtime/src/acp/rpc.rs b/hive-runtime/src/acp/rpc.rs index 076024b6..d3db7cea 100644 --- a/hive-runtime/src/acp/rpc.rs +++ b/hive-runtime/src/acp/rpc.rs @@ -239,9 +239,14 @@ pub(super) fn permission_outcome( ) -> Value { let tool_call = params.get("toolCall"); let field = |k: &str| tool_call.and_then(|t| t.get(k)).and_then(Value::as_str); + let kind = field("kind").unwrap_or("other"); let ask = PermissionAsk { - kind: field("kind").unwrap_or("other"), - mcp_server: field("title").and_then(|title| mcp_server_of(title, servers)), + kind, + // MCP tool calls are kind `other`; a title alone must not lift another + // kind (`execute`, `fetch`) into one. + mcp_server: (kind == "other") + .then(|| field("title").and_then(|title| mcp_server_of(title, servers))) + .flatten(), }; let wanted = if permit(&ask) { ["allow_once", "allow_always"] @@ -334,6 +339,26 @@ mod tests { ); } + /// A title shaped like `_` marks an MCP call only on kind + /// `other`, which is what an agent gives an MCP tool call. + #[test] + fn an_mcp_looking_title_on_execute_or_fetch_is_not_an_mcp_call() { + let policy: PermissionPolicy = Arc::new(|ask: &PermissionAsk<'_>| ask.mcp_server.is_some()); + for kind in ["execute", "fetch"] { + assert_eq!( + permission_outcome(&request(kind, "hyperhive_send"), &policy, &servers())["outcome"] + ["optionId"], + "reject", + "{kind}" + ); + } + assert_eq!( + permission_outcome(&request("other", "hyperhive_send"), &policy, &servers())["outcome"] + ["optionId"], + "once" + ); + } + #[test] fn no_matching_option_cancels() { let req = json!({ "toolCall": { "kind": "execute" },