From 21aaf16fd711b7d74875436830478c02cb89a876 Mon Sep 17 00:00:00 2001 From: Devina Date: Fri, 14 Aug 2026 16:18:39 -0500 Subject: [PATCH] Gate agent workflows on trusted authors ## Summary Prevent write-capable agent workflows from running on externally authored issues or PRs, even when a trusted actor triggers them. Add environment approvals for higher-risk agent jobs while allowing naqvitalha to bypass that approval path for now. ## Approach - Require issue and PR authors to be OWNER, MEMBER, or COLLABORATOR before agent workflows can run. - Add approval jobs for agent-fix, agent-bot, and agent-android-bot using GitHub Environments. - Keep existing trigger-actor permission checks and prompt-injection scan headers in place. Co-authored-by: gpt-5.5 Orchestrated-by: ae Assisted-By: devx/1eb1773d-0aa4-4609-9e57-479a175eed30 --- .github/workflows/agent-android-bot.yml | 92 ++++++++++++++++++++++--- .github/workflows/agent-bot.yml | 81 +++++++++++++++++++--- .github/workflows/agent-fix.yml | 82 ++++++++++++++++++---- .github/workflows/agent-triage.yml | 11 ++- 4 files changed, 231 insertions(+), 35 deletions(-) diff --git a/.github/workflows/agent-android-bot.yml b/.github/workflows/agent-android-bot.yml index 29e06ba4b..c92e1a42b 100644 --- a/.github/workflows/agent-android-bot.yml +++ b/.github/workflows/agent-android-bot.yml @@ -39,44 +39,108 @@ env: Bash(cd *),Bash(./gradlew *) jobs: + approval: + name: Agent Android bot approval + runs-on: ubuntu-latest + permissions: {} + if: >- + ( + github.event_name == 'workflow_dispatch' || + ( + contains(github.event.comment.body || github.event.review.body, '@android-agent') && + !contains(github.event.comment.body || github.event.review.body, '/fix') && + ( + (github.event.comment.author_association || github.event.review.author_association) == 'OWNER' || + (github.event.comment.author_association || github.event.review.author_association) == 'MEMBER' || + (github.event.comment.author_association || github.event.review.author_association) == 'COLLABORATOR' + ) && + ( + (github.event.issue.author_association || github.event.pull_request.author_association) == 'OWNER' || + (github.event.issue.author_association || github.event.pull_request.author_association) == 'MEMBER' || + (github.event.issue.author_association || github.event.pull_request.author_association) == 'COLLABORATOR' + ) + ) + ) && + !contains(',naqvitalha,', format(',{0},', github.actor)) + environment: agent-android-bot + steps: + - name: Await approval + run: echo "Approved agent-android-bot run for ${{ github.actor }}" + bot: + needs: approval runs-on: ubuntu-latest if: >- - github.event_name == 'workflow_dispatch' || + always() && ( - contains(github.event.comment.body || github.event.review.body, '@android-agent') && - !contains(github.event.comment.body || github.event.review.body, '/fix') && + github.event_name == 'workflow_dispatch' || ( - (github.event.comment.author_association || github.event.review.author_association) == 'OWNER' || - (github.event.comment.author_association || github.event.review.author_association) == 'MEMBER' || - (github.event.comment.author_association || github.event.review.author_association) == 'COLLABORATOR' + contains(github.event.comment.body || github.event.review.body, '@android-agent') && + !contains(github.event.comment.body || github.event.review.body, '/fix') && + ( + (github.event.comment.author_association || github.event.review.author_association) == 'OWNER' || + (github.event.comment.author_association || github.event.review.author_association) == 'MEMBER' || + (github.event.comment.author_association || github.event.review.author_association) == 'COLLABORATOR' + ) && + ( + (github.event.issue.author_association || github.event.pull_request.author_association) == 'OWNER' || + (github.event.issue.author_association || github.event.pull_request.author_association) == 'MEMBER' || + (github.event.issue.author_association || github.event.pull_request.author_association) == 'COLLABORATOR' + ) ) + ) && + ( + contains(',naqvitalha,', format(',{0},', github.actor)) || + needs.approval.result == 'success' ) steps: + - name: Check triggering actor permission + id: actor_permission + env: + GH_TOKEN: ${{ github.token }} + ACTOR: ${{ github.actor }} + REPOSITORY: ${{ github.repository }} + run: | + set -euo pipefail + permission=$(gh api "repos/${REPOSITORY}/collaborators/${ACTOR}/permission" --jq .permission 2>/dev/null || echo "") + case "$permission" in + admin|maintain|write) + echo "allowed=true" >> "$GITHUB_OUTPUT" + ;; + *) + echo "allowed=false" >> "$GITHUB_OUTPUT" + ;; + esac + - name: Checkout + if: steps.actor_permission.outputs.allowed == 'true' uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: fetch-depth: 0 - name: Enable KVM + if: steps.actor_permission.outputs.allowed == 'true' run: | echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules sudo udevadm control --reload-rules sudo udevadm trigger --name-match=kvm - name: Setup Node + if: steps.actor_permission.outputs.allowed == 'true' uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 with: node-version: "22.18.0" cache: "yarn" - name: Setup Java + if: steps.actor_permission.outputs.allowed == 'true' uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4.7.1 with: distribution: "temurin" java-version: "17" - name: Restore Android SDK cache + if: steps.actor_permission.outputs.allowed == 'true' uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3 id: sdk-cache with: @@ -86,6 +150,7 @@ jobs: key: android-sdk-35 - name: Restore AVD cache + if: steps.actor_permission.outputs.allowed == 'true' uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3 id: avd-cache with: @@ -95,16 +160,19 @@ jobs: key: avd-api-35-pixel6 - name: Install dependencies + if: steps.actor_permission.outputs.allowed == 'true' run: yarn install --frozen-lockfile - name: Install agent-device + if: steps.actor_permission.outputs.allowed == 'true' run: npm install -g agent-device - name: Fetch main branch for diffing + if: steps.actor_permission.outputs.allowed == 'true' run: git fetch origin main - name: Setup Android SDK - if: steps.sdk-cache.outputs.cache-hit != 'true' + if: steps.actor_permission.outputs.allowed == 'true' && steps.sdk-cache.outputs.cache-hit != 'true' run: | echo "$ANDROID_HOME/platform-tools" >> $GITHUB_PATH echo "$ANDROID_HOME/emulator" >> $GITHUB_PATH @@ -117,14 +185,14 @@ jobs: "emulator" - name: Add SDK tools to PATH - if: steps.sdk-cache.outputs.cache-hit == 'true' + if: steps.actor_permission.outputs.allowed == 'true' && steps.sdk-cache.outputs.cache-hit == 'true' run: | echo "$ANDROID_HOME/platform-tools" >> $GITHUB_PATH echo "$ANDROID_HOME/emulator" >> $GITHUB_PATH echo "$ANDROID_HOME/cmdline-tools/latest/bin" >> $GITHUB_PATH - name: Create AVD - if: steps.avd-cache.outputs.cache-hit != 'true' + if: steps.actor_permission.outputs.allowed == 'true' && steps.avd-cache.outputs.cache-hit != 'true' run: | export ANDROID_AVD_HOME="$HOME/.android/avd" mkdir -p "$ANDROID_AVD_HOME" @@ -136,6 +204,7 @@ jobs: --force - name: Start emulator + if: steps.actor_permission.outputs.allowed == 'true' run: | nohup emulator -avd Android35 \ -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim \ @@ -147,7 +216,8 @@ jobs: echo "Android emulator is ready" - name: Run agent - uses: anthropics/claude-code-action@bf4f0de6fccd1eea7044a5f903fc928aff363134 # v1 + if: steps.actor_permission.outputs.allowed == 'true' + uses: anthropics/claude-code-action@ba0aafd4308cbba7165f9f2cdb0cfbed5a3c99ce # v1.0.168 env: ANTHROPIC_BASE_URL: https://proxy.shopify.ai/vendors/anthropic ANTHROPIC_CUSTOM_HEADERS: |- @@ -165,7 +235,7 @@ jobs: prompt: ${{ github.event.inputs.prompt || '' }} - name: Upload agent feedback - if: always() + if: always() && steps.actor_permission.outputs.allowed == 'true' uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: name: agent-feedback-android-bot-${{ github.event.issue.number || github.event.pull_request.number || github.run_id }} diff --git a/.github/workflows/agent-bot.yml b/.github/workflows/agent-bot.yml index b1470b2a2..1bee6e951 100644 --- a/.github/workflows/agent-bot.yml +++ b/.github/workflows/agent-bot.yml @@ -36,42 +36,107 @@ env: Bash(cd *),Bash(./gradlew *) jobs: + approval: + name: Agent bot approval + runs-on: ubuntu-latest + permissions: {} + if: >- + ( + github.event_name == 'workflow_dispatch' || + ( + contains(github.event.comment.body || github.event.review.body, '@agent') && + !contains(github.event.comment.body || github.event.review.body, '/fix') && + ( + (github.event.comment.author_association || github.event.review.author_association) == 'OWNER' || + (github.event.comment.author_association || github.event.review.author_association) == 'MEMBER' || + (github.event.comment.author_association || github.event.review.author_association) == 'COLLABORATOR' + ) && + ( + (github.event.issue.author_association || github.event.pull_request.author_association) == 'OWNER' || + (github.event.issue.author_association || github.event.pull_request.author_association) == 'MEMBER' || + (github.event.issue.author_association || github.event.pull_request.author_association) == 'COLLABORATOR' + ) + ) + ) && + !contains(',naqvitalha,', format(',{0},', github.actor)) + environment: agent-bot + steps: + - name: Await approval + run: echo "Approved agent-bot run for ${{ github.actor }}" + bot: + needs: approval runs-on: macos-latest if: >- - github.event_name == 'workflow_dispatch' || + always() && ( - contains(github.event.comment.body || github.event.review.body, '@agent') && - !contains(github.event.comment.body || github.event.review.body, '/fix') && + github.event_name == 'workflow_dispatch' || ( - (github.event.comment.author_association || github.event.review.author_association) == 'OWNER' || - (github.event.comment.author_association || github.event.review.author_association) == 'MEMBER' || - (github.event.comment.author_association || github.event.review.author_association) == 'COLLABORATOR' + contains(github.event.comment.body || github.event.review.body, '@agent') && + !contains(github.event.comment.body || github.event.review.body, '/fix') && + ( + (github.event.comment.author_association || github.event.review.author_association) == 'OWNER' || + (github.event.comment.author_association || github.event.review.author_association) == 'MEMBER' || + (github.event.comment.author_association || github.event.review.author_association) == 'COLLABORATOR' + ) && + ( + (github.event.issue.author_association || github.event.pull_request.author_association) == 'OWNER' || + (github.event.issue.author_association || github.event.pull_request.author_association) == 'MEMBER' || + (github.event.issue.author_association || github.event.pull_request.author_association) == 'COLLABORATOR' + ) ) + ) && + ( + contains(',naqvitalha,', format(',{0},', github.actor)) || + needs.approval.result == 'success' ) steps: + - name: Check triggering actor permission + id: actor_permission + env: + GH_TOKEN: ${{ github.token }} + ACTOR: ${{ github.actor }} + REPOSITORY: ${{ github.repository }} + run: | + set -euo pipefail + permission=$(gh api "repos/${REPOSITORY}/collaborators/${ACTOR}/permission" --jq .permission 2>/dev/null || echo "") + case "$permission" in + admin|maintain|write) + echo "allowed=true" >> "$GITHUB_OUTPUT" + ;; + *) + echo "allowed=false" >> "$GITHUB_OUTPUT" + ;; + esac + - name: Checkout + if: steps.actor_permission.outputs.allowed == 'true' uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: fetch-depth: 0 - name: Setup Node + if: steps.actor_permission.outputs.allowed == 'true' uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 with: node-version: "22.18.0" cache: "yarn" - name: Install dependencies + if: steps.actor_permission.outputs.allowed == 'true' run: yarn install --frozen-lockfile - name: Install agent-device + if: steps.actor_permission.outputs.allowed == 'true' run: npm install -g agent-device - name: Fetch main branch for diffing + if: steps.actor_permission.outputs.allowed == 'true' run: git fetch origin main - name: Run agent - uses: anthropics/claude-code-action@bf4f0de6fccd1eea7044a5f903fc928aff363134 # v1 + if: steps.actor_permission.outputs.allowed == 'true' + uses: anthropics/claude-code-action@ba0aafd4308cbba7165f9f2cdb0cfbed5a3c99ce # v1.0.168 env: ANTHROPIC_BASE_URL: https://proxy.shopify.ai/vendors/anthropic ANTHROPIC_CUSTOM_HEADERS: |- @@ -89,7 +154,7 @@ jobs: prompt: ${{ github.event.inputs.prompt || '' }} - name: Upload agent feedback - if: always() + if: always() && steps.actor_permission.outputs.allowed == 'true' uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: name: agent-feedback-bot-${{ github.event.issue.number || github.event.pull_request.number || github.run_id }} diff --git a/.github/workflows/agent-fix.yml b/.github/workflows/agent-fix.yml index 40ce05fc5..2ce97cabc 100644 --- a/.github/workflows/agent-fix.yml +++ b/.github/workflows/agent-fix.yml @@ -28,23 +28,79 @@ env: Bash(cd *),Bash(./gradlew *) jobs: + approval: + name: Agent fix approval + runs-on: ubuntu-latest + permissions: {} + if: >- + ( + ( + github.event.action == 'labeled' && + github.event.label.name == 'agent-fix' && + github.event.issue.pull_request == null && + github.event.sender.type != 'Bot' && + ( + github.event.issue.author_association == 'OWNER' || + github.event.issue.author_association == 'MEMBER' || + github.event.issue.author_association == 'COLLABORATOR' + ) + ) || ( + github.event.action == 'created' && + contains(github.event.comment.body, '/fix') && + !github.event.issue.pull_request && + ( + github.event.issue.author_association == 'OWNER' || + github.event.issue.author_association == 'MEMBER' || + github.event.issue.author_association == 'COLLABORATOR' + ) && + ( + github.event.comment.author_association == 'OWNER' || + github.event.comment.author_association == 'MEMBER' || + github.event.comment.author_association == 'COLLABORATOR' + ) + ) + ) && + !contains(',naqvitalha,', format(',{0},', github.actor)) + environment: agent-fix + steps: + - name: Await approval + run: echo "Approved agent-fix run for ${{ github.actor }}" + fix: + needs: approval runs-on: macos-latest if: >- + always() && ( - github.event.action == 'labeled' && - github.event.label.name == 'agent-fix' && - github.event.issue.pull_request == null && - github.event.sender.type != 'Bot' - ) || ( - github.event.action == 'created' && - contains(github.event.comment.body, '/fix') && - !github.event.issue.pull_request && ( - github.event.comment.author_association == 'OWNER' || - github.event.comment.author_association == 'MEMBER' || - github.event.comment.author_association == 'COLLABORATOR' + github.event.action == 'labeled' && + github.event.label.name == 'agent-fix' && + github.event.issue.pull_request == null && + github.event.sender.type != 'Bot' && + ( + github.event.issue.author_association == 'OWNER' || + github.event.issue.author_association == 'MEMBER' || + github.event.issue.author_association == 'COLLABORATOR' + ) + ) || ( + github.event.action == 'created' && + contains(github.event.comment.body, '/fix') && + !github.event.issue.pull_request && + ( + github.event.issue.author_association == 'OWNER' || + github.event.issue.author_association == 'MEMBER' || + github.event.issue.author_association == 'COLLABORATOR' + ) && + ( + github.event.comment.author_association == 'OWNER' || + github.event.comment.author_association == 'MEMBER' || + github.event.comment.author_association == 'COLLABORATOR' + ) ) + ) && + ( + contains(',naqvitalha,', format(',{0},', github.actor)) || + needs.approval.result == 'success' ) steps: - name: Check triggering actor permission @@ -92,7 +148,7 @@ jobs: - name: Fix issue if: steps.actor_permission.outputs.allowed == 'true' - uses: anthropics/claude-code-action@bf4f0de6fccd1eea7044a5f903fc928aff363134 # v1 + uses: anthropics/claude-code-action@ba0aafd4308cbba7165f9f2cdb0cfbed5a3c99ce # v1.0.168 env: ANTHROPIC_BASE_URL: https://proxy.shopify.ai/vendors/anthropic ANTHROPIC_CUSTOM_HEADERS: |- @@ -125,7 +181,7 @@ jobs: - Suggestions for improvement - name: Upload agent feedback - if: always() + if: always() && steps.actor_permission.outputs.allowed == 'true' uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: name: agent-feedback-fix-${{ github.event.issue.number || github.run_id }} diff --git a/.github/workflows/agent-triage.yml b/.github/workflows/agent-triage.yml index 032bf5003..2ccac69c0 100644 --- a/.github/workflows/agent-triage.yml +++ b/.github/workflows/agent-triage.yml @@ -15,7 +15,12 @@ jobs: if: >- github.event.label.name == 'agent-triage' && github.event.issue.pull_request == null && - github.event.sender.type != 'Bot' + github.event.sender.type != 'Bot' && + ( + github.event.issue.author_association == 'OWNER' || + github.event.issue.author_association == 'MEMBER' || + github.event.issue.author_association == 'COLLABORATOR' + ) steps: - name: Check triggering actor permission id: actor_permission @@ -41,7 +46,7 @@ jobs: - name: Triage issue if: steps.actor_permission.outputs.allowed == 'true' - uses: anthropics/claude-code-action@bf4f0de6fccd1eea7044a5f903fc928aff363134 # v1 + uses: anthropics/claude-code-action@ba0aafd4308cbba7165f9f2cdb0cfbed5a3c99ce # v1.0.168 env: ANTHROPIC_BASE_URL: https://proxy.shopify.ai/vendors/anthropic ANTHROPIC_CUSTOM_HEADERS: |- @@ -71,7 +76,7 @@ jobs: - Suggestions for improvement - name: Upload agent feedback - if: always() + if: always() && steps.actor_permission.outputs.allowed == 'true' uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: name: agent-feedback-triage-${{ github.event.issue.number }}