@larkup/sandbox 0.1.2 → 0.1.4

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.
Files changed (73) hide show
  1. package/.turbo/turbo-build.log +2 -2
  2. package/.turbo/turbo-test.log +26 -0
  3. package/.turbo/turbo-type-check.log +4 -0
  4. package/CHANGELOG.md +14 -0
  5. package/dist/docker-runner.js +27 -15
  6. package/dist/index.d.ts +4 -2
  7. package/dist/index.js +3 -1
  8. package/dist/local-runner.d.ts +5 -0
  9. package/dist/local-runner.js +176 -0
  10. package/dist/mime.d.ts +1 -0
  11. package/dist/mime.js +16 -0
  12. package/dist/providers/browserbase.d.ts +12 -0
  13. package/dist/providers/browserbase.js +61 -0
  14. package/dist/providers/cloudflare.d.ts +5 -0
  15. package/dist/providers/cloudflare.js +55 -0
  16. package/dist/providers/daytona.d.ts +12 -0
  17. package/dist/providers/daytona.js +84 -0
  18. package/dist/providers/e2b.d.ts +9 -0
  19. package/dist/providers/e2b.js +70 -0
  20. package/dist/providers/flyio.d.ts +9 -0
  21. package/dist/providers/flyio.js +98 -0
  22. package/dist/providers/index.d.ts +13 -0
  23. package/dist/providers/index.js +26 -0
  24. package/dist/providers/modal.d.ts +8 -0
  25. package/dist/providers/modal.js +96 -0
  26. package/dist/providers/northflank.d.ts +9 -0
  27. package/dist/providers/northflank.js +122 -0
  28. package/dist/providers/vercel.d.ts +9 -0
  29. package/dist/providers/vercel.js +79 -0
  30. package/dist/providers/webcontainers.d.ts +13 -0
  31. package/dist/providers/webcontainers.js +28 -0
  32. package/dist/registry.d.ts +20 -0
  33. package/dist/registry.js +281 -0
  34. package/dist/remote-exec.d.ts +42 -0
  35. package/dist/remote-exec.js +144 -0
  36. package/dist/sandbox-manager.d.ts +5 -2
  37. package/dist/sandbox-manager.js +67 -46
  38. package/dist/types.d.ts +58 -9
  39. package/dist/types.js +1 -1
  40. package/package.json +21 -4
  41. package/src/docker-runner.ts +29 -19
  42. package/src/index.ts +11 -2
  43. package/src/local-runner.test.ts +22 -0
  44. package/src/local-runner.ts +199 -0
  45. package/src/mime.ts +17 -0
  46. package/src/providers/browserbase.test.ts +55 -0
  47. package/src/providers/browserbase.ts +88 -0
  48. package/src/providers/cloudflare.test.ts +63 -0
  49. package/src/providers/cloudflare.ts +72 -0
  50. package/src/providers/daytona.test.ts +71 -0
  51. package/src/providers/daytona.ts +144 -0
  52. package/src/providers/e2b.test.ts +100 -0
  53. package/src/providers/e2b.ts +115 -0
  54. package/src/providers/flyio.test.ts +69 -0
  55. package/src/providers/flyio.ts +148 -0
  56. package/src/providers/index.ts +42 -0
  57. package/src/providers/modal.test.ts +107 -0
  58. package/src/providers/modal.ts +167 -0
  59. package/src/providers/northflank.test.ts +110 -0
  60. package/src/providers/northflank.ts +199 -0
  61. package/src/providers/vercel.test.ts +84 -0
  62. package/src/providers/vercel.ts +143 -0
  63. package/src/providers/webcontainers.test.ts +21 -0
  64. package/src/providers/webcontainers.ts +36 -0
  65. package/src/registry.test.ts +84 -0
  66. package/src/registry.ts +308 -0
  67. package/src/remote-exec.test.ts +155 -0
  68. package/src/remote-exec.ts +190 -0
  69. package/src/sandbox-manager.test.ts +74 -0
  70. package/src/sandbox-manager.ts +71 -51
  71. package/src/types.ts +81 -23
  72. package/tsconfig.build.json +4 -0
  73. package/vitest.config.ts +9 -0
