@larkup/sandbox 0.1.1 → 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 -4
- package/CHANGELOG.md +12 -0
- package/dist/docker-runner.js +35 -21
- 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 +23 -6
- package/src/docker-runner.ts +39 -27
- 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
|
@@ -1,6 +1,4 @@
|
|
|
1
|
-
(node:18200) Warning: The 'NO_COLOR' env is ignored due to the 'FORCE_COLOR' env being set.
|
|
2
|
-
(Use `node --trace-warnings ...` to show where the warning was created)
|
|
3
1
|
|
|
4
|
-
> @larkup/sandbox@0.1.
|
|
5
|
-
> tsc --outDir dist
|
|
2
|
+
> @larkup/sandbox@0.1.3 build /home/runner/work/larkup/larkup/packages/sandbox
|
|
3
|
+
> tsc -p tsconfig.build.json --outDir dist
|
|
6
4
|
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# @larkup/sandbox
|
|
2
2
|
|
|
3
|
+
## 0.1.3
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- Add a Docker-free local runtime for trusted Python and JavaScript analysis, alongside hardened provider verification and reusable remote snapshots.
|
|
8
|
+
|
|
9
|
+
## 0.1.2
|
|
10
|
+
|
|
11
|
+
### Patch Changes
|
|
12
|
+
|
|
13
|
+
- fix: lazy-import dockerode to prevent Turbopack hash mangling at runtime
|
|
14
|
+
|
|
3
15
|
## 0.1.1
|
|
4
16
|
|
|
5
17
|
### Patch Changes
|
package/dist/docker-runner.js
CHANGED
|
@@ -5,7 +5,6 @@
|
|
|
5
5
|
* Each execution spins up a container, runs the code, captures output +
|
|
6
6
|
* generated files, then auto-removes the container.
|
|
7
7
|
*/
|
|
8
|
-
import Docker from 'dockerode';
|
|
9
8
|
import { promises as fs } from 'node:fs';
|
|
10
9
|
import path from 'node:path';
|
|
11
10
|
import os from 'node:os';
|
|
@@ -15,17 +14,17 @@ const OUTPUT_DIR = '/sandbox/output';
|
|
|
15
14
|
const INPUT_DIR = '/sandbox/input';
|
|
16
15
|
const DOCKERFILE_DIR = path.join(import.meta.dirname ?? __dirname, '..');
|
|
17
16
|
let dockerInstance = null;
|
|
18
|
-
function getDocker() {
|
|
17
|
+
async function getDocker() {
|
|
19
18
|
if (!dockerInstance) {
|
|
19
|
+
// Dynamic import prevents Turbopack from statically bundling dockerode
|
|
20
|
+
// and mangling its module name with a hash suffix.
|
|
21
|
+
const { default: Docker } = await import(/* turbopackIgnore: true */ 'dockerode');
|
|
20
22
|
dockerInstance = new Docker();
|
|
21
23
|
}
|
|
22
24
|
return dockerInstance;
|
|
23
25
|
}
|
|
24
|
-
/* ------------------------------------------------------------------ */
|
|
25
|
-
/* Health check */
|
|
26
|
-
/* ------------------------------------------------------------------ */
|
|
27
26
|
export async function checkDockerHealth() {
|
|
28
|
-
const docker = getDocker();
|
|
27
|
+
const docker = await getDocker();
|
|
29
28
|
try {
|
|
30
29
|
const info = await docker.version();
|
|
31
30
|
const images = await docker.listImages({
|
|
@@ -39,18 +38,19 @@ export async function checkDockerHealth() {
|
|
|
39
38
|
};
|
|
40
39
|
}
|
|
41
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);
|
|
42
43
|
return {
|
|
43
44
|
status: 'docker-not-found',
|
|
44
45
|
backend: 'docker',
|
|
45
|
-
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',
|
|
46
49
|
};
|
|
47
50
|
}
|
|
48
51
|
}
|
|
49
|
-
/* ------------------------------------------------------------------ */
|
|
50
|
-
/* Image management */
|
|
51
|
-
/* ------------------------------------------------------------------ */
|
|
52
52
|
export async function buildSandboxImage(onProgress) {
|
|
53
|
-
const docker = getDocker();
|
|
53
|
+
const docker = await getDocker();
|
|
54
54
|
const dockerfilePath = path.join(DOCKERFILE_DIR, 'Dockerfile');
|
|
55
55
|
// Check if Dockerfile exists in the package
|
|
56
56
|
try {
|
|
@@ -78,7 +78,7 @@ export async function buildSandboxImage(onProgress) {
|
|
|
78
78
|
onProgress?.('Sandbox image built successfully ✓');
|
|
79
79
|
}
|
|
80
80
|
export async function ensureImage() {
|
|
81
|
-
const docker = getDocker();
|
|
81
|
+
const docker = await getDocker();
|
|
82
82
|
const images = await docker.listImages({
|
|
83
83
|
filters: { reference: [SANDBOX_IMAGE] },
|
|
84
84
|
});
|
|
@@ -92,9 +92,6 @@ export async function ensureImage() {
|
|
|
92
92
|
return false;
|
|
93
93
|
}
|
|
94
94
|
}
|
|
95
|
-
/* ------------------------------------------------------------------ */
|
|
96
|
-
/* Code execution */
|
|
97
|
-
/* ------------------------------------------------------------------ */
|
|
98
95
|
/**
|
|
99
96
|
* Build the Python wrapper script that:
|
|
100
97
|
* 1. Sets up pandas compatibility shims to handle deprecated APIs
|
|
@@ -247,7 +244,7 @@ if artifacts:
|
|
|
247
244
|
`;
|
|
248
245
|
}
|
|
249
246
|
export async function executeInDocker(request, config) {
|
|
250
|
-
const docker = getDocker();
|
|
247
|
+
const docker = await getDocker();
|
|
251
248
|
const startTime = Date.now();
|
|
252
249
|
// Create a temporary directory for input files
|
|
253
250
|
const tmpDir = path.join(os.tmpdir(), `larkup-sandbox-${randomUUID()}`);
|
|
@@ -258,6 +255,20 @@ export async function executeInDocker(request, config) {
|
|
|
258
255
|
// Write input files
|
|
259
256
|
if (request.files) {
|
|
260
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
|
+
}
|
|
261
272
|
const filePath = path.join(inputDir, file.name);
|
|
262
273
|
if (file.isBase64) {
|
|
263
274
|
await fs.writeFile(filePath, Buffer.from(file.content, 'base64'));
|
|
@@ -270,16 +281,19 @@ export async function executeInDocker(request, config) {
|
|
|
270
281
|
// Prepare the execution code
|
|
271
282
|
let execCode;
|
|
272
283
|
let cmd;
|
|
284
|
+
let scriptName;
|
|
273
285
|
if (request.language === 'python') {
|
|
274
286
|
execCode = wrapPythonCode(request.code);
|
|
275
287
|
const scriptPath = path.join(tmpDir, 'run.py');
|
|
276
288
|
await fs.writeFile(scriptPath, execCode, 'utf8');
|
|
289
|
+
scriptName = 'run.py';
|
|
277
290
|
cmd = ['python3', '/sandbox/run.py'];
|
|
278
291
|
}
|
|
279
292
|
else {
|
|
280
293
|
// JavaScript/TypeScript (future)
|
|
281
294
|
const scriptPath = path.join(tmpDir, 'run.js');
|
|
282
295
|
await fs.writeFile(scriptPath, request.code, 'utf8');
|
|
296
|
+
scriptName = 'run.js';
|
|
283
297
|
cmd = ['node', '/sandbox/run.js'];
|
|
284
298
|
}
|
|
285
299
|
const timeout = request.timeout ?? config.timeoutMs;
|
|
@@ -298,7 +312,7 @@ export async function executeInDocker(request, config) {
|
|
|
298
312
|
Binds: [
|
|
299
313
|
`${inputDir}:${INPUT_DIR}:ro`,
|
|
300
314
|
`${outputDir}:${OUTPUT_DIR}:rw`,
|
|
301
|
-
`${tmpDir}
|
|
315
|
+
`${tmpDir}/${scriptName}:/sandbox/${scriptName}:ro`,
|
|
302
316
|
],
|
|
303
317
|
ReadonlyRootfs: false,
|
|
304
318
|
},
|
|
@@ -309,8 +323,9 @@ export async function executeInDocker(request, config) {
|
|
|
309
323
|
await container.start();
|
|
310
324
|
// Wait for completion with timeout
|
|
311
325
|
const waitPromise = container.wait();
|
|
326
|
+
let timeoutHandle;
|
|
312
327
|
const timeoutPromise = new Promise((_, reject) => {
|
|
313
|
-
setTimeout(async () => {
|
|
328
|
+
timeoutHandle = setTimeout(async () => {
|
|
314
329
|
try {
|
|
315
330
|
await container?.kill();
|
|
316
331
|
}
|
|
@@ -321,6 +336,8 @@ export async function executeInDocker(request, config) {
|
|
|
321
336
|
}, timeout);
|
|
322
337
|
});
|
|
323
338
|
const result = await Promise.race([waitPromise, timeoutPromise]);
|
|
339
|
+
if (timeoutHandle)
|
|
340
|
+
clearTimeout(timeoutHandle);
|
|
324
341
|
// Capture logs
|
|
325
342
|
const logs = await container.logs({
|
|
326
343
|
stdout: true,
|
|
@@ -384,9 +401,6 @@ export async function executeInDocker(request, config) {
|
|
|
384
401
|
}
|
|
385
402
|
}
|
|
386
403
|
}
|
|
387
|
-
/* ------------------------------------------------------------------ */
|
|
388
|
-
/* Helpers */
|
|
389
|
-
/* ------------------------------------------------------------------ */
|
|
390
404
|
/**
|
|
391
405
|
* Docker multiplexes stdout/stderr into a single stream with 8-byte headers.
|
|
392
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;
|