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.
156 lines
5.6 KiB
JavaScript
156 lines
5.6 KiB
JavaScript
// C0R3 page entry (/core.html). The host/coordinator surface, its own
|
|
// standalone page so the dashboard tab strip stays lean: kept state
|
|
// (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
|
|
// /api/state and subscribes to /dashboard/stream (the same broker event
|
|
// channel the dashboard uses), maintaining its own copy of the tombstones
|
|
// state.
|
|
|
|
import { $, form, openStream, initServerWarnings } from "./common.js";
|
|
import { el } from "@hive/shared/dom.js";
|
|
import { bindAsyncForms } from "@hive/shared/forms.js";
|
|
|
|
// ─── derived state (own copies; this bundle has its own runtime) ──────────
|
|
let tombstonesState = [];
|
|
function syncFromSnapshot(s) {
|
|
tombstonesState = (s.tombstones || []).slice();
|
|
}
|
|
|
|
// ─── kept state (tombstones) ──────────────────────────────────────────────
|
|
function renderTombstones(s) {
|
|
const root = $("tombstones-section");
|
|
if (!root) return;
|
|
root.replaceChildren();
|
|
if (!s.tombstones || !s.tombstones.length) {
|
|
root.append(el("p", { class: "empty" }, "no kept state — clean"));
|
|
return;
|
|
}
|
|
const fmtBytes = (n) => {
|
|
if (n < 1024) return n + " B";
|
|
if (n < 1024 * 1024) return (n / 1024).toFixed(1) + " KB";
|
|
if (n < 1024 * 1024 * 1024) return (n / (1024 * 1024)).toFixed(1) + " MB";
|
|
return (n / (1024 * 1024 * 1024)).toFixed(2) + " GB";
|
|
};
|
|
const fmtAgeDays = (ts) => {
|
|
if (!ts) return "?";
|
|
const d = Math.floor((Date.now() / 1000 - ts) / 86400);
|
|
if (d <= 0) return "today";
|
|
if (d === 1) return "1 day ago";
|
|
return d + " days ago";
|
|
};
|
|
// Only shown alongside actual rows — a caveat over an empty list is noise.
|
|
// Wording is deliberately about what the list *is* rather than what it
|
|
// isn't: nothing records a destroy, so "container absent" is the only thing
|
|
// the backend can actually tell.
|
|
const warn = el("hive-warn", { level: "warning" });
|
|
warn.append(
|
|
el("strong", {}, "shows every agent whose container is absent"),
|
|
" — not only destroyed ones. An agent part-way through being spawned " +
|
|
"looks identical here, because nothing records a destroy. Check it is " +
|
|
"really gone before you PURG3.",
|
|
);
|
|
root.append(warn);
|
|
const ul = el("ul", { class: "containers" });
|
|
for (const t of s.tombstones) {
|
|
const li = el("li", { class: "container-row tombstone" });
|
|
const head = el("div", { class: "head" });
|
|
head.append(
|
|
el("span", { class: "name" }, t.name),
|
|
// Was `destroyed`, which the backend cannot actually know — see the
|
|
// caveat above. `offline` is what the absence of a container proves.
|
|
el("span", { class: "hive-pill-sm badge-muted" }, "offline"),
|
|
);
|
|
if (t.has_creds)
|
|
head.append(
|
|
el("span", { class: "hive-pill-sm badge-muted" }, "creds kept"),
|
|
);
|
|
head.append(
|
|
el(
|
|
"span",
|
|
{ class: "meta" },
|
|
`${fmtBytes(t.state_bytes)} · ${fmtAgeDays(t.last_seen)}`,
|
|
),
|
|
);
|
|
li.append(head);
|
|
|
|
const actions = el("div", { class: "actions" });
|
|
actions.append(
|
|
form(
|
|
"/api/purge-tombstone/" + t.name,
|
|
"btn-destroy",
|
|
"PURG3",
|
|
"PURGE " +
|
|
t.name +
|
|
"? config history, claude creds, " +
|
|
"and notes are all WIPED. no undo.",
|
|
{},
|
|
{ noRefresh: true },
|
|
),
|
|
);
|
|
li.append(actions);
|
|
ul.append(li);
|
|
}
|
|
root.append(ul);
|
|
}
|
|
|
|
// ─── render-all (cold load + any full re-render) ──────────────────────────
|
|
function renderAll() {
|
|
renderTombstones({ tombstones: tombstonesState });
|
|
}
|
|
|
|
// ─── live SSE handlers ───────────────────────────────────────────────────
|
|
const SSE_HANDLERS = {
|
|
tombstones_changed(ev) {
|
|
tombstonesState = (ev.tombstones || []).slice();
|
|
renderTombstones({ tombstones: tombstonesState });
|
|
},
|
|
};
|
|
|
|
// ─── boot ─────────────────────────────────────────────────────────────────
|
|
// Re-fetch /api/state, re-sync the derived state, re-render. Used for the
|
|
// cold load and as the post-submit refresh for `data-async` forms whose
|
|
// mutation doesn't arrive via an SSE event (e.g. the meta-update / spawn /
|
|
// purge actions that opt out of `data-no-refresh`).
|
|
async function refreshState() {
|
|
try {
|
|
const resp = await fetch("/api/state");
|
|
if (resp.ok) syncFromSnapshot(await resp.json());
|
|
} catch {
|
|
// best-effort: the page keeps its last-rendered state
|
|
}
|
|
renderAll();
|
|
}
|
|
|
|
async function init() {
|
|
initServerWarnings();
|
|
|
|
// `data-async` form submit interceptor — without this the meta-update /
|
|
// cancel / respawn / purge buttons POST natively and the browser navigates
|
|
// to the bare `ok` response page.
|
|
bindAsyncForms(() => refreshState());
|
|
|
|
await refreshState();
|
|
|
|
// 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");
|
|
if (es) {
|
|
es.onmessage = (e) => {
|
|
let ev;
|
|
try {
|
|
ev = JSON.parse(e.data);
|
|
} catch {
|
|
return;
|
|
}
|
|
const h = SSE_HANDLERS[ev.kind];
|
|
if (h) h(ev);
|
|
};
|
|
}
|
|
}
|
|
|
|
init();
|