Uh oh!
There was an error while loading. Please reload this page.
fix(ci): the required SET is readable — retire the 403 premise and diff the live ruleset - #9679
Conversation
…ff the live ruleset `check-required-contexts.mjs` reasoned from "no agent seat can read the ruleset", citing a 403 from `GET /repos/objectstack-ai/objectstack/branches/main/protection`. That is the CLASSIC branch-protection endpoint — GitHub prices it at `administration=read` (its own `X-Accepted-GitHub-Permissions` header), which is not among the 17 permissions a workflow may grant a `GITHUB_TOKEN` — and it is not where this repository's configuration lives. `main` is governed by a repository RULESET, and the ruleset endpoints answer 200 from an ordinary agent seat at `metadata=read`. - Corrects the premise in all three sites the card names (the script header, the failure prescription, the lint.yml step comment) and a fourth the sweep found (`check-partof-closing-keyword.mjs`), naming BOTH endpoints and which one this repo uses so the next reader cannot test the wrong URL and conclude the comment is wrong. - Adds `--verify-required-set`: reads every ruleset that applies to the repo (including inherited org rulesets), and diffs the live required set against REQUIRED_CONTEXTS in BOTH directions — a registry row that is not required (the family is advisory today, #5617's unsignalled half) and a required context no row pins (a rename detaches it silently). - Report-only, and OFF the required path by construction: the settings half of any required-set change is maintainer-only and lands AFTER the merge, so a merge-blocking version would be red on the very PR carrying the repo half. Posture is `check-governed-merges.mjs`'s — a completed sweep exits 0 whether it found 0 or 40 disagreements; a non-zero exit classifies the ENVIRONMENT, and unreachable prints NOT VERIFIED, never a pass (#4690). - The self-test (offline) covers both directions, `evaluate`-mode rulesets, ref-scope, the unprotected reading, the NOT-VERIFIED renderer, and pins that the pin stays network-free and that nothing wires the live read into a job publishing a required context. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XqDQYVU5smx29ts9pAErja
✅ PM ACCEPT — #9642 / PR #9679Verified independently: 3 files ⭐ H1 — you found the decisive argument, and it is not one I asked aboutI framed H1 as three risks: token scope, fork PRs, outage posture. You answered the token half cleanly, then produced a structural reason that outranks all three:
That is the right answer and I would not have got there. My worry was "the gate might be flaky or unauthorised"; the real problem is that the gate would be correctly red at exactly the moment it must be green, forever, by construction of the workflow it polices. No amount of permissions or retry logic touches it. The token half is also better evidence than I expected — not "it worked for me", but GitHub's own Result: prose + capability, not prose + required gate. Report-only ⭐ And the three self-test assertions are the best design decision herePinning that the check stays network-free, and that no workflow step and no
You did not just decline to build the deadlock — you made building it by accident impossible, and left the deliberate route open with a speed bump that explains itself. That is a materially better outcome than a comment saying "don't do this." Both ablations for it landed red as predicted: the live read added as a step in the required lint job → red; added as a H2 — both directions, and the justification is the reason to accept it
A (registry row not in the live set) = that family is silently advisory, #5617's unsignalled half, maintainer's remedy. B (live context with no row) = a required context whose The edge cases are the part that makes it trustworthy: H3 — confirmed from your own read, and #9533 answered without touching itOne ruleset H4 — four sites in three files, not three in twoThe fourth ( And the negative result matters as much:
|
Uh oh!
There was an error while loading. Please reload this page.
Fixes#9642
check-required-contexts.mjstold every reader that the required SET is unreadable, citing a 403 fromGET /repos/objectstack-ai/objectstack/branches/main/protection. That endpoint is real and it does answer 403 — it is just not the one holding this repository's configuration.mainis governed by a repository ruleset, and the ruleset endpoints answer 200 from an ordinary agent seat.H1 — can the live read be made safe in CI? Yes for the token; no for merge-blocking, and the second reason is the decisive one
Token/permissions: the read needs no grant this repo does not already have. GitHub prices each endpoint in its own response header:
X-Accepted-GitHub-Permissions.../branches/main/protection(classic branch protection)Resource not accessible by integrationadministration=read.../rulesetsand.../rulesets/{id}metadata=readadministrationis not among the 17 permissions a workflow may grant itsGITHUB_TOKEN(SchemaStore Actions workflow schema,permissions-eventproperty set:actions, artifact-metadata, attestations, checks, code-quality, contents, deployments, discussions, id-token, issues, models, packages, pages, pull-requests, repository-projects, security-events, statuses) — so the classic endpoint is unreachable from CI by construction, not by configuration.metadatais absent from that same list because it is the baseline read a token cannot give up.lint.yml'slintjob declarespermissions: contents: read; the ruleset read would need no addition to it and no PAT.Fork PRs / outage: made irrelevant rather than argued about. A fork PR's token is read-only but still carries
metadata, and rate limits and 5xx exist regardless. Rather than reason about how often that bites, the mode is report-only and unreachable printsNOT VERIFIEDwith exit 2 — thecheck-governed-merges.mjsposture, quoted in its header: "a completed sweep exits 0 whether it found 0 or 40 entries; non-zero exits classify the ENVIRONMENT, not the tree."The decisive reason the diff is not a required gate is structural, not flakiness. The settings half of any required-set change is maintainer-only and lands after the merge — the #9325 two-step, documented at length in this very file. A required check that reddened on registry-vs-settings disagreement would therefore be red on precisely the PR carrying the repo half, and could never go green before merging. It would deadlock the sitting it claims to protect, exactly as required-izing
Console Pin Freshnessdeadlocks the queue. That holds no matter how good the token is, so "make it required later once the token is proven" is not a deferred option — it is a shape that cannot work here.Consequence, stated plainly: this PR is prose + capability, not prose + required gate. The pin proper stays entirely network-free, and a self-test assertion now enforces that (
the pin (main) stays network-free), as do two more asserting that no workflow step and no package script runs the live read.H2 — both directions, because they are different defects with different owners
name:literal nothing pins; renaming that job detaches its gate silentlyREQUIRED_CONTEXTSrow with its authorizing rulingBoth are reported; neither is judged. Reporting only A would have missed the shape this whole file exists to prevent; reporting only B would have missed the incident that caused the file to be written.
Direction A fires today, which is the point:
Build DocsandConsole Pin Gateare registered and are not required. The report names #9533 as the open decision and says out loud that it decides nothing.Two shapes the diff refuses to get wrong, both self-tested: a ruleset in
evaluate(dry-run) ordisabledenforcement contributes a shadow set and never counts as required — counting it would manufacture the exact false green this file prevents — and a ruleset whoseconditions.ref_namedoes not cover the default branch is ignored. Org rulesets inherited from above are read (includes_parents=true); a failure to read any listed ruleset is an environment failure, never a partial reading, because "some of the rulesets" cannot answer "is this context required".H3 — confirmed independently; measurement posted on the card
Re-measured from this container with its own token, not inherited from the dispatch: one ruleset,
main, id12119582, active,target: branch, repository-sourced,conditions.ref_name.include: ["~DEFAULT_BRANCH"],strict_required_status_checks_policy: false, six required contexts,Build DocsandConsole Pin Gateabsent.includes_parents=truereturns that same single entry, so no organization ruleset is being missed.That card's first reading is therefore the true one — the maintainer's six-item reading was complete. The measurement plus the newly-answered token caveat are posted there as a comment. Its labels were not touched and its disposition question stays open for the maintainer: #9533 is not addressed here.
H4 — the sweep: four sites in three files, and two governed-surface items for a maintainer
Corrected in this PR:
scripts/check-required-contexts.mjsheader — the "What this pin does NOT assert" section.scripts/check-required-contexts.mjsfailure prescription (the text printed when the pin fails)..github/workflows/lint.yml— theRequired-context name pinstep comment.scripts/check-partof-closing-keyword.mjsheader — not named by the card, found by the sweep. It read: "whether that check run becomes a REQUIRED context is a settings change no agent seat can make (the protection endpoint answers 403 here)". Its conclusion is correct (a seat cannot write the settings —GET /repos/objectstack-ai/objectstackreportspermissions.admin: falsefor this seat) but the cited evidence is the same conflation. Taken under the bounded in-place exemption: same defect class, mechanical, comment-only, no new verification surface, same gate family (pnpm check:partof-closing-keyword, green), and no open PR claims that file. The correction narrows the claim to not-writable and names the right endpoint.Governed surfaces — listed for a maintainer edit, not touched:
AGENTS.md(around line 347) states "the queue enforces only the required set — Lint & Repo Gates ... and TypeScript Type Check block ...; everything else is advisory and rides through". The live ruleset requires six contexts, so four of them are described to every seat as advisory when they block. Filed unassigned as AGENTS.md names two blocking contexts and calls the rest advisory — the live ruleset requires six #9677..claude/skills/pm-dispatch/**. Filed unassigned as finding: the live required-set diff shipped by #9642 has no standing caller — it needs a PM round-report slot (governed surface) #9678.Swept clean: no file under
.claude/**orskills/**carries the premise. Outside the repo it survives in issue bodies (the #9325 rename procedure, #9533's own body), which are point-in-time records and are not retro-edited.H5 — the prose names both endpoints, so the next reader cannot repeat the error
Every corrected site now names the classic endpoint and the ruleset endpoints and says which one this repository uses. A reader who doubts the comment and tests a URL will test the right one.
One trap the header records because it re-creates this card's defect from a different cause: in an agent container all GitHub traffic goes through the session proxy, and Node's global fetch does not read
HTTPS_PROXYon its own. WithoutNODE_OPTIONS=--use-env-proxythe read answers 401, and a reader who stops there concludes "unreadable from this seat" — the exact false inference #9642 retired. TheNOT VERIFIEDoutput now detects that combination and says so.check-governed-merges.mjsandscripts/pm/check-half-states.mjsreach the API the same way and share the trap (#7412).Verification
All at
7ab79908d(the final commit), re-run after it:Derived with
node scripts/pm/dispatch-gates.mjson the three changed paths; that derivation also namescheck:type-check-debt, which cannot run locally — it refuses without the whole workspacedist/closure built (--re-measure cannot run: 55 workspace dependenc(ies) ... have no built type entry point on disk). It matched only because a workflow file changed, its inputs are TypeScript packages, and this diff touches no TypeScript; CI runs it with the closure built.Live run of the new mode, against the real ruleset:
Reverse verification — six ablations, each reverted from the committed state, each red in the predicted direction:
advisory: [])evaluate-mode rulesets counted as enforcingawait fetch(...)inmain)lintjobcheck:*package scriptThe last two are the assertions that keep this trade honest: they make wiring the live read onto the required path a maintainer's deliberate act that goes red here first, rather than something a later edit does silently.
Not shipped
scripts/and.github/publish nothing;skip-changesetlabel applied.content/docs/releases/**,docs/adr/**,.claude/**,skills/**,AGENTS.md,CLAUDE.md: untouched.Note for the reviewer
Open PR #9645 also edits
.github/workflows/lint.yml(a different job region). No overlap with the comment block edited here, but the two will need the usual ordering if both land close together.Generated by Claude Code