agent permissions: one set, tool groups included, set at agent creation
mara on #5170: "dont split caps and perms. its all perms. / dont include the now empty tool groups / nothing set -> default / also add perms to agent creation screen". - hive-sh4re: one `Permission` enum (messaging, meta, inbox, execution, web_tools, manage_root_agent) replaces `ToolGroup` and `Capability`; `lifecycle`/`forge` are gone. `Permission::default_for` is the default for an agent the swarm sets nothing for. - The record is `{ "permissions": [...] }`. Missing key, unset field or no successful read yet = default; `[]` = none; a failed read after a good one keeps the last value. - hive-agent reads its own key every 10s and builds each turn's --tools, --allowedTools and MCP config (rewritten before every turn) from it; the subagent daemon reads it on each start/continue. Agents get a read-only grant on their own key. - hive-c0re restarts an agent only when its manage_root_agent changes. The import now merges meta/capabilities.json and meta/tool-groups.json into one list per agent and deletes both; the "import pending, don't read" decision is `ImportOutcome::read_allowed`, table-tested. - swarm-controller: `GET /api/permissions` (names + default) and an optional `permissions` on `POST /api/agents`, stored before the job is queued. swarm-ui's create-agent form gets a permissions field. - Deleted: HIVE_TOOL_GROUPS and its forward to the subagent daemon, tool_groups.rs, ruth's tool-group seed, the permissions rebuild job (PermPayload/WritePermFile/perm_change), the dashboard permissions tab and its stale-entries panel. Part of #5170.
This commit is contained in:
parent
f862354e43
commit
f0844f6da3
73 changed files with 1578 additions and 2742 deletions
|
|
@ -1,6 +1,6 @@
|
|||
// C0R3 page entry (/core.html). The host/coordinator surface, its own
|
||||
// standalone page so the dashboard tab strip stays lean: kept state
|
||||
// (tombstones) and the stale permission entries.
|
||||
// (tombstones).
|
||||
//
|
||||
// Rebuild queue + meta inputs are tabs of /dashboard.html, not here. This
|
||||
// page is its own esbuild bundle with its own runtime: it cold-loads
|
||||
|
|
@ -10,7 +10,7 @@
|
|||
|
||||
import { $, form, openStream, initServerWarnings } from "./common.js";
|
||||
import { el } from "@hive/shared/dom.js";
|
||||
import { asyncBtn, bindAsyncForms } from "@hive/shared/forms.js";
|
||||
import { bindAsyncForms } from "@hive/shared/forms.js";
|
||||
|
||||
// ─── derived state (own copies; this bundle has its own runtime) ──────────
|
||||
let tombstonesState = [];
|
||||
|
|
@ -95,92 +95,6 @@ function renderTombstones(s) {
|
|||
root.append(ul);
|
||||
}
|
||||
|
||||
// ─── stale permission entries (K3PT ST4T3 pane sub-section) ──────────────
|
||||
// Agents with explicit 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 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: "hive-pill-sm badge-muted" }, "stale perms"));
|
||||
const btn = el(
|
||||
"button",
|
||||
{
|
||||
type: "button",
|
||||
class: "btn btn-destroy",
|
||||
title: "remove explicit tool-group entries for " + name,
|
||||
},
|
||||
"✕ clear perms",
|
||||
);
|
||||
btn.addEventListener("click", () =>
|
||||
asyncBtn(btn, async () => {
|
||||
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;
|
||||
return;
|
||||
}
|
||||
await fetchAndRenderStalePerms();
|
||||
} catch (err) {
|
||||
errP.textContent = "failed to clear perms for " + name + ": " + err;
|
||||
errP.hidden = false;
|
||||
}
|
||||
}),
|
||||
);
|
||||
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;
|
||||
}
|
||||
|
||||
// ─── render-all (cold load + any full re-render) ──────────────────────────
|
||||
function renderAll() {
|
||||
renderTombstones({ tombstones: tombstonesState });
|
||||
|
|
@ -192,11 +106,6 @@ 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).
|
||||
tool_groups_changed(_ev) {
|
||||
if (stalePermsLoaded) fetchAndRenderStalePerms();
|
||||
},
|
||||
};
|
||||
|
||||
// ─── boot ─────────────────────────────────────────────────────────────────
|
||||
|
|
@ -223,16 +132,13 @@ async function init() {
|
|||
bindAsyncForms(() => refreshState());
|
||||
|
||||
await refreshState();
|
||||
fetchAndRenderStalePerms();
|
||||
|
||||
// kinds= matches SSE_HANDLERS below verbatim — narrows this from all
|
||||
// 13 wire kinds down to the 3 this page acts on. This page was one
|
||||
// kinds= matches SSE_HANDLERS below verbatim — narrows the stream down
|
||||
// to the kinds this page acts on. This page was one
|
||||
// of 4 unfiltered `/api/dashboard/stream` subscribers before this
|
||||
// (subscription discipline, part 1 of the dashboard-event-stream-
|
||||
// split issue).
|
||||
const es = openStream(
|
||||
"/api/dashboard/stream?kinds=tombstones_changed,tool_groups_changed",
|
||||
);
|
||||
const es = openStream("/api/dashboard/stream?kinds=tombstones_changed");
|
||||
if (es) {
|
||||
es.onmessage = (e) => {
|
||||
let ev;
|
||||
|
|
|
|||
Loading…
Reference in a new issue