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:
parent
f862354e43
commit
f0844f6da3
73 changed files with 1578 additions and 2742 deletions
|
|
@ -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()
|
||||
|
|
|
|||
Loading…
Reference in a new issue