Uh oh!
There was an error while loading. Please reload this page.
fix(2157): scope the four kanban mints whose reason was "not yet measured" (backend#2157) - #324
Conversation
…ured" (backend#2157) Four of `mint-scope.py`'s ten exemptions said, in as many words, "scopes not yet measured". This measures them and drops the rows. Each scope set is derived from the workflow's OWN calls -- every `gh api`, `gh issue`, and GraphQL query/mutation the App token touches -- and from GitHub's documented per-endpoint requirement, not copied between files: advance-deploy-env issues:read pull-requests:read org-projects:write customer-priority-bump issues:write fr-pass-comment issues:write pull-requests:read org-projects:write kanban-closure-router issues:write pull-requests:read org-projects:write `contents:write` -- the grant that makes a leaked token able to push -- drops from all four, along with administration/actions/checks read. Two derivations are worth naming because they are the ones a reader would guess wrong: * labelling a PULL REQUEST (kanban-closure-router's sibling-merge marker) needs only `issues: write`. The label endpoints document "at least one of" Issues write / Pull requests write, and every shared action on a PR is served by the Issues endpoints. So pull-requests stays at READ. * the issue-comment REACTION endpoint (fr-pass-comment's thumbs) documents `Issues: write` ALONE -- no pull-requests alternative -- which is what forces a write grant in that file rather than a read one. `repositories:` is left unnarrowed everywhere: advance-deploy-env's closing-issue advancement and kanban-closure-router's closer lookup are deliberately cross-repo, and repo narrowing is measured in this org for a READ and not for a WRITE. Six exemptions remain, each for a stated reason rather than for lack of time, and the note above EXEMPT now says which is which. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Uh oh!
There was an error while loading. Please reload this page.
saadqbal
left a comment
There was a problem hiding this comment.
Careful work and the comments are the best part — deriving each scope from the actual call sites,
saying which drop and why, and stating outright that an under-scoped token fails at the call rather
than at the mint. That last paragraph is the right kind of honesty for this class of change.
I verified the one I could verify completely: customer-priority-bump.yml's bump job makes exactly
one call across lines 60-126 — gh issue edit --add-label priority at :107 — sopermission-issues: write is precisely sufficient, not approximate. And EXEMPT is now six rows,
every one carrying a stated reason, none saying "not yet measured", which matches the 10 → 6 in the
description.
Not approving: Bugbot's High on kanban-closure-router.yml is correct and I confirmed it. The
mint grants no contents, but :165 runs branch_status_map.py with $REPO_FULL — the caller
repo — while the only checkout is tracebloc/.github itself. So the .kanban.yml it needs is another
repo's and must be an API read, which needs contents: read.
The comment is where it slipped: it proves contents:write can drop, then concludes nothing reads
content either, in the same sentence that names the read. And persist-credentials: false applies to
the .github checkout, not the repo being read.
Worse, this one does not fail loudly. if ! STATUS=$(...) falls to the holding state by design, so a
403 parks every merged PR at override-unusable with a green job — the exact silent-mis-routing yourNOT PROVEN BY READING note was braced for, except it turned out to be readable from the argument
list.
Detail on the thread. Everything else here I would happily take.
…nts:read stays (backend#2157) Bugbot High, confirmed by saadqbal: the router mint dropped contents entirely while branch_status_map.py fetches the CALLER repo .kanban.yml with that token. The mapper refuses on any non-404 fetch failure and the call sites fall to the holding state, so a 403 would park every merged PR at override-unusable with a green job. advance-deploy-env.yml has the same three call sites and was flagged by nobody; scoping one and not the other would have shipped the same silent mis-routing through the other writer. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
LukasWodka
commented
Aug 24, 2026
bugbot run |
Uh oh!
There was an error while loading. Please reload this page.
saadqbal
left a comment
There was a problem hiding this comment.
My router finding is properly addressed — permission-contents: read at :106, and I re-checked that both mapper calls there (:180, :289) really do run under the App token, so the scope is earned rather than assumed. Good fix.
Still not approving, on Bugbot's new Medium: 8d138e5 carried the same grant into advance-deploy-env.yml, where that workflow's single mapper call runs with github.token in a step before the mint — so contents: read there buys nothing, and the mint comment justifying it contradicts the resolve step's own comment 100 lines above. Evidence on the thread.
Narrow one, and it's the last thing between this and an approval.
…kend#2157) saadqbal on .github#324, and he is right. I added contents:read there by analogy with kanban-closure-router without checking the second instance. advance-deploy-env has ONE mapper call, at :94 in the env step, which runs BEFORE the mint and under github.token -- the comment at :169 already said so. It also maps $GITHUB_REPOSITORY, its own repo, where the router maps $REPO_FULL, the caller. Same script, different token, different repo: the scope is required there and inert here. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
LukasWodka
commented
Aug 24, 2026
bugbot run |
There was a problem hiding this comment.
✅ Bugbot reviewed your changes and found no new issues!
Comment @cursor review or bugbot run to trigger another review on this PR
Reviewed by Cursor Bugbot for commit 8d88c87. Configure here.
LukasWodka
commented
Aug 24, 2026
Both of these landed on @saadqbal's evidence is the sharper version of it, so recording it here: the file already said What I got wrong, precisely: I generalised from a grep that returned three The two workflows call the same script from opposite sides of the mint. Required there, inert On the head now:
The over-reach and the under-reach were both on this PR, which is a fair summary of the change: Re-requesting review — nothing else outstanding, and the |
saadqbal
left a comment
There was a problem hiding this comment.
Verified 8d88c87 — the grant is gone, the router still has its own contents: read, and the new comment is better than what I asked for: it names that this workflow maps $GITHUB_REPOSITORY while the router maps $REPO_FULL, which is the actual reason the need doesn't generalise. Diff is one file, router untouched.
Nothing left from me. Not flipping to approve only because Bugbot's re-review of this commit is still pending — I don't approve ahead of a verdict. It's green everywhere else (12 pass), so as soon as that lands I'll approve on the next pass.
saadqbal
left a comment
There was a problem hiding this comment.
Bugbot's re-review has landed green and the fix is verified, so clearing my change request. Good PR — deriving each scope from the call sites, and the advance-deploy-env comment now explains the asymmetry better than my note did.
Uh oh!
There was an error while loading. Please reload this page.
What this does
scripts/mint-scope.pyhas been armed and green since it landed — with tenexemptions. Four of those rows said, literally, "scopes not yet measured". This
measures them and deletes the rows.
mint-scope.pytreats a stale exemption as a finding, so removing the rows wasrequired, not optional — the guard would have gone red otherwise.
The scopes, and where each came from
Derived per file from every call the App token makes in that workflow — each
gh api, eachgh issue, each GraphQL query and mutation — then mapped againstGitHub's documented per-endpoint requirement. Not copied between files.
advance-deploy-env.ymlissues:readpull-requests:readorganization-projects:writeorganization().projectV2lookup +updateProjectV2ItemFieldValue;pullRequest(n).projectItems;closingIssuesReferences; cross-repoissue(n){state,projectItems}customer-priority-bump.yml(bumpjob)issues:writegh issue edit --add-label priorityfr-pass-comment.ymlissues:writepull-requests:readorganization-projects:writeGET pulls/{n}; board read +updateProjectV2ItemFieldValue;POST issues/comments/{id}/reactions;POST issues/{n}/commentskanban-closure-router.ymlissues:writepull-requests:readorganization-projects:writeissue(n).timelineItemscloser lookup; board read + mutation;gh issue comment; label get/create;POST issues/{n}/labelscontents: write— the grant that lets a leaked token push, and the one theticket calls out as needed by nothing — drops from all four, together with
administration:read,actions:readandchecks:read.Two derivations are worth naming, because they are the two a reader would guess
wrong in opposite directions:
issues: write.kanban-closure-routerputs its
sibling-mergemarker on a PR, and the intuitive answer ispull-requests: write. GitHub documents the label endpoints as "at least oneof" Issues write / Pull requests write, and states that every shared action on
a PR — labels, assignees, milestones — is served by the Issues endpoints. So
pull-requestsstays at read, which is all theprojectItemslookup needs.Issues: writealone — nopull-requests alternative. That single call is what forces a write grant in
fr-pass-comment.ymlrather than a read one, and it is why that file cannot bereduced further.
repositories:is deliberately left unnarrowed on all four.advance-deploy-env'sclosing-issue advancement and
kanban-closure-router's closer lookup arecross-repo by design (a client or website PR routinely closes a
backendissue), and repo narrowing is measured in this org for a READ
(
add-to-kanban.yml, backend#2181) and not for a WRITE.What is NOT proven here — please read this bit
Static reading cannot show these scopes are sufficient. Every one of them is
what the call sites and the docs say is needed; none has been exercised by a run.
An under-scoped token does not fail at mint time — it fails at the call, and
each of these workflows fails in a way that is quiet:
advance-deploy-env/kanban-closure-router→ a card left behind its ownshipped code, and for the router the built-in "Item closed" automation then sets
Cancelled(.github#157).fr-pass-commenthas a worse-than-red mode worth watching: if thecollaborator-permission read fails, the step warns and falls back to
author_association, a looser authorisation test. IfCould not read repository permissionstarts appearing in those runs, treat itas this PR's regression, not as noise.
The first real exercise is the next board write — the next merge to a tracked
branch, the next closed PR, the next
/fr-pass. If one reddens, read the failingcall and narrow the fix; do not widen the list back to a full grant.
The six left, deliberately
Not "the rest of the same job" — each is left for a stated reason, and the note
above
EXEMPTnow records the split:fr-gate.yml,set-pr-status.yml,standards-sync.yml,kanban-reconcile.yml— high exposure (a required check fleet-wide, a caller in every repo, a
contents:writesweep, the widest board surface). Each needs its own window soone bad scope cannot redden the whole fleet at once.
bricked-prs.yml,merge-settings-drift.yml— branch-protection reads, where anarrower token silently returns less data rather than erroring. That failure
mode is invisible to a green run, so it needs a measurement, not a derivation.
add-to-kanban.ymlis untouched: it is already scoped, it is the template thesefour follow, and it is the byte-compared per-repo copy — nothing here needs a
fleet sweep, since all four files above are single-copy reusables.
repo-inventory.ymlis untouched.Verification
make mutationswas run in full (not just the--drypasscheckdoes) becausethis touches
customer-priority-bump.yml, which carries a mutation target. Themutation "the new mint drops its scopes and takes the App's full grant" still
catches, and it is not made vacuous by the new scopes: it resolves the mint step
inside
_bug_jobspecifically, so thebumpjob's newpermission-issues: writecannot mask a removal from the
bug-to-readymint.Refs tracebloc/backend#2157
Note
Medium Risk
Hardens token blast radius, but under-scoped tokens fail at call time, not mint time, and can silently stall kanban routing or loosen
/fr-passauth until the first live board write.Overview
Narrows GitHub App installation tokens on four kanban reusables so they no longer inherit the App’s full grant (
contents:writeincluded). Scopes are derived from each job’s actual API calls.advance-deploy-env.yml: issues/PR read + org-projects write (no contents)customer-priority-bump.ymlbump job: issues write onlyfr-pass-comment.yml: issues write, PR read, org-projects writekanban-closure-router.yml: contents read (fetches caller.kanban.ymlover the API), issues write, PR read, org-projects writemint-scope.pydrops those four “scopes not yet measured” exemptions (10 → 6). Repo lists stay unnarrowed for cross-repo board/issue lookups. Sufficiency is not proven at mint time; a too-tight token fails at the call and can leave cards unmoved (or, on/fr-pass, fall back to a looser auth check).Reviewed by Cursor Bugbot for commit 8d88c87. Bugbot is set up for automated code reviews on this repo. Configure here.