forge: external forge accounts live in swarm bao; the agent fetches them itself
An operator now links an agent's external forge account (label, base URL, token) in the swarm UI. swarm-controller stores it at swarm/agents/<agent>/forge/<label>. There is no index: the store's listing of the agent's forge/ directory is the set of accounts. In the agent, hive-agent-forge-accounts (oneshot + 2-minute timer, as the agent user, under its own store certificate) lists swarm/agents/<agent>/forge/ with the `list` #4866 grants an agent on its own metadata subtree, reads each account, and writes <state>/forge-<label>-token and forge-<label>.json in the names and shape hive-forge -f already reads. An empty listing (a 404, which `bao kv list -format=json` answers with `{}` and an empty stderr) is zero accounts; a denial or an unreachable store fails the unit. It never deletes: files for labels not listed, including ones the hive wrote, stay as they are. Removed: the dashboard FORGES tab (credentials.js/html section and its CSS), hive-c0re's extra_forges.rs and its routes, priv_client's extra-forge calls, and hive-priv's WriteAgentExtraForgeAccount / DeleteAgentExtraForgeAccount with their helpers. The GITHUB tab and WriteAgentGithubToken stay. Also: persistence.md's matrix avatar note names the exit-75 restart on a changed account listing, not the dashboard, as what brings a linked account up. Refs #4348
This commit is contained in:
parent
97fb76ce99
commit
2c7e586f47
27 changed files with 815 additions and 748 deletions
|
|
@ -81,43 +81,6 @@ body.cred-shell {
|
|||
border-color: var(--purple);
|
||||
}
|
||||
|
||||
.ma-accounts {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
.ma-account {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.6rem;
|
||||
padding: 0.45rem 0.2rem;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
.ma-dot {
|
||||
width: 0.6rem;
|
||||
height: 0.6rem;
|
||||
border-radius: 50%;
|
||||
flex: none;
|
||||
}
|
||||
.ma-dot.ok {
|
||||
background: var(--green);
|
||||
}
|
||||
.ma-name {
|
||||
font-weight: 600;
|
||||
color: var(--fg);
|
||||
}
|
||||
.ma-hs {
|
||||
color: var(--muted);
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
.ma-status {
|
||||
margin-left: auto;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
.ma-status.ok {
|
||||
color: var(--green);
|
||||
}
|
||||
|
||||
.ma-result {
|
||||
margin-top: 0.7rem;
|
||||
font-size: 0.9rem;
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@
|
|||
</head>
|
||||
<body class="cred-shell">
|
||||
<!-- Minimal chrome: back link + sub-tab strip, same pattern as
|
||||
logs.html (GITHUB / FORGES instead of AGENT/INFRA/SYSTEM). Back
|
||||
logs.html (GITHUB instead of AGENT/INFRA/SYSTEM). Back
|
||||
link points to the H0M3 hub (served at /). -->
|
||||
<header class="page-header">
|
||||
<a class="page-back" href="/">← home</a>
|
||||
|
|
@ -25,8 +25,7 @@
|
|||
</header>
|
||||
|
||||
<main class="cred-main">
|
||||
<!-- Agent picker: shared across the tabs (one agent selected at a
|
||||
time drives both the github status and the forge list). -->
|
||||
<!-- Agent picker: the selected agent drives the github status. -->
|
||||
<h3>◇ agent</h3>
|
||||
<label class="ma-field">
|
||||
<span>agent</span>
|
||||
|
|
@ -81,61 +80,6 @@
|
|||
<p id="gh-result" class="ma-result" aria-live="polite"></p>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
<!-- FORGES tab: external Forgejo/Gitea/Codeberg-compatible forges.
|
||||
Entirely dashboard-provisioned, no host-side nix config — same
|
||||
shape as GITHUB plus a base-URL field.
|
||||
The operator creates a token on the external forge themselves
|
||||
(however that forge lets them) and pastes label + URL + token
|
||||
below. No remote account minting/revoking — purely local. -->
|
||||
<section
|
||||
class="cred-pane"
|
||||
id="cred-pane-forges"
|
||||
data-tab-pane="forges"
|
||||
role="tabpanel"
|
||||
aria-labelledby="cred-tab-forges"
|
||||
hidden
|
||||
>
|
||||
<p class="meta">
|
||||
store a <strong>label + base URL + access token</strong> for an
|
||||
external Forgejo/Gitea/Codeberg-compatible forge, per agent. no
|
||||
account is created on the remote forge — create the token there
|
||||
yourself first. the token is never displayed back on this page.
|
||||
</p>
|
||||
|
||||
<h3>◇ provisioned forges</h3>
|
||||
<div id="ef-list" class="ef-list">
|
||||
<p class="meta">select an agent to see its forge accounts.</p>
|
||||
</div>
|
||||
|
||||
<h3>◇ add forge account</h3>
|
||||
<form id="ef-form" class="ma-form" autocomplete="off">
|
||||
<label class="ma-field">
|
||||
<span>label</span>
|
||||
<input
|
||||
type="text"
|
||||
name="label"
|
||||
placeholder="e.g. codeberg"
|
||||
required
|
||||
/>
|
||||
</label>
|
||||
<label class="ma-field">
|
||||
<span>base url</span>
|
||||
<input
|
||||
type="text"
|
||||
name="base_url"
|
||||
placeholder="https://codeberg.org"
|
||||
required
|
||||
/>
|
||||
</label>
|
||||
<label class="ma-field">
|
||||
<span>access token</span>
|
||||
<input type="password" name="token" autocomplete="off" required />
|
||||
</label>
|
||||
<button type="submit" class="btn btn-spawn">store account</button>
|
||||
<p id="ef-result" class="ma-result" aria-live="polite"></p>
|
||||
</form>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<script type="module" src="/static/credentials.js" defer></script>
|
||||
|
|
|
|||
|
|
@ -1,25 +1,17 @@
|
|||
// CR3D3NTIALS page entry (/credentials.html).
|
||||
//
|
||||
// Operator surface to provision per-agent credentials without editing the
|
||||
// agent's config repo. Two sub-tabs, sharing one agent picker:
|
||||
// agent's config repo. One sub-tab and an agent picker:
|
||||
// GITHUB — single-account PAT paste against /api/github-account
|
||||
// (GET -> {present}, POST form-encoded {agent, token} ->
|
||||
// {ok:true}; error_response shape on failure).
|
||||
// No account name / homeserver / login mode, and no
|
||||
// live/heartbeat concept for a static PAT — just present/absent.
|
||||
// FORGES — external forge accounts, entirely dashboard-provisioned (no
|
||||
// host-side config): GET /api/extra-forges?agent= lists the
|
||||
// agent's stored {label, base_url} pairs, POST
|
||||
// /api/extra-forge-account (form agent/label/base_url/token/
|
||||
// action=add|remove) stores or removes one. No remote account
|
||||
// creation — the operator makes the token on the external forge
|
||||
// themselves and pastes it in, same trust model as GITHUB.
|
||||
// Per-tab detail comments live next to their section below.
|
||||
|
||||
import { $, esc, renderServerWarnings } from "./common.js";
|
||||
import { el } from "@hive/shared/dom.js";
|
||||
import "@hive/shared/hive-tab-strip.js";
|
||||
import { themedConfirm, themedToast } from "@hive/shared/modal.js";
|
||||
import { readApiError, problemMessage } from "@hive/shared/api-error.js";
|
||||
|
||||
let agents = [];
|
||||
|
|
@ -185,179 +177,10 @@ async function submitGithub(e) {
|
|||
}
|
||||
}
|
||||
|
||||
// ─── FORGES tab ─────────────────────────────────────────────────────────
|
||||
// Entirely dashboard-provisioned, no host-side nix config: per-agent list
|
||||
// (GET /api/extra-forges?agent=, derived from the agent's own
|
||||
// forge-<label>-token files) + an add form (POST /api/extra-forge-account,
|
||||
// form label/base_url/token, action=add) and a remove button per row
|
||||
// (same POST, action=remove). No remote account creation — purely local
|
||||
// bookkeeping for a token the operator already created on the external
|
||||
// forge themselves.
|
||||
|
||||
async function loadForgeAccounts(agent) {
|
||||
const list = $("ef-list");
|
||||
if (!agent) {
|
||||
list.replaceChildren(
|
||||
el("p", { class: "meta" }, "select an agent to see its forge accounts."),
|
||||
);
|
||||
return;
|
||||
}
|
||||
list.replaceChildren(el("p", { class: "meta" }, "loading…"));
|
||||
let forges;
|
||||
try {
|
||||
const resp = await fetch(
|
||||
"/api/extra-forges?agent=" + encodeURIComponent(agent),
|
||||
);
|
||||
if (!resp.ok) throw new Error("HTTP " + resp.status);
|
||||
forges = (await resp.json()).forges || [];
|
||||
} catch (err) {
|
||||
list.replaceChildren(
|
||||
el(
|
||||
"p",
|
||||
{ class: "err" },
|
||||
"could not load forge accounts: " +
|
||||
esc(String(err)) +
|
||||
" (the backend endpoint may not be deployed yet).",
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
list.replaceChildren();
|
||||
if (!forges.length) {
|
||||
list.replaceChildren(
|
||||
el("p", { class: "meta" }, "no forge accounts stored for this agent."),
|
||||
);
|
||||
return;
|
||||
}
|
||||
const ul = el("ul", { class: "ma-accounts" });
|
||||
for (const forge of forges) {
|
||||
const btn = el("button", { class: "btn", type: "button" }, "remove");
|
||||
btn.addEventListener("click", () => onForgeRemoveClick(agent, forge, btn));
|
||||
ul.append(
|
||||
el(
|
||||
"li",
|
||||
{ class: "ma-account" },
|
||||
el("span", { class: "ma-dot ok" }),
|
||||
el("span", { class: "ma-name" }, forge.label),
|
||||
el("span", { class: "ma-hs" }, forge.base_url || "—"),
|
||||
el("span", { class: "ma-status ok" }, "token stored ✓"),
|
||||
btn,
|
||||
),
|
||||
);
|
||||
}
|
||||
list.append(ul);
|
||||
}
|
||||
|
||||
async function onForgeRemoveClick(agent, forge, btn) {
|
||||
const r = await themedConfirm({
|
||||
message: `remove ${agent}'s stored token for ${forge.label}? this only deletes the local copy — nothing changes on the remote forge.`,
|
||||
danger: true,
|
||||
confirmLabel: "⊘ remove",
|
||||
});
|
||||
if (!r) return;
|
||||
|
||||
btn.disabled = true;
|
||||
const orig = btn.textContent;
|
||||
btn.textContent = "removing…";
|
||||
try {
|
||||
const resp = await fetch("/api/extra-forge-account", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
||||
body: new URLSearchParams({
|
||||
agent,
|
||||
label: forge.label,
|
||||
action: "remove",
|
||||
}),
|
||||
});
|
||||
if (resp.ok) {
|
||||
loadForgeAccounts(agent);
|
||||
return;
|
||||
}
|
||||
const msg = problemMessage(await readApiError(resp));
|
||||
btn.textContent = orig;
|
||||
btn.disabled = false;
|
||||
themedToast("✗ " + (msg || "remove failed (HTTP " + resp.status + ")"), {
|
||||
type: "error",
|
||||
});
|
||||
} catch (err) {
|
||||
btn.textContent = orig;
|
||||
btn.disabled = false;
|
||||
themedToast("✗ request failed: " + String(err), { type: "error" });
|
||||
}
|
||||
}
|
||||
|
||||
async function submitForgeAccount(e) {
|
||||
e.preventDefault();
|
||||
const formEl = e.target;
|
||||
const out = $("ef-result");
|
||||
out.className = "ma-result";
|
||||
out.textContent = "";
|
||||
|
||||
const agent = $("ma-agent").value;
|
||||
if (!agent) {
|
||||
out.className = "ma-result err";
|
||||
out.textContent = "select an agent first.";
|
||||
return;
|
||||
}
|
||||
|
||||
const fd = new FormData(formEl);
|
||||
fd.set("agent", agent);
|
||||
fd.set("action", "add");
|
||||
|
||||
const btn = formEl.querySelector('button[type="submit"]');
|
||||
const orig = btn.textContent;
|
||||
btn.disabled = true;
|
||||
btn.textContent = "storing…";
|
||||
|
||||
try {
|
||||
const resp = await fetch("/api/extra-forge-account", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
||||
body: new URLSearchParams(fd),
|
||||
});
|
||||
|
||||
if (resp.ok) {
|
||||
let body = {};
|
||||
try {
|
||||
body = await resp.json();
|
||||
} catch {
|
||||
/* tolerate odd 2xx body */
|
||||
}
|
||||
if (body.ok) {
|
||||
out.className = "ma-result ok";
|
||||
out.textContent = "✓ forge account stored.";
|
||||
clearSecrets(formEl);
|
||||
loadForgeAccounts(agent);
|
||||
} else {
|
||||
out.className = "ma-result err";
|
||||
out.textContent = "✗ store failed (unexpected response).";
|
||||
clearSecrets(formEl);
|
||||
}
|
||||
} else {
|
||||
const msg = problemMessage(await readApiError(resp));
|
||||
out.className = "ma-result err";
|
||||
out.textContent =
|
||||
"✗ " + (msg || "store failed (HTTP " + resp.status + ")");
|
||||
clearSecrets(formEl);
|
||||
}
|
||||
} catch (err) {
|
||||
out.className = "ma-result err";
|
||||
out.textContent =
|
||||
"✗ request failed: " +
|
||||
String(err) +
|
||||
" (the backend endpoint may not be deployed yet).";
|
||||
} finally {
|
||||
btn.disabled = false;
|
||||
btn.textContent = orig;
|
||||
}
|
||||
}
|
||||
|
||||
// ─── init ─────────────────────────────────────────────────────────────
|
||||
|
||||
async function onAgentChange(agent) {
|
||||
loadGithubStatus(agent);
|
||||
loadForgeAccounts(agent);
|
||||
}
|
||||
|
||||
async function init() {
|
||||
|
|
@ -367,13 +190,9 @@ async function init() {
|
|||
onAgentChange(e.target.value),
|
||||
);
|
||||
$("gh-form").addEventListener("submit", submitGithub);
|
||||
$("ef-form").addEventListener("submit", submitForgeAccount);
|
||||
|
||||
document.getElementById("cred-tabbar").configure({
|
||||
tabs: [
|
||||
{ id: "github", label: "GITHUB" },
|
||||
{ id: "forges", label: "FORGES" },
|
||||
],
|
||||
tabs: [{ id: "github", label: "GITHUB" }],
|
||||
defaultId: "github",
|
||||
});
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue