Skip to content

Commit eb44031

Browse files
committed
refactor(satellite): improve nsjail configuration comments and structure
1 parent a711fc3 commit eb44031

File tree

3 files changed

+99
-45
lines changed

3 files changed

+99
-45
lines changed

services/satellite/src/config/nsjail.ts

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,30 @@
11
/**
22
* nsjail Resource Limits Configuration
33
* These limits apply only in production on Linux platforms when nsjail isolation is enabled
4-
*
5-
* Defaults based on empirical testing with npx and Node.js V8 requirements:
6-
* - 2048MB memory: Absolute minimum for V8 initialization (cannot be reduced)
7-
* - 1000 processes: Sufficient for npm operations which spawn many child processes
4+
*
5+
* Defaults based on empirical testing with package managers (npm, uvx) and runtime requirements:
6+
* - 2048MB memory: Sufficient for V8, Python interpreters, and other runtimes
7+
* - 1000 processes: Adequate for package managers which spawn many child processes
88
* - 1024 file descriptors: Adequate for file I/O operations
9-
* - 50MB file size: Prevents oversized downloads while accommodating 99% of npm packages
10-
* - 100MB tmpfs: Sufficient for npm cache operations
9+
* - 50MB file size: Prevents oversized downloads while accommodating most packages
10+
* - 100MB tmpfs: Sufficient for package manager cache operations
1111
*/
1212
export const nsjailConfig = {
13-
/** Memory limit per MCP server process in MB (default: 2048, V8 minimum) */
13+
/** Memory limit per MCP server process in MB (default: 2048, sufficient for most runtimes) */
1414
memoryLimitMB: parseInt(process.env.NSJAIL_MEMORY_LIMIT_MB || '2048', 10),
15-
15+
1616
/** CPU time limit per MCP server process in seconds (default: 60) */
1717
cpuTimeLimitSeconds: parseInt(process.env.NSJAIL_CPU_TIME_LIMIT_SECONDS || '60', 10),
18-
19-
/** Maximum number of processes per MCP server (default: 1000, required for npm) */
18+
19+
/** Maximum number of processes per MCP server (default: 1000, required for package managers) */
2020
maxProcesses: parseInt(process.env.NSJAIL_MAX_PROCESSES || '1000', 10),
21-
21+
2222
/** Maximum number of open file descriptors (default: 1024) */
2323
maxOpenFiles: parseInt(process.env.NSJAIL_RLIMIT_NOFILE || '1024', 10),
24-
25-
/** Maximum file size in MB (default: 50, prevents oversized npm downloads) */
24+
25+
/** Maximum file size in MB (default: 50, prevents oversized package downloads) */
2626
maxFileSizeMB: parseInt(process.env.NSJAIL_RLIMIT_FSIZE || '50', 10),
27-
27+
2828
/** Tmpfs size for /tmp directory (default: 100M) */
2929
tmpfsSize: process.env.NSJAIL_TMPFS_SIZE || '100M'
3030
};

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

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -30,12 +30,13 @@ export class GitHubDeploymentHandler {
3030
) {}
3131

