Watch
0
0
Fork
You've already forked hyperhive
0

agent permissions: one set, tool groups included, set at agent creation

mara on #5170: "dont split caps and perms. its all perms. / dont include
the now empty tool groups / nothing set -> default / also add perms to
agent creation screen".

- hive-sh4re: one `Permission` enum (messaging, meta, inbox, execution,
  web_tools, manage_root_agent) replaces `ToolGroup` and `Capability`;
  `lifecycle`/`forge` are gone. `Permission::default_for` is the default
  for an agent the swarm sets nothing for.
- The record is `{ "permissions": [...] }`. Missing key, unset field or
  no successful read yet = default; `[]` = none; a failed read after a
  good one keeps the last value.
- hive-agent reads its own key every 10s and builds each turn's --tools,
  --allowedTools and MCP config (rewritten before every turn) from it;
  the subagent daemon reads it on each start/continue. Agents get a
  read-only grant on their own key.
- hive-c0re restarts an agent only when its manage_root_agent changes.
  The import now merges meta/capabilities.json and meta/tool-groups.json
  into one list per agent and deletes both; the "import pending, don't
  read" decision is `ImportOutcome::read_allowed`, table-tested.
- swarm-controller: `GET /api/permissions` (names + default) and an
  optional `permissions` on `POST /api/agents`, stored before the job is
  queued. swarm-ui's create-agent form gets a permissions field.
- Deleted: HIVE_TOOL_GROUPS and its forward to the subagent daemon,
  tool_groups.rs, ruth's tool-group seed, the permissions rebuild job
  (PermPayload/WritePermFile/perm_change), the dashboard permissions tab
  and its stale-entries panel.

Part of #5170.
This commit is contained in:
atlas 2026-10-15 04:09:53 +02:00 • committed by mara
commit f0844f6da3
73 changed files with 1578 additions and 2742 deletions

View file

@ -1,17 +1,18 @@
//! The per-agent permissions KV bucket: one key per agent, holding the
//! capabilities the swarm grants it.
//! [`hive_sh4re::permissions::Permission`] set the swarm grants it.
//!
//! **swarm-controller is the only writer**, and creates the bucket when it
//! starts, so a bucket that does not answer means the queue is not serving it,
//! never "nothing set". A field left unset leaves the agent on its hive's
//! compiled-in default; an empty list grants nothing.
//! starts. A missing key or an unset `permissions` field leaves the agent on
//! the compiled-in default (`Permission::default_for`); an empty list grants
//! nothing.
//!
//! **The hive running the agent reads the key**, not the agent: capabilities
//! are bind mounts the host sets up at container start. A hive reads with one
//! `DIRECT.GET` per agent
//! ([`crate::agent_permissions::direct_get_subject`]), and its grant covers
//! every key of this bucket, read-only, for the reason [`crate::agent_limits`]
//! gives.
//! **Two readers, each enforcing its own part.** The hive running the agent
//! applies `manage_root_agent` as bind mounts at container start; its grant
//! covers every key of this bucket, read-only, for the reason
//! [`crate::agent_limits`] gives. The agent itself (its harness and subagent
//! daemon) applies the tool permissions; its grant covers only its own key.
//! Both read with one `DIRECT.GET` of
//! [`crate::agent_permissions::direct_get_subject`].
//!
//! Keyed by the agent alone, like [`crate::agent_limits`]: an agent can move
//! between hives, and its permissions go with it.
@ -76,14 +77,14 @@ pub const IMPORT_REPLY_DEADLINE: std::time::Duration = std::time::Duration::from
#[derive(Debug, Clone, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct AgentPermissions {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub capabilities: Option<Vec<hive_sh4re::permissions::Capability>>,
pub permissions: Option<Vec<hive_sh4re::permissions::Permission>>,
}
/// What a hive sends once: the capabilities its meta repo held, by agent.
/// What a hive sends once: the permissions its meta repo held, by agent.
#[cfg(feature = "agent-permissions")]
#[derive(Debug, Clone, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct Import {
pub capabilities: std::collections::BTreeMap<String, Vec<hive_sh4re::permissions::Capability>>,
pub permissions: std::collections::BTreeMap<String, Vec<hive_sh4re::permissions::Permission>>,
}
/// The controller's answer to an [`Import`].
@ -131,7 +132,8 @@ pub enum Read {
NoBucket,
}
/// Read `agent`'s current permissions with one direct get.
/// Read `agent`'s current permissions with one direct get: the one subject
/// an agent's own grant covers, and one of those a hive's covers.
///
/// # Errors
///
@ -220,16 +222,16 @@ mod tests {
#[cfg(feature = "agent-permissions")]
#[test]
fn unset_and_empty_are_different_values() {
use hive_sh4re::permissions::Capability;
use hive_sh4re::permissions::Permission;
assert_eq!(
serde_json::to_string(&AgentPermissions::default()).unwrap(),
"{}"
);
let none: AgentPermissions = serde_json::from_str(r#"{"capabilities":[]}"#).unwrap();
assert_eq!(none.capabilities, Some(Vec::new()));
let none: AgentPermissions = serde_json::from_str(r#"{"permissions":[]}"#).unwrap();
assert_eq!(none.permissions, Some(Vec::new()));
let ruth: AgentPermissions =
serde_json::from_str(r#"{"capabilities":["manage_root_agent"]}"#).unwrap();
assert_eq!(ruth.capabilities, Some(vec![Capability::ManageRootAgent]));
serde_json::from_str(r#"{"permissions":["manage_root_agent"]}"#).unwrap();
assert_eq!(ruth.permissions, Some(vec![Permission::ManageRootAgent]));
}
/// Fields a newer controller adds must not cost an older hive the ones it
@ -239,10 +241,10 @@ mod tests {
fn decoding_tolerates_unknown_fields_and_reads_a_delete_as_unset() {
let decoded = decode(
None,
br#"{"capabilities":["manage_root_agent"],"tool_groups":["meta"]}"#,
br#"{"permissions":["manage_root_agent"],"later_field":["meta"]}"#,
)
.unwrap();
assert_eq!(decoded.capabilities.map(|c| c.len()), Some(1));
assert_eq!(decoded.permissions.map(|c| c.len()), Some(1));
assert_eq!(
decode(Some("DEL"), b"").unwrap(),
AgentPermissions::default()