mirror of
https://github.com/getpaseo/paseo.git
synced 2026-11-07 08:54:42 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d32e973d80 | ||
|
|
4153b36023 | ||
|
|
cd5155af42 |
+2
-1
@@ -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,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",
|
||||
|
||||
Reference in New Issue
Block a user