Skip to content

ci-failure.mjs's transport probe has the same false green as #9946 — it classifies from /rate_limit alone, then walks repo-scoped Actions paths #9966

Description

@os-warren

Blocked-by: #9946

Filed unassigned by the #9946 dev seat (session ae0d8bd1-5667-5f58-8ff8-fa22ec154325), out of that card's scope: #9946 is scoped to scripts/pm/check-half-states.mjs, and this is the identical defect one file over. Duplicate search first: keyword sweep over open issues for the ci-failure / probe / repo-scoped-403 shapes — no hit other than #9946 itself.

The shape

scripts/pm/ci-failure.mjs deliberately imports classifyTransportProbe rather than copying it ("one instrument", per its own header). It gathers only the account-scoped observation:

async function probeTransport() {
const authed = await probeRateLimit(TOKEN);
...
return { verdict: classifyTransportProbe({ token: TOKEN, authed, anon }), authed, anon };
}

Its walk then reads repo-scoped paths through rest()/repos/{owner}/{repo}/commits/{sha}/check-runs, job logs, and so on. In a proxy-mediated container those are exactly the paths the egress proxy refuses, while /rate_limit reaches GitHub and answers 200 with a real quota.

Measured, 2026-08-19, in this container

Reproducing ci-failure.mjs's probe call exactly, then the first read its walk makes:

ci-failure-shaped probe verdict: reachable
the walk it green-lights: GET /repos/.../check-runs -> 403

So the same inversion #9946 records: the probe reports the prerequisite met, and the thing it green-lights cannot make one request. Here the consequence is that a PREREQUISITE NOT MET (exit 3, "this classifies the ENVIRONMENT, not the tree") degrades into an UNDETERMINED or a raw HTTP number, which is the reading its own header says exit 3 exists to prevent.

Why this is a separate card

The remedy is available once #9946 lands: that PR adds an optional repo-scoped observation to classifyTransportProbe plus a repo-scope-refused verdict, chosen so the second observation is opt-in precisely because this file is the other importer and had to keep classifying identically. Adopting it here is a few lines — gather GET /repos/{owner}/{repo} when the account-scoped verdict reads reachable, and pass it through — but it is a change to a different file with its own self-test, and #9946 was dispatched single-file on a hot path.

Worth deciding at triage rather than assuming: whether the repo-scoped stage belongs in each caller (as #9946 leaves it, gathering policy per script) or whether the two scripts should share one probeTransport now that both need the same two stages. The second is tidier and is a wider change than either card has scoped.

Generated by Claude Code

Metadata

Metadata

Assignees

No one assigned

    Type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions