//! 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>> = RwLock::new(None); /// The permissions this agent holds now. #[must_use] pub fn current() -> Vec { 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) -> Option> { 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 { // 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); } }