Watch
0
0
Fork
You've already forked hyperhive
0

move Permission into hive-types, drop swarm-controller's hive-sh4re dep

This commit is contained in:
atlas 2026-10-18 10:14:30 +02:00
commit e4a9005468
27 changed files with 221 additions and 175 deletions

5
Cargo.lock generated
View file

@ -1675,6 +1675,7 @@ dependencies = [
"hive-runtime",
"hive-sh4re",
"hive-sock-client",
"hive-types",
"http-body-util",
"hyper",
"hyper-util",
@ -2024,7 +2025,6 @@ dependencies = [
"schemars",
"serde",
"serde_json",
"strum",
]
[[package]]
@ -4876,7 +4876,6 @@ dependencies = [
"hive-jobq-metrics",
"hive-jobq-wire",
"hive-log",
"hive-sh4re",
"hive-types",
"hmac 0.13.0",
"http",
@ -5031,7 +5030,7 @@ dependencies = [
"async-nats",
"axum",
"data-encoding",
"hive-sh4re",
"hive-types",
"reqwest",
"serde",
"serde_json",

View file

@ -272,7 +272,7 @@ JSON-line protocol with their own result types; RFC 9457 is an HTTP format.
## Permissions
One set of named `Permission` values (`hive_sh4re::permissions::Permission`)
One set of named `Permission` values (`hive_types::Permission`)
decides what an agent may do: which MCP and built-in tools it gets, and which
host access it has.
@ -342,7 +342,7 @@ agent grant itself any tool by submitting a config commit, bypassing the
operator gate.
**Adding to the set** — add a variant to `Permission` in
`hive-sh4re/src/permissions.rs` and to `Permission::ALL`, and enforce it where
`hive-types/src/permission.rs` and to `Permission::ALL`, and enforce it where
it applies. A new `#[tool]` fn on `AgentServer` (`hive-agent-mcp/src/mcp/mod.rs`)
goes into the matching `Permission::tools()` slice: that's the single source
of truth `mcp_config::allowed_mcp_tools` (`hive-agent/src/mcp_config.rs`)

View file

@ -197,7 +197,8 @@ status hint lives in the wake prompt + UI header, not here.
## Tool allowlist (`hive_sh4re::permissions::ALLOWED_BUILTIN_TOOLS`)
The built-in list and the `--tools` value it resolves to live in
`hive-sh4re` alongside `Permission`, not in the harness, because the
`hive-sh4re` (the `Permission` enum itself is in `hive-types`), not in the
harness, because the
subagent daemon spawns its own `claude` and must resolve the same set —
see [`docs/tools/subagent.md`](../tools/subagent.md).

View file

@ -135,7 +135,7 @@ impl AgentServer {
}
// IMPORTANT: when adding a new `#[tool]` fn to this impl, also add
// its name to the matching `Permission::tools()` slice in hive-sh4re.
// its name to the matching `Permission::tools()` slice in hive-types.
// Claude Code's permission gate refuses uninlisted MCP tools in
// non-interactive `--print` mode with "permissions not granted yet".
#[tool_router]

View file

@ -29,6 +29,7 @@ hive-core-agent-sock.workspace = true
hive-log.workspace = true
hive-sh4re.workspace = true
hive-sock-client.workspace = true
hive-types.workspace = true
libc.workspace = true
opentelemetry.workspace = true
opentelemetry_sdk.workspace = true

View file

@ -6,7 +6,7 @@
//! running MCP server (a separate binary) — the `send` allow-list check that
//! server enforces lives alongside it in the `hive-agent-mcp` crate.
use hive_sh4re::permissions::Permission;
use hive_types::Permission;
/// Name of the hyperhive MCP server inside claude's view. Claude prefixes
/// tools as `mcp__<this>__<tool>` (e.g. `mcp__hyperhive__send`).
@ -55,7 +55,7 @@ fn extra_server_enabled(server: &str, perms: &[Permission]) -> bool {
#[cfg(test)]
mod extra_server_gate_tests {
use super::{extra_server_enabled, extra_server_required_permission};
use hive_sh4re::permissions::Permission;
use hive_types::Permission;
#[test]
fn bash_is_gated_behind_execution() {
@ -84,7 +84,7 @@ mod extra_server_gate_tests {
/// MCP tools claude is allowed to call without prompting, derived from
/// the supplied permissions. Adding a new `#[tool]` fn to a server impl
/// requires updating the matching `Permission::tools()` slice in hive-sh4re
/// requires updating the matching `Permission::tools()` slice in hive-types
/// (single source of truth).
#[must_use]
pub fn allowed_mcp_tools(perms: &[Permission]) -> Vec<String> {
@ -212,7 +212,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::permissions::Permission;
use hive_types::Permission;
fn qualified(tool: &str) -> String {
format!("mcp__{SERVER_NAME}__{tool}")

View file

@ -17,7 +17,7 @@ use std::path::Path;
use std::sync::{PoisonError, RwLock};
use std::time::Duration;
use hive_sh4re::permissions::Permission;
use hive_types::Permission;
use swarm_queue_client::agent_permissions::{HIVE_COPY_PATH, HiveCopy, Read, read};
/// How often the key is read. Also the longest a change can take to reach

View file

@ -351,8 +351,7 @@ pub fn make_session(bus: &Bus) -> Result<AgentSession> {
/// against the agent's permissions at the moment it asks.
fn acp_permission_policy() -> PermissionPolicy {
std::sync::Arc::new(|ask: &PermissionAsk<'_>| {
let web = crate::swarm_permissions::current()
.contains(&hive_sh4re::permissions::Permission::WebTools);
let web = crate::swarm_permissions::current().contains(&hive_types::Permission::WebTools);
acp_permits(ask, web)
})
}

View file

@ -17,7 +17,7 @@
use std::collections::BTreeMap;
use std::path::{Path, PathBuf};
use hive_sh4re::permissions::Permission;
use hive_types::Permission;
use swarm_queue_client::agent_permissions::{AgentPermissions, HIVE_COPY_FILE, HiveCopy};
const FILE: &str = "swarm-agent-permissions.json";

View file

@ -9,7 +9,7 @@
//! dashboard's tree view. All of that is gone; what the file is
//! *for* now is the one thing that survived the removal — naming every
//! agent, which is the set a
//! [`hive_sh4re::permissions::Permission::ManageRootAgent`] holder gets
//! [`hive_types::Permission::ManageRootAgent`] holder gets
//! bind-mounted ([`all_agents`]).
//!
//! Broader-than-your-own bind-mount grants are **not** stored here: they
@ -65,7 +65,7 @@ fn read_from(path: &Path) -> std::io::Result<BTreeSet<String>> {
}
/// Every agent the roster knows about, in name order. This is the set
/// a [`hive_sh4re::permissions::Permission::ManageRootAgent`] holder gets
/// a [`hive_types::Permission::ManageRootAgent`] holder gets
/// bind-mounted, and it is deliberately unfiltered: that permission means
/// "may manage any agent", so the set is all of them.
///

View file

@ -7,7 +7,7 @@ use std::path::{Path, PathBuf};
use anyhow::{Context, Result};
use hive_priv_sock::{BindMount, CredentialMount};
use hive_sh4re::permissions::Permission;
use hive_types::Permission;
use crate::coordinator::{AgentPaths, HiveEnv};

View file

@ -13,7 +13,7 @@ use std::collections::{BTreeMap, BTreeSet};
use std::path::{Path, PathBuf};
use anyhow::{Context, Result};
use hive_sh4re::permissions::Permission;
use hive_types::Permission;
use swarm_queue_client::agent_permissions::{
self, AgentPermissions, IMPORT_REPLY_DEADLINE, Import, ImportReply,
};
@ -240,7 +240,7 @@ async fn send_at(client: &async_nats::Client, hive: &str, meta: &Path) -> Result
#[cfg(test)]
mod tests {
use super::{FileMap, grants, seed_local_at};
use hive_sh4re::permissions::Permission::{self, *};
use hive_types::Permission::{self, *};
fn file(json: &str) -> FileMap {
serde_json::from_str(json).expect("decodes")

View file

@ -18,7 +18,7 @@ use std::sync::Arc;
use std::time::Duration;
use anyhow::{Context, Result};
use hive_sh4re::permissions::Permission;
use hive_types::Permission;
use swarm_queue_client::agent_permissions::{self, AgentPermissions, Read};
use crate::coordinator::Coordinator;
@ -216,12 +216,8 @@ mod tests {
assert!(changed.is_empty());
assert!(crate::permissions::read_at(&path).expect("read").is_empty());
assert!(
!crate::permissions::holds(
&path,
"ruth",
hive_sh4re::permissions::Permission::ManageRootAgent
)
.expect("read")
!crate::permissions::holds(&path, "ruth", hive_types::Permission::ManageRootAgent)
.expect("read")
);
}

View file

@ -15,4 +15,3 @@ schemars.workspace = true
serde.workspace = true
# `stream_enrich` walks raw claude `stream-json` values into `term_msg` rows.
serde_json.workspace = true
strum.workspace = true

View file

@ -18,7 +18,7 @@
//! server instead); the permission transfers to a subagent, not the
//! mechanism.
use serde::{Deserialize, Serialize};
use hive_types::Permission;
/// Built-in claude tools present in every session regardless of permissions.
/// Anything not in this list (and not added by a permission's
@ -90,131 +90,10 @@ pub fn subagent_builtin_tools_for(perms: &[Permission]) -> Vec<&'static str> {
tools
}
/// One thing an agent may be permitted. Stored by its `snake_case` name.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, strum::IntoStaticStr)]
#[serde(rename_all = "snake_case")]
#[strum(serialize_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"],
_ => &[],
}
}
}
#[cfg(test)]
mod tests {
use super::*;
/// The stored names are the `snake_case` spellings the old tool-group
/// and capability files used, so an imported grant decodes.
#[test]
fn stored_names_are_snake_case() {
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"
]
);
for perm in Permission::ALL {
let json = serde_json::to_string(perm).unwrap();
assert_eq!(json, format!("\"{}\"", <&str>::from(*perm)));
}
assert!(serde_json::from_str::<Permission>("\"lifecycle\"").is_err());
}
/// Adding permissions only ever adds built-ins — the base set is in every
/// resolution, and no permission can remove one. Both `--tools` callers (the
/// harness and the subagent daemon) rely on this to reason about "the

View file

@ -98,7 +98,7 @@ use hive_claude::{Attach, Claude, Config, SessionStore};
use hive_runtime::{
AcpCommand, AcpRuntime, PermissionAsk, PermissionPolicy, Runtime as _, RuntimeSpec, TurnStop,
};
use hive_sh4re::permissions::Permission;
use hive_types::Permission;
use swarm_wire::agent_inference::AgentInference;
use tokio::sync::oneshot;

View file

@ -26,7 +26,7 @@ use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::{Arc, OnceLock};
use std::time::Duration;
use hive_sh4re::permissions::Permission;
use hive_types::Permission;
use swarm_queue_client::agent_inference::{self, read_own};
use swarm_queue_client::agent_permissions::{self, HIVE_COPY_PATH, HiveCopy};
use swarm_wire::agent_inference::AgentInference;

View file

@ -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

View file

@ -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.
///

View 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());
}
}

Some files were not shown because too many files have changed in this diff Show more