diff --git a/hive-agent/src/turn.rs b/hive-agent/src/turn.rs index 535a9693..aeadb1d2 100644 --- a/hive-agent/src/turn.rs +++ b/hive-agent/src/turn.rs @@ -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 { }) } -/// 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)); + } }