Skip to content

fix(pm): give check-half-states' transport probe a repo-scoped second stage - #9963

Merged
os-warren merged 1 commit into
mainfrom
claude/issue-9946-transport-probe-fourth-class
Aug 19, 2026
Merged

fix(pm): give check-half-states' transport probe a repo-scoped second stage#9963
os-warren merged 1 commit into
mainfrom
claude/issue-9946-transport-probe-fourth-class

Conversation

@os-warren

Copy link
Copy Markdown
Collaborator

Fixes#9946

The --probe flag 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_limit ITSELF with a fabricated quota". That is wrong, and the correction is what determined the fix. Measured live in this container, 2026-08-19:

GET /rate_limit -> 200, remaining 14979, limit 15000
server: github.com, real x-github-request-id
GET /user -> 200, the real login
GET /repos/objectstack-ai/objectstack -> 403, NO x-ratelimit-* headers,
no server: github.com, body from the proxy vendor

GitHub really did answer /rate_limit, the quota is real, and the placeholder token is substituted by the proxy for a working credential/user returns the real login. What the proxy intercepts is the other side: the repo-scoped reads.

Two consequences:

  1. No amount of care applied to /rate_limit could ever classify this container. Its account-scoped observation is genuinely healthy and byte-for-byte indistinguishable from the healthy Routine runner's.
  2. "Probe a real endpoint" is not quite the right rule. GET /user is 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 returned reachable — that is, only on the path that previously returned a green without having read anything repo-scoped.

  • The three failing container classes short-circuit before stage 2 and cost exactly what they cost before. Only the previously-green path spends one core request more (accepted on the card).
  • The match is a structural contradiction, not a vendor string and not a token shape: the quota endpoint reports thousands of core requests available, and a core request was just refused. That stays true if the intercepting proxy rewrites its message tomorrow.
  • Directions B (pattern-match the proxy's 403 body) and C (disqualify by token shape) were rejected on the card and are not implemented. C would in fact have mispredicted this class in both directions, since the prox… placeholder here is swapped for a working credential. Both refusals are recorded in the header and pinned as self-test cases.
  • New verdict kinds: repo-scope-refused (the fourth class) and repo-not-visible (a repo-scoped 404 — wrong PM_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

legpredictedobserved
--probe, proxied, beforeexit 0, falsely "prerequisite met"exit 0, transport prerequisite met — api.github.com is reachable and the token authenticates
full sweep, proxied, beforeexit 2, raw HTTP numberexit 2, sweep failed to run — GET /repos/.../issues?... -> HTTP 403
--probe, proxied, afterexit 3, classified, prerequisite NOT metexit 3, PREREQUISITE NOT MET — the transport authenticates but repo-scoped reads are refused
full sweep, proxied, afterexit 3, classified before listingexit 3, same verdict, nothing listed
--probe, no proxy (class 3), before vs afterbyte-identicaldiff reports IDENTICAL, exit 3 both ways

The 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.yml reads only the exit code (0/2/3) and never parses a verdict line. Exit codes are unchanged: reachable gives 0, everything else 3.
  • scripts/pm/ci-failure.mjs imports classifyTransportProbe, describeProbe, parseRemaining, EXIT_PREREQUISITE_NOT_MET and 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.mjs imports h7PartOfWithClosingKeyword, 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.mjs with no args derived the set from the real diff (1 path, merge base 5989b0de9):

  • 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-inputsOK: 12 package(s) read outside themselves, all declared, and turbo.json hashes every declared glob. (EXIT=0)
  • pnpm check:nul-bytescheck-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.mjs under half-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

… 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
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

skip-changesetPR has no user-facing published change; bypasses the changeset gate

Projects

None yet

Development

Successfully merging this pull request may close these issues.

check-half-states' transport probe greens a container where every real endpoint 403s — the agent proxy answers /rate_limit itself with a 14871 quota

1 participant

@os-warren