@@ -2,10 +2,12 @@ import { spawn } from 'child_process';
22import { EventEmitter } from 'events' ;
33import { v4 as uuidv4 } from 'uuid' ;
44import { Logger } from 'pino' ;
5+ import { mkdir } from 'fs/promises' ;
6+ import { existsSync } from 'fs' ;
57import { MCPServerConfig , ProcessInfo } from './types' ;
68import type { EventBus } from '../services/event-bus' ;
79import type { RuntimeState } from './runtime-state' ;
8- import { nsjailConfig } from '../config/nsjail' ;
10+ import { nsjailConfig , mcpCacheBaseDir } from '../config/nsjail' ;
911
1012/**
1113 * Process Manager for MCP server subprocesses
@@ -310,7 +312,7 @@ export class ProcessManager extends EventEmitter {
310312 // Determine isolation mode based on environment
311313 const useNsjail = this . shouldUseNsjail ( ) ;
312314 const childProcess = useNsjail
313- ? this . spawnWithNsjail ( config )
315+ ? await this . spawnWithNsjail ( config )
314316 : this . spawnDirect ( config ) ;
315317
316318 const processInfo : ProcessInfo = {
@@ -912,41 +914,113 @@ export class ProcessManager extends EventEmitter {
912914 } ) ;
913915 }
914916
917+ /**
918+ * Ensure team-specific cache directory exists
919+ */
920+ private async ensureCacheDirectory ( teamId : string ) : Promise < string > {
921+ const cacheDir = `${ mcpCacheBaseDir } /mcp-cache/${ teamId } ` ;
922+
923+ if ( ! existsSync ( cacheDir ) ) {
924+ this . logger . info ( {
925+ operation : 'create_cache_directory' ,
926+ team_id : teamId ,
927+ cache_dir : cacheDir
928+ } , `Creating team cache directory: ${ cacheDir } ` ) ;
929+
930+ try {
931+ await mkdir ( cacheDir , { recursive : true } ) ;
932+
933+ this . logger . info ( {
934+ operation : 'cache_directory_created' ,
935+ team_id : teamId ,
936+ cache_dir : cacheDir
937+ } , `Team cache directory created successfully` ) ;
938+ } catch ( error ) {
939+ const errorMessage = error instanceof Error ? error . message : String ( error ) ;
940+ this . logger . error ( {
941+ operation : 'cache_directory_creation_failed' ,
942+ team_id : teamId ,
943+ cache_dir : cacheDir ,
944+ error : errorMessage
945+ } , `Failed to create team cache directory` ) ;
946+ throw new Error ( `Failed to create cache directory: ${ errorMessage } ` ) ;
947+ }
948+ }
949+
950+ return cacheDir ;
951+ }
952+
915953 /**
916954 * Spawn process with nsjail isolation (production mode on Linux)
955+ *
956+ * Configuration based on empirical testing with npx and Node.js:
957+ * - Memory: 2048MB (V8 minimum requirement)
958+ * - Processes: 1000 (npm spawns many child processes)
959+ * - File descriptors: 1024 (adequate for I/O operations)
960+ * - File size: 50MB (prevents oversized downloads)
961+ * - /dev files: Required for Node.js crypto and I/O operations
962+ * - --proc_rw: Required for pthread_create and thread management
917963 */
918- private spawnWithNsjail ( config : MCPServerConfig ) {
964+ private async spawnWithNsjail ( config : MCPServerConfig ) {
965+ // Ensure team-specific cache directory exists before mounting
966+ const cacheDir = await this . ensureCacheDirectory ( config . team_id ) ;
967+
919968 this . logger . info ( {
920969 operation : 'spawn_nsjail' ,
921970 installation_name : config . installation_name ,
922971 team_id : config . team_id ,
972+ cache_dir : cacheDir ,
923973 memory_limit_mb : nsjailConfig . memoryLimitMB ,
924974 cpu_time_limit_seconds : nsjailConfig . cpuTimeLimitSeconds ,
925- max_processes : nsjailConfig . maxProcesses
975+ max_processes : nsjailConfig . maxProcesses ,
976+ max_open_files : nsjailConfig . maxOpenFiles ,
977+ max_file_size_mb : nsjailConfig . maxFileSizeMB ,
978+ tmpfs_size : nsjailConfig . tmpfsSize
926979 } , 'Spawning process with nsjail isolation' ) ;
927980
928- // Build nsjail arguments
981+ // Get current user UID and GID (deploystack user in production)
982+ const uid = process . getuid ? process . getuid ( ) : 1000 ;
983+ const gid = process . getgid ? process . getgid ( ) : 1000 ;
984+
985+ // Build nsjail arguments based on working production configuration
929986 const nsjailArgs = [
930- '-Mo' , // Mount mode: once, don't remount
931- '--rlimit_as' , String ( nsjailConfig . memoryLimitMB ) , // Memory limit (MB)
987+ '-Mo' , // Mount mode: once, don't remount
988+ '--proc_rw' , // CRITICAL: Required for Node.js pthread_create
989+ '--user' , String ( uid ) , // Use current user (deploystack)
990+ '--group' , String ( gid ) , // Use current group (deploystack)
991+ '--rlimit_as' , String ( nsjailConfig . memoryLimitMB ) , // Memory limit (MB) - 2048 minimum for V8
932992 '--rlimit_cpu' , String ( nsjailConfig . cpuTimeLimitSeconds ) , // CPU time limit (seconds)
933- '--rlimit_nproc' , String ( nsjailConfig . maxProcesses ) , // Max processes
934- '--time_limit' , '0' , // No wall-clock time limit
935- '--user' , '99999' , // Non-root user
936- '--group' , '99999' , // Non-root group
937- '-R' , '/usr' , // Read-only mount: /usr
938- '-R' , '/lib' , // Read-only mount: /lib
939- '-R' , '/lib64' , // Read-only mount: /lib64
940- '-R' , '/bin' , // Read-only mount: /bin
941- '-R' , '/etc/resolv.conf' , // DNS resolution
942- '-T' , '/tmp' , // Writable temp directory
943- '--disable_clone_newnet' , // Allow network access
944- '--hostname' , `mcp-${ config . team_id } ` , // Team-specific hostname
945- // Inject environment variables
993+ '--rlimit_nproc' , String ( nsjailConfig . maxProcesses ) , // Max processes - 1000 for npm
994+ '--rlimit_nofile' , String ( nsjailConfig . maxOpenFiles ) , // Max file descriptors
995+ '--rlimit_fsize' , String ( nsjailConfig . maxFileSizeMB ) , // Max file size (MB)
996+ '--time_limit' , '0' , // No wall-clock time limit
997+ '-R' , '/usr' , // Read-only mount: /usr
998+ '-R' , '/lib' , // Read-only mount: /lib
999+ '-R' , '/lib64' , // Read-only mount: /lib64
1000+ '-R' , '/bin' , // Read-only mount: /bin
1001+ '-R' , '/sbin' , // Read-only mount: /sbin
1002+ '-R' , '/etc' , // Read-only mount: /etc (includes resolv.conf)
1003+ '-T' , `/tmp:size=${ nsjailConfig . tmpfsSize } ` , // Writable temp with size limit (100M)
1004+ '-B' , `${ cacheDir } :/home/npx` , // Team-specific cache directory mount
1005+ '--bindmount' , '/dev/null:/dev/null' , // Required for I/O redirection
1006+ '--bindmount' , '/dev/urandom:/dev/urandom' , // Required for crypto operations
1007+ '--bindmount' , '/dev/zero:/dev/zero' , // Required for memory allocation
1008+ '--symlink' , '/proc/self/fd:/dev/fd' , // Required for file descriptor management
1009+ '-E' , 'HOME=/home/npx' , // Set HOME for npx cache
1010+ '-E' , 'PATH=/usr/bin:/bin:/usr/local/bin' , // Set PATH
1011+ '-E' , 'NPM_CONFIG_CACHE=/home/npx/.npm' , // npm cache location
1012+ '-E' , 'NPM_CONFIG_PREFIX=/home/npx/.npm-global' , // npm global prefix
1013+ '-E' , 'NPM_CONFIG_UPDATE_NOTIFIER=false' , // Disable update notifier
1014+ '-E' , 'NO_UPDATE_NOTIFIER=1' , // Disable update notifier (alternative)
1015+ // Inject user-provided environment variables
9461016 ...Object . entries ( config . env ) . flatMap ( ( [ key , value ] ) => [ '-E' , `${ key } =${ value } ` ] ) ,
947- '--' , // End of nsjail args
948- config . command , // MCP server command
949- ...config . args // MCP server arguments
1017+ '--disable_clone_newnet' , // Allow network access (required for npm downloads)
1018+ '--disable_clone_newcgroup' , // Disable cgroup namespace (causes clone() errors on some kernels)
1019+ '--disable_no_new_privs' , // May be needed for some packages
1020+ '--hostname' , `mcp-${ config . team_id } ` , // Team-specific hostname
1021+ '--' , // End of nsjail args
1022+ config . command , // MCP server command (e.g., /usr/bin/npx)
1023+ ...config . args // MCP server arguments
9501024 ] ;
9511025
9521026 return spawn ( 'nsjail' , nsjailArgs , {
0 commit comments