From aa2384b390166d30b9501c9395b26c4216f4a73e Mon Sep 17 00:00:00 2001 From: Derek Misler Date: Fri, 6 Mar 2026 16:23:18 -0500 Subject: [PATCH] Allow docker agent to request reviews Signed-off-by: Derek Misler --- .github/workflows/review-pr.yml | 24 ++++++++++++++---------- .github/workflows/self-review-pr.yml | 25 +++++++++++++++---------- action.yml | 24 ++++++++++++++++++++++-- review-pr/action.yml | 6 ++++++ 4 files changed, 57 insertions(+), 22 deletions(-) diff --git a/.github/workflows/review-pr.yml b/.github/workflows/review-pr.yml index 39d4ddc..753595b 100644 --- a/.github/workflows/review-pr.yml +++ b/.github/workflows/review-pr.yml @@ -207,6 +207,17 @@ jobs: exit-code: ${{ steps.run-review.outputs.exit-code }} steps: + # Generate GitHub App token first so the check run is created under the app's identity + # (prevents GitHub from nesting it under unrelated pull_request-triggered workflows) + - name: Generate GitHub App token + if: env.HAS_APP_SECRETS == 'true' + id: app-token + continue-on-error: true # Don't fail workflow if token generation fails + uses: tibdex/github-app-token@3beb63f4bd073e61482598c45c71c1019b59b73a # v2 + with: + app_id: ${{ secrets.CAGENT_REVIEWER_APP_ID }} + private_key: ${{ secrets.CAGENT_REVIEWER_APP_PRIVATE_KEY }} + - name: Create check run id: create-check continue-on-error: true # Don't fail if caller didn't grant checks: write @@ -214,6 +225,7 @@ jobs: env: PR_NUMBER: ${{ inputs.pr-number || github.event.issue.number }} with: + github-token: ${{ steps.app-token.outputs.token || github.token }} script: | const prNumber = parseInt(process.env.PR_NUMBER, 10); const { data: pr } = await github.rest.pulls.get({ @@ -241,16 +253,6 @@ jobs: fetch-depth: 0 ref: refs/pull/${{ github.event.issue.number }}/head - # Generate GitHub App token for custom app identity (optional - falls back to github.token) - - name: Generate GitHub App token - if: env.HAS_APP_SECRETS == 'true' - id: app-token - continue-on-error: true # Don't fail workflow if token generation fails - uses: tibdex/github-app-token@3beb63f4bd073e61482598c45c71c1019b59b73a # v2 - with: - app_id: ${{ secrets.CAGENT_REVIEWER_APP_ID }} - private_key: ${{ secrets.CAGENT_REVIEWER_APP_PRIVATE_KEY }} - - name: Run PR Review id: run-review continue-on-error: true # Don't fail the calling workflow if the review errors @@ -262,6 +264,7 @@ jobs: add-prompt-files: ${{ inputs.add-prompt-files }} model: ${{ inputs.model }} github-token: ${{ steps.app-token.outputs.token || github.token }} + trusted-bot-app-id: ${{ secrets.CAGENT_REVIEWER_APP_ID }} anthropic-api-key: ${{ secrets.ANTHROPIC_API_KEY }} openai-api-key: ${{ secrets.OPENAI_API_KEY }} google-api-key: ${{ secrets.GOOGLE_API_KEY }} @@ -277,6 +280,7 @@ jobs: CHECK_ID: ${{ steps.create-check.outputs.check-id }} JOB_STATUS: ${{ job.status }} with: + github-token: ${{ steps.app-token.outputs.token || github.token }} script: | const conclusion = process.env.JOB_STATUS === 'cancelled' ? 'cancelled' : process.env.JOB_STATUS === 'success' ? 'success' : 'failure'; try { diff --git a/.github/workflows/self-review-pr.yml b/.github/workflows/self-review-pr.yml index 73b8fc8..4147460 100644 --- a/.github/workflows/self-review-pr.yml +++ b/.github/workflows/self-review-pr.yml @@ -112,11 +112,25 @@ jobs: HAS_APP_SECRETS: ${{ secrets.CAGENT_REVIEWER_APP_ID != '' }} steps: + # Generate GitHub App token first so the check run is created under the app's identity + # (prevents GitHub from nesting it under unrelated pull_request-triggered workflows) + - name: Generate GitHub App token + if: env.HAS_APP_SECRETS == 'true' + id: app-token + continue-on-error: true # Don't fail workflow if token generation fails + uses: tibdex/github-app-token@3beb63f4bd073e61482598c45c71c1019b59b73a # v2 + with: + app_id: ${{ secrets.CAGENT_REVIEWER_APP_ID }} + private_key: ${{ secrets.CAGENT_REVIEWER_APP_PRIVATE_KEY }} + - name: Create check run id: create-check continue-on-error: true # Don't fail if checks: write permission is missing uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 + env: + APP_TOKEN: ${{ steps.app-token.outputs.token }} with: + github-token: ${{ steps.app-token.outputs.token || github.token }} script: | const prNumber = context.issue.number; const { data: pr } = await github.rest.pulls.get({ @@ -144,16 +158,6 @@ jobs: fetch-depth: 0 ref: refs/pull/${{ github.event.issue.number }}/head - # Generate GitHub App token for custom app identity (optional - falls back to github.token) - - name: Generate GitHub App token - if: env.HAS_APP_SECRETS == 'true' - id: app-token - continue-on-error: true # Don't fail workflow if token generation fails - uses: tibdex/github-app-token@3beb63f4bd073e61482598c45c71c1019b59b73a # v2 - with: - app_id: ${{ secrets.CAGENT_REVIEWER_APP_ID }} - private_key: ${{ secrets.CAGENT_REVIEWER_APP_PRIVATE_KEY }} - - name: Run PR Review id: run-review continue-on-error: true # Don't fail the calling workflow if the review errors @@ -177,6 +181,7 @@ jobs: CHECK_ID: ${{ steps.create-check.outputs.check-id }} JOB_STATUS: ${{ job.status }} with: + github-token: ${{ steps.app-token.outputs.token || github.token }} script: | const conclusion = process.env.JOB_STATUS === 'cancelled' ? 'cancelled' : process.env.JOB_STATUS === 'success' ? 'success' : 'failure'; try { diff --git a/action.yml b/action.yml index 9f65150..b737036 100644 --- a/action.yml +++ b/action.yml @@ -78,6 +78,10 @@ inputs: description: "Additional arguments to pass to cagent run" required: false default: "" + trusted-bot-app-id: + description: "GitHub App ID of a trusted bot that can bypass comment-based auth checks (e.g., for self-review triggers)" + required: false + default: "" add-prompt-files: description: "Comma-separated list of files to append to the prompt (e.g., 'AGENTS.md,CLAUDE.md')" required: false @@ -190,10 +194,12 @@ runs: shell: bash env: ACTION_PATH: ${{ github.action_path }} - # Get author_association from comment events (the main risk) - COMMENT_ASSOCIATION: ${{ github.event.comment.author_association }} + TRUSTED_BOT_APP_ID: ${{ inputs.trusted-bot-app-id }} DEBUG: ${{ inputs.debug }} run: | + # Read comment fields directly from the event payload (cannot be overridden by workflow env vars) + COMMENT_ASSOCIATION=$(jq -r '.comment.author_association // empty' "$GITHUB_EVENT_PATH") + # Only enforce auth for comment-triggered events # This prevents abuse via /commands while allowing PR-triggered workflows to run if [ -z "$COMMENT_ASSOCIATION" ]; then @@ -202,6 +208,20 @@ runs: exit 0 fi + # Allow a trusted GitHub App bot to bypass auth (e.g., auto-triage posts /review). + # Verified via user type + app ID from the event payload to prevent spoofing. + if [ -n "$TRUSTED_BOT_APP_ID" ]; then + COMMENT_USER_TYPE=$(jq -r '.comment.user.type // empty' "$GITHUB_EVENT_PATH") + COMMENT_APP_ID=$(jq -r '.comment.performed_via_github_app.id // empty' "$GITHUB_EVENT_PATH") + + if [ "$COMMENT_USER_TYPE" = "Bot" ] && [ -n "$COMMENT_APP_ID" ] && [ "$COMMENT_APP_ID" = "$TRUSTED_BOT_APP_ID" ]; then + COMMENT_USER_LOGIN=$(jq -r '.comment.user.login // empty' "$GITHUB_EVENT_PATH") + echo "ℹ️ Skipping auth check (trusted bot: $COMMENT_USER_LOGIN, app_id: $COMMENT_APP_ID)" + echo "authorized=bot" >> $GITHUB_OUTPUT + exit 0 + fi + fi + echo "Using comment author_association: $COMMENT_ASSOCIATION" # Allowed roles (hardcoded for security - cannot be overridden) diff --git a/review-pr/action.yml b/review-pr/action.yml index 52d9c4d..7cca1ac 100644 --- a/review-pr/action.yml +++ b/review-pr/action.yml @@ -53,6 +53,10 @@ inputs: description: "Comma-separated list of files to append to the prompt (e.g., 'AGENTS.md,CLAUDE.md')" required: false default: "" + trusted-bot-app-id: + description: "GitHub App ID of a trusted bot that can bypass comment-based auth checks" + required: false + default: "" outputs: exit-code: @@ -473,6 +477,7 @@ runs: nebius-api-key: ${{ inputs.nebius-api-key }} mistral-api-key: ${{ inputs.mistral-api-key }} github-token: ${{ steps.resolve-token.outputs.token }} + trusted-bot-app-id: ${{ inputs.trusted-bot-app-id }} extra-args: ${{ inputs.model && format('--model={0}', inputs.model) || '' }} # ======================================== @@ -560,6 +565,7 @@ runs: nebius-api-key: ${{ inputs.nebius-api-key }} mistral-api-key: ${{ inputs.mistral-api-key }} github-token: ${{ steps.resolve-token.outputs.token }} + trusted-bot-app-id: ${{ inputs.trusted-bot-app-id }} extra-args: ${{ inputs.model && format('--model={0}', inputs.model) || '' }} add-prompt-files: ${{ inputs.add-prompt-files }} max-retries: "0" # Disable retries — the review agent recovers internally (root falls back when sub-agents fail), so retrying the pipeline produces duplicate reviews