//! Tool-group + capability permission endpoints for the dashboard. //! //! Read endpoints return the full set of known groups/capabilities plus //! descriptions and the per-agent assignment map (the UI never hard-codes //! the lists). Write endpoints validate, then enqueue a `PermChange` so the //! JSON file write is serialised through the FIFO rebuild worker. use axum::{ extract::{Path as AxumPath, State}, http::StatusCode, response::{IntoResponse, Response}, }; use serde::{Deserialize, Serialize}; use super::{AppState, error_response, guard_agent_name, strip_container_prefix}; #[derive(Serialize)] pub(super) struct ToolGroupsSnapshot { /// Ordered list of all known tool-group names. Drives the column /// headers in the capabilities table — the UI does not hard-code them. groups: Vec<&'static str>, /// Short description for each group name. Keys match `groups` entries. descriptions: std::collections::BTreeMap<&'static str, &'static str>, /// Per-agent assignment map. Absent agents use the role default /// (agents: messaging+meta+inbox+execution; manager: all groups). assignments: std::collections::BTreeMap>, } pub(super) async fn get_tool_groups( State(_state): State, ) -> axum::Json { let groups = hive_sh4re::ToolGroup::ALL .iter() .map(|g| g.as_str()) .collect(); let descriptions = hive_sh4re::ToolGroup::ALL .iter() .map(|g| (g.as_str(), g.description())) .collect(); let assignments = crate::tool_groups::read(); axum::Json(ToolGroupsSnapshot { groups, descriptions, assignments, }) } #[derive(Deserialize)] pub(super) struct SetToolGroupsBody { groups: Vec, } pub(super) async fn post_tool_groups( State(state): State, AxumPath(name): AxumPath, axum::Json(body): axum::Json, ) -> Response { let logical = strip_container_prefix(&name); if let Some(reject) = guard_agent_name(&state, &logical).await { return reject; } // Validate group names before queuing — fail fast so the operator // sees the error immediately rather than waiting for the worker. if let Err(e) = crate::tool_groups::validate_groups(&body.groups) { return error_response(&format!("invalid tool-groups for {logical}: {e}")); } // Enqueue a PermChange so the JSON file write is serialised through // the FIFO worker. Prevents concurrent batch-apply actions for // different agents from racing on the shared tool-groups.json. state.coord.rebuild_queue.enqueue_with_perm( logical.clone(), crate::rebuild_queue::QueueSource::Manual, "tool-group change via permissions UI".to_owned(), crate::rebuild_queue::PermPayload::ToolGroups { groups: body.groups.clone(), }, ); state.coord.emit_rebuild_queue_snapshot(); tracing::info!(agent = %logical, groups = ?body.groups, "operator: set tool-groups via dashboard"); (StatusCode::OK, "ok").into_response() } #[derive(Serialize)] pub(super) struct CapabilitiesSnapshot { /// Ordered list of all known capability names. Drives the column /// headers in the capabilities table — the UI does not hard-code them. caps: Vec<&'static str>, /// Short description for each capability name. Keys match `caps` entries. descriptions: std::collections::BTreeMap<&'static str, &'static str>, /// Per-agent capability grant map. Absent agents have no extra caps. assignments: std::collections::BTreeMap>, } pub(super) async fn get_capabilities( State(_state): State, ) -> axum::Json { use hive_sh4re::Capability; let caps = Capability::ALL.iter().map(|c| c.as_str()).collect(); let descriptions = Capability::ALL .iter() .map(|c| (c.as_str(), c.description())) .collect(); let assignments = crate::capabilities::read(); axum::Json(CapabilitiesSnapshot { caps, descriptions, assignments, }) } #[derive(Deserialize)] pub(super) struct SetCapabilitiesBody { caps: Vec, } pub(super) async fn post_capabilities( State(state): State, AxumPath(name): AxumPath, axum::Json(body): axum::Json, ) -> Response { let logical = strip_container_prefix(&name); if let Some(reject) = guard_agent_name(&state, &logical).await { return reject; } let known: Vec<&str> = hive_sh4re::Capability::ALL .iter() .map(|c| c.as_str()) .collect(); for cap in &body.caps { if !known.contains(&cap.as_str()) { return error_response(&format!("unknown capability: {cap}")); } } // Enqueue a PermChange so the JSON file write is serialised through // the FIFO worker. Prevents concurrent batch-apply actions for // different agents from racing on the shared capabilities.json. state.coord.rebuild_queue.enqueue_with_perm( logical.clone(), crate::rebuild_queue::QueueSource::Manual, "capability change via dashboard".to_owned(), crate::rebuild_queue::PermPayload::Capabilities { caps: body.caps.clone(), }, ); state.coord.emit_rebuild_queue_snapshot(); tracing::info!(agent = %logical, caps = ?body.caps, "operator: set capabilities via dashboard"); (StatusCode::OK, "ok").into_response() }