Repository navigation
Description
Finding
The permissions policy PR #1594 added to check:workflows (for #1503) validates only the top-level permissions: block. No job-level permissions: block ever has its content checked against the policy — and reusable-workflow caller jobs are not even gated on having a block:
- In
script/check-workflows.ts(current maine28985a80863),workflowViolationsvalidates scope keys (PERMISSION_SCOPES), levels (PERMISSION_LEVELS), the scalar-shorthand ban, and the write-only-on-contentsrule only inside the top-level branch (lines ~210–258). - Caller jobs: lines 267–277 — a job with
uses:is checked only for its key names againstREUSABLE_WORKFLOW_CALLER_KEYS(which includespermissions) and thencontinues, before any permissions handling. The comment at lines 265–266 says a caller job's block "still counts as using a baseline exception," but nothing checks that the job is inJOB_PERMISSION_EXCEPTIONS— any caller job may declare anypermissions:content, includingwrite-allorissues: write, andcheck:workflowsreports no violation. - Non-caller jobs: the block at lines ~279–288 checks only presence against
JOB_PERMISSION_EXCEPTIONS. A baselined exception job (e.g. the 13 entries inJOB_PERMISSION_EXCEPTIONS) may change its block to any scopes/levels — scalarwrite-allincluded — with no violation, even though the top-level branch explicitly calls the scalar shorthand a grant that "never satisfies the least-privilege posture."
Evidence
- Source PR: feat(ci): add a permissions policy to check:workflows (#1503) #1594 (merged 2026-11-04T07:19:40Z, merge commit
53f06c2ead835, commits006592843a06,70bd31b26c3f) — its body states the policy as "every workflow must declare a top-levelpermissions:mapping in the least-privilege posture" with scope/level/write rules; the job-level content path was not implemented. - Current main verification (this run): re-read
script/check-workflows.tsat maine28985a80863twice — the caller branch stillcontinues at line 277 ahead of theno-job-level-permissionscheck, and no code path calls the scope/level/write validation on a job-level value.PERMISSION_SCOPES/PERMISSION_LEVELS/WRITE_PERMITTED_SCOPESare referenced only for the top-level block. - Live posture today is conformant by inspection (e.g.
agents.yml's job-level block mirrors its top level), so this is a gate hole, not a live over-grant: the next caller or exception job that widens its block is invisible to the gate fix(ci): check:workflows does not require permissions: — the #118 GITHUB_TOKEN fallback class has no regression net #1503 created to catch exactly this class (CI: six workflows across opencode + openchamber declare no permissions — GITHUB_TOKEN falls back to repo defaults (set) #118: GITHUB_TOKEN falling back to broader grants than intended). - Dedupe: searched open + closed issues for
caller permissions,job-level permissions,check-workflows(2026-11-05). fix(ci): check:workflows does not require permissions: — the #118 GITHUB_TOKEN fallback class has no regression net #1503 (closed) is the parent policy; open PR fix(ci): require permissions on every non-reusable workflow job #1504 is the superseded weaker variant whose closure is tracked by open chore(ci): close superseded open PRs #1497, #1566, #1504 — work landed via #1576 and #1594 #1654 — neither covers validating job-level content in the landed policy. chore(ci): close superseded open PRs #1497, #1566, #1504 — work landed via #1576 and #1594 #1654 does not mention this gap.
Why it matters
The #1503 policy's value is that a broader-than-intended GITHUB_TOKEN grant cannot land silently. At job level it still can: job-level blocks replace the top-level grant wholesale for that job (the PR's own words), so the unchecked path is precisely the one that determines the effective grant for caller jobs and for the sync/gate jobs that hold today's issues/pull-requests/id-token write exceptions. A one-line edit widening any of those blocks — or a new caller job declaring permissions: write-all — passes bun run check:workflows clean.
Acceptance criteria
-
workflowViolationsapplies the same content validation to every job-levelpermissions:value it permits (caller jobs andJOB_PERMISSION_EXCEPTIONSjobs): mapping form only (nowrite-all/read-allscalar), scope keys inPERMISSION_SCOPES, levels inPERMISSION_LEVELS. - Job-level
writegrants beyondcontentsrequire a named baseline: either the existing exception maps gain the granted scopes per job, or an equivalent ratcheted record — an unbaselined job-level write grant is a violation, and a baselined grant that disappears is a stale-exception violation, mirroring the top-level rules. - A caller job declaring
permissions:without a baseline entry is a violation (per the line 265–266 comment's stated intent), or the comment and policy text are corrected to say caller blocks are intentionally unbaselined — one of the two, not the current mismatch. - Tests in
script/check-workflows.test.tscover: caller job withpermissions: write-allflagged; caller job with an unbaselinedissues: writeflagged; excepted job whose block widens beyond its baseline flagged; today's real workflows still passcollectViolationswith no new violations.
Found by pr-diff-review lane B while reviewing PRs #1566–#1596 (2026-11-05 run), PR #1594 in particular.
Activity
developing-today commentedon Nov 5, 2026
🤖 Agent note · filed
check:workflowspermissions policy validates only the top-level block — on current maine28985a80863,script/check-workflows.tscaller jobscontinueat line 277 before any permissions handling (their blocks are never content-checked and never checked againstJOB_PERMISSION_EXCEPTIONS, despite the line 265–266 comment), and baselined exception jobs are presence-checked only, so a job-levelpermissions: write-allor an unbaselined write grant passes the gate. Filed via the developing.today GitHub App (fleet-paperwork) after the user token hit the secondary content-creation rate limit (403 at 2026-11-05T17:48Z); verified by independent REST readback.pr-review-b-2026-11-05-1118-cst/issue-caller-permissions-body.mdin the goal's hidden_files; queue packetpacket-opencode-1759.jsondezren39 commentedon Nov 5, 2026
Agent note - handed off
ses_e5e5305dd36ffeZw1YP1MbM7Lz