@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,74 @@
1
+ import { beforeEach, describe, expect, it, vi } from 'vitest';
2
+
3
+ const dockerRunner = vi.hoisted(() => ({
4
+ checkDockerHealth: vi.fn(),
5
+ ensureImage: vi.fn(),
6
+ executeInDocker: vi.fn(),
7
+ }));
8
+ const localRunner = vi.hoisted(() => ({
9
+ checkLocalRuntime: vi.fn(),
10
+ executeLocally: vi.fn(),
11
+ }));
12
+
13
+ vi.mock('./docker-runner.js', () => ({
14
+ checkDockerHealth: dockerRunner.checkDockerHealth,
15
+ ensureImage: dockerRunner.ensureImage,
16
+ executeInDocker: dockerRunner.executeInDocker,
17
+ buildSandboxImage: vi.fn(),
18
+ }));
19
+
20
+ vi.mock('./providers/index.js', () => ({
21
+ getSandboxProviderAdapter: vi.fn(),
22
+ }));
23
+ vi.mock('./local-runner.js', () => ({
24
+ checkLocalRuntime: localRunner.checkLocalRuntime,
25
+ executeLocally: localRunner.executeLocally,
26
+ }));
27
+
28
+ import { SandboxManager } from './sandbox-manager.js';
29
+
30
+ describe('SandboxManager', () => {
31
+ beforeEach(() => {
32
+ vi.clearAllMocks();
33
+ });
34
+
35
+ it('returns a safe recovery message when Docker is unavailable', async () => {
36
+ dockerRunner.checkDockerHealth.mockResolvedValue({
37
+ status: 'docker-not-found',
38
+ backend: 'docker',
39
+ error: 'connect ENOENT /var/run/docker.sock',
40
+ });
41
+
42
+ const result = await new SandboxManager({ backend: 'docker' }).execute({
43
+ code: 'print(1)',
44
+ language: 'python',
45
+ });
46
+
47
+ expect(result).toMatchObject({
48
+ exitCode: 1,
49
+ stdout: '',
50
+ artifacts: [],
51
+ executionTimeMs: 0,
52
+ });
53
+ expect(result.stderr).toContain('Docker is not running');
54
+ expect(result.stderr).not.toContain('/var/run/docker.sock');
55
+ expect(dockerRunner.ensureImage).not.toHaveBeenCalled();
56
+ expect(dockerRunner.executeInDocker).not.toHaveBeenCalled();
57
+ });
58
+
59
+ it('uses the local runtime by default without contacting Docker', async () => {
60
+ localRunner.executeLocally.mockResolvedValue({
61
+ stdout: '1\n',
62
+ stderr: '',
63
+ exitCode: 0,
64
+ artifacts: [],
65
+ executionTimeMs: 1,
66
+ });
67
+
68
+ const result = await new SandboxManager().execute({ code: 'print(1)', language: 'python' });
69
+
70
+ expect(result.exitCode).toBe(0);
71
+ expect(localRunner.executeLocally).toHaveBeenCalledOnce();
72
+ expect(dockerRunner.checkDockerHealth).not.toHaveBeenCalled();
73
+ });
74
+ });
@@ -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
 
