hyperhive/hive-c0re/src/dashboard/permissions.rs

148 lines
5.4 KiB
Rust

//! 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<String, Vec<String>>,
}
pub(super) async fn get_tool_groups(
State(_state): State<AppState>,
) -> axum::Json<ToolGroupsSnapshot> {
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<String>,
}
pub(super) async fn post_tool_groups(
State(state): State<AppState>,
AxumPath(name): AxumPath<String>,
axum::Json(body): axum::Json<SetToolGroupsBody>,
) -> 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<String, Vec<String>>,
}
pub(super) async fn get_capabilities(
State(_state): State<AppState>,
) -> axum::Json<CapabilitiesSnapshot> {
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<String>,
}
pub(super) async fn post_capabilities(
State(state): State<AppState>,
AxumPath(name): AxumPath<String>,
axum::Json(body): axum::Json<SetCapabilitiesBody>,
) -> 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()
}