//! Per-agent authorization: named MCP-tool groups (`ToolGroup`) and //! system-level capability grants (`Capability`). Both are declared in //! per-agent config (`tool-groups.json` / `capabilities.json`), injected //! into the container as env vars, and read by the harness to decide //! which MCP tools claude actually sees. use serde::{Deserialize, Serialize}; /// Named group of MCP tools an agent may be granted. The harness reads /// `HIVE_TOOL_GROUPS` from the environment (a comma-separated list of /// `snake_case` group names written by the meta renderer from per-agent /// config) and expands it to the matching tool names for `--allowedTools`. /// When the env var is absent the harness falls back to `AGENT_DEFAULT`. /// See `docs/conventions.md::Tool groups`. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] pub enum ToolGroup { /// `send`, `recv`, `ask`, `answer` Messaging, /// `get_agent_meta` (`set_status` is always-on — see `ALWAYS_ON_TOOLS`) Meta, /// `get_loose_ends`, `cancel_loose_end`, `remind` Inbox, /// `kill`, `start`, `restart`, `update` - *(privileged)* Lifecycle, /// `request_init_config`, `request_update_meta_inputs` - *(privileged)* Approvals, /// `request_schedule_prompt`, `fire_schedule_now`, `cancel_schedule`, /// `edit_schedule`, `list_schedules` - *(privileged)* Scheduling, /// `get_logs` - *(privileged)* Diagnostics, /// `create_repo` — create git repos through hive-c0re (the only path /// now that agents can't create them directly). Opt-in per /// agent so the operator controls who can spin up repos. Forge, /// `run`, `status` (via `mcp__bash__*`) Execution, /// Claude built-in web egress tools: `WebFetch` (retrieve a URL) and /// `WebSearch` (search the web). Both are omitted from `--tools` by /// default; adding this group to an agent enables them in the session /// and in `--allowedTools` so they run without a confirmation prompt. /// Does not gate any MCP tools — `tools()` returns `&[]`. WebTools, } impl ToolGroup { /// The MCP tool names (without the `mcp__hyperhive__` prefix) in this group. /// Returns `&[]` for `WebTools` — it enables Claude built-in tools, /// not MCP tools; see `builtin_tools()`. #[must_use] pub fn tools(self) -> &'static [&'static str] { match self { Self::Messaging => &["send", "recv", "ack_until", "ask", "answer"], Self::Meta => &["get_agent_meta"], Self::Inbox => &["get_loose_ends", "cancel_loose_end", "remind"], Self::Lifecycle => &["kill", "start", "restart", "update", "list_containers"], Self::Approvals => &["request_init_config", "request_update_meta_inputs"], Self::Scheduling => &[ "request_schedule_prompt", "fire_schedule_now", "cancel_schedule", "edit_schedule", "list_schedules", ], Self::Diagnostics => &["get_logs"], Self::Forge => &["create_repo"], Self::Execution => &["run", "status"], Self::WebTools => &[], } } /// MCP tools that are always exposed regardless of which tool groups an /// agent is granted. `set_status` lives here because the operator /// dashboard depends on every agent being able to report its status /// chip — gating it behind a group would let a misconfigured agent go /// dark on the dashboard. The server-side `SetStatus` handler has no /// tool-group check either (only length validation), so listing it here /// keeps the `--allowedTools` list honest with that reality. /// /// `compact` lives here too: it's pure self-management (no cross-agent /// effect, no privilege), gated server-side on context usage rather /// than on tool groups, and every agent should be able to reach for it /// regardless of which optional groups it's been granted — same /// reasoning as `set_status`. /// /// `mark_todos_done` too (a critical bug every agent hit): it was /// declared as a `#[tool]` fn but never added to *any* /// group's [`tools`](Self::tools), including `Inbox`, so no agent could /// ever get it into `--allowedTools` and every call prompted for /// approval it can't get. Todos are pushed to an agent independent of /// whether it holds `Inbox` (that group only gates /// `get_loose_ends`/`cancel_loose_end`/`remind`), so an agent without /// `Inbox` could accumulate todos it can never clear — same /// "every agent needs this regardless of optional groups" shape as /// `set_status`/`compact`, not a narrower `Inbox`-only fix. pub const ALWAYS_ON_TOOLS: &'static [&'static str] = &["set_status", "compact", "mark_todos_done"]; /// The Claude built-in tool names enabled by this group. Only /// `WebTools` returns a non-empty slice; all other groups return `&[]` /// (they control MCP tools via `tools()` instead). #[must_use] pub fn builtin_tools(self) -> &'static [&'static str] { match self { Self::WebTools => &["WebFetch", "WebSearch"], _ => &[], } } /// Default tool groups for an agent harness. Used when `HIVE_TOOL_GROUPS` is unset. pub const AGENT_DEFAULT: &'static [Self] = &[Self::Messaging, Self::Meta, Self::Inbox, Self::Execution]; /// Convenience preset for a fully-privileged agent (all groups). /// Use this as a starting point in `tool-groups.json` for root/manager agents. pub const MANAGER_DEFAULT: &'static [Self] = &[ Self::Messaging, Self::Meta, Self::Inbox, Self::Lifecycle, Self::Approvals, Self::Scheduling, Self::Diagnostics, Self::Execution, ]; /// Every known tool group in a stable order. Use this to enumerate /// columns in the capabilities UI or any other place that needs the /// full list without hard-coding it at the call site. pub const ALL: &'static [Self] = &[ Self::Messaging, Self::Meta, Self::Inbox, Self::Lifecycle, Self::Approvals, Self::Scheduling, Self::Diagnostics, Self::Forge, Self::Execution, Self::WebTools, ]; /// The `snake_case` wire name for this group (matches `serde(rename_all = /// "snake_case")` serialisation). #[must_use] pub fn as_str(self) -> &'static str { match self { Self::Messaging => "messaging", Self::Meta => "meta", Self::Inbox => "inbox", Self::Lifecycle => "lifecycle", Self::Approvals => "approvals", Self::Scheduling => "scheduling", Self::Diagnostics => "diagnostics", Self::Forge => "forge", Self::Execution => "execution", Self::WebTools => "web_tools", } } /// Short human-readable description suitable for a tooltip or help text. #[must_use] pub fn description(self) -> &'static str { match self { Self::Messaging => "send, recv, ask, answer — core agent communication", Self::Meta => { "get_agent_meta — identity introspection (set_status is always available)" } Self::Inbox => "get_loose_ends, cancel_loose_end, remind — self-scheduling", Self::Lifecycle => { "kill, start, restart, update, list_containers — container lifecycle (privileged)" } Self::Approvals => { "request_init_config, request_update_meta_inputs — config change flow (privileged)" } Self::Scheduling => { "request_schedule_prompt and related — operator-visible scheduled prompts (privileged)" } Self::Diagnostics => { "get_logs — read a sub-agent container's systemd journal (privileged)" } Self::Forge => { "create_repo — create git repos through hive-c0re (operator-gated merge)" } Self::Execution => { "run, status — run shell commands via mcp__bash__run / mcp__bash__status" } Self::WebTools => "WebFetch, WebSearch — Claude built-in web egress; not MCP tools", } } } /// Per-agent capability grants. Stored in `meta/capabilities.json` /// (same shape as `tool-groups.json`: `{ "alice": ["read_host_journal"] }`). /// Capabilities control system-level access hive-c0re enforces at /// dispatch time; they are orthogonal to tool groups (which control /// which MCP tools the harness exposes to claude). /// /// Injected into containers as `HIVE_CAPABILITIES` (comma-separated /// `snake_case`) via `meta::render_flake`. The harness reads this to /// conditionally register capability-gated MCP tools so claude only /// sees tools it can actually invoke. See `docs/conventions.md::Capabilities`. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] pub enum Capability { /// Agent can lifecycle-manage the root agent (kill/start/restart) /// on behalf of the hive when the root has crashed. Named capability /// for the existing manager privilege — future topology enforcement /// will gate this via the capability system instead of the hardcoded /// `container == MANAGER_CONTAINER` check. ManageRootAgent, /// Agent can read the full host journal via `GetHostJournal`. /// hive-c0re checks this capability before running journalctl. /// MCP tool `get_host_journal` is only registered in the harness /// when this capability is present. ReadHostJournal, /// Agent can query non-child agents via `GetLooseEnds`, /// `CountPendingReminders`, and `ReminderRollup` on the agent /// socket. Without this capability, targeting a non-child agent is /// rejected with an error (direct children are always accessible /// without any capability). The `"*"` hive-wide value is not /// available on the agent socket even with this capability — use the /// manager socket for swarm-wide scans. QueryAgentState, /// Agent can restart hive infrastructure containers (hive-ci, /// hive-forge, hive-matrix) via the `restart` MCP tool. hive-c0re /// checks this capability before routing the restart through /// hive-priv; the concrete service allowlist lives root-side in /// hive-priv. Deliberately generic ("infra admin") so future /// privileged infra ops can hang off the same grant. /// /// ⚠️ The gateway is **not** in reach of this capability, by operator /// ruling — it is the host's nginx and fronts the forge, dashboard and /// matrix, so an agent restarting it can cut the path its own fix /// travels. That refusal is a property of the target, not of the /// grant: no capability re-opens it. InfraAdmin, } impl Capability { /// Every known capability in a stable order. Use this to enumerate /// columns in the permissions UI or validate incoming capability strings. pub const ALL: &'static [Self] = &[ Self::ManageRootAgent, Self::ReadHostJournal, Self::QueryAgentState, Self::InfraAdmin, ]; /// Canonical `snake_case` name for this capability (matches serde). #[must_use] pub fn as_str(self) -> &'static str { match self { Self::ManageRootAgent => "manage_root_agent", Self::ReadHostJournal => "read_host_journal", Self::QueryAgentState => "query_agent_state", Self::InfraAdmin => "infra_admin", } } /// Short human-readable description suitable for a tooltip or help text. #[must_use] pub fn description(self) -> &'static str { match self { Self::ManageRootAgent => { "lifecycle-manage the root/manager agent on hive crash recovery" } Self::ReadHostJournal => "read host journald via get_host_journal MCP tool", Self::QueryAgentState => { "query non-child agents' loose ends and reminder state via get_loose_ends" } Self::InfraAdmin => { "restart hive infrastructure containers (hive-ci, hive-forge, hive-matrix; not the gateway) via the restart tool" } } } }