hive-c0re: annotate remaining dashboard routes with utoipa

This commit is contained in:
damocles 2026-07-31 23:00:12 +02:00 committed by mara
commit 582ebe5eee
21 changed files with 738 additions and 45 deletions

View file

@ -11,12 +11,13 @@ use axum::{
response::{IntoResponse, Response},
};
use serde::{Deserialize, Serialize};
use utoipa::ToSchema;
use problem_details::ProblemDetails;
use super::{AppState, Ident, guard_agent_name, strip_container_prefix};
#[derive(Serialize)]
#[derive(Serialize, ToSchema)]
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.
@ -36,6 +37,14 @@ pub(super) struct ToolGroupsSnapshot {
effective: std::collections::BTreeMap<String, Vec<String>>,
}
/// `GET /api/tool-groups` — every known tool-group name + description,
/// plus the per-agent explicit/effective assignment maps.
#[utoipa::path(
get,
path = "/api/tool-groups",
responses((status = 200, description = "tool-group catalogue + assignments", body = ToolGroupsSnapshot)),
tag = "permissions"
)]
pub(super) async fn get_tool_groups(
State(state): State<AppState>,
) -> axum::Json<ToolGroupsSnapshot> {
@ -105,11 +114,25 @@ pub(crate) fn roster_and_effective(
(agents, effective)
}
#[derive(Deserialize)]
#[derive(Deserialize, ToSchema)]
pub(super) struct SetToolGroupsBody {
groups: Vec<String>,
}
/// `POST /api/tool-groups/{agent}` — replace `agent`'s explicit tool-group
/// assignment (JSON body `{"groups": [...]}`).
#[utoipa::path(
post,
path = "/api/tool-groups/{agent}",
params(("agent" = String, Path, description = "agent name")),
request_body = SetToolGroupsBody,
responses(
(status = 200, description = "tool-groups queued for write", body = String),
(status = 400, description = "unknown group name, or no such agent"),
(status = 404, description = "no such agent"),
),
tag = "permissions"
)]
pub(super) async fn post_tool_groups(
State(state): State<AppState>,
AxumPath(name): AxumPath<String>,
@ -145,7 +168,7 @@ pub(super) async fn post_tool_groups(
Ok((StatusCode::OK, "ok").into_response())
}
#[derive(Serialize)]
#[derive(Serialize, ToSchema)]
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.
@ -164,6 +187,14 @@ pub(super) struct CapabilitiesSnapshot {
effective: std::collections::BTreeMap<String, Vec<String>>,
}
/// `GET /api/capabilities` — every known capability name + description,
/// plus the per-agent explicit/effective grant maps.
#[utoipa::path(
get,
path = "/api/capabilities",
responses((status = 200, description = "capability catalogue + assignments", body = CapabilitiesSnapshot)),
tag = "permissions"
)]
pub(super) async fn get_capabilities(
State(state): State<AppState>,
) -> axum::Json<CapabilitiesSnapshot> {
@ -191,11 +222,25 @@ pub(super) async fn get_capabilities(
})
}
#[derive(Deserialize)]
#[derive(Deserialize, ToSchema)]
pub(super) struct SetCapabilitiesBody {
caps: Vec<String>,
}
/// `POST /api/capabilities/{agent}` — replace `agent`'s explicit capability
/// grant set (JSON body `{"caps": [...]}`).
#[utoipa::path(
post,
path = "/api/capabilities/{agent}",
params(("agent" = String, Path, description = "agent name")),
request_body = SetCapabilitiesBody,
responses(
(status = 200, description = "capabilities queued for write", body = String),
(status = 400, description = "unknown capability name"),
(status = 404, description = "no such agent"),
),
tag = "permissions"
)]
pub(super) async fn post_capabilities(
State(state): State<AppState>,
AxumPath(name): AxumPath<String>,
@ -236,7 +281,7 @@ pub(super) async fn post_capabilities(
/// field leaves that perm-type untouched, an empty array clears it, a
/// populated array fully replaces it (same replace semantics as the
/// per-agent endpoints).
#[derive(Deserialize)]
#[derive(Deserialize, ToSchema)]
pub(super) struct PermChangeBody {
agent: String,
#[serde(default)]
@ -245,7 +290,7 @@ pub(super) struct PermChangeBody {
capabilities: Option<Vec<String>>,
}
#[derive(Deserialize)]
#[derive(Deserialize, ToSchema)]
pub(super) struct BatchPermsBody {
changes: Vec<PermChangeBody>,
}
@ -261,6 +306,17 @@ type StagedPerm = (String, Option<Vec<String>>, Option<Vec<String>>);
/// groups both changed rebuilds once, not twice. The whole batch is
/// atomic: every change is validated up front and on any validation
/// error nothing is written or enqueued.
#[utoipa::path(
post,
path = "/api/permissions",
request_body = BatchPermsBody,
responses(
(status = 200, description = "batch permission change queued", body = String),
(status = 400, description = "unknown tool-group/capability name, or no such agent"),
(status = 404, description = "no such agent"),
),
tag = "permissions"
)]
pub(super) async fn post_permissions(
State(state): State<AppState>,
axum::Json(body): axum::Json<BatchPermsBody>,
@ -320,12 +376,20 @@ pub(super) async fn post_permissions(
/// persisted). The client uses this to drive the "stale permission entries"
/// sub-section in K3PT ST4T3 without having to fetch three separate
/// endpoints and perform set arithmetic on the client side.
#[derive(Serialize)]
#[derive(Serialize, ToSchema)]
pub(super) struct StalePermsResponse {
/// Ghost agent names, sorted. Empty list → no stale entries.
stale: Vec<String>,
}
/// `GET /api/permissions/stale` — agent names with explicit permission
/// entries but no matching live container or kept-state dir.
#[utoipa::path(
get,
path = "/api/permissions/stale",
responses((status = 200, description = "ghost agent names with stale permission entries", body = StalePermsResponse)),
tag = "permissions"
)]
pub(super) async fn get_stale_permissions(
State(state): State<AppState>,
) -> axum::Json<StalePermsResponse> {
@ -375,6 +439,17 @@ pub(super) async fn get_stale_permissions(
/// the format check ([`Ident::parse`]) is applied. No rebuild is
/// enqueued (the agent doesn't exist to rebuild); the SSE snapshots
/// update the P3RM1SS10NS tab live.
#[utoipa::path(
delete,
path = "/api/permissions/{agent}",
params(("agent" = String, Path, description = "agent name (need not be live)")),
responses(
(status = 200, description = "stale permission entries cleared", body = String),
(status = 400, description = "bad agent name"),
(status = 500, description = "tool-groups/capabilities file write failed"),
),
tag = "permissions"
)]
pub(super) async fn delete_agent_permissions(
State(state): State<AppState>,
AxumPath(name): AxumPath<String>,