@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.
Files changed (108) hide show
  1. package/dist/compilers/index.d.ts +24 -0
  2. package/dist/compilers/index.d.ts.map +1 -0
  3. package/dist/compilers/index.js +42 -0
  4. package/dist/compilers/index.js.map +1 -0
  5. package/dist/compilers/javascript.d.ts +117 -0
  6. package/dist/compilers/javascript.d.ts.map +1 -0
  7. package/dist/compilers/javascript.js +462 -0
  8. package/dist/compilers/javascript.js.map +1 -0
  9. package/dist/compilers/python.d.ts +140 -0
  10. package/dist/compilers/python.d.ts.map +1 -0
  11. package/dist/compilers/python.js +650 -0
  12. package/dist/compilers/python.js.map +1 -0
  13. package/dist/compilers/typescript.d.ts +99 -0
  14. package/dist/compilers/typescript.d.ts.map +1 -0
  15. package/dist/compilers/typescript.js +323 -0
  16. package/dist/compilers/typescript.js.map +1 -0
  17. package/dist/core/cell.d.ts +160 -0
  18. package/dist/core/cell.d.ts.map +1 -0
  19. package/dist/core/cell.js +319 -0
  20. package/dist/core/cell.js.map +1 -0
  21. package/dist/core/compiler.d.ts +126 -0
  22. package/dist/core/compiler.d.ts.map +1 -0
  23. package/dist/core/compiler.js +123 -0
  24. package/dist/core/compiler.js.map +1 -0
  25. package/dist/core/index.d.ts +19 -0
  26. package/dist/core/index.d.ts.map +1 -0
  27. package/dist/core/index.js +14 -0
  28. package/dist/core/index.js.map +1 -0
  29. package/dist/core/limits.d.ts +173 -0
  30. package/dist/core/limits.d.ts.map +1 -0
  31. package/dist/core/limits.js +440 -0
  32. package/dist/core/limits.js.map +1 -0
  33. package/dist/core/permissions.d.ts +103 -0
  34. package/dist/core/permissions.d.ts.map +1 -0
  35. package/dist/core/permissions.js +341 -0
  36. package/dist/core/permissions.js.map +1 -0
  37. package/dist/core/runtime.d.ts +127 -0
  38. package/dist/core/runtime.d.ts.map +1 -0
  39. package/dist/core/runtime.js +325 -0
  40. package/dist/core/runtime.js.map +1 -0
  41. package/dist/core/types.d.ts +380 -0
  42. package/dist/core/types.d.ts.map +1 -0
  43. package/dist/core/types.js +67 -0
  44. package/dist/core/types.js.map +1 -0
  45. package/dist/index.d.ts +145 -0
  46. package/dist/index.d.ts.map +1 -0
  47. package/dist/index.js +279 -0
  48. package/dist/index.js.map +1 -0
  49. package/dist/multi/index.d.ts +9 -0
  50. package/dist/multi/index.d.ts.map +1 -0
  51. package/dist/multi/index.js +7 -0
  52. package/dist/multi/index.js.map +1 -0
  53. package/dist/multi/polyglot.d.ts +179 -0
  54. package/dist/multi/polyglot.d.ts.map +1 -0
  55. package/dist/multi/polyglot.js +319 -0
  56. package/dist/multi/polyglot.js.map +1 -0
  57. package/dist/runtimes/docker.d.ts +97 -0
  58. package/dist/runtimes/docker.d.ts.map +1 -0
  59. package/dist/runtimes/docker.js +368 -0
  60. package/dist/runtimes/docker.js.map +1 -0
  61. package/dist/runtimes/index.d.ts +11 -0
  62. package/dist/runtimes/index.d.ts.map +1 -0
  63. package/dist/runtimes/index.js +9 -0
  64. package/dist/runtimes/index.js.map +1 -0
  65. package/dist/runtimes/process.d.ts +47 -0
  66. package/dist/runtimes/process.d.ts.map +1 -0
  67. package/dist/runtimes/process.js +230 -0
  68. package/dist/runtimes/process.js.map +1 -0
  69. package/dist/session/index.d.ts +12 -0
  70. package/dist/session/index.d.ts.map +1 -0
  71. package/dist/session/index.js +9 -0
  72. package/dist/session/index.js.map +1 -0
  73. package/dist/session/kernel.d.ts +199 -0
  74. package/dist/session/kernel.d.ts.map +1 -0
  75. package/dist/session/kernel.js +400 -0
  76. package/dist/session/kernel.js.map +1 -0
  77. package/dist/session/notebook.d.ts +168 -0
  78. package/dist/session/notebook.d.ts.map +1 -0
  79. package/dist/session/notebook.js +499 -0
  80. package/dist/session/notebook.js.map +1 -0
  81. package/dist/session/repl.d.ts +159 -0
  82. package/dist/session/repl.d.ts.map +1 -0
  83. package/dist/session/repl.js +409 -0
  84. package/dist/session/repl.js.map +1 -0
  85. package/package.json +142 -0
  86. package/src/compilers/index.ts +80 -0
  87. package/src/compilers/javascript.ts +571 -0
  88. package/src/compilers/python.ts +785 -0
  89. package/src/compilers/typescript.ts +442 -0
  90. package/src/core/cell.ts +439 -0
  91. package/src/core/compiler.ts +250 -0
  92. package/src/core/index.ts +123 -0
  93. package/src/core/limits.ts +508 -0
  94. package/src/core/permissions.ts +409 -0
  95. package/src/core/runtime.ts +499 -0
  96. package/src/core/types.ts +528 -0
  97. package/src/global.d.ts +59 -0
  98. package/src/index.ts +515 -0
  99. package/src/multi/index.ts +22 -0
  100. package/src/multi/polyglot.ts +461 -0
  101. package/src/runtimes/docker.ts +501 -0
  102. package/src/runtimes/index.ts +21 -0
  103. package/src/runtimes/process.ts +316 -0
  104. package/src/session/index.ts +41 -0
  105. package/src/session/kernel.ts +553 -0
  106. package/src/session/notebook.ts +635 -0
  107. package/src/session/repl.ts +521 -0
  108. 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";