fix(server): preserve provider environment during availability probes

This commit is contained in:
paseo-bot[bot]
2026-11-02 20:39:33 +00:00
parent 8854cd4c3b
commit 47953bce00
3 changed files with 89 additions and 12 deletions
@@ -65,11 +65,15 @@ export async function probeExecutable(
executablePath: string,
options: number | ExecutableResolutionOptions = PROBE_TIMEOUT_MS,
): Promise<boolean> {
const { probeTimeoutMs: timeoutMs = PROBE_TIMEOUT_MS, env } =
typeof options === "number" ? { probeTimeoutMs: options } : options;
const {
probeTimeoutMs: timeoutMs = PROBE_TIMEOUT_MS,
env,
envMode,
} = typeof options === "number" ? { probeTimeoutMs: options } : options;
try {
await execCommand(executablePath, ["--version"], {
baseEnv: env,
envMode,
timeout: timeoutMs,
killSignal: "SIGKILL",
maxBuffer: 64 * 1024,
@@ -140,14 +144,19 @@ export async function findExecutableWithoutProbing(
export interface ExecutableResolutionOptions {
probeTimeoutMs?: number;
env?: ProcessEnvRecord;
/** Preserve an environment already finalized by the caller's launch policy. */
envMode?: "external" | "internal";
}
export async function findExecutable(
name: string,
options: number | ExecutableResolutionOptions = PROBE_TIMEOUT_MS,
): Promise<string | null> {
const { probeTimeoutMs = PROBE_TIMEOUT_MS, env } =
typeof options === "number" ? { probeTimeoutMs: options } : options;
const {
probeTimeoutMs = PROBE_TIMEOUT_MS,
env,
envMode,
} = typeof options === "number" ? { probeTimeoutMs: options } : options;
const trimmed = name.trim();
if (!trimmed) {
return null;
@@ -157,19 +166,19 @@ export async function findExecutable(
return windowsExecutableResolution.find(trimmed, {
enumeratePathCandidates: (command) => enumerateCandidates(command, env),
probeExecutable: (command, timeout) =>
probeExecutable(command, { probeTimeoutMs: timeout, env }),
probeExecutable(command, { probeTimeoutMs: timeout, env, envMode }),
exists: existsSync,
probeTimeoutMs,
});
}
if (hasPathSeparator(trimmed)) {
return (await probeExecutable(trimmed, { probeTimeoutMs, env })) ? trimmed : null;
return (await probeExecutable(trimmed, { probeTimeoutMs, env, envMode })) ? trimmed : null;
}
const candidates = await enumerateCandidates(trimmed, env);
for (const candidate of candidates) {
if (await probeExecutable(candidate, { probeTimeoutMs, env })) {
if (await probeExecutable(candidate, { probeTimeoutMs, env, envMode })) {
return candidate;
}
}
@@ -1,4 +1,4 @@
import { chmodSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
import { chmodSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import path from "node:path";
import { afterEach, describe, expect, test } from "vitest";
@@ -189,6 +189,71 @@ describe("resolveProviderLaunch", () => {
});
describe("checkProviderLaunchAvailable", () => {
test.each([
{ lookup: "absolute", configured: true },
{ lookup: "PATH", configured: true },
{ lookup: "default", configured: true },
{ lookup: "absolute", configured: false },
{ lookup: "PATH", configured: false },
{ lookup: "default", configured: false },
])(
"passes finalized provider environment to the $lookup probe (configured=$configured)",
async ({ lookup, configured }) => {
const dir = makeTempDir();
const observed = path.join(dir, "probe.json");
const script = path.join(dir, "probe.cjs");
writeFileSync(
script,
`require("node:fs").writeFileSync(${JSON.stringify(observed)}, JSON.stringify({
args: process.argv.slice(2),
mode: process.env.ELECTRON_RUN_AS_NODE ?? null,
inherited: process.env.PASEO_NODE_ENV ?? null,
parent: process.env.CLAUDECODE ?? null,
ordinary: process.env.CUSTOM
}));`,
);
const command = process.platform === "win32" ? "provider.cmd" : "provider";
const executable = path.join(dir, command);
writeFileSync(
executable,
process.platform === "win32"
? `@echo off\r\n"${process.execPath}" "${script}" %*\r\n`
: `#!/bin/sh\nexec '${process.execPath.replaceAll("'", "'\\''")}' '${script.replaceAll("'", "'\\''")}' "$@"\n`,
);
chmodSync(executable, 0o755);
const env = createProviderEnv({
baseEnv: {
...process.env,
ELECTRON_RUN_AS_NODE: "inherited",
PASEO_NODE_ENV: "production",
CLAUDECODE: "1",
},
runtimeSettings: { env: configured ? { ELECTRON_RUN_AS_NODE: "1" } : {} },
overlays: [{ CUSTOM: "session", PASEO_NODE_ENV: "development" }],
});
env.PATH = `${dir}${path.delimiter}${env.PATH ?? ""}`;
const launch = await resolveProviderLaunch({
commandConfig:
lookup === "default"
? undefined
: { mode: "replace", argv: [lookup === "absolute" ? executable : command] },
defaultBinary: command,
});
await expect(checkProviderLaunchAvailable({ ...launch, env }, { command })).resolves.toEqual({
available: true,
resolvedPath: executable,
});
expect(JSON.parse(readFileSync(observed, "utf8"))).toEqual({
args: ["--version"],
mode: configured ? "1" : null,
inherited: null,
parent: null,
ordinary: "session",
});
},
);
test("reports available with a resolved path", async () => {
const binDir = makeTempDir();
const binary = createExecutable(binDir, "default-provider");
@@ -44,7 +44,8 @@ export interface ResolvedProviderLaunch {
command: string;
args: string[];
source: ProviderLaunchSource;
env?: ProcessEnvRecord;
/** Finalized provider environment from createProviderEnv(), ready for child execution. */
env?: ExternalProcessEnv;
}
export interface ProviderLaunchAvailability {
@@ -67,7 +68,8 @@ function normalizeLaunchDefault(
}
async function resolveLaunchPath(command: string, env?: ProcessEnvRecord): Promise<string | null> {
const found = await findExecutable(command, { env });
// launch.env is finalized by the provider builder, including explicit runtime controls.
const found = await findExecutable(command, { env, envMode: env ? "internal" : "external" });
if (found) {
return found;
}
@@ -79,10 +81,11 @@ async function resolveLaunchPath(command: string, env?: ProcessEnvRecord): Promi
async function resolveDefaultLaunchPath(
defaultBinary: ProviderLaunchDefault,
env?: ProcessEnvRecord,
): Promise<string | null> {
return defaultBinary.resolvePath
? await defaultBinary.resolvePath()
: await resolveLaunchPath(defaultBinary.command);
: await resolveLaunchPath(defaultBinary.command, env);
}
export interface ResolveProviderLaunchOptions {
@@ -121,7 +124,7 @@ export async function checkProviderLaunchAvailable(
): Promise<ProviderLaunchAvailability> {
const resolvedPath =
defaultBinary && launch.source !== "override"
? await resolveDefaultLaunchPath(defaultBinary)
? await resolveDefaultLaunchPath(defaultBinary, launch.env)
: await resolveLaunchPath(launch.command, launch.env);
return {
available: resolvedPath !== null,