forgedock 1.48.1 → 1.50.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -32,6 +32,9 @@
32
32
  * `openExecutionBudget` enforces the hard caps (`budget_minutes`/`budget_usd` per batch, `day_budget_*` per host UTC day,
33
33
  * from the host-day store `execution-budget.json` in the sched dir): pure arithmetic, no LLM.
34
34
  *
35
+ * #5112: `execution.capacity_fallback_after` (positive integer, `capacityFallbackAfter`) is how many `sandbox.capacity` deferrals of one
36
+ * issue make `fallback_local` run it on the host (orchestrate `CAPACITY_FALLBACK_AFTER` when unset), gated by `hostHeadroom`.
37
+ *
35
38
  * Hybrid (#4256): `placeLane` places each lane from facts (lane kind, validation weight, host headroom, verify.mode);
36
39
  * orchestrate runs a remote-placed lane locally when `max_remote` or the execution budget is exhausted.
37
40
  *
@@ -113,9 +116,12 @@ export function parseExecution(sec = {}) {
113
116
  let endpoint = null;
114
117
  try { const u = new URL(str(s.endpoint)); if (u.protocol === "https:" || u.protocol === "http:") endpoint = str(s.endpoint); } catch { endpoint = null; }
115
118
  const tokenEnv = ENV_RE.test(str(s.token_env)) ? str(s.token_env) : null;
119
+ const tokenFile = str(s.token_file) || null; // #5131: explicit pool/remote token file
116
120
  const cacheStore = str(s.cache_store) || null;
117
121
  const allowActions = str(s.allow_actions).toLowerCase() === "true";
118
122
  const fallbackLocal = str(s.fallback_local).toLowerCase() === "true";
123
+ const cfa = Number(str(s.capacity_fallback_after));
124
+ const capacityFallbackAfter = Number.isInteger(cfa) && cfa > 0 ? cfa : null; // #5112: unset: orchestrate's CAPACITY_FALLBACK_AFTER
119
125
  const heavyRaw = str(s.heavy).toLowerCase();
120
126
  const heavy = heavyRaw === "true" ? true : heavyRaw === "false" ? false : null;
121
127
  const cap = (v) => { const t = str(v), x = Number(t); return t !== "" && Number.isFinite(x) && x >= 0 ? x : null; };
@@ -133,7 +139,7 @@ export function parseExecution(sec = {}) {
133
139
  const uw = Number(str(s.unreachable_window_min));
134
140
  const accountRotator = str(s.account_rotator) === "tokenbaton" ? "tokenbaton" : null; // #5094: rotator.mjs reads its per-account quota state
135
141
  const unreachableWindowMin = str(s.unreachable_window_min) !== "" && Number.isFinite(uw) && uw >= 0 ? uw : null; // unset: providerLanes defaults to 5
136
- return { mode, maxRemote, provider, image, secrets, budget, ...(hostCapabilities.length ? { hostCapabilities } : {}), ...(hostCapabilityPatterns.length ? { hostCapabilityPatterns } : {}), ...(endpoint ? { endpoint } : {}), ...(tokenEnv ? { tokenEnv } : {}), ...(cacheStore ? { cacheStore } : {}), ...(allowActions ? { allowActions } : {}), ...(fallbackLocal ? { fallbackLocal } : {}), ...(heavy !== null ? { heavy } : {}), ...(unreachableWindowMin !== null ? { unreachableWindowMin } : {}), ...(Object.keys(relay).length ? { relay } : {}), ...(warmSandboxes ? { warmSandboxes } : {}), ...(accountRotator ? { accountRotator } : {}) };
142
+ return { mode, maxRemote, provider, image, secrets, budget, ...(hostCapabilities.length ? { hostCapabilities } : {}), ...(hostCapabilityPatterns.length ? { hostCapabilityPatterns } : {}), ...(endpoint ? { endpoint } : {}), ...(tokenEnv ? { tokenEnv } : {}), ...(tokenFile ? { tokenFile } : {}), ...(cacheStore ? { cacheStore } : {}), ...(allowActions ? { allowActions } : {}), ...(fallbackLocal ? { fallbackLocal } : {}), ...(capacityFallbackAfter !== null ? { capacityFallbackAfter } : {}), ...(heavy !== null ? { heavy } : {}), ...(unreachableWindowMin !== null ? { unreachableWindowMin } : {}), ...(Object.keys(relay).length ? { relay } : {}), ...(warmSandboxes ? { warmSandboxes } : {}), ...(accountRotator ? { accountRotator } : {}) };
137
143
  }
138
144
 
139
145
  const round1 = (x) => Math.round(x * 10) / 10;
@@ -480,18 +486,49 @@ export function containerProvider({ batch, repo, image, gitDir, cacheDir = `${ho
480
486
 
481
487
  const typedError = (code, message) => Object.assign(new Error(message), { code });
482
488
 
489
+ /** #5131: an endpoint as `scheme://host:port` (default ports filled, no path or trailing slash); null when unparsable. */
490
+ const endpointKey = (u) => { try { const x = new URL(String(u)); return `${x.protocol}//${x.hostname}:${x.port || (x.protocol === "https:" ? "443" : "80")}`; } catch { return null; } };
491
+
492
+ /** #5131: the trimmed contents of a token file (`~/` is the home dir); null when unset, unreadable or empty. */
493
+ const readTokenFile = (file) => {
494
+ if (!file) return null;
495
+ try { return readFileSync(String(file).startsWith("~/") ? join(homedir(), String(file).slice(2)) : String(file), "utf8").trim() || null; } catch { return null; }
496
+ };
497
+
498
+ /**
499
+ * #5131: the one pool/remote credential resolver, shared by both providers, `remoteTransportFor` and `preflight.pool_auth`:
500
+ * `token_env` from the env, then the keychain; then `execution.token_file`; then the local router's `forgedock_token_file`
501
+ * (`router`, poolRouterConfig output): always for `pool`, for `remote` only when its endpoint is unset or the router's.
502
+ * Returns `{token, source}`; `source` names where it came from (env name, `token_file`, `forgedock_token_file`), never the value.
503
+ */
504
+ export function poolCredential(exec = {}, { env = process.env, router = null, keychain } = {}) {
505
+ if (exec?.tokenEnv) {
506
+ const v = resolveCredential(exec.tokenEnv, { env, ...(keychain ? { keychain } : {}) });
507
+ if (v) return { token: v, source: exec.tokenEnv };
508
+ }
509
+ const f = readTokenFile(exec?.tokenFile);
510
+ if (f) return { token: f, source: "token_file" };
511
+ const routerOk = Boolean(router?.token) && ((exec?.provider ?? "pool") === "pool" || !exec?.endpoint || endpointKey(exec.endpoint) === endpointKey(router.endpoint));
512
+ return routerOk ? { token: router.token, source: "forgedock_token_file" } : { token: null, source: null };
513
+ }
514
+
483
515
  /**
484
516
  * The remote transport for forge.yaml `execution` settings: `opts.transport` when injected, else `httpTransport` on
485
- * `exec.endpoint` with the credential `exec.tokenEnv` (env, then keychain). Throws `execution.provider.misconfigured`
517
+ * `exec.endpoint` with the credential `exec.tokenEnv` from `credentials`, or by default poolCredential (#5131: env,
518
+ * keychain, `token_file`, the matching local router's token). `fetchFn` is a test seam. Throws `execution.provider.misconfigured`
486
519
  * without an endpoint, `execution.provider.actions_refused` for a GitHub Actions endpoint without `allow_actions`.
487
520
  */
488
- export function remoteTransportFor(exec = {}, { transport = null, env = process.env, credentials = (n) => resolveCredential(n, { env }) } = {}) {
521
+ export function remoteTransportFor(exec = {}, { transport = null, env = process.env, credentials = null, router, fetchFn } = {}) {
489
522
  if (transport) return transport;
490
523
  if (!exec?.endpoint) throw typedError("execution.provider.misconfigured", "execution.provider remote needs execution.endpoint");
491
524
  if (isActionsEndpoint(exec.endpoint) && exec.allowActions !== true) {
492
525
  throw typedError("execution.provider.actions_refused", "execution.endpoint is a GitHub Actions API; set execution.allow_actions: true to spend Actions minutes");
493
526
  }
494
- return httpTransport({ endpoint: exec.endpoint, token: exec.tokenEnv ? credentials(exec.tokenEnv) : null });
527
+ // #5144: a getter, so the bearer is resolved on every request (a token fixed or rotated mid-batch is picked up), not once here.
528
+ const rt = credentials ? null : (router !== undefined ? router : poolRouterConfig({ env }));
529
+ const token = credentials ? () => (exec.tokenEnv ? credentials(exec.tokenEnv) : null)
530
+ : () => poolCredential({ provider: "remote", ...exec }, { env, router: rt }).token;
531
+ return httpTransport({ endpoint: exec.endpoint, token, ...(fetchFn ? { fetchFn } : {}) });
495
532
  }
496
533
 
497
534
  /**
@@ -510,26 +547,35 @@ export function poolRouterConfig({ env = process.env, path = env.COMPUTESHARE_CO
510
547
  export function providerByName(name, opts) {
511
548
  if (name === "local") return localProvider(opts);
512
549
  if (name === "container") return containerProvider(opts);
513
- if (name === "remote") {
514
- const exec = opts?.exec ?? {};
515
- const env = opts?.env ?? process.env;
516
- return remoteProvider({ batch: opts.batch, repo: opts.repo, settings: { ...exec, ...(typeof opts?.prefetch === "function" ? { prefetch: opts.prefetch } : {}) }, env, transport: remoteTransportFor(exec, { transport: opts?.transport, env }), ...(opts?.now ? { now: opts.now } : {}) });
517
- }
518
- if (name === "pool") {
519
- // #4546: `remote` with the endpoint and token defaulted to the local router, and the repo's forge.yaml sent along.
550
+ if (name === "remote" || name === "pool") {
520
551
  const env = opts?.env ?? process.env;
521
552
  const exec = opts?.exec ?? {};
522
- const router = opts?.transport || exec.endpoint ? null : poolRouterConfig({ env, ...(opts?.poolConfigPath ? { path: opts.poolConfigPath } : {}) });
523
- if (!opts?.transport && !exec.endpoint && !router) throw typedError("execution.provider.misconfigured", "execution.provider pool needs a local router config (forgedock pool router) or execution.endpoint");
553
+ const pool = name === "pool";
554
+ // #4546: `pool` is `remote` with the endpoint and token defaulted to the local router, and the repo's forge.yaml sent along.
555
+ // #5131: `remote` reads the router config too, for the token fallback when its endpoint is the local router's.
556
+ const router = opts?.transport || (pool && exec.endpoint) ? null : poolRouterConfig({ env, ...(opts?.poolConfigPath ? { path: opts.poolConfigPath } : {}) });
557
+ if (pool && !opts?.transport && !exec.endpoint && !router) throw typedError("execution.provider.misconfigured", "execution.provider pool needs a local router config (forgedock pool router) or execution.endpoint");
524
558
  let forgeYaml = null;
525
- try { forgeYaml = opts?.cwd ? readFileSync(join(opts.cwd, "forge.yaml"), "utf8") : null; } catch { forgeYaml = null; }
526
- // The router's token is resolved under a fixed name so remoteProvider redacts it like any `token_env` credential.
527
- const tokenEnv = exec.tokenEnv ?? (router?.token ? "FORGEDOCK_POOL_TOKEN" : undefined);
528
- // #4685: an empty env/keychain lookup of a configured token_env falls back to the router's token file.
529
- const credentials = (n) => (n === "FORGEDOCK_POOL_TOKEN" && !exec.tokenEnv ? router?.token ?? null : resolveCredential(n, { env }) ?? router?.token ?? null);
530
- const settings = { ...exec, ...(exec.endpoint ? {} : router ? { endpoint: router.endpoint } : {}), ...(tokenEnv ? { tokenEnv } : {}), ...(forgeYaml ? { forgeYaml } : {}),
559
+ if (pool) { try { forgeYaml = opts?.cwd ? readFileSync(join(opts.cwd, "forge.yaml"), "utf8") : null; } catch { forgeYaml = null; } }
560
+ const endpoint = exec.endpoint ?? (pool ? router?.endpoint : undefined);
561
+ // #5144: the router's token file is re-read per call (a rotated or newly written token), its endpoint stays as at construction.
562
+ const routerEligible = !(opts?.transport || (pool && exec.endpoint));
563
+ const routerNow = () => {
564
+ if (!routerEligible) return null;
565
+ const c = poolRouterConfig({ env, ...(opts?.poolConfigPath ? { path: opts.poolConfigPath } : {}) });
566
+ return c ? { endpoint: router?.endpoint ?? c.endpoint, token: c.token } : router;
567
+ };
568
+ const resolve = () => poolCredential({ ...exec, provider: name, ...(endpoint ? { endpoint } : {}) }, { env, router: routerNow() });
569
+ // A token read from a file is resolved under a fixed name so remoteProvider redacts it like any `token_env` credential.
570
+ // #5144: set even when no token exists yet, so one that appears later is still resolved per call (and redacted).
571
+ const fixed = pool ? "FORGEDOCK_POOL_TOKEN" : "FORGEDOCK_REMOTE_TOKEN";
572
+ const tokenEnv = exec.tokenEnv ?? fixed;
573
+ // #4685/#5131: an empty env/keychain lookup of `token_env` falls back to `token_file`, then the (matching) router's token file.
574
+ const credentials = (n) => (n === tokenEnv ? resolve().token : resolveCredential(n, { env })); // per call, like resolveCredential
575
+ const settings = { ...exec, ...(endpoint ? { endpoint } : {}), ...(tokenEnv ? { tokenEnv } : {}), ...(forgeYaml ? { forgeYaml } : {}),
531
576
  ...(typeof opts?.prefetch === "function" ? { prefetch: opts.prefetch } : {}) }; // #4778: dispatch-time relay pre-fetch
532
- return remoteProvider({ batch: opts.batch, repo: opts.repo, settings, name: "pool", env, credentials, transport: remoteTransportFor(settings, { transport: opts?.transport, env, credentials }), ...(opts?.now ? { now: opts.now } : {}) });
577
+ return remoteProvider({ batch: opts.batch, repo: opts.repo, settings, ...(pool ? { name: "pool" } : {}), env, credentials,
578
+ transport: remoteTransportFor(settings, { transport: opts?.transport, env, credentials, ...(opts?.fetchFn ? { fetchFn: opts.fetchFn } : {}) }), ...(opts?.now ? { now: opts.now } : {}) });
533
579
  }
534
580
  const e = new Error(`unknown execution.provider ${JSON.stringify(name)} (known: ${SANDBOX_PROVIDERS.join(", ")})`);
535
581
  e.code = "execution.provider.unknown";
@@ -7,7 +7,7 @@ import { mkdirSync, writeFileSync, existsSync, readFileSync } from "node:fs";
7
7
  import { join } from "node:path";
8
8
  import { tmpDir } from "../tests/_tmp.mjs";
9
9
  import { renderContract } from "./render.mjs";
10
- import { parseExecution, SANDBOX_PROVIDERS, detectNeeds, hostCapabilityBlock, placementText, contractHostNeeds, placement, placeLane, validationWeight, hostHeadroom, scopedEnv, providerLanes, remoteHoldId, usageLimitInTail, isTransportError, isCloneFailure, containerProvider, providerByName, openExecutionBudget } from "./execution.mjs";
10
+ import { parseExecution, SANDBOX_PROVIDERS, detectNeeds, hostCapabilityBlock, placementText, contractHostNeeds, placement, placeLane, validationWeight, hostHeadroom, scopedEnv, providerLanes, remoteHoldId, usageLimitInTail, isTransportError, isCloneFailure, containerProvider, providerByName, openExecutionBudget, poolCredential, remoteTransportFor } from "./execution.mjs";
11
11
  import { makeLedger, countLiveLanes } from "./ledger.mjs";
12
12
  import { openBatch } from "./batch.mjs";
13
13
  import { orchestrate, formatTable, formatSandboxSummary } from "./orchestrate.mjs";
@@ -399,6 +399,27 @@ describe("runBatch wiring (#4253)", () => {
399
399
  assert.deepEqual(prov.calls, []);
400
400
  }
401
401
  });
402
+
403
+ it("#5138: a live --set-placement local makes the next lane of a remote batch a detached host lane", async () => {
404
+ const work = tmpDir("exec-cli-live-");
405
+ const stub = join(work, "stub.mjs");
406
+ writeFileSync(stub, `import { writeFileSync } from "node:fs"; const a = process.argv.slice(2); writeFileSync(a[a.indexOf("--result-file") + 1], JSON.stringify({ terminalReason: "merged", pr: 9 }));\n`);
407
+ const root = join(work, "batches");
408
+ let recorded = null;
409
+ const prov = fakeProvider({ finish: (n) => {
410
+ const b = openBatch(root, "blive");
411
+ recorded = b.readExecution();
412
+ if (n === 1) b.setExecution({ mode: "local" }); // the operator's --set-placement before #2 is admitted
413
+ return { terminalReason: "merged", pr: 100 + n, cost_usd: 0 };
414
+ } });
415
+ const res = await quiet(() => orchestrateFromCli(["1", "2", "--repo", "o/r", "--no-prefilter", "--parallel", "1"], base(work, { batchId: "blive", laneScript: stub,
416
+ execution: parseExecution({ mode: "remote", max_remote: "1" }), sandboxProvider: prov,
417
+ sandboxLedger: (n) => makeLedger({ root: join(work, "ledger"), batch: "blive", lane: `sandbox-${n}`, owner: "t", issue: n }) })));
418
+ assert.deepEqual(recorded, { provider: true });
419
+ assert.deepEqual(res.rows.map((r) => [r.issue, r.outcome, r.pr, r.placement?.where]), [[1, "merged", 101, "remote"], [2, "merged", 9, "local"]]);
420
+ assert.match(res.rows[1].placement.reason, /live placement/);
421
+ assert.deepEqual(prov.calls.filter((c) => c[0] === "start").map((c) => c[1]), [1]);
422
+ });
402
423
  });
403
424
 
404
425
  describe("execution budget and sandbox cost (#4255)", () => {
@@ -716,6 +737,10 @@ describe("#4547: fallback_local and agent liveness", () => {
716
737
  assert.equal(parseExecution({ mode: "remote", max_remote: "2", fallback_local: "true" }).fallbackLocal, true);
717
738
  assert.equal(parseExecution({ mode: "remote", fallback_local: "yes" }).fallbackLocal, undefined);
718
739
  });
740
+ it("#5112 parseExecution: capacity_fallback_after is a positive integer, absent otherwise", () => {
741
+ assert.equal(parseExecution({ mode: "remote", fallback_local: "true", capacity_fallback_after: "5" }).capacityFallbackAfter, 5);
742
+ for (const v of [undefined, "", "0", "-1", "2.5", "x"]) assert.equal("capacityFallbackAfter" in parseExecution({ mode: "remote", capacity_fallback_after: v }), false, String(v));
743
+ });
719
744
  it("transport capacity carries free and liveness; none valid is unknown", async () => {
720
745
  const { httpTransport } = await import("./sandbox-remote.mjs");
721
746
  const cap = async (data, ok = true) => httpTransport({ endpoint: "http://pool.test", token: "t", fetchFn: async () => ({ ok, status: ok ? 200 : 500, headers: new Headers(), text: async () => JSON.stringify(data) }) }).capacity();
@@ -901,3 +926,110 @@ describe("#5095: launch 5xx and a pool usage limit are deferred, never needs-hum
901
926
  assert.equal(plain.terminalReason, "engine-error");
902
927
  });
903
928
  });
929
+
930
+ describe("remote/pool credential resolver (#5131)", () => {
931
+ /** A router config dir: router.json listening on 127.0.0.1:7710 and the sibling forgedock.token. */
932
+ const routerDir = () => {
933
+ const d = tmpDir("router-5131-");
934
+ writeFileSync(join(d, "router.json"), JSON.stringify({ listen: { host: "127.0.0.1", port: 7710 } }));
935
+ writeFileSync(join(d, "forgedock.token"), "router-tok\n");
936
+ return d;
937
+ };
938
+ const recorder = () => {
939
+ const seen = [];
940
+ const fetchFn = async (url, init = {}) => { seen.push({ url: String(url), auth: init.headers?.authorization ?? null }); return new Response("{}", { status: 200 }); };
941
+ return { seen, fetchFn };
942
+ };
943
+ const remoteVia = (exec, { fetchFn, dir }) => providerByName("remote", { batch: { id: "b5131" }, repo: "o/r", exec: parseExecution({ mode: "remote", max_remote: "1", provider: "remote", ...exec }),
944
+ env: {}, poolConfigPath: join(dir, "router.json"), fetchFn });
945
+
946
+ it("5131: providerByName remote with no env/keychain token sends the router token file's bearer only to the router's endpoint", async () => {
947
+ const dir = routerDir();
948
+ const hit = recorder();
949
+ await remoteVia({ endpoint: "http://127.0.0.1:7710/" }, { fetchFn: hit.fetchFn, dir }).capacity();
950
+ assert.ok(hit.seen.length >= 1);
951
+ assert.equal(hit.seen[0].auth, "Bearer router-tok");
952
+ const miss = recorder();
953
+ await remoteVia({ endpoint: "https://runner.example.com" }, { fetchFn: miss.fetchFn, dir }).capacity();
954
+ assert.ok(miss.seen.length >= 1);
955
+ assert.equal(miss.seen[0].auth, null, "the local router token never goes to another endpoint");
956
+ });
957
+
958
+ it("5131: poolCredential order is token_env (env, keychain), token_file, then the matching router token; source never holds the value", () => {
959
+ const dir = routerDir();
960
+ const file = join(dir, "explicit.token");
961
+ writeFileSync(file, " file-tok \n");
962
+ const router = { endpoint: "http://127.0.0.1:7710", token: "router-tok" };
963
+ const keychain = () => null;
964
+ assert.deepEqual(poolCredential({ provider: "remote", tokenEnv: "COMPUTESHARE_TOKEN", endpoint: "http://127.0.0.1:7710" }, { env: { COMPUTESHARE_TOKEN: "env-tok" }, router, keychain }), { token: "env-tok", source: "COMPUTESHARE_TOKEN" });
965
+ assert.deepEqual(poolCredential({ provider: "remote", tokenEnv: "COMPUTESHARE_TOKEN", endpoint: "http://127.0.0.1:7710" }, { env: {}, router, keychain: () => "kc-tok" }), { token: "kc-tok", source: "COMPUTESHARE_TOKEN" });
966
+ assert.deepEqual(poolCredential({ provider: "remote", tokenEnv: "COMPUTESHARE_TOKEN", endpoint: "http://127.0.0.1:7710/" }, { env: {}, router, keychain }), { token: "router-tok", source: "forgedock_token_file" });
967
+ assert.deepEqual(poolCredential({ provider: "remote", tokenEnv: "COMPUTESHARE_TOKEN", tokenFile: file, endpoint: "https://x.example" }, { env: {}, router, keychain }), { token: "file-tok", source: "token_file" });
968
+ assert.deepEqual(poolCredential({ provider: "remote", tokenEnv: "COMPUTESHARE_TOKEN", endpoint: "https://x.example" }, { env: {}, router, keychain }), { token: null, source: null });
969
+ assert.deepEqual(poolCredential({ provider: "pool", endpoint: "https://x.example" }, { env: {}, router, keychain }), { token: "router-tok", source: "forgedock_token_file" });
970
+ assert.deepEqual(poolCredential({ provider: "remote", tokenFile: join(dir, "missing.token") }, { env: {}, router: null }), { token: null, source: null });
971
+ });
972
+
973
+ it("5131: execution.token_file is parsed and sent as the bearer to any endpoint", async () => {
974
+ const dir = routerDir();
975
+ const file = join(dir, "explicit.token");
976
+ writeFileSync(file, "file-tok\n");
977
+ assert.equal(parseExecution({ token_file: file }).tokenFile, file);
978
+ assert.equal(parseExecution({ token_file: "" }).tokenFile, undefined);
979
+ const hit = recorder();
980
+ await remoteVia({ endpoint: "https://runner.example.com", token_file: file }, { fetchFn: hit.fetchFn, dir }).capacity();
981
+ assert.equal(hit.seen[0].auth, "Bearer file-tok");
982
+ });
983
+
984
+ it("5144: a token that appears or rotates after the provider is built is the next request's bearer", async () => {
985
+ const dir = tmpDir("router-5144-");
986
+ const tokenFile = join(dir, "forgedock.token");
987
+ writeFileSync(join(dir, "router.json"), JSON.stringify({ listen: { host: "127.0.0.1", port: 7710 } }));
988
+ writeFileSync(tokenFile, "\n"); // router config with no token yet
989
+ const hit = recorder();
990
+ const env = {};
991
+ const mk = (name, exec) => providerByName(name, { batch: { id: "b5144" }, repo: "o/r", exec: parseExecution({ mode: "remote", max_remote: "1", provider: name, ...exec }),
992
+ env, poolConfigPath: join(dir, "router.json"), fetchFn: hit.fetchFn });
993
+ for (const [name, exec] of [["remote", { endpoint: "http://127.0.0.1:7710" }], ["pool", {}]]) {
994
+ writeFileSync(tokenFile, "\n");
995
+ hit.seen.length = 0;
996
+ const p = mk(name, exec);
997
+ await p.capacity();
998
+ assert.equal(hit.seen.at(-1).auth, null, `${name}: no token at construction`);
999
+ writeFileSync(tokenFile, "router-new\n");
1000
+ await p.capacity();
1001
+ assert.equal(hit.seen.at(-1).auth, "Bearer router-new", `${name}: a token written later is sent`);
1002
+ writeFileSync(tokenFile, "router-rotated\n");
1003
+ await p.capacity();
1004
+ assert.equal(hit.seen.at(-1).auth, "Bearer router-rotated", `${name}: a rotated token is sent`);
1005
+ }
1006
+ });
1007
+
1008
+ it("5144: an env token_env that is set after construction is sent; remoteTransportFor default path resolves per call", async () => {
1009
+ const dir = routerDir();
1010
+ const hit = recorder();
1011
+ const penv = {};
1012
+ const q = providerByName("remote", { batch: { id: "b5144e" }, repo: "o/r", exec: parseExecution({ mode: "remote", max_remote: "1", provider: "remote", endpoint: "https://runner.example.com", token_env: "COMPUTESHARE_TOKEN" }),
1013
+ env: penv, poolConfigPath: join(dir, "router.json"), fetchFn: hit.fetchFn });
1014
+ await q.capacity();
1015
+ assert.equal(hit.seen.at(-1).auth, null);
1016
+ penv.COMPUTESHARE_TOKEN = "env-late";
1017
+ await q.capacity();
1018
+ assert.equal(hit.seen.at(-1).auth, "Bearer env-late");
1019
+ const tenv = {};
1020
+ const t = remoteTransportFor({ endpoint: "https://runner.example.com", tokenEnv: "COMPUTESHARE_TOKEN" }, { env: tenv, router: null, fetchFn: hit.fetchFn });
1021
+ await t.capacity();
1022
+ assert.equal(hit.seen.at(-1).auth, null);
1023
+ tenv.COMPUTESHARE_TOKEN = "env-late-2";
1024
+ await t.capacity();
1025
+ assert.equal(hit.seen.at(-1).auth, "Bearer env-late-2");
1026
+ });
1027
+
1028
+ it("5131: remoteTransportFor's default credentials (gc) fall back to the matching router token", async () => {
1029
+ const hit = recorder();
1030
+ const t = remoteTransportFor({ endpoint: "http://127.0.0.1:7710" }, { env: {}, router: { endpoint: "http://127.0.0.1:7710", token: "router-tok" }, fetchFn: hit.fetchFn });
1031
+ await t.capacity();
1032
+ assert.equal(hit.seen[0].auth, "Bearer router-tok");
1033
+ assert.throws(() => remoteTransportFor({}, { env: {}, router: null }), (e) => e.code === "execution.provider.misconfigured");
1034
+ });
1035
+ });
@@ -266,10 +266,16 @@ export function heavyShimScript(name, real, { node = process.execPath, script =
266
266
  return `${SHIM_HEADER}export FORGE_HEAVY_SHIM_DIR="$(cd "$(dirname "$0")" && pwd)"\nexec ${q(node)} ${q(script)} run --name ${q(name)} -- ${q(real)} "$@"\n`;
267
267
  }
268
268
 
269
- /** True when `text` is a shell-style script that looks `name` up on PATH itself (a pass-through wrapper, #4680). */
269
+ /**
270
+ * True when `text` is a shell-style script that looks `name` up on PATH itself (a pass-through wrapper, #4680). The lookup
271
+ * target may be the literal name, or (#5148) a variable/substitution when the script derives that name from `$0`
272
+ * (a `${0##…}` strip or `basename "$0"`), the shape of a wrapper symlinked under several command names.
273
+ */
270
274
  export function isPassThroughWrapper(text, name) {
271
275
  const t = String(text ?? "");
272
- return t.startsWith("#!") && t.includes(name) && /\b(command\s+-v|which|type\s+-p)\b/.test(t);
276
+ if (!t.startsWith("#!") || !/\b(command\s+-v|which|type\s+-p)\b/.test(t)) return false;
277
+ if (t.includes(name)) return true;
278
+ return /(?:command\s+-v|which|type\s+-p)\s+(?:-\s+)?["']?\$/.test(t) && /\$\{0##\*\/\}|basename\s+(?:--\s+)?["']?\$\{?0\b/.test(t);
273
279
  }
274
280
  const head = (p) => { try { return readFileSync(p, "utf8").slice(0, 4096); } catch { return ""; } };
275
281
 
@@ -332,6 +338,8 @@ export function findShimLoops(path, { names = HEAVY_BINS, find = which, read = h
332
338
  }
333
339
 
334
340
  export const SHIM_LOOP_MAX = 4;
341
+ /** #5148: hard bound on shim entries (FORGE_HEAVY_DEPTH) whatever the wrapper shape; far above legitimate `npm run` nesting. */
342
+ export const SHIM_DEPTH_MAX = 32;
335
343
  /**
336
344
  * Re-entry guard (#4680): FORGE_HEAVY_CHAIN lists `name:cmd` per shim run. A repeat of the same name+cmd where cmd is a
337
345
  * pass-through wrapper means it sent the call back to the shim; a repeat of a real binary is a legitimate nested call
@@ -342,15 +350,17 @@ export function guardChain(name, cmd, env, read = head) {
342
350
  const chain = String(env.FORGE_HEAVY_CHAIN ?? "").split("|").filter(Boolean);
343
351
  const dirs = chain.map((e) => dirname(e.slice(e.indexOf(":") + 1)));
344
352
  const loop = (why) => ({ error: `env.shim_loop: ${name} ${why} (PATH dirs: ${[env.FORGE_HEAVY_SHIM_DIR, ...dirs].filter(Boolean).join(", ") || "none"})` });
353
+ const depth = Math.max(0, Number.parseInt(env.FORGE_HEAVY_DEPTH, 10) || 0);
354
+ if (depth >= SHIM_DEPTH_MAX) return loop(`re-entered the heavy shim ${depth} levels deep (max ${SHIM_DEPTH_MAX})`);
345
355
  const repeat = chain.includes(`${name}:${cmd}`);
346
- if (repeat && !isPassThroughWrapper(read(cmd), name)) return { cmd, env };
356
+ if (repeat && !isPassThroughWrapper(read(cmd), name)) return { cmd, env: { ...env, FORGE_HEAVY_DEPTH: String(depth + 1) } };
347
357
  if (chain.length >= SHIM_LOOP_MAX) return loop(`re-entered the heavy shim more than ${SHIM_LOOP_MAX} times`);
348
358
  if (repeat) {
349
359
  const next = which(name, env.PATH ?? "", [env.FORGE_HEAVY_SHIM_DIR, ...dirs].filter(Boolean));
350
360
  if (!next) return loop(`re-entered via ${cmd} and no other ${name} is on PATH`);
351
361
  cmd = next;
352
362
  }
353
- return { cmd, env: { ...env, FORGE_HEAVY_CHAIN: [...chain, `${name}:${cmd}`].join("|") } };
363
+ return { cmd, env: { ...env, FORGE_HEAVY_DEPTH: String(depth + 1), FORGE_HEAVY_CHAIN: [...chain, `${name}:${cmd}`].join("|") } };
354
364
  }
355
365
 
356
366
  /**
@@ -6,7 +6,7 @@ import { spawnSync } from "node:child_process";
6
6
  import { writeFileSync, readFileSync, mkdirSync, chmodSync, existsSync } from "node:fs";
7
7
  import { join } from "node:path";
8
8
  import { tmpDir } from "../tests/_tmp.mjs";
9
- import { guardChain, isPassThroughWrapper, SHIM_LOOP_MAX, classifyStep, heavyConfig, openHeavy, acquireHeavy, writeHeavyShims, heavyShimScript, sumWaits, runWrapped, HEAVY_BINS, isHeavyProcess } from "./heavy.mjs";
9
+ import { guardChain, isPassThroughWrapper, SHIM_LOOP_MAX, SHIM_DEPTH_MAX, classifyStep, heavyConfig, openHeavy, acquireHeavy, writeHeavyShims, heavyShimScript, sumWaits, runWrapped, HEAVY_BINS, isHeavyProcess } from "./heavy.mjs";
10
10
  import { checkAdmission, ADMISSION_DEFAULTS, localLaneCap } from "./admission.mjs";
11
11
  import { formatAlertLines, formatWaitSummary } from "./orchestrate.mjs";
12
12
  import { readHeavy, readAdmission, liveLaneCap } from "../engine-cli.mjs";
@@ -347,3 +347,57 @@ describe("validation steps and the local lane cap (#4777)", () => {
347
347
  assert.equal(liveLaneCap(null, { cfg: readAdmission(d), host: { cores: () => 16, memAvailableMb: () => 64000 } })(), 2);
348
348
  });
349
349
  });
350
+
351
+ describe("#5148 dynamic-name pass-through wrapper", () => {
352
+ const WRAP = (log) => `#!/bin/bash\nname=\${0##*/}\necho w >> "${log}"\n[ "$(wc -l < "${log}")" -gt 12 ] && exit 99\nself_dir=$(cd "\${0%/*}" && pwd)\nreal=$(PATH=$(printf '%s' "$PATH" | tr ':' '\\n' | grep -vxF "$self_dir" | paste -sd:) command -v "$name")\nexec "$real" "$@"\n`;
353
+ const fixture = () => {
354
+ const d = tmpDir("heavy-5148-"), wrap = join(d, "wrap"), real = join(d, "real"), bin = join(d, "bin"), log = join(d, "log");
355
+ for (const x of [wrap, real, bin]) mkdirSync(x, { recursive: true });
356
+ writeFileSync(log, "");
357
+ writeFileSync(join(wrap, "docker"), WRAP(log));
358
+ writeFileSync(join(real, "docker"), `#!/bin/sh\necho r >> ${log}\necho real-docker\n`);
359
+ for (const x of [wrap, real]) chmodSync(join(x, "docker"), 0o755);
360
+ return { d, wrap, real, bin, log };
361
+ };
362
+ const lines = (log) => readFileSync(log, "utf8").split("\n").filter(Boolean);
363
+
364
+ it("#5148: a ${0##*/} pass-through wrapper on PATH does not recurse through the heavy shim", async () => {
365
+ const { wrap, real, bin, log } = fixture();
366
+ assert.ok(isPassThroughWrapper(readFileSync(join(wrap, "docker"), "utf8"), "docker"));
367
+ assert.ok(isPassThroughWrapper('#!/bin/sh\nexec "$(command -v "$(basename "$0")")" "$@"', "docker"));
368
+ assert.ok(!isPassThroughWrapper("#!/bin/sh\necho hi\n", "docker"));
369
+ assert.ok(!isPassThroughWrapper('#!/bin/sh\nx=git\nexec "$(command -v "$x")" "$@"', "docker"), "dynamic lookup without a $0-derived name is not a wrapper");
370
+ assert.deepEqual(await writeHeavyShims(bin, { path: `${wrap}:${real}`, warn: () => {} }), ["docker"]);
371
+ assert.match(readFileSync(join(bin, "docker"), "utf8"), new RegExp(`-- '${real}/docker' "\\$@"`));
372
+ const env = { ...process.env, PATH: `${bin}:${wrap}:${real}:/usr/bin:/bin`, FORGE_HEAVY_DISABLED: "1" };
373
+ const r = spawnSync(join(bin, "docker"), ["ps"], { env, encoding: "utf8", timeout: 20000 });
374
+ assert.equal(r.status, 0, r.stderr);
375
+ assert.match(r.stdout, /real-docker/);
376
+ assert.deepEqual(lines(log), ["r"], "the wrapper never ran; the real binary ran once");
377
+ });
378
+
379
+ it("#5148: with the shim baking the wrapper, the call reaches the real binary within SHIM_LOOP_MAX levels", () => {
380
+ const { wrap, real, bin, log } = fixture();
381
+ writeFileSync(join(bin, "docker"), heavyShimScript("docker", join(wrap, "docker"))); chmodSync(join(bin, "docker"), 0o755);
382
+ const env = { ...process.env, PATH: `${bin}:${wrap}:${real}:/usr/bin:/bin`, FORGE_HEAVY_DISABLED: "1" };
383
+ const r = spawnSync(join(bin, "docker"), ["ps"], { env, encoding: "utf8", timeout: 20000 });
384
+ assert.equal(r.status, 0, r.stderr);
385
+ assert.ok(lines(log).length <= SHIM_LOOP_MAX + 1, lines(log).join(","));
386
+ assert.equal(lines(log).at(-1), "r");
387
+ });
388
+
389
+ it("#5148: guardChain stops at SHIM_DEPTH_MAX even for a repeat it judges legitimate, and counts each entry", () => {
390
+ const env = { PATH: "/s:/r", FORGE_HEAVY_SHIM_DIR: "/s", FORGE_HEAVY_CHAIN: "npm:/r/npm", FORGE_HEAVY_DEPTH: String(SHIM_DEPTH_MAX) };
391
+ assert.match(guardChain("npm", "/r/npm", env, () => "").error, /^env\.shim_loop: npm .*levels deep/);
392
+ const ok = guardChain("npm", "/r/npm", { ...env, FORGE_HEAVY_DEPTH: String(SHIM_DEPTH_MAX - 1) }, () => "");
393
+ assert.equal(ok.error, undefined); assert.equal(ok.env.FORGE_HEAVY_DEPTH, String(SHIM_DEPTH_MAX));
394
+ });
395
+
396
+ it("#5148: under systemd-run TasksMax the shimmed call finishes without hitting the limit", { skip: spawnSync("systemd-run", ["--user", "--scope", "-q", "true"], { timeout: 10000 }).status !== 0 && "no systemd --user manager" }, () => {
397
+ const { wrap, real, bin, log } = fixture();
398
+ writeFileSync(join(bin, "docker"), heavyShimScript("docker", join(wrap, "docker"))); chmodSync(join(bin, "docker"), 0o755);
399
+ const r = spawnSync("systemd-run", ["--user", "--scope", "-q", "-p", "TasksMax=64", "env", `PATH=${bin}:${wrap}:${real}:/usr/bin:/bin`, "FORGE_HEAVY_DISABLED=1", join(bin, "docker"), "ps"], { encoding: "utf8", timeout: 30000 });
400
+ assert.equal(r.status, 0, r.stderr);
401
+ assert.ok(lines(log).length < 13);
402
+ });
403
+ });
@@ -31,20 +31,49 @@ const C = PREFLIGHT_CODES;
31
31
  /** A remote or hybrid execution is configured (the only case the pool checks apply to). */
32
32
  export const usesPool = (exec) => Boolean(exec) && (exec.mode === "remote" || exec.mode === "hybrid") && ["pool", "remote"].includes(exec.provider ?? "pool");
33
33
 
34
- /** The pool token resolves (env/keychain `token_env`, else the router's `forgedock_token_file`) and the router does not answer 401/403 to an authenticated ping. */
35
- export async function checkPoolAuth({ exec, router = null, credential = () => null, ping = null }) {
34
+ /** #5131: `a` and `b` name the same `scheme://host:port` (default ports filled, path and trailing slash ignored). */
35
+ const sameEndpoint = (a, b) => {
36
+ const key = (u) => { try { const x = new URL(String(u)); return `${x.protocol}//${x.hostname}:${x.port || (x.protocol === "https:" ? "443" : "80")}`; } catch { return null; } };
37
+ return key(a) !== null && key(a) === key(b);
38
+ };
39
+
40
+ /**
41
+ * The injectable stand-in for execution.mjs `poolCredential` when no `poolToken` is given (tests, older callers): `credential`
42
+ * on `token_env` (default COMPUTESHARE_TOKEN), then the router token, for `remote` only when its endpoint is unset or the router's.
43
+ */
44
+ export const legacyPoolToken = (credential = () => null) => (exec, router) => {
45
+ const name = exec?.tokenEnv ?? "COMPUTESHARE_TOKEN";
46
+ const v = credential(name);
47
+ if (v) return { token: v, source: name };
48
+ const ok = Boolean(router?.token) && ((exec?.provider ?? "pool") === "pool" || !exec?.endpoint || sameEndpoint(exec.endpoint, router.endpoint));
49
+ return ok ? { token: router.token, source: "forgedock_token_file" } : { token: null, source: null };
50
+ };
51
+
52
+ /** The sources a resolver tries for `exec` (names only, never values), for a FAIL detail. */
53
+ const sourcesTried = (exec, router) => [
54
+ `env/keychain ${exec.tokenEnv ?? (exec.provider === "remote" ? "(no token_env)" : "COMPUTESHARE_TOKEN")}`,
55
+ ...(exec.tokenFile ? [`token_file ${exec.tokenFile}`] : []),
56
+ !router ? "no router config (COMPUTESHARE_CONFIG / ~/.config/computeshare/router.json)"
57
+ : (exec.provider ?? "pool") === "remote" && exec.endpoint && !sameEndpoint(exec.endpoint, router.endpoint) ? `the router's forgedock_token_file (not used: endpoint ${exec.endpoint} is not the local router ${router.endpoint})`
58
+ : "the router's forgedock_token_file",
59
+ ].join(", ");
60
+
61
+ /**
62
+ * The pool token resolves through `poolToken(exec, router) -> {token, source}` (#5131: hostLaneIo wires execution.mjs
63
+ * poolCredential with the dispatch env, so preflight and lane dispatch resolve the same token), else `credential`/`router`
64
+ * (legacyPoolToken), and the router does not answer 401/403 to an authenticated ping. Details name sources, never values.
65
+ */
66
+ export async function checkPoolAuth({ exec, router = null, credential = () => null, ping = null, poolToken = null }) {
36
67
  const code = C.PREFLIGHT_POOL_AUTH;
37
68
  if (!usesPool(exec)) return pass(code, "skipped: no remote or hybrid execution", "remote");
38
- const name = exec.tokenEnv ?? "COMPUTESHARE_TOKEN";
39
- const fromEnv = credential(name);
40
- const token = fromEnv || router?.token || null;
41
- if (!token) return fail(code, `pool token does not resolve: tried env/keychain ${name}, ${router ? "the router's forgedock_token_file" : "no router config (COMPUTESHARE_CONFIG / ~/.config/computeshare/router.json)"}`, "remote");
69
+ const { token, source } = (poolToken ?? legacyPoolToken(credential))(exec, router) ?? {};
70
+ if (!token) return fail(code, `pool token does not resolve: tried ${sourcesTried(exec, router)}`, "remote");
42
71
  const endpoint = exec.endpoint ?? router?.endpoint ?? null;
43
- if (!endpoint || typeof ping !== "function") return pass(code, `token resolved from ${fromEnv ? name : "forgedock_token_file"}`, "remote");
72
+ if (!endpoint || typeof ping !== "function") return pass(code, `token resolved from ${source}`, "remote");
44
73
  let r = null;
45
74
  try { r = await ping(endpoint, token); } catch { r = null; }
46
- if ([401, 403].includes(r?.status)) return fail(code, `router ${endpoint} answered HTTP ${r.status} to the token from ${fromEnv ? name : "forgedock_token_file"}`, "remote");
47
- return pass(code, `token resolved from ${fromEnv ? name : "forgedock_token_file"}${r ? "" : "; router unreachable (not an auth failure)"}`, "remote");
75
+ if ([401, 403].includes(r?.status)) return fail(code, `router ${endpoint} answered HTTP ${r.status} to the token from ${source}`, "remote");
76
+ return pass(code, `token resolved from ${source}${r ? "" : "; router unreachable (not an auth failure)"}`, "remote");
48
77
  }
49
78
 
50
79
  /** Every online pool agent runs the CLI's version. `agents` is the router's `GET /agents` list (each spreads versionOf: version, image). */
@@ -99,9 +128,11 @@ export async function runLaneChecks({ exec = null, env = process.env, cliVersion
99
128
  let router = null;
100
129
  try { router = io.router?.() ?? null; } catch { router = null; }
101
130
  const ping = io.ping ?? null;
102
- const agentsOf = async () => { try { return (await io.agents?.(exec?.endpoint ?? router?.endpoint, exec?.tokenEnv ? (io.credential?.(exec.tokenEnv) ?? router?.token) : router?.token)) ?? null; } catch { return null; } };
131
+ // #5131: one resolver for the auth check and the agents read: io.poolToken (poolCredential), else the injected credential/router.
132
+ const poolToken = io.poolToken ?? legacyPoolToken(io.credential ?? (() => null));
133
+ const agentsOf = async () => { try { return (await io.agents?.(exec?.endpoint ?? router?.endpoint, poolToken(exec, router)?.token ?? null)) ?? null; } catch { return null; } };
103
134
  return Promise.all([
104
- safe(C.PREFLIGHT_POOL_AUTH, "remote", () => checkPoolAuth({ exec, router, credential: io.credential, ping })),
135
+ safe(C.PREFLIGHT_POOL_AUTH, "remote", () => checkPoolAuth({ exec, router, poolToken, ping })),
105
136
  safe(C.PREFLIGHT_POOL_VERSION, "remote", async () => checkPoolVersion({ exec, agents: usesPool(exec) ? await agentsOf() : null, cliVersion: cliV })),
106
137
  safe(C.PREFLIGHT_PATH_LOOP, "all", () => checkPathLoop({ env: { ...env, PATH: env.PATH ?? "" }, ...(io.loops ? { loops: io.loops } : {}) })),
107
138
  safe(C.PREFLIGHT_GH_FIELDS, "all", () => (io.ghExec ? checkGhFields({ exec: io.ghExec }) : pass(C.PREFLIGHT_GH_FIELDS, "skipped: no gh", "all"))),
@@ -113,7 +144,7 @@ export const formatLaneChecks = (checks) => checks.map((c) => `${c.ok ? "PASS" :
113
144
 
114
145
  /** Real-host io for the daemon, the batch and `doctor --lanes`. gh output is cached per process (it cannot change under a running engine). */
115
146
  export async function hostLaneIo({ env = process.env } = {}) {
116
- const { poolRouterConfig } = await import("./execution.mjs");
147
+ const { poolCredential, poolRouterConfig } = await import("./execution.mjs");
117
148
  const { resolveCredential } = await import("./credentials.mjs");
118
149
  const { execFile } = await import("node:child_process");
119
150
  const call = (url, token, timeoutMs = 5000) => fetch(url, { headers: token ? { authorization: `Bearer ${token}` } : {}, signal: AbortSignal.timeout(timeoutMs) });
@@ -122,6 +153,7 @@ export async function hostLaneIo({ env = process.env } = {}) {
122
153
  return {
123
154
  router: () => poolRouterConfig({ env }),
124
155
  credential: (n) => resolveCredential(n, { env }),
156
+ poolToken: (exec, router) => poolCredential(exec, { env, router }), // #5131: the dispatch resolver on the dispatch env
125
157
  ping: async (endpoint, token) => ({ status: (await call(`${endpoint}/status`, token)).status }),
126
158
  agents: async (endpoint, token) => { const r = await call(`${endpoint}/agents`, token); return r.ok ? (await r.json())?.agents ?? null : null; },
127
159
  ghExec: (args) => { const k = args.join(" "); if (!memo.has(k)) memo.set(k, run(args)); return memo.get(k); },
@@ -31,6 +31,29 @@ describe("pool_auth", () => {
31
31
  });
32
32
  });
33
33
 
34
+ describe("pool_auth shared resolver (#5131)", () => {
35
+ const remote = { mode: "remote", provider: "remote", endpoint: "https://runner.example.com", tokenEnv: "COMPUTESHARE_TOKEN" };
36
+ const router = { endpoint: "http://127.0.0.1:7710", token: "router-tok" };
37
+ it("5131: FAILs remote on a non-matching endpoint although a router token exists, naming sources never the value", async () => {
38
+ const a = await checkPoolAuth({ exec: remote, router, credential: () => null });
39
+ assert.equal(a.ok, false); assert.equal(a.code, "preflight.pool_auth"); assert.equal(a.scope, "remote");
40
+ assert.match(a.detail, /COMPUTESHARE_TOKEN/); assert.match(a.detail, /not the local router/); assert.doesNotMatch(a.detail, /router-tok/);
41
+ const viaIo = await checkPoolAuth({ exec: remote, router, poolToken: () => ({ token: null, source: null }) });
42
+ assert.equal(viaIo.ok, false);
43
+ assert.equal((await checkPoolAuth({ exec: { ...remote, endpoint: "http://127.0.0.1:7710/" }, router, credential: () => null })).ok, true, "matching endpoint: router token applies");
44
+ });
45
+ it("5131: runLaneChecks uses io.poolToken for the auth check and the agents read, and FAILs a rejected token", async () => {
46
+ const calls = [];
47
+ const io = { router: () => router, poolToken: (exec, r) => { calls.push([exec.provider, r?.endpoint]); return { token: "tok", source: "token_file" }; },
48
+ ping: async (_e, t) => ({ status: t === "tok" ? 401 : 200 }), agents: async (_e, t) => { calls.push(["agents", t]); return []; } };
49
+ const checks = await runLaneChecks({ exec: remote, env: { PATH: "" }, io: { ...io, loops: () => [] } });
50
+ const auth = checks.find((c) => c.code === "preflight.pool_auth");
51
+ assert.equal(auth.ok, false); assert.match(auth.detail, /HTTP 401/); assert.match(auth.detail, /token_file/); assert.doesNotMatch(auth.detail, /\btok\b/);
52
+ assert.ok(calls.some((c) => c[0] === "agents" && c[1] === "tok"));
53
+ assert.ok(calls.some((c) => c[0] === "remote" && c[1] === router.endpoint));
54
+ });
55
+ });
56
+
34
57
  describe("pool_version", () => {
35
58
  it("reports the agent, image version, CLI version and upgrade command", () => {
36
59
  const c = checkPoolVersion({ exec: pool, cliVersion: "1.39.0", agents: [{ name: "a1", ok: true, version: "1.37.0" }, { name: "a2", ok: true, version: "1.39.0" }] });