mirror of
https://github.com/anthropics/claude-code-action.git
synced 2026-01-22 22:44:13 +08:00
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
This commit is contained in:
@@ -6,11 +6,11 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import type { Octokit } from "@octokit/rest";
|
import type { Octokit } from "@octokit/rest";
|
||||||
import type { ParsedGitHubContext } from "../context";
|
import type { GitHubContext } from "../context";
|
||||||
|
|
||||||
export async function checkHumanActor(
|
export async function checkHumanActor(
|
||||||
octokit: Octokit,
|
octokit: Octokit,
|
||||||
githubContext: ParsedGitHubContext,
|
githubContext: GitHubContext,
|
||||||
) {
|
) {
|
||||||
// Fetch user information from GitHub API
|
// Fetch user information from GitHub API
|
||||||
const { data: userData } = await octokit.users.getByUsername({
|
const { data: userData } = await octokit.users.getByUsername({
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import {
|
|||||||
configureGitAuth,
|
configureGitAuth,
|
||||||
setupSshSigning,
|
setupSshSigning,
|
||||||
} from "../../github/operations/git-config";
|
} from "../../github/operations/git-config";
|
||||||
|
import { checkHumanActor } from "../../github/validation/actor";
|
||||||
import type { GitHubContext } from "../../github/context";
|
import type { GitHubContext } from "../../github/context";
|
||||||
import { isEntityContext } from "../../github/context";
|
import { isEntityContext } from "../../github/context";
|
||||||
|
|
||||||
@@ -80,7 +81,14 @@ export const agentMode: Mode = {
|
|||||||
return false;
|
return false;
|
||||||
},
|
},
|
||||||
|
|
||||||
async prepare({ context, githubToken }: ModeOptions): Promise<ModeResult> {
|
async prepare({
|
||||||
|
context,
|
||||||
|
octokit,
|
||||||
|
githubToken,
|
||||||
|
}: ModeOptions): Promise<ModeResult> {
|
||||||
|
// Check if actor is human (prevents bot-triggered loops)
|
||||||
|
await checkHumanActor(octokit.rest, context);
|
||||||
|
|
||||||
// Configure git authentication for agent mode (same as tag mode)
|
// Configure git authentication for agent mode (same as tag mode)
|
||||||
// SSH signing takes precedence if provided
|
// SSH signing takes precedence if provided
|
||||||
const useSshSigning = !!context.inputs.sshSigningKey;
|
const useSshSigning = !!context.inputs.sshSigningKey;
|
||||||
|
|||||||
@@ -145,12 +145,12 @@ describe("Agent Mode", () => {
|
|||||||
users: {
|
users: {
|
||||||
getAuthenticated: mock(() =>
|
getAuthenticated: mock(() =>
|
||||||
Promise.resolve({
|
Promise.resolve({
|
||||||
data: { login: "test-user", id: 12345 },
|
data: { login: "test-user", id: 12345, type: "User" },
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
getByUsername: mock(() =>
|
getByUsername: mock(() =>
|
||||||
Promise.resolve({
|
Promise.resolve({
|
||||||
data: { login: "test-user", id: 12345 },
|
data: { login: "test-user", id: 12345, type: "User" },
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
@@ -187,6 +187,65 @@ describe("Agent Mode", () => {
|
|||||||
process.env.GITHUB_REF_NAME = originalRefName;
|
process.env.GITHUB_REF_NAME = originalRefName;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("prepare method rejects bot actors without allowed_bots", async () => {
|
||||||
|
const contextWithPrompts = createMockAutomationContext({
|
||||||
|
eventName: "workflow_dispatch",
|
||||||
|
});
|
||||||
|
contextWithPrompts.actor = "claude[bot]";
|
||||||
|
contextWithPrompts.inputs.allowedBots = "";
|
||||||
|
|
||||||
|
const mockOctokit = {
|
||||||
|
rest: {
|
||||||
|
users: {
|
||||||
|
getByUsername: mock(() =>
|
||||||
|
Promise.resolve({
|
||||||
|
data: { login: "claude[bot]", id: 12345, type: "Bot" },
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} as any;
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
agentMode.prepare({
|
||||||
|
context: contextWithPrompts,
|
||||||
|
octokit: mockOctokit,
|
||||||
|
githubToken: "test-token",
|
||||||
|
}),
|
||||||
|
).rejects.toThrow(
|
||||||
|
"Workflow initiated by non-human actor: claude (type: Bot)",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("prepare method allows bot actors when in allowed_bots list", async () => {
|
||||||
|
const contextWithPrompts = createMockAutomationContext({
|
||||||
|
eventName: "workflow_dispatch",
|
||||||
|
});
|
||||||
|
contextWithPrompts.actor = "dependabot[bot]";
|
||||||
|
contextWithPrompts.inputs.allowedBots = "dependabot";
|
||||||
|
|
||||||
|
const mockOctokit = {
|
||||||
|
rest: {
|
||||||
|
users: {
|
||||||
|
getByUsername: mock(() =>
|
||||||
|
Promise.resolve({
|
||||||
|
data: { login: "dependabot[bot]", id: 12345, type: "Bot" },
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} as any;
|
||||||
|
|
||||||
|
// Should not throw - bot is in allowed list
|
||||||
|
await expect(
|
||||||
|
agentMode.prepare({
|
||||||
|
context: contextWithPrompts,
|
||||||
|
octokit: mockOctokit,
|
||||||
|
githubToken: "test-token",
|
||||||
|
}),
|
||||||
|
).resolves.toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
test("prepare method creates prompt file with correct content", async () => {
|
test("prepare method creates prompt file with correct content", async () => {
|
||||||
const contextWithPrompts = createMockAutomationContext({
|
const contextWithPrompts = createMockAutomationContext({
|
||||||
eventName: "workflow_dispatch",
|
eventName: "workflow_dispatch",
|
||||||
@@ -199,12 +258,12 @@ describe("Agent Mode", () => {
|
|||||||
users: {
|
users: {
|
||||||
getAuthenticated: mock(() =>
|
getAuthenticated: mock(() =>
|
||||||
Promise.resolve({
|
Promise.resolve({
|
||||||
data: { login: "test-user", id: 12345 },
|
data: { login: "test-user", id: 12345, type: "User" },
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
getByUsername: mock(() =>
|
getByUsername: mock(() =>
|
||||||
Promise.resolve({
|
Promise.resolve({
|
||||||
data: { login: "test-user", id: 12345 },
|
data: { login: "test-user", id: 12345, type: "User" },
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user