Files
oh-my-pi/packages/coding-agent/src/ipy/runtime.ts
T
can1357 ec801665bc refactor(env): migrated environment variables from OMP_ to PI_ prefix and centralized access via getEnv()
- 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).
2026-02-05 02:55:01 +01:00

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,
};
}