Let the forge's tools never weaken protection or merge past the checks
mesh/merge-gate pass: builds gitea → novox; no bus step; every machine composes with the change as it did without (4 of 4 compose)
mesh/repo-check pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-check-typescript.sh (typescript)
mesh/delivery superseded: a newer head of the same pull request
mesh/merge-gate pass: builds gitea → novox; no bus step; every machine composes with the change as it did without (4 of 4 compose)
mesh/repo-check pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-check-typescript.sh (typescript)
mesh/delivery superseded: a newer head of the same pull request
gitea_api took any endpoint with a token minted for the forge's administrator with write:admin: an agent could drop a branch's protection or its required checks, set the merge check's status, write to main or merge its own pull request (novox/hq ADR 0266 §7, the review of 2026-10-09). gitea_api now reads anything but the administration and writes only what an author writes; protection is only strengthened; a merge needs every required status a success and merges only that head; a guarded repository is not deleted; and the token is minted without write:admin, a kept one re-minted at the next start.
This commit is contained in:
@@ -17,7 +17,7 @@ import { join } from "node:path";
|
||||
import { collectTools } from "@novox/mesh-sdk/tools";
|
||||
import { AdminRefused, MintedToken, TOKEN_SCOPES } from "../dist/token.js";
|
||||
import { GiteaClient } from "../dist/client.js";
|
||||
import "../dist/tools/index.js";
|
||||
import { getGiteaTools } from "../dist/tools/index.js";
|
||||
|
||||
const ADMIN = "mesh-admin";
|
||||
const PASSWORD = "the-vault-minted-this";
|
||||
@@ -230,10 +230,12 @@ test("first start: mints with the admin account, keeps the token at 0600, asks f
|
||||
|
||||
assert.equal(repos[0]?.full_name, "novox/hq");
|
||||
assert.equal(forge.mints, 1);
|
||||
assert.deepEqual(forge.lastScopes, ["write:repository", "write:issue", "read:user", "write:admin"]);
|
||||
// Never write:admin (novox/hq ADR 0266): the forge's administration is the operator's, not a tool's.
|
||||
assert.deepEqual(forge.lastScopes, ["write:repository", "write:issue", "read:user"]);
|
||||
assert.deepEqual(forge.lastScopes, [...TOKEN_SCOPES]);
|
||||
const token = forge.tokens.get("mesh-tools")!;
|
||||
assert.equal(await readFile(file, "utf8"), token + "\n");
|
||||
assert.equal(await readFile(file + ".scopes", "utf8"), "read:user,write:issue,write:repository\n");
|
||||
assert.equal((await stat(file)).mode & 0o777, 0o600);
|
||||
// Said that it minted, and where it keeps it — never what it is.
|
||||
assert.ok(logs.some((l) => l.startsWith("minted a token")), logs.join("\n"));
|
||||
@@ -268,32 +270,45 @@ test("the forge rejects the kept token (its data was restored): minted afresh, o
|
||||
assert.ok(logs.some((l) => l.startsWith("the forge rejected the kept token")), logs.join("\n"));
|
||||
});
|
||||
|
||||
test("a kept token from before write:admin: the forge refuses the admin route for the scope, the token is re-minted with the whole list, and the call goes through", async () => {
|
||||
test("a token kept with write:admin by an earlier build is minted again without it at the next start, and the forge drops the old one", async () => {
|
||||
const { env, file, logs } = await delivered(forge);
|
||||
await minted(env, logs).listRepos();
|
||||
const old = forge.tokens.get("mesh-tools")!;
|
||||
forge.scopesOf.set(old, ["write:repository", "write:issue", "read:user", "write:admin"]);
|
||||
await rm(file + ".scopes"); // an earlier build kept no scopes beside it
|
||||
const before = forge.mints;
|
||||
|
||||
const again: string[] = [];
|
||||
await minted(env, again).listRepos();
|
||||
|
||||
assert.equal(forge.mints, before + 1);
|
||||
assert.deepEqual(forge.lastScopes, [...TOKEN_SCOPES]);
|
||||
assert.ok(!forge.lastScopes!.includes("write:admin"));
|
||||
assert.notEqual(forge.tokens.get("mesh-tools"), old);
|
||||
assert.ok(![...forge.tokens.values()].includes(old), "the old token still stands on the forge");
|
||||
assert.ok(again.some((l) => l.includes("minted with other scopes")), again.join("\n"));
|
||||
// An admin route through the escape hatch is refused before it is sent.
|
||||
const tool = getGiteaTools(minted(env, again)).find((t) => t.name === "gitea_api")!;
|
||||
await assert.rejects(tool.run({ path: "/admin/users/mesh_novox_builder", method: "PATCH", body: { admin: true } }),
|
||||
/not a write gitea_api makes/);
|
||||
});
|
||||
|
||||
test("a kept token lacking a scope the tools need: the forge refuses the call for the scope, the token is re-minted with the whole list, and the call goes through", async () => {
|
||||
const { env, file, logs } = await delivered(forge);
|
||||
const client = minted(env, logs);
|
||||
await client.listRepos();
|
||||
const before = forge.mints;
|
||||
const old = forge.tokens.get("mesh-tools")!;
|
||||
forge.scopesOf.set(old, ["write:repository", "write:issue", "read:user"]); // minted by the previous build
|
||||
forge.scopesOf.set(old, ["write:repository", "write:issue"]); // minted before read:user was added
|
||||
|
||||
const user = await client.api<{ login: string; is_admin: boolean }>("/admin/users/mesh_novox_builder", {
|
||||
method: "PATCH",
|
||||
body: JSON.stringify({ admin: true }),
|
||||
});
|
||||
const repos = await client.api<unknown[]>("/user/repos");
|
||||
|
||||
assert.equal(user.is_admin, true);
|
||||
assert.equal(repos.length, 1);
|
||||
assert.equal(forge.mints, before + 1);
|
||||
assert.deepEqual(forge.lastScopes, [...TOKEN_SCOPES]);
|
||||
assert.notEqual(forge.tokens.get("mesh-tools"), old);
|
||||
assert.equal(await readFile(file, "utf8"), forge.tokens.get("mesh-tools") + "\n");
|
||||
assert.ok(logs.some((l) => l.startsWith("the forge rejected the kept token")), logs.join("\n"));
|
||||
// A 403 that is not about scopes is the forge's answer, not a reason to mint.
|
||||
const again = forge.mints;
|
||||
await assert.rejects(
|
||||
client.api("/admin/users/untouchable", { method: "PATCH", body: JSON.stringify({ admin: true }) }),
|
||||
/403 .*untouchable/,
|
||||
);
|
||||
assert.equal(forge.mints, again);
|
||||
});
|
||||
|
||||
test("the kept file is gone but the forge still holds a token by that name: replaced, not refused", async () => {
|
||||
|
||||
Reference in New Issue
Block a user