Skip to content

Commit f3de230

Browse files
committed
refactor(pm): drop the duplicate proxy re-exec — #13574 landed the same repair on main
1 parent 1ffc222 commit f3de230

1 file changed

Lines changed: 8 additions & 177 deletions

File tree

scripts/pm/check-half-states.mjs

Lines changed: 8 additions & 177 deletions
Original file line numberDiff line numberDiff line change
@@ -7551,120 +7551,6 @@ export function parseOutputOptions(argv) {
75517551
return out;
75527552
}
75537553

7554-
// ---------------------------------------------------------------------------
7555-
// The EGRESS PREREQUISITE — why this gate could not run in an agent container,
7556-
// and the one line of plumbing that was actually missing (#13526).
7557-
//
7558-
// ## The finding, and why it read as a credential problem for months
7559-
//
7560-
// This file's standing diagnosis of an agent container was "no usable
7561-
// credential": `GITHUB_TOKEN` holds the proxy's 14-character `prox…`
7562-
// placeholder, which 401s, and the anonymous fallback is a shared-NAT quota
7563-
// that neighbours have already spent — so `classifyTransportProbe` returned
7564-
// `bad-credential` and the run exited 3. Every word of that verdict was TRUE
7565-
// and the conclusion drawn from it was wrong, because it measured the wrong
7566-
// transport.
7567-
//
7568-
// Measured 2026-08-31 in an agent container, same container, same second:
7569-
//
7570-
// node fetch('https://api.github.com/rate_limit') -> 401 / anon 0 left
7571-
// curl https://api.github.com/rate_limit -> 200, limit 15000
7572-
// curl https://api.github.com/repos/objectstack-ai/objectstack -> 200
7573-
//
7574-
// The container has FULL authenticated repo-scoped REST access. It is reached
7575-
// through `HTTPS_PROXY`, which injects the credential — and **Node's global
7576-
// `fetch` does not honour `HTTPS_PROXY`**. So this script egressed directly,
7577-
// arrived unauthenticated on the shared NAT IP, and correctly diagnosed the
7578-
// only transport it had ever tried.
7579-
//
7580-
// ⭐ The knowledge was already in this file and never acted on: the
7581-
// `host-unreachable` verdict says, in as many words, "Node's fetch does not use
7582-
// HTTPS_PROXY, so this says nothing about `curl`, `gh` or the `mcp__github__*`
7583-
// tools". It was written as a caveat on a failure message instead of as the
7584-
// cause, and the population the gate guards grew unobserved behind it — the
7585-
// #13526 filing's whole point: **a gate nobody can run is indistinguishable
7586-
// from a gate that finds nothing.**
7587-
//
7588-
// ## The fix, and why it is a RE-EXEC rather than a dispatcher call
7589-
//
7590-
// Node 22 can route `fetch` through the environment's proxy, but only via
7591-
// `NODE_USE_ENV_PROXY=1`, which is read at BOOTSTRAP — after this module runs
7592-
// there is no supported way to set it, and `undici` is not resolvable here (no
7593-
// dependency in `scripts/`). So the process re-executes itself ONCE with the
7594-
// flag set. The decision is a pure function the self-test drives; the exec is
7595-
// three lines in the entrypoint.
7596-
//
7597-
// ⚠️ The properties that make this safe, each deliberate:
7598-
//
7599-
// - **CI is byte-identical.** A GitHub Actions runner sets no proxy variable,
7600-
// so `resolveProxyRelaunch` returns `relaunch: false` and the scheduled
7601-
// patrol's every request path is unchanged. This adds a path for the
7602-
// container that could not run; it does not alter the one that could.
7603-
// - **Exactly once.** The child carries `PM_SWEEP_PROXY_RELAUNCHED`, so a
7604-
// misconfigured proxy cannot produce a fork bomb — the child runs with
7605-
// whatever transport it has and reports its own verdict.
7606-
// - **`--self-test` never relaunches.** It makes no request and must stay
7607-
// runnable in any container whatever the environment carries — the same
7608-
// exemption the malformed-repo and malformed-floor refusals already make.
7609-
// - **An unsupported flag is not passed.** `--disable-warning` (which mutes
7610-
// undici's experimental `EnvHttpProxyAgent` notice) is Node 21.3+; on an
7611-
// older runtime passing it would make node refuse to start, turning a
7612-
// degraded reading into no reading at all. It is gated on
7613-
// `process.allowedNodeEnvironmentFlags`, and an older Node that ignores
7614-
// `NODE_USE_ENV_PROXY` simply lands back on today's honest exit 3.
7615-
// ---------------------------------------------------------------------------
7616-
7617-
/** The guard the child carries, so a relaunch can happen at most once. */
7618-
export const PROXY_RELAUNCH_GUARD = 'PM_SWEEP_PROXY_RELAUNCHED';
7619-
7620-
/**
7621-
* The proxy variables Node's `EnvHttpProxyAgent` itself reads, in its own
7622-
* precedence order. Listed rather than probed so the decision stays pure, and
7623-
* matched case-insensitively in BOTH spellings because the lowercase form is
7624-
* the one most container images actually export.
7625-
*/
7626-
export const PROXY_ENV_VARS = ['HTTPS_PROXY', 'https_proxy', 'HTTP_PROXY', 'http_proxy'];
7627-
7628-
/**
7629-
* Should this process re-exec itself with `NODE_USE_ENV_PROXY=1`?
7630-
*
7631-
* Pure, so the self-test pins every branch — including the two that must NEVER
7632-
* relaunch (a CI runner with no proxy, and a child that already did).
7633-
*
7634-
* @param {Record<string,string|undefined>} env
7635-
* @param {string[]} argv — the argv TAIL (`process.argv.slice(2)`).
7636-
* @returns {{ relaunch: boolean, reason: string, proxy: string|null }}
7637-
*/
7638-
export function resolveProxyRelaunch(env = {}, argv = []) {
7639-
const proxyVar = PROXY_ENV_VARS.find((name) => String(env?.[name] ?? '').trim().length > 0);
7640-
const proxy = proxyVar ? String(env[proxyVar]).trim() : null;
7641-
if (argv.includes('--self-test')) {
7642-
return { relaunch: false, reason: 'self-test makes no request and stays runnable anywhere', proxy };
7643-
}
7644-
if (String(env?.[PROXY_RELAUNCH_GUARD] ?? '') === '1') {
7645-
return { relaunch: false, reason: 'already relaunched once; this IS the child', proxy };
7646-
}
7647-
if (String(env?.NODE_USE_ENV_PROXY ?? '') === '1') {
7648-
return { relaunch: false, reason: 'the caller already set NODE_USE_ENV_PROXY', proxy };
7649-
}
7650-
if (!proxy) {
7651-
return { relaunch: false, reason: 'no proxy in the environment (a CI runner: direct egress)', proxy: null };
7652-
}
7653-
return { relaunch: true, reason: `${proxyVar} is set and node's fetch ignores it`, proxy };
7654-
}
7655-
7656-
/**
7657-
* The child's `execArgv`. Separated from the decision because it depends on the
7658-
* RUNTIME's flag support rather than on the environment, and because passing an
7659-
* unsupported flag is the one way this mechanism could make things worse.
7660-
*
7661-
* @param {{ has: (flag: string) => boolean }} allowed — normally
7662-
* `process.allowedNodeEnvironmentFlags`.
7663-
*/
7664-
export function proxyRelaunchExecArgv(allowed) {
7665-
return allowed?.has?.('--disable-warning') ? ['--disable-warning=UNDICI-EHPA'] : [];
7666-
}
7667-
76687554
// ---------------------------------------------------------------------------
76697555
// Transport prerequisite — the classifier (pure) and the probe that feeds it.
76707556
//
@@ -8143,15 +8029,14 @@ function reportPrerequisiteNotMet(v, options = {}) {
81438029
v.fix.slice(1).map((l) => ` ${l}\n`).join('') +
81448030
`\n${nothing.join('\n')}\n\n` +
81458031
` A GATE NOBODY CAN RUN IS INDISTINGUISHABLE FROM A GATE THAT FINDS NOTHING (#13526).\n` +
8146-
` That is not a slogan here, it is this refusal's measured cost: while this exit was the\n` +
8147-
` only thing an agent container could get out of this script, the population it guards —\n` +
8148-
` closed cards still carrying \`pm:*\` state — grew to 100+ per lane per label across at\n` +
8149-
` least three lanes, back to 2026-08-05, and no reading anywhere said so. If you are\n` +
8150-
` seeing this in a proxy-mediated container, check the egress prerequisite FIRST: node's\n` +
8151-
` \`fetch\` ignores \`HTTPS_PROXY\`, and this script now re-execs itself with\n` +
8152-
` \`NODE_USE_ENV_PROXY=1\` when a proxy is set (see \`resolveProxyRelaunch\`). Reaching this\n` +
8153-
` line WITH a proxy configured means the relaunch already happened and the credential the\n` +
8154-
` proxy injects is genuinely not working — not that the transport was never tried.\n` +
8032+
` That is not a slogan here, it is this refusal's measured cost. For as long as this exit\n` +
8033+
` was the only thing an agent container could get out of this script, the population it\n` +
8034+
` guards grew unobserved — and when the route was finally fixed and a census run, closed\n` +
8035+
` cards still carrying \`pm:*\` came to 2,063 \`pm:dispatched\` and 846 \`pm:queue\`, with 555\n` +
8036+
` carrying BOTH, back to 2026-08-02. The filing seat could only say "at least 100 per lane\n` +
8037+
` per label", because a first page is not a count and this gate could not be run to get one.\n` +
8038+
` ⇒ Treat this exit as an unread instrument, never as a quiet board: the H39 census below\n` +
8039+
` does not appear at all in a run that ends here.\n` +
81558040
` (Exit code ${EXIT_PREREQUISITE_NOT_MET}, distinct from the unclassified failure's 2 — but piping this\n` +
81568041
` reports the PIPE's status, so \`… | tail -4\` reads green either way. Use \`echo "EXIT=$?"\`.)`,
81578042
);
@@ -13032,35 +12917,6 @@ function selfTest() {
1303212917
t('H8 summary: …and does NOT claim the horizon was reached', h8win({ mergedPages: MERGED_WINDOW_PAGE_CEILING, mergedWindowTruncated: true }).includes('horizon reached'), false);
1303312918
t('H8 summary: …and says a delivery past it is invisible', h8win({ mergedPages: MERGED_WINDOW_PAGE_CEILING, mergedWindowTruncated: true }).includes('is invisible'), true);
1303412919

13035-
// -- The EGRESS prerequisite: proxy relaunch (#13526 leg 1) ----------------
13036-
//
13037-
// Measured: an agent container's `GITHUB_TOKEN` is a 14-char proxy
13038-
// placeholder and node's fetch bypasses the proxy that holds the real
13039-
// credential, so this gate exited 3 while the container had a working
13040-
// 15,000/h authenticated path the whole time.
13041-
const PROXY = 'http://proxy.internal:8080';
13042-
t('proxy: a proxied container relaunches', resolveProxyRelaunch({ HTTPS_PROXY: PROXY }, []).relaunch, true);
13043-
t('proxy: …and names the proxy it found', resolveProxyRelaunch({ HTTPS_PROXY: PROXY }, []).proxy, PROXY);
13044-
t('proxy: the lowercase spelling is read too', resolveProxyRelaunch({ https_proxy: PROXY }, []).relaunch, true);
13045-
t('proxy: HTTP_PROXY alone is enough', resolveProxyRelaunch({ HTTP_PROXY: PROXY }, []).relaunch, true);
13046-
// The property that keeps CI byte-identical: no proxy, no relaunch.
13047-
t('proxy: a CI runner with no proxy does NOT relaunch', resolveProxyRelaunch({}, []).relaunch, false);
13048-
t('proxy: …and says why, so a reader is not left guessing', resolveProxyRelaunch({}, []).reason.includes('no proxy'), true);
13049-
t('proxy: an empty proxy value is unset, not a proxy', resolveProxyRelaunch({ HTTPS_PROXY: '' }, []).relaunch, false);
13050-
t('proxy: whitespace is unset too', resolveProxyRelaunch({ HTTPS_PROXY: ' ' }, []).relaunch, false);
13051-
// Exactly once — the guard is what makes a misconfigured proxy cost one extra
13052-
// process rather than a fork bomb.
13053-
t('proxy: the child does NOT relaunch again', resolveProxyRelaunch({ HTTPS_PROXY: PROXY, [PROXY_RELAUNCH_GUARD]: '1' }, []).relaunch, false);
13054-
t('proxy: a caller who already set the flag is left alone', resolveProxyRelaunch({ HTTPS_PROXY: PROXY, NODE_USE_ENV_PROXY: '1' }, []).relaunch, false);
13055-
// `--self-test` must stay runnable in ANY container, whatever it carries —
13056-
// the same exemption the malformed-repo and malformed-floor refusals make.
13057-
t('proxy: --self-test never relaunches', resolveProxyRelaunch({ HTTPS_PROXY: PROXY }, ['--self-test']).relaunch, false);
13058-
// Passing an unsupported flag would make node refuse to START, turning a
13059-
// degraded reading into no reading — the direction this whole change refuses.
13060-
t('proxy: the warning flag is passed when the runtime supports it', proxyRelaunchExecArgv(new Set(['--disable-warning']))[0], '--disable-warning=UNDICI-EHPA');
13061-
t('proxy: …and omitted on a runtime that would reject it', proxyRelaunchExecArgv(new Set()).length, 0);
13062-
t('proxy: …and a missing flag set is not a crash', proxyRelaunchExecArgv(undefined).length, 0);
13063-
1306412920
// -- H26: a block whose target can never close, + the stale chain (#11219) --
1306512921
// The measured cards, by name, and both directions of every leg.
1306612922
const waiting = (number = 1119) => ({
@@ -14045,31 +13901,6 @@ function selfTest() {
1404513901

1404613902
const isMain = isEntrypoint(import.meta.url);
1404713903
if (isMain) {
14048-
// The EGRESS prerequisite (#13526), answered before anything else — including
14049-
// the malformed-input refusals below, which cost nothing to re-run in the
14050-
// child and would otherwise be printed twice. See `resolveProxyRelaunch` for
14051-
// why this exists: without it, node's `fetch` bypasses the proxy that holds
14052-
// this container's credential and every run here ends in exit 3.
14053-
const relaunch = resolveProxyRelaunch(process.env, process.argv.slice(2));
14054-
if (relaunch.relaunch) {
14055-
const child = spawnSync(
14056-
process.execPath,
14057-
[
14058-
...proxyRelaunchExecArgv(process.allowedNodeEnvironmentFlags),
14059-
fileURLToPath(import.meta.url),
14060-
...process.argv.slice(2),
14061-
],
14062-
{
14063-
stdio: 'inherit',
14064-
env: { ...process.env, NODE_USE_ENV_PROXY: '1', [PROXY_RELAUNCH_GUARD]: '1' },
14065-
},
14066-
);
14067-
// A child that could not be spawned at all falls THROUGH to the in-process
14068-
// path rather than failing the run: the parent's transport is worse, but a
14069-
// worse reading beats no reading, and its own prerequisite verdict is the
14070-
// honest report of what it then finds. A child that ran owns the exit code.
14071-
if (!child.error) process.exit(child.status ?? 1);
14072-
}
1407313904
// A malformed sweep target is bad usage (exit 2), refused BEFORE any request
1407413905
// — including the probe's, whose second stage is a repo-scoped read of this
1407513906
// very string. Silently falling back to the default would sweep a board

0 commit comments

Comments
 (0)