gitea: only the forge bridge writes mesh/check, through a tool of its own (issue 1301, S4)

Once main requires mesh/check, a hand that set it could pass a merge past the
checker, the gap issue 293 closed for the two statuses before it.
gitea_commit_status now refuses mesh/check in every state, and the new
gitea_check_status writes it alone; the bus grants that tool only to a module
whose manifest invokes it, the forge bridge (the lead's choice A).
This commit is contained in:
2026-10-22 17:49:53 +02:00
parent 5e24deb0f1
commit 939475cb63
4 changed files with 100 additions and 4 deletions
+35
View File
@@ -61,3 +61,38 @@ test("one view per pull request, found by its marker; only the mesh's statuses",
assert.equal(deliveryStatus("mesh/repo-check", "pending", "checking again: main moved on").context, "mesh/repo-check");
assert.equal(deliveryStatus("mesh/delivery", "success", "x", "javascript:alert(1)").target_url, undefined);
});
// Issue 1301-S4 (ADR 0447 §1; the lead's choice A): main requires mesh/check, so no hand may set it. The status tool
// every caller may use refuses it in every state; it is written only by gitea_check_status, which the forge bridge
// alone lists in its manifest's `invokes`, so only its account may call it.
test("mesh/check is refused by the status tool and written by the check's own tool alone", async () => {
const { commitStatusTool, checkStatusTool, deliveryStatus, CHECK_STATUS } = await import("../delivery.ts");
assert.equal(CHECK_STATUS, "mesh/check");
const sha = "0123456789abcdef0123456789abcdef01234567";
const sent: string[] = [];
const setCommitStatus = async (o: string, r: string, s: string, st: any) =>
void sent.push(`${o}/${r}@${s.slice(0, 8)} ${st.context} ${st.state} ${st.description}${st.target_url ? " " + st.target_url : ""}`);
const gates = { rechecked: async () => { throw new Error("not the gate writer's"); } };
for (const state of ["pending", "success", "failure", "error", "warning"]) {
assert.throws(() => deliveryStatus("mesh/check", state, "x"), /mesh\/check is the checker's status/);
await assert.rejects(commitStatusTool({ setCommitStatus, gates } as any).run({ owner: "novox", repo: "mesh-sdk", sha,
context: "mesh/check", state, description: "by hand" }), /gitea_check_status/);
}
assert.deepEqual(sent, [], "nothing was sent for a hand-set mesh/check");
const tool = checkStatusTool({ setCommitStatus });
assert.equal(tool.name, "gitea_check_status");
for (const k of ["owner", "repo", "sha", "state", "description", "target_url", "context"]) assert.ok(k in tool.input, k);
// The bridge's arguments as it sends them for gitea_commit_status: the context may be given, and is mesh/check.
await tool.run({ owner: "novox", repo: "mesh-sdk", sha, context: "mesh/check", state: "success", description: "pass",
target_url: "https://forge.invalid/novox/mesh-sdk/pulls/1" });
await tool.run({ owner: "novox", repo: "mesh-sdk", sha, state: "pending", description: "y".repeat(300) });
assert.equal(sent[0], "novox/mesh-sdk@01234567 mesh/check success pass https://forge.invalid/novox/mesh-sdk/pulls/1");
assert.ok(sent[1].startsWith("novox/mesh-sdk@01234567 mesh/check pending yyyy"), sent[1]);
assert.ok(sent[1].length < 200, "the description is cut as every status's is");
await assert.rejects(tool.run({ owner: "novox", repo: "mesh-sdk", sha, context: "mesh/delivery", state: "success" }), /mesh\/check alone/);
await assert.rejects(tool.run({ owner: "novox", repo: "mesh-sdk", sha: sha.slice(0, 12), state: "success" }), /whole sha/);
await assert.rejects(tool.run({ owner: "novox", repo: "mesh-sdk", sha, state: "green" }), /not a status/);
await assert.rejects(tool.run({ owner: "../x", repo: "mesh-sdk", sha, state: "success" }));
assert.equal(sent.length, 2, "nothing more was sent");
});
+6 -2
View File
@@ -15,7 +15,7 @@ import { GiteaClient } from "../dist/client.js";
import { ConfiguredToken } from "../dist/token.js";
import { getGiteaTools } from "../dist/tools/index.js";
import { mergeTool } from "../dist/merge.js";
import { commitStatusTool } from "../dist/delivery.js";
import { checkStatusTool, commitStatusTool } from "../dist/delivery.js";
import { GateWriter } from "../dist/gate.js";
import { judgeApiCall, judgeProtectionChange, ruleCovers, routedPath } from "../dist/guard.js";
@@ -168,7 +168,7 @@ const gates = { rechecked: (o: string, r: string, sha: string, s: any) => {
} };
const tools = [...getGiteaTools(client), commitStatusTool({
setCommitStatus: (o, r, sha, s) => client.setCommitStatus(o, r, sha, s), gates: writer,
}), mergeTool(client, { gates, stateDir, emit: async (type, body) => { state.emitted.push({ type, body }); },
}), checkStatusTool({ setCommitStatus: (o, r, sha, s) => client.setCommitStatus(o, r, sha, s) }), mergeTool(client, { gates, stateDir, emit: async (type, body) => { state.emitted.push({ type, body }); },
now: () => new Date(2026, 9, 12, 15, 1) })];
const tool = (name: string) => tools.find((t) => t.name === name)!;
const writes = () => state.sent.map((s) => `${s.method} ${s.path}`);
@@ -568,6 +568,10 @@ test("no tool's caller text becomes another path or another user: `..` and `?sud
["gitea_get_file", { ...repo, path: "README.md?sudo=jschoubben" }],
["gitea_commit_status", { ...repo, sha: "abc1234?sudo=jschoubben", context: "mesh/delivery", state: "success" }],
["gitea_commit_status", { ...repo, sha: "../../../admin/users", context: "mesh/delivery", state: "success" }],
["gitea_check_status", { ...repo, sha: "abc1234?sudo=jschoubben", state: "success" }],
["gitea_check_status", { ...repo, sha: "../../../admin/users", state: "success" }],
["gitea_check_status", { owner: "novox", repo: "..", sha: "0123456789abcdef0123456789abcdef01234567", state: "success" }],
["gitea_commit_status", { ...repo, sha: "0123456789abcdef0123456789abcdef01234567", context: "mesh/check", state: "success" }],
["gitea_commit_statuses", { ...repo, sha: ".." }],
["gitea_commit_statuses", { ...repo, sha: "main?sudo=jschoubben" }],
["gitea_branch_protection_set", { ...repo, branch: "..", status_checks: "mesh/merge-gate" }],