diff --git a/frontend/packages/dashboard/src/dashboard.css b/frontend/packages/dashboard/src/dashboard.css index d3e6140d..d53825a6 100644 --- a/frontend/packages/dashboard/src/dashboard.css +++ b/frontend/packages/dashboard/src/dashboard.css @@ -902,28 +902,6 @@ footer .banner-thin { } .perm-save-err { color: var(--red); } -/* Stale permission entry (agent not in the live roster) */ -.perm-row-stale td { opacity: 0.7; } -.perm-stale-label { - font-size: 0.75em; - color: var(--muted); - margin-left: 0.4em; -} -.perm-remove-btn { - margin-left: 0.5em; - padding: 0.05em 0.5em; - font-size: 0.75em; - color: var(--red); - background: transparent; - border: 1px solid currentColor; - border-radius: 3px; - cursor: pointer; - font-family: inherit; - opacity: 0.8; -} -.perm-remove-btn:hover { opacity: 1; } -.perm-remove-btn:disabled { opacity: 0.4; cursor: not-allowed; } - /* ─── scheduled prompts tab ──────────────────────────────────────── Creation form at the top, list of queued schedule cards below. */ diff --git a/frontend/packages/dashboard/src/permissions.js b/frontend/packages/dashboard/src/permissions.js index f1d7bef8..83ac0684 100644 --- a/frontend/packages/dashboard/src/permissions.js +++ b/frontend/packages/dashboard/src/permissions.js @@ -122,26 +122,11 @@ function renderCapabilities(root, data) { // Effective caps (explicit-or-default) drive the checkboxes so // default-perms agents show their real grants, not blank. const assigned = effective[name] || assignments[name] || []; - // `containersState` is keyed from `nixos-container list`, which - // includes stopped-but-configured containers — so a temporarily-stopped - // agent is NOT stale. Only destroyed/renamed agents are absent here. - const isStale = !containersState.has(name); - const tr = el('tr', { class: 'cap-row' + (isStale ? ' perm-row-stale' : ''), 'data-agent': name }); + const tr = el('tr', { class: 'cap-row', 'data-agent': name }); // Agent name cell. - const nameTd = el('td', { class: 'cap-agent-col' }); - nameTd.append(el('span', { class: 'cap-agent-name' }, name)); - if (isStale) { - nameTd.append(el('span', { class: 'perm-stale-label' }, '(not running)')); - const removeBtn = el('button', { - type: 'button', - class: 'perm-remove-btn', - title: 'remove stale permission entries for ' + name, - }, '✕ remove'); - removeBtn.addEventListener('click', () => clearStaleAgent(name, root)); - nameTd.append(removeBtn); - } - tr.append(nameTd); + tr.append(el('td', { class: 'cap-agent-col' }, + el('span', { class: 'cap-agent-name' }, name))); // One checkbox per capability. for (const c of caps) { @@ -229,25 +214,12 @@ function renderToolGroups(root, data) { // badge still keys off explicit-assignment presence. const assigned = effective[name] || assignments[name] || []; const hasExplicit = Object.prototype.hasOwnProperty.call(assignments, name); - // `containersState` is keyed from `nixos-container list`, which - // includes stopped-but-configured containers — only destroyed/renamed - // agents are absent. - const isStale = !containersState.has(name); - const tr = el('tr', { class: 'tg-row' + (isStale ? ' perm-row-stale' : ''), 'data-agent': name }); + const tr = el('tr', { class: 'tg-row', 'data-agent': name }); // Agent name cell. const nameTd = el('td', { class: 'tg-agent-col' }); nameTd.append(el('span', { class: 'tg-agent-name' }, name)); - if (isStale) { - nameTd.append(el('span', { class: 'perm-stale-label' }, '(not running)')); - const removeBtn = el('button', { - type: 'button', - class: 'perm-remove-btn', - title: 'remove stale permission entries for ' + name, - }, '✕ remove'); - removeBtn.addEventListener('click', () => clearStaleAgent(name, root)); - nameTd.append(removeBtn); - } else if (!hasExplicit) { + if (!hasExplicit) { nameTd.append(el('span', { class: 'meta tg-default-label' }, '(default)')); } tr.append(nameTd); @@ -328,35 +300,6 @@ function updateSaveBar() { btn.textContent = n === 0 ? 'save all' : `save all (${n} agent${n === 1 ? '' : 's'})`; } -// Remove all explicit permission entries for a stale (non-running) -// agent. Calls DELETE /api/permissions/{agent}, which bypasses the -// roster guard so the stale entries can be cleaned up even though the -// agent isn't in the live container list. Re-fetches both tables after -// the delete so the row disappears immediately. -async function clearStaleAgent(name, sectionRoot) { - // Disable the row's remove button while the request is in flight to - // prevent a double-submit. - const btn = sectionRoot - ? sectionRoot.querySelector(`[data-agent="${CSS.escape(name)}"] .perm-remove-btn`) - : null; - if (btn) btn.disabled = true; - try { - const resp = await fetch('/api/permissions/' + encodeURIComponent(name), { method: 'DELETE' }); - if (!resp.ok) { - const text = await resp.text().catch(() => resp.status); - setSaveNote('failed to remove ' + name + ': ' + text, true); - if (btn) btn.disabled = false; - return; - } - } catch (err) { - setSaveNote('failed to remove ' + name + ': ' + err, true); - if (btn) btn.disabled = false; - return; - } - // Re-fetch both sections so the stale row disappears. - await Promise.all([fetchAndRenderCapabilities(), fetchAndRenderToolGroups()]); -} - function clearSaveStatus() { const note = $('perm-save-note'); if (note) { note.textContent = ''; note.classList.remove('perm-save-err'); } diff --git a/hive-c0re/src/dashboard.rs b/hive-c0re/src/dashboard.rs index c977ec62..3c744202 100644 --- a/hive-c0re/src/dashboard.rs +++ b/hive-c0re/src/dashboard.rs @@ -115,10 +115,6 @@ pub async fn serve(port: u16, coord: Arc) -> Result<()> { post(permissions::post_capabilities), ) .route("/api/permissions", post(permissions::post_permissions)) - .route( - "/api/permissions/{agent}", - axum::routing::delete(permissions::delete_agent_permissions), - ) .route( "/api/schedules", get(schedules::api_schedules).post(schedules::post_schedule_new), diff --git a/hive-c0re/src/dashboard/permissions.rs b/hive-c0re/src/dashboard/permissions.rs index 87031e5d..3829eb32 100644 --- a/hive-c0re/src/dashboard/permissions.rs +++ b/hive-c0re/src/dashboard/permissions.rs @@ -14,7 +14,7 @@ use serde::{Deserialize, Serialize}; use problem_details::ProblemDetails; -use super::{AppState, guard_agent_name, strip_container_prefix, validate_agent_name}; +use super::{AppState, guard_agent_name, strip_container_prefix}; #[derive(Serialize)] pub(super) struct ToolGroupsSnapshot { @@ -312,54 +312,6 @@ pub(super) async fn post_permissions( Ok((StatusCode::OK, "ok").into_response()) } -/// Clear all explicit permission entries for a named agent without -/// requiring it to exist in the live roster. Used by the P3RM1SS10NS -/// tab's "remove" button for agents that have stale explicit entries -/// in `tool-groups.json` / `capabilities.json` but are no longer -/// running (e.g. an agent that was renamed or destroyed while its -/// JSON entries persisted). -/// -/// Bypasses `guard_agent_name`'s live-roster check intentionally — -/// the whole point is to remove entries for non-roster agents. Only -/// the format check (`validate_agent_name`) is applied. No rebuild is -/// enqueued (the agent doesn't exist to rebuild); the SSE snapshots -/// update the P3RM1SS10NS tab live. -pub(super) async fn delete_agent_permissions( - State(state): State, - AxumPath(name): AxumPath, -) -> Response { - let logical = strip_container_prefix(&name); - if let Some(reason) = validate_agent_name(&logical) { - return (StatusCode::BAD_REQUEST, format!("bad agent name: {reason}")).into_response(); - } - // Run both removals regardless so we clean up as much as possible - // even on partial I/O errors. Collect errors to surface below. - let tg_err = crate::tool_groups::remove_agent(&logical).err(); - if let Some(ref e) = tg_err { - tracing::warn!(agent = %logical, error = ?e, "failed to remove tool-groups entry"); - } - let cap_err = crate::capabilities::remove_agent(&logical).err(); - if let Some(ref e) = cap_err { - tracing::warn!(agent = %logical, error = ?e, "failed to remove capabilities entry"); - } - // Emit live snapshots even on partial failure so the UI stays as - // accurate as possible — the surviving table gets updated immediately. - state.coord.emit_tool_groups_snapshot(); - state.coord.emit_capabilities_snapshot(); - // Surface any I/O error as 500 so the frontend's `!resp.ok` path - // fires and the operator sees a meaningful message rather than a - // silent "success" followed by the row reappearing unchanged. - if let Some(e) = tg_err.or(cap_err) { - return ( - StatusCode::INTERNAL_SERVER_ERROR, - format!("failed to remove permission entries for {logical}: {e}"), - ) - .into_response(); - } - tracing::info!(agent = %logical, "operator: cleared stale permission entries via dashboard"); - (StatusCode::OK, "ok").into_response() -} - #[cfg(test)] mod tests { use super::roster_and_effective;