Skip to content

fix(2157): scope the four kanban mints whose reason was "not yet measured" (backend#2157) - #324

Merged
LukasWodka merged 3 commits into
developfrom
fix/2157-scope-kanban-mints
Aug 24, 2026
Merged

fix(2157): scope the four kanban mints whose reason was "not yet measured" (backend#2157)#324
LukasWodka merged 3 commits into
developfrom
fix/2157-scope-kanban-mints

Conversation

@LukasWodka

@LukasWodkaLukasWodka commented Aug 24, 2026

Copy link
Copy Markdown
Contributor

What this does

scripts/mint-scope.py has been armed and green since it landed — with ten
exemptions. Four of those rows said, literally, "scopes not yet measured". This
measures them and deletes the rows.

before: 16 mint step(s) across 36 workflow file(s) / 10 unscoped, 10 exempted, 0 finding(s)
after: 16 mint step(s) across 36 workflow file(s) / 6 unscoped, 6 exempted, 0 finding(s)

mint-scope.py treats a stale exemption as a finding, so removing the rows was
required, 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, each gh issue, each GraphQL query and mutation — then mapped against
GitHub's documented per-endpoint requirement. Not copied between files.

workflowscopesthe calls that force them
advance-deploy-env.ymlissues:readpull-requests:readorganization-projects:writeorganization().projectV2 lookup + updateProjectV2ItemFieldValue; pullRequest(n).projectItems; closingIssuesReferences; cross-repo issue(n){state,projectItems}
customer-priority-bump.yml (bump job)issues:writeone call: gh issue edit --add-label priority
fr-pass-comment.ymlissues:writepull-requests:readorganization-projects:writecollaborator-permission read (Metadata, implicit); GET pulls/{n}; board read + updateProjectV2ItemFieldValue; POST issues/comments/{id}/reactions; POST issues/{n}/comments
kanban-closure-router.ymlissues:writepull-requests:readorganization-projects:writeissue(n).timelineItems closer lookup; board read + mutation; gh issue comment; label get/create; POST issues/{n}/labels

contents: write — the grant that lets a leaked token push, and the one the
ticket calls out as needed by nothing — drops from all four, together with
administration:read, actions:read and checks:read.

Two derivations are worth naming, because they are the two a reader would guess
wrong in opposite directions:

  • Labelling a pull request needs only issues: write.kanban-closure-router
    puts its sibling-merge marker on a PR, and the intuitive answer is
    pull-requests: write. GitHub documents the label endpoints as "at least one
    of"
    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-requests stays at read, which is all the projectItems lookup needs.
  • The issue-comment reaction endpoint documents Issues: write alone — no
    pull-requests alternative. That single call is what forces a write grant in
    fr-pass-comment.yml rather than a read one, and it is why that file cannot be
    reduced further.

repositories: is deliberately left unnarrowed on all four. advance-deploy-env's
closing-issue advancement and kanban-closure-router's closer lookup are
cross-repo by design (a client or website PR routinely closes a backend
issue), 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 own
    shipped code, and for the router the built-in "Item closed" automation then sets
    Cancelled (.github#157).
  • fr-pass-comment has a worse-than-red mode worth watching: if the
    collaborator-permission read fails, the step warns and falls back to
    author_association, a looser authorisation test. If
    Could not read repository permission starts appearing in those runs, treat it
    as 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 failing
call 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 EXEMPT now 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:write sweep, the widest board surface). Each needs its own window so
    one bad scope cannot redden the whole fleet at once.
  • bricked-prs.yml, merge-settings-drift.yml — branch-protection reads, where a
    narrower 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.yml is untouched: it is already scoped, it is the template these
four 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.yml is untouched.

Verification

$ python3 scripts/mint-scope.py
mint-scope: 16 `actions/create-github-app-token` step(s) across 36 workflow file(s)
6 unscoped, 6 exempted, 0 finding(s)
no findings (exemptions all still apply)
exit=0
$ python3 scripts/tests/mint-scope-selftest.py
13 passed, 0 failed
$ make check
house-rules: no findings across 7 file(s).
action-pins: 36 file(s) scanned, 0 finding(s)
actionlint: 0 findings
bug-to-ready-selftest: 51 passed, 0 failed
==> check: green
$ make mutations
23 mutation(s): 0 stale, 0 uncaught
$ actionlint <the four edited workflows> # 0 findings
$ python3 -c "import yaml,sys; yaml.safe_load(...)" # all four parse

make mutations was run in full (not just the --dry pass check does) because
this touches customer-priority-bump.yml, which carries a mutation target. The
mutation "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_job specifically, so the bump job's new permission-issues: write
cannot mask a removal from the bug-to-ready mint.

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-pass auth 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:write included). 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.yml bump job: issues write only
  • fr-pass-comment.yml: issues write, PR read, org-projects write
  • kanban-closure-router.yml: contents read (fetches caller .kanban.yml over the API), issues write, PR read, org-projects write

mint-scope.py drops 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.

…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>
@LukasWodkaLukasWodka self-assigned this Aug 24, 2026
Comment thread.github/workflows/kanban-closure-router.yml

@saadqbalsaadqbal left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 — so
permission-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 your
NOT 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

Copy link
Copy Markdown
ContributorAuthor

bugbot run

Comment thread.github/workflows/advance-deploy-env.yml Outdated

@saadqbalsaadqbal left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Copy Markdown
ContributorAuthor

bugbot run

@cursorcursorBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ 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

Copy link
Copy Markdown
ContributorAuthor

Both of these landed on 8d138e5, and they're already fixed on the head8d88c87, pushed
before this review came in. Flagging rather than re-fixing, since the finding is correct and the
remedy is the one you both describe.

@saadqbal's evidence is the sharper version of it, so recording it here: the file already said
the opposite of itself.
The resolve step's comment at :84-86 reads "The workflow token, not
the App token: this reads .kanban.yml from the caller's own repo"
, and my mint comment then
claimed the mapper fetches it "with this token" and granted contents: read on that basis.

What I got wrong, precisely: I generalised from a grep that returned threebranch_status_map
matches in advance-deploy-env.yml. Two are comments (:25, :32) and the third was the comment
I had just written (:187). One real call, :94, inside the resolve step at :80 under
GH_TOKEN: github.token — which runs before the mint at :148, so the App token cannot be what
it uses. In the router it is the other way round: both calls (:180, :289) sit in
Determine target Status at :111 under GH_TOKEN: steps.app-token.outputs.token.

The two workflows call the same script from opposite sides of the mint. Required there, inert
here.

On the head now:

  • permission-contents: read appears once in the entire diff, in the kanban-closure-router.yml
    hunk — verified against gh pr diff, not against my memory of what I pushed
  • the stale comment is gone and replaced by the distinction above, since as @saadqbal says that is
    the more expensive half: it is the reasoning the next person reuses
  • mint-scope 6 exempted / 0 findings · selftest 13/13 · actionlint clean · all checks pass

The over-reach and the under-reach were both on this PR, which is a fair summary of the change:
Bugbot found a scope I dropped that was needed, and then a scope I added that wasn't.

Re-requesting review — nothing else outstanding, and the CHANGES_REQUESTED is the only thing
between this and green.

@saadqbalsaadqbal left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@saadqbalsaadqbal left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@LukasWodka
LukasWodka merged commit 0f4c3f6 into developAug 24, 2026
14 checks passed
@LukasWodka
LukasWodka deleted the fix/2157-scope-kanban-mints branch August 24, 2026 13:47
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@LukasWodka@saadqbal