Uh oh!
There was an error while loading. Please reload this page.
sec(mint-scope): the board-writing mint asks for a board write, not everything - #349
Conversation
backend#2157, ONE OF FOUR -- this does not close it.
`set-pr-status.yml`s first mint passed `owner:` with no `permission-*`
inputs, so it minted the App ENTIRE permission set -- contents:write,
issues:write, administration:read and the rest -- across every repo the
installation covers, on every PR event in the fleet. Largest exposure of
the four purely because of how often it runs.
SCOPES DERIVED FROM THE CALLS, not copied from a template:
organization.projectV2 { fields } -> projects, read
repository.pullRequest.projectItems -> pull-requests, read
updateProjectV2ItemFieldValue -> projects, WRITE
No `issues` scope: this job is PR-only. The SECOND mint in this same file
already carried `permission-pull-requests: read` + `permission-issues:
read` -- the closing-ref job reads the linked issue. Worth naming because
the exemption is per FILE, so `set-pr-status.yml` read as wholly unscoped
while half of it was already done.
Exemption removed. The guard now reports 3 unscoped / 3 exempted / 0
findings, down from 4 and 4.
A PASSING MINT IS NOT THE PROOF, and the comment says so: release-train
add-to-kanban.yml records that "a mint can succeed with a scope the board
write then needs and lacks". The evidence is a card that actually moves,
which is why this lands alone rather than with the other three.
Verified: make lint clean, selftests 28 passed, actionlint clean.
Part of tracebloc/backend#2157
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>LukasWodka
commented
Aug 26, 2026
Correcting a claim I made in the original body before anyone acts on it. I wrote that this PR's own card reaching so this card — and every card until the train promotes The first real exercise is the first PR opened anywhere in the fleet after that promotion. If its card does not reach |
aptracebloc
left a comment
There was a problem hiding this comment.
Reviewed (high effort — security scope change). Correct and safe. The mint now requests exactly pull-requests: read + organization-projects: write, which matches the set-status job's actual API calls — read the org ProjectV2 fields, read the PR's project items, write the Status field — and nothing more (no issues / contents / administration). organization-projects is the right ProjectsV2 permission and a strict subset of the canonical add-to-kanban scope (this even drops issues:read). And it's proven at runtime: the set-status / set-status job is green on this head, i.e. the board write actually ran under the narrowed token — not just a successful mint. The EXEMPT removal is required (both mints in the file are now scoped).
Approving.
Two non-blocking follow-ups — ironic in a "don't leave restated claims that drift" PR, so worth a quick fix (ideally here): the comment in set-pr-status.yml (~262–265) still calls the board-write mint "unscoped" and says this file "keeps its mint-scope exemption" — both now false; and mint-scope.py prose (~94, ~120–124) still says "six became four" and lists set-pr-status among the exempt four, but EXEMPT now holds three. The guard behavior is correct (dict-driven) — it's only the prose that drifted.
— drafted with Claude Code
Uh oh!
There was an error while loading. Please reload this page.
backend#2157. `set-pr-status.yml` landed on develop (#349) and dropped its own EXEMPT row; this branch drops `kanban-reconcile.yml`'s. Git saw one line changed twice and called it a conflict -- both deletions are correct, so the resolution keeps NEITHER row. EXEMPT is now fr-gate + standards-sync, and the prose above it is updated to say two rather than four. It already said four on develop while three remained, so this fixes a drift it did not introduce -- in the commit that would otherwise have made it twice wrong. Verified: make lint exit 0 (mint-scope 2 unscoped / 2 exempted / 0 findings, "exemptions all still apply"), make selftests exit 0. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
backend#2157. `set-pr-status.yml` landed on develop (#349) and dropped its own EXEMPT row; this branch drops `standards-sync.yml`'s. Adjacent line deletions, so git called it a conflict -- both are correct and the resolution keeps NEITHER row. EXEMPT is now fr-gate + kanban-reconcile, and the prose above it is updated to say two rather than four. It already said four on develop while three remained; this fixes that drift rather than adding a second wrong number. Verified: make lint exit 0 (mint-scope 2 unscoped / 2 exempted / 0 findings, "exemptions all still apply"), make selftests exit 0. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
backend#2157, LAST OF FOUR. With #349, #352 and #353 this empties EXEMPT. Scoping this to the UNION would have kept fleet-wide contents:WRITE on every scheduled report -- which is the mismatch, not the fix, since `report` mode only reads. So the contents scope is conditional on the mode: permission-contents: ${{ (dispatch && create-prs == true) && write || read }} On a schedule run `inputs` is empty, the condition is false, and the same job gets READ. DERIVED FROM THE CALLS THIS TOKEN MAKES, checked one at a time: git/ref/heads/develop, repos/{r}, contents/CLAUDE.md?ref= -> contents: read PUT contents/CLAUDE.md, POST git/refs (create-prs only) -> contents: WRITE gh pr list -> pull-requests: read gh issue comment 1602 (the drift report) -> issues: WRITE NOT pull-requests:write: `pr create` and both `pr edit` calls run on SYNC_PR_AUTHOR_TOKEN, not on this token (backend#2590). The App only LISTS. TWO CORRECTIONS TO WHAT I SAID EARLIER ON THE TICKET, both from reading the file rather than the summary: * The minting job is gated on `schedule || workflow_dispatch`, so it never minted on push/pull_request/issues. The exposure was real but narrower than "nearly every run". * `== true` rather than a bare truthiness test, matching the CREATE_PRS line below it. Equivalent for a `type: boolean` input -- but if it ever became a string, `false` is TRUTHY and this would mint write on every scheduled run while still looking conditional. Verified: make lint clean, selftests 29 passed, actionlint clean, mint-scope 3 unscoped / 3 exempted / 0 findings on this branch (the other three are on their own branches; together they reach 0 and 0). Part of tracebloc/backend#2157 -- the LAST of four. Merge order is not guaranteed, so this does not carry a closing keyword: if it landed first it would close the ticket with three mints still unscoped. Close #2157 by hand once #349, #352, #353 and this one are all in. Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
actions/create-github-app-tokenwith nopermission-*inputs mints the App's fullinstallation grant. For
tracebloc-release-trainthat is contents + pull-requests +issues + organization-projects write across every installed repo — and the App holds
bypass_reviewson staging and prod fleet-wide, so the blast radius of a misused token ismerge past review, not merely write.
set-pr-status.ymlonly moves a card between board columns. It now asks for exactly that:The guard is the point, not the one-line fix
Nine workflows acquired a full-permission mint in a single day because the first one did
and the rest were copied from it. Least privilege applied by hand is a state;
scripts/mint-scope.pyis what makes it a property — without a check, the twentiethmint arrives the way the first nineteen did.
It is written to the house rules rather than around them:
found by parsing YAML and matching the
uses:value, not by grepping names typed intothe checker. Add a workflow and it is covered; rename one and nothing silently stops
being checked.
found are all findings. Zero is the important one: the premise is that mints exist, so
finding none means the matcher broke, not that the fleet got clean.
finding, so it cannot become quiet cover for the next unscoped mint in that file.
Current state on this branch: 17 mint steps across 37 workflows — 14 scoped, 3 exempted
with stated reasons (
fr-gate,kanban-reconcile,standards-sync), 0 findings.This does NOT close backend#2157 — and I found why while checking
I was ready to add a closing keyword, because a green
mint-scopeover this repo readslike piece 1 of #2157 being finished. It isn't.
release-trainhas three unscopedmints —
promote.yml(×2) andhotfix.yml— filed as backend#2599.mint-scope.pycannot see them: it scans this repo only, on the argument that "thereusables live here, and the one per-repo copy is compared byte-for-byte … so a scoped
canonical here means scoped everywhere." That holds for reusables and copies. It does not
hold for
release-train, whose prod-merging workflows are first-party and neither. So thedocstring's "the whole surface rather than a convenient subset" is false, in the section
headed
DERIVED, NEVER RESTATED.Worth deciding in #2599 whether
mint-scope.pywidens past its own repo or narrows itsclaim to match its reach — the current pairing, widest claim over narrowest scan, is the
one combination that isn't defensible.
Part of tracebloc/backend#2157. Follow-up: tracebloc/backend#2599.