add allowedBashPatterns NixOS option for finer-grained Bash tool approval

This commit is contained in:
damocles 2026-05-20 13:08:15 +02:00
parent c05a750409
commit 3c672ed6b2
3 changed files with 88 additions and 10 deletions

View file

@ -1269,12 +1269,30 @@ pub fn allowed_mcp_tools(flavor: Flavor) -> Vec<String> {
}
/// 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<String> = 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();
all.extend(allowed_mcp_tools(flavor));
all.join(",")
@ -1287,6 +1305,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.<key>` block in the rendered claude config + a
@ -1357,9 +1382,26 @@ fn default_allowed_tools() -> Vec<String> {
vec!["*".to_owned()]
}
/// Read + parse the extra-MCP spec. Returns an empty map on missing /
/// unparsable file (the agent has none configured, or the file is
/// malformed — both cases degrade to "no extra servers").
/// 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").
fn load_extra_mcp() -> std::collections::BTreeMap<String, ExtraMcpServer> {
let Ok(raw) = std::fs::read_to_string(EXTRA_MCP_PATH) else {
return std::collections::BTreeMap::new();