Review human AFF3 #11873
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Claude Code | |
| on: | |
| issue_comment: | |
| types: [created] | |
| pull_request_review_comment: | |
| types: [created] | |
| issues: | |
| types: [opened, assigned] | |
| pull_request_review: | |
| types: [submitted] | |
| # `edited` is not a trigger here, but a comment can still arrive while a run is | |
| # in flight; keep one agent per issue/PR. | |
| concurrency: | |
| group: claude-${{ github.event.issue.number || github.event.pull_request.number }} | |
| cancel-in-progress: false | |
| jobs: | |
| claude: | |
| # Only a repo OWNER/MEMBER/COLLABORATOR can dispatch the agent. Without this, | |
| # any GitHub user could drive a write-capable agent by writing "@claude" in a | |
| # comment on a public repo. | |
| # | |
| # For `issues: assigned`, this deliberately gates on the *issue author's* | |
| # association rather than the assigner's: assigning an externally-authored | |
| # issue should not turn untrusted issue text into an agent trigger. | |
| if: | | |
| ( | |
| github.event_name == 'issue_comment' && | |
| contains(github.event.comment.body, '@claude') && | |
| contains(fromJSON('["OWNER","MEMBER","COLLABORATOR"]'), github.event.comment.author_association) | |
| ) || | |
| ( | |
| github.event_name == 'pull_request_review_comment' && | |
| contains(github.event.comment.body, '@claude') && | |
| contains(fromJSON('["OWNER","MEMBER","COLLABORATOR"]'), github.event.comment.author_association) | |
| ) || | |
| ( | |
| github.event_name == 'pull_request_review' && | |
| contains(github.event.review.body, '@claude') && | |
| contains(fromJSON('["OWNER","MEMBER","COLLABORATOR"]'), github.event.review.author_association) | |
| ) || | |
| ( | |
| github.event_name == 'issues' && | |
| (contains(github.event.issue.body, '@claude') || contains(github.event.issue.title, '@claude')) && | |
| contains(fromJSON('["OWNER","MEMBER","COLLABORATOR"]'), github.event.issue.author_association) | |
| ) | |
| runs-on: ubuntu-latest | |
| # A GitHub App installation token expires 60 minutes after minting and does | |
| # not refresh; stay inside that window. | |
| timeout-minutes: 55 | |
| permissions: | |
| # Scopes the default GITHUB_TOKEN only; the agent works with the ai4c-agent | |
| # App token below. | |
| contents: read | |
| actions: read # Required for Claude to read CI results on PRs | |
| steps: | |
| - name: Generate ai4c-agent token | |
| id: ai4c-token | |
| uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0 | |
| with: | |
| app-id: ${{ secrets.AI4C_AGENT_APP_ID }} | |
| private-key: ${{ secrets.AI4C_AGENT_PRIVATE_KEY }} | |
| - name: Checkout repository | |
| uses: actions/checkout@v7 | |
| with: | |
| fetch-depth: 1 | |
| token: ${{ steps.ai4c-token.outputs.token }} | |
| # This agent has Bash(*); do not leave a token in .git/config for it | |
| # to read. git authenticates via the gh credential helper below. | |
| persist-credentials: false | |
| - name: Configure git identity and credential helper | |
| env: | |
| GH_TOKEN: ${{ steps.ai4c-token.outputs.token }} | |
| APP_SLUG: ${{ steps.ai4c-token.outputs.app-slug }} | |
| run: | | |
| APP_USER_ID="$(gh api "/users/${APP_SLUG}[bot]" --jq .id)" | |
| git config --global user.name "${APP_SLUG}[bot]" | |
| git config --global user.email "${APP_USER_ID}+${APP_SLUG}[bot]@users.noreply.github.com" | |
| gh auth setup-git | |
| - name: Resolve agent config | |
| uses: ./.github/actions/resolve-agent-config | |
| with: | |
| workflow: claude | |
| - name: Install uv | |
| uses: astral-sh/setup-uv@v7 | |
| - name: Install python tools | |
| run: | | |
| uv sync | |
| - name: Run Claude Code | |
| id: claude | |
| uses: anthropics/claude-code-action@be7b93b1907a4abad570368f3c74b6fe3807510b # v1 | |
| env: | |
| GH_TOKEN: ${{ steps.ai4c-token.outputs.token }} | |
| GITHUB_TOKEN: ${{ steps.ai4c-token.outputs.token }} | |
| with: | |
| claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} | |
| github_token: ${{ steps.ai4c-token.outputs.token }} | |
| # Lets Claude read CI results on PRs. | |
| additional_permissions: | | |
| actions: read | |
| # No `prompt:` on purpose. This is the interactive "@claude" responder, | |
| # and v1 switches to non-interactive automation mode as soon as a prompt | |
| # is supplied. Repository conventions come from CLAUDE.md, which the | |
| # agent reads; they used to be duplicated here in a `custom_instructions:` | |
| # block that v1 does not accept and silently discarded. | |
| # | |
| # `mcp_config:`, `model:` and `allowed_tools:` were dropped the same way | |
| # — none of them are v1 inputs. Everything now goes through claude_args, | |
| # which is how v1 takes CLI flags. Both flags ACCUMULATE with tag mode's | |
| # own values rather than replacing them, so the comment-update and CI | |
| # MCP tools survive. | |
| # | |
| # The old file asked for `Bash(*),Edit,MultiEdit,Write`, but since the | |
| # input was being discarded it never took effect — the agent has been | |
| # running on tag mode's narrow set. Restoring that literally would grant | |
| # blanket write for the first time, in the same commit that calls this | |
| # workflow out as the incident shape, so it is scoped to what the | |
| # gene-review protocol needs instead. Edit/MultiEdit/Write are | |
| # deliberately absent: tag mode runs `--permission-mode acceptEdits`, | |
| # which already permits edits inside the workspace, and listing the | |
| # tools would extend that to the whole runner. | |
| # | |
| # BE CLEAR ABOUT WHAT THIS LIST IS AND IS NOT. It is not a confinement | |
| # boundary: `Bash(uv:*)` covers `uv run python -c '...'`, `Bash(uvx:*)` | |
| # runs an arbitrary PyPI package, and `gh alias set --shell` is a shell | |
| # escape. Arbitrary execution is ACCEPTED for this agent, because only | |
| # OWNER/MEMBER/COLLABORATOR can trigger it (see the job `if:`) and it | |
| # holds a token that expires with the job. What the list actually buys | |
| # is workspace-scoped writes and the removal of `Bash(*)`. | |
| # | |
| # Git is the one exception worth spelling out, because it has a | |
| # documented escape rather than an implied one: a `Bash(git:*)` prefix | |
| # rule permits `git push --receive-pack='sh -c ...' ext::sh`, which is | |
| # exactly the RCE the action's vendored scripts/git-push.sh wrapper | |
| # exists to close (HackerOne #3556799). Tag mode already unions in | |
| # `git add`/`git commit`/`git rm` and that wrapper, so only read-only | |
| # verbs were missing and only those are added. `git fetch` is NOT among | |
| # them despite looking read-only: `git fetch 'ext::sh -c <cmd>'` runs | |
| # <cmd> locally, since protocol.ext.allow defaults to `user`. | |
| claude_args: | | |
| --model ${{ env.AGENT_MODEL }} | |
| --mcp-config '{"mcpServers": {"ols": {"command": "uvx", "args": ["ols-mcp"]}, "sequential-thinking": {"command": "npx", "args": ["-y", "@modelcontextprotocol/server-sequential-thinking"]}}}' | |
| --allowedTools "Read,Glob,Grep,WebSearch,WebFetch,Bash(just:*),Bash(uv:*),Bash(uvx:*),Bash(gh:*),Bash(git status:*),Bash(git diff:*),Bash(git log:*),Bash(git show:*),Bash(git checkout:*),mcp__ols__search_all_ontologies,mcp__ols__get_terms_from_ontology,mcp__ols__get_ontology_info,mcp__sequential-thinking__sequentialthinking" | |
| - name: Write step summary | |
| # Not `always()`: with cancel-in-progress a superseded run writes no | |
| # execution log, and the summary would report that as breakage. | |
| if: ${{ !cancelled() }} | |
| uses: ./.github/actions/agent-run-summary | |
| with: | |
| execution-file: ${{ steps.claude.outputs.execution_file }} | |
| title: "@claude Run" |