hive-sh4re: split tool-group + capability enums into their own topic module

This commit is contained in:
damocles 2026-08-10 22:54:32 +02:00 committed by mara
commit d3ac4de8fb
11 changed files with 313 additions and 296 deletions

View file

@ -35,14 +35,14 @@ pub const DEFAULT_MCP_HTTP_PORT: u16 = 8790;
pub const ALLOWED_BUILTIN_TOOLS: &[&str] = &["Edit", "Glob", "Grep", "Read", "Skill", "Write"];
/// Env var written by the meta renderer with a comma-separated list of
/// `hive_sh4re::ToolGroup` `snake_case` names (e.g. `"messaging,inbox,meta"`).
/// `hive_sh4re::permissions::ToolGroup` `snake_case` names (e.g. `"messaging,inbox,meta"`).
/// When present, the harness expands the groups into per-tool allow entries
/// instead of using the hardcoded flavor default. See `docs/conventions.md::Tool groups`.
const TOOL_GROUPS_ENV: &str = "HIVE_TOOL_GROUPS";
/// `HIVE_CAPABILITIES` env var injected by `meta::render_flake` when the
/// operator grants capabilities to this agent. Comma-separated
/// `hive_sh4re::Capability` `snake_case` names. Absent = no extra capabilities.
/// `hive_sh4re::permissions::Capability` `snake_case` names. Absent = no extra capabilities.
const CAPABILITIES_ENV: &str = "HIVE_CAPABILITIES";
/// Returns the MCP tool names (without `mcp__hyperhive__` prefix) that are
@ -87,18 +87,18 @@ fn allowed_capability_tools() -> Vec<String> {
/// (`messaging`, `meta`, `inbox`, `lifecycle`, `approvals`, `scheduling`,
/// `diagnostics`, `execution`). Unrecognised tokens are logged and skipped.
/// Falls back to `AGENT_DEFAULT` when the env var is absent or empty.
fn effective_tool_groups() -> Vec<hive_sh4re::ToolGroup> {
fn effective_tool_groups() -> Vec<hive_sh4re::permissions::ToolGroup> {
let raw = match std::env::var(TOOL_GROUPS_ENV) {
Ok(v) if !v.trim().is_empty() => v,
_ => return hive_sh4re::ToolGroup::AGENT_DEFAULT.to_vec(),
_ => return hive_sh4re::permissions::ToolGroup::AGENT_DEFAULT.to_vec(),
};
let mut groups = Vec::new();
for token in raw.split(',') {
let t = token.trim().to_ascii_lowercase();
// Parse via serde_json (the canonical deserialization path).
if let Ok(g) =
serde_json::from_value::<hive_sh4re::ToolGroup>(serde_json::Value::String(t.clone()))
{
if let Ok(g) = serde_json::from_value::<hive_sh4re::permissions::ToolGroup>(
serde_json::Value::String(t.clone()),
) {
groups.push(g);
} else {
tracing::warn!(token = %t, "{TOOL_GROUPS_ENV}: unknown tool group, skipping");
@ -109,7 +109,7 @@ fn effective_tool_groups() -> Vec<hive_sh4re::ToolGroup> {
"{TOOL_GROUPS_ENV} set but contained no recognised groups; \
falling back to AGENT_DEFAULT"
);
return hive_sh4re::ToolGroup::AGENT_DEFAULT.to_vec();
return hive_sh4re::permissions::ToolGroup::AGENT_DEFAULT.to_vec();
}
groups
}
@ -124,9 +124,9 @@ fn effective_tool_groups() -> Vec<hive_sh4re::ToolGroup> {
/// an out-of-process server has **no** later enforcement point — once it is
/// in the claude MCP config the agent can call it. So this gate, applied at
/// config-render time, is the security boundary for those servers.
fn extra_server_required_group(server: &str) -> Option<hive_sh4re::ToolGroup> {
fn extra_server_required_group(server: &str) -> Option<hive_sh4re::permissions::ToolGroup> {
match server {
"bash" => Some(hive_sh4re::ToolGroup::Execution),
"bash" => Some(hive_sh4re::permissions::ToolGroup::Execution),
_ => None,
}
}
@ -134,14 +134,14 @@ fn extra_server_required_group(server: &str) -> Option<hive_sh4re::ToolGroup> {
/// Whether an extra MCP server should be exposed to claude given the active
/// tool `groups`. A gated server (see [`extra_server_required_group`]) is
/// suppressed when the agent lacks its required group.
fn extra_server_enabled(server: &str, groups: &[hive_sh4re::ToolGroup]) -> bool {
fn extra_server_enabled(server: &str, groups: &[hive_sh4re::permissions::ToolGroup]) -> bool {
extra_server_required_group(server).is_none_or(|required| groups.contains(&required))
}
#[cfg(test)]
mod extra_server_gate_tests {
use super::{extra_server_enabled, extra_server_required_group};
use hive_sh4re::ToolGroup;
use hive_sh4re::permissions::ToolGroup;
#[test]
fn bash_is_gated_behind_execution() {
@ -173,13 +173,13 @@ mod extra_server_gate_tests {
/// requires updating the matching `ToolGroup::tools()` slice in hive-sh4re
/// (single source of truth). See `docs/conventions.md::Tool groups`.
#[must_use]
pub fn allowed_mcp_tools(groups: &[hive_sh4re::ToolGroup]) -> Vec<String> {
pub fn allowed_mcp_tools(groups: &[hive_sh4re::permissions::ToolGroup]) -> Vec<String> {
// Collect all tool names, deduplicating while preserving order.
// Always-on tools (e.g. `set_status`) come first so they're present
// regardless of which groups the agent is granted — a misconfigured
// agent still has to be able to report its dashboard status.
let mut seen = std::collections::HashSet::new();
let mut out: Vec<String> = hive_sh4re::ToolGroup::ALWAYS_ON_TOOLS
let mut out: Vec<String> = hive_sh4re::permissions::ToolGroup::ALWAYS_ON_TOOLS
.iter()
.copied()
.chain(groups.iter().flat_map(|g| g.tools().iter().copied()))
@ -406,7 +406,7 @@ fn build_mcp_servers() -> serde_json::Map<String, serde_json::Value> {
#[cfg(test)]
mod tests {
use super::{SERVER_NAME, allowed_mcp_tools};
use hive_sh4re::ToolGroup;
use hive_sh4re::permissions::ToolGroup;
fn qualified(tool: &str) -> String {
format!("mcp__{SERVER_NAME}__{tool}")