feat(#1058): disallow built-in Bash; remove allowedBashPatterns; prompt sweep

This commit is contained in:
damocles 2026-06-02 01:14:24 +02:00
commit 88f22065b0
5 changed files with 16 additions and 87 deletions

View file

@ -569,22 +569,13 @@ status hint moved to the wake prompt + UI header.
### Tool whitelist (`mcp::ALLOWED_BUILTIN_TOOLS`)
- Allowed built-ins: `Bash`, `Edit`, `Glob`, `Grep`, `Read`, `Write`.
- Denied by omission: `WebFetch`, `WebSearch`, `Task`,
`NotebookEdit`, `TodoWrite`.
- Allowed built-ins: `Edit`, `Glob`, `Grep`, `Read`, `Write`.
- Denied by omission or `claude-settings.json` deny list: `Bash`,
`WebFetch`, `WebSearch`, `Task`, `NotebookEdit`, `TodoWrite`.
- Allowed MCP tools: as listed above per flavor.
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.
`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.

View file

@ -3,6 +3,6 @@
"autoMemoryEnabled": false,
"effortLevel": "medium",
"permissions": {
"deny": ["WebFetch", "WebSearch", "Task", "TodoWrite"]
"deny": ["Bash", "WebFetch", "WebSearch", "Task", "TodoWrite"]
}
}

View file

@ -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.
<!-- /role:agent -->
**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 <branch>`, `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)`).
**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/<org>/<repo>.git` (credentials are pre-configured).
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 <sub>` runs triage queries (`unassigned`, `no-reviewer --reviewer NAME`, `stale-branches [--days N]`, `assignments [--user NAME]`). Default repo comes from `HIVE_FORGE_REPO`; pass `-r <repo>` (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 <branch> [--base main] [--body "..." | --body-file <path>] [--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 <path>] [--assignee <user>]`. `--body-file -` means stdin, so a HEREDOC body works naturally: `hive-forge comment <num> --body-file - <<EOF ... EOF`. To attach a file: `hive-forge attach-issue <number> <file>` / `hive-forge attach-comment <comment-id> <file>` — both print the `browser_download_url`. Key ops: `hive-forge diff <pr>` prints the unified diff; `hive-forge subscription [--watch|--ignore|--unwatch]` manages repo watch state. Note: forge notifications are delivered via the internal message daemon.
<!-- role:agent -->

View file

@ -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 for now; `Bash` is allowed pending a
/// finer-grained allow-list system for shell command patterns. `TodoWrite`
/// (`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`
/// 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. Edit later as our trust model
/// evolves.
pub const ALLOWED_BUILTIN_TOOLS: &[&str] = &["Bash", "Edit", "Glob", "Grep", "Read", "Write"];
/// should plan in /state notes instead.
pub const ALLOWED_BUILTIN_TOOLS: &[&str] = &["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,27 +2032,12 @@ pub fn allowed_mcp_tools(groups: &[hive_sh4re::ToolGroup]) -> Vec<String> {
}
/// Combined allow-list passed to `--allowedTools` (auto-approve) — covers
/// 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.
/// both the built-ins and the MCP surface.
#[must_use]
pub fn allowed_tools_arg(flavor: Flavor) -> String {
let mut all: Vec<String> = ALLOWED_BUILTIN_TOOLS
.iter()
.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()]
}
})
.map(|s| (*s).to_owned())
.collect();
let groups = effective_tool_groups(flavor);
all.extend(allowed_mcp_tools(&groups));
@ -2073,13 +2058,6 @@ 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.<key>` block in the rendered claude config + a
@ -2164,20 +2142,6 @@ fn default_allowed_tools() -> Vec<String> {
/// 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<String> {
let Ok(raw) = std::fs::read_to_string(BASH_ALLOW_PATH) else {
return Vec::new();
};
serde_json::from_str::<Vec<String>>(&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").

View file

@ -129,29 +129,6 @@ in
'';
};
options.hyperhive.allowedBashPatterns = lib.mkOption {
type = lib.types.listOf lib.types.str;
default = [ ];
example = [
"git *"
"ls *"
"cat /agents/*/state/*"
];
description = ''
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.
'';
};
options.hyperhive.allowedRecipients = lib.mkOption {
type = lib.types.listOf lib.types.str;
default = [ ];
@ -824,9 +801,6 @@ 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;