refactor(#1456): extract dashboard permission endpoints into dashboard/permissions.rs
This commit is contained in:
parent
09cb705738
commit
7ade5f27ea
2 changed files with 160 additions and 137 deletions
|
|
@ -30,6 +30,8 @@ use crate::container_view::{ContainerView, claude_has_session};
|
|||
use crate::coordinator::Coordinator;
|
||||
use crate::lifecycle::{self, MANAGER_NAME};
|
||||
|
||||
mod permissions;
|
||||
|
||||
#[derive(Clone)]
|
||||
struct AppState {
|
||||
coord: Arc<Coordinator>,
|
||||
|
|
@ -81,10 +83,16 @@ pub async fn serve(port: u16, coord: Arc<Coordinator>) -> Result<()> {
|
|||
.route("/request-spawn", post(post_request_spawn))
|
||||
.route("/api/topology/set-parent", post(post_set_parent))
|
||||
.route("/api/topology/set-parent-bulk", post(post_set_parent_bulk))
|
||||
.route("/api/tool-groups", get(get_tool_groups))
|
||||
.route("/api/tool-groups/{agent}", post(post_tool_groups))
|
||||
.route("/api/capabilities", get(get_capabilities))
|
||||
.route("/api/capabilities/{agent}", post(post_capabilities))
|
||||
.route("/api/tool-groups", get(permissions::get_tool_groups))
|
||||
.route(
|
||||
"/api/tool-groups/{agent}",
|
||||
post(permissions::post_tool_groups),
|
||||
)
|
||||
.route("/api/capabilities", get(permissions::get_capabilities))
|
||||
.route(
|
||||
"/api/capabilities/{agent}",
|
||||
post(permissions::post_capabilities),
|
||||
)
|
||||
.route("/op-send", post(post_op_send))
|
||||
.route("/meta-update", post(post_meta_update))
|
||||
.route("/api/schedules", get(api_schedules).post(post_schedule_new))
|
||||
|
|
@ -2586,139 +2594,6 @@ async fn post_set_parent_bulk(
|
|||
}
|
||||
}
|
||||
|
||||
// ── tool-group endpoints ──────────────────────────────────
|
||||
|
||||
#[derive(Serialize)]
|
||||
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>>,
|
||||
}
|
||||
|
||||
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)]
|
||||
struct SetToolGroupsBody {
|
||||
groups: Vec<String>,
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
|
||||
// ── capability endpoints ──────────────────────────────────────────────────
|
||||
|
||||
#[derive(Serialize)]
|
||||
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>>,
|
||||
}
|
||||
|
||||
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)]
|
||||
struct SetCapabilitiesBody {
|
||||
caps: Vec<String>,
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
|
||||
async fn post_rebuild(State(state): State<AppState>, AxumPath(name): AxumPath<String>) -> Response {
|
||||
let logical = strip_container_prefix(&name);
|
||||
if let Some(reject) = guard_agent_name(&state, &logical).await {
|
||||
|
|
|
|||
148
hive-c0re/src/dashboard/permissions.rs
Normal file
148
hive-c0re/src/dashboard/permissions.rs
Normal file
|
|
@ -0,0 +1,148 @@
|
|||
//! 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()
|
||||
}
|
||||
Loading…
Reference in a new issue