|
1 | 1 | import { spawn } from 'child_process'; |
2 | 2 | import * as path from 'path'; |
3 | 3 | import * as fs from 'fs'; |
| 4 | +import { promises as fsPromises } from 'fs'; |
4 | 5 | import { Logger } from 'pino'; |
5 | 6 | import { MCPServerConfig } from './types'; |
6 | 7 | import { LogBuffer } from './log-buffer'; |
@@ -755,6 +756,77 @@ export class GitHubDeploymentHandler { |
755 | 756 | return result; |
756 | 757 | } |
757 | 758 |
|
| 759 | + /** |
| 760 | + * Check if a file exists |
| 761 | + */ |
| 762 | + private async fileExistsAsync(filePath: string): Promise<boolean> { |
| 763 | + try { |
| 764 | + await fsPromises.access(filePath); |
| 765 | + return true; |
| 766 | + } catch { |
| 767 | + return false; |
| 768 | + } |
| 769 | + } |
| 770 | + |
| 771 | + /** |
| 772 | + * Get the site-packages path from a venv directory |
| 773 | + * Detects Python version by scanning .venv/lib/pythonX.Y/site-packages |
| 774 | + * |
| 775 | + * @param deploymentDir - Real deployment directory path (for reading filesystem) |
| 776 | + * @param venvPath - Venv path to use in the result (could be /app/.venv for nsjail) |
| 777 | + * @returns Site-packages path using venvPath prefix |
| 778 | + */ |
| 779 | + private async getPythonSitePackagesPath( |
| 780 | + deploymentDir: string, |
| 781 | + venvPath: string |
| 782 | + ): Promise<string> { |
| 783 | + const libDir = path.join(deploymentDir, '.venv', 'lib'); |
| 784 | + |
| 785 | + try { |
| 786 | + const entries = await fsPromises.readdir(libDir); |
| 787 | + |
| 788 | + // Find python3.X directory |
| 789 | + const pythonDir = entries.find(e => e.startsWith('python3.')); |
| 790 | + |
| 791 | + if (!pythonDir) { |
| 792 | + throw new Error('Could not find Python version in venv lib directory'); |
| 793 | + } |
| 794 | + |
| 795 | + this.logger.debug({ |
| 796 | + operation: 'python_site_packages_detected', |
| 797 | + python_dir: pythonDir, |
| 798 | + venv_path: venvPath |
| 799 | + }, `Detected Python version: ${pythonDir}`); |
| 800 | + |
| 801 | + // Return site-packages path (use venvPath for the prefix, not deploymentDir) |
| 802 | + return path.join(venvPath, 'lib', pythonDir, 'site-packages'); |
| 803 | + } catch (error) { |
| 804 | + this.logger.warn({ |
| 805 | + operation: 'python_site_packages_fallback', |
| 806 | + error: error instanceof Error ? error.message : String(error) |
| 807 | + }, 'Could not detect Python version, trying common versions'); |
| 808 | + |
| 809 | + // Fallback: try common Python versions |
| 810 | + const commonVersions = ['python3.13', 'python3.12', 'python3.11', 'python3.10', 'python3.9']; |
| 811 | + |
| 812 | + for (const version of commonVersions) { |
| 813 | + const candidatePath = path.join(deploymentDir, '.venv', 'lib', version, 'site-packages'); |
| 814 | + |
| 815 | + if (await this.fileExistsAsync(candidatePath)) { |
| 816 | + this.logger.info({ |
| 817 | + operation: 'python_site_packages_found', |
| 818 | + python_version: version, |
| 819 | + venv_path: venvPath |
| 820 | + }, `Found Python ${version} site-packages`); |
| 821 | + |
| 822 | + return path.join(venvPath, 'lib', version, 'site-packages'); |
| 823 | + } |
| 824 | + } |
| 825 | + |
| 826 | + throw new Error('Could not determine Python site-packages path in venv'); |
| 827 | + } |
| 828 | + } |
| 829 | + |
758 | 830 |
|
759 | 831 | /** |
760 | 832 | * Prepare a GitHub deployment - downloads, extracts, builds, and updates config |
@@ -903,23 +975,30 @@ export class GitHubDeploymentHandler { |
903 | 975 | // Running with system python3 interpreter - make script path relative |
904 | 976 | const relativeEntryPoint = path.relative(deploymentDir, entryPoint); |
905 | 977 |
|
906 | | - // Activate venv by setting VIRTUAL_ENV environment variable |
| 978 | + // Activate venv by setting PYTHONPATH environment variable |
907 | 979 | // This allows system python3 to find packages in .venv/lib/python3.x/site-packages |
908 | 980 | // |
909 | | - // Note: In production (nsjail), deployment dir is mounted as /app |
910 | | - // So we set VIRTUAL_ENV=/app/.venv which is accessible inside nsjail |
| 981 | + // Note: PATH is blocked by security sanitization, so we use PYTHONPATH instead |
| 982 | + // In production (nsjail), deployment dir is mounted as /app |
911 | 983 | // In development, we use the actual deployment directory path |
912 | 984 | const isProduction = process.env.NODE_ENV === 'production'; |
913 | 985 | const venvPath = isProduction ? '/app/.venv' : path.join(deploymentDir, '.venv'); |
914 | | - const venvBinPath = isProduction ? '/app/.venv/bin' : path.join(deploymentDir, '.venv/bin'); |
| 986 | + |
| 987 | + // Detect Python version and get site-packages path |
| 988 | + const sitePackagesPath = await this.getPythonSitePackagesPath(deploymentDir, venvPath); |
915 | 989 |
|
916 | 990 | const activatedEnv = { |
917 | 991 | ...config.env, |
918 | | - VIRTUAL_ENV: venvPath, |
919 | | - // Prepend .venv/bin to PATH so python3 uses venv packages |
920 | | - PATH: `${venvBinPath}:${process.env.PATH || '/usr/bin:/bin'}` |
| 992 | + // Set PYTHONPATH so python3 finds packages in venv |
| 993 | + PYTHONPATH: sitePackagesPath |
921 | 994 | }; |
922 | 995 |
|
| 996 | + this.logger.info({ |
| 997 | + operation: 'python_venv_activated', |
| 998 | + site_packages_path: sitePackagesPath, |
| 999 | + is_production: isProduction |
| 1000 | + }, `Activated Python venv via PYTHONPATH: ${sitePackagesPath}`); |
| 1001 | + |
923 | 1002 | updatedConfig = { |
924 | 1003 | ...config, |
925 | 1004 | command: 'python3', |
|
0 commit comments