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
109
hive-agent/src/swarm_permissions.rs
Normal file
109
hive-agent/src/swarm_permissions.rs
Normal file
|
|
@ -0,0 +1,109 @@
|
|||
//! The permissions the swarm grants this agent.
|
||||
//!
|
||||
//! swarm-controller keeps the agent's set under its key in the
|
||||
//! `agent-permissions` KV bucket. This task reads that key every
|
||||
//! [`POLL_INTERVAL`] with the one subject the agent's own queue credential is
|
||||
//! granted there, and [`current`] answers with the last value read. The turn
|
||||
//! loop asks [`current`] for every turn's `--tools`, `--allowedTools` and MCP
|
||||
//! config, so a change reaches the next turn.
|
||||
//!
|
||||
//! Until the first successful read after a start, and for an unset key, the
|
||||
//! agent holds [`Permission::default_for`]. A failed read keeps the last
|
||||
//! value.
|
||||
|
||||
use std::sync::{PoisonError, RwLock};
|
||||
use std::time::Duration;
|
||||
|
||||
use hive_sh4re::permissions::Permission;
|
||||
use swarm_queue_client::agent_permissions::{AgentPermissions, Read, read};
|
||||
|
||||
/// How often the key is read. Also the longest a change can take to reach
|
||||
/// the agent's next turn.
|
||||
const POLL_INTERVAL: Duration = Duration::from_secs(10);
|
||||
|
||||
/// The last value read; `None` until the first read, which is the default.
|
||||
static CURRENT: RwLock<Option<Vec<Permission>>> = RwLock::new(None);
|
||||
|
||||
/// The permissions this agent holds now.
|
||||
#[must_use]
|
||||
pub fn current() -> Vec<Permission> {
|
||||
CURRENT
|
||||
.read()
|
||||
.unwrap_or_else(PoisonError::into_inner)
|
||||
.clone()
|
||||
.unwrap_or_else(|| Permission::default_for(&crate::identity::label()))
|
||||
}
|
||||
|
||||
/// What one read of the key leaves the agent holding: the swarm's list when
|
||||
/// it sets one, the default when the key is unset, and `None` (keep the last
|
||||
/// value) when the read failed or the bucket did not answer.
|
||||
fn after(agent: &str, read: Option<Read>) -> Option<Vec<Permission>> {
|
||||
match read? {
|
||||
Read::Current(AgentPermissions { permissions }) => {
|
||||
Some(permissions.unwrap_or_else(|| Permission::default_for(agent)))
|
||||
}
|
||||
Read::NoBucket => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Run the poller for the harness's lifetime, if this agent has a label to
|
||||
/// name its key with.
|
||||
pub async fn run() {
|
||||
let agent = crate::identity::label();
|
||||
if agent.is_empty() {
|
||||
tracing::warn!("this agent has no label; not reading its swarm permissions");
|
||||
return;
|
||||
}
|
||||
let client = crate::swarm_queue::client().await;
|
||||
tracing::info!(%agent, "reading swarm permissions from the swarm queue");
|
||||
loop {
|
||||
if let Some(perms) = after(&agent, read_once(&client, &agent).await) {
|
||||
let mut current = CURRENT.write().unwrap_or_else(PoisonError::into_inner);
|
||||
if current.as_ref() != Some(&perms) {
|
||||
tracing::info!(?perms, "permissions: the swarm's set changed");
|
||||
*current = Some(perms);
|
||||
}
|
||||
}
|
||||
tokio::time::sleep(POLL_INTERVAL).await;
|
||||
}
|
||||
}
|
||||
|
||||
/// The key's current value, or `None` when it could not be read.
|
||||
async fn read_once(client: &async_nats::Client, agent: &str) -> Option<Read> {
|
||||
// An unconnected client does not fail a JetStream request, it hangs on it.
|
||||
swarm_queue_client::ensure_connected(client).ok()?;
|
||||
match read(client, agent).await {
|
||||
Ok(read) => Some(read),
|
||||
Err(e) => {
|
||||
tracing::warn!(error = %e, "permissions: reading this agent's key failed");
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn record(json: &str) -> Read {
|
||||
Read::Current(serde_json::from_str(json).expect("decodes"))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_read_sets_the_list_or_the_default_and_a_failure_keeps_the_last() {
|
||||
assert_eq!(
|
||||
after("atlas", Some(record(r#"{"permissions":["web_tools"]}"#))),
|
||||
Some(vec![Permission::WebTools])
|
||||
);
|
||||
assert_eq!(
|
||||
after("atlas", Some(record(r#"{"permissions":[]}"#))),
|
||||
Some(Vec::new())
|
||||
);
|
||||
assert_eq!(
|
||||
after("atlas", Some(record("{}"))),
|
||||
Some(Permission::DEFAULT.to_vec())
|
||||
);
|
||||
assert_eq!(after("atlas", Some(Read::NoBucket)), None);
|
||||
assert_eq!(after("atlas", None), None);
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue