@ebowwa/sandbox 0.1.1
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/dist/compilers/index.d.ts +24 -0
- package/dist/compilers/index.d.ts.map +1 -0
- package/dist/compilers/index.js +42 -0
- package/dist/compilers/index.js.map +1 -0
- package/dist/compilers/javascript.d.ts +117 -0
- package/dist/compilers/javascript.d.ts.map +1 -0
- package/dist/compilers/javascript.js +462 -0
- package/dist/compilers/javascript.js.map +1 -0
- package/dist/compilers/python.d.ts +140 -0
- package/dist/compilers/python.d.ts.map +1 -0
- package/dist/compilers/python.js +650 -0
- package/dist/compilers/python.js.map +1 -0
- package/dist/compilers/typescript.d.ts +99 -0
- package/dist/compilers/typescript.d.ts.map +1 -0
- package/dist/compilers/typescript.js +323 -0
- package/dist/compilers/typescript.js.map +1 -0
- package/dist/core/cell.d.ts +160 -0
- package/dist/core/cell.d.ts.map +1 -0
- package/dist/core/cell.js +319 -0
- package/dist/core/cell.js.map +1 -0
- package/dist/core/compiler.d.ts +126 -0
- package/dist/core/compiler.d.ts.map +1 -0
- package/dist/core/compiler.js +123 -0
- package/dist/core/compiler.js.map +1 -0
- package/dist/core/index.d.ts +19 -0
- package/dist/core/index.d.ts.map +1 -0
- package/dist/core/index.js +14 -0
- package/dist/core/index.js.map +1 -0
- package/dist/core/limits.d.ts +173 -0
- package/dist/core/limits.d.ts.map +1 -0
- package/dist/core/limits.js +440 -0
- package/dist/core/limits.js.map +1 -0
- package/dist/core/permissions.d.ts +103 -0
- package/dist/core/permissions.d.ts.map +1 -0
- package/dist/core/permissions.js +341 -0
- package/dist/core/permissions.js.map +1 -0
- package/dist/core/runtime.d.ts +127 -0
- package/dist/core/runtime.d.ts.map +1 -0
- package/dist/core/runtime.js +325 -0
- package/dist/core/runtime.js.map +1 -0
- package/dist/core/types.d.ts +380 -0
- package/dist/core/types.d.ts.map +1 -0
- package/dist/core/types.js +67 -0
- package/dist/core/types.js.map +1 -0
- package/dist/index.d.ts +145 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +279 -0
- package/dist/index.js.map +1 -0
- package/dist/multi/index.d.ts +9 -0
- package/dist/multi/index.d.ts.map +1 -0
- package/dist/multi/index.js +7 -0
- package/dist/multi/index.js.map +1 -0
- package/dist/multi/polyglot.d.ts +179 -0
- package/dist/multi/polyglot.d.ts.map +1 -0
- package/dist/multi/polyglot.js +319 -0
- package/dist/multi/polyglot.js.map +1 -0
- package/dist/runtimes/docker.d.ts +97 -0
- package/dist/runtimes/docker.d.ts.map +1 -0
- package/dist/runtimes/docker.js +368 -0
- package/dist/runtimes/docker.js.map +1 -0
- package/dist/runtimes/index.d.ts +11 -0
- package/dist/runtimes/index.d.ts.map +1 -0
- package/dist/runtimes/index.js +9 -0
- package/dist/runtimes/index.js.map +1 -0
- package/dist/runtimes/process.d.ts +47 -0
- package/dist/runtimes/process.d.ts.map +1 -0
- package/dist/runtimes/process.js +230 -0
- package/dist/runtimes/process.js.map +1 -0
- package/dist/session/index.d.ts +12 -0
- package/dist/session/index.d.ts.map +1 -0
- package/dist/session/index.js +9 -0
- package/dist/session/index.js.map +1 -0
- package/dist/session/kernel.d.ts +199 -0
- package/dist/session/kernel.d.ts.map +1 -0
- package/dist/session/kernel.js +400 -0
- package/dist/session/kernel.js.map +1 -0
- package/dist/session/notebook.d.ts +168 -0
- package/dist/session/notebook.d.ts.map +1 -0
- package/dist/session/notebook.js +499 -0
- package/dist/session/notebook.js.map +1 -0
- package/dist/session/repl.d.ts +159 -0
- package/dist/session/repl.d.ts.map +1 -0
- package/dist/session/repl.js +409 -0
- package/dist/session/repl.js.map +1 -0
- package/package.json +142 -0
- package/src/compilers/index.ts +80 -0
- package/src/compilers/javascript.ts +571 -0
- package/src/compilers/python.ts +785 -0
- package/src/compilers/typescript.ts +442 -0
- package/src/core/cell.ts +439 -0
- package/src/core/compiler.ts +250 -0
- package/src/core/index.ts +123 -0
- package/src/core/limits.ts +508 -0
- package/src/core/permissions.ts +409 -0
- package/src/core/runtime.ts +499 -0
- package/src/core/types.ts +528 -0
- package/src/global.d.ts +59 -0
- package/src/index.ts +515 -0
- package/src/multi/index.ts +22 -0
- package/src/multi/polyglot.ts +461 -0
- package/src/runtimes/docker.ts +501 -0
- package/src/runtimes/index.ts +21 -0
- package/src/runtimes/process.ts +316 -0
- package/src/session/index.ts +41 -0
- package/src/session/kernel.ts +553 -0
- package/src/session/notebook.ts +635 -0
- package/src/session/repl.ts +521 -0
- package/src/wasm2wasm.d.ts +35 -0
|
@@ -0,0 +1,501 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Docker Runtime
|
|
3
|
+
*
|
|
4
|
+
* Runs code in Docker containers for maximum isolation.
|
|
5
|
+
* Provides full filesystem and network control with container-level security.
|
|
6
|
+
*
|
|
7
|
+
* Isolation level: 'container'
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { spawn, type ChildProcess } from "node:child_process";
|
|
11
|
+
import { randomUUID } from "node:crypto";
|
|
12
|
+
import { setTimeout as setTimeoutPromise } from "node:timers/promises";
|
|
13
|
+
import type {
|
|
14
|
+
ExecutionResult,
|
|
15
|
+
Permissions,
|
|
16
|
+
Limits,
|
|
17
|
+
DisplayOutput,
|
|
18
|
+
} from "../core/types.js";
|
|
19
|
+
import type {
|
|
20
|
+
IRuntime,
|
|
21
|
+
RuntimeOptions,
|
|
22
|
+
ExecutionRequest,
|
|
23
|
+
RuntimeCapabilities,
|
|
24
|
+
} from "../core/runtime.js";
|
|
25
|
+
|
|
26
|
+
/** Docker runtime configuration */
|
|
27
|
+
export interface DockerRuntimeConfig {
|
|
28
|
+
/** Default Docker image to use */
|
|
29
|
+
defaultImage?: string;
|
|
30
|
+
/** Docker socket path */
|
|
31
|
+
dockerSocket?: string;
|
|
32
|
+
/** Network mode for containers */
|
|
33
|
+
networkMode?: "none" | "bridge" | "host";
|
|
34
|
+
/** Cleanup containers after execution */
|
|
35
|
+
autoCleanup?: boolean;
|
|
36
|
+
/** Memory limit for containers (bytes) */
|
|
37
|
+
defaultMemory?: number;
|
|
38
|
+
/** CPU limit (number of CPUs) */
|
|
39
|
+
cpuLimit?: number;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Container execution options */
|
|
43
|
+
export interface ContainerOptions {
|
|
44
|
+
/** Docker image to use */
|
|
45
|
+
image?: string;
|
|
46
|
+
/** Container name */
|
|
47
|
+
name?: string;
|
|
48
|
+
/** Environment variables */
|
|
49
|
+
env?: Record<string, string>;
|
|
50
|
+
/** Working directory in container */
|
|
51
|
+
workdir?: string;
|
|
52
|
+
/** Mount points */
|
|
53
|
+
mounts?: Array<{
|
|
54
|
+
source: string;
|
|
55
|
+
target: string;
|
|
56
|
+
readonly?: boolean;
|
|
57
|
+
}>;
|
|
58
|
+
/** Port mappings */
|
|
59
|
+
ports?: Array<{
|
|
60
|
+
hostPort: number;
|
|
61
|
+
containerPort: number;
|
|
62
|
+
protocol?: "tcp" | "udp";
|
|
63
|
+
}>;
|
|
64
|
+
/** Network mode */
|
|
65
|
+
network?: "none" | "bridge" | "host";
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Docker Runtime
|
|
70
|
+
*
|
|
71
|
+
* Executes code in isolated Docker containers with full resource control.
|
|
72
|
+
* Maximum isolation with configurable filesystem and network access.
|
|
73
|
+
*/
|
|
74
|
+
export class DockerRuntime implements IRuntime {
|
|
75
|
+
readonly name = "docker";
|
|
76
|
+
readonly capabilities: RuntimeCapabilities = {
|
|
77
|
+
isolation: "container",
|
|
78
|
+
stateful: true,
|
|
79
|
+
async: true,
|
|
80
|
+
filesystem: true,
|
|
81
|
+
network: true,
|
|
82
|
+
maxMemory: 16 * 1024 * 1024 * 1024, // 16GB
|
|
83
|
+
wasi: true,
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
private executing = false;
|
|
87
|
+
private config: Required<DockerRuntimeConfig>;
|
|
88
|
+
private containers = new Map<string, string>(); // name -> containerId
|
|
89
|
+
|
|
90
|
+
constructor(config: DockerRuntimeConfig = {}) {
|
|
91
|
+
this.config = {
|
|
92
|
+
defaultImage: config.defaultImage ?? "node:20-slim",
|
|
93
|
+
dockerSocket: config.dockerSocket ?? "/var/run/docker.sock",
|
|
94
|
+
networkMode: config.networkMode ?? "none",
|
|
95
|
+
autoCleanup: config.autoCleanup ?? true,
|
|
96
|
+
defaultMemory: config.defaultMemory ?? 512 * 1024 * 1024, // 512MB
|
|
97
|
+
cpuLimit: config.cpuLimit ?? 1,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
async init(): Promise<void> {
|
|
102
|
+
// Verify Docker is available
|
|
103
|
+
const available = await this.checkDockerAvailable();
|
|
104
|
+
if (!available) {
|
|
105
|
+
throw new Error("Docker is not available. Ensure Docker daemon is running.");
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
async isAvailable(): Promise<boolean> {
|
|
110
|
+
return this.checkDockerAvailable();
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
async execute(
|
|
114
|
+
request: ExecutionRequest,
|
|
115
|
+
options: RuntimeOptions
|
|
116
|
+
): Promise<ExecutionResult> {
|
|
117
|
+
const startTime = Date.now();
|
|
118
|
+
this.executing = true;
|
|
119
|
+
|
|
120
|
+
const stdout: string[] = [];
|
|
121
|
+
const stderr: string[] = [];
|
|
122
|
+
const displays: DisplayOutput[] = [];
|
|
123
|
+
|
|
124
|
+
const containerName = `sandbox-${randomUUID().slice(0, 8)}`;
|
|
125
|
+
|
|
126
|
+
try {
|
|
127
|
+
// Parse limits
|
|
128
|
+
const timeout = this.parseTimeout(options.limits?.timeout);
|
|
129
|
+
const maxMemory = this.parseMemory(options.limits?.memory) || this.config.defaultMemory;
|
|
130
|
+
const cpuLimit = options.limits?.cpuTime
|
|
131
|
+
? Math.max(0.1, (options.limits.cpuTime / 1000))
|
|
132
|
+
: this.config.cpuLimit;
|
|
133
|
+
|
|
134
|
+
// Build container options from permissions
|
|
135
|
+
const containerOptions = this.buildContainerOptions(request, options, {
|
|
136
|
+
memory: maxMemory,
|
|
137
|
+
cpu: cpuLimit,
|
|
138
|
+
name: containerName,
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
// Create abort controller for timeout
|
|
142
|
+
const controller = new AbortController();
|
|
143
|
+
const timeoutId = setTimeout(() => controller.abort(), timeout);
|
|
144
|
+
|
|
145
|
+
if (options.signal) {
|
|
146
|
+
options.signal.addEventListener("abort", () => {
|
|
147
|
+
controller.abort();
|
|
148
|
+
clearTimeout(timeoutId);
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Pull image if needed
|
|
153
|
+
await this.pullImageIfNeeded(containerOptions.image!);
|
|
154
|
+
|
|
155
|
+
// Create and start container
|
|
156
|
+
const containerId = await this.createContainer(containerOptions);
|
|
157
|
+
this.containers.set(containerName, containerId);
|
|
158
|
+
|
|
159
|
+
// Execute code in container
|
|
160
|
+
const result = await this.runInContainer(
|
|
161
|
+
containerId,
|
|
162
|
+
request,
|
|
163
|
+
controller.signal
|
|
164
|
+
);
|
|
165
|
+
|
|
166
|
+
clearTimeout(timeoutId);
|
|
167
|
+
|
|
168
|
+
// Collect output
|
|
169
|
+
stdout.push(...result.stdout);
|
|
170
|
+
stderr.push(...result.stderr);
|
|
171
|
+
|
|
172
|
+
return {
|
|
173
|
+
success: result.exitCode === 0,
|
|
174
|
+
value: result.value,
|
|
175
|
+
metrics: {
|
|
176
|
+
duration: Date.now() - startTime,
|
|
177
|
+
memoryUsed: maxMemory, // Approximate
|
|
178
|
+
},
|
|
179
|
+
output: { stdout, stderr, displays },
|
|
180
|
+
};
|
|
181
|
+
} catch (error) {
|
|
182
|
+
if (error instanceof Error && error.name === "AbortError") {
|
|
183
|
+
return this.createErrorResult(
|
|
184
|
+
"Execution timed out",
|
|
185
|
+
"timeout",
|
|
186
|
+
startTime,
|
|
187
|
+
stdout,
|
|
188
|
+
stderr
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
return this.createErrorResult(
|
|
193
|
+
error instanceof Error ? error.message : String(error),
|
|
194
|
+
"runtime",
|
|
195
|
+
startTime,
|
|
196
|
+
stdout,
|
|
197
|
+
stderr
|
|
198
|
+
);
|
|
199
|
+
} finally {
|
|
200
|
+
// Cleanup container
|
|
201
|
+
if (this.config.autoCleanup) {
|
|
202
|
+
await this.removeContainer(containerName);
|
|
203
|
+
}
|
|
204
|
+
this.executing = false;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
async terminate(): Promise<void> {
|
|
209
|
+
// Cleanup all containers
|
|
210
|
+
for (const [name] of this.containers) {
|
|
211
|
+
await this.removeContainer(name);
|
|
212
|
+
}
|
|
213
|
+
this.containers.clear();
|
|
214
|
+
this.executing = false;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
isExecuting(): boolean {
|
|
218
|
+
return this.executing;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Check if Docker is available
|
|
223
|
+
*/
|
|
224
|
+
private async checkDockerAvailable(): Promise<boolean> {
|
|
225
|
+
return new Promise((resolve) => {
|
|
226
|
+
const proc = spawn("docker", ["version"], { stdio: "ignore" });
|
|
227
|
+
proc.on("close", (code) => resolve(code === 0));
|
|
228
|
+
proc.on("error", () => resolve(false));
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Pull Docker image if not present
|
|
234
|
+
*/
|
|
235
|
+
private async pullImageIfNeeded(image: string): Promise<void> {
|
|
236
|
+
// Check if image exists
|
|
237
|
+
const exists = await new Promise<boolean>((resolve) => {
|
|
238
|
+
const proc = spawn("docker", ["image", "inspect", image], { stdio: "ignore" });
|
|
239
|
+
proc.on("close", (code) => resolve(code === 0));
|
|
240
|
+
proc.on("error", () => resolve(false));
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
if (!exists) {
|
|
244
|
+
await new Promise<void>((resolve, reject) => {
|
|
245
|
+
const proc = spawn("docker", ["pull", image], { stdio: "inherit" });
|
|
246
|
+
proc.on("close", (code) => {
|
|
247
|
+
if (code === 0) resolve();
|
|
248
|
+
else reject(new Error(`Failed to pull image: ${image}`));
|
|
249
|
+
});
|
|
250
|
+
proc.on("error", reject);
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Build container options from execution request
|
|
257
|
+
*/
|
|
258
|
+
private buildContainerOptions(
|
|
259
|
+
request: ExecutionRequest,
|
|
260
|
+
options: RuntimeOptions,
|
|
261
|
+
limits: { memory: number; cpu: number; name: string }
|
|
262
|
+
): ContainerOptions {
|
|
263
|
+
const perms = options.permissions;
|
|
264
|
+
const containerOptions: ContainerOptions = {
|
|
265
|
+
image: this.config.defaultImage,
|
|
266
|
+
name: limits.name,
|
|
267
|
+
env: options.env,
|
|
268
|
+
workdir: options.cwd ?? "/workspace",
|
|
269
|
+
network: this.config.networkMode,
|
|
270
|
+
};
|
|
271
|
+
|
|
272
|
+
// Filesystem permissions
|
|
273
|
+
if (perms.fs?.read || perms.fs?.write) {
|
|
274
|
+
containerOptions.mounts = [
|
|
275
|
+
{
|
|
276
|
+
source: options.cwd ?? process.cwd(),
|
|
277
|
+
target: containerOptions.workdir ?? "/workspace",
|
|
278
|
+
readonly: !perms.fs.write,
|
|
279
|
+
},
|
|
280
|
+
];
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// Network permissions
|
|
284
|
+
if (perms.network?.outbound || perms.network?.inbound) {
|
|
285
|
+
containerOptions.network = "bridge";
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
return containerOptions;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* Create Docker container
|
|
293
|
+
*/
|
|
294
|
+
private async createContainer(opts: ContainerOptions): Promise<string> {
|
|
295
|
+
const args = ["create"];
|
|
296
|
+
|
|
297
|
+
// Name
|
|
298
|
+
args.push("--name", opts.name!);
|
|
299
|
+
|
|
300
|
+
// Memory limit
|
|
301
|
+
args.push("--memory", `${Math.floor(this.config.defaultMemory / (1024 * 1024))}m`);
|
|
302
|
+
|
|
303
|
+
// CPU limit
|
|
304
|
+
args.push("--cpus", this.config.cpuLimit.toString());
|
|
305
|
+
|
|
306
|
+
// Network mode
|
|
307
|
+
if (opts.network) {
|
|
308
|
+
args.push(`--network=${opts.network}`);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// Working directory
|
|
312
|
+
if (opts.workdir) {
|
|
313
|
+
args.push("--workdir", opts.workdir);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// Environment variables
|
|
317
|
+
if (opts.env) {
|
|
318
|
+
for (const [key, value] of Object.entries(opts.env)) {
|
|
319
|
+
args.push("--env", `${key}=${value}`);
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// Mounts
|
|
324
|
+
if (opts.mounts) {
|
|
325
|
+
for (const mount of opts.mounts) {
|
|
326
|
+
const mode = mount.readonly ? ":ro" : "";
|
|
327
|
+
args.push("--volume", `${mount.source}:${mount.target}${mode}`);
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// Port mappings
|
|
332
|
+
if (opts.ports) {
|
|
333
|
+
for (const port of opts.ports) {
|
|
334
|
+
const protocol = port.protocol ?? "tcp";
|
|
335
|
+
args.push("--publish", `${port.hostPort}:${port.containerPort}/${protocol}`);
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// Security options
|
|
340
|
+
args.push("--security-opt", "no-new-privileges");
|
|
341
|
+
|
|
342
|
+
// Disable inter-container communication
|
|
343
|
+
args.push("--icc=false");
|
|
344
|
+
|
|
345
|
+
// Image
|
|
346
|
+
args.push(opts.image!);
|
|
347
|
+
|
|
348
|
+
return new Promise((resolve, reject) => {
|
|
349
|
+
const proc = spawn("docker", args);
|
|
350
|
+
let output = "";
|
|
351
|
+
|
|
352
|
+
proc.stdout.on("data", (data: Buffer) => {
|
|
353
|
+
output += data.toString();
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
proc.stderr.on("data", (data: Buffer) => {
|
|
357
|
+
console.error(`docker stderr: ${data}`);
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
proc.on("close", (code) => {
|
|
361
|
+
if (code === 0) {
|
|
362
|
+
resolve(output.trim());
|
|
363
|
+
} else {
|
|
364
|
+
reject(new Error(`Failed to create container: ${output}`));
|
|
365
|
+
}
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
proc.on("error", reject);
|
|
369
|
+
});
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
/**
|
|
373
|
+
* Run code in container
|
|
374
|
+
*/
|
|
375
|
+
private async runInContainer(
|
|
376
|
+
containerId: string,
|
|
377
|
+
request: ExecutionRequest,
|
|
378
|
+
signal: AbortSignal
|
|
379
|
+
): Promise<{ stdout: string[]; stderr: string[]; exitCode: number; value?: unknown }> {
|
|
380
|
+
const stdout: string[] = [];
|
|
381
|
+
const stderr: string[] = [];
|
|
382
|
+
|
|
383
|
+
// Start container
|
|
384
|
+
await new Promise<void>((resolve, reject) => {
|
|
385
|
+
const proc = spawn("docker", ["start", containerId]);
|
|
386
|
+
proc.on("close", (code) => {
|
|
387
|
+
if (code === 0) resolve();
|
|
388
|
+
else reject(new Error("Failed to start container"));
|
|
389
|
+
});
|
|
390
|
+
proc.on("error", reject);
|
|
391
|
+
});
|
|
392
|
+
|
|
393
|
+
// Write WASM to temp file and execute
|
|
394
|
+
const wasmBase64 = Buffer.from(request.wasm.wasmBytes).toString("base64");
|
|
395
|
+
const execCommand = `node -e "require('fs').writeFileSync('/tmp/code.wasm',Buffer.from('${wasmBase64}','base64'));const {WASI}=require('wasi');const wasi=new WASI({version:'preview1'});WebAssembly.instantiate(require('fs').readFileSync('/tmp/code.wasm'),{wasi_snapshot_preview1:wasi.wasiImport}).then(({instance})=>{wasi.start(instance)})"`;
|
|
396
|
+
|
|
397
|
+
// Execute in container
|
|
398
|
+
return new Promise((resolve, reject) => {
|
|
399
|
+
const proc = spawn("docker", [
|
|
400
|
+
"exec",
|
|
401
|
+
containerId,
|
|
402
|
+
"/bin/sh",
|
|
403
|
+
"-c",
|
|
404
|
+
execCommand,
|
|
405
|
+
]);
|
|
406
|
+
|
|
407
|
+
proc.stdout.on("data", (data: Buffer) => {
|
|
408
|
+
stdout.push(data.toString("utf8"));
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
proc.stderr.on("data", (data: Buffer) => {
|
|
412
|
+
stderr.push(data.toString("utf8"));
|
|
413
|
+
});
|
|
414
|
+
|
|
415
|
+
signal.addEventListener("abort", () => {
|
|
416
|
+
proc.kill("SIGKILL");
|
|
417
|
+
});
|
|
418
|
+
|
|
419
|
+
proc.on("close", (code) => {
|
|
420
|
+
// Try to parse output as JSON
|
|
421
|
+
let value: unknown = undefined;
|
|
422
|
+
const output = stdout.join("");
|
|
423
|
+
try {
|
|
424
|
+
const parsed = JSON.parse(output);
|
|
425
|
+
if (parsed.__return_value !== undefined) {
|
|
426
|
+
value = parsed.__return_value;
|
|
427
|
+
}
|
|
428
|
+
} catch {
|
|
429
|
+
value = output.trim() || undefined;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
resolve({
|
|
433
|
+
stdout,
|
|
434
|
+
stderr,
|
|
435
|
+
exitCode: code ?? 1,
|
|
436
|
+
value,
|
|
437
|
+
});
|
|
438
|
+
});
|
|
439
|
+
|
|
440
|
+
proc.on("error", reject);
|
|
441
|
+
});
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
/**
|
|
445
|
+
* Remove container
|
|
446
|
+
*/
|
|
447
|
+
private async removeContainer(nameOrId: string): Promise<void> {
|
|
448
|
+
const containerId = this.containers.get(nameOrId) ?? nameOrId;
|
|
449
|
+
|
|
450
|
+
await new Promise<void>((resolve) => {
|
|
451
|
+
const proc = spawn("docker", ["rm", "-f", containerId], { stdio: "ignore" });
|
|
452
|
+
proc.on("close", () => {
|
|
453
|
+
this.containers.delete(nameOrId);
|
|
454
|
+
resolve();
|
|
455
|
+
});
|
|
456
|
+
proc.on("error", () => resolve());
|
|
457
|
+
});
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
private createErrorResult(
|
|
461
|
+
message: string,
|
|
462
|
+
type: "compile" | "runtime" | "permission" | "limit" | "timeout" | "cancel",
|
|
463
|
+
startTime: number,
|
|
464
|
+
stdout: string[] = [],
|
|
465
|
+
stderr: string[] = []
|
|
466
|
+
): ExecutionResult {
|
|
467
|
+
return {
|
|
468
|
+
success: false,
|
|
469
|
+
error: { message, type },
|
|
470
|
+
metrics: {
|
|
471
|
+
duration: Date.now() - startTime,
|
|
472
|
+
memoryUsed: 0,
|
|
473
|
+
},
|
|
474
|
+
output: { stdout, stderr, displays: [] },
|
|
475
|
+
};
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
private parseMemory(memory: string | number | undefined): number {
|
|
479
|
+
if (!memory) return this.config.defaultMemory;
|
|
480
|
+
if (typeof memory === "number") return memory;
|
|
481
|
+
const match = memory.match(/^(\d+(?:\.\d+)?)\s*(b|kb|mb|gb)?$/i);
|
|
482
|
+
if (!match) return this.config.defaultMemory;
|
|
483
|
+
const [, num, unit] = match;
|
|
484
|
+
const multipliers: Record<string, number> = {
|
|
485
|
+
b: 1, kb: 1024, mb: 1024 ** 2, gb: 1024 ** 3,
|
|
486
|
+
};
|
|
487
|
+
return Math.floor(parseFloat(num) * (multipliers[unit?.toLowerCase() ?? "b"] ?? 1));
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
private parseTimeout(timeout: string | number | undefined): number {
|
|
491
|
+
if (!timeout) return 300000; // 5 minutes default for containers
|
|
492
|
+
if (typeof timeout === "number") return timeout;
|
|
493
|
+
const match = timeout.match(/^(\d+(?:\.\d+)?)\s*(ms|s|m)?$/i);
|
|
494
|
+
if (!match) return 300000;
|
|
495
|
+
const [, num, unit] = match;
|
|
496
|
+
const multipliers: Record<string, number> = {
|
|
497
|
+
ms: 1, s: 1000, m: 60000,
|
|
498
|
+
};
|
|
499
|
+
return Math.floor(parseFloat(num) * (multipliers[unit?.toLowerCase() ?? "ms"] ?? 1));
|
|
500
|
+
}
|
|
501
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Runtime Exports
|
|
3
|
+
*
|
|
4
|
+
* Exports all available runtimes for sandboxed code execution.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
// Type exports
|
|
8
|
+
export type { ProcessRuntimeConfig } from "./process.js";
|
|
9
|
+
export type { DockerRuntimeConfig, ContainerOptions } from "./docker.js";
|
|
10
|
+
|
|
11
|
+
// Runtime implementations
|
|
12
|
+
export { ProcessRuntime } from "./process.js";
|
|
13
|
+
export { DockerRuntime } from "./docker.js";
|
|
14
|
+
|
|
15
|
+
// Re-export types from core for convenience
|
|
16
|
+
export type {
|
|
17
|
+
IRuntime,
|
|
18
|
+
RuntimeOptions,
|
|
19
|
+
ExecutionRequest,
|
|
20
|
+
RuntimeCapabilities,
|
|
21
|
+
} from "../core/runtime.js";
|