Skip to content

Commit 16cb493

Browse files
os-steveclaude
andauthored
fix(scripts): route check:single-claim-paths through the session proxy so a seat can run it (#18935)
Fixes #18314 Clause-②: no `check:single-claim-paths` could not be run outside CI. It reads a PR's file list over the network, node's `fetch` does not read `HTTPS_PROXY`, and in an agent container the request therefore left without its credential and GitHub answered `401`. A seat could not pre-run the gate its own PR would be judged by, and correctly recorded NOT MEASURED where a reading was one flag away. **Premise re-verified before anything was written.** The card's repro is from `53dd5aaa` on 2026-09-15 and `main` has moved. On `625db0e` (2026-09-18, this container) it still reproduces exactly: ``` $ PR_NUMBER=18313 GITHUB_REPOSITORY=objectstack-ai/objectstack node scripts/check-single-claim-paths.mjs Error: GitHub API 401 for /repos/objectstack-ai/objectstack/pulls/18313/files?per_page=100&page=1 exit 1 ``` ## What changed One file, `scripts/check-single-claim-paths.mjs`, +117 / -1 across two commits: - imports `PROXY_FLAG`, `PROXY_REARM_GUARD` and `proxyRearmPlan` from `scripts/pm/check-half-states.mjs`, and re-execs this process with `--use-env-proxy` when a proxy is configured and this run is not already routed through it; - declares its own re-exec guard, `OS_SINGLE_CLAIM_PATHS_PROXY_REARMED`, and presents it to the imported plan through a small pure `proxyPlanEnv()`; - performs the hand-off **only on the judging path** — a NOT WIRED run reaches no network, so re-execing it would spend a process to reprint the identical wiring verdict — and forwards the child's exit status **verbatim**, so the documented `0` / `1` / `2` contract is unchanged; - adds a nine-case self-test battery and raises the pinned battery-roster floor from 7 to 8. Nothing else is touched. The donor files named on the card stayed read-only. ## The design fork the card leaves open is already settled in the tree The card offers "lift the re-exec from `scripts/pm/check-clause2-carriers.mjs` unchanged", and the real fork behind that is a fourth verbatim copy versus extracting a shared helper. Measured on `625db0e`, neither is what the tree wants, because the helper already exists: - `proxyRearmPlan` is an exported function with **13 importers** in this repo — the card's "the fix exists three times" table counts performers, not the decision; - one importer is already **outside** `scripts/pm/`: `scripts/check-issue-citations.mjs` takes it as `./pm/check-half-states.mjs`, which is exactly this file's position in the tree; - `scripts/pm/check-expected-skips.mjs` pins the preference as a self-test case of its own — `structural: the proxy plan is imported, not restated`. So this PR imports the decision and lifts only the roughly twenty-line performer, which has to stay per-script for the reason in the next section. The bootstrapping constraint is not in the way: an ESM `import` is resolved and evaluated before this module's body runs, and the hand-off is decided at the dispatch, before the first network read. Import cost measured at 96 ms wall, node startup included. ## Why the guard variable is local, and why that is not cosmetic `proxyRearmPlan` reads one env name to decide "already re-armed once this run". Four sibling instruments hand it `env: process.env` unchanged, so they ask about the name the PLAN owns rather than their own — and a sibling's guard then suppresses their re-exec. Measured here, read-only, same box and same minute: ``` $ node scripts/pm/check-clause2-carriers.mjs --pair 18313 ℹ️ re-exec with --use-env-proxy: HTTPS_PROXY is set (...) and node's fetch does not read it. ...a real verdict. exit 2 $ OS_HALF_STATES_PROXY_REARMED=1 node scripts/pm/check-clause2-carriers.mjs --pair 18313 (no re-exec banner at all) ℹ️ the token in GITHUB_TOKEN/GH_TOKEN was refused (HTTP 401); falling back to the token-less public read... check-clause2-carriers: PREREQUISITE NOT MET — GET /repos/.../pulls?state=open... -> HTTP 403. exit 3 ``` That is the "the credential died" false story of comment 5715705458, produced mechanically by one inherited variable. This file therefore maps its OWN guard onto the name the plan reads, and pins both halves: its own guard stops its own loop, and a sibling's guard does not stop it. ## Evidence Every exit code captured before any pipe, into a file. Tree at `7c3c674`, clean. | what | result | |---|---| | the card's repro, after the fix | `exit 0` + the clean verdict, banner on stderr | | `node scripts/check-single-claim-paths.mjs --self-test` | `exit 0`, 74 cases pass (65 before) | | NOT WIRED legs: no `PR_NUMBER` / no `GITHUB_REPOSITORY` / empty `GITHUB_TOKEN` | `exit 2` each, byte-identical text, no re-exec attempted | | own guard pre-set, otherwise identical | no re-exec, `401`, `exit 1` — the guard is what fires it | | a sibling's guard pre-set, otherwise identical | re-execs, `exit 0` — no cross-suppression | | a non-zero child status (`PR_NUMBER=99999999`, a 404 in the child) | `exit 1` reaches the caller through the hand-off | | `node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands` | 29 families derived from the real change set | | all 29, re-run at final head `7c3c674` | 29 of 29 `exit 0` | | `dispatch-gates --ran` reconciliation | 29 derived, 29 run, 0 NOT-MEASURED (derived from recorded exit codes), 0 UNRUN | | `pnpm lint` — the repo-wide scan, not a narrowing | `exit 0`, 68 s, at `7c3c674` | ## Ablation — both legs, on-disk mutation proved, restore proved Through `scripts/ablation-replace.mjs`, which asserts the anchor count fell, the blob hash moved, and that the restore left `git diff HEAD` empty. 1. Delete the hand-off, run the self-test: `anchor 1 -> 0, blob 79d8c82 -> bf456081441a`, then `✗ structural: the hand-off is CALLED exactly once, and decided BEFORE the first network read (got [1,true], want [2,true])` — `1 of 74 case(s) failed`, and only that one. 2. Delete the hand-off, run the card's repro: `GitHub API 401 for /repos/objectstack-ai/objectstack/pulls/18313/files...`, the exact pre-fix failure. Both legs restored with `blob == HEAD` and an empty `git diff HEAD`. ⭐ Planning leg 1 is what produced the second commit. The first version of that pin compared only positions, and under the mutation the last occurrence of the performer's name became its own **declaration** — which sits above the collection and satisfied the comparison with no hand-off left in the file at all. The ablation reported `true` for a deleted call. Counting the call sites is the repair; the `got [1,true]` above is the strengthened pin failing as it should. ## Changeset: `skip-changeset` Nothing published moves. Measured rather than assumed: across the 70 published packages in this workspace, 217 `files[]` entries were scanned and **zero** name a `scripts` path; `files[]` is package-relative and no package root lives under `scripts/`, so a repo-root script cannot be shipped by any of them. The root manifest is `private: true`. Positive control on the scan: the same pass finds all 70 packages shipping a `dist` entry, so it can see entries when they are there. ## Acceptance notes Found while measuring, deliberately **not** fixed here — out of this card's scope, reported for filing rather than folded in. 1. **A transport failure on this gate exits `1`, which its own header defines as an accusation.** `PR_NUMBER=99999999 GITHUB_REPOSITORY=objectstack-ai/objectstack node scripts/check-single-claim-paths.mjs` ends in an unhandled rejection and `exit 1`, and the header says `1 judged, an earlier open PR already claims a listed path` and that "a mis-wired gate must not read as an accusation, because it would be red on every PR at once for something no author did". Unchanged by this PR in both directions, and a new exit code is a contract change, not an internal parameter. 2. **The self-test handshake cannot fire.** `selfTestReachedVerdict = true` executes synchronously, before the async block that prints the verdict, so an early `return` inside that block leaves the flag set — the exact `#13798` hole the flag exists to close, and the opposite of the rule in AGENTS.md ("Set the flag as the self-test's last statement, after its success line prints"). 3. **The four sibling instruments carry the guard hazard demonstrated above** (`check-clause2-carriers`, `post-stamped`, `board-snapshot`, `label-write`): each declares a local guard name nothing reads, and asks the plan about the imported one. Donor files are read-only on this card and #18376 is already open against one of them, so this is a report, not an edit. Noted, not filed: the card's suggested donor, `scripts/pm/check-clause2-carriers.mjs`, returns 4 for the card's own grep pattern, not the 3-of-4 shape the card's table implies; the counts in the dispatch for `check-prerelease-pin-watch`, `check-required-contexts` and `check-override-consistency` were not re-derived because none of those files is the donor. Carrier: whoever re-measures that table next; there may be none. --- _Generated by [Claude Code](https://claude.ai/code/session_01AmH9bKvGoLjiY86Q4Z3og2)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 46559f6 commit 16cb493

1 file changed

Lines changed: 122 additions & 1 deletion

File tree

‎scripts/check-single-claim-paths.mjs‎

Lines changed: 122 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -145,10 +145,18 @@
145145
* names a tree that does not exist, so a hint on one can never match anything.
146146
*/
147147

148+
import { spawnSync } from 'node:child_process';
148149
import { existsSync, readFileSync } from 'node:fs';
149150
import { join } from 'node:path';
150151
import process from 'node:process';
152+
import { fileURLToPath } from 'node:url';
151153
import { isEntrypoint } from './invoked-as.mjs';
154+
import { maskCommentsAndLiterals } from './js-comment-mask.mjs';
155+
// ⛔ Not copied. The proxy-rearm PLAN is ONE source for every instrument in this
156+
// tree, so this gate and the sweeps can never disagree about whether this
157+
// container's fetch reaches GitHub at all. Only the guard variable below is
158+
// this file's own, and the block above `rearmThroughProxy` says why.
159+
import { PROXY_FLAG, PROXY_REARM_GUARD, proxyRearmPlan } from './pm/check-half-states.mjs';
152160

153161
// ── The self-test's own battery roster and floor (#13489) ──────────────────
154162
//
@@ -182,11 +190,12 @@ const SELF_TEST_BATTERIES = Object.freeze({
182190
'UNDETERMINED is its own answer. It must never read as clean, and it': 5,
183191
'Wiring absent: never clean, never an accusation.': 16,
184192
'The short-circuit. This is the property that makes the gate affordable,': 22,
193+
'The route to GitHub. A 401 from a bypassed proxy reads as a dead': 9,
185194
});
186195

187196
// DELETING an entry silences that battery's floor exactly as effectively as
188197
// zeroing it, so the roster's own size is pinned too.
189-
const SELF_TEST_BATTERY_FLOOR = 7;
198+
const SELF_TEST_BATTERY_FLOOR = 8;
190199

191200
// The key an assertion is filed under when no battery is open. It is not a
192201
// declared battery, so it reds by the same set difference rather than silently
@@ -262,6 +271,9 @@ function batteryFloorFailures() {
262271

263272
const ROOT = new URL('..', import.meta.url).pathname;
264273

274+
/** This file, resolved for the proxy re-exec and for the self-test's own structural reads. */
275+
const SELF_PATH = fileURLToPath(import.meta.url);
276+
265277
/** The wiring that gives this gate a PR to judge. */
266278
const WIRING_WORKFLOW = '.github/workflows/single-claim-path-guard.yml';
267279

@@ -532,6 +544,84 @@ const githubApi = (token) => async (path) => {
532544
return response.json();
533545
};
534546

547+
// ---------------------------------------------------------------------------
548+
// The route to GitHub (#18314) — why this gate re-execs itself.
549+
//
550+
// Every real run reads a PR's file list over the network, and in an agent
551+
// container that network is reachable only through the session proxy. Node's
552+
// `fetch` does not read the proxy variables, so an unrouted run left WITHOUT
553+
// its credential and GitHub answered 401. That 401 has three readings available
554+
// to whoever sees it — the token is wrong, the PR does not exist, the repo is
555+
// wrong — and none of them is the true one, so a seat that tries to pre-run the
556+
// gate its own PR will be judged by records NOT MEASURED where a real reading
557+
// was one flag away.
558+
//
559+
// The switch is read at process START, which is the whole reason this is a
560+
// re-exec rather than an assignment.
561+
//
562+
// The DECISION is imported, not restated: one spelling of "go through the
563+
// proxy" for the whole tree. What is local is the guard VARIABLE, deliberately.
564+
// Sharing a sibling's guard would let that sibling's re-exec suppress this
565+
// one's — a grandchild is spawned without the flag in its argv, so it needs the
566+
// re-exec even though the guard the parent set says one already happened.
567+
//
568+
// ⭐ On a GitHub Actions runner there is no proxy in the environment, so the
569+
// plan never re-arms and this gate behaves exactly as it did before. The fix is
570+
// inert in CI, which is all this card ever claimed about CI.
571+
// ---------------------------------------------------------------------------
572+
573+
/** This file's OWN re-exec guard — deliberately not any sibling's. */
574+
export const OWN_PROXY_REARM_GUARD = 'OS_SINGLE_CLAIM_PATHS_PROXY_REARMED';
575+
576+
/**
577+
* The environment the imported plan is asked about: this file's own guard,
578+
* presented under the name the plan reads. Pure, so the self-test drives every
579+
* branch offline — and the branch worth pinning is a SIBLING's guard being set,
580+
* which must NOT stop this run from re-arming.
581+
*/
582+
export function proxyPlanEnv(env) {
583+
return { ...env, [PROXY_REARM_GUARD]: env[OWN_PROXY_REARM_GUARD] };
584+
}
585+
586+
/**
587+
* Hand this run off through the proxy, or `null` to carry on in-process.
588+
*
589+
* A returned number is the child's exit status, forwarded VERBATIM: the three
590+
* exit codes above are this gate's contract with CI, so the hand-off has to be
591+
* invisible in them.
592+
*
593+
* Everything printed here goes to STDERR. The clean verdict is this script's
594+
* only stdout, and a status line there would land inside whatever reads it.
595+
*
596+
* @param {string[]} args this run's own argv tail, forwarded unchanged
597+
*/
598+
function rearmThroughProxy(args) {
599+
const plan = proxyRearmPlan({
600+
env: proxyPlanEnv(process.env),
601+
execArgv: process.execArgv,
602+
flagSupported: process.allowedNodeEnvironmentFlags.has(PROXY_FLAG),
603+
});
604+
if (plan.hint) {
605+
console.error(`ℹ️ ${plan.reason}. A refusal below may be about the route, not this container.`);
606+
return null;
607+
}
608+
if (!plan.rearm) return null;
609+
console.error(`ℹ️ re-exec with ${plan.flag}: ${plan.reason}.`);
610+
// The env proxy agent is experimental and says so once per run. Nobody can
611+
// act on that notice, so silence it where the node in use can.
612+
const quiet = process.allowedNodeEnvironmentFlags.has('--disable-warning') ? ['--disable-warning=UNDICI-EHPA'] : [];
613+
const child = spawnSync(process.execPath, [plan.flag, ...quiet, SELF_PATH, ...args], {
614+
stdio: 'inherit',
615+
env: { ...process.env, [OWN_PROXY_REARM_GUARD]: '1' },
616+
});
617+
if (typeof child.status === 'number') return child.status;
618+
console.error(
619+
`⚠️ could not re-exec with ${plan.flag} (${child.error?.message ?? 'no exit status'}); ` +
620+
'continuing in-process — every request will bypass the proxy.',
621+
);
622+
return null;
623+
}
624+
535625
// ---------------------------------------------------------------------------
536626
// Self-test — the verdict layer, the exit-code contract, the declared list's
537627
// own invariants, the short-circuit that makes this affordable, and the wiring.
@@ -676,6 +766,30 @@ function selfTest() {
676766
{ number: '16326', repo: 'o/r', token: 't' },
677767
);
678768

769+
// --- The route to GitHub. A 401 from a bypassed proxy reads as a dead
770+
// credential, so both legs are pinned rather than one: the CI leg must stay
771+
// untouched (no proxy in the environment, no re-exec, no behaviour change at
772+
// all), and a container must re-arm EXACTLY once. The fixture proxy names no
773+
// tree in any repo, per this file's header rule on quoted literals.
774+
battery('The route to GitHub. A 401 from a bypassed proxy reads as a dead');
775+
const proxied = { HTTPS_PROXY: 'http://127.0.0.1:41733' };
776+
t('the Actions-runner leg: no proxy in the env, so no re-exec and nothing changes in CI', proxyRearmPlan({ env: proxyPlanEnv({}) }).rearm, false);
777+
t('an agent container re-arms, which is the whole of the fix', proxyRearmPlan({ env: proxyPlanEnv(proxied), flagSupported: true }).rearm, true);
778+
t("...exactly once — this run's OWN guard is what stops the loop", proxyRearmPlan({ env: proxyPlanEnv({ ...proxied, [OWN_PROXY_REARM_GUARD]: '1' }), flagSupported: true }).rearm, false);
779+
t("...and a SIBLING instrument's guard does NOT suppress it", proxyRearmPlan({ env: proxyPlanEnv({ ...proxied, [PROXY_REARM_GUARD]: '1' }), flagSupported: true }).rearm, true);
780+
t("this file's guard is not the imported one, which is what makes that hold", OWN_PROXY_REARM_GUARD === PROXY_REARM_GUARD, false);
781+
t('a run already routed through the proxy does not re-arm again', proxyRearmPlan({ env: proxyPlanEnv(proxied), execArgv: [PROXY_FLAG], flagSupported: true }).rearm, false);
782+
const unsupported = proxyRearmPlan({ env: proxyPlanEnv(proxied), flagSupported: false });
783+
t('a node that cannot take the flag SAYS so rather than bypassing silently', [unsupported.rearm, unsupported.hint], [false, true]);
784+
const ownSource = maskCommentsAndLiterals(readFileSync(SELF_PATH, 'utf8'));
785+
t('structural: the plan is imported, not restated here', /\bproxyRearmPlan\b/.test(ownSource) && !/function\s+proxyRearmPlan\b/.test(ownSource), true);
786+
// Both halves, because the ablation that planned this case found the
787+
// ordering alone vacuous: with the CALL deleted, the last occurrence is the
788+
// DECLARATION, which sits above the collection and satisfied the comparison
789+
// with no hand-off left in the file at all.
790+
const rearmSites = ownSource.split('rearmThroughProxy(').length - 1;
791+
t('structural: the hand-off is CALLED exactly once, and decided BEFORE the first network read', [rearmSites, ownSource.lastIndexOf('rearmThroughProxy(') < ownSource.lastIndexOf('await collect(')], [2, true]);
792+
679793
// --- The short-circuit. This is the property that makes the gate affordable,
680794
// and it is invisible in the verdict layer, so it is pinned here against a
681795
// recording fake API. Fixture paths name a tree that exists in no repo.
@@ -777,6 +891,13 @@ if (isMain) {
777891
}
778892
} else {
779893
const ctx = readPrContext(process.env);
894+
// Only a run with a usable context reaches the network, so only that run
895+
// needs the route. Re-execing a NOT WIRED run would spend a process to
896+
// reprint the identical wiring verdict.
897+
if (ctx !== null && ctx.wired !== false) {
898+
const handed = rearmThroughProxy(process.argv.slice(2));
899+
if (handed !== null) process.exit(handed);
900+
}
780901
const resolved = ctx === null || ctx.wired === false ? ctx : await collect(ctx, githubApi(ctx.token));
781902
const result = judge(resolved);
782903
const emit = result.exit === EXIT_CLEAN ? console.log : console.error;

0 commit comments

Comments
 (0)