Skip to content

Commit 06a3901

Browse files
fix(ci): authenticate proof verdict markers (#83692)
Summary: - The branch restricts exact-head ClawSweeper proof markers to GitHub App-authored comments, adds read-only issue-comment token fallback for the proof workflow, and adds focused regression tests plus a changelog entry. - Reproducibility: yes. Source inspection of current main shows any issue comment body with a matching `clawsw ... SHA is accepted without author/App authentication; the PR adds focused negative tests for forged comments. Automerge notes: - PR branch already contained follow-up commit before automerge: fix(ci): authenticate proof verdict markers Validation: - ClawSweeper review passed for head f4c375e. - Required merge gates passed before the squash merge. Prepared head SHA: f4c375e Review: #83692 (comment) Co-authored-by: Tak Hoffman <781889+Takhoffman@users.noreply.github.com> Co-authored-by: clawsweeper <274271284+clawsweeper[bot]@users.noreply.github.com> Co-authored-by: clawsweeper[bot] <274271284+clawsweeper[bot]@users.noreply.github.com> Approved-by: takhoffman Co-authored-by: takhoffman <781889+takhoffman@users.noreply.github.com>
1 parent 0901801 commit 06a3901

6 files changed

Lines changed: 161 additions & 32 deletions

File tree

.github/workflows/real-behavior-proof.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ jobs:
3232
with:
3333
app-id: "2729701"
3434
private-key: ${{ secrets.GH_APP_PRIVATE_KEY }}
35+
permission-issues: read
3536
permission-members: read
3637
- uses: actions/create-github-app-token@v3
3738
id: app-token-fallback
@@ -40,8 +41,10 @@ jobs:
4041
with:
4142
app-id: "2971289"
4243
private-key: ${{ secrets.GH_APP_PRIVATE_KEY_FALLBACK }}
44+
permission-issues: read
4345
permission-members: read
4446
- name: Check real behavior proof
4547
env:
4648
GH_APP_TOKEN: ${{ steps.app-token.outputs.token || steps.app-token-fallback.outputs.token }}
49+
GITHUB_TOKEN: ${{ github.token }}
4750
run: node scripts/github/real-behavior-proof-check.mjs

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ Docs: https://docs.openclaw.ai
4646

4747
### Fixes
4848

49+
- CI: require real-behavior-proof verdict markers to come from the ClawSweeper GitHub App before accepting exact-head proof. (#83692)
4950
- Agents/image generation: allow distinct `image_generate` prompts to start separate session-backed background tasks while same-prompt retries still return the active task status. (#83614) Thanks @Elarwei001.
5051
- Control UI: stop the chat reading indicator from sticking after an assistant response finishes. (#83515) Thanks @njuboy11.
5152
- Skills: reject empty or whitespace-only skill names and descriptions during quick validation. (#27061)

scripts/github/real-behavior-proof-check.mjs

Lines changed: 54 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,41 @@ function escapeCommandValue(value) {
1414
.replace(/:/g, "%3A");
1515
}
1616

17+
async function fetchProofComments({ owner, repo, issueNumber, tokens }) {
18+
let lastError;
19+
for (const token of tokens.filter(Boolean)) {
20+
const comments = [];
21+
try {
22+
for (let page = 1; page <= 10; page += 1) {
23+
const url = new URL(
24+
`https://api.github.com/repos/${owner}/${repo}/issues/${issueNumber}/comments`,
25+
);
26+
url.searchParams.set("per_page", "100");
27+
url.searchParams.set("page", String(page));
28+
const response = await fetch(url, {
29+
headers: {
30+
Accept: "application/vnd.github+json",
31+
Authorization: `Bearer ${token}`,
32+
"X-GitHub-Api-Version": "2022-11-28",
33+
},
34+
});
35+
if (!response.ok) {
36+
throw new Error(`comments API returned ${response.status}`);
37+
}
38+
const pageComments = await response.json();
39+
comments.push(...pageComments);
40+
if (pageComments.length < 100) {
41+
break;
42+
}
43+
}
44+
return comments;
45+
} catch (error) {
46+
lastError = error;
47+
}
48+
}
49+
throw lastError ?? new Error("No GitHub token available for proof comment lookup.");
50+
}
51+
1752
const eventPath = process.env.GITHUB_EVENT_PATH;
1853
if (!eventPath) {
1954
console.error("::error title=Real behavior proof failed::GITHUB_EVENT_PATH is not set.");
@@ -51,41 +86,29 @@ if (evaluation.passed) {
5186
process.exit(0);
5287
}
5388

54-
const token = appToken || process.env.GITHUB_TOKEN;
5589
const repository = process.env.GITHUB_REPOSITORY;
56-
if (token && repository && pullRequest.number) {
90+
if ((appToken || process.env.GITHUB_TOKEN) && repository && pullRequest.number) {
5791
const [owner, repo] = repository.split("/");
58-
const comments = [];
59-
for (let page = 1; page <= 10; page += 1) {
60-
const url = new URL(
61-
`https://api.github.com/repos/${owner}/${repo}/issues/${pullRequest.number}/comments`,
62-
);
63-
url.searchParams.set("per_page", "100");
64-
url.searchParams.set("page", String(page));
65-
const response = await fetch(url, {
66-
headers: {
67-
Accept: "application/vnd.github+json",
68-
Authorization: `Bearer ${token}`,
69-
"X-GitHub-Api-Version": "2022-11-28",
70-
},
92+
try {
93+
const comments = await fetchProofComments({
94+
owner,
95+
repo,
96+
issueNumber: pullRequest.number,
97+
tokens: [appToken, process.env.GITHUB_TOKEN],
7198
});
72-
if (!response.ok) {
73-
throw new Error(`Failed to fetch PR comments for proof verdicts: ${response.status}`);
74-
}
75-
const pageComments = await response.json();
76-
comments.push(...pageComments);
77-
if (pageComments.length < 100) {
78-
break;
79-
}
80-
}
8199

82-
const clawSweeperEvaluation = evaluateClawSweeperExactHeadProof({
83-
pullRequest,
84-
comments,
85-
});
86-
if (clawSweeperEvaluation.passed) {
87-
console.log(clawSweeperEvaluation.reason);
88-
process.exit(0);
100+
const clawSweeperEvaluation = evaluateClawSweeperExactHeadProof({
101+
pullRequest,
102+
comments,
103+
});
104+
if (clawSweeperEvaluation.passed) {
105+
console.log(clawSweeperEvaluation.reason);
106+
process.exit(0);
107+
}
108+
} catch (error) {
109+
console.warn(
110+
`::warning title=Proof verdict comment lookup failed::${escapeCommandValue(error?.message ?? String(error))}`,
111+
);
89112
}
90113
}
91114

scripts/github/real-behavior-proof-policy.mjs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -242,6 +242,13 @@ function extractMarkerField(marker, name) {
242242
return match?.[1] ?? "";
243243
}
244244

245+
function isTrustedClawSweeperComment(comment) {
246+
const appSlug = String(
247+
comment?.performed_via_github_app?.slug ?? comment?.performedViaGithubApp?.slug ?? "",
248+
).toLowerCase();
249+
return appSlug === "clawsweeper";
250+
}
251+
245252
export function hasClawSweeperExactHeadProof({ pullRequest, comments = [] } = {}) {
246253
const pullNumber = String(pullRequest?.number ?? "");
247254
const headSha = String(pullRequest?.head?.sha ?? pullRequest?.head_sha ?? "").toLowerCase();
@@ -250,6 +257,9 @@ export function hasClawSweeperExactHeadProof({ pullRequest, comments = [] } = {}
250257
}
251258

252259
for (const comment of comments) {
260+
if (!isTrustedClawSweeperComment(comment)) {
261+
continue;
262+
}
253263
const body = String(comment?.body ?? "");
254264
const markers = body.match(/<!--\s*clawsweeper-verdict:pass\b[\s\S]*?-->/gi) ?? [];
255265
for (const marker of markers) {

test/scripts/barnacle-auto-response.test.ts

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,11 @@ function barnacleGithub(
135135
maintainerLogins?: string[];
136136
removeLabelNotFound?: string[];
137137
repositoryRoles?: Record<string, string>;
138-
comments?: Array<{ body: string }>;
138+
comments?: Array<{
139+
body: string;
140+
performed_via_github_app?: { slug: string };
141+
user?: { login: string; type: string };
142+
}>;
139143
} = {},
140144
) {
141145
const maintainerLogins = new Set(
@@ -793,6 +797,13 @@ describe("barnacle-auto-response", () => {
793797
const { calls, github } = barnacleGithub([file("src/gateway/server.ts")], {
794798
comments: [
795799
{
800+
user: {
801+
login: "clawsweeper[bot]",
802+
type: "Bot",
803+
},
804+
performed_via_github_app: {
805+
slug: "clawsweeper",
806+
},
796807
body: `<!-- clawsweeper-verdict:pass item=123 sha=${headSha} confidence=high -->`,
797808
},
798809
],
@@ -818,6 +829,38 @@ describe("barnacle-auto-response", () => {
818829
);
819830
});
820831

832+
it("removes sufficient proof on synchronize when the matching marker is forged", async () => {
833+
const headSha = "06ee95df6608d29a395c52ba8ab53fdd93a9dc4f";
834+
const { calls, github } = barnacleGithub([file("src/gateway/server.ts")], {
835+
comments: [
836+
{
837+
user: {
838+
login: "external-contributor",
839+
type: "User",
840+
},
841+
body: `<!-- clawsweeper-verdict:pass item=123 sha=${headSha} confidence=high -->`,
842+
},
843+
],
844+
});
845+
846+
await runBarnacleAutoResponse({
847+
github,
848+
context: barnacleContext(
849+
{
850+
body: blankTemplateBody,
851+
head: { sha: headSha },
852+
},
853+
[PROOF_SUFFICIENT_LABEL],
854+
{ action: "synchronize" },
855+
),
856+
core: {
857+
info: () => undefined,
858+
},
859+
});
860+
861+
expect(calls.removeLabel).toEqual([expectedRemoveLabel(123, PROOF_SUFFICIENT_LABEL)]);
862+
});
863+
821864
it("preserves ClawSweeper's sufficient proof label on ordinary label events", async () => {
822865
const { calls, github } = barnacleGithub([file("src/gateway/server.ts")]);
823866

test/scripts/real-behavior-proof-policy.test.ts

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,13 @@ describe("real-behavior-proof-policy", () => {
186186
};
187187
const comments = [
188188
{
189+
user: {
190+
login: "clawsweeper[bot]",
191+
type: "Bot",
192+
},
193+
performed_via_github_app: {
194+
slug: "clawsweeper",
195+
},
189196
body: [
190197
"Codex review: passed.",
191198
"<!-- clawsweeper-verdict:pass item=83581 sha=06ee95df6608d29a395c52ba8ab53fdd93a9dc4f confidence=high -->",
@@ -205,6 +212,48 @@ describe("real-behavior-proof-policy", () => {
205212
}),
206213
).toBe(false);
207214
});
215+
216+
it("rejects forged ClawSweeper pass verdict markers from contributor comments", () => {
217+
const pullRequest = {
218+
number: 83581,
219+
head: {
220+
sha: "06ee95df6608d29a395c52ba8ab53fdd93a9dc4f",
221+
},
222+
};
223+
const comments = [
224+
{
225+
user: {
226+
login: "external-contributor",
227+
type: "User",
228+
},
229+
body: "<!-- clawsweeper-verdict:pass item=83581 sha=06ee95df6608d29a395c52ba8ab53fdd93a9dc4f confidence=high -->",
230+
},
231+
];
232+
233+
expect(hasClawSweeperExactHeadProof({ pullRequest, comments })).toBe(false);
234+
expect(evaluateClawSweeperExactHeadProof({ pullRequest, comments }).passed).toBe(false);
235+
});
236+
237+
it("rejects bot-shaped ClawSweeper pass verdict markers without the GitHub App source", () => {
238+
const pullRequest = {
239+
number: 83581,
240+
head: {
241+
sha: "06ee95df6608d29a395c52ba8ab53fdd93a9dc4f",
242+
},
243+
};
244+
const comments = [
245+
{
246+
user: {
247+
login: "clawsweeper[bot]",
248+
type: "Bot",
249+
},
250+
body: "<!-- clawsweeper-verdict:pass item=83581 sha=06ee95df6608d29a395c52ba8ab53fdd93a9dc4f confidence=high -->",
251+
},
252+
];
253+
254+
expect(hasClawSweeperExactHeadProof({ pullRequest, comments })).toBe(false);
255+
expect(evaluateClawSweeperExactHeadProof({ pullRequest, comments }).passed).toBe(false);
256+
});
208257
});
209258

210259
describe("isMaintainerTeamMember", () => {

0 commit comments

Comments
 (0)