Skip to content

Commit 0dca68d

Browse files
committed
fix(scripts): re-exec check:single-claim-paths through the session proxy
`PR_NUMBER=<n> node scripts/check-single-claim-paths.mjs` exited 1 on `GitHub API 401` inside an agent container: the live read goes through node's global `fetch`, which does not read `HTTPS_PROXY`, and that proxy is what injects the credential. The gate's verdict could be taken in CI only, and a dev running the derived gate list locally recorded it NOT MEASURED instead. This takes the shim its siblings already take — the plan imported from the half-states patrol, the guard variable this file's own — at the point where a read is imminent: after the PR-context read, so a NOT WIRED run still makes no request and spawns no child, and never on the offline self-test. A new self-test battery pins the decision (proxy set: routed; absent: not routed; flag or env spelling already present: not routed again; own guard: no loop) and the two runs that take no decision at all. No verdict, no exit code and no part of the NOT WIRED routing changes. Claude-Session: https://claude.ai/code/session_01BTeBejoPUvRHN8WdAJC6oF Co-authored-by: Claude <noreply@anthropic.com>
1 parent 54145cc commit 0dca68d

1 file changed

Lines changed: 132 additions & 1 deletion

File tree

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

Lines changed: 132 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88
* node scripts/check-single-claim-paths.mjs # judge this PR (CI)
99
* node scripts/check-single-claim-paths.mjs --self-test # verify it offline
1010
*
11+
* A live run re-execs itself once through the session proxy (transport, below).
12+
*
1113
* ⚠️ Repo paths are named UNQUOTED in this header on purpose, and the self-test
1214
* fixtures below use paths that exist in no repo. Both are load-bearing; the
1315
* last section carries the measurement that forces them.
@@ -145,10 +147,17 @@
145147
* names a tree that does not exist, so a hint on one can never match anything.
146148
*/
147149

150+
import { spawnSync } from 'node:child_process';
148151
import { existsSync, readFileSync } from 'node:fs';
149152
import { join } from 'node:path';
150153
import process from 'node:process';
154+
import { fileURLToPath } from 'node:url';
151155
import { isEntrypoint } from './invoked-as.mjs';
156+
// ⛔ Not copied. The proxy-rearm plan is ONE source in the half-states patrol,
157+
// the same import post-stamped.mjs and check-prior-rulings.mjs take, so this gate
158+
// and they cannot come to disagree about whether this container's fetch reaches
159+
// the API. Only the guard variable below is this file's.
160+
import { PROXY_FLAG, PROXY_REARM_GUARD, proxyRearmPlan } from './pm/check-half-states.mjs';
152161

153162
// ── The self-test's own battery roster and floor (#13489) ──────────────────
154163
//
@@ -181,12 +190,13 @@ const SELF_TEST_BATTERIES = Object.freeze({
181190
'The failure has to carry the remedy, not just the verdict.': 3,
182191
'UNDETERMINED is its own answer. It must never read as clean, and it': 5,
183192
'Wiring absent: never clean, never an accusation.': 16,
193+
'The transport route: the decision, and the two runs that take none.': 18,
184194
'The short-circuit. This is the property that makes the gate affordable,': 22,
185195
});
186196

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

191201
// The key an assertion is filed under when no battery is open. It is not a
192202
// declared battery, so it reds by the same set difference rather than silently
@@ -520,6 +530,94 @@ export async function collect(ctx, api) {
520530
return { ...ctx, claimed, others, undetermined };
521531
}
522532

