sandboxbox 2.5.5 → 3.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/cli.js +49 -45
- package/package.json +10 -9
- package/testproject/Dockerfile +3 -0
- package/utils/commands/claude.js +27 -83
- package/utils/commands/container.js +31 -266
- package/utils/commands/index.js +2 -5
- package/utils/sandbox.js +106 -0
- package/scripts/download-podman.js +0 -103
- package/scripts/podman-config.js +0 -20
- package/scripts/podman-extract.js +0 -117
- package/utils/claude-workspace.js +0 -69
- package/utils/isolation.js +0 -222
- package/utils/podman.js +0 -228
package/utils/sandbox.js
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { mkdtempSync, rmSync, cpSync, existsSync, mkdirSync } from 'fs';
|
|
2
|
+
import { tmpdir, homedir } from 'os';
|
|
3
|
+
import { join } from 'path';
|
|
4
|
+
import { spawn, execSync } from 'child_process';
|
|
5
|
+
|
|
6
|
+
export function createSandbox(projectDir) {
|
|
7
|
+
const sandboxDir = mkdtempSync(join(tmpdir(), 'sandboxbox-'));
|
|
8
|
+
const workspaceDir = join(sandboxDir, 'workspace');
|
|
9
|
+
|
|
10
|
+
if (projectDir && existsSync(projectDir)) {
|
|
11
|
+
const isGitRepo = existsSync(join(projectDir, '.git'));
|
|
12
|
+
|
|
13
|
+
if (isGitRepo) {
|
|
14
|
+
execSync(`git clone "${projectDir}" "${workspaceDir}"`, {
|
|
15
|
+
stdio: 'pipe',
|
|
16
|
+
shell: true,
|
|
17
|
+
windowsHide: true
|
|
18
|
+
});
|
|
19
|
+
} else {
|
|
20
|
+
cpSync(projectDir, workspaceDir, {
|
|
21
|
+
recursive: true,
|
|
22
|
+
filter: (src) => !src.includes('node_modules')
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const claudeDir = join(sandboxDir, '.claude');
|
|
28
|
+
const hostClaudeDir = join(homedir(), '.claude');
|
|
29
|
+
|
|
30
|
+
if (existsSync(hostClaudeDir)) {
|
|
31
|
+
try {
|
|
32
|
+
cpSync(hostClaudeDir, claudeDir, {
|
|
33
|
+
recursive: true,
|
|
34
|
+
filter: (src) => {
|
|
35
|
+
return !src.includes('shell-snapshots') &&
|
|
36
|
+
!src.includes('logs') &&
|
|
37
|
+
!src.includes('debug') &&
|
|
38
|
+
!src.includes('.log');
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
} catch (e) {
|
|
42
|
+
console.error('Warning: Failed to copy Claude config:', e.message);
|
|
43
|
+
}
|
|
44
|
+
} else {
|
|
45
|
+
mkdirSync(claudeDir, { recursive: true });
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const playwrightDir = join(sandboxDir, '.playwright');
|
|
49
|
+
mkdirSync(playwrightDir, { recursive: true });
|
|
50
|
+
|
|
51
|
+
const cleanup = () => {
|
|
52
|
+
try {
|
|
53
|
+
rmSync(sandboxDir, { recursive: true, force: true });
|
|
54
|
+
} catch (e) {
|
|
55
|
+
console.error('Cleanup failed:', e.message);
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
return { sandboxDir, cleanup };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function createSandboxEnv(sandboxDir, options = {}) {
|
|
63
|
+
const hostHome = homedir();
|
|
64
|
+
|
|
65
|
+
const env = {
|
|
66
|
+
PATH: process.env.PATH,
|
|
67
|
+
HOME: sandboxDir,
|
|
68
|
+
USERPROFILE: sandboxDir,
|
|
69
|
+
TMPDIR: join(sandboxDir, 'tmp'),
|
|
70
|
+
TEMP: join(sandboxDir, 'tmp'),
|
|
71
|
+
TMP: join(sandboxDir, 'tmp'),
|
|
72
|
+
PLAYWRIGHT_BROWSERS_PATH: join(sandboxDir, 'browsers'),
|
|
73
|
+
PLAYWRIGHT_STORAGE_STATE: join(sandboxDir, '.playwright', 'storage-state.json'),
|
|
74
|
+
ANTHROPIC_AUTH_TOKEN: process.env.ANTHROPIC_AUTH_TOKEN,
|
|
75
|
+
CLAUDECODE: '1',
|
|
76
|
+
NPM_CONFIG_CACHE: process.env.NPM_CONFIG_CACHE || join(hostHome, '.npm'),
|
|
77
|
+
npm_config_cache: process.env.npm_config_cache || join(hostHome, '.npm'),
|
|
78
|
+
...options
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
return env;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function runInSandbox(commandStr, args, sandboxDir, env) {
|
|
85
|
+
return new Promise((resolve, reject) => {
|
|
86
|
+
const fullCommand = args.length > 0 ? `${commandStr} ${args.join(' ')}` : commandStr;
|
|
87
|
+
|
|
88
|
+
const proc = spawn(fullCommand, [], {
|
|
89
|
+
cwd: join(sandboxDir, 'workspace'),
|
|
90
|
+
env,
|
|
91
|
+
stdio: 'inherit',
|
|
92
|
+
shell: true,
|
|
93
|
+
windowsHide: false
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
proc.on('close', (code) => {
|
|
97
|
+
if (code === 0) {
|
|
98
|
+
resolve();
|
|
99
|
+
} else {
|
|
100
|
+
reject(new Error(`Process exited with code ${code}`));
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
proc.on('error', reject);
|
|
105
|
+
});
|
|
106
|
+
}
|
|
@@ -1,103 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Auto-download Podman portable binaries
|
|
5
|
-
* Similar to how sqlite/playwright auto-downloads platform-specific binaries
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
import { existsSync, mkdirSync, chmodSync, unlinkSync } from 'fs';
|
|
9
|
-
import { join, dirname } from 'path';
|
|
10
|
-
import { fileURLToPath } from 'url';
|
|
11
|
-
import { PODMAN_VERSION, DOWNLOADS } from './podman-config.js';
|
|
12
|
-
import { download, extractZip, extractTarGz } from './podman-extract.js';
|
|
13
|
-
|
|
14
|
-
const __filename = fileURLToPath(import.meta.url);
|
|
15
|
-
const __dirname = dirname(__filename);
|
|
16
|
-
const binDir = join(__dirname, '..', 'bin');
|
|
17
|
-
|
|
18
|
-
async function main() {
|
|
19
|
-
const platform = process.platform;
|
|
20
|
-
|
|
21
|
-
console.log(`\n📦 Setting up Podman portable binaries for ${platform}...`);
|
|
22
|
-
|
|
23
|
-
if (!DOWNLOADS[platform]) {
|
|
24
|
-
console.log(`⚠️ Platform ${platform} not supported for auto-download`);
|
|
25
|
-
console.log(` Skipping auto-download (will use system Podman if available)`);
|
|
26
|
-
return;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
if (!existsSync(binDir)) {
|
|
30
|
-
mkdirSync(binDir, { recursive: true });
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
const { url, binary, extract } = DOWNLOADS[platform];
|
|
34
|
-
const binaryPath = join(binDir, binary);
|
|
35
|
-
|
|
36
|
-
if (existsSync(binaryPath)) {
|
|
37
|
-
console.log(`✅ Podman already installed at ${binaryPath}`);
|
|
38
|
-
return;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
console.log(`📥 Downloading Podman v${PODMAN_VERSION}...`);
|
|
42
|
-
|
|
43
|
-
const archiveName = url.split('/').pop();
|
|
44
|
-
const archivePath = join(binDir, archiveName);
|
|
45
|
-
|
|
46
|
-
try {
|
|
47
|
-
const extractedDir = join(binDir, 'podman-4.9.3');
|
|
48
|
-
if (existsSync(extractedDir)) {
|
|
49
|
-
const fs = await import('fs/promises');
|
|
50
|
-
await fs.rm(extractedDir, { recursive: true, force: true });
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
console.log(` Downloading from GitHub releases...`);
|
|
54
|
-
await download(url, archivePath);
|
|
55
|
-
console.log(`✅ Downloaded successfully`);
|
|
56
|
-
|
|
57
|
-
console.log(`📦 Extracting...`);
|
|
58
|
-
if (extract === 'tar') {
|
|
59
|
-
await extractTarGz(archivePath, binDir, 1);
|
|
60
|
-
} else if (extract === 'unzip') {
|
|
61
|
-
await extractZip(archivePath, binDir);
|
|
62
|
-
|
|
63
|
-
if (platform === 'win32') {
|
|
64
|
-
const extractedDir = join(binDir, `podman-4.9.3`);
|
|
65
|
-
const extractedPodman = join(extractedDir, 'usr', 'bin', 'podman.exe');
|
|
66
|
-
const targetPodman = join(binDir, binary);
|
|
67
|
-
|
|
68
|
-
if (existsSync(extractedPodman)) {
|
|
69
|
-
const fs = await import('fs/promises');
|
|
70
|
-
await fs.copyFile(extractedPodman, targetPodman);
|
|
71
|
-
await fs.rm(extractedDir, { recursive: true, force: true });
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
if (platform !== 'win32' && existsSync(binaryPath)) {
|
|
77
|
-
chmodSync(binaryPath, 0o755);
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
console.log(`✅ Podman installed successfully!`);
|
|
81
|
-
console.log(` Binary: ${binaryPath}\n`);
|
|
82
|
-
|
|
83
|
-
if (existsSync(archivePath)) {
|
|
84
|
-
unlinkSync(archivePath);
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
} catch (error) {
|
|
88
|
-
console.error(`⚠️ Auto-download failed: ${error.message}`);
|
|
89
|
-
console.log(`\n💡 No problem! You can install Podman manually:`);
|
|
90
|
-
if (platform === 'win32') {
|
|
91
|
-
console.log(` winget install RedHat.Podman`);
|
|
92
|
-
} else if (platform === 'darwin') {
|
|
93
|
-
console.log(` brew install podman && podman machine init && podman machine start`);
|
|
94
|
-
} else {
|
|
95
|
-
console.log(` sudo apt-get install podman # Ubuntu/Debian`);
|
|
96
|
-
}
|
|
97
|
-
console.log(`\n Or it will use system Podman if installed.\n`);
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
main().catch(() => {
|
|
102
|
-
// Silently fail - will use system Podman
|
|
103
|
-
});
|
package/scripts/podman-config.js
DELETED
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
export const PODMAN_VERSION = '4.9.3';
|
|
2
|
-
export const ARCH = process.arch === 'arm64' ? 'arm64' : 'amd64';
|
|
3
|
-
|
|
4
|
-
export const DOWNLOADS = {
|
|
5
|
-
win32: {
|
|
6
|
-
url: `https://github.com/containers/podman/releases/download/v${PODMAN_VERSION}/podman-remote-release-windows_${ARCH}.zip`,
|
|
7
|
-
binary: 'podman.exe',
|
|
8
|
-
extract: 'unzip'
|
|
9
|
-
},
|
|
10
|
-
darwin: {
|
|
11
|
-
url: `https://github.com/containers/podman/releases/download/v${PODMAN_VERSION}/podman-remote-release-darwin_${ARCH}.tar.gz`,
|
|
12
|
-
binary: 'podman',
|
|
13
|
-
extract: 'tar'
|
|
14
|
-
},
|
|
15
|
-
linux: {
|
|
16
|
-
url: `https://github.com/containers/podman/releases/download/v${PODMAN_VERSION}/podman-remote-static-linux_${ARCH}.tar.gz`,
|
|
17
|
-
binary: `podman-remote-static-linux_${ARCH}`,
|
|
18
|
-
extract: 'tar'
|
|
19
|
-
}
|
|
20
|
-
};
|
|
@@ -1,117 +0,0 @@
|
|
|
1
|
-
import { createWriteStream, createReadStream, unlinkSync } from 'fs';
|
|
2
|
-
import { execSync } from 'child_process';
|
|
3
|
-
import { createGunzip } from 'zlib';
|
|
4
|
-
import { pipeline } from 'stream/promises';
|
|
5
|
-
|
|
6
|
-
export async function extractZip(zipPath, extractTo) {
|
|
7
|
-
return new Promise((resolve, reject) => {
|
|
8
|
-
try {
|
|
9
|
-
const psCommand = `Add-Type -AssemblyName System.IO.Compression.FileSystem; [System.IO.Compression.ZipFile]::ExtractToDirectory('${zipPath.replace(/'/g, "''")}', '${extractTo.replace(/'/g, "''")}')`;
|
|
10
|
-
|
|
11
|
-
// Add retry logic for ZIP extraction (file lock issues)
|
|
12
|
-
let retries = 0;
|
|
13
|
-
const maxRetries = 3;
|
|
14
|
-
|
|
15
|
-
while (retries < maxRetries) {
|
|
16
|
-
try {
|
|
17
|
-
execSync(`powershell -Command "${psCommand}"`, {
|
|
18
|
-
stdio: 'pipe',
|
|
19
|
-
shell: true,
|
|
20
|
-
windowsHide: true,
|
|
21
|
-
timeout: 120000 // ZIP extraction can take time
|
|
22
|
-
});
|
|
23
|
-
resolve();
|
|
24
|
-
return;
|
|
25
|
-
} catch (error) {
|
|
26
|
-
retries++;
|
|
27
|
-
if (retries < maxRetries && error.message.includes('being used by another process')) {
|
|
28
|
-
console.log(` File locked, retrying extraction (${retries}/${maxRetries})...`);
|
|
29
|
-
// Wait 2 seconds before retry
|
|
30
|
-
const start = Date.now();
|
|
31
|
-
while (Date.now() - start < 2000) {
|
|
32
|
-
// Wait
|
|
33
|
-
}
|
|
34
|
-
continue;
|
|
35
|
-
}
|
|
36
|
-
reject(error);
|
|
37
|
-
return;
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
} catch (error) {
|
|
41
|
-
reject(error);
|
|
42
|
-
}
|
|
43
|
-
});
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
export async function extractTarGz(tarPath, extractTo, stripComponents = 0) {
|
|
47
|
-
return new Promise(async (resolve, reject) => {
|
|
48
|
-
try {
|
|
49
|
-
const tarWithoutGz = tarPath.replace('.gz', '');
|
|
50
|
-
const readStream = createReadStream(tarPath);
|
|
51
|
-
const writeStream = createWriteStream(tarWithoutGz);
|
|
52
|
-
const gunzip = createGunzip();
|
|
53
|
-
|
|
54
|
-
await pipeline(readStream, gunzip, writeStream);
|
|
55
|
-
|
|
56
|
-
try {
|
|
57
|
-
execSync(`tar -xf "${tarWithoutGz}" -C "${extractTo}"${stripComponents ? ` --strip-components=${stripComponents}` : ''}`, {
|
|
58
|
-
stdio: 'pipe',
|
|
59
|
-
shell: process.platform === 'win32',
|
|
60
|
-
windowsHide: process.platform === 'win32',
|
|
61
|
-
timeout: 120000
|
|
62
|
-
});
|
|
63
|
-
} catch (tarError) {
|
|
64
|
-
if (process.platform === 'win32') {
|
|
65
|
-
try {
|
|
66
|
-
execSync(`bsdtar -xf "${tarWithoutGz}" -C "${extractTo}"${stripComponents ? ` --strip-components=${stripComponents}` : ''}`, {
|
|
67
|
-
stdio: 'pipe',
|
|
68
|
-
shell: true,
|
|
69
|
-
windowsHide: true,
|
|
70
|
-
timeout: 120000
|
|
71
|
-
});
|
|
72
|
-
} catch (bsdtarError) {
|
|
73
|
-
throw new Error(`Failed to extract tar archive. Please install tar or bsdtar: ${tarError.message}`);
|
|
74
|
-
}
|
|
75
|
-
} else {
|
|
76
|
-
throw tarError;
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
unlinkSync(tarWithoutGz);
|
|
81
|
-
resolve();
|
|
82
|
-
} catch (error) {
|
|
83
|
-
reject(error);
|
|
84
|
-
}
|
|
85
|
-
});
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
export function download(url, dest) {
|
|
89
|
-
return new Promise(async (resolve, reject) => {
|
|
90
|
-
const { get: httpsGet } = await import('https');
|
|
91
|
-
const { get: httpGet } = await import('http');
|
|
92
|
-
const { createWriteStream } = await import('fs');
|
|
93
|
-
|
|
94
|
-
const get = url.startsWith('https') ? httpsGet : httpGet;
|
|
95
|
-
|
|
96
|
-
get(url, (response) => {
|
|
97
|
-
if (response.statusCode === 302 || response.statusCode === 301) {
|
|
98
|
-
return download(response.headers.location, dest).then(resolve).catch(reject);
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
if (response.statusCode !== 200) {
|
|
102
|
-
reject(new Error(`Download failed: ${response.statusCode}`));
|
|
103
|
-
return;
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
const file = createWriteStream(dest);
|
|
107
|
-
response.pipe(file);
|
|
108
|
-
|
|
109
|
-
file.on('finish', () => {
|
|
110
|
-
file.close();
|
|
111
|
-
resolve();
|
|
112
|
-
});
|
|
113
|
-
|
|
114
|
-
file.on('error', reject);
|
|
115
|
-
}).on('error', reject);
|
|
116
|
-
});
|
|
117
|
-
}
|
|
@@ -1,69 +0,0 @@
|
|
|
1
|
-
import { execSync } from 'child_process';
|
|
2
|
-
import { buildContainerMounts } from './isolation.js';
|
|
3
|
-
|
|
4
|
-
export function getClaudeEnvironment() {
|
|
5
|
-
const envVars = {};
|
|
6
|
-
|
|
7
|
-
// Collect Anthropic and Claude environment variables
|
|
8
|
-
for (const [key, value] of Object.entries(process.env)) {
|
|
9
|
-
if (key.startsWith('ANTHROPIC_') || key.startsWith('CLAUDE')) {
|
|
10
|
-
envVars[key] = value;
|
|
11
|
-
}
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
return envVars;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
export function buildClaudeContainerCommand(projectPath, podmanPath, command = 'claude', customMounts = null) {
|
|
18
|
-
const envVars = getClaudeEnvironment();
|
|
19
|
-
const envArgs = Object.entries(envVars)
|
|
20
|
-
.map(([key, value]) => `-e ${key}="${value}"`)
|
|
21
|
-
.join(' ');
|
|
22
|
-
|
|
23
|
-
const homeDir = process.platform === 'win32' ? process.env.USERPROFILE : process.env.HOME;
|
|
24
|
-
|
|
25
|
-
let allMounts = [];
|
|
26
|
-
|
|
27
|
-
if (customMounts) {
|
|
28
|
-
// Use provided custom mounts (includes git identity and host remote)
|
|
29
|
-
allMounts = customMounts;
|
|
30
|
-
} else {
|
|
31
|
-
// Build base mounts from isolation utility (includes git identity)
|
|
32
|
-
const baseMounts = buildContainerMounts(projectPath);
|
|
33
|
-
allMounts = baseMounts;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
// Add Claude-specific mounts
|
|
37
|
-
const claudeMounts = [
|
|
38
|
-
`-v "${homeDir}/.claude:/root/.claude-host:ro"`,
|
|
39
|
-
`-v "${process.cwd()}/claude-settings.json:/root/.claude/settings.json:ro"`
|
|
40
|
-
];
|
|
41
|
-
|
|
42
|
-
allMounts = [...allMounts, ...claudeMounts];
|
|
43
|
-
|
|
44
|
-
return `${podmanPath} run --rm -it ${allMounts.join(' ')} ${envArgs} --env HOME=/root sandboxbox-local:latest ${command}`;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
export function createClaudeDockerfile() {
|
|
48
|
-
return `FROM node:20
|
|
49
|
-
|
|
50
|
-
# Install development tools
|
|
51
|
-
RUN apt-get update && apt-get install -y --no-install-recommends \\
|
|
52
|
-
git curl bash sudo nano vim \\
|
|
53
|
-
&& apt-get clean && rm -rf /var/lib/apt/lists/*
|
|
54
|
-
|
|
55
|
-
WORKDIR /workspace
|
|
56
|
-
|
|
57
|
-
# Install Claude Code
|
|
58
|
-
RUN npm install -g @anthropic-ai/claude-code@latest
|
|
59
|
-
|
|
60
|
-
# Setup MCP servers after Claude installation
|
|
61
|
-
RUN claude mcp add glootie -- npx -y mcp-glootie@latest && \\
|
|
62
|
-
claude mcp add vexify -- npx -y mcp-vexify@latest && \\
|
|
63
|
-
claude mcp add playwright -- npx @playwright/mcp@latest
|
|
64
|
-
|
|
65
|
-
# Create isolated workspace script with cleanup
|
|
66
|
-
RUN echo '#!/bin/bash\\nset -e\\n\\necho "🚀 Starting SandboxBox with Claude Code in isolated environment..."\\necho "📁 Working directory: /workspace"\\necho "🎯 This is an isolated copy of your repository"\\n\\n# Cleanup function for temporary files\\ncleanup_temp_files() {\\n echo "🧹 Cleaning up temporary files..."\\n find /tmp -user root -name "claude-*" -type f -delete 2>/dev/null || true\\n find /tmp -user root -name "*.tmp" -type f -delete 2>/dev/null || true\\n find /var/tmp -user root -name "claude-*" -type f -delete 2>/dev/null || true\\n}\\n\\n# Set up cleanup trap\\ntrap cleanup_temp_files EXIT INT TERM\\n\\nif [ -d "/workspace/.git" ]; then\\n echo "✅ Git repository detected in workspace"\\n echo "📋 Current status:"\\n git status\\n echo ""\\n echo "🔧 Starting Claude Code..."\\n echo "💡 Changes will be isolated and will NOT affect the original repository"\\n echo "📝 To save changes, use git commands to commit and push before exiting"\\n echo "🔧 MCP servers: glootie, vexify, playwright"\\n exec claude\\nelse\\n echo "❌ Error: /workspace is not a valid git repository"\\n exit 1\\nfi' > /usr/local/bin/start-isolated-sandbox.sh && chmod +x /usr/local/bin/start-isolated-sandbox.sh
|
|
67
|
-
|
|
68
|
-
CMD ["/usr/local/bin/start-isolated-sandbox.sh"]`;
|
|
69
|
-
}
|
package/utils/isolation.js
DELETED
|
@@ -1,222 +0,0 @@
|
|
|
1
|
-
import { mkdtempSync, rmSync, existsSync } from 'fs';
|
|
2
|
-
import { tmpdir } from 'os';
|
|
3
|
-
import { join } from 'path';
|
|
4
|
-
import { execSync } from 'child_process';
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* Builds container volume mounts with git identity and host remote
|
|
8
|
-
* @param {string} tempProjectDir - Temporary project directory
|
|
9
|
-
* @param {string} originalProjectDir - Original host project directory
|
|
10
|
-
* @returns {Array} - Array of volume mount strings
|
|
11
|
-
*/
|
|
12
|
-
export function buildContainerMounts(tempProjectDir, originalProjectDir) {
|
|
13
|
-
const mounts = [`-v "${tempProjectDir}:/workspace:rw"`];
|
|
14
|
-
|
|
15
|
-
// Add host repository as git remote
|
|
16
|
-
mounts.push(`-v "${originalProjectDir}:/host-repo:rw"`);
|
|
17
|
-
|
|
18
|
-
// Add git identity mounts
|
|
19
|
-
const homeDir = process.platform === 'win32' ? process.env.USERPROFILE : process.env.HOME;
|
|
20
|
-
|
|
21
|
-
if (existsSync(`${homeDir}/.gitconfig`)) {
|
|
22
|
-
mounts.push(`-v "${homeDir}/.gitconfig:/root/.gitconfig:ro"`);
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
if (existsSync(`${homeDir}/.ssh`)) {
|
|
26
|
-
mounts.push(`-v "${homeDir}/.ssh:/root/.ssh:ro"`);
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
return mounts;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
/**
|
|
33
|
-
* Creates an isolated temporary environment for running commands
|
|
34
|
-
* @param {string} projectDir - Source project directory
|
|
35
|
-
* @returns {Object} - Contains tempDir, tempProjectDir, and cleanup function
|
|
36
|
-
*/
|
|
37
|
-
export function createIsolatedEnvironment(projectDir) {
|
|
38
|
-
const tempDir = mkdtempSync(join(tmpdir(), 'sandboxbox-'));
|
|
39
|
-
const projectName = projectDir.split(/[\\\/]/).pop() || 'project';
|
|
40
|
-
const tempProjectDir = join(tempDir, projectName);
|
|
41
|
-
|
|
42
|
-
// Copy project to temporary directory (creates isolation)
|
|
43
|
-
// First create the directory (cross-platform)
|
|
44
|
-
if (process.platform === 'win32') {
|
|
45
|
-
execSync(`powershell -Command "New-Item -ItemType Directory -Path '${tempProjectDir}' -Force"`, {
|
|
46
|
-
stdio: 'pipe',
|
|
47
|
-
shell: true,
|
|
48
|
-
windowsHide: true,
|
|
49
|
-
timeout: 30000
|
|
50
|
-
});
|
|
51
|
-
} else {
|
|
52
|
-
execSync(`mkdir -p "${tempProjectDir}"`, {
|
|
53
|
-
stdio: 'pipe',
|
|
54
|
-
shell: true,
|
|
55
|
-
timeout: 30000
|
|
56
|
-
});
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
if (process.platform === 'win32') {
|
|
60
|
-
// Windows approach - include hidden files like .git
|
|
61
|
-
execSync(`powershell -Command "Copy-Item -Path '${projectDir}\\*' -Destination '${tempProjectDir}' -Recurse -Force -Exclude 'node_modules'"`, {
|
|
62
|
-
stdio: 'pipe',
|
|
63
|
-
shell: true,
|
|
64
|
-
windowsHide: true,
|
|
65
|
-
timeout: 60000 // Copy operations can take longer
|
|
66
|
-
});
|
|
67
|
-
// Also copy hidden files separately
|
|
68
|
-
execSync(`powershell -Command "Get-ChildItem -Path '${projectDir}' -Force -Name | Where-Object { $_ -like '.*' } | ForEach-Object { Copy-Item -Path (Join-Path '${projectDir}' $_) -Destination '${tempProjectDir}' -Recurse -Force }"`, {
|
|
69
|
-
stdio: 'pipe',
|
|
70
|
-
shell: true,
|
|
71
|
-
windowsHide: true,
|
|
72
|
-
timeout: 60000
|
|
73
|
-
});
|
|
74
|
-
} else {
|
|
75
|
-
// Unix approach - include hidden files
|
|
76
|
-
execSync(`cp -r "${projectDir}"/.* "${tempProjectDir}/" 2>/dev/null || true`, {
|
|
77
|
-
stdio: 'pipe',
|
|
78
|
-
shell: true,
|
|
79
|
-
timeout: 60000
|
|
80
|
-
});
|
|
81
|
-
execSync(`cp -r "${projectDir}"/* "${tempProjectDir}/"`, {
|
|
82
|
-
stdio: 'pipe',
|
|
83
|
-
shell: true,
|
|
84
|
-
timeout: 60000
|
|
85
|
-
});
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
// Configure git remote to point to mounted host repository (only if git repo)
|
|
89
|
-
try {
|
|
90
|
-
// Check if the project directory is a git repository
|
|
91
|
-
const gitDirPath = process.platform === 'win32'
|
|
92
|
-
? `${projectDir}\\.git`
|
|
93
|
-
: `${projectDir}/.git`;
|
|
94
|
-
|
|
95
|
-
if (!existsSync(gitDirPath)) {
|
|
96
|
-
// Not a git repository, skip git setup
|
|
97
|
-
// Define cleanup function early for early return
|
|
98
|
-
const cleanup = () => {
|
|
99
|
-
try {
|
|
100
|
-
rmSync(tempDir, { recursive: true, force: true });
|
|
101
|
-
} catch (cleanupError) {
|
|
102
|
-
// Ignore cleanup errors
|
|
103
|
-
}
|
|
104
|
-
};
|
|
105
|
-
return { tempDir, tempProjectDir, cleanup };
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
// Also check if the temp directory has .git after copy
|
|
109
|
-
const tempGitDirPath = process.platform === 'win32'
|
|
110
|
-
? `${tempProjectDir}\\.git`
|
|
111
|
-
: `${tempProjectDir}/.git`;
|
|
112
|
-
|
|
113
|
-
if (!existsSync(tempGitDirPath)) {
|
|
114
|
-
// Copy didn't preserve git, skip git setup
|
|
115
|
-
// Define cleanup function early for early return
|
|
116
|
-
const cleanup = () => {
|
|
117
|
-
try {
|
|
118
|
-
rmSync(tempDir, { recursive: true, force: true });
|
|
119
|
-
} catch (cleanupError) {
|
|
120
|
-
// Ignore cleanup errors
|
|
121
|
-
}
|
|
122
|
-
};
|
|
123
|
-
return { tempDir, tempProjectDir, cleanup };
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
// Normalize paths for cross-platform compatibility
|
|
127
|
-
const normalizedTempDir = tempProjectDir.replace(/\\/g, '/');
|
|
128
|
-
const normalizedOriginalDir = projectDir.replace(/\\/g, '/');
|
|
129
|
-
|
|
130
|
-
// Configure git to allow operations in mounted directories
|
|
131
|
-
execSync(`git config --global --add safe.directory /workspace`, {
|
|
132
|
-
stdio: 'pipe',
|
|
133
|
-
shell: true,
|
|
134
|
-
windowsHide: process.platform === 'win32',
|
|
135
|
-
timeout: 60000 // 1 minute for git operations
|
|
136
|
-
});
|
|
137
|
-
|
|
138
|
-
// Configure host repository to accept pushes to checked-out branch
|
|
139
|
-
if (process.platform === 'win32') {
|
|
140
|
-
try {
|
|
141
|
-
execSync(`cd "${normalizedOriginalDir}" && git config receive.denyCurrentBranch ignore`, {
|
|
142
|
-
stdio: 'pipe',
|
|
143
|
-
shell: true,
|
|
144
|
-
windowsHide: true,
|
|
145
|
-
timeout: 60000 // 1 minute for git operations
|
|
146
|
-
});
|
|
147
|
-
} catch (e) {
|
|
148
|
-
// Ignore if git config fails
|
|
149
|
-
}
|
|
150
|
-
} else {
|
|
151
|
-
execSync(`cd "${normalizedOriginalDir}" && git config receive.denyCurrentBranch ignore`, {
|
|
152
|
-
stdio: 'pipe',
|
|
153
|
-
shell: true,
|
|
154
|
-
timeout: 60000 // 1 minute for git operations
|
|
155
|
-
});
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
// Remove any existing origin first (Windows-compatible)
|
|
159
|
-
if (process.platform === 'win32') {
|
|
160
|
-
try {
|
|
161
|
-
execSync(`cd "${normalizedTempDir}" && git remote remove origin`, {
|
|
162
|
-
stdio: 'pipe',
|
|
163
|
-
shell: true,
|
|
164
|
-
windowsHide: true,
|
|
165
|
-
timeout: 60000 // 1 minute for git operations
|
|
166
|
-
});
|
|
167
|
-
} catch (e) {
|
|
168
|
-
// Ignore if origin doesn't exist
|
|
169
|
-
}
|
|
170
|
-
} else {
|
|
171
|
-
execSync(`cd "${normalizedTempDir}" && git remote remove origin 2>/dev/null || true`, {
|
|
172
|
-
stdio: 'pipe',
|
|
173
|
-
shell: true,
|
|
174
|
-
timeout: 60000 // 1 minute for git operations
|
|
175
|
-
});
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
// Add origin pointing to mounted host repository (accessible from container)
|
|
179
|
-
execSync(`cd "${normalizedTempDir}" && git remote add origin /host-repo`, {
|
|
180
|
-
stdio: 'pipe',
|
|
181
|
-
shell: true,
|
|
182
|
-
windowsHide: process.platform === 'win32',
|
|
183
|
-
timeout: 60000 // 1 minute for git operations
|
|
184
|
-
});
|
|
185
|
-
|
|
186
|
-
// Set up upstream tracking for current branch (use push -u to set upstream)
|
|
187
|
-
const currentBranch = execSync(`cd "${normalizedTempDir}" && git branch --show-current`, {
|
|
188
|
-
encoding: 'utf8',
|
|
189
|
-
stdio: 'pipe',
|
|
190
|
-
windowsHide: process.platform === 'win32',
|
|
191
|
-
timeout: 60000 // 1 minute for git operations
|
|
192
|
-
}).trim();
|
|
193
|
-
|
|
194
|
-
// Note: Upstream will be set automatically on first push with -u flag
|
|
195
|
-
// No need to set up upstream manually as it may not exist yet
|
|
196
|
-
} catch (error) {
|
|
197
|
-
// Log git remote setup errors for debugging
|
|
198
|
-
console.error(`Git remote setup failed: ${error.message}`);
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
// Ensure cleanup on exit
|
|
202
|
-
const cleanup = () => {
|
|
203
|
-
try {
|
|
204
|
-
rmSync(tempDir, { recursive: true, force: true });
|
|
205
|
-
} catch (cleanupError) {
|
|
206
|
-
// Ignore cleanup errors
|
|
207
|
-
}
|
|
208
|
-
};
|
|
209
|
-
|
|
210
|
-
return { tempDir, tempProjectDir, cleanup };
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
/**
|
|
214
|
-
* Sets up cleanup handlers for process signals
|
|
215
|
-
* @param {Function} cleanup - Cleanup function to call
|
|
216
|
-
*/
|
|
217
|
-
export function setupCleanupHandlers(cleanup) {
|
|
218
|
-
// Set up cleanup handlers
|
|
219
|
-
process.on('exit', cleanup);
|
|
220
|
-
process.on('SIGINT', () => { cleanup(); process.exit(130); });
|
|
221
|
-
process.on('SIGTERM', () => { cleanup(); process.exit(143); });
|
|
222
|
-
}
|