move Permission into hive-types, drop swarm-controller's hive-sh4re dep
This commit is contained in:
parent
04786da58f
commit
e4a9005468
27 changed files with 221 additions and 175 deletions
|
|
@ -8,6 +8,8 @@ Foundational shared **newtypes** for the hyperhive workspace. A zero-dependency
|
|||
|
||||
## What's here
|
||||
|
||||
- **`Permission`** — the set of named permissions an agent may hold, with its
|
||||
stored `snake_case` names and the MCP and built-in tools each grants.
|
||||
- **`Ident`** — a validated agent-name newtype. Typing agent-name fields as
|
||||
`Ident` (rather than bare `String`) gets serde-validated parsing **at the
|
||||
socket boundary for free**: a malformed name is rejected at deserialize time
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
//! Foundational shared newtypes for the hyperhive workspace.
|
||||
//! Foundational shared types for the hyperhive workspace: [`Ident`] and [`Permission`].
|
||||
//!
|
||||
//! A zero-dependency (bar `serde`) leaf crate so every wire-type crate
|
||||
//! (`hive-sh4re`, `hive-host-sock`, `hive-core-agent-sock`) and both binaries
|
||||
|
|
@ -6,6 +6,10 @@
|
|||
//! and get serde-validated parsing at the socket boundary for free — with
|
||||
//! no cross-crate coupling and without growing `hive-sh4re`.
|
||||
|
||||
mod permission;
|
||||
|
||||
pub use permission::Permission;
|
||||
|
||||
/// The environment variable nix uses to hand this process the blacklist of
|
||||
/// names that already mean something to the message layer.
|
||||
///
|
||||
|
|
|
|||
169
hive-types/src/permission.rs
Normal file
169
hive-types/src/permission.rs
Normal file
|
|
@ -0,0 +1,169 @@
|
|||
//! 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<Permission> 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::<Permission>(json).unwrap(), perm);
|
||||
}
|
||||
assert!(serde_json::from_str::<Permission>("\"lifecycle\"").is_err());
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue