sandboxbox 1.2.2 โ 2.0.1
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/README.md +180 -99
- package/cli.js +125 -162
- package/package.json +11 -12
- package/BUBBLEWRAP-REALITY.md +0 -210
- package/Dockerfile.test +0 -16
- package/USAGE.md +0 -111
- package/bin/bwrap +0 -0
- package/build-final.log +0 -2217
- package/build-output.log +0 -289
- package/complete-build.log +0 -231
- package/container.js +0 -847
- package/debug-cli.js +0 -15
- package/final-build.log +0 -268
- package/final-complete-build.log +0 -240
- package/full-build.log +0 -234
- package/init-firewall.sh +0 -36
- package/lib/bubblewrap.js +0 -203
- package/npm-build-test.log +0 -410
- package/playwright.sh +0 -183
- package/run.sh +0 -12
- package/sandboxbox-sandbox/build.sh +0 -83
- package/scripts/build.js +0 -303
- package/scripts/download-bubblewrap.js +0 -186
- package/test-cli.js +0 -72
- package/test-project/Dockerfile.sandboxbox +0 -20
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;
|