Files
stack/agents/sage/work/gitea-setup/setup.mjs
T
jason.woltjeandClaude Opus 5.5 5fe6a051d2 docs(slice1): runbook section 1 run through the Gitea admin API (row 35, #1517, lead decision 74)
Jason ruled that agents run the steps his admin grant to the jarvis
Gitea token covers. Sage created the four mosaic-stack bots (ids
114-117, restricted, non-admin), added them as collaborators (W/W/W/R),
and minted one scoped token each (ids 191-194). The tokens were written
0600 outside the repo. Scripts and receipt are in
agents/sage/work/gitea-setup/. The guide and SR brief now say who
runs which section. Sections 2 to 4 (Vikunja) stay with Jason.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-09 17:25:08 -05:00

75 lines
3.8 KiB
JavaScript

// Runbook section 1 (docs/guides/slice-1-identities.md) via the admin API.
// Prints names, ids and HTTP statuses only. No secret reaches argv, stdout or a URL.
import fs from "node:fs";
import crypto from "node:crypto";
import os from "node:os";
const BASE = "https://git.mosaicstack.dev/api/v1";
const REPO = "mosaicstack/stack";
const S = `${os.homedir()}/.config/mosaic-dev/secrets/mosaic-stack`;
const DATE = new Date().toISOString().slice(0, 10);
const ADMIN_FILE = `${os.homedir()}/.mosaic/fleet/agents/jarvis/secrets/gitea-mosaicstack-jarvis.token`;
const st = fs.lstatSync(ADMIN_FILE);
if (!st.isFile() || (st.mode & 0o077) !== 0) throw new Error("admin token file refused");
const adminTok = fs.readFileSync(ADMIN_FILE, "utf8").trim();
if (!/^[0-9a-f]{40}$/.test(adminTok)) throw new Error("admin token shape refused");
const sd = fs.lstatSync(S);
if (!sd.isDirectory() || (sd.mode & 0o077) !== 0) throw new Error("secrets dir refused");
const ROLES = [
{ r: "pm", perm: "write", scopes: ["write:issue", "read:repository", "read:user"] },
{ r: "cto", perm: "write", scopes: ["write:issue", "write:repository", "read:user"] },
{ r: "coder", perm: "write", scopes: ["write:issue", "write:repository", "read:user"] },
{ r: "reviewer", perm: "read", scopes: ["write:issue", "write:repository", "read:user"] },
];
async function call(method, path, body, auth = `token ${adminTok}`) {
const res = await fetch(`${BASE}/${path}`, {
method,
headers: { Authorization: auth, "Content-Type": "application/json", Accept: "application/json" },
body: body === undefined ? undefined : JSON.stringify(body),
});
const text = await res.text();
let json = null;
try { json = text ? JSON.parse(text) : null; } catch {}
return { status: res.status, json };
}
const only = process.argv.slice(2);
for (const { r, perm, scopes } of ROLES) {
if (only.length && !only.includes(r)) continue;
const u = `mosaic-stack-${r}-bot`;
const file = `${S}/${r}-gitea.token`;
if (fs.existsSync(file)) { console.log(`${u}: token file exists, skipped`); continue; }
let pw = crypto.randomBytes(36).toString("base64url");
const got = await call("GET", `users/${u}`);
let res;
if (got.status === 404) {
res = await call("POST", "admin/users", {
username: u, full_name: `mosaic-stack ${r} bot`, email: `${u}@noreply.mosaicstack.dev`,
password: pw, must_change_password: false, send_notify: false,
restricted: true, visibility: "private",
});
console.log(`${u}: create HTTP ${res.status} id=${res.json?.id} admin=${res.json?.is_admin} restricted=${res.json?.restricted}`);
if (res.status !== 201) { console.log(` message: ${res.json?.message}`); process.exit(1); }
} else if (got.status === 200) {
res = await call("PATCH", `admin/users/${u}`, { login_name: u, source_id: 0, password: pw, must_change_password: false });
console.log(`${u}: exists id=${got.json?.id}, password reset HTTP ${res.status}`);
if (res.status !== 200) process.exit(1);
} else { console.log(`${u}: lookup HTTP ${got.status}`); process.exit(1); }
res = await call("PUT", `repos/${REPO}/collaborators/${u}`, { permission: perm });
console.log(`${u}: collaborator ${perm} HTTP ${res.status}`);
if (res.status !== 204) process.exit(1);
const basic = "Basic " + Buffer.from(`${u}:${pw}`).toString("base64");
pw = null;
res = await call("POST", `users/${u}/tokens`, { name: `mosaic-stack-${r}-${DATE}`, scopes }, basic);
const tok = res.json?.sha1;
console.log(`${u}: token "mosaic-stack-${r}-${DATE}" HTTP ${res.status} id=${res.json?.id} scopes=${(res.json?.scopes || []).join(",")}`);
if (res.status !== 201 || typeof tok !== "string" || tok.length === 0) { console.log(` message: ${res.json?.message}`); process.exit(1); }
fs.writeFileSync(file, tok, { flag: "wx", mode: 0o600 });
}