From 7ec658851a782e11c0987aa8e6782f7ec794706b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?m=C3=BCde?= Date: Sat, 16 May 2026 15:58:41 +0200 Subject: [PATCH] back out bypassPermissions: claude refuses it under root uid claude-code rejects --dangerously-skip-permissions / defaultMode= bypassPermissions when running as root, which all hyperhive containers do. revert to the previous explicit allow-list plumbing (per-flavor list spliced into permissions.allow + --tools enable list), keep TodoWrite out of the built-in allow set, and keep the deny list (TodoWrite, WebFetch, WebSearch, Task) as belt-and-braces in case anything sneaks past the allow gate. --- hive-ag3nt/prompts/claude-settings.json | 6 +- hive-ag3nt/src/mcp.rs | 82 ++++++++++++++++++++++--- hive-ag3nt/src/turn.rs | 21 +++---- 3 files changed, 82 insertions(+), 27 deletions(-) diff --git a/hive-ag3nt/prompts/claude-settings.json b/hive-ag3nt/prompts/claude-settings.json index 56c2ef9..612237d 100644 --- a/hive-ag3nt/prompts/claude-settings.json +++ b/hive-ag3nt/prompts/claude-settings.json @@ -1,9 +1,5 @@ { "autoCompactEnabled": false, "autoMemoryEnabled": false, - "effortLevel": "medium", - "permissions": { - "defaultMode": "bypassPermissions", - "deny": ["WebFetch", "WebSearch", "Task", "TodoWrite"] - } + "effortLevel": "medium" } diff --git a/hive-ag3nt/src/mcp.rs b/hive-ag3nt/src/mcp.rs index 0a72639..26a5962 100644 --- a/hive-ag3nt/src/mcp.rs +++ b/hive-ag3nt/src/mcp.rs @@ -597,23 +597,86 @@ impl ServerHandler for ManagerServer {} /// tools as `mcp____` (e.g. `mcp__hyperhive__send`). pub const SERVER_NAME: &str = "hyperhive"; -/// Which hyperhive MCP surface to advertise — sub-agent (short tool -/// list) or manager (full lifecycle surface). Threaded through the -/// system-prompt renderer and the per-flavor web UI dispatch; tool -/// gating itself now lives in `claude-settings.json`'s -/// `permissions.{defaultMode, deny}`, not here. +/// Built-in claude tools the turn loop enables via `--tools`. Anything not +/// in this list literally doesn't exist in the session (claude won't even +/// try to call it). Web egress (`WebFetch`/`WebSearch`) and nested agents +/// (`Task`) are intentionally omitted for now; `Bash` is allowed pending a +/// finer-grained allow-list system for shell command patterns. Edit later +/// as our trust model evolves. +pub const ALLOWED_BUILTIN_TOOLS: &[&str] = + &["Bash", "Edit", "Glob", "Grep", "Read", "TodoWrite", "Write"]; + +/// Which MCP tool surface to advertise via `--allowedTools`. The agent +/// list is the strict subset of the manager list, so we just thread the +/// flavor through. #[derive(Debug, Clone, Copy)] pub enum Flavor { Agent, Manager, } +/// MCP tools claude is allowed to call without prompting. Mirrors the +/// hyperhive surface so a new tool added in the corresponding `#[tool_router]` +/// impl needs to be listed here too. +#[must_use] +pub fn allowed_mcp_tools(flavor: Flavor) -> Vec { + let names: &[&str] = match flavor { + Flavor::Agent => &["send", "recv", "ask_operator"], + Flavor::Manager => &[ + "send", + "recv", + "request_spawn", + "kill", + "start", + "restart", + "update", + "request_apply_commit", + "ask_operator", + ], + }; + let mut out: Vec = names + .iter() + .map(|t| format!("mcp__{SERVER_NAME}__{t}")) + .collect(); + // Extra MCP servers declared via `hyperhive.extraMcpServers` in + // the agent's NixOS config. Each entry maps its `allowedTools` + // pattern list to `mcp____` so claude can call + // them without per-tool operator approval. `["*"]` (the default) + // expands to `mcp____*` — every tool from that server. + for (server, spec) in load_extra_mcp() { + if server == SERVER_NAME { + continue; + } + for pat in spec.allowed_tools { + out.push(format!("mcp__{server}__{pat}")); + } + } + out +} + +/// Combined allow-list passed to `--allowedTools` (auto-approve) — covers +/// both the built-ins and the MCP surface. +#[must_use] +pub fn allowed_tools_arg(flavor: Flavor) -> String { + let mut all: Vec = ALLOWED_BUILTIN_TOOLS + .iter() + .map(|s| (*s).to_owned()) + .collect(); + all.extend(allowed_mcp_tools(flavor)); + all.join(",") +} + +/// Built-in tools list for `--tools` (which built-ins exist in this +/// session). Same as `ALLOWED_BUILTIN_TOOLS` but joined comma-separated. +#[must_use] +pub fn builtin_tools_arg() -> String { + ALLOWED_BUILTIN_TOOLS.join(",") +} + /// Where the NixOS module writes the per-agent extra-MCP spec (see /// `nix/templates/harness-base.nix`). Each entry becomes an additional -/// `mcpServers.` block in the rendered claude config; the -/// `allowedTools` field is parsed for back-compat but no longer wired -/// anywhere — under `bypassPermissions` every MCP tool auto-approves -/// unless listed in `permissions.deny`. +/// `mcpServers.` block in the rendered claude config + a +/// `mcp____` pattern in `--allowedTools`. const EXTRA_MCP_PATH: &str = "/etc/hyperhive/extra-mcp.json"; /// Where the NixOS module writes the per-agent send allow-list (see @@ -673,7 +736,6 @@ struct ExtraMcpServer { env: std::collections::BTreeMap, #[serde(default = "default_allowed_tools")] #[serde(rename = "allowedTools")] - #[allow(dead_code)] // back-compat: superseded by `permissions.deny` allowed_tools: Vec, } diff --git a/hive-ag3nt/src/turn.rs b/hive-ag3nt/src/turn.rs index b9fe6fe..e0c3ab0 100644 --- a/hive-ag3nt/src/turn.rs +++ b/hive-ag3nt/src/turn.rs @@ -22,13 +22,9 @@ use crate::mcp; /// to read and edit; we ship it via `include_str!`. We turn off claude's /// in-session auto-compaction and its cross-session auto-memory because /// hyperhive owns those concerns (`/compact` on overflow, notes -/// persistence under `/state`). `permissions.defaultMode = -/// bypassPermissions` skips the per-tool approval prompt entirely; -/// `permissions.deny` keeps a short list of tools we don't want claude -/// reaching for (web egress, nested agents, the ephemeral todo list). -/// Unknown keys are silently ignored by claude-code; if a key gets -/// renamed we'll spot it because the corresponding behavior will start -/// firing mid-turn again. +/// persistence under `/state`). Unknown keys are silently ignored by +/// claude-code; if a key gets renamed we'll spot it because the +/// corresponding behavior will start firing mid-turn again. const CLAUDE_SETTINGS: &str = include_str!("../prompts/claude-settings.json"); /// Regex-ish marker claude-code emits when context overflows. Same string @@ -85,10 +81,7 @@ pub async fn write_mcp_config(socket: &Path) -> Result { /// Drop the static `--settings` JSON next to the MCP config so we can /// pass a path (`--settings `) instead of an ever-growing inline -/// blob — the CLI argv has a finite length budget. The file carries -/// `permissions.defaultMode = bypassPermissions` + a small `deny` list, -/// so everything not in `deny` auto-approves without a per-flavor allow -/// list. +/// blob — the CLI argv has a finite length budget. pub async fn write_settings(socket: &Path) -> Result { let parent = socket.parent().unwrap_or_else(|| Path::new("/run/hive")); tokio::fs::create_dir_all(parent).await.ok(); @@ -252,7 +245,11 @@ async fn run_claude(prompt: &str, files: &TurnFiles, bus: &Bus) -> Result cmd.arg("--system-prompt-file").arg(&files.system_prompt); cmd.arg("--mcp-config") .arg(&files.mcp_config) - .arg("--strict-mcp-config"); + .arg("--strict-mcp-config") + .arg("--tools") + .arg(mcp::builtin_tools_arg()) + .arg("--allowedTools") + .arg(mcp::allowed_tools_arg(files.flavor)); let mut child = cmd .stdin(Stdio::piped()) .stdout(Stdio::piped())