// — writes a GitHub personal access token for one // agent into the swarm secret store. // PUTs `/api/hives/{hive}/agents/{agent}/github-account` — 204 on success, // 400/500 as `problem+json`, shown via `ApiErrorPanel` like // `LinkForgeAccountForm`. // // One token per agent. A blind set/update: no route says whether a token is // stored, and none hands one back. import { useState } from "preact/hooks"; import { ApiErrorPanel } from "@hive/shared/api-error-panel.js"; import { readApiError, type ProblemDetails } from "@hive/shared/api-error.js"; import { Panel } from "../ui/panel/Panel.js"; import { TextField } from "../ui/text-field/TextField.js"; import { Button } from "../ui/button/Button.js"; import "./LinkMatrixAccountForm.css"; type SubmitState = | { status: "idle" } | { status: "submitting" } | { status: "done" } | { status: "error"; problem: ProblemDetails }; export function LinkGithubAccountForm({ hive, agent, onClose, }: { hive: string; agent: string; onClose?: () => void; }) { const [token, setToken] = useState(""); const [result, setResult] = useState({ status: "idle" }); async function submit(e: Event) { e.preventDefault(); setResult({ status: "submitting" }); try { const r = await fetch( `/api/hives/${encodeURIComponent(hive)}/agents/${encodeURIComponent(agent)}/github-account`, { method: "PUT", headers: { "content-type": "application/json" }, body: JSON.stringify({ token }), }, ); if (!r.ok) { setResult({ status: "error", problem: await readApiError(r) }); return; } setResult({ status: "done" }); // The store holds the token; nothing here needs it. setToken(""); } catch (err) { setResult({ status: "error", problem: { detail: String(err) } }); } } return (

Writes the token to the swarm secret store. The agent fetches it within two minutes, and its gh and git push to github.com then authenticate with it. Use a dedicated bot account and a minimally scoped token, created at{" "} github.com/settings/tokens ; GitHub notifications also need the notifications scope.

); }