Watch
0
0
Fork
You've already forked hyperhive
0

hive-runtime, hive-agent: MCP permission only for kind other

A permission request counted as an MCP tool call whenever its title
looked like `<server>_<tool>`, whatever its kind, and acp_permits
allowed MCP calls before looking at the kind. So an `execute` request
titled e.g. `hyperhive_x`, or a `fetch` without web_tools, was allowed.

MCP tool calls come with kind `other` (opencode's toToolKind maps every
tool it doesn't name, MCP tools included, to "other"). The runtime now
sets PermissionAsk::mcp_server only for kind `other`, and acp_permits
allows an MCP server's tool only under the `other` arm.

Refs #4391
This commit is contained in:
atlas 2026-09-29 22:14:06 +02:00 • committed by mara
commit bed72ce280
3 changed files with 49 additions and 10 deletions

View file

@ -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 `<server>_<tool>`,
/// `<server>__<tool>` or `mcp__<server>__<tool>`.
/// 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
/// `<server>_<tool>`, `<server>__<tool>` or `mcp__<server>__<tool>`.
/// `None` for every other kind.
pub mcp_server: Option<&'a str>,
}

View file

@ -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 `<server>_<tool>` 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" },