Uh oh!
There was an error while loading. Please reload this page.
fix(pm): give check-half-states' transport probe a repo-scoped second stage - #9963
Merged
Merged
Conversation
… stage The probe reported "prerequisite met" in a container where every repo-scoped endpoint 403s, then the sweep it green-lit failed on its first listing page — the #4690 inversion inside the mechanism built to prevent it. Measured 2026-08-19: the obvious hypothesis (the proxy fabricates a /rate_limit quota) is wrong. /rate_limit there carries server: github.com, a real x-github-request-id and a real 15000-limit quota, and GET /user returns the real login — the proxy substitutes a working credential. What it intercepts is the repo-scoped reads. So the account-scoped observation is genuinely healthy and identical to the Routine runner's, and only a repo-scoped read separates them. Stage 2 is GET /repos/{owner}/{repo}, fired only when stage 1 already said reachable — so the three failing container classes cost exactly what they did before, and only the previously-green path spends one core request more. The match is a structural contradiction (the quota endpoint reports thousands of core requests left, and a core request was just refused), carrying no vendor string and no token-shape test — both directions were considered and refused on the card. Self-test 273 -> 292 cases; no existing case rewritten. Report-only contract, exit codes and printed verdict lines all unchanged. Fixes#9946 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AeA3nU1B5Q2pgxqxgUrexd
os-warren
marked this pull request as ready for review
August 19, 2026 10:33
os-warren
enabled auto-merge
August 19, 2026 10:33
Uh oh!
There was an error while loading. Please reload this page.
This was referenced Aug 19, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for freeto join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes#9946
The
--probeflag exists so a seat can answer "can live mode run HERE?" before spending a round. In a proxy-mediated cloud container it answered prerequisite met and the sweep it green-lit then failed on its first listing page — the #4690 inversion (a value that cannot be read must surface) landing inside the mechanism built to enforce it.The card's stated mechanism is partly refuted — measured before fixing
The card diagnosed this as "the agent proxy answers
/rate_limitITSELF with a fabricated quota". That is wrong, and the correction is what determined the fix. Measured live in this container, 2026-08-19:GitHub really did answer
/rate_limit, the quota is real, and the placeholder token is substituted by the proxy for a working credential —/userreturns the real login. What the proxy intercepts is the other side: the repo-scoped reads.Two consequences:
/rate_limitcould ever classify this container. Its account-scoped observation is genuinely healthy and byte-for-byte indistinguishable from the healthy Routine runner's.GET /useris a real endpoint and answers 200 here, so it would have green-lit this container too. The discriminator is the kind of endpoint the sweep actually needs: repo-scoped.The change
A second probe stage,
GET /repos/{owner}/{repo}, fired only when stage 1 already returnedreachable— that is, only on the path that previously returned a green without having read anything repo-scoped.prox…placeholder here is swapped for a working credential. Both refusals are recorded in the header and pinned as self-test cases.repo-scope-refused(the fourth class) andrepo-not-visible(a repo-scoped 404 — wrongPM_SWEEP_REPO, or a credential that cannot see it). A repo-scoped 5xx or a transient network error stays unclassified, matching the classifier's existing narrowness.The fourth container class is added to the header's enumeration alongside the other three, with its corrected mechanism, per the file's convention for recording transport surprises.
Reverse verification — directions predicted in writing first, then run
--probe, proxied, beforetransport prerequisite met — api.github.com is reachable and the token authenticatessweep failed to run — GET /repos/.../issues?... -> HTTP 403--probe, proxied, afterPREREQUISITE NOT MET — the transport authenticates but repo-scoped reads are refused--probe, no proxy (class 3), before vs afterdiffreports IDENTICAL, exit 3 both waysThe last row is the no-regression leg: the pre-existing bad-credential class classifies exactly as it did, same wording, same exit.
The defect is also pinned as a self-test case in its own right: the same observations classified without the stage-2 reading still return
reachable. Removing stage 2 turns that case green again, which is what makes the fourth-class fixture a real regression pin rather than a restatement.Self-test
273 -> 292 cases, all passing. Nineteen added; no existing case rewritten or weakened. Class 2 (the Routine runner) is extended with a both-stages-pass case rather than having its original one-observation case edited.
Consumers checked before changing anything printed
.github/workflows/half-state-patrol.ymlreads only the exit code (0/2/3) and never parses a verdict line. Exit codes are unchanged:reachablegives 0, everything else 3.scripts/pm/ci-failure.mjsimportsclassifyTransportProbe,describeProbe,parseRemaining,EXIT_PREREQUISITE_NOT_METand calls the classifier with{token, authed, anon}. The repo reading is an optional observation, so that call classifies byte-identically. The guarantee that this script never green-lights on account-scoped evidence alone therefore lives in the gathering policy (needsRepoProbe), which is pinned.scripts/check-partof-closing-keyword.mjsimportsh7PartOfWithClosingKeyword, untouched.No printed verdict line that any machinery reads was changed.
Gates (run on
62f5dd18a, the final commit; each quoted from the gate's own verdict line)node scripts/pm/dispatch-gates.mjswith no args derived the set from the real diff (1 path, merge base5989b0de9):pnpm check:pm-half-states—✓ check-half-states self-test: 292 cases pass.(EXIT=0)pnpm check:partof-closing-keyword—✓ check-partof-closing-keyword self-test: 28 cases pass.(EXIT=0)pnpm check:cross-package-test-inputs—OK: 12 package(s) read outside themselves, all declared, and turbo.json hashes every declared glob.(EXIT=0)pnpm check:nul-bytes—check-nul-bytes: OK (scanned 6314 text file(s) -- 6314 tracked, 0 untracked-not-ignored; skipped 5 binary; no raw ASCII control bytes).(EXIT=0)Every exit code was captured before any pipe. The sixth derived entry,
node scripts/pm/check-half-states.mjsunderhalf-state-patrol.yml, is the live sweep — it cannot run in this container, which is the defect under test. That workflow subscribes to pull requests touching this file, so CI exercises the new stage 2 on a runner where the prerequisite really is met.Report-only contract unchanged: nothing new reddens, no label is written, a completed sweep still exits 0 on 0 or 40 findings.
Labelled
skip-changeset:scripts/pm/**is internal PM tooling and publishes nothing (#9945 / #9929 precedent).Generated by Claude Code