From 3fef2d3f202e8e347e900cc04366e3a4c54a278d Mon Sep 17 00:00:00 2001 From: km-anthropic Date: Thu, 24 Jul 2025 00:01:40 -0700 Subject: [PATCH] feat: add agent mode for automation scenarios MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add agent mode that always triggers without checking for mentions - Implement Mode interface with support for mode-specific tool configuration - Add getAllowedTools() and getDisallowedTools() methods to Mode interface - Simplify tests by combining related test cases - Update documentation and examples to include agent mode - Fix TypeScript imports to prevent circular dependencies Agent mode is designed for automation and workflow_dispatch scenarios where Claude should always run without requiring trigger phrases. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- README.md | 98 +++++++++++++++++++++--------- action.yml | 2 +- examples/claude-modes.yml | 117 ++++++++++++++++++++++++++++++++++++ src/create-prompt/index.ts | 21 ++++++- src/entrypoints/prepare.ts | 2 +- src/github/context.ts | 5 +- src/modes/agent/index.ts | 42 +++++++++++++ src/modes/registry.ts | 11 ++-- src/modes/types.ts | 14 ++--- test/modes/agent.test.ts | 82 +++++++++++++++++++++++++ test/modes/registry.test.ts | 16 +++-- 11 files changed, 356 insertions(+), 54 deletions(-) create mode 100644 examples/claude-modes.yml create mode 100644 src/modes/agent/index.ts create mode 100644 test/modes/agent.test.ts diff --git a/README.md b/README.md index 646387f..a269af5 100644 --- a/README.md +++ b/README.md @@ -167,41 +167,79 @@ jobs: ## Inputs -| Input | Description | Required | Default | -| ------------------------------ | -------------------------------------------------------------------------------------------------------------------- | -------- | --------- | -| `mode` | Execution mode for the action. Currently supports 'tag' (default). Future modes: 'review', 'freeform' | No | `tag` | -| `anthropic_api_key` | Anthropic API key (required for direct API, not needed for Bedrock/Vertex) | No\* | - | -| `claude_code_oauth_token` | Claude Code OAuth token (alternative to anthropic_api_key) | No\* | - | -| `direct_prompt` | Direct prompt for Claude to execute automatically without needing a trigger (for automated workflows) | No | - | -| `override_prompt` | Complete replacement of Claude's prompt with custom template (supports variable substitution) | No | - | -| `base_branch` | The base branch to use for creating new branches (e.g., 'main', 'develop') | No | - | -| `max_turns` | Maximum number of conversation turns Claude can take (limits back-and-forth exchanges) | No | - | -| `timeout_minutes` | Timeout in minutes for execution | No | `30` | -| `use_sticky_comment` | Use just one comment to deliver PR comments (only applies for pull_request event workflows) | No | `false` | -| `github_token` | GitHub token for Claude to operate with. **Only include this if you're connecting a custom GitHub app of your own!** | No | - | -| `model` | Model to use (provider-specific format required for Bedrock/Vertex) | No | - | -| `fallback_model` | Enable automatic fallback to specified model when primary model is unavailable | No | - | -| `anthropic_model` | **DEPRECATED**: Use `model` instead. Kept for backward compatibility. | No | - | -| `use_bedrock` | Use Amazon Bedrock with OIDC authentication instead of direct Anthropic API | No | `false` | -| `use_vertex` | Use Google Vertex AI with OIDC authentication instead of direct Anthropic API | No | `false` | -| `allowed_tools` | Additional tools for Claude to use (the base GitHub tools will always be included) | No | "" | -| `disallowed_tools` | Tools that Claude should never use | No | "" | -| `custom_instructions` | Additional custom instructions to include in the prompt for Claude | No | "" | -| `mcp_config` | Additional MCP configuration (JSON string) that merges with the built-in GitHub MCP servers | No | "" | -| `assignee_trigger` | The assignee username that triggers the action (e.g. @claude). Only used for issue assignment | No | - | -| `label_trigger` | The label name that triggers the action when applied to an issue (e.g. "claude") | No | - | -| `trigger_phrase` | The trigger phrase to look for in comments, issue/PR bodies, and issue titles | No | `@claude` | -| `branch_prefix` | The prefix to use for Claude branches (defaults to 'claude/', use 'claude-' for dash format) | No | `claude/` | -| `claude_env` | Custom environment variables to pass to Claude Code execution (YAML format) | No | "" | -| `settings` | Claude Code settings as JSON string or path to settings JSON file | No | "" | -| `additional_permissions` | Additional permissions to enable. Currently supports 'actions: read' for viewing workflow results | No | "" | -| `experimental_allowed_domains` | Restrict network access to these domains only (newline-separated). | No | "" | -| `use_commit_signing` | Enable commit signing using GitHub's commit signature verification. When false, Claude uses standard git commands | No | `false` | +| Input | Description | Required | Default | +| ------------------------------ | ---------------------------------------------------------------------------------------------------------------------- | -------- | --------- | +| `mode` | Execution mode: 'tag' (default - triggered by mentions/assignments), 'agent' (for automation with no trigger checking) | No | `tag` | +| `anthropic_api_key` | Anthropic API key (required for direct API, not needed for Bedrock/Vertex) | No\* | - | +| `claude_code_oauth_token` | Claude Code OAuth token (alternative to anthropic_api_key) | No\* | - | +| `direct_prompt` | Direct prompt for Claude to execute automatically without needing a trigger (for automated workflows) | No | - | +| `override_prompt` | Complete replacement of Claude's prompt with custom template (supports variable substitution) | No | - | +| `base_branch` | The base branch to use for creating new branches (e.g., 'main', 'develop') | No | - | +| `max_turns` | Maximum number of conversation turns Claude can take (limits back-and-forth exchanges) | No | - | +| `timeout_minutes` | Timeout in minutes for execution | No | `30` | +| `use_sticky_comment` | Use just one comment to deliver PR comments (only applies for pull_request event workflows) | No | `false` | +| `github_token` | GitHub token for Claude to operate with. **Only include this if you're connecting a custom GitHub app of your own!** | No | - | +| `model` | Model to use (provider-specific format required for Bedrock/Vertex) | No | - | +| `fallback_model` | Enable automatic fallback to specified model when primary model is unavailable | No | - | +| `anthropic_model` | **DEPRECATED**: Use `model` instead. Kept for backward compatibility. | No | - | +| `use_bedrock` | Use Amazon Bedrock with OIDC authentication instead of direct Anthropic API | No | `false` | +| `use_vertex` | Use Google Vertex AI with OIDC authentication instead of direct Anthropic API | No | `false` | +| `allowed_tools` | Additional tools for Claude to use (the base GitHub tools will always be included) | No | "" | +| `disallowed_tools` | Tools that Claude should never use | No | "" | +| `custom_instructions` | Additional custom instructions to include in the prompt for Claude | No | "" | +| `mcp_config` | Additional MCP configuration (JSON string) that merges with the built-in GitHub MCP servers | No | "" | +| `assignee_trigger` | The assignee username that triggers the action (e.g. @claude). Only used for issue assignment | No | - | +| `label_trigger` | The label name that triggers the action when applied to an issue (e.g. "claude") | No | - | +| `trigger_phrase` | The trigger phrase to look for in comments, issue/PR bodies, and issue titles | No | `@claude` | +| `branch_prefix` | The prefix to use for Claude branches (defaults to 'claude/', use 'claude-' for dash format) | No | `claude/` | +| `claude_env` | Custom environment variables to pass to Claude Code execution (YAML format) | No | "" | +| `settings` | Claude Code settings as JSON string or path to settings JSON file | No | "" | +| `additional_permissions` | Additional permissions to enable. Currently supports 'actions: read' for viewing workflow results | No | "" | +| `experimental_allowed_domains` | Restrict network access to these domains only (newline-separated). | No | "" | +| `use_commit_signing` | Enable commit signing using GitHub's commit signature verification. When false, Claude uses standard git commands | No | `false` | \*Required when using direct Anthropic API (default and when not using Bedrock or Vertex) > **Note**: This action is currently in beta. Features and APIs may change as we continue to improve the integration. +## Execution Modes + +The action supports two execution modes, each optimized for different use cases: + +### Tag Mode (Default) + +The traditional implementation mode that responds to @claude mentions, issue assignments, or labels. + +- **Triggers**: `@claude` mentions, issue assignment, label application +- **Features**: Creates tracking comments with progress checkboxes, full implementation capabilities +- **Use case**: General-purpose code implementation and Q&A + +```yaml +- uses: anthropics/claude-code-action@main + with: + anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }} + # mode: tag is the default +``` + +### Agent Mode + +For automation and scheduled tasks without trigger checking. + +- **Triggers**: Always runs (no trigger checking) +- **Features**: Perfect for scheduled tasks, works with `override_prompt` +- **Use case**: Maintenance tasks, automated reporting, scheduled checks + +```yaml +- uses: anthropics/claude-code-action@main + with: + mode: agent + anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }} + override_prompt: | + Check for outdated dependencies and create an issue if any are found. +``` + +See [`examples/claude-modes.yml`](./examples/claude-modes.yml) for complete examples of each mode. + ### Using Custom MCP Configuration The `mcp_config` input allows you to add custom MCP (Model Context Protocol) servers to extend Claude's capabilities. These servers merge with the built-in GitHub MCP servers. diff --git a/action.yml b/action.yml index 0704ba5..fb54de0 100644 --- a/action.yml +++ b/action.yml @@ -26,7 +26,7 @@ inputs: # Mode configuration mode: - description: "Execution mode for the action. Currently only 'tag' mode is supported (traditional implementation triggered by mentions/assignments)" + description: "Execution mode for the action. Valid modes: 'tag' (default - triggered by mentions/assignments), 'agent' (for automation with no trigger checking)" required: false default: "tag" diff --git a/examples/claude-modes.yml b/examples/claude-modes.yml new file mode 100644 index 0000000..16812eb --- /dev/null +++ b/examples/claude-modes.yml @@ -0,0 +1,117 @@ +name: Claude Code Action Mode Examples + +on: + # Example 1: Tag mode (default) - traditional implementation triggered by mentions + issue_comment: + types: [created] + issues: + types: [opened, assigned, labeled] + pull_request: + types: [opened, synchronize] + pull_request_review_comment: + types: [created] + pull_request_review: + types: [submitted] + + # Example 2: Agent mode - for scheduled tasks and automation + schedule: + - cron: "0 0 * * 1" # Weekly on Monday + workflow_dispatch: + inputs: + task: + description: "Task for Claude to perform" + required: true + type: string + +jobs: + # Example 1: Tag mode (default) - triggered by @claude mentions + claude-tag-mode: + if: github.event_name != 'schedule' && github.event_name != 'workflow_dispatch' + runs-on: ubuntu-latest + permissions: + contents: write + pull-requests: write + issues: write + steps: + - uses: actions/checkout@v4 + - uses: anthropics/claude-code-action@main + with: + # mode defaults to 'tag' - no need to specify + anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }} + # Responds to @claude mentions, assignments, and labels + # Creates tracking comments showing progress + # Full implementation capabilities + + # Example 2: Agent mode - for automation + claude-agent-scheduled: + if: github.event_name == 'schedule' + runs-on: ubuntu-latest + permissions: + contents: write + pull-requests: write + issues: write + steps: + - uses: actions/checkout@v4 + - uses: anthropics/claude-code-action@main + with: + mode: agent + anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }} + override_prompt: | + Check the repository for: + 1. Outdated dependencies in package.json + 2. TODO comments that haven't been addressed + 3. Files that haven't been updated in over 6 months + + Create an issue summarizing your findings. + # Always runs - no trigger checking + # Perfect for scheduled tasks + + # Example 3: Agent mode with workflow_dispatch + claude-agent-dispatch: + if: github.event_name == 'workflow_dispatch' + runs-on: ubuntu-latest + permissions: + contents: write + pull-requests: write + issues: write + steps: + - uses: actions/checkout@v4 + - uses: anthropics/claude-code-action@main + with: + mode: agent + anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }} + override_prompt: | + Task: ${{ github.event.inputs.task }} + + Repository: $REPOSITORY + Triggered by: $TRIGGER_USERNAME + + Please complete the requested task and provide a summary of what was done. + + # Example 4: Custom security review with override_prompt + claude-security-review: + if: github.event.pull_request.draft == false && contains(github.event.pull_request.labels.*.name, 'security-review') + runs-on: ubuntu-latest + permissions: + contents: read + pull-requests: write + steps: + - uses: actions/checkout@v4 + - uses: anthropics/claude-code-action@main + with: + anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }} + override_prompt: | + Perform a security review of PR #$PR_NUMBER. + + Focus on: + - SQL injection vulnerabilities + - XSS (Cross-Site Scripting) risks + - Authentication/authorization issues + - Sensitive data exposure + - Input validation problems + + Changed files: + $CHANGED_FILES + + Provide severity ratings (Critical/High/Medium/Low) for any issues found. + Be specific about file paths and line numbers. diff --git a/src/create-prompt/index.ts b/src/create-prompt/index.ts index f9ff35d..27b3281 100644 --- a/src/create-prompt/index.ts +++ b/src/create-prompt/index.ts @@ -840,14 +840,29 @@ export async function createPrompt( const hasActionsReadPermission = context.inputs.additionalPermissions.get("actions") === "read" && context.isPR; + + // Get mode-specific tools + const modeAllowedTools = mode.getAllowedTools(); + const modeDisallowedTools = mode.getDisallowedTools(); + + // Combine with existing allowed tools + const combinedAllowedTools = [ + ...context.inputs.allowedTools, + ...modeAllowedTools, + ]; + const combinedDisallowedTools = [ + ...context.inputs.disallowedTools, + ...modeDisallowedTools, + ]; + const allAllowedTools = buildAllowedToolsString( - context.inputs.allowedTools, + combinedAllowedTools, hasActionsReadPermission, context.inputs.useCommitSigning, ); const allDisallowedTools = buildDisallowedToolsString( - context.inputs.disallowedTools, - context.inputs.allowedTools, + combinedDisallowedTools, + combinedAllowedTools, ); core.exportVariable("ALLOWED_TOOLS", allAllowedTools); diff --git a/src/entrypoints/prepare.ts b/src/entrypoints/prepare.ts index 3e5a956..6653c06 100644 --- a/src/entrypoints/prepare.ts +++ b/src/entrypoints/prepare.ts @@ -55,7 +55,7 @@ async function run() { await checkHumanActor(octokit.rest, context); // Step 6: Create initial tracking comment (mode-aware) - // Some modes (e.g., future review/freeform modes) may not need tracking comments + // Some modes (e.g., agent mode) may not need tracking comments let commentId: number | undefined; let commentData: | Awaited> diff --git a/src/github/context.ts b/src/github/context.ts index 961ac7e..4e0d866 100644 --- a/src/github/context.ts +++ b/src/github/context.ts @@ -7,9 +7,8 @@ import type { PullRequestReviewEvent, PullRequestReviewCommentEvent, } from "@octokit/webhooks-types"; -import type { ModeName } from "../modes/registry"; -import { DEFAULT_MODE } from "../modes/registry"; -import { isValidMode } from "../modes/registry"; +import type { ModeName } from "../modes/types"; +import { DEFAULT_MODE, isValidMode } from "../modes/registry"; export type ParsedGitHubContext = { runId: string; diff --git a/src/modes/agent/index.ts b/src/modes/agent/index.ts new file mode 100644 index 0000000..ed4c414 --- /dev/null +++ b/src/modes/agent/index.ts @@ -0,0 +1,42 @@ +import type { Mode } from "../types"; + +/** + * Agent mode implementation. + * + * This mode is designed for automation and workflow_dispatch scenarios. + * It always triggers (no checking), allows highly flexible configurations, + * and works well with override_prompt for custom workflows. + * + * In the future, this mode could restrict certain tools for safety in automation contexts, + * e.g., disallowing WebSearch or limiting file system operations. + */ +export const agentMode: Mode = { + name: "agent", + description: "Automation mode that always runs without trigger checking", + + shouldTrigger() { + return true; + }, + + prepareContext(context, data) { + return { + mode: "agent", + githubContext: context, + commentId: data?.commentId, + baseBranch: data?.baseBranch, + claudeBranch: data?.claudeBranch, + }; + }, + + getAllowedTools() { + return []; + }, + + getDisallowedTools() { + return []; + }, + + shouldCreateTrackingComment() { + return true; + }, +}; diff --git a/src/modes/registry.ts b/src/modes/registry.ts index 37aadd4..043137a 100644 --- a/src/modes/registry.ts +++ b/src/modes/registry.ts @@ -5,17 +5,17 @@ * * To add a new mode: * 1. Add the mode name to VALID_MODES below - * 2. Create the mode implementation in a new directory (e.g., src/modes/review/) + * 2. Create the mode implementation in a new directory (e.g., src/modes/new-mode/) * 3. Import and add it to the modes object below * 4. Update action.yml description to mention the new mode */ -import type { Mode } from "./types"; -import { tagMode } from "./tag/index"; +import type { Mode, ModeName } from "./types"; +import { tagMode } from "./tag"; +import { agentMode } from "./agent"; export const DEFAULT_MODE = "tag" as const; -export const VALID_MODES = ["tag"] as const; -export type ModeName = (typeof VALID_MODES)[number]; +export const VALID_MODES = ["tag", "agent"] as const; /** * All available modes. @@ -23,6 +23,7 @@ export type ModeName = (typeof VALID_MODES)[number]; */ const modes = { tag: tagMode, + agent: agentMode, } as const satisfies Record; /** diff --git a/src/modes/types.ts b/src/modes/types.ts index 2cb2a75..cd3d1b7 100644 --- a/src/modes/types.ts +++ b/src/modes/types.ts @@ -1,5 +1,6 @@ import type { ParsedGitHubContext } from "../github/context"; -import type { ModeName } from "./registry"; + +export type ModeName = "tag" | "agent"; export type ModeContext = { mode: ModeName; @@ -20,9 +21,9 @@ export type ModeData = { * Each mode defines its own behavior for trigger detection, prompt generation, * and tracking comment creation. * - * Future modes might include: - * - 'review': Optimized for code reviews without tracking comments - * - 'freeform': For automation with no trigger checking + * Current modes include: + * - 'tag': Traditional implementation triggered by mentions/assignments + * - 'agent': For automation with no trigger checking */ export type Mode = { name: ModeName; @@ -39,13 +40,12 @@ export type Mode = { prepareContext(context: ParsedGitHubContext, data?: ModeData): ModeContext; /** - * Returns additional tools that should be allowed for this mode - * (base GitHub tools are always included) + * Returns the list of tools that should be allowed for this mode */ getAllowedTools(): string[]; /** - * Returns tools that should be disallowed for this mode + * Returns the list of tools that should be disallowed for this mode */ getDisallowedTools(): string[]; diff --git a/test/modes/agent.test.ts b/test/modes/agent.test.ts new file mode 100644 index 0000000..f5fc1c7 --- /dev/null +++ b/test/modes/agent.test.ts @@ -0,0 +1,82 @@ +import { describe, test, expect, beforeEach } from "bun:test"; +import { agentMode } from "../../src/modes/agent"; +import type { ParsedGitHubContext } from "../../src/github/context"; +import { createMockContext } from "../mockContext"; + +describe("Agent Mode", () => { + let mockContext: ParsedGitHubContext; + + beforeEach(() => { + mockContext = createMockContext({ + eventName: "workflow_dispatch", + isPR: false, + }); + }); + + test("agent mode has correct properties and behavior", () => { + // Basic properties + expect(agentMode.name).toBe("agent"); + expect(agentMode.description).toBe( + "Automation mode that always runs without trigger checking", + ); + expect(agentMode.shouldCreateTrackingComment()).toBe(true); + + // Tool methods return empty arrays + expect(agentMode.getAllowedTools()).toEqual([]); + expect(agentMode.getDisallowedTools()).toEqual([]); + + // Always triggers regardless of context + const contextWithoutTrigger = createMockContext({ + eventName: "workflow_dispatch", + isPR: false, + inputs: { + ...createMockContext().inputs, + triggerPhrase: "@claude", + }, + payload: {} as any, + }); + expect(agentMode.shouldTrigger(contextWithoutTrigger)).toBe(true); + }); + + test("prepareContext includes all required data", () => { + const data = { + commentId: 789, + baseBranch: "develop", + claudeBranch: "claude/automated-task", + }; + + const context = agentMode.prepareContext(mockContext, data); + + expect(context.mode).toBe("agent"); + expect(context.githubContext).toBe(mockContext); + expect(context.commentId).toBe(789); + expect(context.baseBranch).toBe("develop"); + expect(context.claudeBranch).toBe("claude/automated-task"); + }); + + test("prepareContext works without data", () => { + const context = agentMode.prepareContext(mockContext); + + expect(context.mode).toBe("agent"); + expect(context.githubContext).toBe(mockContext); + expect(context.commentId).toBeUndefined(); + expect(context.baseBranch).toBeUndefined(); + expect(context.claudeBranch).toBeUndefined(); + }); + + test("agent mode triggers for all event types", () => { + const events = [ + "push", + "schedule", + "workflow_dispatch", + "repository_dispatch", + "issue_comment", + "pull_request", + ]; + + events.forEach((eventName) => { + const context = createMockContext({ eventName, isPR: false }); + expect(agentMode.shouldTrigger(context)).toBe(true); + }); + }); +}); diff --git a/test/modes/registry.test.ts b/test/modes/registry.test.ts index 699c3f3..2e7b011 100644 --- a/test/modes/registry.test.ts +++ b/test/modes/registry.test.ts @@ -1,6 +1,8 @@ import { describe, test, expect } from "bun:test"; -import { getMode, isValidMode, type ModeName } from "../../src/modes/registry"; +import { getMode, isValidMode } from "../../src/modes/registry"; +import type { ModeName } from "../../src/modes/types"; import { tagMode } from "../../src/modes/tag"; +import { agentMode } from "../../src/modes/agent"; describe("Mode Registry", () => { test("getMode returns tag mode by default", () => { @@ -9,20 +11,26 @@ describe("Mode Registry", () => { expect(mode.name).toBe("tag"); }); + test("getMode returns agent mode", () => { + const mode = getMode("agent"); + expect(mode).toBe(agentMode); + expect(mode.name).toBe("agent"); + }); + test("getMode throws error for invalid mode", () => { const invalidMode = "invalid" as unknown as ModeName; expect(() => getMode(invalidMode)).toThrow( - "Invalid mode 'invalid'. Valid modes are: 'tag'. Please check your workflow configuration.", + "Invalid mode 'invalid'. Valid modes are: 'tag', 'agent'. Please check your workflow configuration.", ); }); - test("isValidMode returns true for tag mode", () => { + test("isValidMode returns true for all valid modes", () => { expect(isValidMode("tag")).toBe(true); + expect(isValidMode("agent")).toBe(true); }); test("isValidMode returns false for invalid mode", () => { expect(isValidMode("invalid")).toBe(false); expect(isValidMode("review")).toBe(false); - expect(isValidMode("freeform")).toBe(false); }); });