@larkup/sandbox 0.1.2 → 0.1.3
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/.turbo/turbo-build.log +2 -2
- package/CHANGELOG.md +6 -0
- package/dist/docker-runner.js +27 -15
- package/dist/index.d.ts +4 -2
- package/dist/index.js +3 -1
- package/dist/local-runner.d.ts +5 -0
- package/dist/local-runner.js +176 -0
- package/dist/mime.d.ts +1 -0
- package/dist/mime.js +16 -0
- package/dist/providers/browserbase.d.ts +12 -0
- package/dist/providers/browserbase.js +61 -0
- package/dist/providers/cloudflare.d.ts +5 -0
- package/dist/providers/cloudflare.js +55 -0
- package/dist/providers/daytona.d.ts +12 -0
- package/dist/providers/daytona.js +84 -0
- package/dist/providers/e2b.d.ts +9 -0
- package/dist/providers/e2b.js +70 -0
- package/dist/providers/flyio.d.ts +9 -0
- package/dist/providers/flyio.js +98 -0
- package/dist/providers/index.d.ts +13 -0
- package/dist/providers/index.js +26 -0
- package/dist/providers/modal.d.ts +8 -0
- package/dist/providers/modal.js +96 -0
- package/dist/providers/northflank.d.ts +9 -0
- package/dist/providers/northflank.js +122 -0
- package/dist/providers/vercel.d.ts +9 -0
- package/dist/providers/vercel.js +79 -0
- package/dist/providers/webcontainers.d.ts +13 -0
- package/dist/providers/webcontainers.js +28 -0
- package/dist/registry.d.ts +20 -0
- package/dist/registry.js +281 -0
- package/dist/remote-exec.d.ts +42 -0
- package/dist/remote-exec.js +144 -0
- package/dist/sandbox-manager.d.ts +5 -2
- package/dist/sandbox-manager.js +67 -46
- package/dist/types.d.ts +58 -9
- package/dist/types.js +1 -1
- package/package.json +21 -4
- package/src/docker-runner.ts +29 -19
- package/src/index.ts +11 -2
- package/src/local-runner.test.ts +22 -0
- package/src/local-runner.ts +199 -0
- package/src/mime.ts +17 -0
- package/src/providers/browserbase.test.ts +55 -0
- package/src/providers/browserbase.ts +88 -0
- package/src/providers/cloudflare.test.ts +63 -0
- package/src/providers/cloudflare.ts +72 -0
- package/src/providers/daytona.test.ts +60 -0
- package/src/providers/daytona.ts +144 -0
- package/src/providers/e2b.test.ts +100 -0
- package/src/providers/e2b.ts +115 -0
- package/src/providers/flyio.test.ts +69 -0
- package/src/providers/flyio.ts +148 -0
- package/src/providers/index.ts +42 -0
- package/src/providers/modal.test.ts +107 -0
- package/src/providers/modal.ts +167 -0
- package/src/providers/northflank.test.ts +110 -0
- package/src/providers/northflank.ts +199 -0
- package/src/providers/vercel.test.ts +84 -0
- package/src/providers/vercel.ts +143 -0
- package/src/providers/webcontainers.test.ts +21 -0
- package/src/providers/webcontainers.ts +36 -0
- package/src/registry.test.ts +84 -0
- package/src/registry.ts +308 -0
- package/src/remote-exec.test.ts +155 -0
- package/src/remote-exec.ts +190 -0
- package/src/sandbox-manager.test.ts +74 -0
- package/src/sandbox-manager.ts +71 -51
- package/src/types.ts +81 -23
- package/tsconfig.build.json +4 -0
- package/vitest.config.ts +9 -0
package/.turbo/turbo-build.log
CHANGED
package/CHANGELOG.md
CHANGED
package/dist/docker-runner.js
CHANGED
|
@@ -23,9 +23,6 @@ async function getDocker() {
|
|
|
23
23
|
}
|
|
24
24
|
return dockerInstance;
|
|
25
25
|
}
|
|
26
|
-
/* ------------------------------------------------------------------ */
|
|
27
|
-
/* Health check */
|
|
28
|
-
/* ------------------------------------------------------------------ */
|
|
29
26
|
export async function checkDockerHealth() {
|
|
30
27
|
const docker = await getDocker();
|
|
31
28
|
try {
|
|
@@ -41,16 +38,17 @@ export async function checkDockerHealth() {
|
|
|
41
38
|
};
|
|
42
39
|
}
|
|
43
40
|
catch (err) {
|
|
41
|
+
const raw = err?.message ?? '';
|
|
42
|
+
const dockerMissing = /ENOENT|ECONNREFUSED|EACCES|no such file|cannot connect to the docker daemon/i.test(raw);
|
|
44
43
|
return {
|
|
45
44
|
status: 'docker-not-found',
|
|
46
45
|
backend: 'docker',
|
|
47
|
-
error:
|
|
46
|
+
error: dockerMissing
|
|
47
|
+
? "Docker isn't installed or running on this machine. Local code execution is optional — install Docker Desktop to enable it, or configure a remote sandbox provider below."
|
|
48
|
+
: raw || 'Docker daemon not available',
|
|
48
49
|
};
|
|
49
50
|
}
|
|
50
51
|
}
|
|
51
|
-
/* ------------------------------------------------------------------ */
|
|
52
|
-
/* Image management */
|
|
53
|
-
/* ------------------------------------------------------------------ */
|
|
54
52
|
export async function buildSandboxImage(onProgress) {
|
|
55
53
|
const docker = await getDocker();
|
|
56
54
|
const dockerfilePath = path.join(DOCKERFILE_DIR, 'Dockerfile');
|
|
@@ -94,9 +92,6 @@ export async function ensureImage() {
|
|
|
94
92
|
return false;
|
|
95
93
|
}
|
|
96
94
|
}
|
|
97
|
-
/* ------------------------------------------------------------------ */
|
|
98
|
-
/* Code execution */
|
|
99
|
-
/* ------------------------------------------------------------------ */
|
|
100
95
|
/**
|
|
101
96
|
* Build the Python wrapper script that:
|
|
102
97
|
* 1. Sets up pandas compatibility shims to handle deprecated APIs
|
|
@@ -260,6 +255,20 @@ export async function executeInDocker(request, config) {
|
|
|
260
255
|
// Write input files
|
|
261
256
|
if (request.files) {
|
|
262
257
|
for (const file of request.files) {
|
|
258
|
+
if (!file.name ||
|
|
259
|
+
file.name === '.' ||
|
|
260
|
+
file.name === '..' ||
|
|
261
|
+
file.name.includes('/') ||
|
|
262
|
+
file.name.includes('\\') ||
|
|
263
|
+
file.name.includes('\0')) {
|
|
264
|
+
return {
|
|
265
|
+
stdout: '',
|
|
266
|
+
stderr: 'Sandbox file names must be plain file names.',
|
|
267
|
+
exitCode: 1,
|
|
268
|
+
artifacts: [],
|
|
269
|
+
executionTimeMs: Date.now() - startTime,
|
|
270
|
+
};
|
|
271
|
+
}
|
|
263
272
|
const filePath = path.join(inputDir, file.name);
|
|
264
273
|
if (file.isBase64) {
|
|
265
274
|
await fs.writeFile(filePath, Buffer.from(file.content, 'base64'));
|
|
@@ -272,16 +281,19 @@ export async function executeInDocker(request, config) {
|
|
|
272
281
|
// Prepare the execution code
|
|
273
282
|
let execCode;
|
|
274
283
|
let cmd;
|
|
284
|
+
let scriptName;
|
|
275
285
|
if (request.language === 'python') {
|
|
276
286
|
execCode = wrapPythonCode(request.code);
|
|
277
287
|
const scriptPath = path.join(tmpDir, 'run.py');
|
|
278
288
|
await fs.writeFile(scriptPath, execCode, 'utf8');
|
|
289
|
+
scriptName = 'run.py';
|
|
279
290
|
cmd = ['python3', '/sandbox/run.py'];
|
|
280
291
|
}
|
|
281
292
|
else {
|
|
282
293
|
// JavaScript/TypeScript (future)
|
|
283
294
|
const scriptPath = path.join(tmpDir, 'run.js');
|
|
284
295
|
await fs.writeFile(scriptPath, request.code, 'utf8');
|
|
296
|
+
scriptName = 'run.js';
|
|
285
297
|
cmd = ['node', '/sandbox/run.js'];
|
|
286
298
|
}
|
|
287
299
|
const timeout = request.timeout ?? config.timeoutMs;
|
|
@@ -300,7 +312,7 @@ export async function executeInDocker(request, config) {
|
|
|
300
312
|
Binds: [
|
|
301
313
|
`${inputDir}:${INPUT_DIR}:ro`,
|
|
302
314
|
`${outputDir}:${OUTPUT_DIR}:rw`,
|
|
303
|
-
`${tmpDir}
|
|
315
|
+
`${tmpDir}/${scriptName}:/sandbox/${scriptName}:ro`,
|
|
304
316
|
],
|
|
305
317
|
ReadonlyRootfs: false,
|
|
306
318
|
},
|
|
@@ -311,8 +323,9 @@ export async function executeInDocker(request, config) {
|
|
|
311
323
|
await container.start();
|
|
312
324
|
// Wait for completion with timeout
|
|
313
325
|
const waitPromise = container.wait();
|
|
326
|
+
let timeoutHandle;
|
|
314
327
|
const timeoutPromise = new Promise((_, reject) => {
|
|
315
|
-
setTimeout(async () => {
|
|
328
|
+
timeoutHandle = setTimeout(async () => {
|
|
316
329
|
try {
|
|
317
330
|
await container?.kill();
|
|
318
331
|
}
|
|
@@ -323,6 +336,8 @@ export async function executeInDocker(request, config) {
|
|
|
323
336
|
}, timeout);
|
|
324
337
|
});
|
|
325
338
|
const result = await Promise.race([waitPromise, timeoutPromise]);
|
|
339
|
+
if (timeoutHandle)
|
|
340
|
+
clearTimeout(timeoutHandle);
|
|
326
341
|
// Capture logs
|
|
327
342
|
const logs = await container.logs({
|
|
328
343
|
stdout: true,
|
|
@@ -386,9 +401,6 @@ export async function executeInDocker(request, config) {
|
|
|
386
401
|
}
|
|
387
402
|
}
|
|
388
403
|
}
|
|
389
|
-
/* ------------------------------------------------------------------ */
|
|
390
|
-
/* Helpers */
|
|
391
|
-
/* ------------------------------------------------------------------ */
|
|
392
404
|
/**
|
|
393
405
|
* Docker multiplexes stdout/stderr into a single stream with 8-byte headers.
|
|
394
406
|
* Header format: [type(1)][0(3)][size(4)] where type: 1=stdout, 2=stderr
|
package/dist/index.d.ts
CHANGED
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Re-exports the public API for the sandbox package.
|
|
5
5
|
*/
|
|
6
|
-
export { SandboxManager } from './sandbox-manager.js';
|
|
6
|
+
export { SandboxManager, verifySandboxProvider } from './sandbox-manager.js';
|
|
7
7
|
export { checkDockerHealth, buildSandboxImage, ensureImage, executeInDocker, } from './docker-runner.js';
|
|
8
|
-
export
|
|
8
|
+
export { checkLocalRuntime, executeLocally } from './local-runner.js';
|
|
9
|
+
export { SANDBOX_PROVIDERS, SANDBOX_PROVIDER_LIST, getSandboxProvider, validateSandboxCredentials, } from './registry.js';
|
|
10
|
+
export type { SandboxBackend, SandboxConfig, DockerConfig, SandboxLanguage, SandboxFile, ExecutionRequest, ExecutionResult, ExecutionArtifact, SandboxStatus, SandboxHealthCheck, SandboxCredentialField, SandboxProviderDescriptor, SandboxProviderAdapter, } from './types.js';
|
package/dist/index.js
CHANGED
|
@@ -3,5 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Re-exports the public API for the sandbox package.
|
|
5
5
|
*/
|
|
6
|
-
export { SandboxManager } from './sandbox-manager.js';
|
|
6
|
+
export { SandboxManager, verifySandboxProvider } from './sandbox-manager.js';
|
|
7
7
|
export { checkDockerHealth, buildSandboxImage, ensureImage, executeInDocker, } from './docker-runner.js';
|
|
8
|
+
export { checkLocalRuntime, executeLocally } from './local-runner.js';
|
|
9
|
+
export { SANDBOX_PROVIDERS, SANDBOX_PROVIDER_LIST, getSandboxProvider, validateSandboxCredentials, } from './registry.js';
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { ExecutionRequest, ExecutionResult, SandboxHealthCheck } from './types.js';
|
|
2
|
+
/** Checks whether the host can run Python analysis without Docker. */
|
|
3
|
+
export declare function checkLocalRuntime(): Promise<SandboxHealthCheck>;
|
|
4
|
+
/** Executes trusted code in a temporary host workspace and collects its output. */
|
|
5
|
+
export declare function executeLocally(request: ExecutionRequest): Promise<ExecutionResult>;
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { promises as fs } from 'node:fs';
|
|
3
|
+
import os from 'node:os';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { spawn } from 'node:child_process';
|
|
6
|
+
import { getMimeType } from './mime.js';
|
|
7
|
+
let pythonCommand;
|
|
8
|
+
function run(command, args, cwd, timeoutMs = 10_000) {
|
|
9
|
+
return new Promise((resolve, reject) => {
|
|
10
|
+
const child = spawn(command.executable, [...command.args, ...args], {
|
|
11
|
+
cwd,
|
|
12
|
+
env: process.env,
|
|
13
|
+
shell: false,
|
|
14
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
15
|
+
});
|
|
16
|
+
// Node ChildProcess is an EventEmitter at runtime. TypeScript 7's narrowed stdio return type
|
|
17
|
+
// omits that inherited surface, so retain it explicitly for lifecycle events.
|
|
18
|
+
const lifecycle = child;
|
|
19
|
+
let stdout = '';
|
|
20
|
+
let stderr = '';
|
|
21
|
+
let timedOut = false;
|
|
22
|
+
const timer = setTimeout(() => {
|
|
23
|
+
timedOut = true;
|
|
24
|
+
child.kill('SIGKILL');
|
|
25
|
+
}, timeoutMs);
|
|
26
|
+
child.stdout.on('data', (chunk) => {
|
|
27
|
+
stdout = (stdout + chunk).slice(-1_000_000);
|
|
28
|
+
});
|
|
29
|
+
child.stderr.on('data', (chunk) => {
|
|
30
|
+
stderr = (stderr + chunk).slice(-1_000_000);
|
|
31
|
+
});
|
|
32
|
+
lifecycle.once('error', (error) => {
|
|
33
|
+
clearTimeout(timer);
|
|
34
|
+
reject(error);
|
|
35
|
+
});
|
|
36
|
+
lifecycle.once('close', (code) => {
|
|
37
|
+
clearTimeout(timer);
|
|
38
|
+
resolve({
|
|
39
|
+
stdout,
|
|
40
|
+
stderr: timedOut ? `${stderr}\nExecution timed out.`.trim() : stderr,
|
|
41
|
+
exitCode: timedOut ? 124 : code ?? 1,
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
async function resolvePython() {
|
|
47
|
+
if (pythonCommand !== undefined)
|
|
48
|
+
return pythonCommand;
|
|
49
|
+
const candidates = process.env.LARKUP_PYTHON
|
|
50
|
+
? [{ executable: process.env.LARKUP_PYTHON, args: [] }]
|
|
51
|
+
: process.platform === 'win32'
|
|
52
|
+
? [
|
|
53
|
+
{ executable: 'py', args: ['-3'] },
|
|
54
|
+
{ executable: 'python', args: [] },
|
|
55
|
+
]
|
|
56
|
+
: [
|
|
57
|
+
{ executable: 'python3', args: [] },
|
|
58
|
+
{ executable: 'python', args: [] },
|
|
59
|
+
];
|
|
60
|
+
for (const candidate of candidates) {
|
|
61
|
+
try {
|
|
62
|
+
if ((await run(candidate, ['--version'])).exitCode === 0) {
|
|
63
|
+
pythonCommand = candidate;
|
|
64
|
+
return candidate;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
pythonCommand = null;
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
function assertSafeFileName(name) {
|
|
75
|
+
if (!name || name === '.' || name === '..' || /[\\/\0]/.test(name)) {
|
|
76
|
+
throw new Error('Sandbox file names must be plain file names.');
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
function pythonWrapper(code, outputDir) {
|
|
80
|
+
return `import base64, os, sys, traceback
|
|
81
|
+
os.makedirs(${JSON.stringify(outputDir)}, exist_ok=True)
|
|
82
|
+
os.environ.setdefault('MPLBACKEND', 'Agg')
|
|
83
|
+
try:
|
|
84
|
+
import numpy as np
|
|
85
|
+
import pandas as pd
|
|
86
|
+
except ImportError:
|
|
87
|
+
pass
|
|
88
|
+
try:
|
|
89
|
+
import matplotlib
|
|
90
|
+
matplotlib.use('Agg')
|
|
91
|
+
import matplotlib.pyplot as plt
|
|
92
|
+
def _save_figures(*args, **kwargs):
|
|
93
|
+
for i, number in enumerate(plt.get_fignums()):
|
|
94
|
+
plt.figure(number).savefig(os.path.join(${JSON.stringify(outputDir)}, f'chart_{i}.png'), dpi=150, bbox_inches='tight')
|
|
95
|
+
plt.close('all')
|
|
96
|
+
plt.show = _save_figures
|
|
97
|
+
except ImportError:
|
|
98
|
+
pass
|
|
99
|
+
try:
|
|
100
|
+
exec(compile(base64.b64decode(${JSON.stringify(Buffer.from(code).toString('base64'))}), 'analysis.py', 'exec'))
|
|
101
|
+
if 'plt' in globals() and plt.get_fignums():
|
|
102
|
+
plt.show()
|
|
103
|
+
except Exception as error:
|
|
104
|
+
traceback.print_exception(error, file=sys.stderr)
|
|
105
|
+
sys.exit(1)
|
|
106
|
+
`;
|
|
107
|
+
}
|
|
108
|
+
/** Checks whether the host can run Python analysis without Docker. */
|
|
109
|
+
export async function checkLocalRuntime() {
|
|
110
|
+
const python = await resolvePython();
|
|
111
|
+
if (!python) {
|
|
112
|
+
return {
|
|
113
|
+
status: 'error',
|
|
114
|
+
backend: 'local',
|
|
115
|
+
error: 'Local code execution needs Python 3. Install Python or choose Docker or a remote sandbox provider.',
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
return { status: 'ready', backend: 'local' };
|
|
119
|
+
}
|
|
120
|
+
/** Executes trusted code in a temporary host workspace and collects its output. */
|
|
121
|
+
export async function executeLocally(request) {
|
|
122
|
+
const startTime = Date.now();
|
|
123
|
+
const tempDir = path.join(os.tmpdir(), `larkup-local-${randomUUID()}`);
|
|
124
|
+
const inputDir = path.join(tempDir, 'input');
|
|
125
|
+
const outputDir = path.join(tempDir, 'output');
|
|
126
|
+
try {
|
|
127
|
+
await fs.mkdir(inputDir, { recursive: true });
|
|
128
|
+
await fs.mkdir(outputDir, { recursive: true });
|
|
129
|
+
for (const file of request.files ?? []) {
|
|
130
|
+
assertSafeFileName(file.name);
|
|
131
|
+
await fs.writeFile(path.join(inputDir, file.name), file.isBase64 ? Buffer.from(file.content, 'base64') : file.content);
|
|
132
|
+
}
|
|
133
|
+
const isPython = request.language === 'python';
|
|
134
|
+
const command = isPython ? await resolvePython() : { executable: process.execPath, args: [] };
|
|
135
|
+
if (!command) {
|
|
136
|
+
return {
|
|
137
|
+
stdout: '',
|
|
138
|
+
stderr: 'Local code execution needs Python 3. Install Python or choose Docker or a remote sandbox provider.',
|
|
139
|
+
exitCode: 1,
|
|
140
|
+
artifacts: [],
|
|
141
|
+
executionTimeMs: Date.now() - startTime,
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
const script = path.join(tempDir, isPython ? 'run.py' : 'run.js');
|
|
145
|
+
const source = isPython
|
|
146
|
+
? pythonWrapper(request.code, outputDir)
|
|
147
|
+
: `process.env.LARKUP_OUTPUT_DIR = ${JSON.stringify(outputDir)};\n${request.code}`;
|
|
148
|
+
await fs.writeFile(script, source, 'utf8');
|
|
149
|
+
const result = await run(command, [script], inputDir, request.timeout ?? 30_000);
|
|
150
|
+
const artifacts = [];
|
|
151
|
+
for (const name of await fs.readdir(outputDir).catch(() => [])) {
|
|
152
|
+
const filePath = path.join(outputDir, name);
|
|
153
|
+
const stat = await fs.stat(filePath).catch(() => null);
|
|
154
|
+
if (!stat?.isFile())
|
|
155
|
+
continue;
|
|
156
|
+
artifacts.push({
|
|
157
|
+
name,
|
|
158
|
+
mimeType: getMimeType(name),
|
|
159
|
+
data: (await fs.readFile(filePath)).toString('base64'),
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
return { ...result, artifacts, executionTimeMs: Date.now() - startTime };
|
|
163
|
+
}
|
|
164
|
+
catch (error) {
|
|
165
|
+
return {
|
|
166
|
+
stdout: '',
|
|
167
|
+
stderr: error instanceof Error ? error.message : 'Local execution failed.',
|
|
168
|
+
exitCode: 1,
|
|
169
|
+
artifacts: [],
|
|
170
|
+
executionTimeMs: Date.now() - startTime,
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
finally {
|
|
174
|
+
await fs.rm(tempDir, { recursive: true, force: true }).catch(() => { });
|
|
175
|
+
}
|
|
176
|
+
}
|
package/dist/mime.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function getMimeType(filename: string): string;
|
package/dist/mime.js
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
const MIME_TYPES = {
|
|
2
|
+
png: 'image/png',
|
|
3
|
+
jpg: 'image/jpeg',
|
|
4
|
+
jpeg: 'image/jpeg',
|
|
5
|
+
svg: 'image/svg+xml',
|
|
6
|
+
gif: 'image/gif',
|
|
7
|
+
csv: 'text/csv',
|
|
8
|
+
json: 'application/json',
|
|
9
|
+
txt: 'text/plain',
|
|
10
|
+
html: 'text/html',
|
|
11
|
+
pdf: 'application/pdf',
|
|
12
|
+
};
|
|
13
|
+
export function getMimeType(filename) {
|
|
14
|
+
const ext = filename.slice(filename.lastIndexOf('.') + 1).toLowerCase();
|
|
15
|
+
return MIME_TYPES[ext] ?? 'application/octet-stream';
|
|
16
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Browserbase adapter — https://docs.browserbase.com/welcome/introduction
|
|
3
|
+
*
|
|
4
|
+
* Browserbase provisions headless-Chrome sessions for Playwright/Puppeteer
|
|
5
|
+
* automation, not an arbitrary code-execution sandbox — see
|
|
6
|
+
* SANDBOX_PROVIDERS.browserbase.executionCaveat. `execute()` fails fast with
|
|
7
|
+
* that explanation instead of pretending to run Python/JS here.
|
|
8
|
+
* `verifyCredentials`/`healthCheck` are still fully real: they confirm the
|
|
9
|
+
* API key and project actually work.
|
|
10
|
+
*/
|
|
11
|
+
import type { SandboxProviderAdapter } from '../types.js';
|
|
12
|
+
export declare const browserbaseAdapter: SandboxProviderAdapter;
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Browserbase adapter — https://docs.browserbase.com/welcome/introduction
|
|
3
|
+
*
|
|
4
|
+
* Browserbase provisions headless-Chrome sessions for Playwright/Puppeteer
|
|
5
|
+
* automation, not an arbitrary code-execution sandbox — see
|
|
6
|
+
* SANDBOX_PROVIDERS.browserbase.executionCaveat. `execute()` fails fast with
|
|
7
|
+
* that explanation instead of pretending to run Python/JS here.
|
|
8
|
+
* `verifyCredentials`/`healthCheck` are still fully real: they confirm the
|
|
9
|
+
* API key and project actually work.
|
|
10
|
+
*/
|
|
11
|
+
import { failedResult } from '../remote-exec.js';
|
|
12
|
+
import { SANDBOX_PROVIDERS } from '../registry.js';
|
|
13
|
+
async function loadSdk() {
|
|
14
|
+
return (await import('@browserbasehq/sdk'));
|
|
15
|
+
}
|
|
16
|
+
function requireFields(credentials) {
|
|
17
|
+
const apiKey = credentials.apiKey?.trim();
|
|
18
|
+
const projectId = credentials.projectId?.trim();
|
|
19
|
+
if (!apiKey || !projectId)
|
|
20
|
+
throw new Error('Browserbase API key and project ID are both required.');
|
|
21
|
+
return { apiKey, projectId };
|
|
22
|
+
}
|
|
23
|
+
async function verifyCredentials(credentials) {
|
|
24
|
+
const { apiKey, projectId } = requireFields(credentials);
|
|
25
|
+
const { default: Browserbase } = await loadSdk();
|
|
26
|
+
const client = new Browserbase({ apiKey });
|
|
27
|
+
const projects = await client.projects.list();
|
|
28
|
+
if (!projects.some((project) => project.id === projectId)) {
|
|
29
|
+
throw new Error('This API key cannot see the given Browserbase project ID.');
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
async function healthCheck(credentials) {
|
|
33
|
+
const caveat = SANDBOX_PROVIDERS.browserbase.executionCaveat;
|
|
34
|
+
if (!credentials.apiKey?.trim() || !credentials.projectId?.trim()) {
|
|
35
|
+
return {
|
|
36
|
+
status: 'missing-credentials',
|
|
37
|
+
backend: 'browserbase',
|
|
38
|
+
error: 'Browserbase API key and project ID are required.',
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
try {
|
|
42
|
+
await verifyCredentials(credentials);
|
|
43
|
+
return { status: 'unsupported', backend: 'browserbase', error: caveat };
|
|
44
|
+
}
|
|
45
|
+
catch (err) {
|
|
46
|
+
return {
|
|
47
|
+
status: 'error',
|
|
48
|
+
backend: 'browserbase',
|
|
49
|
+
error: err instanceof Error ? err.message : 'Browserbase health check failed.',
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
async function execute(_request, _credentials) {
|
|
54
|
+
return failedResult(SANDBOX_PROVIDERS.browserbase.executionCaveat, Date.now());
|
|
55
|
+
}
|
|
56
|
+
export const browserbaseAdapter = {
|
|
57
|
+
id: 'browserbase',
|
|
58
|
+
verifyCredentials,
|
|
59
|
+
healthCheck,
|
|
60
|
+
execute,
|
|
61
|
+
};
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cloudflare Sandbox adapter — https://developers.cloudflare.com/sandbox/
|
|
3
|
+
*/
|
|
4
|
+
import { failedResult } from '../remote-exec.js';
|
|
5
|
+
import { SANDBOX_PROVIDERS } from '../registry.js';
|
|
6
|
+
function requireFields(credentials) {
|
|
7
|
+
const apiToken = credentials.apiToken?.trim();
|
|
8
|
+
const accountId = credentials.accountId?.trim();
|
|
9
|
+
if (!apiToken || !accountId)
|
|
10
|
+
throw new Error('Cloudflare API token and account ID are both required.');
|
|
11
|
+
return { apiToken, accountId };
|
|
12
|
+
}
|
|
13
|
+
async function verifyCredentials(credentials) {
|
|
14
|
+
const { apiToken, accountId } = requireFields(credentials);
|
|
15
|
+
const tokenRes = await fetch('https://api.cloudflare.com/client/v4/user/tokens/verify', {
|
|
16
|
+
headers: { Authorization: `Bearer ${apiToken}` },
|
|
17
|
+
});
|
|
18
|
+
if (!tokenRes.ok)
|
|
19
|
+
throw new Error(`Cloudflare rejected the API token (${tokenRes.status}).`);
|
|
20
|
+
const accountRes = await fetch(`https://api.cloudflare.com/client/v4/accounts/${accountId}`, {
|
|
21
|
+
headers: { Authorization: `Bearer ${apiToken}` },
|
|
22
|
+
});
|
|
23
|
+
if (!accountRes.ok)
|
|
24
|
+
throw new Error('This API token cannot see the given Cloudflare account ID.');
|
|
25
|
+
}
|
|
26
|
+
async function healthCheck(credentials) {
|
|
27
|
+
const caveat = SANDBOX_PROVIDERS.cloudflare.executionCaveat;
|
|
28
|
+
if (!credentials.apiToken?.trim() || !credentials.accountId?.trim()) {
|
|
29
|
+
return {
|
|
30
|
+
status: 'missing-credentials',
|
|
31
|
+
backend: 'cloudflare',
|
|
32
|
+
error: 'Cloudflare API token and account ID are required.',
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
try {
|
|
36
|
+
await verifyCredentials(credentials);
|
|
37
|
+
return { status: 'unsupported', backend: 'cloudflare', error: caveat };
|
|
38
|
+
}
|
|
39
|
+
catch (err) {
|
|
40
|
+
return {
|
|
41
|
+
status: 'error',
|
|
42
|
+
backend: 'cloudflare',
|
|
43
|
+
error: err instanceof Error ? err.message : 'Cloudflare health check failed.',
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
async function execute(_request, _credentials) {
|
|
48
|
+
return failedResult(SANDBOX_PROVIDERS.cloudflare.executionCaveat, Date.now());
|
|
49
|
+
}
|
|
50
|
+
export const cloudflareAdapter = {
|
|
51
|
+
id: 'cloudflare',
|
|
52
|
+
verifyCredentials,
|
|
53
|
+
healthCheck,
|
|
54
|
+
execute,
|
|
55
|
+
};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Daytona adapter — https://www.daytona.io/docs/en/sandboxes/
|
|
3
|
+
*
|
|
4
|
+
* Daytona's `process.executeCommand` result doesn't document a separate
|
|
5
|
+
* stderr field (output comes back merged into `result`), so unlike the
|
|
6
|
+
* other adapters we can't cleanly split stdout/stderr — everything lands in
|
|
7
|
+
* stdout, and the combined output is mirrored into stderr only when the
|
|
8
|
+
* command actually failed (exitCode !== 0), so error output still surfaces
|
|
9
|
+
* where callers expect it.
|
|
10
|
+
*/
|
|
11
|
+
import type { SandboxProviderAdapter } from '../types.js';
|
|
12
|
+
export declare const daytonaAdapter: SandboxProviderAdapter;
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Daytona adapter — https://www.daytona.io/docs/en/sandboxes/
|
|
3
|
+
*
|
|
4
|
+
* Daytona's `process.executeCommand` result doesn't document a separate
|
|
5
|
+
* stderr field (output comes back merged into `result`), so unlike the
|
|
6
|
+
* other adapters we can't cleanly split stdout/stderr — everything lands in
|
|
7
|
+
* stdout, and the combined output is mirrored into stderr only when the
|
|
8
|
+
* command actually failed (exitCode !== 0), so error output still surfaces
|
|
9
|
+
* where callers expect it.
|
|
10
|
+
*/
|
|
11
|
+
import { DEFAULT_TIMEOUT_MS, failedResult, runScriptedExecution, safeCleanup, withTimeout, } from '../remote-exec.js';
|
|
12
|
+
async function loadSdk() {
|
|
13
|
+
return (await import('@daytonaio/sdk'));
|
|
14
|
+
}
|
|
15
|
+
function requireFields(credentials) {
|
|
16
|
+
const apiKey = credentials.apiKey?.trim();
|
|
17
|
+
if (!apiKey)
|
|
18
|
+
throw new Error('Daytona API key is required.');
|
|
19
|
+
return { apiKey, apiUrl: credentials.apiUrl?.trim() || undefined };
|
|
20
|
+
}
|
|
21
|
+
async function verifyCredentials(credentials) {
|
|
22
|
+
const fields = requireFields(credentials);
|
|
23
|
+
const { Daytona } = await loadSdk();
|
|
24
|
+
const daytona = new Daytona(fields);
|
|
25
|
+
await daytona.list().next();
|
|
26
|
+
}
|
|
27
|
+
async function healthCheck(credentials) {
|
|
28
|
+
if (!credentials.apiKey?.trim()) {
|
|
29
|
+
return {
|
|
30
|
+
status: 'missing-credentials',
|
|
31
|
+
backend: 'daytona',
|
|
32
|
+
error: 'Daytona API key is required.',
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
try {
|
|
36
|
+
await verifyCredentials(credentials);
|
|
37
|
+
return { status: 'ready', backend: 'daytona' };
|
|
38
|
+
}
|
|
39
|
+
catch (err) {
|
|
40
|
+
return {
|
|
41
|
+
status: 'error',
|
|
42
|
+
backend: 'daytona',
|
|
43
|
+
error: err instanceof Error ? err.message : 'Daytona health check failed.',
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
async function execute(request, credentials) {
|
|
48
|
+
const startTime = Date.now();
|
|
49
|
+
let fields;
|
|
50
|
+
try {
|
|
51
|
+
fields = requireFields(credentials);
|
|
52
|
+
}
|
|
53
|
+
catch (err) {
|
|
54
|
+
return failedResult(err instanceof Error ? err.message : 'Missing Daytona credentials.', startTime);
|
|
55
|
+
}
|
|
56
|
+
const timeoutMs = request.timeout ?? DEFAULT_TIMEOUT_MS;
|
|
57
|
+
let sandbox;
|
|
58
|
+
try {
|
|
59
|
+
const { Daytona } = await loadSdk();
|
|
60
|
+
const daytona = new Daytona(fields);
|
|
61
|
+
const snapshot = credentials.snapshot?.trim();
|
|
62
|
+
sandbox = await withTimeout(daytona.create(snapshot ? { snapshot } : { language: 'python' }), timeoutMs + 30_000, 'Daytona sandbox create');
|
|
63
|
+
const activeSandbox = sandbox;
|
|
64
|
+
return await withTimeout(runScriptedExecution(async (script) => {
|
|
65
|
+
const result = await activeSandbox.process.executeCommand(script, undefined, undefined, timeoutMs / 1000);
|
|
66
|
+
const exitCode = result.exitCode ?? 0;
|
|
67
|
+
const output = result.result ?? '';
|
|
68
|
+
return { stdout: output, stderr: exitCode !== 0 ? output : '', exitCode };
|
|
69
|
+
}, request), timeoutMs + 15_000, 'Daytona execution');
|
|
70
|
+
}
|
|
71
|
+
catch (err) {
|
|
72
|
+
return failedResult(err instanceof Error ? err.message : 'Daytona execution failed.', startTime);
|
|
73
|
+
}
|
|
74
|
+
finally {
|
|
75
|
+
if (sandbox)
|
|
76
|
+
await safeCleanup(() => sandbox.delete());
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
export const daytonaAdapter = {
|
|
80
|
+
id: 'daytona',
|
|
81
|
+
verifyCredentials,
|
|
82
|
+
healthCheck,
|
|
83
|
+
execute,
|
|
84
|
+
};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* E2B adapter — https://docs.e2b.dev/
|
|
3
|
+
*
|
|
4
|
+
* Uses the base `e2b` SDK (not `@e2b/code-interpreter`) so execution stays a
|
|
5
|
+
* plain "run this shell script, get stdout/stderr/exitCode" call, matching
|
|
6
|
+
* every other remote adapter and letting them share `runScriptedExecution`.
|
|
7
|
+
*/
|
|
8
|
+
import type { SandboxProviderAdapter } from '../types.js';
|
|
9
|
+
export declare const e2bAdapter: SandboxProviderAdapter;
|