Watch
0
0
Fork
You've already forked hyperhive
0

hive-agent: default-deny ACP permission requests

acp_permits allows tools of the session's MCP servers, the read, edit
and search kinds, and fetch with web_tools; every other kind, including
execute, other and kinds it doesn't know, is refused. Before, anything
but execute (and fetch without web_tools) was allowed, which was only
safe while the preset's own config denied the risky built-ins.

Refs #4391
This commit is contained in:
atlas 2026-09-29 21:47:28 +02:00 • committed by mara
commit 868fc789d1

View file

@ -9,8 +9,8 @@ use std::path::{Path, PathBuf};
use anyhow::Result;
use hive_runtime::{
AcpRuntime, AgentRuntime, ClaudeRuntime, Config, PercentPolicy, PermissionPolicy, Runtime,
RuntimeSpec, Sink,
AcpRuntime, AgentRuntime, ClaudeRuntime, Config, PercentPolicy, PermissionAsk,
PermissionPolicy, Runtime, RuntimeSpec, Sink,
};
use serde_json::Value;
@ -334,18 +334,33 @@ pub fn make_session(bus: &Bus) -> Result<AgentSession> {
})
}
/// Which ACP tool-call kinds an ACP agent may run when it asks. Mirrors the
/// claude built-ins (`hive_sh4re::permissions`): never a built-in shell
/// (`execute`) — shell goes through `mcp__bash__run` — and web access
/// (`fetch`) only with the `web_tools` group.
/// The answer to an ACP agent's permission requests, see [`acp_permits`].
fn acp_permission_policy() -> PermissionPolicy {
let web =
mcp_config::effective_tool_groups().contains(&hive_sh4re::permissions::ToolGroup::WebTools);
std::sync::Arc::new(move |kind: &str| match kind {
"execute" => false,
std::sync::Arc::new(move |ask: &PermissionAsk<'_>| acp_permits(ask, web))
}
/// 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`);
/// - the file tools, mirroring the claude built-ins
/// (`hive_sh4re::permissions`): `read`, `edit`, `search`;
/// - `fetch` with the `web_tools` group (`web`).
///
/// 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 {
"read" | "edit" | "search" => true,
"fetch" => web,
_ => true,
})
_ => false,
}
}
/// Drive one turn end-to-end. The durable [`AgentSession`] owns the
@ -795,7 +810,7 @@ fn archive_session(bus: &Bus, session: &AgentSession) {
#[cfg(test)]
mod tests {
use super::{nonzero_or, parse_u64};
use super::{PermissionAsk, acp_permits, nonzero_or, parse_u64};
#[test]
fn an_absent_or_blank_value_is_not_a_number() {
@ -840,4 +855,45 @@ mod tests {
"rejected: 0 is not a duration"
);
}
fn ask(kind: &str) -> PermissionAsk<'_> {
PermissionAsk {
kind,
mcp_server: None,
}
}
#[test]
fn acp_permissions_default_to_deny() {
for kind in [
"execute",
"delete",
"move",
"switch_mode",
"other",
"think",
"",
"new_kind",
] {
assert!(!acp_permits(&ask(kind), true), "{kind:?} was allowed");
}
}
#[test]
fn acp_file_tools_and_mcp_servers_are_allowed() {
for kind in ["read", "edit", "search"] {
assert!(acp_permits(&ask(kind), false), "{kind:?} was refused");
}
let mcp = PermissionAsk {
kind: "other",
mcp_server: Some("hyperhive"),
};
assert!(acp_permits(&mcp, false));
}
#[test]
fn acp_fetch_follows_the_web_tools_group() {
assert!(acp_permits(&ask("fetch"), true));
assert!(!acp_permits(&ask("fetch"), false));
}
}