Compare commits

...
Author SHA1 Message Date
paseo-bot[bot] d32e973d80 fix(server): preserve provider environment during availability probes 2026-11-04 08:26:37 +00:00
paseo-bot[bot]andOkan YILDIRIM 4153b36023 fix(claude): preserve configured env through current launch handling
Keep the finalized provider environment when applying the launch-only client identity. Cover configured runtime controls in actual diagnostic and SDK child processes.

Co-authored-by: Okan YILDIRIM <63511519+ragokan@users.noreply.github.com>
2026-11-04 08:26:37 +00:00
Okan YILDIRIM cd5155af42 fix(server): honor explicit provider runtime env overrides
Restore runtime controls only from configured provider env, while preserving inherited and session filtering. Carry finalized provider environments through spawn boundaries so a second scrub cannot drop explicit settings.

Cover configured controls across ACP launches and OpenCode V2 reconnect/resume, including both environment-builder consumer paths. Keep parent-session and inherited control filtering checks, and identify Pi/OMP Node RPC fixture coverage accurately.
2026-11-04 08:26:37 +00:00
17 changed files with 347 additions and 57 deletions
+2 -1
View File
@@ -493,7 +493,8 @@ export default function contribute(server: PluginServerContext) {
Declare `command: ["agent", "serve"]` when your provider launches an executable. The daemon applies
`agents.providers.<id>.command` and `env`, resolves the executable against the effective PATH, and
removes parent-session and daemon-control environment variables. `connect({ launch })` receives
removes parent-session variables and inherited/session daemon-control values; explicitly configured
provider env survives. `connect({ launch })` receives
`{ command, args, env }`: an executable path, effective arguments, and the complete sanitized
environment. Spawn with those values; do not merge the plugin process's environment back in.
The launch travels as data through the same provider protocol for built-in and subprocess plugins.
@@ -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;
}
}
@@ -4,7 +4,8 @@ import { join } from "node:path";
import { AgentStorage } from "./agent-storage.js";
import { createIdleRuntimeCleanup } from "./idle-runtime-cleanup/index.js";
import { relative } from "node:path";
import { execFileSync } from "node:child_process";
import { promisify } from "node:util";
import { execFile, execFileSync } from "node:child_process";
import { fileURLToPath } from "node:url";
import type {
ProviderConnection,
@@ -1858,6 +1859,48 @@ describe("pending provider responses", () => {
});
describe("plugin provider launch and status", () => {
test("passes configured Electron mode to a real diagnostic child process", async () => {
const logger = createTestLogger();
const registry = new PluginAgentClientRegistry(logger);
registry.replace([
{
id: "environment-probe",
label: "Environment probe",
command: [
process.execPath,
"-e",
"console.log(JSON.stringify({ mode: process.env.ELECTRON_RUN_AS_NODE ?? null, internal: process.env.PASEO_NODE_ENV ?? null, parent: process.env.CLAUDECODE ?? null }))",
],
async status({ launch }) {
if (!launch) throw new Error("Missing provider launch");
const { stdout } = await promisify(execFile)(launch.command, launch.args, {
env: launch.env,
timeout: 5000,
});
return { available: true, diagnostic: stdout.trim() };
},
async connect() {
throw new Error("A diagnostic does not need a provider connection");
},
},
]);
const definition = registry.definitions()["environment-probe"]!;
const client = definition.createClient(
logger,
ProviderOverrideSchema.parse({ env: { ELECTRON_RUN_AS_NODE: "1" } }),
);
try {
const diagnostic = await client.getDiagnostic!();
expect(JSON.parse(diagnostic.diagnostic)).toEqual({
mode: "1",
internal: null,
parent: null,
});
} finally {
await registry.shutdown();
}
});
test("resolves the executable against the provider's PATH override", async () => {
const harness = createProviderHarness();
const registration: ProviderRegistration = { ...harness.registration, command: ["node"] };
@@ -1975,7 +2018,12 @@ describe("plugin provider launch and status", () => {
try {
const client = registry.definitions()[registration.id]!.createClient(logger, {
command,
env: { PLUGIN_SETTING: "custom", CLAUDECODE: "parent" },
env: {
PLUGIN_SETTING: "custom",
CLAUDECODE: "parent",
ELECTRON_RUN_AS_NODE: "1",
ESBUILD_BINARY_PATH: "/provider/esbuild",
},
});
expect(await client.isAvailable()).toBe(true);
expect(await client.getCatalogCacheKey!({ scope: "global" })).toBe("shared");
@@ -1988,7 +2036,11 @@ describe("plugin provider launch and status", () => {
expect(launch).toMatchObject({
command: process.execPath,
args: expectedArgs,
env: { PLUGIN_SETTING: "custom" },
env: {
PLUGIN_SETTING: "custom",
ELECTRON_RUN_AS_NODE: "1",
ESBUILD_BINARY_PATH: "/provider/esbuild",
},
});
expect(launch!.env).not.toHaveProperty("CLAUDECODE");
expect(launch!.env).not.toHaveProperty("PASEO_NODE_ENV");
@@ -1,11 +1,13 @@
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";
import { execCommand } from "../../utils/spawn.js";
import {
checkProviderLaunchAvailable,
createProviderEnv,
createProviderEnvSpec,
migrateProviderSettings,
ProviderOverrideSchema,
resolveProviderLaunch,
@@ -187,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");
@@ -217,6 +284,63 @@ describe("checkProviderLaunchAvailable", () => {
});
describe("createProviderEnv", () => {
test("passes configured runtime controls through command execution", async () => {
const result = await execCommand(
process.execPath,
[
"-e",
"console.log(JSON.stringify({ ELECTRON_RUN_AS_NODE: process.env.ELECTRON_RUN_AS_NODE ?? null, PASEO_NODE_ENV: process.env.PASEO_NODE_ENV ?? null }))",
],
createProviderEnvSpec({
baseEnv: { ELECTRON_RUN_AS_NODE: "inherited", PASEO_NODE_ENV: "production" },
runtimeSettings: { env: { ELECTRON_RUN_AS_NODE: "1" } },
overlays: [{ PASEO_NODE_ENV: "development" }],
}),
);
expect(result).toEqual({
stdout: '{"ELECTRON_RUN_AS_NODE":"1","PASEO_NODE_ENV":null}\n',
stderr: "",
});
});
test.each([
"ELECTRON_RUN_AS_NODE",
"ELECTRON_NO_ATTACH_CONSOLE",
"PASEO_NODE_ENV",
"PASEO_DESKTOP_MANAGED",
"PASEO_SUPERVISED",
"ESBUILD_BINARY_PATH",
])("preserves explicitly configured %s while scrubbing inherited and session controls", (key) => {
const baseEnv = {
PATH: "/usr/bin",
ELECTRON_RUN_AS_NODE: "inherited",
PASEO_NODE_ENV: "inherited",
ESBUILD_BINARY_PATH: "/inherited/esbuild",
};
const overlays = [
{
[key]: "session",
PASEO_SUPERVISED: "session",
CUSTOM: "session",
REMOVED: undefined,
},
];
const runtimeSettings = {
env: { [key]: "configured", CUSTOM: "configured", REMOVED: "configured", CLAUDECODE: "1" },
};
expect(createProviderEnv({ baseEnv, runtimeSettings, overlays })).toEqual({
PATH: "/usr/bin",
CUSTOM: "session",
[key]: "configured",
});
expect(createProviderEnv({ baseEnv, overlays })).toEqual({
PATH: "/usr/bin",
CUSTOM: "session",
});
});
test("merges provider env overrides", () => {
const base = {
PATH: "/usr/bin",
@@ -5,6 +5,7 @@ import {
} from "../../executable-resolution/executable-resolution.js";
import {
createExternalProcessEnv,
RUNTIME_CONTROL_ENV_KEYS,
type ProcessEnvRecord,
type ExternalProcessEnv,
} from "../paseo-env.js";
@@ -43,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 {
@@ -66,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;
}
@@ -78,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 {
@@ -120,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,
@@ -222,8 +226,8 @@ export interface ProviderEnvOptions {
}
export interface ProviderEnvSpec {
baseEnv?: ProcessEnvRecord;
envOverlay: ProcessEnvRecord;
env: ExternalProcessEnv;
envMode: "internal";
}
function collectProviderEnvOverlays(
@@ -236,20 +240,24 @@ function collectProviderEnvOverlays(
}
export function createProviderEnvSpec(options: ProviderEnvOptions = {}): ProviderEnvSpec {
const overlays = collectProviderEnvOverlays(options.runtimeSettings, options.overlays ?? []);
const envOverlay: ProcessEnvRecord = Object.assign({}, ...overlays);
for (const key of PARENT_SESSION_ENV_VARS) {
envOverlay[key] = undefined;
}
return {
...(options.baseEnv ? { baseEnv: options.baseEnv } : {}),
envOverlay,
};
// Provider env is finalized here so spawning cannot scrub explicit user settings again.
return { env: createProviderEnv(options), envMode: "internal" };
}
export function createProviderEnv(options: ProviderEnvOptions = {}): ExternalProcessEnv {
const spec = createProviderEnvSpec(options);
return createExternalProcessEnv(spec.baseEnv ?? process.env, spec.envOverlay);
const overlays = collectProviderEnvOverlays(options.runtimeSettings, options.overlays ?? []);
const envOverlay: ProcessEnvRecord = Object.assign({}, ...overlays);
const env = createExternalProcessEnv(options.baseEnv ?? process.env, envOverlay);
for (const key of RUNTIME_CONTROL_ENV_KEYS) {
const configured = options.runtimeSettings?.env?.[key];
if (configured !== undefined) {
env[key] = configured;
}
}
for (const key of PARENT_SESSION_ENV_VARS) {
delete env[key];
}
return env;
}
export async function isProviderCommandAvailable(
@@ -616,9 +616,11 @@ describe("createLoggedNdJsonStream", () => {
describe("ACPAgentSession terminal tools", () => {
afterEach(() => {
vi.restoreAllMocks();
vi.unstubAllEnvs();
});
test("runs single-string terminal commands through the platform shell", async () => {
vi.stubEnv("BASH_ENV", "/unexpected/bash-env");
const child = createTerminalChildStub();
const spawn = vi.spyOn(spawnUtils, "spawnProcess").mockReturnValue(child);
const session = createSession();
@@ -638,13 +640,15 @@ describe("ACPAgentSession terminal tools", () => {
shell.args,
expect.objectContaining({
cwd: "/repo",
envOverlay: expect.objectContaining({ BASH_ENV: undefined }),
envMode: "internal",
shell: false,
}),
);
expect(spawn.mock.calls[0]?.[2]?.env).not.toHaveProperty("BASH_ENV");
});
test("preserves cmd semantics for single-string terminal commands on Windows", async () => {
vi.stubEnv("BASH_ENV", "/unexpected/bash-env");
const originalPlatform = process.platform;
Object.defineProperty(process, "platform", {
value: "win32",
@@ -666,10 +670,11 @@ describe("ACPAgentSession terminal tools", () => {
["/c", "echo %TEMP% && echo ok"],
expect.objectContaining({
cwd: "C:\\repo",
envOverlay: expect.objectContaining({ BASH_ENV: undefined }),
envMode: "internal",
shell: false,
}),
);
expect(spawn.mock.calls[0]?.[2]?.env).not.toHaveProperty("BASH_ENV");
} finally {
Object.defineProperty(process, "platform", {
value: originalPlatform,
@@ -782,6 +787,7 @@ describe("ACPAgentSession terminal tools", () => {
});
test("carries the agent's identity into single-string shell commands", async () => {
vi.stubEnv("BASH_ENV", "/unexpected/bash-env");
const child = createTerminalChildStub();
const spawn = vi.spyOn(spawnUtils, "spawnProcess").mockReturnValue(child);
const session = createSession({ launchEnv: { PASEO_AGENT_ID: "agent-1" } });
@@ -796,12 +802,13 @@ describe("ACPAgentSession terminal tools", () => {
expect.any(String),
expect.any(Array),
expect.objectContaining({
envOverlay: expect.objectContaining({
env: expect.objectContaining({
PASEO_AGENT_ID: "agent-1",
BASH_ENV: undefined,
}),
envMode: "internal",
}),
);
expect(spawn.mock.calls[0]?.[2]?.env).not.toHaveProperty("BASH_ENV");
});
});
@@ -42,7 +42,13 @@ describe("Claude SDK env", () => {
const query = claudeQuery(
{ prompt: "", options: {} },
{
runtimeSettings: { env: { CLAUDE_CODE_ENTRYPOINT: "sdk-ts" } },
runtimeSettings: {
env: {
CLAUDE_CODE_ENTRYPOINT: "sdk-ts",
ELECTRON_RUN_AS_NODE: "1",
ESBUILD_BINARY_PATH: "/configured/esbuild",
},
},
launchEnv: entrypoint === undefined ? {} : { CLAUDE_CODE_ENTRYPOINT: entrypoint },
queryFactory: ({ options }) => {
if (!options.spawnClaudeCodeProcess) {
@@ -52,7 +58,7 @@ describe("Claude SDK env", () => {
command,
args: [
"-e",
"process.stdout.write(JSON.stringify({ entrypoint: process.env.CLAUDE_CODE_ENTRYPOINT, parentSession: process.env.CLAUDECODE ?? null }))",
"process.stdout.write(JSON.stringify({ entrypoint: process.env.CLAUDE_CODE_ENTRYPOINT, parentSession: process.env.CLAUDECODE ?? null, electronMode: process.env.ELECTRON_RUN_AS_NODE ?? null, esbuild: process.env.ESBUILD_BINARY_PATH ?? null }))",
],
cwd: process.cwd(),
env: { ...process.env, CLAUDE_CODE_ENTRYPOINT: "sdk-ts", CLAUDECODE: "1" },
@@ -72,6 +78,8 @@ describe("Claude SDK env", () => {
expect(JSON.parse(output)).toEqual({
...(entrypoint === undefined ? {} : { entrypoint }),
parentSession: null,
electronMode: "1",
esbuild: "/configured/esbuild",
});
} finally {
query.close();
@@ -5,7 +5,7 @@ import {
createProviderEnvSpec,
type ProviderRuntimeSettings,
} from "../../provider-launch-config.js";
import { buildSelfNodeCommand, createExternalProcessEnv } from "../../../paseo-env.js";
import { buildSelfNodeCommand } from "../../../paseo-env.js";
import { spawnProcess } from "../../../../utils/spawn.js";
// Keep the raw SDK query import in this module only. Claude process launch behavior
@@ -75,8 +75,10 @@ function applyRuntimeSettingsToClaudeOptions(
});
// Session-open hooks may explicitly supply a client identity. Keep inherited
// SDK/parent identities cleared while honoring this launch-only contribution.
providerEnvSpec.envOverlay.CLAUDE_CODE_ENTRYPOINT = launchEnv?.CLAUDE_CODE_ENTRYPOINT;
const providerEnv = createExternalProcessEnv(spawnOptions.env, providerEnvSpec.envOverlay);
const providerEnv = providerEnvSpec.env;
if (launchEnv?.CLAUDE_CODE_ENTRYPOINT !== undefined) {
providerEnv.CLAUDE_CODE_ENTRYPOINT = launchEnv.CLAUDE_CODE_ENTRYPOINT;
}
const selfNodeCommand = isDefaultRuntime
? buildSelfNodeCommand(resolved.args, providerEnv)
: null;
@@ -7534,6 +7534,7 @@ export class CodexAppServerAgentClient implements AgentClient {
detached: process.platform !== "win32",
stdio: ["pipe", "pipe", "pipe"],
baseEnv: options?.environment ?? buildCodexAppServerEnv(this.runtimeSettings, launchEnv),
envMode: "internal",
});
assertChildWithPipes(child);
return child;
@@ -1,7 +1,7 @@
import { type ChildProcess, type ChildProcessWithoutNullStreams } from "node:child_process";
import type { Logger } from "pino";
import { spawnProcess } from "../../../utils/spawn.js";
import { spawnProcess, type SpawnProcessOptions } from "../../../utils/spawn.js";
import { terminateWithTreeKill } from "../../../utils/tree-kill.js";
import { JsonlFrameDecoder, ProviderStreamLimitError } from "./jsonl-frame-decoder.js";
export { supportsJsonlRpcProtocolV2 } from "./jsonl-frame-decoder.js";
@@ -23,6 +23,7 @@ export interface JsonlRpcLaunch {
args: string[];
cwd: string;
env?: Record<string, string>;
envMode?: SpawnProcessOptions["envMode"];
}
interface JsonlRpcResponse {
@@ -65,7 +66,9 @@ function assertChildWithPipes(
function spawnJsonlRpcProcess(launch: JsonlRpcLaunch): ChildProcessWithoutNullStreams {
const child = spawnProcess(launch.command, launch.args, {
cwd: launch.cwd,
envOverlay: launch.env,
...(launch.envMode === "internal"
? { env: launch.env, envMode: launch.envMode }
: { envOverlay: launch.env }),
stdio: ["pipe", "pipe", "pipe"],
});
assertChildWithPipes(child);
@@ -121,6 +121,38 @@ function withoutRequestId(command: Record<string, unknown>): Record<string, unkn
}
describe("OMP CLI runtime", () => {
test("passes configured runtime controls to the spawned RPC process", async () => {
const source = `
const readline = require("node:readline");
console.log(JSON.stringify({ type: "ready" }));
readline.createInterface({ input: process.stdin }).on("line", (line) => {
const command = JSON.parse(line);
const env = {
ELECTRON_RUN_AS_NODE: process.env.ELECTRON_RUN_AS_NODE ?? null,
PASEO_NODE_ENV: process.env.PASEO_NODE_ENV ?? null,
};
console.log(JSON.stringify({ type: "response", id: command.id, success: true,
data: { commands: [{ name: "env", description: JSON.stringify(env) }] } }));
});
`;
const runtime = new OmpCliRuntime({
logger: pino({ level: "silent" }),
command: [process.execPath, "-e", source, "--"],
runtimeSettings: { env: { ELECTRON_RUN_AS_NODE: "1" } },
});
const session = await runtime.startSession({
cwd: process.cwd(),
env: { ELECTRON_RUN_AS_NODE: "session", PASEO_NODE_ENV: "development" },
});
try {
expect(await session.getCommands()).toEqual([
{ name: "env", description: '{"ELECTRON_RUN_AS_NODE":"1","PASEO_NODE_ENV":null}' },
]);
} finally {
await session.close();
}
});
test("steer waits for OMP's response and surfaces rejection", async () => {
const child = createOmpChild();
let pending: Record<string, unknown> | null = null;
@@ -1,9 +1,11 @@
import { createExternalProcessEnv } from "../../../paseo-env.js";
import type { ChildProcessWithoutNullStreams } from "node:child_process";
import type { Logger } from "pino";
import { z } from "zod";
import type { ProviderRuntimeSettings } from "../../provider-launch-config.js";
import {
createProviderEnvSpec,
type ProviderRuntimeSettings,
} from "../../provider-launch-config.js";
import {
JSONL_RPC_NO_TIMEOUT,
JsonlRpcProcess,
@@ -76,13 +78,17 @@ export class OmpCliRuntime implements OmpRuntime {
runtimeSettings: this.options.runtimeSettings,
session: input,
});
launch.env = createExternalProcessEnv(globalThis.process.env, launch.env ?? {});
const envSpec = createProviderEnvSpec({
runtimeSettings: this.options.runtimeSettings,
overlays: [input.env],
});
launch.env = envSpec.env;
const [command, ...args] = launch.argv;
const processLaunch: JsonlRpcLaunch = {
command,
args,
cwd: launch.cwd,
env: launch.env,
...envSpec,
};
const spawn = this.spawnProcess;
const processOptions = {
@@ -345,6 +345,7 @@ export class OpenCodeServerManager implements OpenCodeServerManagerLike {
detached: process.platform !== "win32",
stdio: ["ignore", "pipe", "pipe"],
baseEnv: environment,
envMode: "internal",
});
const managedProcessRecord = this.recordManagedServerProcess({
process: serverProcess,
@@ -593,15 +593,14 @@ describe("OpenCode v2 session lifecycle", () => {
const assertEnvironment = (index: number) => {
const env = environments.get([first, second][index].info.id);
expect(env?.PATH).toBe(process.env.PATH);
expect(env).toMatchObject({ ...launches[index].env, PASEO_ENV_TEST: "configured" });
expect(env).toMatchObject({
...launches[index].env,
PASEO_ENV_TEST: "configured",
ELECTRON_RUN_AS_NODE: "1",
PASEO_SUPERVISED: "1",
});
expect(env?.USER).toBe(process.env.USER);
for (const key of [
"ELECTRON_RUN_AS_NODE",
"PASEO_SUPERVISED",
"CLAUDECODE",
"ESBUILD_BINARY_PATH",
"PASEO_NODE_ENV",
]) {
for (const key of ["CLAUDECODE", "ESBUILD_BINARY_PATH", "PASEO_NODE_ENV"]) {
expect(env).not.toHaveProperty(key);
}
};
@@ -131,6 +131,37 @@ function writePiResponse(
}
describe("PiCliRuntime", () => {
test("passes configured runtime controls to the spawned RPC process", async () => {
const source = `
const readline = require("node:readline");
readline.createInterface({ input: process.stdin }).on("line", (line) => {
const command = JSON.parse(line);
const env = {
ELECTRON_RUN_AS_NODE: process.env.ELECTRON_RUN_AS_NODE ?? null,
PASEO_NODE_ENV: process.env.PASEO_NODE_ENV ?? null,
};
console.log(JSON.stringify({ type: "response", id: command.id, success: true,
data: { commands: [{ name: "env", description: JSON.stringify(env) }] } }));
});
`;
const runtime = new PiCliRuntime({
logger: pino({ level: "silent" }),
command: [process.execPath, "-e", source, "--"],
runtimeSettings: { env: { ELECTRON_RUN_AS_NODE: "1" } },
});
const session = await runtime.startSession({
cwd: process.cwd(),
env: { ELECTRON_RUN_AS_NODE: "session", PASEO_NODE_ENV: "development" },
});
try {
expect(await session.getCommands()).toEqual([
{ name: "env", description: '{"ELECTRON_RUN_AS_NODE":"1","PASEO_NODE_ENV":null}' },
]);
} finally {
await session.close();
}
});
test("starts pi in rpc mode and resolves command responses", async () => {
const child = createPiChild();
replyToCommands(child, (command) =>
@@ -1,10 +1,12 @@
import { readFile } from "node:fs/promises";
import { readPiTranscript, type PiTranscript } from "./transcript.js";
import { createExternalProcessEnv } from "../../../paseo-env.js";
import type { ChildProcessWithoutNullStreams } from "node:child_process";
import type { Logger } from "pino";
import type { ProviderRuntimeSettings } from "../../provider-launch-config.js";
import {
createProviderEnvSpec,
type ProviderRuntimeSettings,
} from "../../provider-launch-config.js";
import {
JSONL_RPC_NO_TIMEOUT,
JsonlRpcProcess,
@@ -58,13 +60,17 @@ export class PiCliRuntime implements PiRuntime {
runtimeSettings: this.options.runtimeSettings,
session: input,
});
launch.env = createExternalProcessEnv(globalThis.process.env, launch.env ?? {});
const envSpec = createProviderEnvSpec({
runtimeSettings: this.options.runtimeSettings,
overlays: [input.env],
});
launch.env = envSpec.env;
const [command, ...args] = launch.argv;
const processLaunch: JsonlRpcLaunch = {
command,
args,
cwd: launch.cwd,
env: launch.env,
...envSpec,
};
const spawn = this.spawnProcess;
const processOptions = {
+1 -1
View File
@@ -1,7 +1,7 @@
const PASEO_NODE_ENV = "PASEO_NODE_ENV";
const ELECTRON_RUN_AS_NODE = "ELECTRON_RUN_AS_NODE";
const RUNTIME_CONTROL_ENV_KEYS = [
export const RUNTIME_CONTROL_ENV_KEYS = [
PASEO_NODE_ENV,
"PASEO_DESKTOP_MANAGED",
"PASEO_SUPERVISED",