From d306b5371ea4d399801b62e43afe6db901ffc23a Mon Sep 17 00:00:00 2001 From: Andre Manoel Date: Mon, 13 Apr 2026 21:02:49 -0300 Subject: [PATCH 1/4] fix: use pull_request_target for agentic CI on fork PRs --- .github/workflows/agentic-ci-pr-review.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/agentic-ci-pr-review.yml b/.github/workflows/agentic-ci-pr-review.yml index 0e6edfcc4..0a16cb313 100644 --- a/.github/workflows/agentic-ci-pr-review.yml +++ b/.github/workflows/agentic-ci-pr-review.yml @@ -1,7 +1,7 @@ name: "Agentic CI: PR Review" on: - pull_request: + pull_request_target: types: [opened, ready_for_review, labeled] branches: [main] workflow_dispatch: @@ -80,6 +80,7 @@ jobs: needs: gate if: needs.gate.outputs.allowed == 'true' runs-on: [self-hosted, agentic-ci] + environment: agentic-ci timeout-minutes: 15 steps: - name: Determine PR number From c203c8b930099444b757b43d9a6d41f52f0cf72f Mon Sep 17 00:00:00 2001 From: Andre Manoel Date: Tue, 14 Apr 2026 00:30:23 +0000 Subject: [PATCH 2/4] fix: read recipe files from base branch to prevent prompt injection Recipe files define the agent's prompt. When using pull_request_target, the fork's HEAD is checked out, so a malicious fork could craft recipe files to exfiltrate API secrets via prompt injection. Fix by adding a second sparse checkout from the base branch for .agents/recipes/ and reading prompts from there instead of the fork tree. --- .github/workflows/agentic-ci-pr-review.yml | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/.github/workflows/agentic-ci-pr-review.yml b/.github/workflows/agentic-ci-pr-review.yml index 0a16cb313..0d9fc2e0a 100644 --- a/.github/workflows/agentic-ci-pr-review.yml +++ b/.github/workflows/agentic-ci-pr-review.yml @@ -129,6 +129,16 @@ jobs: ref: ${{ steps.head.outputs.sha }} fetch-depth: 0 + # SECURITY: Recipe files define the agent's prompt. Always read them + # from the base branch so a fork PR cannot inject malicious instructions + # while API secrets are in scope. + - name: Checkout base branch recipes + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + with: + ref: ${{ github.event.pull_request.base.sha || 'main' }} + sparse-checkout: .agents/recipes + path: base-recipes + - name: Pre-flight checks env: ANTHROPIC_BASE_URL: ${{ secrets.AGENTIC_CI_API_BASE_URL }} @@ -169,8 +179,10 @@ jobs: set -o pipefail # Build the prompt from _runner.md + recipe, substituting template vars. - RUNNER_CTX=$(cat .agents/recipes/_runner.md) - RECIPE_BODY=$(cat .agents/recipes/pr-review/recipe.md \ + # Read from base-recipes/ (checked out from the base branch) so fork + # PRs cannot tamper with the agent prompt. + RUNNER_CTX=$(cat base-recipes/.agents/recipes/_runner.md) + RECIPE_BODY=$(cat base-recipes/.agents/recipes/pr-review/recipe.md \ | sed '1,/^---$/{ /^---$/,/^---$/d }') PROMPT=$(printf '%s\n\n%s\n' "${RUNNER_CTX}" "${RECIPE_BODY}" \ From d308a52cba4deb866e785469413e17fa3d584909 Mon Sep 17 00:00:00 2001 From: Andre Manoel Date: Tue, 14 Apr 2026 00:39:50 +0000 Subject: [PATCH 3/4] fix: align actions/checkout version for base-recipes checkout Match the base-branch recipe checkout to v6.0.2 (same SHA as the PR branch checkout) for consistency. --- .github/workflows/agentic-ci-pr-review.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/agentic-ci-pr-review.yml b/.github/workflows/agentic-ci-pr-review.yml index 0d9fc2e0a..ca250326b 100644 --- a/.github/workflows/agentic-ci-pr-review.yml +++ b/.github/workflows/agentic-ci-pr-review.yml @@ -133,7 +133,7 @@ jobs: # from the base branch so a fork PR cannot inject malicious instructions # while API secrets are in scope. - name: Checkout base branch recipes - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: ref: ${{ github.event.pull_request.base.sha || 'main' }} sparse-checkout: .agents/recipes From 65aab94fbf352cc4094fd461f09ea384191cc011 Mon Sep 17 00:00:00 2001 From: Andre Manoel Date: Tue, 14 Apr 2026 00:42:01 +0000 Subject: [PATCH 4/4] fix: move expression interpolations to env vars in gate and review jobs Replace direct ${{ }} interpolation in run: blocks with env vars. Most values are GitHub-controlled, but github.event.label.name can contain arbitrary characters and could break shell quoting. Moving everything to env: is consistent with the injection-hardening pattern applied in the rest of the workflow. --- .github/workflows/agentic-ci-pr-review.yml | 39 ++++++++++++++-------- 1 file changed, 26 insertions(+), 13 deletions(-) diff --git a/.github/workflows/agentic-ci-pr-review.yml b/.github/workflows/agentic-ci-pr-review.yml index ca250326b..7b995d58e 100644 --- a/.github/workflows/agentic-ci-pr-review.yml +++ b/.github/workflows/agentic-ci-pr-review.yml @@ -32,23 +32,30 @@ jobs: id: check env: GH_TOKEN: ${{ github.token }} + EVENT_NAME: ${{ github.event_name }} + EVENT_ACTION: ${{ github.event.action }} + LABEL_NAME: ${{ github.event.label.name }} + IS_DRAFT: ${{ github.event.pull_request.draft }} + SENDER_LOGIN: ${{ github.event.sender.login }} + PR_AUTHOR: ${{ github.event.pull_request.user.login }} + REPO: ${{ github.repository }} run: | # workflow_dispatch callers already have write access. - if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then + if [ "$EVENT_NAME" = "workflow_dispatch" ]; then echo "allowed=true" >> "$GITHUB_OUTPUT" exit 0 fi # Only the agent-review label should trigger a run. - if [ "${{ github.event.action }}" = "labeled" ] && [ "${{ github.event.label.name }}" != "agent-review" ]; then + if [ "$EVENT_ACTION" = "labeled" ] && [ "$LABEL_NAME" != "agent-review" ]; then echo "Skipping: labeled event but not agent-review" echo "allowed=false" >> "$GITHUB_OUTPUT" exit 0 fi # Skip drafts unless agent-review label is being added. - if [ "${{ github.event.pull_request.draft }}" = "true" ]; then - if [ "${{ github.event.action }}" != "labeled" ] || [ "${{ github.event.label.name }}" != "agent-review" ]; then + if [ "$IS_DRAFT" = "true" ]; then + if [ "$EVENT_ACTION" != "labeled" ] || [ "$LABEL_NAME" != "agent-review" ]; then echo "Skipping: draft PR" echo "allowed=false" >> "$GITHUB_OUTPUT" exit 0 @@ -58,15 +65,15 @@ jobs: # For labeled events, check the sender (who added the label) so # maintainers can authorize reviews on external PRs. # For other events, check the PR author. - if [ "${{ github.event.action }}" = "labeled" ]; then - USER="${{ github.event.sender.login }}" + if [ "$EVENT_ACTION" = "labeled" ]; then + USER="$SENDER_LOGIN" echo "Checking sender (labeler): ${USER}" else - USER="${{ github.event.pull_request.user.login }}" + USER="$PR_AUTHOR" echo "Checking PR author: ${USER}" fi - PERMISSION=$(gh api "repos/${{ github.repository }}/collaborators/${USER}/permission" --jq '.permission' 2>/dev/null || echo "none") + PERMISSION=$(gh api "repos/${REPO}/collaborators/${USER}/permission" --jq '.permission' 2>/dev/null || echo "none") echo "permission=${PERMISSION}" if [ "$PERMISSION" = "admin" ] || [ "$PERMISSION" = "write" ]; then @@ -85,11 +92,15 @@ jobs: steps: - name: Determine PR number id: pr + env: + EVENT_NAME: ${{ github.event_name }} + INPUT_PR_NUMBER: ${{ github.event.inputs.pr_number }} + PR_NUMBER: ${{ github.event.pull_request.number }} run: | - if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then - echo "number=${{ github.event.inputs.pr_number }}" >> "$GITHUB_OUTPUT" + if [ "$EVENT_NAME" = "workflow_dispatch" ]; then + echo "number=${INPUT_PR_NUMBER}" >> "$GITHUB_OUTPUT" else - echo "number=${{ github.event.pull_request.number }}" >> "$GITHUB_OUTPUT" + echo "number=${PR_NUMBER}" >> "$GITHUB_OUTPUT" fi - name: Validate PR number @@ -114,12 +125,14 @@ jobs: id: head env: GH_TOKEN: ${{ github.token }} + EVENT_NAME: ${{ github.event_name }} PR_NUMBER: ${{ steps.pr.outputs.number }} + PR_HEAD_SHA: ${{ github.event.pull_request.head.sha }} run: | - if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then + if [ "$EVENT_NAME" = "workflow_dispatch" ]; then SHA=$(gh pr view "$PR_NUMBER" --json headRefOid -q '.headRefOid') else - SHA="${{ github.event.pull_request.head.sha }}" + SHA="$PR_HEAD_SHA" fi echo "sha=$SHA" >> "$GITHUB_OUTPUT"