148 lines
5.4 KiB
Rust
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()
|
|
}
|