//! The [`Permission`] set an agent may hold. //! //! Lives in this leaf crate so every crate that names a permission, hive and //! swarm side alike, can do so without depending on `hive-sh4re` (and through //! it `hive-priv-sock`). The built-in tool resolution that consumes it is //! `hive_sh4re::permissions`. use serde::{Deserialize, Serialize}; /// One thing an agent may be permitted. Stored by its `snake_case` name. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] pub enum Permission { /// `send`, `recv`, `ack_until` Messaging, /// `get_agent_meta` (`set_status` is always-on — see `ALWAYS_ON_TOOLS`) Meta, /// `get_loose_ends`, `cancel_loose_end`, `remind` Inbox, /// Gates whether the `bash` MCP server (`mcp__bash__run`/`status`/ /// `kill`) is rendered into the agent's config at all — see /// `extra_server_required_permission` in `hive-agent/src/mcp_config.rs`. /// `tools()` returns `&[]`: this isn't a `mcp__hyperhive__*` /// allowlist entry, the gate lives at config-render time instead. Execution, /// Claude built-in web egress tools: `WebFetch` (retrieve a URL) and /// `WebSearch` (search the web). Both are omitted from `--tools` /// without it; holding it enables them in the session and in /// `--allowedTools` so they run without a confirmation prompt. WebTools, /// Agent may manage *any* agent in the hive. Concretely, hive-c0re /// bind-mounts every agent's state dir (RW) and config dir (RO) into the /// holder's container, plus `/applied` and `/meta` read-only, so the /// holder can recover an agent that is down. See /// `lifecycle::host_config::set_nspawn_flags`. /// /// The name is historical, from when "the root agent" was the widest /// thing to manage. Kept under that spelling because every stored grant /// carries it. ManageRootAgent, } impl Permission { /// Every permission in a stable order. pub const ALL: &'static [Self] = &[ Self::Messaging, Self::Meta, Self::Inbox, Self::Execution, Self::WebTools, Self::ManageRootAgent, ]; /// What an agent the swarm sets nothing for holds, whatever its name. pub const DEFAULT: &'static [Self] = &[Self::Messaging, Self::Meta, Self::Inbox, Self::Execution]; /// The MCP tool names (without the `mcp__hyperhive__` prefix) this /// permission grants. Empty for the permissions that grant something /// else; see each variant's doc comment. #[must_use] pub fn tools(self) -> &'static [&'static str] { match self { Self::Messaging => &["send", "recv", "ack_until"], Self::Meta => &["get_agent_meta"], Self::Inbox => &["get_loose_ends", "cancel_loose_end", "remind"], // `Execution` grants the out-of-process `bash` MCP server, // gated at config-render time in `hive-agent/src/mcp_config.rs` // — an out-of-process server has no later enforcement point, so // that gate is the actual security boundary. `WebTools` grants // Claude built-ins (`builtin_tools()`); `ManageRootAgent` grants // bind mounts. Self::Execution | Self::WebTools | Self::ManageRootAgent => &[], } } /// MCP tools that are always exposed regardless of which permissions an /// agent holds. `set_status` lives here because the operator /// dashboard depends on every agent being able to report its status /// chip — gating it would let a misconfigured agent go dark on the /// dashboard. The server-side `SetStatus` handler has no permission /// 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, and every /// agent should be able to reach for it — same reasoning as `set_status`. /// /// `mark_todos_done` too: todos are pushed to an agent whether or not it /// holds `Inbox` (which only gates `get_loose_ends`/`cancel_loose_end`/ /// `remind`), so an agent without `Inbox` must still be able to clear /// them. pub const ALWAYS_ON_TOOLS: &'static [&'static str] = &["set_status", "compact", "mark_todos_done"]; /// The Claude built-in tool names this permission enables. Only /// `WebTools` returns a non-empty slice. #[must_use] pub fn builtin_tools(self) -> &'static [&'static str] { match self { Self::WebTools => &["WebFetch", "WebSearch"], _ => &[], } } } impl Permission { /// The stored `snake_case` name; identical to the serde form. #[must_use] pub const fn as_str(self) -> &'static str { match self { Self::Messaging => "messaging", Self::Meta => "meta", Self::Inbox => "inbox", Self::Execution => "execution", Self::WebTools => "web_tools", Self::ManageRootAgent => "manage_root_agent", } } } impl From for &'static str { fn from(perm: Permission) -> Self { perm.as_str() } } #[cfg(test)] mod tests { use super::*; /// The `as_str` spelling of every variant. `ALL` is stable-ordered, so /// a variant added to the enum but not to `ALL` or `as_str` shows here. #[test] fn as_str_pins_every_variants_name() { let names: Vec<&str> = Permission::ALL.iter().map(|p| <&str>::from(*p)).collect(); assert_eq!( names, [ "messaging", "meta", "inbox", "execution", "web_tools", "manage_root_agent" ] ); } /// The serde form of every variant, pinned independently of `as_str`: /// stored grants decode by it, so it must stay byte-identical. #[test] fn serde_form_pins_every_variants_name() { let want = [ (Permission::Messaging, "\"messaging\""), (Permission::Meta, "\"meta\""), (Permission::Inbox, "\"inbox\""), (Permission::Execution, "\"execution\""), (Permission::WebTools, "\"web_tools\""), (Permission::ManageRootAgent, "\"manage_root_agent\""), ]; assert_eq!(want.len(), Permission::ALL.len()); for (perm, json) in want { assert_eq!(serde_json::to_string(&perm).unwrap(), json); assert_eq!(serde_json::from_str::(json).unwrap(), perm); } assert!(serde_json::from_str::("\"lifecycle\"").is_err()); } }