Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
75e49f7752 | ||
|
|
cffe645197 | ||
|
|
4df286345a | ||
|
|
6ab0757cc6 |
5 changed files with 181 additions and 2 deletions
|
|
@ -71,13 +71,19 @@
|
|||
</div>
|
||||
</section>
|
||||
|
||||
<!-- K3PT ST4T3: tombstoned-agent kept state. -->
|
||||
<!-- K3PT ST4T3: tombstoned-agent kept state + stale permission entries.
|
||||
tombstones-section shows destroyed agents (revive / purge buttons).
|
||||
tombstones-stale-perms is lazy-loaded on tab activation and shows
|
||||
agents with explicit capability/tool-group entries but no live
|
||||
container (typically renamed/deleted agents whose JSON entries
|
||||
persisted). Each stale entry gets a "✕ clear perms" button. -->
|
||||
<section class="core-pane" id="core-pane-kept" data-tab-pane="kept"
|
||||
role="tabpanel" aria-labelledby="core-tab-kept">
|
||||
<p class="meta">kept state from previously tombstoned agents — recreating an agent with the same name reuses it.</p>
|
||||
<div id="tombstones-section">
|
||||
<p class="meta">loading…</p>
|
||||
</div>
|
||||
<div id="tombstones-stale-perms"></div>
|
||||
</section>
|
||||
|
||||
<!-- C0NT41N3R L04D: live cpu + memory per agent container, from
|
||||
|
|
|
|||
|
|
@ -20,7 +20,6 @@ let metaInputsState = [];
|
|||
let metaUpdateRunning = false;
|
||||
let tombstonesState = [];
|
||||
let rebuildQueueState = [];
|
||||
|
||||
function syncFromSnapshot(s) {
|
||||
metaInputsState = (s.meta_inputs || []).slice();
|
||||
metaUpdateRunning = !!s.meta_update_running;
|
||||
|
|
@ -447,6 +446,78 @@ function renderTombstones(s) {
|
|||
root.append(ul);
|
||||
}
|
||||
|
||||
// ─── stale permission entries (K3PT ST4T3 pane sub-section) ──────────────
|
||||
// Agents with explicit capability / tool-group JSON entries but no live
|
||||
// container AND no kept-state tombstone (e.g. renamed agents like the old
|
||||
// "root" manager name). Ghost detection is server-side via
|
||||
// GET /api/permissions/stale so the client doesn't need to maintain a
|
||||
// container-roster cache or perform set arithmetic. Lazy-loaded on first
|
||||
// "kept" tab activation; refreshed when perm data changes via SSE.
|
||||
let stalePermsLoaded = false;
|
||||
|
||||
function renderStalePerms(root, ghosts) {
|
||||
root.replaceChildren();
|
||||
if (!ghosts.length) return;
|
||||
root.append(el('p', { class: 'tombstones-stale-heading' }, 'stale permission entries'));
|
||||
root.append(el('p', { class: 'meta' },
|
||||
'agents with explicit capability or tool-group entries but no live container '
|
||||
+ 'or kept state (typically renamed or manually-deleted agents whose JSON entries persisted).'));
|
||||
const errP = el('p', { class: 'tombstones-stale-err', hidden: true });
|
||||
const ul = el('ul', { class: 'tombstones-stale-list' });
|
||||
for (const name of ghosts) {
|
||||
const li = el('li', { class: 'tombstones-stale-row' });
|
||||
li.append(el('span', { class: 'tombstones-stale-name' }, name));
|
||||
li.append(el('span', { class: 'badge badge-muted' }, 'stale perms'));
|
||||
const btn = el('button', {
|
||||
type: 'button',
|
||||
class: 'btn btn-destroy',
|
||||
title: 'remove explicit capability and tool-group entries for ' + name,
|
||||
}, '✕ clear perms');
|
||||
btn.addEventListener('click', async () => {
|
||||
btn.disabled = true;
|
||||
errP.hidden = true;
|
||||
try {
|
||||
const resp = await fetch('/api/permissions/' + encodeURIComponent(name), { method: 'DELETE' });
|
||||
if (!resp.ok) {
|
||||
const msg = await resp.text().catch(() => String(resp.status));
|
||||
errP.textContent = 'failed to clear perms for ' + name + ': ' + msg;
|
||||
errP.hidden = false;
|
||||
btn.disabled = false;
|
||||
return;
|
||||
}
|
||||
} catch (err) {
|
||||
errP.textContent = 'failed to clear perms for ' + name + ': ' + err;
|
||||
errP.hidden = false;
|
||||
btn.disabled = false;
|
||||
return;
|
||||
}
|
||||
await fetchAndRenderStalePerms();
|
||||
});
|
||||
li.append(btn);
|
||||
ul.append(li);
|
||||
}
|
||||
root.append(ul, errP);
|
||||
}
|
||||
|
||||
// Ghost detection is entirely server-side: GET /api/permissions/stale
|
||||
// returns the computed list of agent names that have explicit JSON entries
|
||||
// but are absent from both the live roster and the kept-state tombstones.
|
||||
// One call, no client-side roster cache, always authoritative.
|
||||
async function fetchAndRenderStalePerms() {
|
||||
const root = $('tombstones-stale-perms');
|
||||
if (!root) return;
|
||||
try {
|
||||
const resp = await fetch('/api/permissions/stale');
|
||||
if (!resp.ok) throw new Error('http ' + resp.status);
|
||||
const data = await resp.json();
|
||||
renderStalePerms(root, data.stale || []);
|
||||
} catch (err) {
|
||||
root.replaceChildren();
|
||||
root.append(el('p', { class: 'meta' }, 'failed to load stale perm data: ' + err));
|
||||
}
|
||||
stalePermsLoaded = true;
|
||||
}
|
||||
|
||||
// ─── container load (live cgroup poll while the LOAD sub-tab is open) ──────
|
||||
let containerLoadTimer = null;
|
||||
|
||||
|
|
@ -602,6 +673,14 @@ const SSE_HANDLERS = {
|
|||
tombstonesState = (ev.tombstones || []).slice();
|
||||
renderTombstones({ tombstones: tombstonesState });
|
||||
},
|
||||
// Refresh the stale-perms sub-section when perm data changes (a ghost
|
||||
// was cleared, or perms were saved for an agent whose name collides).
|
||||
capabilities_changed(_ev) {
|
||||
if (stalePermsLoaded) fetchAndRenderStalePerms();
|
||||
},
|
||||
tool_groups_changed(_ev) {
|
||||
if (stalePermsLoaded) fetchAndRenderStalePerms();
|
||||
},
|
||||
};
|
||||
|
||||
// ─── boot ─────────────────────────────────────────────────────────────────
|
||||
|
|
@ -635,6 +714,9 @@ async function init() {
|
|||
onShow: (id) => {
|
||||
if (id === 'load') startContainerLoadPolling();
|
||||
else stopContainerLoadPolling();
|
||||
// Lazy-load stale-perms on first K3PT ST4T3 activation; always
|
||||
// re-fetch on subsequent visits in case perms changed.
|
||||
if (id === 'kept') fetchAndRenderStalePerms();
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -221,3 +221,42 @@
|
|||
.cload-meter .fill.hot {
|
||||
background: var(--red);
|
||||
}
|
||||
|
||||
/* ─── K3PT ST4T3 › stale permission entries sub-section ────────────────────
|
||||
Agents with explicit capability / tool-group entries in the JSON but no
|
||||
live container (renamed or manually-deleted agents). Lazy-loaded on tab
|
||||
activation, refreshed on capabilities_changed / tool_groups_changed. */
|
||||
.tombstones-stale-heading {
|
||||
margin: 1.2em 0 0.3em;
|
||||
color: var(--muted);
|
||||
font-size: 0.8em;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.08em;
|
||||
border-top: 1px solid var(--border);
|
||||
padding-top: 0.8em;
|
||||
}
|
||||
.tombstones-stale-list {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
display: grid;
|
||||
gap: 0.3em;
|
||||
}
|
||||
.tombstones-stale-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5em;
|
||||
padding: 0.35em 0.6em;
|
||||
border: 1px dashed var(--border);
|
||||
background: color-mix(in srgb, var(--bg-elev) 35%, transparent);
|
||||
}
|
||||
.tombstones-stale-name {
|
||||
color: var(--muted);
|
||||
font-weight: bold;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
.tombstones-stale-err {
|
||||
margin-top: 0.4em;
|
||||
color: var(--red);
|
||||
font-size: 0.85em;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -115,6 +115,10 @@ pub async fn serve(port: u16, coord: Arc<Coordinator>) -> Result<()> {
|
|||
post(permissions::post_capabilities),
|
||||
)
|
||||
.route("/api/permissions", post(permissions::post_permissions))
|
||||
.route(
|
||||
"/api/permissions/stale",
|
||||
get(permissions::get_stale_permissions),
|
||||
)
|
||||
.route(
|
||||
"/api/permissions/{agent}",
|
||||
axum::routing::delete(permissions::delete_agent_permissions),
|
||||
|
|
|
|||
|
|
@ -312,6 +312,54 @@ pub(super) async fn post_permissions(
|
|||
Ok((StatusCode::OK, "ok").into_response())
|
||||
}
|
||||
|
||||
/// Agent names that have explicit capability/tool-group entries but are
|
||||
/// not in the live container roster AND not in the kept-state directory
|
||||
/// list (i.e. truly gone — renamed or destroyed agents whose JSON entries
|
||||
/// 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)]
|
||||
pub(super) struct StalePermsResponse {
|
||||
/// Ghost agent names, sorted. Empty list → no stale entries.
|
||||
stale: Vec<String>,
|
||||
}
|
||||
|
||||
pub(super) async fn get_stale_permissions(
|
||||
State(state): State<AppState>,
|
||||
) -> axum::Json<StalePermsResponse> {
|
||||
// Live container names — includes stopped-but-configured containers.
|
||||
let live: std::collections::HashSet<String> = state
|
||||
.coord
|
||||
.containers_snapshot()
|
||||
.await
|
||||
.into_iter()
|
||||
.map(|c| c.name)
|
||||
.collect();
|
||||
// Kept-state directories: every agent that ever had a state dir on disk,
|
||||
// including both live containers (already in `live`) and soft-deleted
|
||||
// tombstones (destroyed but kept). A name present here is either live
|
||||
// or a properly-removed tombstone — neither is a ghost.
|
||||
let kept: std::collections::HashSet<String> =
|
||||
crate::coordinator::Coordinator::kept_state_names()
|
||||
.into_iter()
|
||||
.collect();
|
||||
// Known = live roster ∪ kept-state names.
|
||||
let known: std::collections::HashSet<&String> = live.iter().chain(kept.iter()).collect();
|
||||
// Explicit entries in either JSON file.
|
||||
let caps = crate::capabilities::read();
|
||||
let tgs = crate::tool_groups::read();
|
||||
let mut ghost_names: Vec<String> = caps
|
||||
.keys()
|
||||
.chain(tgs.keys())
|
||||
.filter(|n| !known.contains(n))
|
||||
.cloned()
|
||||
.collect::<std::collections::HashSet<_>>()
|
||||
.into_iter()
|
||||
.collect();
|
||||
ghost_names.sort();
|
||||
axum::Json(StalePermsResponse { stale: ghost_names })
|
||||
}
|
||||
|
||||
/// 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
|
||||
|
|
|
|||
Loading…
Reference in a new issue