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.
109 lines
3.9 KiB
Rust
109 lines
3.9 KiB
Rust
//! 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);
|
|
}
|
|
}
|