sandboxbox 1.2.2 โ†’ 2.0.0

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 DELETED
@@ -1,847 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- /**
4
- * Bubblewrap Container Runner - Playwright + True Isolation
5
- *
6
- * This uses bubblewrap for truly rootless container operation
7
- * Perfect for Playwright with 8ms startup and zero privileged setup
8
- *
9
- * Requirements:
10
- * - bubblewrap (bwrap) - install with: apt-get install bubblewrap
11
- * - No root access needed after installation
12
- */
13
-
14
- import { readFileSync, writeFileSync, existsSync, mkdirSync, cpSync } from 'fs';
15
- import { execSync, spawn } from 'child_process';
16
- import { join, resolve, dirname } from 'path';
17
- import { fileURLToPath } from 'url';
18
- import { bubblewrap } from './lib/bubblewrap.js';
19
-
20
- const __filename = fileURLToPath(import.meta.url);
21
- const __dirname = dirname(__filename);
22
-
23
- class BubblewrapContainer {
24
- constructor(options = {}) {
25
- this.sandboxDir = options.sandboxDir || './sandboxbox-sandbox';
26
- this.alpineRoot = bubblewrap.getAlpineRoot();
27
- this.verbose = options.verbose !== false;
28
- this.env = { ...process.env };
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
- }
35
- }
36
-
37
- /**
38
- * Check if bubblewrap is available
39
- */
40
- checkBubblewrap() {
41
- if (!bubblewrap.isAvailable()) {
42
- throw new Error(bubblewrap.findBubblewrap().message);
43
- }
44
- return true;
45
- }
46
-
47
- /**
48
- * Set up Alpine Linux rootfs with Playwright support
49
- */
50
- async setupAlpineRootfs() {
51
- console.log('๐Ÿ”๏ธ Setting up Alpine Linux rootfs for Playwright...\n');
52
-
53
- await bubblewrap.ensureAlpineRoot();
54
- console.log('โœ… Alpine rootfs ready!\n');
55
- }
56
-
57
- /**
58
- * Install required packages in Alpine with Playwright compatibility fixes
59
- */
60
- async setupAlpinePackages() {
61
- console.log('๐Ÿ“ฆ Installing packages in Alpine with Playwright compatibility...');
62
-
63
- // Create a temporary setup script addressing glibc/musl issues
64
- const setupScript = `
65
- #!/bin/sh
66
- set -e
67
-
68
- # Setup repositories
69
- echo 'https://dl-cdn.alpinelinux.org/alpine/v3.20/main' > /etc/apk/repositories
70
- echo 'https://dl-cdn.alpinelinux.org/alpine/v3.20/community' >> /etc/apk/repositories
71
-
72
- # Update package index
73
- apk update
74
-
75
- # Install Node.js and required tools for Playwright on Alpine
76
- # NOTE: Using Alpine's system Chromium to avoid glibc/musl compatibility issues
77
- apk add --no-cache \\
78
- nodejs \\
79
- npm \\
80
- chromium \\
81
- nss \\
82
- freetype \\
83
- harfbuzz \\
84
- ttf-freefont \\
85
- ca-certificates \\
86
- wget \\
87
- curl \\
88
- git \\
89
- bash \\
90
- xvfb \\
91
- mesa-gl \\
92
- libx11 \\
93
- libxrandr \\
94
- libxss \\
95
- libgcc \\
96
- libstdc++ \\
97
- expat \\
98
- dbus
99
-
100
- # Create workspace directory
101
- mkdir -p /workspace
102
-
103
- # Install Claude Code CLI
104
- npm install -g @anthropic-ai/claude-code
105
-
106
- # Create Playwright config for Alpine
107
- mkdir -p /workspace/playwright-config
108
-
109
- # Create Playwright configuration for Alpine (addresses sandbox conflicts)
110
- cat > /workspace/playwright.config.js << 'EOF'
111
- export default defineConfig({
112
- use: {
113
- // Required: Chromium's sandbox conflicts with bubblewrap's sandbox
114
- chromiumSandbox: false,
115
- headless: true,
116
- // Use Alpine's system Chromium
117
- executablePath: '/usr/bin/chromium-browser',
118
- },
119
- projects: [
120
- {
121
- name: 'chromium',
122
- use: {
123
- executablePath: '/usr/bin/chromium-browser',
124
- },
125
- },
126
- ],
127
- // Skip browser downloads since we use system Chromium
128
- webServer: {
129
- command: 'echo "Skipping browser download"',
130
- },
131
- });
132
- EOF
133
-
134
- # Create test script that handles Chromium sandbox issues
135
- cat > /workspace/run-playwright.sh << 'EOF'
136
- #!/bin/sh
137
- set -e
138
-
139
- # Environment variables for Playwright on Alpine with bubblewrap
140
- export PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1
141
- export PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH=/usr/bin/chromium-browser
142
- export DISPLAY=:99
143
-
144
- # Start virtual display for headless operation
145
- Xvfb :99 -screen 0 1024x768x24 > /dev/null 2>&1 &
146
- XVFB_PID=$!
147
-
148
- # Give Xvfb time to start
149
- sleep 2
150
-
151
- # Cleanup function
152
- cleanup() {
153
- if [ ! -z "$XVFB_PID" ]; then
154
- kill $XVFB_PID 2>/dev/null || true
155
- fi
156
- }
157
-
158
- # Trap cleanup
159
- trap cleanup EXIT INT TERM
160
-
161
- echo "๐ŸŽญ Running Playwright in Alpine with bubblewrap isolation..."
162
- echo "๐Ÿ“ Workspace: $(pwd)"
163
- echo "๐ŸŒ Display: $DISPLAY"
164
- echo "๐Ÿš€ Chromium: $(which chromium-browser)"
165
-
166
- # Run Playwright tests
167
- exec "$@"
168
- EOF
169
-
170
- chmod +x /workspace/run-playwright.sh
171
-
172
- echo "โœ… Alpine setup complete with Playwright compatibility fixes"
173
- echo "๐Ÿ“‹ Installed: Node.js, Chromium, Xvfb, fonts, libraries"
174
- echo "๐ŸŽฏ Created: Playwright config and wrapper scripts"
175
- echo "โš ๏ธ Note: Chromium-only (Firefox/WebKit need glibc - use Ubuntu)"
176
- `;
177
-
178
- const scriptPath = join(this.sandboxDir, 'setup-alpine.sh');
179
- writeFileSync(scriptPath, setupScript, { mode: 0o755 });
180
-
181
- try {
182
- // Run setup inside Alpine using bubblewrap
183
- const bwrapCmd = [
184
- 'bwrap',
185
- '--ro-bind', `${this.alpineRoot}`, '/',
186
- '--proc', '/proc',
187
- '--dev', '/dev',
188
- '--tmpfs', '/tmp',
189
- '--tmpfs', '/var/tmp',
190
- '--tmpfs', '/run',
191
- '--bind', `${this.sandboxDir}`, '/host',
192
- '--share-net',
193
- '--die-with-parent',
194
- '--new-session',
195
- scriptPath
196
- ];
197
-
198
- console.log('๐Ÿ”ง Running Alpine setup...');
199
- execSync(bwrapCmd.join(' '), { stdio: 'inherit' });
200
-
201
- } catch (error) {
202
- throw new Error(`Alpine setup failed: ${error.message}`);
203
- }
204
- }
205
-
206
- /**
207
- * Run Playwright tests in bubblewrap sandbox with proper sandbox conflict handling
208
- */
209
- async runPlaywright(options = {}) {
210
- this.checkBubblewrap();
211
-
212
- const {
213
- projectDir = '.',
214
- testCommand = 'npx playwright test',
215
- mountProject = true,
216
- headless = true
217
- } = options;
218
-
219
- console.log('๐ŸŽญ Starting Playwright in bubblewrap sandbox...\n');
220
- console.log('๐Ÿ”ง Addressing Alpine/Playwright compatibility issues...\n');
221
-
222
- // Resolve project directory
223
- const resolvedProjectDir = resolve(projectDir);
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
-
306
- // Build bubblewrap command with proper namespace isolation
307
- const bwrapCmd = [
308
- bubblewrap.findBubblewrap(),
309
-
310
- // Core filesystem - read-only Alpine rootfs
311
- '--ro-bind', `${this.alpineRoot}`, '/',
312
- '--proc', '/proc',
313
- '--dev', '/dev',
314
-
315
- // Critical: Bind mount /dev/dri for GPU acceleration (Chrome needs this)
316
- '--dev-bind', '/dev/dri', '/dev/dri',
317
-
318
- // Temporary directories (fresh for each run)
319
- '--tmpfs', '/tmp',
320
- '--tmpfs', '/var/tmp',
321
- '--tmpfs', '/run',
322
- '--tmpfs', '/dev/shm', // Chrome shared memory
323
-
324
- // Mount project directory
325
- ...(mountProject ? [
326
- '--bind', resolvedProjectDir, '/workspace',
327
- '--chdir', '/workspace'
328
- ] : []),
329
-
330
- // Mount X11 socket for display
331
- '--bind', '/tmp/.X11-unix', '/tmp/.X11-unix',
332
-
333
- // Host directory access
334
- '--bind', this.sandboxDir, '/host',
335
-
336
- // Networking (required for Playwright)
337
- '--share-net',
338
-
339
- // Process isolation - critical for security
340
- '--unshare-pid',
341
- '--unshare-ipc',
342
- '--unshare-uts',
343
- '--unshare-cgroup', // Prevent process group interference
344
-
345
- // Safety features
346
- '--die-with-parent',
347
- '--new-session',
348
- '--as-pid-1', // Make bash PID 1 in the namespace
349
-
350
- // Set hostname for isolation
351
- '--hostname', 'playwright-sandbox',
352
-
353
- // Environment variables for Playwright on Alpine
354
- '--setenv', 'PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1',
355
- '--setenv', 'PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH=/usr/bin/chromium-browser',
356
- '--setenv', 'DISPLAY=:99', // Virtual display
357
- '--setenv', 'CI=true',
358
- '--setenv', 'NODE_ENV=test',
359
-
360
- // Chrome/Chromium specific variables
361
- '--setenv', 'CHROMIUM_FLAGS=--no-sandbox --disable-dev-shm-usage --disable-gpu',
362
- '--setenv', 'CHROME_BIN=/usr/bin/chromium-browser',
363
-
364
- // Preserve important user environment
365
- ...Object.entries(this.env)
366
- .filter(([key]) => !['PATH', 'HOME', 'DISPLAY'].includes(key))
367
- .flatMap(([key, value]) => ['--setenv', key, value])
368
- ];
369
-
370
- // Use the wrapper script that handles Xvfb and Chromium sandbox issues
371
- const wrappedCommand = `/workspace/run-playwright.sh ${testCommand}`;
372
- const fullCmd = [...bwrapCmd, '/bin/sh', '-c', wrappedCommand];
373
-
374
- console.log(`๐Ÿš€ Running: ${testCommand}`);
375
- console.log(`๐Ÿ“ Project directory: ${resolvedProjectDir}`);
376
- console.log(`๐ŸŽฏ Sandbox isolation: full bubblewrap namespace isolation\n`);
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) {
420
- try {
421
- // Execute with spawn for better control
422
- const child = spawn(fullCmd[0], fullCmd.slice(1), {
423
- stdio: 'inherit',
424
- cwd: resolvedProjectDir,
425
- env: this.env
426
- });
427
-
428
- return new Promise((resolve, reject) => {
429
- child.on('close', (code) => {
430
- if (code === 0) {
431
- console.log('\nโœ… Playwright tests completed successfully!');
432
- resolve(code);
433
- } else {
434
- console.log(`\nโŒ Playwright tests failed with exit code: ${code}`);
435
- reject(new Error(`Playwright tests failed with exit code: ${code}`));
436
- }
437
- });
438
-
439
- child.on('error', (error) => {
440
- reject(new Error(`Failed to start Playwright: ${error.message}`));
441
- });
442
- });
443
-
444
- } catch (error) {
445
- throw new Error(`Playwright execution failed: ${error.message}`);
446
- }
447
- }
448
-
449
- /**
450
- * Run interactive shell in sandbox
451
- */
452
- async runShell(options = {}) {
453
- this.checkBubblewrap();
454
-
455
- const { projectDir = '.' } = options;
456
- const resolvedProjectDir = resolve(projectDir);
457
-
458
- const bwrapCmd = [
459
- 'bwrap',
460
- '--ro-bind', `${this.alpineRoot}`, '/',
461
- '--proc', '/proc',
462
- '--dev', '/dev',
463
- '--tmpfs', '/tmp',
464
- '--bind', resolvedProjectDir, '/workspace',
465
- '--chdir', '/workspace',
466
- '--share-net',
467
- '--unshare-pid',
468
- '--die-with-parent',
469
- '--new-session',
470
- '--hostname', 'claudebox-sandbox',
471
- '--setenv', 'PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1',
472
- '--setenv', 'PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH=/usr/bin/chromium-browser',
473
- '/bin/bash'
474
- ];
475
-
476
- console.log('๐Ÿš Starting interactive shell in sandbox...\n');
477
-
478
- try {
479
- execSync(bwrapCmd.join(' '), { stdio: 'inherit' });
480
- } catch (error) {
481
- throw new Error(`Shell execution failed: ${error.message}`);
482
- }
483
- }
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
-
516
- /**
517
- * Build container from Dockerfile
518
- */
519
- async buildFromDockerfile(dockerfilePath, options = {}) {
520
- this.checkBubblewrap();
521
-
522
- console.log('๐Ÿณ Building container with bubblewrap isolation...\n');
523
-
524
- const content = readFileSync(dockerfilePath, 'utf-8');
525
- const lines = content.split('\n')
526
- .map(line => line.trim())
527
- .filter(line => line && !line.startsWith('#'));
528
-
529
- // Parse Dockerfile
530
- let baseImage = 'alpine';
531
- let workdir = '/workspace';
532
- const buildCommands = [];
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 = '';
541
- for (const line of lines) {
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, '');
567
- console.log(`๐Ÿ“ WORKDIR ${workdir}\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');
595
- }
596
- }
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
-
605
- // Create build script
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}
613
-
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
- });
651
-
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
- };
754
- }
755
- }
756
-
757
- // Main execution
758
- async function main() {
759
- const args = process.argv.slice(2);
760
-
761
- if (args.includes('--help') || args.includes('-h')) {
762
- console.log(`
763
- Bubblewrap Container Runner - Playwright + True Isolation
764
-
765
- Usage:
766
- node bubblewrap-container.js <command> [options]
767
-
768
- Commands:
769
- setup Set up Alpine Linux rootfs with Playwright
770
- build <dockerfile> Build from Dockerfile
771
- run [project-dir] Run Playwright tests
772
- shell [project-dir] Start interactive shell
773
-
774
- Examples:
775
- node bubblewrap-container.js setup
776
- node bubblewrap-container.js build Dockerfile.claudebox
777
- node bubblewrap-container.js run ./my-project
778
- node bubblewrap-container.js shell ./my-project
779
-
780
- Requirements:
781
- - bubblewrap (bwrap): sudo apt-get install bubblewrap
782
- - No root privileges needed after installation
783
- `);
784
- process.exit(0);
785
- }
786
-
787
- const container = new BubblewrapContainer({ verbose: true });
788
-
789
- try {
790
- if (args[0] === 'setup') {
791
- await container.setupAlpineRootfs();
792
-
793
- } else if (args[0] === 'build') {
794
- const dockerfile = args[1] || './Dockerfile';
795
- if (!existsSync(dockerfile)) {
796
- throw new Error(`Dockerfile not found: ${dockerfile}`);
797
- }
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);
808
-
809
- } else if (args[0] === 'run') {
810
- const projectDir = args[1] || '.';
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
- }
826
-
827
- } else if (args[0] === 'shell') {
828
- const projectDir = args[1] || '.';
829
- await container.runShell({ projectDir });
830
-
831
- } else {
832
- console.error('โŒ Unknown command. Use --help for usage.');
833
- process.exit(1);
834
- }
835
-
836
- } catch (error) {
837
- console.error('โŒ Error:', error.message);
838
- process.exit(1);
839
- }
840
- }
841
-
842
- // Run if called directly
843
- if (import.meta.url === `file://${process.argv[1]}`) {
844
- main().catch(console.error);
845
- }
846
-
847
- export default BubblewrapContainer;