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::coordinator::Coordinator;
|
||||||
use crate::lifecycle::{self, MANAGER_NAME};
|
use crate::lifecycle::{self, MANAGER_NAME};
|
||||||
|
|
||||||
|
mod permissions;
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
struct AppState {
|
struct AppState {
|
||||||
coord: Arc<Coordinator>,
|
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("/request-spawn", post(post_request_spawn))
|
||||||
.route("/api/topology/set-parent", post(post_set_parent))
|
.route("/api/topology/set-parent", post(post_set_parent))
|
||||||
.route("/api/topology/set-parent-bulk", post(post_set_parent_bulk))
|
.route("/api/topology/set-parent-bulk", post(post_set_parent_bulk))
|
||||||
.route("/api/tool-groups", get(get_tool_groups))
|
.route("/api/tool-groups", get(permissions::get_tool_groups))
|
||||||
.route("/api/tool-groups/{agent}", post(post_tool_groups))
|
.route(
|
||||||
.route("/api/capabilities", get(get_capabilities))
|
"/api/tool-groups/{agent}",
|
||||||
.route("/api/capabilities/{agent}", post(post_capabilities))
|
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("/op-send", post(post_op_send))
|
||||||
.route("/meta-update", post(post_meta_update))
|
.route("/meta-update", post(post_meta_update))
|
||||||
.route("/api/schedules", get(api_schedules).post(post_schedule_new))
|
.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 {
|
async fn post_rebuild(State(state): State<AppState>, AxumPath(name): AxumPath<String>) -> Response {
|
||||||
let logical = strip_container_prefix(&name);
|
let logical = strip_container_prefix(&name);
|
||||||
if let Some(reject) = guard_agent_name(&state, &logical).await {
|
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