Skip to content

Commit 3de872a

Browse files
committed
refactor(satellite): update Python environment variable handling for venv
1 parent 7087647 commit 3de872a

File tree

2 files changed

+88
-8
lines changed

2 files changed

+88
-8
lines changed

services/satellite/src/config/nsjail.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,8 @@ export const BLOCKED_ENV_VARS = new Set([
7979

8080
// Python specific
8181
'PYTHONSTARTUP', // Executes script on Python interpreter start
82-
'PYTHONPATH', // Module resolution hijacking
82+
// 'PYTHONPATH' - REMOVED: Required for venv activation in GitHub deployments
83+
// Module path additions are sandboxed within the jail anyway
8384
'PYTHONHOME', // Python installation path hijacking
8485
'PYTHONWARNINGS', // Warning behavior manipulation
8586
'PYTHONDEBUG', // Debug mode enabling

services/satellite/src/process/github-deployment.ts

Lines changed: 86 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { spawn } from 'child_process';
22
import * as path from 'path';
33
import * as fs from 'fs';
4+
import { promises as fsPromises } from 'fs';
45
import { Logger } from 'pino';
56
import { MCPServerConfig } from './types';
67
import { LogBuffer } from './log-buffer';
@@ -755,6 +756,77 @@ export class GitHubDeploymentHandler {
755756
return result;
756757
}
757758

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+
758830

759831
/**
760832
* Prepare a GitHub deployment - downloads, extracts, builds, and updates config
@@ -903,23 +975,30 @@ export class GitHubDeploymentHandler {
903975
// Running with system python3 interpreter - make script path relative
904976
const relativeEntryPoint = path.relative(deploymentDir, entryPoint);
905977

906-
// Activate venv by setting VIRTUAL_ENV environment variable
978+
// Activate venv by setting PYTHONPATH environment variable
907979
// This allows system python3 to find packages in .venv/lib/python3.x/site-packages
908980
//
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
911983
// In development, we use the actual deployment directory path
912984
const isProduction = process.env.NODE_ENV === 'production';
913985
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);
915989

916990
const activatedEnv = {
917991
...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
921994
};
922995

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+
9231002
updatedConfig = {
9241003
...config,
9251004
command: 'python3',

0 commit comments

Comments
 (0)