3232
/**
33-
* Parse GitHub URL from NPX arguments
34-
* Supports: github:owner/repo#ref
33+
* Parse GitHub URL from package manager arguments
34+
* Supports: github:owner/repo#ref (for npx and uvx)
3535
*/
3636
parseGitHubUrl(command: string, args: string[]): GitHubInfo | null {
37-
// Check if this is an NPX command with GitHub shorthand
38-
if (command !== 'npx') {
37+
// Check if this is a supported package manager command with GitHub shorthand
38+
const supportedCommands = ['npx', 'uvx'];
39+
if (!supportedCommands.includes(command)) {
3940
return null;
4041
}
4142

@@ -50,8 +51,9 @@ export class GitHubDeploymentHandler {
5051
if (!match) {
5152
this.logger.warn({
5253
operation: 'github_url_parse_failed',
53-
github_arg: githubArg
54-
}, 'Failed to parse GitHub URL from NPX arguments');
54+
github_arg: githubArg,
55+
command: command
56+
}, `Failed to parse GitHub URL from ${command} arguments`);
5557
return null;
5658
}
5759

@@ -64,9 +66,13 @@ export class GitHubDeploymentHandler {
6466

6567
/**
6668
* Check if a config requires GitHub deployment
69+
* Supports both Node.js (npx) and Python (uvx) runtimes
6770
*/
6871
isGitHubDeployment(config: MCPServerConfig): boolean {
69-
return config.source === 'github' && config.command === 'npx' && !!this.backendClient;
72+
const supportedCommands = ['npx', 'uvx'];
73+
return config.source === 'github' &&
74+
supportedCommands.includes(config.command) &&
75+
!!this.backendClient;
7076
}
7177

7278
/**
@@ -504,10 +510,11 @@ export class GitHubDeploymentHandler {
504510
installation_name: config.installation_name,
505511
installation_id: config.installation_id,
506512
command: config.command,
513+
runtime: config.runtime || 'unknown',
507514
args: config.args
508-
}, 'GitHub deployment detected, downloading repository via Octokit');
515+
}, `GitHub deployment detected (${config.runtime || 'unknown'} runtime), downloading repository via Octokit`);
509516

510-
// Parse GitHub URL from NPX arguments
517+
// Parse GitHub URL from package manager arguments
511518
const githubInfo = this.parseGitHubUrl(config.command, config.args || []);
512519
if (!githubInfo) {
513520
throw new Error('Failed to parse GitHub URL from NPX arguments');

services/satellite/src/process/nsjail-spawner.ts

Lines changed: 69 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@ export class ProcessSpawner {
7474
const commandPaths: Record<string, string> = {
7575
'npx': '/usr/bin/npx',
7676
'node': '/usr/bin/node',
77+
'uvx': '/usr/bin/uvx',
7778
'python': '/usr/bin/python',
7879
'python3': '/usr/bin/python3'
7980
};
@@ -92,6 +93,49 @@ export class ProcessSpawner {
9293
return `/usr/bin/${command}`;
9394
}
9495

96+
/**
97+
* Get runtime-specific environment variables for nsjail isolation
98+
* Different runtimes need different cache directories and package manager settings
99+
*/
100+
private getEnvironmentForRuntime(config: MCPServerConfig): string[] {
101+
const runtime = config.runtime || 'node'; // Default to node for backward compatibility
102+
const envVars: string[] = [];
103+
104+
switch (runtime) {
105+
case 'node':
106+
envVars.push(
107+
'-E', 'HOME=/home/node',
108+
'-E', 'PATH=/usr/bin:/bin:/usr/local/bin',
109+
'-E', 'NPM_CONFIG_CACHE=/home/node/.npm',
110+
'-E', 'NPM_CONFIG_PREFIX=/home/node/.npm-global',
111+
'-E', 'NPM_CONFIG_UPDATE_NOTIFIER=false',
112+
'-E', 'NO_UPDATE_NOTIFIER=1'
113+
);
114+
break;
115+
116+
case 'python':
117+
envVars.push(
118+
'-E', 'HOME=/home/python',
119+
'-E', 'PATH=/usr/bin:/bin:/usr/local/bin',
120+
'-E', 'UV_CACHE_DIR=/home/python/.cache/uv',
121+
'-E', 'UV_TOOL_DIR=/home/python/.local/bin',
122+
'-E', 'PYTHONUNBUFFERED=1',
123+
'-E', 'UV_NO_UPDATE_NOTIFIER=1'
124+
);
125+
break;
126+
127+
default:
128+
// Generic runtime - minimal environment
129+
envVars.push(
130+
'-E', `HOME=/home/${runtime}`,
131+
'-E', 'PATH=/usr/bin:/bin:/usr/local/bin'
132+
);
133+
break;
134+
}
135+
136+
return envVars;
137+
}
138+
95139
/**
96140
* Spawn a process - routes to direct or nsjail based on environment
97141
*/
@@ -120,31 +164,34 @@ export class ProcessSpawner {
120164
}
121165

122166
/**
123-
* Ensure team-specific cache directory exists
167+
* Ensure team-specific cache directory exists for the runtime
124168
*/
125-
async ensureCacheDirectory(teamId: string): Promise<string> {
126-
const cacheDir = `${mcpCacheBaseDir}/mcp-cache/${teamId}`;
169+
async ensureCacheDirectory(teamId: string, runtime: string): Promise<string> {
170+
const cacheDir = `${mcpCacheBaseDir}/mcp-cache/${runtime}/${teamId}`;
127171

128172
if (!existsSync(cacheDir)) {
129173
this.logger.info({
130174
operation: 'create_cache_directory',
131175
team_id: teamId,
176+
runtime: runtime,
132177
cache_dir: cacheDir
133-
}, `Creating team cache directory: ${cacheDir}`);
178+
}, `Creating team cache directory for ${runtime} runtime: ${cacheDir}`);
134179

135180
try {
136181
await mkdir(cacheDir, { recursive: true });
137182

138183
this.logger.info({
139184
operation: 'cache_directory_created',
140185
team_id: teamId,
186+
runtime: runtime,
141187
cache_dir: cacheDir
142188
}, `Team cache directory created successfully`);
143189
} catch (error) {
144190
const errorMessage = error instanceof Error ? error.message : String(error);
145191
this.logger.error({
146192
operation: 'cache_directory_creation_failed',
147193
team_id: teamId,
194+
runtime: runtime,
148195
cache_dir: cacheDir,
149196
error: errorMessage
150197
}, `Failed to create team cache directory`);
@@ -158,30 +205,34 @@ export class ProcessSpawner {
158205
/**
159206
* Spawn process with nsjail isolation (production mode on Linux)
160207
*
161-
* Configuration based on empirical testing with npx and Node.js:
162-
* - Memory: 2048MB (V8 minimum requirement)
163-
* - Processes: 1000 (npm spawns many child processes)
208+
* Configuration supports multiple runtimes (Node.js, Python, etc.):
209+
* - Memory: 2048MB (adequate for V8 and Python interpreters)
210+
* - Processes: 1000 (package managers spawn many child processes)
164211
* - File descriptors: 1024 (adequate for I/O operations)
165212
* - File size: 50MB (prevents oversized downloads)
166-
* - /dev files: Required for Node.js crypto and I/O operations
213+
* - /dev files: Required for crypto and I/O operations
167214
* - --proc_rw: Required for pthread_create and thread management
168215
*/
169216
async spawnWithNsjail(config: MCPServerConfig): Promise<ChildProcess> {
217+
// Determine runtime (default to 'node' for backward compatibility)
218+
const runtime = config.runtime || 'node';
219+
170220
// Ensure team-specific cache directory exists before mounting
171-
const cacheDir = await this.ensureCacheDirectory(config.team_id);
221+
const cacheDir = await this.ensureCacheDirectory(config.team_id, runtime);
172222

173223
this.logger.info({
174224
operation: 'spawn_nsjail',
175225
installation_name: config.installation_name,
176226
team_id: config.team_id,
227+
runtime: runtime,
177228
cache_dir: cacheDir,
178229
memory_limit_mb: nsjailConfig.memoryLimitMB,
179230
cpu_time_limit_seconds: nsjailConfig.cpuTimeLimitSeconds,
180231
max_processes: nsjailConfig.maxProcesses,
181232
max_open_files: nsjailConfig.maxOpenFiles,
182233
max_file_size_mb: nsjailConfig.maxFileSizeMB,
183234
tmpfs_size: nsjailConfig.tmpfsSize
184-
}, 'Spawning process with nsjail isolation');
235+
}, `Spawning ${runtime} MCP server with nsjail isolation`);
185236

186237
// Get current user UID and GID (deploystack user in production)
187238
const uid = process.getuid ? process.getuid() : 1000;
@@ -199,12 +250,12 @@ export class ProcessSpawner {
199250
// Build nsjail arguments based on working production configuration
200251
const nsjailArgs = [
201252
'-Mo', // Mount mode: once, don't remount
202-
'--proc_rw', // Required for Node.js pthread_create
253+
'--proc_rw', // Required for pthread_create and thread management
203254
'--user', String(uid), // Use current user (deploystack)
204255
'--group', String(gid), // Use current group (deploystack)
205-
'--rlimit_as', String(nsjailConfig.memoryLimitMB), // Memory limit (MB) - 2048 minimum for V8
256+
'--rlimit_as', String(nsjailConfig.memoryLimitMB), // Memory limit (MB) - 2048 for interpreters
206257
'--rlimit_cpu', String(nsjailConfig.cpuTimeLimitSeconds), // CPU time limit (seconds)
207-
'--rlimit_nproc', String(nsjailConfig.maxProcesses), // Max processes - 1000 for npm
258+
'--rlimit_nproc', String(nsjailConfig.maxProcesses), // Max processes - 1000 for package managers
208259
'--rlimit_nofile', String(nsjailConfig.maxOpenFiles), // Max file descriptors
209260
'--rlimit_fsize', String(nsjailConfig.maxFileSizeMB), // Max file size (MB)
210261
'--time_limit', '0', // No wall-clock time limit
@@ -215,25 +266,21 @@ export class ProcessSpawner {
215266
'-R', '/sbin', // Read-only mount: /sbin
216267
'-R', '/etc', // Read-only mount: /etc (includes resolv.conf)
217268
'-T', `/tmp:size=${nsjailConfig.tmpfsSize}`, // Writable temp with size limit (100M)
218-
'-B', `${cacheDir}:/home/npx`, // Team-specific cache directory mount
269+
'-B', `${cacheDir}:/home/${runtime}`, // Runtime-specific cache directory mount
219270
'--bindmount', '/dev/null:/dev/null', // Required for I/O redirection
220271
'--bindmount', '/dev/urandom:/dev/urandom', // Required for crypto operations
221272
'--bindmount', '/dev/zero:/dev/zero', // Required for memory allocation
222273
'--symlink', '/proc/self/fd:/dev/fd', // Required for file descriptor management
223-
'-E', 'HOME=/home/npx', // Set HOME for npx cache
224-
'-E', 'PATH=/usr/bin:/bin:/usr/local/bin', // Set PATH
225-
'-E', 'NPM_CONFIG_CACHE=/home/npx/.npm', // npm cache location
226-
'-E', 'NPM_CONFIG_PREFIX=/home/npx/.npm-global', // npm global prefix
227-
'-E', 'NPM_CONFIG_UPDATE_NOTIFIER=false', // Disable update notifier
228-
'-E', 'NO_UPDATE_NOTIFIER=1', // Disable update notifier (alternative)
274+
// Runtime-specific environment variables
275+
...this.getEnvironmentForRuntime(config),
229276
// Inject user-provided environment variables (sanitized)
230277
...this.sanitizeEnvVars(config.env, config.installation_name),
231-
'--disable_clone_newnet', // Allow network access (required for npm downloads)
278+
'--disable_clone_newnet', // Allow network access (required for package downloads)
232279
'--disable_clone_newcgroup', // Disable cgroup namespace (causes clone() errors on some kernels)
233280
'--disable_no_new_privs', // May be needed for some packages
234281
'--hostname', `mcp-${config.team_id}`, // Team-specific hostname
235282
'--', // End of nsjail args
236-
fullCommandPath, // MCP server command with full path (e.g., /usr/bin/npx)
283+
fullCommandPath, // MCP server command with full path (e.g., /usr/bin/npx or /usr/bin/uvx)
237284
...config.args // MCP server arguments
238285
];
239286

0 commit comments

Comments
 (0)