From 47953bce00522bf17e145045127821b885f17f3d Mon Sep 17 00:00:00 2001 From: "paseo-bot[bot]" <266920839+paseo-bot[bot]@users.noreply.github.com> Date: Mon, 2 Nov 2026 20:39:33 +0000 Subject: [PATCH] fix(server): preserve provider environment during availability probes --- .../executable-resolution.ts | 23 +++++-- .../agent/provider-launch-config.test.ts | 67 ++++++++++++++++++- .../server/agent/provider-launch-config.ts | 11 +-- 3 files changed, 89 insertions(+), 12 deletions(-) diff --git a/packages/server/src/executable-resolution/executable-resolution.ts b/packages/server/src/executable-resolution/executable-resolution.ts index 064dcec64e..863363c018 100644 --- a/packages/server/src/executable-resolution/executable-resolution.ts +++ b/packages/server/src/executable-resolution/executable-resolution.ts @@ -65,11 +65,15 @@ export async function probeExecutable( executablePath: string, options: number | ExecutableResolutionOptions = PROBE_TIMEOUT_MS, ): Promise { - 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 { - 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; } } diff --git a/packages/server/src/server/agent/provider-launch-config.test.ts b/packages/server/src/server/agent/provider-launch-config.test.ts index 31a0c42cbb..afc6567fa3 100644 --- a/packages/server/src/server/agent/provider-launch-config.test.ts +++ b/packages/server/src/server/agent/provider-launch-config.test.ts @@ -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"); diff --git a/packages/server/src/server/agent/provider-launch-config.ts b/packages/server/src/server/agent/provider-launch-config.ts index 88d8c59119..72956d72e1 100644 --- a/packages/server/src/server/agent/provider-launch-config.ts +++ b/packages/server/src/server/agent/provider-launch-config.ts @@ -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 { - 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 { 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 { const resolvedPath = defaultBinary && launch.source !== "override" - ? await resolveDefaultLaunchPath(defaultBinary) + ? await resolveDefaultLaunchPath(defaultBinary, launch.env) : await resolveLaunchPath(launch.command, launch.env); return { available: resolvedPath !== null,