@larkup/sandbox 0.1.2 → 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +2 -2
- package/CHANGELOG.md +6 -0
- package/dist/docker-runner.js +27 -15
- package/dist/index.d.ts +4 -2
- package/dist/index.js +3 -1
- package/dist/local-runner.d.ts +5 -0
- package/dist/local-runner.js +176 -0
- package/dist/mime.d.ts +1 -0
- package/dist/mime.js +16 -0
- package/dist/providers/browserbase.d.ts +12 -0
- package/dist/providers/browserbase.js +61 -0
- package/dist/providers/cloudflare.d.ts +5 -0
- package/dist/providers/cloudflare.js +55 -0
- package/dist/providers/daytona.d.ts +12 -0
- package/dist/providers/daytona.js +84 -0
- package/dist/providers/e2b.d.ts +9 -0
- package/dist/providers/e2b.js +70 -0
- package/dist/providers/flyio.d.ts +9 -0
- package/dist/providers/flyio.js +98 -0
- package/dist/providers/index.d.ts +13 -0
- package/dist/providers/index.js +26 -0
- package/dist/providers/modal.d.ts +8 -0
- package/dist/providers/modal.js +96 -0
- package/dist/providers/northflank.d.ts +9 -0
- package/dist/providers/northflank.js +122 -0
- package/dist/providers/vercel.d.ts +9 -0
- package/dist/providers/vercel.js +79 -0
- package/dist/providers/webcontainers.d.ts +13 -0
- package/dist/providers/webcontainers.js +28 -0
- package/dist/registry.d.ts +20 -0
- package/dist/registry.js +281 -0
- package/dist/remote-exec.d.ts +42 -0
- package/dist/remote-exec.js +144 -0
- package/dist/sandbox-manager.d.ts +5 -2
- package/dist/sandbox-manager.js +67 -46
- package/dist/types.d.ts +58 -9
- package/dist/types.js +1 -1
- package/package.json +21 -4
- package/src/docker-runner.ts +29 -19
- package/src/index.ts +11 -2
- package/src/local-runner.test.ts +22 -0
- package/src/local-runner.ts +199 -0
- package/src/mime.ts +17 -0
- package/src/providers/browserbase.test.ts +55 -0
- package/src/providers/browserbase.ts +88 -0
- package/src/providers/cloudflare.test.ts +63 -0
- package/src/providers/cloudflare.ts +72 -0
- package/src/providers/daytona.test.ts +60 -0
- package/src/providers/daytona.ts +144 -0
- package/src/providers/e2b.test.ts +100 -0
- package/src/providers/e2b.ts +115 -0
- package/src/providers/flyio.test.ts +69 -0
- package/src/providers/flyio.ts +148 -0
- package/src/providers/index.ts +42 -0
- package/src/providers/modal.test.ts +107 -0
- package/src/providers/modal.ts +167 -0
- package/src/providers/northflank.test.ts +110 -0
- package/src/providers/northflank.ts +199 -0
- package/src/providers/vercel.test.ts +84 -0
- package/src/providers/vercel.ts +143 -0
- package/src/providers/webcontainers.test.ts +21 -0
- package/src/providers/webcontainers.ts +36 -0
- package/src/registry.test.ts +84 -0
- package/src/registry.ts +308 -0
- package/src/remote-exec.test.ts +155 -0
- package/src/remote-exec.ts +190 -0
- package/src/sandbox-manager.test.ts +74 -0
- package/src/sandbox-manager.ts +71 -51
- package/src/types.ts +81 -23
- package/tsconfig.build.json +4 -0
- package/vitest.config.ts +9 -0
|
@@ -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
|
+
});
|
package/src/sandbox-manager.ts
CHANGED
|
@@ -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
|
|
6
|
-
* image setup, and execution
|
|
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 ?? '
|
|
39
|
+
backend: config?.backend ?? 'local',
|
|
38
40
|
docker: config?.docker,
|
|
39
|
-
|
|
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
|
-
|
|
46
|
-
|
|
47
|
-
return checkDockerHealth();
|
|
47
|
+
if (this.config.backend === 'local') return checkLocalRuntime();
|
|
48
|
+
if (this.config.backend === 'docker') return checkDockerHealth();
|
|
48
49
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
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:
|
|
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
|
-
|
|
91
|
+
const imageReady = health.imageReady || (await ensureImage().catch(() => false));
|
|
92
|
+
if (!imageReady) {
|
|
115
93
|
return {
|
|
116
94
|
stdout: '',
|
|
117
|
-
stderr:
|
|
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 =
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
|
90
|
-
|
|
|
91
|
-
|
|
|
92
|
-
|
|
|
93
|
-
|
|
|
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
|
+
}
|