260 lines
11 KiB
Rust
260 lines
11 KiB
Rust
//! 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`, `ack_until`
|
|
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"],
|
|
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, ack_until — 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,
|
|
}
|
|
|
|
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,
|
|
];
|
|
|
|
/// 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",
|
|
}
|
|
}
|
|
|
|
/// 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"
|
|
}
|
|
}
|
|
}
|
|
}
|