sandboxbox 1.2.0 → 1.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/container.js CHANGED
@@ -27,6 +27,11 @@ class BubblewrapContainer {
27
27
  this.verbose = options.verbose !== false;
28
28
  this.env = { ...process.env };
29
29
  this.workdir = '/workspace';
30
+
31
+ // Auto-create sandbox directory if it doesn't exist
32
+ if (!existsSync(this.sandboxDir)) {
33
+ mkdirSync(this.sandboxDir, { recursive: true });
34
+ }
30
35
  }
31
36
 
32
37
  /**
@@ -217,6 +222,87 @@ echo "⚠️ Note: Chromium-only (Firefox/WebKit need glibc - use Ubuntu)"
217
222
  // Resolve project directory
218
223
  const resolvedProjectDir = resolve(projectDir);
219
224
 
225
+ // First, try full namespace isolation
226
+ try {
227
+ console.log('🎯 Attempting full namespace isolation...');
228
+ return await this.runPlaywrightWithNamespaces(options);
229
+ } catch (error) {
230
+ console.log(`⚠️ Namespace isolation failed: ${error.message}`);
231
+ console.log('🔄 Falling back to basic isolation mode...\n');
232
+ return await this.runPlaywrightBasic(options);
233
+ }
234
+ }
235
+
236
+ /**
237
+ * Run simple container test without Playwright (for testing purposes)
238
+ */
239
+ async runSimpleTest(options = {}) {
240
+ const { projectDir = '.', testCommand = 'echo "Container is working!" && ls -la /workspace' } = options;
241
+ const resolvedProjectDir = resolve(projectDir);
242
+
243
+ console.log('🧪 Running simple container test...\n');
244
+
245
+ // Try basic isolation first
246
+ try {
247
+ console.log('🎯 Attempting basic isolation...');
248
+ return await this.runBasicTest(options);
249
+ } catch (error) {
250
+ console.log(`⚠️ Basic test failed: ${error.message}`);
251
+ console.log('🔄 Running without isolation...\n');
252
+ return this.runWithoutIsolation(options);
253
+ }
254
+ }
255
+
256
+ /**
257
+ * Run basic test in container
258
+ */
259
+ async runBasicTest(options = {}) {
260
+ const { projectDir = '.', testCommand = 'echo "Container is working!" && ls -la /workspace' } = options;
261
+ const resolvedProjectDir = resolve(projectDir);
262
+
263
+ // Simplified bubblewrap command
264
+ const bwrapCmd = [
265
+ bubblewrap.findBubblewrap(),
266
+ '--bind', resolvedProjectDir, '/workspace',
267
+ '--chdir', '/workspace',
268
+ '--tmpfs', '/tmp',
269
+ '/bin/sh', '-c', testCommand
270
+ ];
271
+
272
+ console.log(`🚀 Running: ${testCommand}`);
273
+ console.log(`📁 Project directory: ${resolvedProjectDir}`);
274
+ console.log(`🎯 Sandbox isolation: basic mode\n`);
275
+
276
+ return this.executeCommand(bwrapCmd, resolvedProjectDir);
277
+ }
278
+
279
+ /**
280
+ * Run without any isolation (last resort)
281
+ */
282
+ async runWithoutIsolation(options = {}) {
283
+ const { projectDir = '.', testCommand = 'echo "Container is working!" && ls -la' } = options;
284
+ const resolvedProjectDir = resolve(projectDir);
285
+
286
+ console.log(`🚀 Running without isolation: ${testCommand}`);
287
+ console.log(`📁 Project directory: ${resolvedProjectDir}`);
288
+ console.log(`🎯 Sandbox isolation: none\n`);
289
+
290
+ try {
291
+ execSync(testCommand, { stdio: 'inherit', cwd: resolvedProjectDir });
292
+ console.log('\n✅ Test completed successfully!');
293
+ return 0;
294
+ } catch (error) {
295
+ throw new Error(`Test failed: ${error.message}`);
296
+ }
297
+ }
298
+
299
+ /**
300
+ * Run Playwright with full namespace isolation (ideal mode)
301
+ */
302
+ async runPlaywrightWithNamespaces(options = {}) {
303
+ const { projectDir = '.', testCommand = 'npx playwright test', mountProject = true } = options;
304
+ const resolvedProjectDir = resolve(projectDir);
305
+
220
306
  // Build bubblewrap command with proper namespace isolation
221
307
  const bwrapCmd = [
222
308
  bubblewrap.findBubblewrap(),
@@ -289,6 +375,48 @@ echo "⚠️ Note: Chromium-only (Firefox/WebKit need glibc - use Ubuntu)"
289
375
  console.log(`📁 Project directory: ${resolvedProjectDir}`);
290
376
  console.log(`🎯 Sandbox isolation: full bubblewrap namespace isolation\n`);
291
377
 
378
+ return this.executeCommand(fullCmd, resolvedProjectDir);
379
+ }
380
+
381
+ /**
382
+ * Run Playwright with basic isolation (fallback mode for limited environments)
383
+ */
384
+ async runPlaywrightBasic(options = {}) {
385
+ const { projectDir = '.', testCommand = 'npx playwright test', mountProject = true } = options;
386
+ const resolvedProjectDir = resolve(projectDir);
387
+
388
+ console.log('🎯 Running in basic isolation mode (limited features)...');
389
+
390
+ // Simplified bubblewrap command without namespaces
391
+ const bwrapCmd = [
392
+ bubblewrap.findBubblewrap(),
393
+
394
+ // Basic filesystem
395
+ '--bind', resolvedProjectDir, '/workspace',
396
+ '--chdir', '/workspace',
397
+ '--tmpfs', '/tmp',
398
+ '--share-net', // Keep network access
399
+
400
+ // Essential environment variables
401
+ '--setenv', 'PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1',
402
+ '--setenv', 'PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH=/usr/bin/chromium-browser',
403
+ '--setenv', 'CHROMIUM_FLAGS=--no-sandbox --disable-dev-shm-usage --disable-gpu',
404
+
405
+ // Run command directly without wrapper script
406
+ '/bin/sh', '-c', testCommand
407
+ ];
408
+
409
+ console.log(`🚀 Running: ${testCommand}`);
410
+ console.log(`📁 Project directory: ${resolvedProjectDir}`);
411
+ console.log(`🎯 Sandbox isolation: basic mode (limited namespaces)\n`);
412
+
413
+ return this.executeCommand(bwrapCmd, resolvedProjectDir);
414
+ }
415
+
416
+ /**
417
+ * Execute bubblewrap command with proper error handling
418
+ */
419
+ executeCommand(fullCmd, resolvedProjectDir) {
292
420
  try {
293
421
  // Execute with spawn for better control
294
422
  const child = spawn(fullCmd[0], fullCmd.slice(1), {
@@ -354,6 +482,37 @@ echo "⚠️ Note: Chromium-only (Firefox/WebKit need glibc - use Ubuntu)"
354
482
  }
355
483
  }
356
484
 
485
+ /**
486
+ * Check if a command needs sudo privileges
487
+ */
488
+ commandNeedsSudo(command) {
489
+ const sudoCommands = [
490
+ 'useradd', 'usermod', 'groupadd', 'userdel', 'chsh',
491
+ 'apt-get', 'apt', 'yum', 'dnf', 'apk',
492
+ 'chown', 'chmod',
493
+ 'systemctl', 'service',
494
+ 'npm install -g', 'npm i -g',
495
+ 'pnpm install -g', 'pnpm i -g',
496
+ 'npx --yes playwright install-deps'
497
+ ];
498
+
499
+ // System directories that need sudo for modifications
500
+ const systemPaths = ['/etc/', '/usr/', '/var/', '/opt/', '/home/'];
501
+
502
+ // Check if command modifies system directories
503
+ const modifiesSystem = systemPaths.some(path =>
504
+ command.includes(path) && (
505
+ command.includes('mkdir') ||
506
+ command.includes('tee') ||
507
+ command.includes('>') ||
508
+ command.includes('cp') ||
509
+ command.includes('mv')
510
+ )
511
+ );
512
+
513
+ return sudoCommands.some(cmd => command.includes(cmd)) || modifiesSystem;
514
+ }
515
+
357
516
  /**
358
517
  * Build container from Dockerfile
359
518
  */
@@ -364,40 +523,234 @@ echo "⚠️ Note: Chromium-only (Firefox/WebKit need glibc - use Ubuntu)"
364
523
 
365
524
  const content = readFileSync(dockerfilePath, 'utf-8');
366
525
  const lines = content.split('\n')
367
- .filter(line => line.trim() && !line.trim().startsWith('#'));
526
+ .map(line => line.trim())
527
+ .filter(line => line && !line.startsWith('#'));
368
528
 
529
+ // Parse Dockerfile
530
+ let baseImage = 'alpine';
369
531
  let workdir = '/workspace';
370
532
  const buildCommands = [];
371
-
533
+ const envVars = {};
534
+ const buildArgs = {}; // ARG variables
535
+ let defaultCmd = null;
536
+ let currentUser = 'root';
537
+
538
+ // Handle multi-line commands (lines ending with \)
539
+ const processedLines = [];
540
+ let currentLine = '';
372
541
  for (const line of lines) {
373
- const trimmed = line.trim();
374
- if (trimmed.startsWith('FROM')) {
375
- console.log(`📦 FROM ${trimmed.substring(5).trim()}`);
376
- console.log(' ✅ Using Alpine Linux base image\n');
377
- } else if (trimmed.startsWith('WORKDIR')) {
378
- workdir = trimmed.substring(9).trim().replace(/['"]/g, '');
542
+ if (line.endsWith('\\')) {
543
+ currentLine += line.slice(0, -1) + ' ';
544
+ } else {
545
+ currentLine += line;
546
+ processedLines.push(currentLine.trim());
547
+ currentLine = '';
548
+ }
549
+ }
550
+
551
+ // Parse instructions
552
+ for (const line of processedLines) {
553
+ if (line.startsWith('FROM ')) {
554
+ baseImage = line.substring(5).trim();
555
+ console.log(`📦 FROM ${baseImage}`);
556
+
557
+ // Detect base image type
558
+ if (baseImage.includes('ubuntu') || baseImage.includes('debian')) {
559
+ console.log(' 🐧 Detected Ubuntu/Debian base image\n');
560
+ } else if (baseImage.includes('alpine')) {
561
+ console.log(' 🏔️ Detected Alpine base image\n');
562
+ } else {
563
+ console.log(' ⚠️ Unknown base image type\n');
564
+ }
565
+ } else if (line.startsWith('WORKDIR ')) {
566
+ workdir = line.substring(9).trim().replace(/['"]/g, '');
379
567
  console.log(`📁 WORKDIR ${workdir}\n`);
380
- } else if (trimmed.startsWith('RUN')) {
381
- const command = trimmed.substring(4).trim();
382
- buildCommands.push(command);
383
- console.log(`⚙️ RUN ${command.substring(0, 60)}${command.length > 60 ? '...' : ''}`);
384
- console.log(' 📝 Added to build script\n');
385
- } else if (trimmed.startsWith('CMD')) {
386
- console.log(`🎯 CMD ${trimmed.substring(4).trim()}`);
387
- console.log(' 📝 Default command recorded\n');
568
+ } else if (line.startsWith('ARG ')) {
569
+ const argLine = line.substring(4).trim();
570
+ const match = argLine.match(/^(\w+)(?:=(.+))?$/);
571
+ if (match) {
572
+ buildArgs[match[1]] = match[2] || '';
573
+ console.log(`🏗️ ARG ${match[1]}${match[2] ? `=${match[2]}` : ''}\n`);
574
+ }
575
+ } else if (line.startsWith('ENV ')) {
576
+ const envLine = line.substring(4).trim();
577
+ const match = envLine.match(/^(\w+)=(.+)$/);
578
+ if (match) {
579
+ envVars[match[1]] = match[2];
580
+ console.log(`🔧 ENV ${match[1]}=${match[2]}\n`);
581
+ }
582
+ } else if (line.startsWith('USER ')) {
583
+ currentUser = line.substring(5).trim();
584
+ console.log(`👤 USER ${currentUser}\n`);
585
+ } else if (line.startsWith('RUN ')) {
586
+ const command = line.substring(4).trim();
587
+ buildCommands.push({ command, user: currentUser, workdir });
588
+ console.log(`⚙️ RUN ${command.substring(0, 70)}${command.length > 70 ? '...' : ''}`);
589
+ } else if (line.startsWith('CMD ')) {
590
+ defaultCmd = line.substring(4).trim();
591
+ console.log(`🎯 CMD ${defaultCmd}\n`);
592
+ } else if (line.startsWith('COPY ') || line.startsWith('ADD ')) {
593
+ console.log(`📋 ${line.substring(0, 70)}${line.length > 70 ? '...' : ''}`);
594
+ console.log(' ⚠️ COPY/ADD commands must be run in project directory\n');
388
595
  }
389
596
  }
390
597
 
598
+ // Create container root directory
599
+ const containerRoot = join(this.sandboxDir, 'rootfs');
600
+ if (!existsSync(containerRoot)) {
601
+ mkdirSync(containerRoot, { recursive: true });
602
+ console.log(`📁 Created container root: ${containerRoot}\n`);
603
+ }
604
+
391
605
  // Create build script
392
- const buildScript = buildCommands.join('\n');
393
- const scriptPath = join(this.sandboxDir, 'build.sh');
394
- writeFileSync(scriptPath, buildScript, { mode: 0o755 });
606
+ console.log('📝 Creating build script...\n');
607
+ const buildScriptContent = `#!/bin/bash
608
+ set -e
609
+
610
+ # Build script generated from ${dockerfilePath}
611
+ # Base image: ${baseImage}
612
+ # Total commands: ${buildCommands.length}
395
613
 
396
- console.log('✅ Container build complete!');
397
- console.log(`📝 Build script: ${scriptPath}`);
398
- console.log(`🎯 To run: node bubblewrap-container.js --run\n`);
614
+ echo "🏗️ Starting build process..."
615
+ echo ""
616
+
617
+ ${buildCommands.map((cmd, idx) => `
618
+ # Command ${idx + 1}/${buildCommands.length}
619
+ echo "⚙️ [${idx + 1}/${buildCommands.length}] Executing: ${cmd.command.substring(0, 60)}..."
620
+ ${cmd.user !== 'root' ? `# Running as user: ${cmd.user}` : ''}
621
+ ${cmd.command}
622
+ echo " ✅ Command ${idx + 1} completed"
623
+ echo ""
624
+ `).join('')}
625
+
626
+ echo "✅ Build complete!"
627
+ `;
628
+
629
+ const buildScriptPath = join(this.sandboxDir, 'build.sh');
630
+ writeFileSync(buildScriptPath, buildScriptContent, { mode: 0o755 });
631
+ console.log(`✅ Build script created: ${buildScriptPath}\n`);
632
+
633
+ // Option to execute the build
634
+ if (options.execute !== false) {
635
+ console.log('🚀 Executing build commands...\n');
636
+ console.log('⚠️ Note: Commands will run on host system (Docker-free mode)\n');
637
+
638
+ // Clean up previous build artifacts for idempotency
639
+ console.log('🧹 Cleaning up previous build artifacts...');
640
+ try {
641
+ // Clean up directories
642
+ execSync('sudo rm -rf /commandhistory /workspace /home/node/.oh-my-zsh /home/node/.zshrc* /usr/local/share/npm-global 2>/dev/null || true', {
643
+ stdio: 'pipe'
644
+ });
645
+
646
+ // Clean up npm global packages that will be reinstalled
647
+ const npmRoot = execSync('npm root -g', { encoding: 'utf-8' }).trim();
648
+ execSync(`sudo rm -rf "${npmRoot}/@anthropic-ai/claude-code" "${npmRoot}/@playwright/mcp" 2>/dev/null || true`, {
649
+ stdio: 'pipe'
650
+ });
399
651
 
400
- return { buildScript: scriptPath, workdir };
652
+ console.log('✅ Cleanup complete\n');
653
+ } catch (error) {
654
+ console.log('⚠️ Cleanup had some errors (may be okay)\n');
655
+ }
656
+
657
+ console.log('─'.repeat(60) + '\n');
658
+
659
+ try {
660
+ for (let i = 0; i < buildCommands.length; i++) {
661
+ const { command, user } = buildCommands[i];
662
+ console.log(`\n📍 [${i + 1}/${buildCommands.length}] ${command.substring(0, 80)}${command.length > 80 ? '...' : ''}`);
663
+
664
+ try {
665
+ // Determine execution mode based on user and command requirements
666
+ let execCommand;
667
+ let runMessage;
668
+
669
+ // Commands that need sudo always run as root, regardless of USER directive
670
+ if (this.commandNeedsSudo(command)) {
671
+ // Sudo-requiring command: always run as root
672
+ const escapedCommand = command.replace(/'/g, "'\\''");
673
+ execCommand = `/usr/bin/sudo -E bash -c '${escapedCommand}'`;
674
+ runMessage = '🔐 Running with sudo (requires root privileges)';
675
+ } else if (user !== 'root') {
676
+ // Non-root user: run as that user
677
+ // Use single quotes to avoid nested quote issues with complex commands
678
+ const escapedCommand = command.replace(/'/g, "'\\''");
679
+ execCommand = `/usr/bin/sudo -u ${user} -E bash -c '${escapedCommand}'`;
680
+ runMessage = `👤 Running as user: ${user}`;
681
+ } else {
682
+ // Regular command
683
+ execCommand = command;
684
+ runMessage = null;
685
+ }
686
+
687
+ if (runMessage) {
688
+ console.log(` ${runMessage}`);
689
+ }
690
+
691
+ // Execute command with proper environment (including ARG and ENV variables)
692
+ // Ensure npm/node paths are included for node user
693
+ const npmPath = execSync('which npm 2>/dev/null || echo ""', { encoding: 'utf-8' }).trim();
694
+ const nodePath = npmPath ? dirname(npmPath) : '';
695
+ const basePath = process.env.PATH || '/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin';
696
+ const fullPath = nodePath ? `${nodePath}:${basePath}` : basePath;
697
+
698
+ const execEnv = {
699
+ ...process.env,
700
+ ...buildArgs,
701
+ ...envVars,
702
+ PATH: fullPath
703
+ };
704
+
705
+ // Set HOME for non-root users
706
+ if (user !== 'root') {
707
+ execEnv.HOME = `/home/${user}`;
708
+ }
709
+
710
+ // Determine working directory (root for system commands, sandbox for user commands)
711
+ const isSystemCommand = user === 'root' && this.commandNeedsSudo(command);
712
+ const cwd = isSystemCommand ? '/' : this.sandboxDir;
713
+
714
+ execSync(execCommand, {
715
+ stdio: 'inherit',
716
+ cwd,
717
+ env: execEnv,
718
+ shell: true
719
+ });
720
+ console.log(`✅ Command ${i + 1} completed successfully`);
721
+ } catch (error) {
722
+ console.log(`❌ Command ${i + 1} failed: ${error.message}`);
723
+ console.log(`\n⚠️ Build failed at command ${i + 1}/${buildCommands.length}`);
724
+ console.log(`📝 Partial build script available at: ${buildScriptPath}`);
725
+ throw error;
726
+ }
727
+ }
728
+
729
+ console.log('\n' + '─'.repeat(60));
730
+ console.log('\n🎉 Build completed successfully!\n');
731
+ console.log(`📦 Container root: ${containerRoot}`);
732
+ console.log(`📝 Build script: ${buildScriptPath}`);
733
+ if (defaultCmd) {
734
+ console.log(`🎯 Default command: ${defaultCmd}`);
735
+ }
736
+ console.log('');
737
+
738
+ } catch (error) {
739
+ throw new Error(`Build failed: ${error.message}`);
740
+ }
741
+ } else {
742
+ console.log('📝 Build script ready (not executed)');
743
+ console.log(` To build manually: bash ${buildScriptPath}\n`);
744
+ }
745
+
746
+ return {
747
+ buildScript: buildScriptPath,
748
+ workdir,
749
+ baseImage,
750
+ containerRoot,
751
+ defaultCmd,
752
+ envVars
753
+ };
401
754
  }
402
755
  }
403
756
 
@@ -438,15 +791,38 @@ Requirements:
438
791
  await container.setupAlpineRootfs();
439
792
 
440
793
  } else if (args[0] === 'build') {
441
- const dockerfile = args[1] || './Dockerfile.claudebox';
794
+ const dockerfile = args[1] || './Dockerfile';
442
795
  if (!existsSync(dockerfile)) {
443
796
  throw new Error(`Dockerfile not found: ${dockerfile}`);
444
797
  }
445
- await container.buildFromDockerfile(dockerfile);
798
+
799
+ // Check for --dry-run flag
800
+ const dryRun = args.includes('--dry-run');
801
+ const options = { execute: !dryRun };
802
+
803
+ if (dryRun) {
804
+ console.log('🔍 Dry-run mode: Commands will be parsed but not executed\n');
805
+ }
806
+
807
+ await container.buildFromDockerfile(dockerfile, options);
446
808
 
447
809
  } else if (args[0] === 'run') {
448
810
  const projectDir = args[1] || '.';
449
- await container.runPlaywright({ projectDir });
811
+
812
+ // First try simple test to verify container works
813
+ console.log('🧪 Testing container functionality...\n');
814
+ try {
815
+ await container.runSimpleTest({ projectDir });
816
+ console.log('✅ Container test successful!\n');
817
+
818
+ // Now try Playwright
819
+ console.log('🎭 Running Playwright tests...\n');
820
+ await container.runPlaywright({ projectDir });
821
+ } catch (error) {
822
+ console.log(`⚠️ Container test failed: ${error.message}`);
823
+ console.log('🚫 Skipping Playwright tests due to container issues\n');
824
+ throw error;
825
+ }
450
826
 
451
827
  } else if (args[0] === 'shell') {
452
828
  const projectDir = args[1] || '.';