diff --git a/docs/turn-loop.md b/docs/turn-loop.md index 112f79fe..3f86a01b 100644 --- a/docs/turn-loop.md +++ b/docs/turn-loop.md @@ -569,13 +569,22 @@ status hint moved to the wake prompt + UI header. ### Tool whitelist (`mcp::ALLOWED_BUILTIN_TOOLS`) -- Allowed built-ins: `Edit`, `Glob`, `Grep`, `Read`, `Write`. -- Denied by omission or `claude-settings.json` deny list: `Bash`, - `WebFetch`, `WebSearch`, `Task`, `NotebookEdit`, `TodoWrite`. +- Allowed built-ins: `Bash`, `Edit`, `Glob`, `Grep`, `Read`, `Write`. +- Denied by omission: `WebFetch`, `WebSearch`, `Task`, + `NotebookEdit`, `TodoWrite`. - Allowed MCP tools: as listed above per flavor. -`Bash` is disallowed — shell execution goes through -`mcp__hyperhive__bash_run` (background tasks with structured output + -task-id tracking) instead of an interactive shell. The `bash_run` / -`bash_status` MCP tools are always in the `--allowedTools` list. +By default `Bash` is approved wholesale — any shell command runs +without confirmation. To restrict an agent to specific command +families, set `hyperhive.allowedBashPatterns` in its `agent.nix`: + +```nix +hyperhive.allowedBashPatterns = [ "git *" "ls *" ]; +``` + +The harness reads `/etc/hyperhive/bash-allow.json` and replaces +`Bash` in `--allowedTools` with `Bash(git *)` + `Bash(ls *)` etc. +Commands outside the pattern list require confirmation — which in +`--print` mode means they will not run. An empty list (default) keeps +the current wholesale `Bash` entry. diff --git a/hive-ag3nt/prompts/claude-settings.json b/hive-ag3nt/prompts/claude-settings.json index edf04d70..7fdf4373 100644 --- a/hive-ag3nt/prompts/claude-settings.json +++ b/hive-ag3nt/prompts/claude-settings.json @@ -3,6 +3,6 @@ "autoMemoryEnabled": false, "effortLevel": "medium", "permissions": { - "deny": ["Bash", "WebFetch", "WebSearch", "Task", "TodoWrite"] + "deny": ["WebFetch", "WebSearch", "Task", "TodoWrite"] } } diff --git a/hive-ag3nt/prompts/system.md b/hive-ag3nt/prompts/system.md index fe66b96d..d60084bd 100644 --- a/hive-ag3nt/prompts/system.md +++ b/hive-ag3nt/prompts/system.md @@ -128,7 +128,7 @@ Claude session (OAuth credentials) lives at `/root/.claude/` and persists across **Shared space**: `/shared` is accessible to all agents (read/write). Only put things here you're willing to lose — other agents may delete them. Use for explicit cross-agent communication or shared artifacts when appropriate. -**Code forge**: a private Forgejo at `http://localhost:3000` is available when `/agents/{label}/state/forge-token` exists. You have your own user account (named `{label}`). Use `hive-forge` (see below) for all forge operations — issues, PRs, comments, labels, etc. For git operations use plain `git` directly against `http://localhost:3000//.git` (credentials are pre-configured). +**Code forge**: a private Forgejo at `http://localhost:3000` is available when `/agents/{label}/state/forge-token` exists. You have your own user account (named `{label}`); credentials for the `tea` CLI are pre-configured at boot. Use `tea repos create`, `tea pulls create --base main --head `, `tea pulls list`, `tea issues create`, etc. for any persistent code work — git repos that should outlive a single turn, code you want a peer or the operator to review, anything you'd otherwise jam into `/shared`. Falls back to plain `git`/`curl` if `tea` doesn't fit; the REST API is at `http://localhost:3000/api/v1/` with the same token (`Authorization: token $(cat /agents/{label}/state/forge-token)`). The `hive-forge` CLI helper wraps common Forgejo API operations: `view`, `issue`, `issue-create`, `issue-edit`, `pr`, `pr-create`, `comment`, `comments`, `comment-show`, `comment-edit`, `assign`, `close`, `labels`, `lint`, `list`, `milestone`, `pr-reviews`, `branches`, `tree-sha`, `diff`, `subscription`, `attach-issue`, `attach-comment`. `lint ` runs triage queries (`unassigned`, `no-reviewer --reviewer NAME`, `stale-branches [--days N]`, `assignments [--user NAME]`). Default repo comes from `HIVE_FORGE_REPO`; pass `-r ` (global flag, works before or after the verb) to target a different repo. Every verb takes `--help` for its full signature. To create a PR: `hive-forge pr-create --title "..." --head [--base main] [--body "..." | --body-file ] [--draft] [--push [--remote forge]]` — prints the PR URL. Add `--push` to also `git push` the head branch before the API call (default remote: `forge`); the noisy post-push "Create a pull request" hint is suppressed since we print the canonical URL ourselves. To create an issue: `hive-forge issue-create --title "..." [--body "..." | --body-file ] [--assignee ]`. `--body-file -` means stdin, so a HEREDOC body works naturally: `hive-forge comment --body-file - < ` / `hive-forge attach-comment ` — both print the `browser_download_url`. Key ops: `hive-forge diff ` prints the unified diff; `hive-forge subscription [--watch|--ignore|--unwatch]` manages repo watch state. Note: forge notifications are delivered via the internal message daemon. diff --git a/hive-ag3nt/src/mcp.rs b/hive-ag3nt/src/mcp.rs index 9d181d63..4d79930a 100644 --- a/hive-ag3nt/src/mcp.rs +++ b/hive-ag3nt/src/mcp.rs @@ -1898,13 +1898,13 @@ pub const SERVER_NAME: &str = "hyperhive"; /// 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. `Bash` is disallowed — shell -/// execution goes through `mcp__hyperhive__bash_run` (background tasks -/// with structured output) instead of a raw interactive shell. `TodoWrite` +/// (`Task`) are intentionally omitted for now; `Bash` is allowed pending a +/// finer-grained allow-list system for shell command patterns. `TodoWrite` /// is omitted because the todo list lives in claude's in-process session /// state and silently evaporates on /compact or session reset — agents -/// should plan in /state notes instead. -pub const ALLOWED_BUILTIN_TOOLS: &[&str] = &["Edit", "Glob", "Grep", "Read", "Write"]; +/// should plan in /state notes instead. Edit later as our trust model +/// evolves. +pub const ALLOWED_BUILTIN_TOOLS: &[&str] = &["Bash", "Edit", "Glob", "Grep", "Read", "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 @@ -2032,12 +2032,27 @@ pub fn allowed_mcp_tools(groups: &[hive_sh4re::ToolGroup]) -> Vec { } /// Combined allow-list passed to `--allowedTools` (auto-approve) — covers -/// both the built-ins and the MCP surface. +/// both the built-ins and the MCP surface. If `hyperhive.allowedBashPatterns` +/// is configured (non-empty list in `/etc/hyperhive/bash-allow.json`), +/// `Bash` is replaced with one `Bash(pattern)` entry per pattern so +/// only vetted command families auto-approve without a blanket shell grant. +/// An empty or missing allow file keeps the current wholesale `Bash` entry. #[must_use] pub fn allowed_tools_arg(flavor: Flavor) -> String { let mut all: Vec = ALLOWED_BUILTIN_TOOLS .iter() - .map(|s| (*s).to_owned()) + .flat_map(|s| { + if *s == "Bash" { + let patterns = load_bash_allow(); + if patterns.is_empty() { + vec!["Bash".to_owned()] + } else { + patterns.into_iter().map(|p| format!("Bash({p})")).collect() + } + } else { + vec![(*s).to_owned()] + } + }) .collect(); let groups = effective_tool_groups(flavor); all.extend(allowed_mcp_tools(&groups)); @@ -2058,6 +2073,13 @@ pub fn builtin_tools_arg() -> String { ALLOWED_BUILTIN_TOOLS.join(",") } +/// Where the NixOS module writes the per-agent Bash command allow-list +/// (see `nix/templates/harness-base.nix`). Contains a JSON array of +/// command-pattern strings like `["git *", "ls *"]`. Empty array = +/// wholesale `Bash` approval (the default). Non-empty = one +/// `Bash(pattern)` entry per item in `--allowedTools`. +const BASH_ALLOW_PATH: &str = "/etc/hyperhive/bash-allow.json"; + /// 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 + a @@ -2139,6 +2161,23 @@ fn default_allowed_tools() -> Vec { vec!["*".to_owned()] } +/// Read + parse the Bash command allow-list. Returns an empty vec when +/// the file is missing or unparsable (degrade to wholesale `Bash` +/// approval — same as the pre-feature behaviour). +fn load_bash_allow() -> Vec { + let Ok(raw) = std::fs::read_to_string(BASH_ALLOW_PATH) else { + return Vec::new(); + }; + serde_json::from_str::>(&raw).unwrap_or_else(|e| { + tracing::warn!( + path = BASH_ALLOW_PATH, + error = ?e, + "bash-allow list parse failed; falling back to wholesale Bash approval", + ); + Vec::new() + }) +} + /// Read + parse the extra-MCP spec. Returns an empty map when /// the file is missing or unparsable (the agent has none configured, /// or the file is malformed — both cases degrade to "no extra servers"). diff --git a/nix/templates/harness-base.nix b/nix/templates/harness-base.nix index d59d11f8..627e66a9 100644 --- a/nix/templates/harness-base.nix +++ b/nix/templates/harness-base.nix @@ -132,12 +132,24 @@ in options.hyperhive.allowedBashPatterns = lib.mkOption { type = lib.types.listOf lib.types.str; default = [ ]; + example = [ + "git *" + "ls *" + "cat /agents/*/state/*" + ]; description = '' - Deprecated - has no effect. The built-in Bash tool is fully - disabled regardless of this list; agents use mcp__hyperhive__bash_run - instead. Remove this option from your agent.nix. + Shell command patterns auto-approved for the `Bash` built-in tool. + Empty list (the default) grants wholesale `Bash` approval — + claude can run any shell command without a prompt. Non-empty list + replaces `Bash` in `--allowedTools` with one `Bash(pattern)` entry + per item; only commands matching a pattern are auto-approved; all + others require confirmation (which in `--print` mode means they + will not run). Use to sandbox agents to a known-safe command + vocabulary. + + Patterns use the same glob syntax claude accepts in `Bash(…)`: + `*` matches any string within a word, shell-style. ''; - visible = false; }; options.hyperhive.allowedRecipients = lib.mkOption { @@ -610,12 +622,6 @@ in }; config = { - warnings = lib.optional (config.hyperhive.allowedBashPatterns != [ ]) '' - hyperhive.allowedBashPatterns is deprecated and has no effect. - The built-in Bash tool is fully disabled; agents use mcp__hyperhive__bash_run instead. - Remove allowedBashPatterns from your agent.nix. - ''; - assertions = [ # Guard the inputs-routed-as-output pattern: the agent flake.nix is # expected to set `_module.args.flakeInputs = builtins.removeAttrs inputs ["self"]`. @@ -818,6 +824,9 @@ in text = config.hyperhive._bashEnvFragments; }; + environment.etc."hyperhive/bash-allow.json".text = + builtins.toJSON config.hyperhive.allowedBashPatterns; + environment.etc."hyperhive/send-allow.json".text = builtins.toJSON config.hyperhive.allowedRecipients;