feat(#1053): permissions tab — capabilities UI + move tool-groups
Add a new P3RM1SS10NS tab to the dashboard that consolidates all
per-agent permission configuration:
Backend:
- GET /api/capabilities returns { caps: [...], assignments: {...} }
driven by Capability::ALL variants (manage_root_agent,
read_host_journal, query_agent_state)
- POST /api/capabilities/{agent} writes capabilities.json and queues
a rebuild so HIVE_CAPABILITIES takes effect
Frontend:
- New 'permissions' entry in TABS, placed after 'system'
- P3RM1SS10NS tab pane with two sections:
C4P4B1L1T13S — agents × capabilities checkbox matrix (.cap-*)
T00L GR0UPS — agents × tool-groups checkbox matrix (.tg-*) moved
from SYST3M tab
- activateTab('permissions') fetches both tables; neither has an SSE
channel so they re-fetch on each activation to stay fresh
- CSS for .cap-* mirrors the .tg-* layout (scrollable, Catppuccin)
This commit is contained in:
parent
9d11e5b6d6
commit
013e8740bd
4 changed files with 252 additions and 7 deletions
|
|
@ -80,6 +80,8 @@ pub async fn serve(port: u16, coord: Arc<Coordinator>) -> Result<()> {
|
|||
.route("/api/topology/set-parent", post(post_set_parent))
|
||||
.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("/op-send", post(post_op_send))
|
||||
.route("/meta-update", post(post_meta_update))
|
||||
.route("/api/schedules", get(api_schedules).post(post_schedule_new))
|
||||
|
|
@ -2503,7 +2505,7 @@ async fn post_set_parent(
|
|||
}
|
||||
}
|
||||
|
||||
// ── tool-group (capabilities) endpoints ──────────────────────────────────
|
||||
// ── tool-group endpoints ──────────────────────────────────
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct ToolGroupsSnapshot {
|
||||
|
|
@ -2549,7 +2551,7 @@ async fn post_tool_groups(
|
|||
crate::rebuild_queue::QueueKind::Rebuild,
|
||||
logical.clone(),
|
||||
crate::rebuild_queue::QueueSource::Manual,
|
||||
"tool-group change via capabilities UI".to_owned(),
|
||||
"tool-group change via permissions UI".to_owned(),
|
||||
None,
|
||||
);
|
||||
state.coord.emit_rebuild_queue_snapshot();
|
||||
|
|
@ -2557,6 +2559,58 @@ async fn post_tool_groups(
|
|||
(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>,
|
||||
/// 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 = vec![
|
||||
Capability::ManageRootAgent.as_str(),
|
||||
Capability::ReadHostJournal.as_str(),
|
||||
Capability::QueryAgentState.as_str(),
|
||||
];
|
||||
let assignments = crate::capabilities::read();
|
||||
axum::Json(CapabilitiesSnapshot { caps, 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;
|
||||
}
|
||||
if let Err(e) = crate::capabilities::set_caps(&logical, &body.caps) {
|
||||
return error_response(&format!("set capabilities for {logical}: {e}"));
|
||||
}
|
||||
// Trigger a rebuild so the new HIVE_CAPABILITIES env var takes effect.
|
||||
state.coord.rebuild_queue.enqueue(
|
||||
crate::rebuild_queue::QueueKind::Rebuild,
|
||||
logical.clone(),
|
||||
crate::rebuild_queue::QueueSource::Manual,
|
||||
"capability change via dashboard".to_owned(),
|
||||
None,
|
||||
);
|
||||
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 {
|
||||
|
|
|
|||
Loading…
Reference in a new issue