@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
@@ -0,0 +1,281 @@
1
+ /**
2
+ * Declarative registry of the remote sandbox providers @larkup/sandbox can
3
+ * execute code on, besides the always-available local Docker backend.
4
+ *
5
+ * This single object powers two consumers:
6
+ * 1. The Settings UI — renders `fields` dynamically so each provider asks
7
+ * for exactly the credentials it needs, the same way vector store and
8
+ * embedding provider fields are rendered from their own registries.
9
+ * 2. `SandboxManager` / the provider adapters — read `id` to route
10
+ * execution and `executionSupport` to fail fast with a clear message
11
+ * instead of pretending every provider can run arbitrary code.
12
+ *
13
+ * Adding a new provider = one entry here + one adapter in `./providers`.
14
+ */
15
+ export const SANDBOX_PROVIDERS = {
16
+ e2b: {
17
+ id: 'e2b',
18
+ label: 'E2B',
19
+ description: 'Firecracker-microVM sandboxes purpose-built for AI code execution, with fast cold starts.',
20
+ sdkPackage: 'e2b',
21
+ docsUrl: 'https://docs.e2b.dev/',
22
+ icon: '/icons/e2b.png',
23
+ executionSupport: 'full',
24
+ fields: [
25
+ {
26
+ key: 'apiKey',
27
+ label: 'API key',
28
+ type: 'password',
29
+ required: true,
30
+ secret: true,
31
+ placeholder: 'e2b_...',
32
+ help: 'From the E2B dashboard. Equivalent to the E2B_API_KEY environment variable.',
33
+ },
34
+ {
35
+ key: 'snapshotId',
36
+ label: 'Snapshot or template ID',
37
+ type: 'text',
38
+ required: false,
39
+ help: 'Optional prebuilt environment with your required packages. New executions start from it.',
40
+ },
41
+ ],
42
+ },
43
+ vercel: {
44
+ id: 'vercel',
45
+ label: 'Vercel Sandbox',
46
+ description: 'Isolated Firecracker microVMs for running arbitrary or untrusted code, billed through your Vercel account.',
47
+ sdkPackage: '@vercel/sandbox',
48
+ docsUrl: 'https://vercel.com/docs/sandbox',
49
+ icon: '/icons/vercel.svg',
50
+ executionSupport: 'full',
51
+ fields: [
52
+ {
53
+ key: 'token',
54
+ label: 'Access token',
55
+ type: 'password',
56
+ required: true,
57
+ secret: true,
58
+ help: 'Vercel access token with Sandbox permissions. Equivalent to VERCEL_TOKEN.',
59
+ },
60
+ {
61
+ key: 'teamId',
62
+ label: 'Team ID',
63
+ type: 'text',
64
+ required: true,
65
+ placeholder: 'team_...',
66
+ help: 'Found in Vercel Team Settings. Equivalent to VERCEL_TEAM_ID.',
67
+ },
68
+ {
69
+ key: 'projectId',
70
+ label: 'Project ID',
71
+ type: 'text',
72
+ required: true,
73
+ placeholder: 'prj_...',
74
+ help: 'The Vercel project sandboxes run under. Equivalent to VERCEL_PROJECT_ID.',
75
+ },
76
+ {
77
+ key: 'snapshotId',
78
+ label: 'Snapshot ID',
79
+ type: 'text',
80
+ required: false,
81
+ help: 'Optional Vercel snapshot to restore for each execution.',
82
+ },
83
+ ],
84
+ },
85
+ modal: {
86
+ id: 'modal',
87
+ label: 'Modal',
88
+ description: 'Sandboxes for executing untrusted user or agent code in secure containers on Modal’s compute platform.',
89
+ sdkPackage: 'modal',
90
+ docsUrl: 'https://modal.com/docs/guide/sandboxes',
91
+ icon: '/icons/modal.png',
92
+ executionSupport: 'full',
93
+ fields: [
94
+ {
95
+ key: 'tokenId',
96
+ label: 'Token ID',
97
+ type: 'text',
98
+ required: true,
99
+ placeholder: 'ak-...',
100
+ help: 'From `modal setup` or the Modal dashboard. Equivalent to MODAL_TOKEN_ID.',
101
+ },
102
+ {
103
+ key: 'tokenSecret',
104
+ label: 'Token secret',
105
+ type: 'password',
106
+ required: true,
107
+ secret: true,
108
+ placeholder: 'as-...',
109
+ help: 'Equivalent to MODAL_TOKEN_SECRET.',
110
+ },
111
+ ],
112
+ },
113
+ daytona: {
114
+ id: 'daytona',
115
+ label: 'Daytona',
116
+ description: 'Elastic infrastructure for running AI-generated code in fast, isolated sandboxes.',
117
+ sdkPackage: '@daytona/sdk',
118
+ docsUrl: 'https://www.daytona.io/docs/en/sandboxes/',
119
+ icon: '/icons/daytonaio.png',
120
+ executionSupport: 'full',
121
+ fields: [
122
+ {
123
+ key: 'apiKey',
124
+ label: 'API key',
125
+ type: 'password',
126
+ required: true,
127
+ secret: true,
128
+ help: 'From the Daytona dashboard. Equivalent to DAYTONA_API_KEY.',
129
+ },
130
+ {
131
+ key: 'apiUrl',
132
+ label: 'API URL',
133
+ type: 'text',
134
+ required: false,
135
+ placeholder: 'https://app.daytona.io/api',
136
+ help: 'Only needed for a self-hosted Daytona instance.',
137
+ },
138
+ {
139
+ key: 'snapshot',
140
+ label: 'Snapshot',
141
+ type: 'text',
142
+ required: false,
143
+ help: 'Optional Daytona snapshot containing the dependencies your agent needs.',
144
+ },
145
+ ],
146
+ },
147
+ browserbase: {
148
+ id: 'browserbase',
149
+ label: 'Browserbase',
150
+ description: 'Headless-Chrome browser sessions for Playwright/Puppeteer automation.',
151
+ sdkPackage: '@browserbasehq/sdk',
152
+ docsUrl: 'https://docs.browserbase.com/welcome/introduction',
153
+ icon: '/icons/browserbase.png',
154
+ executionSupport: 'unsupported',
155
+ executionCaveat: 'Browserbase provisions remote browser sessions reachable over CDP for Playwright/Puppeteer — it does not run arbitrary Python/JS and return stdout the way a code-execution sandbox does. Verify still checks your API key and project, but code-execution tools (data analysis, corpus analytics) need a different provider.',
156
+ fields: [
157
+ {
158
+ key: 'apiKey',
159
+ label: 'API key',
160
+ type: 'password',
161
+ required: true,
162
+ secret: true,
163
+ help: 'Equivalent to BROWSERBASE_API_KEY.',
164
+ },
165
+ {
166
+ key: 'projectId',
167
+ label: 'Project ID',
168
+ type: 'text',
169
+ required: true,
170
+ help: 'Equivalent to BROWSERBASE_PROJECT_ID.',
171
+ },
172
+ ],
173
+ },
174
+ flyio: {
175
+ id: 'flyio',
176
+ label: 'Fly.io Sprites',
177
+ description: 'Persistent, fast-suspending Linux VM sandboxes for AI agents, built on Fly Machines.',
178
+ sdkPackage: '@fly/sprites',
179
+ docsUrl: 'https://fly.io/sprites/',
180
+ icon: '/icons/flyio.jpeg',
181
+ executionSupport: 'full',
182
+ fields: [
183
+ {
184
+ key: 'token',
185
+ label: 'Sprite token',
186
+ type: 'password',
187
+ required: true,
188
+ secret: true,
189
+ help: 'From `sprite org auth` or the Fly dashboard. Equivalent to SPRITE_TOKEN.',
190
+ },
191
+ ],
192
+ },
193
+ northflank: {
194
+ id: 'northflank',
195
+ label: 'Northflank',
196
+ description: 'On-demand sandbox containers running as Northflank services in your own project.',
197
+ sdkPackage: '@northflank/js-client',
198
+ docsUrl: 'https://northflank.com/docs/v1/application/sandboxes/sandboxes-on-northflank',
199
+ icon: '/icons/Northflank.png',
200
+ executionSupport: 'full',
201
+ fields: [
202
+ {
203
+ key: 'token',
204
+ label: 'API token',
205
+ type: 'password',
206
+ required: true,
207
+ secret: true,
208
+ help: 'A personal or team API token from Northflank → Account → API Tokens.',
209
+ },
210
+ {
211
+ key: 'projectId',
212
+ label: 'Project ID',
213
+ type: 'text',
214
+ required: true,
215
+ help: 'The Northflank project sandbox services are created in.',
216
+ },
217
+ ],
218
+ },
219
+ cloudflare: {
220
+ id: 'cloudflare',
221
+ label: 'Cloudflare Sandbox',
222
+ description: 'Container sandboxes running inside a Cloudflare Worker via the Durable Object Sandbox SDK.',
223
+ sdkPackage: '@cloudflare/sandbox',
224
+ docsUrl: 'https://developers.cloudflare.com/sandbox/',
225
+ icon: '/icons/cloudflare.png',
226
+ executionSupport: 'unsupported',
227
+ executionCaveat: '@cloudflare/sandbox only runs inside a Cloudflare Worker through a Durable Object binding declared in wrangler config. There is no external REST API to create or run commands in a sandbox from this server. Verify checks your account API token; actually running code here requires deploying a Worker with the Sandbox binding.',
228
+ fields: [
229
+ {
230
+ key: 'apiToken',
231
+ label: 'API token',
232
+ type: 'password',
233
+ required: true,
234
+ secret: true,
235
+ help: 'Used only to verify account access.',
236
+ },
237
+ {
238
+ key: 'accountId',
239
+ label: 'Account ID',
240
+ type: 'text',
241
+ required: true,
242
+ },
243
+ ],
244
+ },
245
+ webcontainers: {
246
+ id: 'webcontainers',
247
+ label: 'WebContainers',
248
+ description: 'In-browser WebAssembly Node.js runtime for running full-stack apps client-side.',
249
+ sdkPackage: '@webcontainer/api',
250
+ docsUrl: 'https://webcontainers.io/',
251
+ icon: '/icons/webcontainers.svg',
252
+ executionSupport: 'unsupported',
253
+ executionCaveat: 'WebContainers boots entirely inside a cross-origin-isolated browser tab (it needs SharedArrayBuffer). There is no Node.js/server-side way to run it, so this backend cannot execute code from here. Use @webcontainer/api directly in your frontend for in-browser execution instead.',
254
+ fields: [
255
+ {
256
+ key: 'apiKey',
257
+ label: 'License key (optional)',
258
+ type: 'password',
259
+ required: false,
260
+ secret: true,
261
+ help: 'StackBlitz-issued commercial key for production embeds. Leave blank for local/prototype use.',
262
+ },
263
+ ],
264
+ },
265
+ };
266
+ export const SANDBOX_PROVIDER_LIST = Object.values(SANDBOX_PROVIDERS);
267
+ export function getSandboxProvider(id) {
268
+ if (id === 'local' || id === 'docker' || id === 'custom')
269
+ return undefined;
270
+ return SANDBOX_PROVIDERS[id];
271
+ }
272
+ /** Validates credentials against a provider's registered fields, mirroring validateStoreConfig. */
273
+ export function validateSandboxCredentials(provider, values) {
274
+ const errors = {};
275
+ for (const field of provider.fields) {
276
+ if (field.required && !values[field.key]?.trim()) {
277
+ errors[field.key] = `${field.label} is required`;
278
+ }
279
+ }
280
+ return errors;
281
+ }
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Shared execution plumbing for every cloud sandbox adapter (E2B, Vercel
3
+ * Sandbox, Modal, Daytona, Fly Sprites, Northflank).
4
+ *
5
+ * Each of those providers exposes the same primitive — "run this shell
6
+ * command inside my sandbox and give me stdout/stderr/exitCode back" — but
7
+ * with a different SDK shape (commands.run, runCommand, exec, ...). Rather
8
+ * than re-implement file staging + artifact capture per provider, adapters
9
+ * plug their SDK's exec call in as a `RemoteExecFn` and hand it to
10
+ * `runScriptedExecution`, which builds one POSIX shell script that writes
11
+ * any input files, runs the code, and reports back any files written to an
12
+ * output directory (mirroring what the Docker adapter gets for free via a
13
+ * bind mount).
14
+ */
15
+ import type { ExecutionRequest, ExecutionResult } from './types.js';
16
+ /** Default per-execution timeout for remote adapters when a request doesn't specify one. */
17
+ export declare const DEFAULT_TIMEOUT_MS = 30000;
18
+ export interface RemoteCommandResult {
19
+ stdout: string;
20
+ stderr: string;
21
+ exitCode: number;
22
+ }
23
+ /** Runs a single shell command/script inside a provider's sandbox instance. */
24
+ export type RemoteExecFn = (script: string) => Promise<RemoteCommandResult>;
25
+ /** Builds the POSIX shell script a remote adapter hands to its provider's exec call. */
26
+ export declare function buildExecutionScript(request: ExecutionRequest): string;
27
+ /**
28
+ * Runs `request` via `execFn` and reassembles an `ExecutionResult`,
29
+ * including a best-effort fetch of any files the code wrote to the output
30
+ * directory (one follow-up `base64 <file>` call per artifact).
31
+ */
32
+ export declare function runScriptedExecution(execFn: RemoteExecFn, request: ExecutionRequest): Promise<ExecutionResult>;
33
+ /** Uniform safety net for providers whose SDK doesn't take its own per-call timeout. */
34
+ export declare function withTimeout<T>(promise: Promise<T>, timeoutMs: number, label: string): Promise<T>;
35
+ /**
36
+ * Runs a teardown call (kill/delete/terminate/stop) and swallows whatever it
37
+ * throws or rejects with — cleanup failures shouldn't mask the actual
38
+ * execution result. Wrapping in try/catch (rather than `.catch()`) also
39
+ * tolerates a teardown method that doesn't return a real promise.
40
+ */
41
+ export declare function safeCleanup(fn: () => Promise<unknown>): Promise<void>;
42
+ export declare function failedResult(message: string, startTime: number): ExecutionResult;
@@ -0,0 +1,144 @@
1
+ /**
2
+ * Shared execution plumbing for every cloud sandbox adapter (E2B, Vercel
3
+ * Sandbox, Modal, Daytona, Fly Sprites, Northflank).
4
+ *
5
+ * Each of those providers exposes the same primitive — "run this shell
6
+ * command inside my sandbox and give me stdout/stderr/exitCode back" — but
7
+ * with a different SDK shape (commands.run, runCommand, exec, ...). Rather
8
+ * than re-implement file staging + artifact capture per provider, adapters
9
+ * plug their SDK's exec call in as a `RemoteExecFn` and hand it to
10
+ * `runScriptedExecution`, which builds one POSIX shell script that writes
11
+ * any input files, runs the code, and reports back any files written to an
12
+ * output directory (mirroring what the Docker adapter gets for free via a
13
+ * bind mount).
14
+ */
15
+ import { getMimeType } from './mime.js';
16
+ /** Default per-execution timeout for remote adapters when a request doesn't specify one. */
17
+ export const DEFAULT_TIMEOUT_MS = 30_000;
18
+ const WORKDIR = '/tmp/larkup-sandbox';
19
+ const OUTPUT_DIR = `${WORKDIR}/output`;
20
+ const ARTIFACT_MARKER = '__LARKUP_ARTIFACTS__:';
21
+ /** Not part of the base64 alphabet, so it can never collide with an encoded file body. */
22
+ const HEREDOC_DELIMITER = 'LARKUP__EOF__';
23
+ function assertSafeFileName(name) {
24
+ if (!name ||
25
+ name === '.' ||
26
+ name === '..' ||
27
+ name.includes('/') ||
28
+ name.includes('\\') ||
29
+ name.includes('\0')) {
30
+ throw new Error('Sandbox file names must be plain file names.');
31
+ }
32
+ }
33
+ function shellSingleQuote(value) {
34
+ return `'${value.replace(/'/g, `'\\''`)}'`;
35
+ }
36
+ function writeBase64FileScript(name, base64) {
37
+ return `base64 -d > ${shellSingleQuote(name)} <<'${HEREDOC_DELIMITER}'\n${base64}\n${HEREDOC_DELIMITER}`;
38
+ }
39
+ /** Builds the POSIX shell script a remote adapter hands to its provider's exec call. */
40
+ export function buildExecutionScript(request) {
41
+ const lines = [
42
+ 'set -e',
43
+ `mkdir -p ${shellSingleQuote(OUTPUT_DIR)}`,
44
+ `cd ${shellSingleQuote(WORKDIR)}`,
45
+ ];
46
+ for (const file of request.files ?? []) {
47
+ assertSafeFileName(file.name);
48
+ const base64 = file.isBase64
49
+ ? file.content
50
+ : Buffer.from(file.content, 'utf8').toString('base64');
51
+ lines.push(writeBase64FileScript(file.name, base64));
52
+ }
53
+ const scriptName = request.language === 'python' ? 'run.py' : 'run.js';
54
+ const runner = request.language === 'python' ? 'python3' : 'node';
55
+ const codeBase64 = Buffer.from(request.code, 'utf8').toString('base64');
56
+ lines.push(writeBase64FileScript(scriptName, codeBase64));
57
+ lines.push(`${runner} ${shellSingleQuote(scriptName)}`);
58
+ lines.push(`if [ -d ${shellSingleQuote(OUTPUT_DIR)} ]; then echo "${ARTIFACT_MARKER}$(ls -1 ${shellSingleQuote(OUTPUT_DIR)} 2>/dev/null | tr '\\n' ',')"; fi`);
59
+ return lines.join('\n');
60
+ }
61
+ function extractArtifactMarker(stdout) {
62
+ const markerIndex = stdout.lastIndexOf(ARTIFACT_MARKER);
63
+ if (markerIndex === -1)
64
+ return { stdout, artifactNames: [] };
65
+ const before = stdout.slice(0, markerIndex);
66
+ const after = stdout.slice(markerIndex + ARTIFACT_MARKER.length);
67
+ const newlineIndex = after.indexOf('\n');
68
+ const namesPart = newlineIndex === -1 ? after : after.slice(0, newlineIndex);
69
+ const rest = newlineIndex === -1 ? '' : after.slice(newlineIndex + 1);
70
+ const artifactNames = namesPart
71
+ .split(',')
72
+ .map((name) => name.trim())
73
+ .filter(Boolean);
74
+ return { stdout: (before + rest).replace(/\n$/, ''), artifactNames };
75
+ }
76
+ /**
77
+ * Runs `request` via `execFn` and reassembles an `ExecutionResult`,
78
+ * including a best-effort fetch of any files the code wrote to the output
79
+ * directory (one follow-up `base64 <file>` call per artifact).
80
+ */
81
+ export async function runScriptedExecution(execFn, request) {
82
+ const startTime = Date.now();
83
+ const script = buildExecutionScript(request);
84
+ const main = await execFn(script);
85
+ const { stdout, artifactNames } = extractArtifactMarker(main.stdout);
86
+ const artifacts = [];
87
+ for (const name of artifactNames) {
88
+ if (!name || name.includes('/') || name.includes('\\') || name === '.' || name === '..')
89
+ continue;
90
+ try {
91
+ const cat = await execFn(`base64 ${shellSingleQuote(`${OUTPUT_DIR}/${name}`)}`);
92
+ const data = cat.stdout.replace(/\s+/g, '');
93
+ if (cat.exitCode === 0 && data) {
94
+ artifacts.push({ name, mimeType: getMimeType(name), data });
95
+ }
96
+ }
97
+ catch {
98
+ /* one missing/unreadable artifact shouldn't fail the whole execution */
99
+ }
100
+ }
101
+ return {
102
+ stdout,
103
+ stderr: main.stderr,
104
+ exitCode: main.exitCode,
105
+ artifacts,
106
+ executionTimeMs: Date.now() - startTime,
107
+ };
108
+ }
109
+ /** Uniform safety net for providers whose SDK doesn't take its own per-call timeout. */
110
+ export async function withTimeout(promise, timeoutMs, label) {
111
+ let timer;
112
+ const timeout = new Promise((_, reject) => {
113
+ timer = setTimeout(() => reject(new Error(`${label} timed out after ${timeoutMs}ms`)), timeoutMs);
114
+ });
115
+ try {
116
+ return await Promise.race([promise, timeout]);
117
+ }
118
+ finally {
119
+ clearTimeout(timer);
120
+ }
121
+ }
122
+ /**
123
+ * Runs a teardown call (kill/delete/terminate/stop) and swallows whatever it
124
+ * throws or rejects with — cleanup failures shouldn't mask the actual
125
+ * execution result. Wrapping in try/catch (rather than `.catch()`) also
126
+ * tolerates a teardown method that doesn't return a real promise.
127
+ */
128
+ export async function safeCleanup(fn) {
129
+ try {
130
+ await fn();
131
+ }
132
+ catch {
133
+ /* best effort */
134
+ }
135
+ }
136
+ export function failedResult(message, startTime) {
137
+ return {
138
+ stdout: '',
139
+ stderr: message,
140
+ exitCode: 1,
141
+ artifacts: [],
142
+ executionTimeMs: Date.now() - startTime,
143
+ };
144
+ }
@@ -2,8 +2,9 @@
2
2
  * Sandbox manager — high-level API for code execution.
3
3
  *
4
4
  * Provides a unified interface that routes to the appropriate backend
5
- * (Docker, E2B, etc.) based on configuration. Handles health checks,
6
- * image setup, and execution lifecycle.
5
+ * (Docker or one of the remote providers in ./providers) based on
6
+ * configuration. Handles health checks, image setup, and execution
7
+ * lifecycle.
7
8
  */
8
9
  import type { SandboxConfig, ExecutionRequest, ExecutionResult, SandboxHealthCheck } from './types.js';
9
10
  export declare class SandboxManager {
@@ -16,3 +17,5 @@ export declare class SandboxManager {
16
17
  /** Execute code in the sandbox. */
17
18
  execute(request: ExecutionRequest): Promise<ExecutionResult>;
18
19
  }
20
+ /** Verifies a remote provider's credentials — powers the Settings "Verify" button. */
21
+ export declare function verifySandboxProvider(backend: SandboxConfig['backend'], credentials: Record<string, string>): Promise<void>;
@@ -2,10 +2,13 @@
2
2
  * Sandbox manager — high-level API for code execution.
3
3
  *
4
4
  * Provides a unified interface that routes to the appropriate backend
5
- * (Docker, E2B, etc.) based on configuration. Handles health checks,
6
- * image setup, and execution lifecycle.
5
+ * (Docker or one of the remote providers in ./providers) based on
6
+ * configuration. Handles health checks, image setup, and execution
7
+ * lifecycle.
7
8
  */
8
9
  import { checkDockerHealth, buildSandboxImage, ensureImage, executeInDocker, } from './docker-runner.js';
10
+ import { checkLocalRuntime, executeLocally } from './local-runner.js';
11
+ import { getSandboxProviderAdapter } from './providers/index.js';
9
12
  const defaultDockerConfig = {
10
13
  imageName: 'larkup-sandbox:latest',
11
14
  memoryMB: 512,
@@ -17,35 +20,32 @@ export class SandboxManager {
17
20
  config;
18
21
  constructor(config) {
19
22
  this.config = {
20
- backend: config?.backend ?? 'docker',
23
+ backend: config?.backend ?? 'local',
21
24
  docker: config?.docker,
22
- remote: config?.remote,
25
+ credentials: config?.credentials,
23
26
  };
24
27
  }
25
28
  /** Check if the sandbox backend is available and ready. */
26
29
  async healthCheck() {
27
- switch (this.config.backend) {
28
- case 'docker':
29
- return checkDockerHealth();
30
- case 'e2b':
31
- case 'modal':
32
- case 'custom':
33
- return {
34
- status: 'error',
35
- backend: this.config.backend,
36
- error: `${this.config.backend} backend is not yet implemented. Use Docker for local execution.`,
37
- };
38
- default:
39
- return {
40
- status: 'error',
41
- backend: this.config.backend,
42
- error: `Unknown backend: ${this.config.backend}`,
43
- };
30
+ if (this.config.backend === 'local')
31
+ return checkLocalRuntime();
32
+ if (this.config.backend === 'docker')
33
+ return checkDockerHealth();
34
+ const adapter = getSandboxProviderAdapter(this.config.backend);
35
+ if (!adapter) {
36
+ return {
37
+ status: 'error',
38
+ backend: this.config.backend,
39
+ error: `Unknown or unsupported backend: ${this.config.backend}`,
40
+ };
44
41
  }
42
+ return adapter.healthCheck(this.config.credentials ?? {});
45
43
  }
46
44
  /** Set up the sandbox backend (e.g., build Docker image). */
47
45
  async setup(onProgress) {
48
46
  switch (this.config.backend) {
47
+ case 'local':
48
+ return;
49
49
  case 'docker':
50
50
  await buildSandboxImage(onProgress);
51
51
  break;
@@ -55,43 +55,64 @@ export class SandboxManager {
55
55
  }
56
56
  /** Execute code in the sandbox. */
57
57
  async execute(request) {
58
- switch (this.config.backend) {
59
- case 'docker': {
60
- // Ensure image is available
61
- const imageReady = await ensureImage();
62
- if (!imageReady) {
63
- return {
64
- stdout: '',
65
- stderr: 'Sandbox image not available. Run setup first or ensure Docker is installed.',
66
- exitCode: 1,
67
- artifacts: [],
68
- executionTimeMs: 0,
69
- };
70
- }
71
- const dockerConfig = {
72
- ...defaultDockerConfig,
73
- ...this.config.docker,
74
- };
75
- return executeInDocker(request, dockerConfig);
76
- }
77
- case 'e2b':
78
- case 'modal':
79
- case 'custom':
58
+ if (this.config.backend === 'local')
59
+ return executeLocally(request);
60
+ if (this.config.backend === 'docker') {
61
+ const health = await checkDockerHealth();
62
+ if (health.status === 'docker-not-found' || health.status === 'error') {
80
63
  return {
81
64
  stdout: '',
82
- stderr: `${this.config.backend} backend is not yet implemented.`,
65
+ stderr: 'Local code execution is unavailable because Docker is not running. Start Docker, choose a configured remote sandbox provider, or continue without code execution.',
83
66
  exitCode: 1,
84
67
  artifacts: [],
85
68
  executionTimeMs: 0,
86
69
  };
87
- default:
70
+ }
71
+ const imageReady = health.imageReady || (await ensureImage().catch(() => false));
72
+ if (!imageReady) {
88
73
  return {
89
74
  stdout: '',
90
- stderr: `Unknown backend: ${this.config.backend}`,
75
+ stderr: 'Local code execution is not ready. Build the Larkup sandbox image, choose a configured remote sandbox provider, or continue without code execution.',
91
76
  exitCode: 1,
92
77
  artifacts: [],
93
78
  executionTimeMs: 0,
94
79
  };
80
+ }
81
+ const dockerConfig = {
82
+ ...defaultDockerConfig,
83
+ ...this.config.docker,
84
+ };
85
+ return executeInDocker(request, dockerConfig);
95
86
  }
87
+ const adapter = getSandboxProviderAdapter(this.config.backend);
88
+ if (!adapter) {
89
+ return {
90
+ stdout: '',
91
+ stderr: `Unknown or unsupported backend: ${this.config.backend}`,
92
+ exitCode: 1,
93
+ artifacts: [],
94
+ executionTimeMs: 0,
95
+ };
96
+ }
97
+ return adapter.execute(request, this.config.credentials ?? {});
98
+ }
99
+ }
100
+ /** Verifies a remote provider's credentials — powers the Settings "Verify" button. */
101
+ export async function verifySandboxProvider(backend, credentials) {
102
+ if (backend === 'local') {
103
+ const health = await checkLocalRuntime();
104
+ if (health.status !== 'ready')
105
+ throw new Error(health.error ?? 'Local runtime is not available.');
106
+ return;
107
+ }
108
+ if (backend === 'docker') {
109
+ const health = await checkDockerHealth();
110
+ if (health.status !== 'ready')
111
+ throw new Error(health.error ?? 'Docker is not available.');
112
+ return;
96
113
  }
114
+ const adapter = getSandboxProviderAdapter(backend);
115
+ if (!adapter)
116
+ throw new Error(`Unknown or unsupported backend: ${backend}`);
117
+ await adapter.verifyCredentials(credentials);
97
118
  }