diff --git a/apps/frontend/package-lock.json b/apps/frontend/package-lock.json index 6b22c98327..d8098fe9da 100644 --- a/apps/frontend/package-lock.json +++ b/apps/frontend/package-lock.json @@ -19,7 +19,6 @@ "@radix-ui/react-collapsible": "^1.1.3", "@radix-ui/react-dialog": "^1.1.15", "@radix-ui/react-dropdown-menu": "^2.1.16", - "@radix-ui/react-popover": "^1.1.15", "@radix-ui/react-progress": "^1.1.8", "@radix-ui/react-radio-group": "^1.3.8", "@radix-ui/react-scroll-area": "^1.2.10", @@ -51,7 +50,6 @@ "react-markdown": "^10.1.0", "react-resizable-panels": "^3.0.6", "remark-gfm": "^4.0.1", - "semver": "^7.7.3", "tailwind-merge": "^3.4.0", "uuid": "^13.0.0", "zod": "^4.2.1", @@ -68,7 +66,6 @@ "@types/node": "^25.0.0", "@types/react": "^19.2.7", "@types/react-dom": "^19.2.3", - "@types/semver": "^7.7.1", "@types/uuid": "^10.0.0", "@vitejs/plugin-react": "^5.1.2", "autoprefixer": "^10.4.22", @@ -1066,45 +1063,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/@electron/windows-sign": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/@electron/windows-sign/-/windows-sign-1.2.2.tgz", - "integrity": "sha512-dfZeox66AvdPtb2lD8OsIIQh12Tp0GNCRUDfBHIKGpbmopZto2/A8nSpYYLoedPIHpqkeblZ/k8OV0Gy7PYuyQ==", - "dev": true, - "license": "BSD-2-Clause", - "optional": true, - "peer": true, - "dependencies": { - "cross-dirname": "^0.1.0", - "debug": "^4.3.4", - "fs-extra": "^11.1.1", - "minimist": "^1.2.8", - "postject": "^1.0.0-alpha.6" - }, - "bin": { - "electron-windows-sign": "bin/electron-windows-sign.js" - }, - "engines": { - "node": ">=14.14" - } - }, - "node_modules/@electron/windows-sign/node_modules/fs-extra": { - "version": "11.3.2", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.2.tgz", - "integrity": "sha512-Xr9F6z6up6Ws+NjzMCZc6WXg2YFRlrLP9NQDO3VQrWrfiojdhS56TzueT88ze0uBdCTwEIhQ3ptnmKeWGFAe0A==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - }, - "engines": { - "node": ">=14.14" - } - }, "node_modules/@epic-web/invariant": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/@epic-web/invariant/-/invariant-1.0.0.tgz", @@ -2731,61 +2689,6 @@ } } }, - "node_modules/@radix-ui/react-popover": { - "version": "1.1.15", - "resolved": "https://registry.npmjs.org/@radix-ui/react-popover/-/react-popover-1.1.15.tgz", - "integrity": "sha512-kr0X2+6Yy/vJzLYJUPCZEc8SfQcf+1COFoAqauJm74umQhta9M7lNJHP7QQS3vkvcGLQUbWpMzwrXYwrYztHKA==", - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-dismissable-layer": "1.1.11", - "@radix-ui/react-focus-guards": "1.1.3", - "@radix-ui/react-focus-scope": "1.1.7", - "@radix-ui/react-id": "1.1.1", - "@radix-ui/react-popper": "1.2.8", - "@radix-ui/react-portal": "1.1.9", - "@radix-ui/react-presence": "1.1.5", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-slot": "1.2.3", - "@radix-ui/react-use-controllable-state": "1.2.2", - "aria-hidden": "^1.2.4", - "react-remove-scroll": "^2.6.3" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-slot": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", - "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-compose-refs": "1.1.2" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, "node_modules/@radix-ui/react-popper": { "version": "1.2.8", "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.8.tgz", @@ -4492,13 +4395,6 @@ "@types/node": "*" } }, - "node_modules/@types/semver": { - "version": "7.7.1", - "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.7.1.tgz", - "integrity": "sha512-FmgJfu+MOcQ370SD0ev7EI8TlCAfKYU+B4m5T3yXc1CiRN94g/SZPtsCkk506aUDtlMnFZvasDwHHUcZUEaYuA==", - "dev": true, - "license": "MIT" - }, "node_modules/@types/unist": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", @@ -6279,15 +6175,6 @@ "buffer": "^5.1.0" } }, - "node_modules/cross-dirname": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/cross-dirname/-/cross-dirname-0.1.0.tgz", - "integrity": "sha512-+R08/oI0nl3vfPcqftZRpytksBXDzOUveBq/NBVx0sUp1axwzPQrKinNx5yd5sxPu8j1wIy8AfnVQ+5eFdha6Q==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true - }, "node_modules/cross-env": { "version": "10.1.0", "resolved": "https://registry.npmjs.org/cross-env/-/cross-env-10.1.0.tgz", @@ -12626,36 +12513,6 @@ "dev": true, "license": "MIT" }, - "node_modules/postject": { - "version": "1.0.0-alpha.6", - "resolved": "https://registry.npmjs.org/postject/-/postject-1.0.0-alpha.6.tgz", - "integrity": "sha512-b9Eb8h2eVqNE8edvKdwqkrY6O7kAwmI8kcnBv1NScolYJbo59XUF0noFq+lxbC1yN20bmC0WBEbDC5H/7ASb0A==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "commander": "^9.4.0" - }, - "bin": { - "postject": "dist/cli.js" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/postject/node_modules/commander": { - "version": "9.5.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-9.5.0.tgz", - "integrity": "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true, - "engines": { - "node": "^12.20.0 || >=14" - } - }, "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", diff --git a/apps/frontend/src/main/index.ts b/apps/frontend/src/main/index.ts index 7cd856a0fe..cb0b021501 100644 --- a/apps/frontend/src/main/index.ts +++ b/apps/frontend/src/main/index.ts @@ -203,6 +203,10 @@ app.whenReady().then(() => { autoBuildPath: validAutoBuildPath }); agentManager.configure(settings.pythonPath, validAutoBuildPath); + // Also configure pythonEnvManager so venv is created with the right Python + if (settings.pythonPath) { + pythonEnvManager.configure(settings.pythonPath); + } } } catch (error: unknown) { // ENOENT means no settings file yet - that's fine, use defaults diff --git a/apps/frontend/src/main/ipc-handlers/settings-handlers.ts b/apps/frontend/src/main/ipc-handlers/settings-handlers.ts index 8a49d84304..d808e3b6a2 100644 --- a/apps/frontend/src/main/ipc-handlers/settings-handlers.ts +++ b/apps/frontend/src/main/ipc-handlers/settings-handlers.ts @@ -14,6 +14,7 @@ import { getEffectiveVersion } from '../auto-claude-updater'; import { setUpdateChannel } from '../app-updater'; import { getSettingsPath, readSettingsFile } from '../settings-utils'; import { configureTools, getToolPath, getToolInfo } from '../cli-tool-manager'; +import { pythonEnvManager } from '../python-env-manager'; const settingsPath = getSettingsPath(); @@ -176,6 +177,10 @@ export function registerSettingsHandlers( // Apply Python path if changed if (settings.pythonPath || settings.autoBuildPath) { agentManager.configure(settings.pythonPath, settings.autoBuildPath); + // Also update pythonEnvManager so future venv creations use the new path + if (settings.pythonPath) { + pythonEnvManager.configure(settings.pythonPath); + } } // Configure CLI tools if any paths changed diff --git a/apps/frontend/src/main/python-detector.ts b/apps/frontend/src/main/python-detector.ts index f8c80d20c3..77728f088e 100644 --- a/apps/frontend/src/main/python-detector.ts +++ b/apps/frontend/src/main/python-detector.ts @@ -147,7 +147,7 @@ function getPythonVersion(pythonCmd: string): string | null { * @param pythonCmd - The Python command to validate * @returns Validation result with status, version, and message */ -function validatePythonVersion(pythonCmd: string): { +export function validatePythonVersion(pythonCmd: string): { valid: boolean; version?: string; message: string; diff --git a/apps/frontend/src/main/python-env-manager.ts b/apps/frontend/src/main/python-env-manager.ts index 608ba5fda5..1638292b77 100644 --- a/apps/frontend/src/main/python-env-manager.ts +++ b/apps/frontend/src/main/python-env-manager.ts @@ -1,9 +1,9 @@ -import { spawn, execSync, ChildProcess } from 'child_process'; +import { spawn, execSync, execFileSync, ChildProcess } from 'child_process'; import { existsSync, readdirSync } from 'fs'; import path from 'path'; import { EventEmitter } from 'events'; import { app } from 'electron'; -import { findPythonCommand, getBundledPythonPath } from './python-detector'; +import { findPythonCommand, getBundledPythonPath, validatePythonPath, parsePythonCommand, validatePythonVersion } from './python-detector'; export interface PythonEnvStatus { ready: boolean; @@ -40,6 +40,39 @@ export class PythonEnvManager extends EventEmitter { private initializationPromise: Promise | null = null; private activeProcesses: Set = new Set(); private static readonly VENV_CREATION_TIMEOUT_MS = 120000; // 2 minutes timeout for venv creation + // User-configured Python path from settings (takes priority over auto-detection) + private configuredPythonPath: string | null = null; + + /** + * Configure the Python path from user settings. + * This should be called before initialize() to ensure the user's preferred Python is used. + * @param pythonPath - The user-configured Python path from settings + * @returns true if path was accepted, false if rejected (will use auto-detection) + */ + configure(pythonPath?: string): boolean { + if (!pythonPath) { + return false; + } + + // Step 1: Validate path security (no shell injection, valid executable) + const pathValidation = validatePythonPath(pythonPath); + if (!pathValidation.valid || !pathValidation.sanitizedPath) { + console.error(`[PythonEnvManager] Invalid Python path rejected: ${pathValidation.reason}`); + return false; + } + + // Step 2: Validate Python version meets 3.10+ requirement + const versionValidation = validatePythonVersion(pathValidation.sanitizedPath); + if (!versionValidation.valid) { + console.error(`[PythonEnvManager] Python version rejected: ${versionValidation.message}`); + this.emit('error', `Configured Python version is too old: ${versionValidation.message}`); + return false; + } + + this.configuredPythonPath = pathValidation.sanitizedPath; + console.log(`[PythonEnvManager] Configured Python path: ${this.configuredPythonPath} (${versionValidation.version})`); + return true; + } /** * Get the path where the venv should be created. @@ -181,11 +214,33 @@ if sys.version_info >= (3, 12): } /** - * Find Python 3.10+ (bundled or system). + * Find Python 3.10+ (configured, bundled, or system). * Uses the shared python-detector logic which validates version requirements. - * Priority: bundled Python (packaged apps) > system Python + * Priority: user-configured > bundled Python (packaged apps) > system Python */ private findSystemPython(): string | null { + // 1. First check user-configured Python path (from settings) + if (this.configuredPythonPath) { + try { + // Verify the configured path actually works and resolve it to a full path + // Use execFileSync with shell: false for security (defense-in-depth) + const [command, args] = parsePythonCommand(this.configuredPythonPath); + const pythonPath = execFileSync(command, [...args, '-c', 'import sys; print(sys.executable)'], { + stdio: 'pipe', + timeout: 5000, + windowsHide: true, + shell: false // Explicitly disable shell for security + }).toString().trim(); + + console.log(`[PythonEnvManager] Using user-configured Python: ${pythonPath}`); + return pythonPath; + } catch (err) { + console.error(`[PythonEnvManager] User-configured Python failed, falling back to auto-detection:`, err); + // Fall through to auto-detection + } + } + + // 2. Auto-detect Python const pythonCmd = findPythonCommand(); if (!pythonCmd) { return null;