@@ -1,4 +1,4 @@
1
1
 
2
- > @larkup/sandbox@0.1.1 build /Users/abdelrahmanabounida/Desktop/my-ai-startup/larkup/larkup/packages/sandbox
3
- > tsc --outDir dist
2
+ > @larkup/sandbox@0.1.4 build /Users/abdelrahmanabounida/Desktop/my-ai-startup/larkup/larkup/packages/sandbox
3
+ > tsc -p tsconfig.build.json --outDir dist
4
4
 
@@ -0,0 +1,26 @@
1
+
2
+ > @larkup/sandbox@0.1.4 test /Users/abdelrahmanabounida/Desktop/my-ai-startup/larkup/larkup/packages/sandbox
3
+ > vitest run
4
+
5
+
6
+ RUN v4.1.11 /Users/abdelrahmanabounida/Desktop/my-ai-startup/larkup/larkup/packages/sandbox
7
+
8
+ ✓ src/registry.test.ts (9 tests) 3ms
9
+ ✓ src/providers/modal.test.ts (4 tests) 10ms
10
+ ✓ src/providers/e2b.test.ts (9 tests) 5ms
11
+ ✓ src/providers/browserbase.test.ts (5 tests) 4ms
12
+ ✓ src/remote-exec.test.ts (14 tests) 12ms
13
+ ✓ src/providers/northflank.test.ts (7 tests) 26ms
14
+ ✓ src/providers/flyio.test.ts (6 tests) 4ms
15
+ ✓ src/providers/daytona.test.ts (5 tests) 9ms
16
+ ✓ src/local-runner.test.ts (1 test) 191ms
17
+ ✓ src/providers/webcontainers.test.ts (3 tests) 5ms
18
+ ✓ src/providers/vercel.test.ts (6 tests) 35ms
19
+ ✓ src/sandbox-manager.test.ts (2 tests) 5ms
20
+ ✓ src/providers/cloudflare.test.ts (6 tests) 9ms
21
+
22
+ Test Files 13 passed (13)
23
+ Tests 77 passed (77)
24
+ Start at 02:57:15
25
+ Duration 1.60s (transform 2.39s, setup 0ms, import 4.20s, tests 320ms, environment 2ms)
26
+
@@ -0,0 +1,4 @@
1
+
2
+ > @larkup/sandbox@0.1.4 type-check /Users/abdelrahmanabounida/Desktop/my-ai-startup/larkup/larkup/packages/sandbox
3
+ > tsc --noEmit
4
+
package/CHANGELOG.md CHANGED
@@ -1,5 +1,19 @@
1
1
  # @larkup/sandbox
2
2
 
3
+ ## 0.1.4
4
+
5
+ ### Patch Changes
6
+
7
+ - 847b502: Keep global installs and updates quiet and compatible by aligning Apache Arrow with LanceDB's
8
+ supported peer range, shipping the marketplace TypeScript runtime loader, and using the renamed
9
+ Daytona SDK package. Public tarballs also scrub local project state from all traced workspaces.
10
+
11
+ ## 0.1.3
12
+
13
+ ### Patch Changes
14
+
15
+ - Add a Docker-free local runtime for trusted Python and JavaScript analysis, alongside hardened provider verification and reusable remote snapshots.
16
+
3
17
  ## 0.1.2
4
18
 
5
19
  ### Patch Changes
@@ -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: err.message ?? 'Docker daemon not available',
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}/run.py:/sandbox/run.py:ro`,
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 type { SandboxBackend, SandboxConfig, DockerConfig, RemoteConfig, SandboxLanguage, SandboxFile, ExecutionRequest, ExecutionResult, ExecutionArtifact, SandboxStatus, SandboxHealthCheck, } from './types.js';
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,5 @@
1
+ /**
2
+ * Cloudflare Sandbox adapter — https://developers.cloudflare.com/sandbox/
3
+ */
4
+ import type { SandboxProviderAdapter } from '../types.js';
5
+ export declare const cloudflareAdapter: SandboxProviderAdapter;
@@ -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('@daytona/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;