9
10
  import type {
@@ -12,7 +13,6 @@ import type {
12
13
  ExecutionResult,
13
14
  SandboxHealthCheck,
14
15
  DockerConfig,
15
- DEFAULT_DOCKER_CONFIG,
16
16
  } from './types.js';
17
17
  import {
18
18
  checkDockerHealth,
@@ -20,6 +20,8 @@ import {
20
20
  ensureImage,
21
21
  executeInDocker,
22
22
  } from './docker-runner.js';
23
+ import { checkLocalRuntime, executeLocally } from './local-runner.js';
24
+ import { getSandboxProviderAdapter } from './providers/index.js';
23
25
 
24
26
  const defaultDockerConfig: DockerConfig = {
25
27
  imageName: 'larkup-sandbox:latest',
@@ -34,39 +36,33 @@ export class SandboxManager {
34
36
 
35
37
  constructor(config?: Partial<SandboxConfig>) {
36
38
  this.config = {
37
- backend: config?.backend ?? 'docker',
39
+ backend: config?.backend ?? 'local',
38
40
  docker: config?.docker,
39
- remote: config?.remote,
41
+ credentials: config?.credentials,
40
42
  };
41
43
  }
42
44
 
43
45
  /** Check if the sandbox backend is available and ready. */
44
46
  async healthCheck(): Promise<SandboxHealthCheck> {
45
- switch (this.config.backend) {
46
- case 'docker':
47
- return checkDockerHealth();
47
+ if (this.config.backend === 'local') return checkLocalRuntime();
48
+ if (this.config.backend === 'docker') return checkDockerHealth();
48
49
 
49
- case 'e2b':
50
- case 'modal':
51
- case 'custom':
52
- return {
53
- status: 'error',
54
- backend: this.config.backend,
55
- error: `${this.config.backend} backend is not yet implemented. Use Docker for local execution.`,
56
- };
57
-
58
- default:
59
- return {
60
- status: 'error',
61
- backend: this.config.backend,
62
- error: `Unknown backend: ${this.config.backend}`,
63
- };
50
+ const adapter = getSandboxProviderAdapter(this.config.backend);
51
+ if (!adapter) {
52
+ return {
53
+ status: 'error',
54
+ backend: this.config.backend,
55
+ error: `Unknown or unsupported backend: ${this.config.backend}`,
56
+ };
64
57
  }
58
+ return adapter.healthCheck(this.config.credentials ?? {});
65
59
  }
66
60
 
67
61
  /** Set up the sandbox backend (e.g., build Docker image). */
68
62
  async setup(onProgress?: (msg: string) => void): Promise<void> {
69
63
  switch (this.config.backend) {
64
+ case 'local':
65
+ return;
70
66
  case 'docker':
71
67
  await buildSandboxImage(onProgress);
72
68
  break;
@@ -78,47 +74,71 @@ export class SandboxManager {
78
74
 
79
75
  /** Execute code in the sandbox. */
80
76
  async execute(request: ExecutionRequest): Promise<ExecutionResult> {
81
- switch (this.config.backend) {
82
- case 'docker': {
83
- // Ensure image is available
84
- const imageReady = await ensureImage();
85
- if (!imageReady) {
86
- return {
87
- stdout: '',
88
- stderr: 'Sandbox image not available. Run setup first or ensure Docker is installed.',
89
- exitCode: 1,
90
- artifacts: [],
91
- executionTimeMs: 0,
92
- };
93
- }
94
-
95
- const dockerConfig: DockerConfig = {
96
- ...defaultDockerConfig,
97
- ...this.config.docker,
98
- };
99
-
100
- return executeInDocker(request, dockerConfig);
101
- }
102
-
103
- case 'e2b':
104
- case 'modal':
105
- case 'custom':
77
+ if (this.config.backend === 'local') return executeLocally(request);
78
+ if (this.config.backend === 'docker') {
79
+ const health = await checkDockerHealth();
80
+ if (health.status === 'docker-not-found' || health.status === 'error') {
106
81
  return {
107
82
  stdout: '',
108
- stderr: `${this.config.backend} backend is not yet implemented.`,
83
+ stderr:
84
+ 'Local code execution is unavailable because Docker is not running. Start Docker, choose a configured remote sandbox provider, or continue without code execution.',
109
85
  exitCode: 1,
110
86
  artifacts: [],
111
87
  executionTimeMs: 0,
112
88
  };
89
+ }
113
90
 
114
- default:
91
+ const imageReady = health.imageReady || (await ensureImage().catch(() => false));
92
+ if (!imageReady) {
115
93
  return {
116
94
  stdout: '',
117
- stderr: `Unknown backend: ${this.config.backend}`,
95
+ stderr:
96
+ 'Local code execution is not ready. Build the Larkup sandbox image, choose a configured remote sandbox provider, or continue without code execution.',
118
97
  exitCode: 1,
119
98
  artifacts: [],
120
99
  executionTimeMs: 0,
121
100
  };
101
+ }
102
+
103
+ const dockerConfig: DockerConfig = {
104
+ ...defaultDockerConfig,
105
+ ...this.config.docker,
106
+ };
107
+
108
+ return executeInDocker(request, dockerConfig);
122
109
  }
110
+
111
+ const adapter = getSandboxProviderAdapter(this.config.backend);
112
+ if (!adapter) {
113
+ return {
114
+ stdout: '',
115
+ stderr: `Unknown or unsupported backend: ${this.config.backend}`,
116
+ exitCode: 1,
117
+ artifacts: [],
118
+ executionTimeMs: 0,
119
+ };
120
+ }
121
+ return adapter.execute(request, this.config.credentials ?? {});
122
+ }
123
+ }
124
+
125
+ /** Verifies a remote provider's credentials — powers the Settings "Verify" button. */
126
+ export async function verifySandboxProvider(
127
+ backend: SandboxConfig['backend'],
128
+ credentials: Record<string, string>,
129
+ ): Promise<void> {
130
+ if (backend === 'local') {
131
+ const health = await checkLocalRuntime();
132
+ if (health.status !== 'ready')
133
+ throw new Error(health.error ?? 'Local runtime is not available.');
134
+ return;
135
+ }
136
+ if (backend === 'docker') {
137
+ const health = await checkDockerHealth();
138
+ if (health.status !== 'ready') throw new Error(health.error ?? 'Docker is not available.');
139
+ return;
123
140
  }
141
+ const adapter = getSandboxProviderAdapter(backend);
142
+ if (!adapter) throw new Error(`Unknown or unsupported backend: ${backend}`);
143
+ await adapter.verifyCredentials(credentials);
124
144
  }
package/src/types.ts CHANGED
@@ -1,4 +1,16 @@
1
- export type SandboxBackend = "docker" | "e2b" | "modal" | "custom";
1
+ export type SandboxBackend =
2
+ | 'local'
3
+ | 'docker'
4
+ | 'e2b'
5
+ | 'vercel'
6
+ | 'modal'
7
+ | 'daytona'
8
+ | 'browserbase'
9
+ | 'flyio'
10
+ | 'northflank'
11
+ | 'cloudflare'
12
+ | 'webcontainers'
13
+ | 'custom';
2
14
 
3
15
  export interface DockerConfig {
4
16
  /** Docker image to use (default: "larkup-sandbox:latest") */
@@ -13,31 +25,26 @@ export interface DockerConfig {
13
25
  networkDisabled: boolean;
14
26
  }
15
27
 
16
- export interface RemoteConfig {
17
- provider: "e2b" | "modal" | "custom";
18
- apiKey: string;
19
- endpoint?: string;
20
- }
21
-
22
28
  export interface SandboxConfig {
23
29
  backend: SandboxBackend;
24
30
  docker?: Partial<DockerConfig>;
25
- remote?: RemoteConfig;
31
+ /**
32
+ * Credentials for the selected remote provider, keyed by the field `key`s
33
+ * declared in that provider's `SandboxProviderDescriptor` (see registry.ts).
34
+ * Unused by local and Docker backends.
35
+ */
36
+ credentials?: Record<string, string>;
26
37
  }
27
38
 
28
39
  export const DEFAULT_DOCKER_CONFIG: DockerConfig = {
29
- imageName: "larkup-sandbox:latest",
40
+ imageName: 'larkup-sandbox:latest',
30
41
  memoryMB: 512,
31
42
  cpuShares: 1024,
32
43
  timeoutMs: 30_000,
33
44
  networkDisabled: true,
34
45
  };
35
46
 
36
- /* ------------------------------------------------------------------ */
37
- /* Execution request / result */
38
- /* ------------------------------------------------------------------ */
39
-
40
- export type SandboxLanguage = "python" | "javascript" | "typescript";
47
+ export type SandboxLanguage = 'python' | 'javascript' | 'typescript';
41
48
 
42
49
  export interface SandboxFile {
43
50
  /** Filename inside the sandbox (e.g., "data.csv") */
@@ -81,16 +88,14 @@ export interface ExecutionResult {
81
88
  executionTimeMs: number;
82
89
  }
83
90
 
84
- /* ------------------------------------------------------------------ */
85
- /* Sandbox status */
86
- /* ------------------------------------------------------------------ */
87
-
88
91
  export type SandboxStatus =
89
- | "ready"
90
- | "docker-not-found"
91
- | "image-not-found"
92
- | "building-image"
93
- | "error";
92
+ | 'ready'
93
+ | 'docker-not-found'
94
+ | 'image-not-found'
95
+ | 'building-image'
96
+ | 'missing-credentials'
97
+ | 'unsupported'
98
+ | 'error';
94
99
 
95
100
  export interface SandboxHealthCheck {
96
101
  status: SandboxStatus;
@@ -99,3 +104,56 @@ export interface SandboxHealthCheck {
99
104
  imageReady?: boolean;
100
105
  error?: string;
101
106
  }
107
+
108
+ export type SandboxFieldType = 'text' | 'password';
109
+
110
+ /** A single credential/config field a sandbox provider needs. The Settings
111
+ * UI renders these dynamically per provider, the same way vector store and
112
+ * embedding provider fields are rendered from their own registries. */
113
+ export interface SandboxCredentialField {
114
+ key: string;
115
+ label: string;
116
+ type: SandboxFieldType;
117
+ required: boolean;
118
+ placeholder?: string;
119
+ help?: string;
120
+ secret?: boolean;
121
+ }
122
+
123
+ export interface SandboxProviderDescriptor {
124
+ id: SandboxBackend;
125
+ label: string;
126
+ description: string;
127
+ /** npm package this adapter dynamically imports at runtime. Never a hard
128
+ * dependency of @larkup/sandbox — installing all nine SDKs unconditionally
129
+ * would bloat every consumer, so each adapter lazy-imports its own. */
130
+ sdkPackage?: string;
131
+ docsUrl: string;
132
+ /** Path under /public/icons (or /public) in the web app. */
133
+ icon: string;
134
+ /**
135
+ * Whether `execute()` genuinely runs arbitrary code/commands and returns
136
+ * output, or the provider is architecturally something else (browser
137
+ * automation, a client-only runtime, a Worker binding). Surfaced in the
138
+ * UI so operators don't pick a provider that can't do what they expect.
139
+ */
140
+ executionSupport: 'full' | 'unsupported';
141
+ /** Shown in the UI when executionSupport !== "full". */
142
+ executionCaveat?: string;
143
+ fields: SandboxCredentialField[];
144
+ }
145
+
146
+ /**
147
+ * Adapter contract every remote sandbox backend implements. Docker is
148
+ * handled separately (docker-runner.ts) since it talks to the local daemon
149
+ * and needs no credentials.
150
+ */
151
+ export interface SandboxProviderAdapter {
152
+ readonly id: SandboxBackend;
153
+ /** Cheap call that proves the credentials are valid — powers the "Verify" button. */
154
+ verifyCredentials(credentials: Record<string, string>): Promise<void>;
155
+ /** Confirms the backend is reachable/configured without spinning up a full sandbox. */
156
+ healthCheck(credentials: Record<string, string>): Promise<SandboxHealthCheck>;
157
+ /** Runs code/commands inside a fresh sandbox instance and tears it down afterward. */
158
+ execute(request: ExecutionRequest, credentials: Record<string, string>): Promise<ExecutionResult>;
159
+ }
@@ -0,0 +1,4 @@
1
+ {
2
+ "extends": "./tsconfig.json",
3
+ "exclude": ["node_modules", "dist", "src/**/*.test.ts"]
4
+ }
@@ -0,0 +1,9 @@
1
+ import { defineConfig } from 'vitest/config';
2
+
3
+ export default defineConfig({
4
+ test: {
5
+ include: ['src/**/*.test.ts'],
6
+ exclude: ['node_modules/**'],
7
+ environment: 'node',
8
+ },
9
+ });