ec801665bc
- Migrated environment variable access from direct process.env to centralized getEnv() utility function across all packages. - Renamed environment variable prefix from OMP_ to PI_ throughout codebase (e.g., OMP_CODING_AGENT_DIR -> PI_CODING_AGENT_DIR). - Removed automatic environment variable migration from PI_ to OMP_ prefixes via migrate-env.ts module. - Removed env setting from configuration schema and applyEnvironmentVariables() method from settings. - Updated CI/CD build configuration to use PI_COMPILED flag instead of OMP_COMPILED. - Changed venvPath property in PythonRuntime from nullable (string | null) to optional (string | undefined).
196 lines
5.2 KiB
TypeScript
196 lines
5.2 KiB
TypeScript
/**
|
|
* Python runtime resolution utilities.
|
|
*
|
|
* Centralizes environment filtering, venv detection, and Python executable resolution
|
|
* for both the shared gateway and local kernel spawning.
|
|
*/
|
|
import * as fs from "node:fs";
|
|
import * as path from "node:path";
|
|
|
|
import { getEnv } from "@oh-my-pi/pi-utils";
|
|
|
|
const DEFAULT_ENV_ALLOWLIST = new Set([
|
|
"PATH",
|
|
"HOME",
|
|
"USER",
|
|
"LOGNAME",
|
|
"SHELL",
|
|
"LANG",
|
|
"LC_ALL",
|
|
"LC_CTYPE",
|
|
"LC_MESSAGES",
|
|
"TERM",
|
|
"TERM_PROGRAM",
|
|
"TERM_PROGRAM_VERSION",
|
|
"TMPDIR",
|
|
"TEMP",
|
|
"TMP",
|
|
"XDG_CACHE_HOME",
|
|
"XDG_CONFIG_HOME",
|
|
"XDG_DATA_HOME",
|
|
"XDG_RUNTIME_DIR",
|
|
"SSH_AUTH_SOCK",
|
|
"SSH_AGENT_PID",
|
|
"CONDA_PREFIX",
|
|
"CONDA_DEFAULT_ENV",
|
|
"VIRTUAL_ENV",
|
|
"PYTHONPATH",
|
|
]);
|
|
|
|
const WINDOWS_ENV_ALLOWLIST = new Set([
|
|
"APPDATA",
|
|
"COMPUTERNAME",
|
|
"COMSPEC",
|
|
"HOMEDRIVE",
|
|
"HOMEPATH",
|
|
"LOCALAPPDATA",
|
|
"NUMBER_OF_PROCESSORS",
|
|
"OS",
|
|
"PATH",
|
|
"PATHEXT",
|
|
"PROCESSOR_ARCHITECTURE",
|
|
"PROCESSOR_IDENTIFIER",
|
|
"PROGRAMDATA",
|
|
"PROGRAMFILES",
|
|
"PROGRAMFILES(X86)",
|
|
"PROGRAMW6432",
|
|
"SESSIONNAME",
|
|
"SYSTEMDRIVE",
|
|
"SYSTEMROOT",
|
|
"TEMP",
|
|
"TMP",
|
|
"USERDOMAIN",
|
|
"USERDOMAIN_ROAMINGPROFILE",
|
|
"USERPROFILE",
|
|
"USERNAME",
|
|
"WINDIR",
|
|
]);
|
|
|
|
const DEFAULT_ENV_DENYLIST = new Set([
|
|
"OPENAI_API_KEY",
|
|
"ANTHROPIC_API_KEY",
|
|
"GOOGLE_API_KEY",
|
|
"GEMINI_API_KEY",
|
|
"OPENROUTER_API_KEY",
|
|
"PERPLEXITY_API_KEY",
|
|
"EXA_API_KEY",
|
|
"AZURE_OPENAI_API_KEY",
|
|
"MISTRAL_API_KEY",
|
|
]);
|
|
|
|
const DEFAULT_ENV_ALLOW_PREFIXES = ["LC_", "XDG_", "PI_"];
|
|
|
|
const CASE_INSENSITIVE_ENV = process.platform === "win32";
|
|
const BASE_ENV_ALLOWLIST = new Set([...DEFAULT_ENV_ALLOWLIST, ...WINDOWS_ENV_ALLOWLIST]);
|
|
|
|
const NORMALIZED_ALLOWLIST = new Set(
|
|
Array.from(BASE_ENV_ALLOWLIST, key => (CASE_INSENSITIVE_ENV ? key.toUpperCase() : key)),
|
|
);
|
|
const NORMALIZED_DENYLIST = new Set(
|
|
Array.from(DEFAULT_ENV_DENYLIST, key => (CASE_INSENSITIVE_ENV ? key.toUpperCase() : key)),
|
|
);
|
|
const NORMALIZED_ALLOW_PREFIXES = CASE_INSENSITIVE_ENV
|
|
? DEFAULT_ENV_ALLOW_PREFIXES.map(prefix => prefix.toUpperCase())
|
|
: DEFAULT_ENV_ALLOW_PREFIXES;
|
|
|
|
function normalizeEnvKey(key: string): string {
|
|
return CASE_INSENSITIVE_ENV ? key.toUpperCase() : key;
|
|
}
|
|
|
|
function resolvePathKey(env: Record<string, string | undefined>): string {
|
|
if (!CASE_INSENSITIVE_ENV) return "PATH";
|
|
const match = Object.keys(env).find(candidate => candidate.toLowerCase() === "path");
|
|
return match ?? "PATH";
|
|
}
|
|
|
|
export interface PythonRuntime {
|
|
/** Path to python executable */
|
|
pythonPath: string;
|
|
/** Filtered environment variables */
|
|
env: Record<string, string | undefined>;
|
|
/** Path to virtual environment, if detected */
|
|
venvPath?: string;
|
|
}
|
|
|
|
/**
|
|
* Filter environment variables to a safe allowlist for Python subprocesses.
|
|
* Removes sensitive API keys and limits to known-safe variables.
|
|
*/
|
|
export function filterEnv(env: Record<string, string | undefined>): Record<string, string | undefined> {
|
|
const filtered: Record<string, string | undefined> = {};
|
|
for (const [key, value] of Object.entries(env)) {
|
|
if (value === undefined) continue;
|
|
const normalizedKey = normalizeEnvKey(key);
|
|
if (NORMALIZED_DENYLIST.has(normalizedKey)) continue;
|
|
if (NORMALIZED_ALLOWLIST.has(normalizedKey)) {
|
|
const destKey = normalizedKey === "PATH" ? "PATH" : key;
|
|
filtered[destKey] = value;
|
|
continue;
|
|
}
|
|
if (NORMALIZED_ALLOW_PREFIXES.some(prefix => normalizedKey.startsWith(prefix))) {
|
|
filtered[key] = value;
|
|
}
|
|
}
|
|
return filtered;
|
|
}
|
|
|
|
/**
|
|
* Detect virtual environment path from VIRTUAL_ENV or common locations.
|
|
*/
|
|
export function resolveVenvPath(cwd: string): string | undefined {
|
|
if (getEnv("VIRTUAL_ENV")) return getEnv("VIRTUAL_ENV");
|
|
const candidates = [path.join(cwd, ".venv"), path.join(cwd, "venv")];
|
|
for (const candidate of candidates) {
|
|
if (fs.existsSync(candidate)) {
|
|
return candidate;
|
|
}
|
|
}
|
|
return undefined;
|
|
}
|
|
|
|
/**
|
|
* Resolve the windowless Python executable (pythonw.exe) on Windows.
|
|
* Falls back to the regular Python path if pythonw.exe is not available.
|
|
*/
|
|
function resolveWindowlessPython(pythonPath: string): string {
|
|
if (process.platform !== "win32") return pythonPath;
|
|
const pythonwPath = pythonPath.replace(/python\.exe$/i, "pythonw.exe");
|
|
if (pythonwPath !== pythonPath && fs.existsSync(pythonwPath)) {
|
|
return pythonwPath;
|
|
}
|
|
return pythonPath;
|
|
}
|
|
|
|
/**
|
|
* Resolve Python runtime including executable path, environment, and venv detection.
|
|
*/
|
|
export function resolvePythonRuntime(cwd: string, baseEnv: Record<string, string | undefined>): PythonRuntime {
|
|
const env = { ...baseEnv };
|
|
const venvPath = env.VIRTUAL_ENV ?? resolveVenvPath(cwd);
|
|
|
|
if (venvPath) {
|
|
env.VIRTUAL_ENV = venvPath;
|
|
const binDir = process.platform === "win32" ? path.join(venvPath, "Scripts") : path.join(venvPath, "bin");
|
|
const pythonCandidate = path.join(binDir, process.platform === "win32" ? "python.exe" : "python");
|
|
if (fs.existsSync(pythonCandidate)) {
|
|
const pathKey = resolvePathKey(env);
|
|
const currentPath = env[pathKey];
|
|
env[pathKey] = currentPath ? `${binDir}${path.delimiter}${currentPath}` : binDir;
|
|
return {
|
|
pythonPath: resolveWindowlessPython(pythonCandidate),
|
|
env,
|
|
venvPath,
|
|
};
|
|
}
|
|
}
|
|
|
|
const pythonPath = Bun.which("python") ?? Bun.which("python3");
|
|
if (!pythonPath) {
|
|
throw new Error("Python executable not found on PATH");
|
|
}
|
|
return {
|
|
pythonPath: resolveWindowlessPython(pythonPath),
|
|
env,
|
|
};
|
|
}
|