Both remove_agent() calls now run unconditionally for maximum partial
cleanup, but any I/O error is returned as HTTP 500 instead of silently
200-ing — so the frontend's !resp.ok path fires and the operator sees a
meaningful error rather than the stale row reappearing unchanged.
Also add a clarifying comment on isStale in permissions.js explaining
that containersState is keyed from nixos-container list (which includes
stopped-but-configured containers), so a temporarily-stopped agent is
not treated as stale — only destroyed/renamed agents are absent.
The P3RM1SS10NS tab showed agents that no longer exist in the live
container roster — e.g. an agent named 'root' that was renamed or
destroyed but still had explicit entries in tool-groups.json and/or
capabilities.json. The roster-union behaviour is intentional for
temporarily-stopped agents, but stale entries from renamed/destroyed
agents are confusing.
Backend (dashboard/permissions.rs):
- New DELETE /api/permissions/{agent} handler that bypasses the live-
roster guard (intentionally — that's the point). Calls
tool_groups::remove_agent + capabilities::remove_agent to clear both
JSON files, then emits live SSE snapshots so the tab updates without
a page reload. Format-checks the agent name but does not require it to
be in the containers snapshot.
Frontend (permissions.js):
- renderCapabilities / renderToolGroups now cross-reference agentNames
against containersState (the live roster, already imported). Agents
not in the live roster get an isStale flag.
- Stale rows get a '(not running)' label and a '✕ remove' button that
calls clearStaleAgent() — a new async helper that DELETEs the stale
entry and re-fetches both perm tables.
- Non-stale agents without explicit assignments still get '(default)'.
CSS (dashboard.css):
- .perm-row-stale (reduced opacity), .perm-stale-label (muted small
text), .perm-remove-btn (small red-bordered button) + disabled state.
Replace the per-row save buttons on the P3RM1SS10NS tab with one
page-level "save all (N agents)" button covering both the capabilities
and tool-groups matrices.
Toggling cells stages edits in the DOM; each checkbox carries its
render-time baseline (data-baseline). On save we diff against the
baseline and POST only the perm-types that actually changed per agent
to the new combined endpoint:
POST /api/permissions { changes: [ { agent, tool_groups?, capabilities? } ] }
An omitted field leaves that perm-type untouched (no commit, no diff);
an included array fully replaces it. The backend coalesces caps + groups
for one agent into a single rebuild — no more double-rebuild when an
operator changes both for the same agent. The batch is atomic: on a
clean 200 the bar shows queued -> rebuilding and re-fetches (resetting
baselines); on any validation error nothing is applied and an error note
is shown.
Live capabilities_changed / tool_groups_changed re-renders are skipped
while the section has unsaved edits so a half-finished edit set is not
clobbered; the tab-activation and post-save re-fetches are the recovery
paths.
Pairs with the hive-c0re combined-PermPayload half (damocles).
Second module of the tabs.js split (after the roster keystone). The
P3RM1SS10NS tab — the per-agent capabilities + tool-groups matrices — is
the cleanest leaf: six contiguous functions with no module-scoped state
(each render builds fresh from the fetched payload) and no cross-domain
references except the agent roster, which it now imports from state.js.
Moves applyCapabilitiesChanged / applyToolGroupsChanged (the live-update
handlers wired into the entry's mutation dispatch table) and
fetchAndRenderCapabilities / fetchAndRenderToolGroups (called on tab
activation) into a new permissions.js, exporting those four; the two
renderers stay module-private. tabs.js imports the four — the dispatch
table and tab-activation call-sites resolve unchanged.
Behaviour-preserving: pure code motion. esbuild inlines permissions.js
into the tabs.js bundle, so the static output is unchanged. Build green;
tabs.js drops 258 lines.