533+
// ---------------------------------------------------------------------------
534+
// Transport — routing this process's `fetch` at the session proxy before it
535+
// asks GitHub anything.
536+
//
537+
// MEASURED in an agent container: `HTTPS_PROXY` is set, that proxy injects the
538+
// real credential, the token variables hold a placeholder, and node's `fetch`
539+
// does not read `HTTPS_PROXY` — so an unrouted read sends the placeholder and
540+
// the first call throws `GitHub API 401`. `curl` on the same container answers
541+
// 200, which is what says the failure is a ROUTE and not a credential anyone
542+
// could go and fix. Until this shim, that was the whole local story of this
543+
// gate: its verdict could be taken in CI only, and a dev running the derived
544+
// gate list in a container recorded it NOT MEASURED rather than a verdict.
545+
//
546+
// ⛔ Nothing about the verdict changes here. A transport error on a bypassed
547+
// route keeps the class it had before, no exit code is added, and the
548+
// EXIT_NOT_WIRED routing is untouched — a run with no usable PR context reads
549+
// nothing, so it re-execs nothing either.
550+
551+
/** This file, resolved for the re-exec below. */
552+
const SELF_PATH = fileURLToPath(import.meta.url);
553+
554+
/**
555+
* This file's OWN re-exec guard, deliberately not the patrol's: one shared
556+
* variable would let a re-exec of another instrument suppress the re-exec of
557+
* this one, and the symptom would be the silent 401 this exists to close.
558+
*/
559+
export const OWN_PROXY_REARM_GUARD = 'OS_SINGLE_CLAIM_PATHS_PROXY_REARMED';
560+
561+
/**
562+
* Is a network read imminent? Only then is a route worth re-arming. Pure, so
563+
* every branch is pinned offline with no proxy present.
564+
*
565+
* Two runs read nothing and must therefore spawn nothing: `--self-test`, which
566+
* is offline by contract, and a run whose PR context is absent or incomplete,
567+
* which exits EXIT_NOT_WIRED having made no request. Re-execing either would
568+
* spawn a child to prove a route nothing is about to use, and would print an
569+
* informational line in front of the NOT WIRED text a reader is there to read.
570+
*
571+
* @param {{ argv?: string[], ctx?: null | { wired?: boolean } }} [run]
572+
*/
573+
export function transportRouteApplies({ argv = [], ctx = null } = {}) {
574+
if (argv.includes('--self-test')) return false;
575+
return ctx !== null && ctx.wired !== false;
576+
}
577+
578+
/**
579+
* The re-exec decision for this file: the shared plan, read through this file's
580+
* own guard name. Pure — env, execArgv and flag support in, decision out.
581+
*/
582+
export function proxyRearmDecision({ env = {}, execArgv = [], flagSupported = true } = {}) {
583+
return proxyRearmPlan({
584+
// Map this file's guard onto the name the shared plan reads, so the logic
585+
// stays single-sourced while the guards stay independent.
586+
env: { ...env, [PROXY_REARM_GUARD]: env[OWN_PROXY_REARM_GUARD] },
587+
execArgv,
588+
flagSupported,
589+
});
590+
}
591+
592+
/**
593+
* Re-exec ONCE with the proxy flag — argv, env and stdio forwarded — and return
594+
* the child's exit code, or `null` when this run carries on in-process.
595+
*/
596+
function rearmThroughProxy(args) {
597+
const plan = proxyRearmDecision({
598+
env: process.env,
599+
execArgv: process.execArgv,
600+
flagSupported: process.allowedNodeEnvironmentFlags.has(PROXY_FLAG),
601+
});
602+
if (plan.hint) {
603+
console.error(`ℹ️ ${plan.reason}. A failure below may be about the route, not this container.`);
604+
return null;
605+
}
606+
if (!plan.rearm) return null;
607+
console.error(`ℹ️ re-exec with ${plan.flag}: ${plan.reason}.`);
608+
const quiet = process.allowedNodeEnvironmentFlags.has('--disable-warning') ? ['--disable-warning=UNDICI-EHPA'] : [];
609+
const child = spawnSync(process.execPath, [plan.flag, ...quiet, SELF_PATH, ...args], {
610+
stdio: 'inherit',
611+
env: { ...process.env, [OWN_PROXY_REARM_GUARD]: '1' },
612+
});
613+
if (typeof child.status === 'number') return child.status;
614+
console.error(
615+
`⚠️ could not re-exec with ${plan.flag} (${child.error?.message ?? 'no exit status'}); `
616+
+ 'continuing in-process — every request will bypass the proxy.',
617+
);
618+
return null;
619+
}
620+
523621
const githubApi = (token) => async (path) => {
524622
const response = await fetch(`https://api.github.com${path}`, {
525623
headers: {
@@ -676,6 +774,32 @@ function selfTest() {
676774
{ number: '16326', repo: 'o/r', token: 't' },
677775
);
678776

777+
// --- The transport route. The re-exec is what makes this gate readable
778+
// outside CI, and it is invisible in the verdict layer, so the DECISION is
779+
// what is pinned — offline, with no proxy present and no request made.
780+
battery('The transport route: the decision, and the two runs that take none.');
781+
t('a configured proxy routes this run', proxyRearmDecision({ env: { HTTPS_PROXY: 'http://127.0.0.1:45311' } }).rearm, true);
782+
t('...with the flag node only reads at process start', proxyRearmDecision({ env: { HTTPS_PROXY: 'http://x' } }).flag, PROXY_FLAG);
783+
t('...and a reason naming the variable, for a reader of the run log', proxyRearmDecision({ env: { HTTPS_PROXY: 'http://x' } }).reason.includes('HTTPS_PROXY'), true);
784+
t('the lowercase spelling counts too', proxyRearmDecision({ env: { https_proxy: 'http://x' } }).rearm, true);
785+
t('NO proxy in the environment takes no route (the CI runner leg, unchanged)', proxyRearmDecision({ env: {} }).rearm, false);
786+
t('...and it says why it took none', proxyRearmDecision({ env: {} }).reason.includes('directly'), true);
787+
t('the flag already in execArgv does not route a second time', proxyRearmDecision({ env: { HTTPS_PROXY: 'http://x' }, execArgv: [PROXY_FLAG] }).rearm, false);
788+
t('...nor the same flag in NODE_OPTIONS', proxyRearmDecision({ env: { HTTPS_PROXY: 'http://x', NODE_OPTIONS: `--enable-source-maps ${PROXY_FLAG}` } }).rearm, false);
789+
t('...nor the env spelling of that switch', proxyRearmDecision({ env: { HTTPS_PROXY: 'http://x', NODE_USE_ENV_PROXY: '1' } }).rearm, false);
790+
t('this file\'s own guard stops a re-exec loop', proxyRearmDecision({ env: { HTTPS_PROXY: 'http://x', [OWN_PROXY_REARM_GUARD]: '1' } }).rearm, false);
791+
t('...and that guard is this file\'s, not the patrol\'s', OWN_PROXY_REARM_GUARD === PROXY_REARM_GUARD, false);
792+
t('the patrol\'s own guard does not suppress this file\'s re-exec', proxyRearmDecision({ env: { HTTPS_PROXY: 'http://x', [PROXY_REARM_GUARD]: '1' } }).rearm, true);
793+
t('a node that will not take the flag hints instead of re-execing', proxyRearmDecision({ env: { HTTPS_PROXY: 'http://x' }, flagSupported: false }).hint, true);
794+
t('...and re-execs nothing', proxyRearmDecision({ env: { HTTPS_PROXY: 'http://x' }, flagSupported: false }).rearm, false);
795+
796+
// WHERE the decision is taken: only by a run that is about to read the API.
797+
const liveCtx = readPrContext({ PR_NUMBER: '18844', GITHUB_REPOSITORY: 'o/r', GITHUB_TOKEN: 't' });
798+
t('a wired live run takes the routing decision', transportRouteApplies({ argv: [], ctx: liveCtx }), true);
799+
t('the offline self-test never takes it', transportRouteApplies({ argv: ['--self-test'], ctx: liveCtx }), false);
800+
t('no PR context at all reads nothing, so it routes nothing', transportRouteApplies({ argv: [], ctx: readPrContext({}) }), false);
801+
t('an incomplete context (NOT WIRED) routes nothing either', transportRouteApplies({ argv: [], ctx: readPrContext({ PR_NUMBER: '1' }) }), false);
802+
679803
// --- The short-circuit. This is the property that makes the gate affordable,
680804
// and it is invisible in the verdict layer, so it is pinned here against a
681805
// recording fake API. Fixture paths name a tree that exists in no repo.
@@ -777,6 +901,13 @@ if (isMain) {
777901
}
778902
} else {
779903
const ctx = readPrContext(process.env);
904+
// Transport before the questions that need it: unrouted, the first read
905+
// answers 401 in a container where the proxy holds the credential. Placed
906+
// AFTER the context read so a NOT WIRED run never pays for a child.
907+
if (transportRouteApplies({ argv: process.argv, ctx })) {
908+
const rearmed = rearmThroughProxy(process.argv.slice(2));
909+
if (rearmed !== null) process.exit(rearmed);
910+
}
780911
const resolved = ctx === null || ctx.wired === false ? ctx : await collect(ctx, githubApi(ctx.token));
781912
const result = judge(resolved);
782913
const emit = result.exit === EXIT_CLEAN ? console.log : console.error;

0 commit comments

Comments
 (0)