From bfcdfa59ecce41cc957287a866faf2a57ad78f94 Mon Sep 17 00:00:00 2001 From: nate nowack Date: Tue, 18 Aug 2026 11:42:17 -0500 Subject: [PATCH] Scope Marvin App token to each job's declared permissions (#4834) Co-authored-by: Claude Opus 5 (1M context) --- .github/workflows/marvin-dedupe-issues.yml | 12 +++++++++++- .github/workflows/marvin-label-triage.yml | 9 ++++++++- 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/.github/workflows/marvin-dedupe-issues.yml b/.github/workflows/marvin-dedupe-issues.yml index a9de7d0d3..f5a8c7b17 100644 --- a/.github/workflows/marvin-dedupe-issues.yml +++ b/.github/workflows/marvin-dedupe-issues.yml @@ -36,6 +36,12 @@ jobs: with: app-id: ${{ secrets.MARVIN_APP_ID }} private-key: ${{ secrets.MARVIN_APP_PRIVATE_KEY }} + # Match the job's `permissions:` block above. Unscoped, the token + # inherits the App installation's full set — which includes + # contents: write and actions: write, neither of which this job + # declares and both of which end up in the model's shell as GH_TOKEN. + permission-contents: read + permission-issues: write - name: Set dedupe prompt id: dedupe-prompt @@ -114,8 +120,12 @@ jobs: prompt: ${{ steps.dedupe-prompt.outputs.PROMPT }} anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY_FOR_CI }} allowed_non_write_users: "*" + # No `Bash(gh api:*)`: it reaches every endpoint the token can, which + # is the reason marvin-label-triage routes its one write through + # .github/scripts/triage-label.sh instead. Dedupe searches, reads and + # comments — the four verbs below cover that. claude_args: | - --allowedTools "Bash(gh issue view:*)","Bash(gh search:*)","Bash(gh issue list:*)","Bash(gh api:*)","Bash(gh issue comment:*)",Task + --allowedTools "Bash(gh issue view:*)","Bash(gh search:*)","Bash(gh issue list:*)","Bash(gh issue comment:*)",Task settings: | { "model": "claude-sonnet-5", diff --git a/.github/workflows/marvin-label-triage.yml b/.github/workflows/marvin-label-triage.yml index 2cd4fe7f5..b01ad3844 100644 --- a/.github/workflows/marvin-label-triage.yml +++ b/.github/workflows/marvin-label-triage.yml @@ -56,7 +56,14 @@ jobs: with: app-id: ${{ secrets.MARVIN_APP_ID }} private-key: ${{ secrets.MARVIN_APP_PRIVATE_KEY }} - owner: PrefectHQ + # No `owner:` — with it set and `repositories:` empty the token is + # scoped to every repo in the PrefectHQ installation. Triage only + # ever touches this one. The permissions below match the job's + # `permissions:` block; unscoped the token would also carry + # contents: write and actions: write from the App installation. + permission-contents: read + permission-issues: write + permission-pull-requests: write - name: Set triage prompt id: triage-prompt