X Tutup
Skip to content

Commit 1bbc9e7

Browse files
authored
fix: add checkHumanActor to agent mode (#826)
Fixes issue #641 where users were getting banned due to rapid successive Claude runs triggered by the synchronize event. Changes: - Add checkHumanActor call to agent mode's prepare() method to reject bot-triggered workflows unless explicitly allowed via allowed_bots - Update checkHumanActor to accept GitHubContext (union type) instead of just ParsedGitHubContext - Add tests for bot rejection/allowance in agent mode Claude-Generated-By: Claude Code (cli/claude-opus-4-5=100%) Claude-Steers: 1 Claude-Permission-Prompts: 3 Claude-Escapes: 0
1 parent 625ea15 commit 1bbc9e7

File tree

3 files changed

+74
-7
lines changed

3 files changed

+74
-7
lines changed

src/github/validation/actor.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,11 @@
66
*/
77

88
import type { Octokit } from "@octokit/rest";
9-
import type { ParsedGitHubContext } from "../context";
9+
import type { GitHubContext } from "../context";
1010

1111
export async function checkHumanActor(
1212
octokit: Octokit,
13-
githubContext: ParsedGitHubContext,
13+
githubContext: GitHubContext,
1414
) {
1515
// Fetch user information from GitHub API
1616
const { data: userData } = await octokit.users.getByUsername({

src/modes/agent/index.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
configureGitAuth,
99
setupSshSigning,
1010
} from "../../github/operations/git-config";
11+
import { checkHumanActor } from "../../github/validation/actor";
1112
import type { GitHubContext } from "../../github/context";
1213
import { isEntityContext } from "../../github/context";
1314

@@ -80,7 +81,14 @@ export const agentMode: Mode = {
8081
return false;
8182
},
8283

83-
async prepare({ context, githubToken }: ModeOptions): Promise<ModeResult> {
84+
async prepare({
85+
context,
86+
octokit,
87+
githubToken,
88+
}: ModeOptions): Promise<ModeResult> {
89+
// Check if actor is human (prevents bot-triggered loops)
90+
await checkHumanActor(octokit.rest, context);
91+
8492
// Configure git authentication for agent mode (same as tag mode)
8593
// SSH signing takes precedence if provided
8694
const useSshSigning = !!context.inputs.sshSigningKey;

test/modes/agent.test.ts

Lines changed: 63 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -145,12 +145,12 @@ describe("Agent Mode", () => {
145145
users: {
146146
getAuthenticated: mock(() =>
147147
Promise.resolve({
148-
data: { login: "test-user", id: 12345 },
148+
data: { login: "test-user", id: 12345, type: "User" },
149149
}),
150150
),
151151
getByUsername: mock(() =>
152152
Promise.resolve({
153-
data: { login: "test-user", id: 12345 },
153+
data: { login: "test-user", id: 12345, type: "User" },
154154
}),
155155
),
156156
},
@@ -187,6 +187,65 @@ describe("Agent Mode", () => {
187187
process.env.GITHUB_REF_NAME = originalRefName;
188188
});
189189

190+
test("prepare method rejects bot actors without allowed_bots", async () => {
191+
const contextWithPrompts = createMockAutomationContext({
192+
eventName: "workflow_dispatch",
193+
});
194+
contextWithPrompts.actor = "claude[bot]";
195+
contextWithPrompts.inputs.allowedBots = "";
196+
197+
const mockOctokit = {
198+
rest: {
199+
users: {
200+
getByUsername: mock(() =>
201+
Promise.resolve({
202+
data: { login: "claude[bot]", id: 12345, type: "Bot" },
203+
}),
204+
),
205+
},
206+
},
207+
} as any;
208+
209+
await expect(
210+
agentMode.prepare({
211+
context: contextWithPrompts,
212+
octokit: mockOctokit,
213+
githubToken: "test-token",
214+
}),
215+
).rejects.toThrow(
216+
"Workflow initiated by non-human actor: claude (type: Bot)",
217+
);
218+
});
219+
220+
test("prepare method allows bot actors when in allowed_bots list", async () => {
221+
const contextWithPrompts = createMockAutomationContext({
222+
eventName: "workflow_dispatch",
223+
});
224+
contextWithPrompts.actor = "dependabot[bot]";
225+
contextWithPrompts.inputs.allowedBots = "dependabot";
226+
227+
const mockOctokit = {
228+
rest: {
229+
users: {
230+
getByUsername: mock(() =>
231+
Promise.resolve({
232+
data: { login: "dependabot[bot]", id: 12345, type: "Bot" },
233+
}),
234+
),
235+
},
236+
},
237+
} as any;
238+
239+
// Should not throw - bot is in allowed list
240+
await expect(
241+
agentMode.prepare({
242+
context: contextWithPrompts,
243+
octokit: mockOctokit,
244+
githubToken: "test-token",
245+
}),
246+
).resolves.toBeDefined();
247+
});
248+
190249
test("prepare method creates prompt file with correct content", async () => {
191250
const contextWithPrompts = createMockAutomationContext({
192251
eventName: "workflow_dispatch",
@@ -199,12 +258,12 @@ describe("Agent Mode", () => {
199258
users: {
200259
getAuthenticated: mock(() =>
201260
Promise.resolve({
202-
data: { login: "test-user", id: 12345 },
261+
data: { login: "test-user", id: 12345, type: "User" },
203262
}),
204263
),
205264
getByUsername: mock(() =>
206265
Promise.resolve({
207-
data: { login: "test-user", id: 12345 },
266+
data: { login: "test-user", id: 12345, type: "User" },
208267
}),
209268
),
210269
},

0 commit comments

Comments
 (0)
X Tutup