@isol8/core 0.13.0-alpha.0

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 (57) hide show
  1. package/README.md +39 -0
  2. package/dist/client/remote.d.ts +64 -0
  3. package/dist/client/remote.d.ts.map +1 -0
  4. package/dist/config.d.ts +36 -0
  5. package/dist/config.d.ts.map +1 -0
  6. package/dist/docker/Dockerfile +42 -0
  7. package/dist/docker/proxy-handler.sh +180 -0
  8. package/dist/docker/proxy.sh +57 -0
  9. package/dist/docker/seccomp-profile.json +67 -0
  10. package/dist/engine/audit.d.ts +31 -0
  11. package/dist/engine/audit.d.ts.map +1 -0
  12. package/dist/engine/code-fetcher.d.ts +21 -0
  13. package/dist/engine/code-fetcher.d.ts.map +1 -0
  14. package/dist/engine/concurrency.d.ts +46 -0
  15. package/dist/engine/concurrency.d.ts.map +1 -0
  16. package/dist/engine/default-seccomp-profile.d.ts +8 -0
  17. package/dist/engine/default-seccomp-profile.d.ts.map +1 -0
  18. package/dist/engine/docker.d.ts +167 -0
  19. package/dist/engine/docker.d.ts.map +1 -0
  20. package/dist/engine/image-builder.d.ts +71 -0
  21. package/dist/engine/image-builder.d.ts.map +1 -0
  22. package/dist/engine/pool.d.ts +94 -0
  23. package/dist/engine/pool.d.ts.map +1 -0
  24. package/dist/engine/stats.d.ts +35 -0
  25. package/dist/engine/stats.d.ts.map +1 -0
  26. package/dist/engine/utils.d.ts +71 -0
  27. package/dist/engine/utils.d.ts.map +1 -0
  28. package/dist/index.d.ts +19 -0
  29. package/dist/index.d.ts.map +1 -0
  30. package/dist/index.js +2777 -0
  31. package/dist/index.js.map +30 -0
  32. package/dist/runtime/adapter.d.ts +63 -0
  33. package/dist/runtime/adapter.d.ts.map +1 -0
  34. package/dist/runtime/adapters/bash.d.ts +3 -0
  35. package/dist/runtime/adapters/bash.d.ts.map +1 -0
  36. package/dist/runtime/adapters/bun.d.ts +4 -0
  37. package/dist/runtime/adapters/bun.d.ts.map +1 -0
  38. package/dist/runtime/adapters/deno.d.ts +10 -0
  39. package/dist/runtime/adapters/deno.d.ts.map +1 -0
  40. package/dist/runtime/adapters/node.d.ts +4 -0
  41. package/dist/runtime/adapters/node.d.ts.map +1 -0
  42. package/dist/runtime/adapters/python.d.ts +4 -0
  43. package/dist/runtime/adapters/python.d.ts.map +1 -0
  44. package/dist/runtime/index.d.ts +15 -0
  45. package/dist/runtime/index.d.ts.map +1 -0
  46. package/dist/types.d.ts +532 -0
  47. package/dist/types.d.ts.map +1 -0
  48. package/dist/utils/logger.d.ts +32 -0
  49. package/dist/utils/logger.d.ts.map +1 -0
  50. package/dist/version.d.ts +15 -0
  51. package/dist/version.d.ts.map +1 -0
  52. package/docker/Dockerfile +42 -0
  53. package/docker/proxy-handler.sh +180 -0
  54. package/docker/proxy.sh +57 -0
  55. package/docker/seccomp-profile.json +67 -0
  56. package/package.json +48 -0
  57. package/schema/isol8.config.schema.json +315 -0
package/dist/index.js ADDED
@@ -0,0 +1,2777 @@
1
+ import { createRequire } from "node:module";
2
+ var __create = Object.create;
3
+ var __getProtoOf = Object.getPrototypeOf;
4
+ var __defProp = Object.defineProperty;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
7
+ var __toESM = (mod, isNodeMode, target) => {
8
+ target = mod != null ? __create(__getProtoOf(mod)) : {};
9
+ const to = isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target;
10
+ for (let key of __getOwnPropNames(mod))
11
+ if (!__hasOwnProp.call(to, key))
12
+ __defProp(to, key, {
13
+ get: () => mod[key],
14
+ enumerable: true
15
+ });
16
+ return to;
17
+ };
18
+ var __export = (target, all) => {
19
+ for (var name in all)
20
+ __defProp(target, name, {
21
+ get: all[name],
22
+ enumerable: true,
23
+ configurable: true,
24
+ set: (newValue) => all[name] = () => newValue
25
+ });
26
+ };
27
+ var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
28
+ var __require = /* @__PURE__ */ createRequire(import.meta.url);
29
+
30
+ // src/runtime/adapter.ts
31
+ var adapters, extensionMap, RuntimeRegistry;
32
+ var init_adapter = __esm(() => {
33
+ adapters = new Map;
34
+ extensionMap = new Map;
35
+ RuntimeRegistry = {
36
+ register(adapter, aliases = []) {
37
+ adapters.set(adapter.name, adapter);
38
+ extensionMap.set(adapter.getFileExtension(), adapter);
39
+ for (const ext of aliases) {
40
+ extensionMap.set(ext, adapter);
41
+ }
42
+ },
43
+ get(name) {
44
+ const adapter = adapters.get(name);
45
+ if (!adapter) {
46
+ throw new Error(`Unknown runtime: "${name}". Available: ${[...adapters.keys()].join(", ")}`);
47
+ }
48
+ return adapter;
49
+ },
50
+ detect(filename) {
51
+ const ext = `.${filename.split(".").pop()}`;
52
+ const adapter = extensionMap.get(ext);
53
+ if (!adapter) {
54
+ throw new Error(`Cannot detect runtime for "${filename}". Known extensions: ${[...extensionMap.keys()].join(", ")}`);
55
+ }
56
+ return adapter;
57
+ },
58
+ list() {
59
+ return [...adapters.values()];
60
+ }
61
+ };
62
+ });
63
+
64
+ // src/runtime/adapters/bash.ts
65
+ var bashAdapter;
66
+ var init_bash = __esm(() => {
67
+ bashAdapter = {
68
+ name: "bash",
69
+ image: "isol8:bash",
70
+ getCommand(code, filePath) {
71
+ if (filePath) {
72
+ return ["bash", filePath];
73
+ }
74
+ return ["bash", "-c", code];
75
+ },
76
+ getFileExtension() {
77
+ return ".sh";
78
+ }
79
+ };
80
+ });
81
+
82
+ // src/runtime/adapters/bun.ts
83
+ var BunAdapter;
84
+ var init_bun = __esm(() => {
85
+ BunAdapter = {
86
+ name: "bun",
87
+ image: "isol8:bun",
88
+ getCommand(code, filePath) {
89
+ if (filePath) {
90
+ return ["bun", "run", filePath];
91
+ }
92
+ return ["bun", "-e", code];
93
+ },
94
+ getFileExtension() {
95
+ return ".ts";
96
+ }
97
+ };
98
+ });
99
+
100
+ // src/runtime/adapters/deno.ts
101
+ var DenoAdapter;
102
+ var init_deno = __esm(() => {
103
+ DenoAdapter = {
104
+ name: "deno",
105
+ image: "isol8:deno",
106
+ getCommand(_code, filePath) {
107
+ if (!filePath) {
108
+ throw new Error("Deno adapter requires a file path — inline code is not supported.");
109
+ }
110
+ return [
111
+ "deno",
112
+ "run",
113
+ "--allow-read=/sandbox,/tmp",
114
+ "--allow-write=/sandbox,/tmp",
115
+ "--allow-env",
116
+ "--allow-net",
117
+ filePath
118
+ ];
119
+ },
120
+ getFileExtension() {
121
+ return ".mts";
122
+ }
123
+ };
124
+ });
125
+
126
+ // src/runtime/adapters/node.ts
127
+ var NodeAdapter;
128
+ var init_node = __esm(() => {
129
+ NodeAdapter = {
130
+ name: "node",
131
+ image: "isol8:node",
132
+ getCommand(code, filePath) {
133
+ if (filePath) {
134
+ return ["node", filePath];
135
+ }
136
+ return ["node", "-e", code];
137
+ },
138
+ getFileExtension() {
139
+ return ".mjs";
140
+ }
141
+ };
142
+ });
143
+
144
+ // src/runtime/adapters/python.ts
145
+ var PythonAdapter;
146
+ var init_python = __esm(() => {
147
+ PythonAdapter = {
148
+ name: "python",
149
+ image: "isol8:python",
150
+ getCommand(code, filePath) {
151
+ if (filePath) {
152
+ return ["python3", filePath];
153
+ }
154
+ return ["python3", "-c", code];
155
+ },
156
+ getFileExtension() {
157
+ return ".py";
158
+ }
159
+ };
160
+ });
161
+
162
+ // src/runtime/index.ts
163
+ var init_runtime = __esm(() => {
164
+ init_adapter();
165
+ init_bash();
166
+ init_bun();
167
+ init_deno();
168
+ init_node();
169
+ init_python();
170
+ init_adapter();
171
+ init_bash();
172
+ init_bun();
173
+ init_deno();
174
+ init_node();
175
+ init_python();
176
+ RuntimeRegistry.register(PythonAdapter);
177
+ RuntimeRegistry.register(NodeAdapter, [".js", ".cjs"]);
178
+ RuntimeRegistry.register(BunAdapter);
179
+ RuntimeRegistry.register(bashAdapter);
180
+ RuntimeRegistry.register(DenoAdapter);
181
+ });
182
+
183
+ // src/utils/logger.ts
184
+ class Logger {
185
+ debugMode = false;
186
+ setDebug(enabled) {
187
+ this.debugMode = enabled;
188
+ }
189
+ debug(...args) {
190
+ if (this.debugMode) {
191
+ console.log("[DEBUG]", ...args);
192
+ }
193
+ }
194
+ info(...args) {
195
+ console.log(...args);
196
+ }
197
+ warn(...args) {
198
+ console.warn("[WARN]", ...args);
199
+ }
200
+ error(...args) {
201
+ console.error("[ERROR]", ...args);
202
+ }
203
+ }
204
+ var logger;
205
+ var init_logger = __esm(() => {
206
+ logger = new Logger;
207
+ });
208
+
209
+ // src/engine/utils.ts
210
+ var exports_utils = {};
211
+ __export(exports_utils, {
212
+ validatePackageName: () => validatePackageName,
213
+ truncateOutput: () => truncateOutput,
214
+ parseMemoryLimit: () => parseMemoryLimit,
215
+ maskSecrets: () => maskSecrets,
216
+ extractFromTar: () => extractFromTar,
217
+ createTarBuffer: () => createTarBuffer
218
+ });
219
+ function parseMemoryLimit(limit) {
220
+ const match = limit.match(/^(\d+(?:\.\d+)?)\s*([kmgt]?)b?$/i);
221
+ if (!match) {
222
+ throw new Error(`Invalid memory limit format: "${limit}". Use e.g. "512m", "1g".`);
223
+ }
224
+ const value = Number.parseFloat(match[1]);
225
+ const unit = (match[2] || "b").toLowerCase();
226
+ const multipliers = {
227
+ b: 1,
228
+ k: 1024,
229
+ m: 1024 ** 2,
230
+ g: 1024 ** 3,
231
+ t: 1024 ** 4
232
+ };
233
+ return Math.floor(value * (multipliers[unit] ?? 1));
234
+ }
235
+ function truncateOutput(output, maxBytes) {
236
+ const encoder = new TextEncoder;
237
+ const bytes = encoder.encode(output);
238
+ if (bytes.length <= maxBytes) {
239
+ return { text: output, truncated: false };
240
+ }
241
+ const decoder = new TextDecoder("utf-8", { fatal: false });
242
+ const truncated = decoder.decode(bytes.slice(0, maxBytes));
243
+ return {
244
+ text: `${truncated}
245
+
246
+ --- OUTPUT TRUNCATED (${bytes.length} bytes, limit: ${maxBytes}) ---`,
247
+ truncated: true
248
+ };
249
+ }
250
+ function maskSecrets(text, secrets) {
251
+ let result = text;
252
+ for (const value of Object.values(secrets)) {
253
+ if (value.length > 0) {
254
+ result = result.replaceAll(value, "***");
255
+ }
256
+ }
257
+ return result;
258
+ }
259
+ function createTarBuffer(filePath, content) {
260
+ const data = typeof content === "string" ? Buffer.from(content, "utf-8") : content;
261
+ const headerSize = 512;
262
+ const dataBlocks = Math.ceil(data.length / 512);
263
+ const totalSize = headerSize + dataBlocks * 512 + 1024;
264
+ const buf = Buffer.alloc(totalSize);
265
+ buf.write(filePath.replace(/^\//, ""), 0, 100, "utf-8");
266
+ buf.write("0000644\x00", 100, 8, "utf-8");
267
+ buf.write("0000000\x00", 108, 8, "utf-8");
268
+ buf.write("0000000\x00", 116, 8, "utf-8");
269
+ buf.write(`${data.length.toString(8).padStart(11, "0")}\x00`, 124, 12, "utf-8");
270
+ buf.write(`${Math.floor(Date.now() / 1000).toString(8).padStart(11, "0")}\x00`, 136, 12, "utf-8");
271
+ buf.write("0", 156, 1, "utf-8");
272
+ buf.write("ustar\x00", 257, 6, "utf-8");
273
+ buf.write("00", 263, 2, "utf-8");
274
+ buf.write(" ", 148, 8, "utf-8");
275
+ let checksum = 0;
276
+ for (let i = 0;i < headerSize; i++) {
277
+ checksum += buf[i];
278
+ }
279
+ buf.write(`${checksum.toString(8).padStart(6, "0")}\x00 `, 148, 8, "utf-8");
280
+ data.copy(buf, headerSize);
281
+ return buf;
282
+ }
283
+ function extractFromTar(tarBuffer, targetPath) {
284
+ const normalizedTarget = targetPath.replace(/^\//, "");
285
+ const basename = targetPath.split("/").pop() ?? targetPath;
286
+ let offset = 0;
287
+ while (offset < tarBuffer.length - 512) {
288
+ const nameEnd = tarBuffer.indexOf(0, offset);
289
+ const name = tarBuffer.subarray(offset, Math.min(nameEnd, offset + 100)).toString("utf-8");
290
+ if (name.length === 0) {
291
+ break;
292
+ }
293
+ const sizeStr = tarBuffer.subarray(offset + 124, offset + 136).toString("utf-8").trim();
294
+ const size = Number.parseInt(sizeStr, 8);
295
+ if (Number.isNaN(size)) {
296
+ break;
297
+ }
298
+ const dataStart = offset + 512;
299
+ const dataBlocks = Math.ceil(size / 512);
300
+ if (name === normalizedTarget || name.endsWith(`/${normalizedTarget}`) || name === basename) {
301
+ return Buffer.from(tarBuffer.subarray(dataStart, dataStart + size));
302
+ }
303
+ offset = dataStart + dataBlocks * 512;
304
+ }
305
+ throw new Error(`File "${targetPath}" not found in tar archive`);
306
+ }
307
+ function validatePackageName(name) {
308
+ if (!/^[@a-zA-Z0-9_./\-=]+$/.test(name)) {
309
+ throw new Error(`Invalid package name: "${name}". Only alphanumeric, -, _, ., /, @, and = are allowed.`);
310
+ }
311
+ return name;
312
+ }
313
+
314
+ // src/engine/image-builder.ts
315
+ var exports_image_builder = {};
316
+ __export(exports_image_builder, {
317
+ normalizePackages: () => normalizePackages,
318
+ imageExists: () => imageExists,
319
+ getCustomImageTag: () => getCustomImageTag,
320
+ ensureImages: () => ensureImages,
321
+ buildCustomImages: () => buildCustomImages,
322
+ buildCustomImage: () => buildCustomImage,
323
+ buildBaseImages: () => buildBaseImages
324
+ });
325
+ import { createHash as createHash2 } from "node:crypto";
326
+ import { existsSync as existsSync3, readFileSync as readFileSync2, statSync as statSync2 } from "node:fs";
327
+ import { dirname, join as join3 } from "node:path";
328
+ function resolveDockerDir() {
329
+ const fromExec = join3(dirname(process.execPath), "docker");
330
+ if (existsSync3(fromExec) && statSync2(fromExec).isDirectory()) {
331
+ return fromExec;
332
+ }
333
+ if (!import.meta.url.includes("$bunfs")) {
334
+ const fromBundled = new URL("./docker", import.meta.url).pathname;
335
+ if (existsSync3(fromBundled)) {
336
+ return fromBundled;
337
+ }
338
+ }
339
+ if (!import.meta.url.includes("$bunfs")) {
340
+ const fromDev = new URL("../../docker", import.meta.url).pathname;
341
+ if (existsSync3(fromDev)) {
342
+ return fromDev;
343
+ }
344
+ }
345
+ const fromCwd = join3(process.cwd(), "docker");
346
+ if (existsSync3(fromCwd)) {
347
+ return fromCwd;
348
+ }
349
+ return new URL("../../docker", import.meta.url).pathname;
350
+ }
351
+ function computeDockerDirHash() {
352
+ const hash = createHash2("sha256");
353
+ const files = [...DOCKER_BUILD_FILES].sort();
354
+ for (const file of files) {
355
+ const filePath = join3(DOCKERFILE_DIR, file);
356
+ if (existsSync3(filePath)) {
357
+ const content = readFileSync2(filePath);
358
+ hash.update(file);
359
+ hash.update(content);
360
+ }
361
+ }
362
+ return hash.digest("hex");
363
+ }
364
+ function computeDepsHash(runtime, packages) {
365
+ const hash = createHash2("sha256");
366
+ hash.update(runtime);
367
+ for (const pkg of [...packages].sort()) {
368
+ hash.update(pkg);
369
+ }
370
+ return hash.digest("hex");
371
+ }
372
+ function normalizePackages(packages) {
373
+ return [...new Set(packages.map((pkg) => pkg.trim()).filter(Boolean))].sort();
374
+ }
375
+ function getCustomImageTag(runtime, packages) {
376
+ const normalizedPackages = normalizePackages(packages);
377
+ const depsHash = computeDepsHash(runtime, normalizedPackages);
378
+ const shortHash = depsHash.slice(0, 12);
379
+ return `isol8:${runtime}-custom-${shortHash}`;
380
+ }
381
+ async function getImageLabels(docker, imageName) {
382
+ try {
383
+ const image = docker.getImage(imageName);
384
+ const inspect = await image.inspect();
385
+ return inspect.Config?.Labels ?? {};
386
+ } catch {
387
+ return null;
388
+ }
389
+ }
390
+ async function removeImage(docker, imageId) {
391
+ try {
392
+ const image = docker.getImage(imageId);
393
+ await image.remove();
394
+ logger.debug(`[ImageBuilder] Removed old image: ${imageId.slice(0, 12)}`);
395
+ } catch (err) {
396
+ logger.debug(`[ImageBuilder] Could not remove image ${imageId.slice(0, 12)}: ${err}`);
397
+ }
398
+ }
399
+ async function buildBaseImages(docker, onProgress, force = false, onlyRuntimes) {
400
+ const allRuntimes = RuntimeRegistry.list();
401
+ const runtimes = onlyRuntimes ? allRuntimes.filter((r) => onlyRuntimes.includes(r.name)) : allRuntimes;
402
+ const dockerHash = computeDockerDirHash();
403
+ logger.debug(`[ImageBuilder] Docker directory hash: ${dockerHash.slice(0, 16)}...`);
404
+ for (const adapter of runtimes) {
405
+ const target = adapter.name;
406
+ const imageName = adapter.image;
407
+ if (!force) {
408
+ const labels = await getImageLabels(docker, imageName);
409
+ if (labels && labels[LABELS.dockerHash] === dockerHash) {
410
+ logger.debug(`[ImageBuilder] Base image ${target} is up to date, skipping build`);
411
+ onProgress?.({ runtime: target, status: "done", message: "Up to date" });
412
+ continue;
413
+ }
414
+ }
415
+ let oldImageId = null;
416
+ try {
417
+ const oldImage = await docker.getImage(imageName).inspect();
418
+ oldImageId = oldImage.Id;
419
+ logger.debug(`[ImageBuilder] Existing image ${target} ID: ${oldImageId.slice(0, 12)}`);
420
+ } catch {
421
+ logger.debug(`[ImageBuilder] No existing image for ${target}`);
422
+ }
423
+ onProgress?.({ runtime: target, status: "building" });
424
+ try {
425
+ const stream = await docker.buildImage({ context: DOCKERFILE_DIR, src: DOCKER_BUILD_FILES }, {
426
+ t: imageName,
427
+ target,
428
+ dockerfile: "Dockerfile",
429
+ labels: {
430
+ [LABELS.dockerHash]: dockerHash
431
+ }
432
+ });
433
+ await new Promise((resolve2, reject) => {
434
+ docker.modem.followProgress(stream, (err) => {
435
+ if (err) {
436
+ reject(err);
437
+ } else {
438
+ resolve2();
439
+ }
440
+ });
441
+ });
442
+ if (oldImageId) {
443
+ await removeImage(docker, oldImageId);
444
+ }
445
+ onProgress?.({ runtime: target, status: "done" });
446
+ } catch (err) {
447
+ const message = err instanceof Error ? err.message : String(err);
448
+ onProgress?.({ runtime: target, status: "error", message });
449
+ throw new Error(`Failed to build image for ${target}: ${message}`);
450
+ }
451
+ }
452
+ }
453
+ async function buildCustomImages(docker, config, onProgress, force = false) {
454
+ const deps = config.dependencies;
455
+ const python = deps.python ? normalizePackages(deps.python) : [];
456
+ const node = deps.node ? normalizePackages(deps.node) : [];
457
+ const bun = deps.bun ? normalizePackages(deps.bun) : [];
458
+ const deno = deps.deno ? normalizePackages(deps.deno) : [];
459
+ const bash = deps.bash ? normalizePackages(deps.bash) : [];
460
+ if (python.length) {
461
+ await buildCustomImage(docker, "python", python, onProgress, force);
462
+ }
463
+ if (node.length) {
464
+ await buildCustomImage(docker, "node", node, onProgress, force);
465
+ }
466
+ if (bun.length) {
467
+ await buildCustomImage(docker, "bun", bun, onProgress, force);
468
+ }
469
+ if (deno.length) {
470
+ await buildCustomImage(docker, "deno", deno, onProgress, force);
471
+ }
472
+ if (bash.length) {
473
+ await buildCustomImage(docker, "bash", bash, onProgress, force);
474
+ }
475
+ }
476
+ async function buildCustomImage(docker, runtime, packages, onProgress, force = false) {
477
+ const normalizedPackages = normalizePackages(packages);
478
+ const tag = getCustomImageTag(runtime, normalizedPackages);
479
+ const depsHash = computeDepsHash(runtime, normalizedPackages);
480
+ logger.debug(`[ImageBuilder] ${runtime} custom deps hash: ${depsHash.slice(0, 16)}...`);
481
+ if (!force) {
482
+ const labels = await getImageLabels(docker, tag);
483
+ if (labels && labels[LABELS.depsHash] === depsHash) {
484
+ logger.debug(`[ImageBuilder] Custom image ${runtime} is up to date, skipping build`);
485
+ onProgress?.({ runtime, status: "done", message: "Up to date" });
486
+ return;
487
+ }
488
+ }
489
+ let oldImageId = null;
490
+ try {
491
+ const oldImage = await docker.getImage(tag).inspect();
492
+ oldImageId = oldImage.Id;
493
+ logger.debug(`[ImageBuilder] Existing custom image ${runtime} ID: ${oldImageId.slice(0, 12)}`);
494
+ } catch {
495
+ logger.debug(`[ImageBuilder] No existing custom image for ${runtime}`);
496
+ }
497
+ onProgress?.({
498
+ runtime,
499
+ status: "building",
500
+ message: `Custom: ${normalizedPackages.join(", ")}`
501
+ });
502
+ let installCmd;
503
+ switch (runtime) {
504
+ case "python":
505
+ installCmd = `RUN pip install --no-cache-dir ${normalizedPackages.join(" ")}`;
506
+ break;
507
+ case "node":
508
+ installCmd = `RUN npm install -g ${normalizedPackages.join(" ")}`;
509
+ break;
510
+ case "bun":
511
+ installCmd = `RUN bun install -g ${normalizedPackages.join(" ")}`;
512
+ break;
513
+ case "deno":
514
+ installCmd = normalizedPackages.map((p) => `RUN deno cache ${p}`).join(`
515
+ `);
516
+ break;
517
+ case "bash":
518
+ installCmd = `RUN apk add --no-cache ${normalizedPackages.join(" ")}`;
519
+ break;
520
+ default:
521
+ throw new Error(`Unknown runtime: ${runtime}`);
522
+ }
523
+ const dockerfileContent = `FROM isol8:${runtime}
524
+ ${installCmd}
525
+ `;
526
+ const { createTarBuffer: createTarBuffer2, validatePackageName: validatePackageName2 } = await Promise.resolve().then(() => exports_utils);
527
+ const { Readable } = await import("node:stream");
528
+ normalizedPackages.forEach(validatePackageName2);
529
+ const tarBuffer = createTarBuffer2("Dockerfile", dockerfileContent);
530
+ const stream = await docker.buildImage(Readable.from(tarBuffer), {
531
+ t: tag,
532
+ dockerfile: "Dockerfile",
533
+ labels: {
534
+ [LABELS.depsHash]: depsHash
535
+ }
536
+ });
537
+ await new Promise((resolve2, reject) => {
538
+ docker.modem.followProgress(stream, (err) => {
539
+ if (err) {
540
+ reject(err);
541
+ } else {
542
+ resolve2();
543
+ }
544
+ });
545
+ });
546
+ if (oldImageId) {
547
+ await removeImage(docker, oldImageId);
548
+ }
549
+ onProgress?.({ runtime, status: "done" });
550
+ }
551
+ async function imageExists(docker, imageName) {
552
+ try {
553
+ await docker.getImage(imageName).inspect();
554
+ return true;
555
+ } catch {
556
+ return false;
557
+ }
558
+ }
559
+ async function ensureImages(docker, onProgress) {
560
+ const runtimes = RuntimeRegistry.list();
561
+ const missing = [];
562
+ for (const adapter of runtimes) {
563
+ if (!await imageExists(docker, adapter.image)) {
564
+ missing.push(adapter.name);
565
+ }
566
+ }
567
+ if (missing.length > 0) {
568
+ await buildBaseImages(docker, onProgress, false, missing);
569
+ }
570
+ }
571
+ var DOCKERFILE_DIR, LABELS, DOCKER_BUILD_FILES;
572
+ var init_image_builder = __esm(() => {
573
+ init_runtime();
574
+ init_logger();
575
+ DOCKERFILE_DIR = resolveDockerDir();
576
+ LABELS = {
577
+ dockerHash: "org.isol8.build.hash",
578
+ depsHash: "org.isol8.deps.hash"
579
+ };
580
+ DOCKER_BUILD_FILES = ["Dockerfile", "proxy.sh", "proxy-handler.sh"];
581
+ });
582
+
583
+ // src/client/remote.ts
584
+ class RemoteIsol8 {
585
+ host;
586
+ apiKey;
587
+ sessionId;
588
+ isol8Options;
589
+ constructor(options, isol8Options) {
590
+ this.host = options.host.replace(/\/$/, "");
591
+ this.apiKey = options.apiKey;
592
+ this.sessionId = options.sessionId;
593
+ this.isol8Options = isol8Options;
594
+ }
595
+ async start(_options) {
596
+ const res = await this.fetch("/health");
597
+ if (!res.ok) {
598
+ throw new Error(`Remote server health check failed: ${res.status}`);
599
+ }
600
+ }
601
+ async stop() {
602
+ if (this.sessionId) {
603
+ await this.fetch(`/session/${this.sessionId}`, { method: "DELETE" });
604
+ }
605
+ }
606
+ async execute(req) {
607
+ const res = await this.fetch("/execute", {
608
+ method: "POST",
609
+ body: JSON.stringify({
610
+ request: req,
611
+ options: this.isol8Options,
612
+ sessionId: this.sessionId
613
+ })
614
+ });
615
+ if (!res.ok) {
616
+ const body = await res.json().catch(() => ({}));
617
+ throw new Error(`Execution failed: ${body.error ?? res.statusText}`);
618
+ }
619
+ return res.json();
620
+ }
621
+ async* executeStream(req) {
622
+ const res = await this.fetch("/execute/stream", {
623
+ method: "POST",
624
+ body: JSON.stringify({
625
+ request: req,
626
+ options: this.isol8Options,
627
+ sessionId: this.sessionId
628
+ })
629
+ });
630
+ if (!res.ok) {
631
+ const body = await res.json().catch(() => ({}));
632
+ throw new Error(`Stream failed: ${body.error ?? res.statusText}`);
633
+ }
634
+ if (!res.body) {
635
+ throw new Error("No response body for streaming");
636
+ }
637
+ const reader = res.body.getReader();
638
+ const decoder = new TextDecoder;
639
+ let buffer = "";
640
+ try {
641
+ while (true) {
642
+ const { done, value } = await reader.read();
643
+ if (done) {
644
+ break;
645
+ }
646
+ buffer += decoder.decode(value, { stream: true });
647
+ const lines = buffer.split(`
648
+ `);
649
+ buffer = lines.pop() ?? "";
650
+ for (const line of lines) {
651
+ if (line.startsWith("data: ")) {
652
+ const json = line.slice(6).trim();
653
+ if (json) {
654
+ yield JSON.parse(json);
655
+ }
656
+ }
657
+ }
658
+ }
659
+ if (buffer.startsWith("data: ")) {
660
+ const json = buffer.slice(6).trim();
661
+ if (json) {
662
+ yield JSON.parse(json);
663
+ }
664
+ }
665
+ } finally {
666
+ reader.releaseLock();
667
+ }
668
+ }
669
+ async putFile(path, content) {
670
+ if (!this.sessionId) {
671
+ throw new Error("File operations require a sessionId (persistent mode)");
672
+ }
673
+ const base64 = typeof content === "string" ? Buffer.from(content).toString("base64") : content.toString("base64");
674
+ const res = await this.fetch("/file", {
675
+ method: "POST",
676
+ body: JSON.stringify({
677
+ sessionId: this.sessionId,
678
+ path,
679
+ content: base64
680
+ })
681
+ });
682
+ if (!res.ok) {
683
+ throw new Error(`File upload failed: ${res.statusText}`);
684
+ }
685
+ }
686
+ async getFile(path) {
687
+ if (!this.sessionId) {
688
+ throw new Error("File operations require a sessionId (persistent mode)");
689
+ }
690
+ const params = new URLSearchParams({ sessionId: this.sessionId, path });
691
+ const res = await this.fetch(`/file?${params}`);
692
+ if (!res.ok) {
693
+ throw new Error(`File download failed: ${res.statusText}`);
694
+ }
695
+ const body = await res.json();
696
+ return Buffer.from(body.content, "base64");
697
+ }
698
+ async fetch(path, init) {
699
+ return globalThis.fetch(`${this.host}${path}`, {
700
+ ...init,
701
+ headers: {
702
+ "Content-Type": "application/json",
703
+ Authorization: `Bearer ${this.apiKey}`,
704
+ ...init?.headers ?? {}
705
+ }
706
+ });
707
+ }
708
+ }
709
+ // src/config.ts
710
+ import { existsSync, readFileSync } from "node:fs";
711
+ import { homedir } from "node:os";
712
+ import { join, resolve } from "node:path";
713
+ var DEFAULT_CONFIG = {
714
+ maxConcurrent: 10,
715
+ defaults: {
716
+ timeoutMs: 30000,
717
+ memoryLimit: "512m",
718
+ cpuLimit: 1,
719
+ network: "none",
720
+ sandboxSize: "512m",
721
+ tmpSize: "256m",
722
+ readonlyRootFs: true
723
+ },
724
+ network: {
725
+ whitelist: [],
726
+ blacklist: []
727
+ },
728
+ cleanup: {
729
+ autoPrune: true,
730
+ maxContainerAgeMs: 3600000
731
+ },
732
+ poolStrategy: "fast",
733
+ poolSize: { clean: 1, dirty: 1 },
734
+ dependencies: {},
735
+ security: {
736
+ seccomp: "strict"
737
+ },
738
+ remoteCode: {
739
+ enabled: false,
740
+ allowedSchemes: ["https"],
741
+ allowedHosts: [],
742
+ blockedHosts: [
743
+ "^localhost$",
744
+ "^127(?:\\.[0-9]{1,3}){3}$",
745
+ "^\\[::1\\]$",
746
+ "^::1$",
747
+ "^10(?:\\.[0-9]{1,3}){3}$",
748
+ "^172\\.(?:1[6-9]|2[0-9]|3[0-1])(?:\\.[0-9]{1,3}){2}$",
749
+ "^192\\.168(?:\\.[0-9]{1,3}){2}$",
750
+ "^169\\.254(?:\\.[0-9]{1,3}){2}$",
751
+ "^metadata\\.google\\.internal$",
752
+ "^169\\.254\\.169\\.254$"
753
+ ],
754
+ maxCodeSize: 10 * 1024 * 1024,
755
+ fetchTimeoutMs: 30000,
756
+ requireHash: false,
757
+ enableCache: true,
758
+ cacheTtl: 3600
759
+ },
760
+ audit: {
761
+ enabled: false,
762
+ destination: "filesystem",
763
+ logDir: undefined,
764
+ postLogScript: undefined,
765
+ trackResources: true,
766
+ retentionDays: 90,
767
+ includeCode: false,
768
+ includeOutput: false
769
+ },
770
+ debug: false
771
+ };
772
+ function loadConfig(cwd) {
773
+ const searchPaths = [
774
+ join(resolve(cwd ?? process.cwd()), "isol8.config.json"),
775
+ join(homedir(), ".isol8", "config.json")
776
+ ];
777
+ for (const configPath of searchPaths) {
778
+ if (existsSync(configPath)) {
779
+ const raw = readFileSync(configPath, "utf-8");
780
+ const parsed = JSON.parse(raw);
781
+ return mergeConfig(DEFAULT_CONFIG, parsed);
782
+ }
783
+ }
784
+ return { ...DEFAULT_CONFIG };
785
+ }
786
+ function mergeConfig(defaults, overrides) {
787
+ return {
788
+ maxConcurrent: overrides.maxConcurrent ?? defaults.maxConcurrent,
789
+ defaults: {
790
+ ...defaults.defaults,
791
+ ...overrides.defaults,
792
+ readonlyRootFs: overrides.defaults?.readonlyRootFs ?? defaults.defaults.readonlyRootFs
793
+ },
794
+ network: {
795
+ whitelist: overrides.network?.whitelist ?? defaults.network.whitelist,
796
+ blacklist: overrides.network?.blacklist ?? defaults.network.blacklist
797
+ },
798
+ cleanup: {
799
+ ...defaults.cleanup,
800
+ ...overrides.cleanup
801
+ },
802
+ poolStrategy: overrides.poolStrategy ?? defaults.poolStrategy,
803
+ poolSize: overrides.poolSize ?? defaults.poolSize,
804
+ dependencies: {
805
+ ...defaults.dependencies,
806
+ ...overrides.dependencies
807
+ },
808
+ security: {
809
+ seccomp: overrides.security?.seccomp ?? defaults.security.seccomp,
810
+ customProfilePath: overrides.security?.customProfilePath ?? defaults.security.customProfilePath
811
+ },
812
+ remoteCode: {
813
+ ...defaults.remoteCode,
814
+ ...overrides.remoteCode,
815
+ allowedSchemes: overrides.remoteCode?.allowedSchemes ?? defaults.remoteCode.allowedSchemes,
816
+ allowedHosts: overrides.remoteCode?.allowedHosts ?? defaults.remoteCode.allowedHosts,
817
+ blockedHosts: overrides.remoteCode?.blockedHosts ?? defaults.remoteCode.blockedHosts
818
+ },
819
+ audit: {
820
+ ...defaults.audit,
821
+ ...overrides.audit
822
+ },
823
+ debug: overrides.debug ?? defaults.debug
824
+ };
825
+ }
826
+ // src/engine/concurrency.ts
827
+ class Semaphore {
828
+ max;
829
+ current = 0;
830
+ queue = [];
831
+ constructor(max) {
832
+ this.max = max;
833
+ if (max < 1) {
834
+ throw new Error("Semaphore max must be >= 1");
835
+ }
836
+ }
837
+ get available() {
838
+ return this.max - this.current;
839
+ }
840
+ get pending() {
841
+ return this.queue.length;
842
+ }
843
+ async acquire() {
844
+ if (this.current < this.max) {
845
+ this.current++;
846
+ return;
847
+ }
848
+ return new Promise((resolve2) => {
849
+ this.queue.push(resolve2);
850
+ });
851
+ }
852
+ release() {
853
+ const next = this.queue.shift();
854
+ if (next) {
855
+ next();
856
+ } else {
857
+ this.current--;
858
+ }
859
+ }
860
+ }
861
+ // src/engine/docker.ts
862
+ init_runtime();
863
+ init_logger();
864
+ import { randomUUID } from "node:crypto";
865
+ import { existsSync as existsSync4, readFileSync as readFileSync3 } from "node:fs";
866
+ import { PassThrough } from "node:stream";
867
+ import Docker from "dockerode";
868
+
869
+ // src/engine/audit.ts
870
+ init_logger();
871
+ import { spawn } from "node:child_process";
872
+ import { appendFileSync, existsSync as existsSync2, mkdirSync, readdirSync, statSync, unlinkSync } from "node:fs";
873
+ import { join as join2 } from "node:path";
874
+
875
+ class AuditLogger {
876
+ config;
877
+ auditFile;
878
+ constructor(config) {
879
+ this.config = config;
880
+ const auditDir = config.logDir ?? process.env.ISOL8_AUDIT_DIR ?? join2(process.cwd(), "./.isol8_audit");
881
+ this.auditFile = join2(auditDir, "executions.log");
882
+ if (!existsSync2(auditDir)) {
883
+ try {
884
+ mkdirSync(auditDir, { recursive: true });
885
+ } catch (err) {
886
+ logger.error("Failed to create audit dir:", err);
887
+ }
888
+ }
889
+ this.cleanupOldLogs();
890
+ }
891
+ cleanupOldLogs() {
892
+ if (!this.config.enabled || this.config.retentionDays <= 0) {
893
+ return;
894
+ }
895
+ try {
896
+ const auditDir = join2(this.auditFile, "..");
897
+ if (!existsSync2(auditDir)) {
898
+ return;
899
+ }
900
+ const cutoffTime = Date.now() - this.config.retentionDays * 24 * 60 * 60 * 1000;
901
+ const files = readdirSync(auditDir);
902
+ let cleanedCount = 0;
903
+ for (const file of files) {
904
+ if (file.endsWith(".log") || file.endsWith(".jsonl")) {
905
+ const filePath = join2(auditDir, file);
906
+ try {
907
+ const stats = statSync(filePath);
908
+ if (stats.mtimeMs < cutoffTime) {
909
+ unlinkSync(filePath);
910
+ cleanedCount++;
911
+ logger.debug(`Cleaned up old audit log: ${file}`);
912
+ }
913
+ } catch (err) {
914
+ logger.debug(`Failed to check/remove old log file ${file}:`, err);
915
+ }
916
+ }
917
+ }
918
+ if (cleanedCount > 0) {
919
+ logger.info(`Audit log cleanup: removed ${cleanedCount} old log files`);
920
+ }
921
+ } catch (err) {
922
+ logger.error("Failed to cleanup old audit logs:", err);
923
+ }
924
+ }
925
+ record(audit) {
926
+ if (!this.config.enabled) {
927
+ return;
928
+ }
929
+ try {
930
+ const filteredAudit = this.filterAuditData(audit);
931
+ const line = `${JSON.stringify(filteredAudit)}
932
+ `;
933
+ switch (this.config.destination) {
934
+ case "file":
935
+ case "filesystem":
936
+ appendFileSync(this.auditFile, line, { encoding: "utf-8" });
937
+ break;
938
+ case "stdout":
939
+ console.log("AUDIT_LOG:", filteredAudit);
940
+ break;
941
+ default:
942
+ logger.error(`Unsupported audit destination: ${this.config.destination}`);
943
+ return;
944
+ }
945
+ logger.debug("Audit record written:", audit.executionId);
946
+ if (this.config.postLogScript) {
947
+ this.runPostLogScript();
948
+ }
949
+ } catch (err) {
950
+ logger.error("Failed to write audit record:", err);
951
+ }
952
+ }
953
+ runPostLogScript() {
954
+ if (!this.config.postLogScript) {
955
+ return;
956
+ }
957
+ try {
958
+ const child = spawn(this.config.postLogScript, [this.auditFile], {
959
+ detached: true,
960
+ stdio: "ignore"
961
+ });
962
+ child.on("error", (err) => {
963
+ logger.error("Failed to run post-log script:", err);
964
+ });
965
+ child.unref();
966
+ } catch (err) {
967
+ logger.error("Failed to spawn post-log script:", err);
968
+ }
969
+ }
970
+ filterAuditData(audit) {
971
+ const result = {
972
+ executionId: audit.executionId,
973
+ userId: audit.userId,
974
+ timestamp: audit.timestamp,
975
+ runtime: audit.runtime,
976
+ codeHash: audit.codeHash,
977
+ containerId: audit.containerId,
978
+ exitCode: audit.exitCode,
979
+ durationMs: audit.durationMs
980
+ };
981
+ if (audit.resourceUsage !== undefined) {
982
+ result.resourceUsage = audit.resourceUsage;
983
+ }
984
+ if (audit.securityEvents !== undefined) {
985
+ result.securityEvents = audit.securityEvents;
986
+ }
987
+ if (audit.metadata !== undefined) {
988
+ result.metadata = audit.metadata;
989
+ }
990
+ if (this.config.includeCode && audit.code !== undefined) {
991
+ result.code = audit.code;
992
+ }
993
+ if (this.config.includeOutput) {
994
+ if (audit.stdout !== undefined) {
995
+ result.stdout = audit.stdout;
996
+ }
997
+ if (audit.stderr !== undefined) {
998
+ result.stderr = audit.stderr;
999
+ }
1000
+ }
1001
+ return result;
1002
+ }
1003
+ }
1004
+
1005
+ // src/engine/code-fetcher.ts
1006
+ import { createHash } from "node:crypto";
1007
+ import { lookup as dnsLookup } from "node:dns/promises";
1008
+ import { isIP } from "node:net";
1009
+ var IPV4_SEPARATOR = ".";
1010
+ var IPV6_LOOPBACK = "::1";
1011
+ function sha256Hex(input) {
1012
+ return createHash("sha256").update(input, "utf-8").digest("hex");
1013
+ }
1014
+ function normalizeScheme(url) {
1015
+ return url.protocol.replace(/:$/, "").toLowerCase();
1016
+ }
1017
+ function isBlockedByPattern(host, patterns) {
1018
+ return patterns.some((pattern) => new RegExp(pattern, "i").test(host));
1019
+ }
1020
+ function isAllowedByPattern(host, patterns) {
1021
+ if (patterns.length === 0) {
1022
+ return true;
1023
+ }
1024
+ return patterns.some((pattern) => new RegExp(pattern, "i").test(host));
1025
+ }
1026
+ function isPrivateIpv4(ip) {
1027
+ const parts = ip.split(IPV4_SEPARATOR).map((v) => Number.parseInt(v, 10));
1028
+ if (parts.length !== 4 || parts.some((p) => Number.isNaN(p))) {
1029
+ return false;
1030
+ }
1031
+ const a = parts[0];
1032
+ const b = parts[1];
1033
+ if (a === 10 || a === 127 || a === 0) {
1034
+ return true;
1035
+ }
1036
+ if (a === 169 && b === 254) {
1037
+ return true;
1038
+ }
1039
+ if (a === 172 && b >= 16 && b <= 31) {
1040
+ return true;
1041
+ }
1042
+ if (a === 192 && b === 168) {
1043
+ return true;
1044
+ }
1045
+ if (a === 100 && b >= 64 && b <= 127) {
1046
+ return true;
1047
+ }
1048
+ return false;
1049
+ }
1050
+ function isPrivateIpv6(ip) {
1051
+ const normalized = ip.toLowerCase();
1052
+ if (normalized === IPV6_LOOPBACK) {
1053
+ return true;
1054
+ }
1055
+ return normalized.startsWith("fc") || normalized.startsWith("fd") || normalized.startsWith("fe8") || normalized.startsWith("fe9") || normalized.startsWith("fea") || normalized.startsWith("feb");
1056
+ }
1057
+ function isPrivateIp(ip) {
1058
+ const family = isIP(ip);
1059
+ if (family === 4) {
1060
+ return isPrivateIpv4(ip);
1061
+ }
1062
+ if (family === 6) {
1063
+ return isPrivateIpv6(ip);
1064
+ }
1065
+ return false;
1066
+ }
1067
+ async function assertHostResolvesPublic(host, lookupFn) {
1068
+ if (isIP(host) && isPrivateIp(host)) {
1069
+ throw new Error(`Blocked code URL host: ${host}`);
1070
+ }
1071
+ try {
1072
+ const records = await lookupFn(host);
1073
+ for (const record of records) {
1074
+ if (isPrivateIp(record.address)) {
1075
+ throw new Error(`Blocked code URL host: ${host}`);
1076
+ }
1077
+ }
1078
+ } catch (err) {
1079
+ if (err instanceof Error && err.message.startsWith("Blocked code URL host:")) {
1080
+ throw err;
1081
+ }
1082
+ throw new Error(`Failed to resolve code URL host: ${host}`);
1083
+ }
1084
+ }
1085
+ function decodeUtf8(content) {
1086
+ const decoder = new TextDecoder("utf-8", { fatal: true });
1087
+ const text = decoder.decode(content);
1088
+ if (text.includes("\x00")) {
1089
+ throw new Error("Fetched code appears to be binary content");
1090
+ }
1091
+ return text;
1092
+ }
1093
+ async function fetchRemoteCode(request, policy, deps = {}) {
1094
+ if (!policy.enabled) {
1095
+ throw new Error("Remote code fetching is disabled. Set remoteCode.enabled=true to allow it.");
1096
+ }
1097
+ const fetchFn = deps.fetchFn ?? globalThis.fetch;
1098
+ const lookupFn = deps.lookupFn ?? (async (hostname) => {
1099
+ const records = await dnsLookup(hostname, { all: true, verbatim: true });
1100
+ return records;
1101
+ });
1102
+ if (!request.codeUrl) {
1103
+ throw new Error("codeUrl is required for remote code fetching");
1104
+ }
1105
+ const url = new URL(request.codeUrl);
1106
+ const scheme = normalizeScheme(url);
1107
+ if (scheme === "http" && !request.allowInsecureCodeUrl) {
1108
+ throw new Error("Insecure code URL blocked. Use allowInsecureCodeUrl=true to allow HTTP.");
1109
+ }
1110
+ if (!policy.allowedSchemes.map((s) => s.toLowerCase()).includes(scheme)) {
1111
+ throw new Error(`URL scheme not allowed: ${scheme}`);
1112
+ }
1113
+ const host = url.hostname.toLowerCase();
1114
+ if (!isAllowedByPattern(host, policy.allowedHosts) || isBlockedByPattern(host, policy.blockedHosts)) {
1115
+ throw new Error(`Blocked code URL host: ${host}`);
1116
+ }
1117
+ await assertHostResolvesPublic(host, lookupFn);
1118
+ if (policy.requireHash && !request.codeHash) {
1119
+ throw new Error("Hash verification required: provide codeHash for remote code execution.");
1120
+ }
1121
+ const controller = new AbortController;
1122
+ const timeout = setTimeout(() => controller.abort(), policy.fetchTimeoutMs);
1123
+ let response;
1124
+ try {
1125
+ response = await fetchFn(url.toString(), {
1126
+ method: "GET",
1127
+ redirect: "follow",
1128
+ signal: controller.signal
1129
+ });
1130
+ } catch (err) {
1131
+ throw new Error(err instanceof Error && err.name === "AbortError" ? `Remote code fetch timed out after ${policy.fetchTimeoutMs}ms` : `Failed to fetch remote code: ${err instanceof Error ? err.message : String(err)}`);
1132
+ } finally {
1133
+ clearTimeout(timeout);
1134
+ }
1135
+ if (!response.ok) {
1136
+ throw new Error(`Failed to fetch remote code: HTTP ${response.status}`);
1137
+ }
1138
+ const contentLengthHeader = response.headers.get("content-length");
1139
+ if (contentLengthHeader) {
1140
+ const parsedLength = Number.parseInt(contentLengthHeader, 10);
1141
+ if (!Number.isNaN(parsedLength) && parsedLength > policy.maxCodeSize) {
1142
+ throw new Error(`Remote code exceeds maxCodeSize (${policy.maxCodeSize} bytes): ${parsedLength} bytes`);
1143
+ }
1144
+ }
1145
+ if (!response.body) {
1146
+ throw new Error("Remote code response body is empty");
1147
+ }
1148
+ const reader = response.body.getReader();
1149
+ const chunks = [];
1150
+ let totalBytes = 0;
1151
+ while (true) {
1152
+ const { done, value } = await reader.read();
1153
+ if (done) {
1154
+ break;
1155
+ }
1156
+ if (!value) {
1157
+ continue;
1158
+ }
1159
+ totalBytes += value.byteLength;
1160
+ if (totalBytes > policy.maxCodeSize) {
1161
+ throw new Error(`Remote code exceeds maxCodeSize (${policy.maxCodeSize} bytes)`);
1162
+ }
1163
+ chunks.push(value);
1164
+ }
1165
+ const buffer = new Uint8Array(totalBytes);
1166
+ let offset = 0;
1167
+ for (const chunk of chunks) {
1168
+ buffer.set(chunk, offset);
1169
+ offset += chunk.byteLength;
1170
+ }
1171
+ const code = decodeUtf8(buffer);
1172
+ const hash = sha256Hex(code);
1173
+ if (request.codeHash && hash.toLowerCase() !== request.codeHash.toLowerCase()) {
1174
+ throw new Error("Remote code hash mismatch");
1175
+ }
1176
+ return { code, url: url.toString(), hash };
1177
+ }
1178
+
1179
+ // src/engine/default-seccomp-profile.ts
1180
+ var EMBEDDED_DEFAULT_SECCOMP_PROFILE = JSON.stringify({
1181
+ defaultAction: "SCMP_ACT_ALLOW",
1182
+ architectures: ["SCMP_ARCH_X86_64", "SCMP_ARCH_X86", "SCMP_ARCH_X32", "SCMP_ARCH_AARCH64"],
1183
+ syscalls: [
1184
+ {
1185
+ names: [
1186
+ "acct",
1187
+ "add_key",
1188
+ "bpf",
1189
+ "clock_adjtime",
1190
+ "clock_settime",
1191
+ "create_module",
1192
+ "delete_module",
1193
+ "finit_module",
1194
+ "get_mempolicy",
1195
+ "init_module",
1196
+ "ioperm",
1197
+ "iopl",
1198
+ "kcmp",
1199
+ "kexec_file_load",
1200
+ "kexec_load",
1201
+ "keyctl",
1202
+ "lookup_dcookie",
1203
+ "mbind",
1204
+ "mount",
1205
+ "move_pages",
1206
+ "name_to_handle_at",
1207
+ "open_by_handle_at",
1208
+ "perf_event_open",
1209
+ "pivot_root",
1210
+ "process_vm_readv",
1211
+ "process_vm_writev",
1212
+ "ptrace",
1213
+ "query_module",
1214
+ "quotactl",
1215
+ "reboot",
1216
+ "request_key",
1217
+ "set_mempolicy",
1218
+ "setns",
1219
+ "settimeofday",
1220
+ "stime",
1221
+ "swapon",
1222
+ "swapoff",
1223
+ "sysfs",
1224
+ "syslog",
1225
+ "umount",
1226
+ "umount2",
1227
+ "unshare",
1228
+ "uselib",
1229
+ "userfaultfd",
1230
+ "ustat",
1231
+ "vm86",
1232
+ "vm86old"
1233
+ ],
1234
+ action: "SCMP_ACT_ERRNO",
1235
+ args: [],
1236
+ comment: "",
1237
+ includes: {},
1238
+ excludes: {}
1239
+ }
1240
+ ]
1241
+ });
1242
+
1243
+ // src/engine/docker.ts
1244
+ init_image_builder();
1245
+
1246
+ // src/engine/pool.ts
1247
+ init_logger();
1248
+
1249
+ class ContainerPool {
1250
+ docker;
1251
+ poolStrategy;
1252
+ cleanPoolSize;
1253
+ dirtyPoolSize;
1254
+ createOptions;
1255
+ networkMode;
1256
+ securityMode;
1257
+ pools = new Map;
1258
+ replenishing = new Set;
1259
+ pendingReplenishments = new Set;
1260
+ cleaningInterval = null;
1261
+ constructor(options) {
1262
+ this.docker = options.docker;
1263
+ this.poolStrategy = options.poolStrategy ?? "fast";
1264
+ this.createOptions = options.createOptions;
1265
+ this.networkMode = options.networkMode;
1266
+ this.securityMode = options.securityMode;
1267
+ if (typeof options.poolSize === "number") {
1268
+ this.cleanPoolSize = options.poolSize;
1269
+ this.dirtyPoolSize = options.poolSize;
1270
+ } else if (options.poolSize) {
1271
+ this.cleanPoolSize = options.poolSize.clean ?? 1;
1272
+ this.dirtyPoolSize = options.poolSize.dirty ?? 1;
1273
+ } else {
1274
+ this.cleanPoolSize = 1;
1275
+ this.dirtyPoolSize = 1;
1276
+ }
1277
+ if (this.poolStrategy === "fast") {
1278
+ this.startBackgroundCleaning();
1279
+ }
1280
+ }
1281
+ async acquire(image) {
1282
+ const pool = this.pools.get(image) ?? { clean: [], dirty: [] };
1283
+ if (this.poolStrategy === "fast") {
1284
+ if (pool.clean.length > 0) {
1285
+ const entry = pool.clean.shift();
1286
+ this.pools.set(image, pool);
1287
+ this.replenish(image);
1288
+ return entry.container;
1289
+ }
1290
+ if (pool.dirty.length > 0 && pool.clean.length < this.cleanPoolSize) {
1291
+ await this.cleanDirtyImmediate(image);
1292
+ const updatedPool = this.pools.get(image);
1293
+ if (updatedPool && updatedPool.clean.length > 0) {
1294
+ const entry = updatedPool.clean.shift();
1295
+ this.pools.set(image, updatedPool);
1296
+ this.replenish(image);
1297
+ return entry.container;
1298
+ }
1299
+ }
1300
+ return this.createContainer(image);
1301
+ }
1302
+ if (pool.clean && pool.clean.length > 0) {
1303
+ const entry = pool.clean.shift();
1304
+ this.pools.set(image, { clean: pool.clean, dirty: [] });
1305
+ await this.cleanupContainer(entry.container);
1306
+ this.replenish(image);
1307
+ return entry.container;
1308
+ }
1309
+ return this.createContainer(image);
1310
+ }
1311
+ async release(container, image) {
1312
+ let pool = this.pools.get(image);
1313
+ if (!pool) {
1314
+ pool = { clean: [], dirty: [] };
1315
+ this.pools.set(image, pool);
1316
+ }
1317
+ if (this.poolStrategy === "fast") {
1318
+ if (pool.dirty.length >= this.dirtyPoolSize) {
1319
+ await container.remove({ force: true }).catch(() => {});
1320
+ return;
1321
+ }
1322
+ pool.dirty.push({ container, createdAt: Date.now() });
1323
+ } else {
1324
+ if (pool.clean.length >= this.cleanPoolSize) {
1325
+ await container.remove({ force: true }).catch(() => {});
1326
+ return;
1327
+ }
1328
+ if (!pool.clean) {
1329
+ pool.clean = [];
1330
+ }
1331
+ pool.clean.push({ container, createdAt: Date.now() });
1332
+ }
1333
+ }
1334
+ startBackgroundCleaning() {
1335
+ this.cleaningInterval = setInterval(async () => {
1336
+ for (const [_image, pool] of this.pools) {
1337
+ for (let i = 0;i < this.dirtyPoolSize; i++) {
1338
+ if (pool.dirty.length > 0 && pool.clean.length < this.cleanPoolSize) {
1339
+ const entry = pool.dirty.shift();
1340
+ try {
1341
+ await this.cleanupContainer(entry.container);
1342
+ pool.clean.push(entry);
1343
+ } catch {
1344
+ entry.container.remove({ force: true }).catch(() => {});
1345
+ }
1346
+ }
1347
+ }
1348
+ }
1349
+ }, 5000);
1350
+ }
1351
+ async cleanDirtyImmediate(image) {
1352
+ const pool = this.pools.get(image);
1353
+ if (!pool || pool.dirty.length === 0 || pool.clean.length >= this.cleanPoolSize) {
1354
+ return;
1355
+ }
1356
+ const entry = pool.dirty.shift();
1357
+ try {
1358
+ await this.cleanupContainer(entry.container);
1359
+ pool.clean.push(entry);
1360
+ } catch {
1361
+ entry.container.remove({ force: true }).catch(() => {});
1362
+ }
1363
+ }
1364
+ async cleanupContainer(container) {
1365
+ const needsCleanup = this.securityMode === "strict";
1366
+ const needsIptables = this.networkMode === "filtered" && needsCleanup;
1367
+ if (!needsCleanup) {
1368
+ return;
1369
+ }
1370
+ try {
1371
+ const cleanupCmd = needsIptables ? "pkill -9 -u sandbox 2>/dev/null; /usr/sbin/iptables -F OUTPUT 2>/dev/null; rm -rf /sandbox/* /sandbox/.[!.]* 2>/dev/null; true" : "pkill -9 -u sandbox 2>/dev/null; rm -rf /sandbox/* /sandbox/.[!.]* 2>/dev/null; true";
1372
+ const cleanExec = await container.exec({
1373
+ Cmd: ["sh", "-c", cleanupCmd]
1374
+ });
1375
+ await cleanExec.start({ Detach: true });
1376
+ let info = await cleanExec.inspect();
1377
+ while (info.Running) {
1378
+ await new Promise((r) => setTimeout(r, 5));
1379
+ info = await cleanExec.inspect();
1380
+ }
1381
+ } catch {}
1382
+ }
1383
+ async warm(image) {
1384
+ const pool = this.pools.get(image) ?? { clean: [], dirty: [] };
1385
+ this.pools.set(image, pool);
1386
+ const needed = this.poolStrategy === "fast" ? this.cleanPoolSize - pool.clean.length : this.cleanPoolSize - (pool.clean?.length ?? 0);
1387
+ if (needed <= 0) {
1388
+ return;
1389
+ }
1390
+ const promises = [];
1391
+ for (let i = 0;i < needed; i++) {
1392
+ promises.push(this.createContainer(image).then((container) => {
1393
+ if (this.poolStrategy === "fast") {
1394
+ pool.clean.push({ container, createdAt: Date.now() });
1395
+ } else {
1396
+ if (!pool.clean) {
1397
+ pool.clean = [];
1398
+ }
1399
+ pool.clean.push({ container, createdAt: Date.now() });
1400
+ }
1401
+ }));
1402
+ }
1403
+ await Promise.all(promises);
1404
+ }
1405
+ async stop() {
1406
+ return this.drain();
1407
+ }
1408
+ async drain() {
1409
+ if (this.cleaningInterval) {
1410
+ clearInterval(this.cleaningInterval);
1411
+ this.cleaningInterval = null;
1412
+ }
1413
+ await Promise.all(this.pendingReplenishments);
1414
+ const promises = [];
1415
+ for (const [, pool] of this.pools) {
1416
+ for (const entry of pool.clean ?? []) {
1417
+ promises.push(entry.container.remove({ force: true }).catch(() => {}));
1418
+ }
1419
+ for (const entry of pool.dirty) {
1420
+ promises.push(entry.container.remove({ force: true }).catch(() => {}));
1421
+ }
1422
+ }
1423
+ await Promise.all(promises);
1424
+ this.pools.clear();
1425
+ }
1426
+ async createContainer(image) {
1427
+ const container = await this.docker.createContainer({
1428
+ ...this.createOptions,
1429
+ Image: image
1430
+ });
1431
+ logger.debug(`[Pool] Container ${container.id} created for image: ${image}`);
1432
+ await container.start();
1433
+ logger.debug(`[Pool] Container ${container.id} started`);
1434
+ return container;
1435
+ }
1436
+ replenish(image) {
1437
+ if (this.replenishing.has(image)) {
1438
+ return;
1439
+ }
1440
+ const pool = this.pools.get(image);
1441
+ const currentSize = pool ? this.poolStrategy === "fast" ? pool.clean.length : pool.clean?.length ?? 0 : 0;
1442
+ const targetSize = this.cleanPoolSize;
1443
+ if (currentSize >= targetSize) {
1444
+ return;
1445
+ }
1446
+ this.replenishing.add(image);
1447
+ const promise = this.createContainer(image).then((container) => {
1448
+ const p = this.pools.get(image);
1449
+ if (!p) {
1450
+ container.remove({ force: true }).catch(() => {});
1451
+ return;
1452
+ }
1453
+ if (this.poolStrategy === "fast") {
1454
+ if (p.clean.length < this.cleanPoolSize) {
1455
+ p.clean.push({ container, createdAt: Date.now() });
1456
+ } else {
1457
+ container.remove({ force: true }).catch(() => {});
1458
+ }
1459
+ } else {
1460
+ if (!p.clean) {
1461
+ p.clean = [];
1462
+ }
1463
+ if (p.clean.length < this.cleanPoolSize) {
1464
+ p.clean.push({ container, createdAt: Date.now() });
1465
+ } else {
1466
+ container.remove({ force: true }).catch(() => {});
1467
+ }
1468
+ }
1469
+ }).catch((err) => {
1470
+ logger.error(`[Pool] Error during replenishment for ${image}:`, err);
1471
+ }).finally(() => {
1472
+ this.replenishing.delete(image);
1473
+ this.pendingReplenishments.delete(promise);
1474
+ });
1475
+ this.pendingReplenishments.add(promise);
1476
+ }
1477
+ }
1478
+
1479
+ // src/engine/stats.ts
1480
+ function calculateCPUPercent(stats) {
1481
+ const cpuDelta = stats.cpu_stats.cpu_usage.total_usage - stats.precpu_stats.cpu_usage.total_usage;
1482
+ const systemDelta = stats.cpu_stats.system_cpu_usage - stats.precpu_stats.system_cpu_usage;
1483
+ if (systemDelta === 0 || cpuDelta === 0) {
1484
+ return 0;
1485
+ }
1486
+ const numCores = stats.cpu_stats.online_cpus ?? stats.cpu_stats.cpu_usage.percpu_usage?.length ?? 1;
1487
+ return cpuDelta / systemDelta * numCores * 100;
1488
+ }
1489
+ function calculateNetworkStats(stats) {
1490
+ if (!stats.networks) {
1491
+ return { in: 0, out: 0 };
1492
+ }
1493
+ let rxBytes = 0;
1494
+ let txBytes = 0;
1495
+ for (const iface of Object.values(stats.networks)) {
1496
+ rxBytes += iface.rx_bytes;
1497
+ txBytes += iface.tx_bytes;
1498
+ }
1499
+ return { in: rxBytes, out: txBytes };
1500
+ }
1501
+ async function getContainerStats(container) {
1502
+ const stats = await container.stats({
1503
+ stream: false
1504
+ });
1505
+ const cpuPercent = calculateCPUPercent(stats);
1506
+ const memoryBytes = stats.memory_stats.usage;
1507
+ const network = calculateNetworkStats(stats);
1508
+ return {
1509
+ cpuPercent: Math.round(cpuPercent * 100) / 100,
1510
+ memoryMB: Math.round(memoryBytes / (1024 * 1024)),
1511
+ networkBytesIn: network.in,
1512
+ networkBytesOut: network.out
1513
+ };
1514
+ }
1515
+ function calculateResourceDelta(before, after) {
1516
+ return {
1517
+ cpuPercent: after.cpuPercent,
1518
+ memoryMB: after.memoryMB,
1519
+ networkBytesIn: after.networkBytesIn - before.networkBytesIn,
1520
+ networkBytesOut: after.networkBytesOut - before.networkBytesOut
1521
+ };
1522
+ }
1523
+
1524
+ // src/engine/docker.ts
1525
+ async function writeFileViaExec(container, filePath, content) {
1526
+ const data = typeof content === "string" ? Buffer.from(content, "utf-8") : content;
1527
+ const b64 = data.toString("base64");
1528
+ if (b64.length < 20000) {
1529
+ const exec = await container.exec({
1530
+ Cmd: ["sh", "-c", `printf '%s' '${b64}' | base64 -d > ${filePath}`],
1531
+ User: "sandbox"
1532
+ });
1533
+ await exec.start({ Detach: true });
1534
+ let info2 = await exec.inspect();
1535
+ while (info2.Running) {
1536
+ await new Promise((r) => setTimeout(r, 5));
1537
+ info2 = await exec.inspect();
1538
+ }
1539
+ if (info2.ExitCode !== 0) {
1540
+ throw new Error(`Failed to write file ${filePath} in container (exit code ${info2.ExitCode})`);
1541
+ }
1542
+ return;
1543
+ }
1544
+ const tempPath = `/tmp/b64_${Date.now()}.tmp`;
1545
+ const chunkSize = 8000;
1546
+ for (let i = 0;i < b64.length; i += chunkSize) {
1547
+ const chunk = b64.slice(i, i + chunkSize);
1548
+ const exec = await container.exec({
1549
+ Cmd: ["sh", "-c", `printf '%s' '${chunk}' >> ${tempPath}`],
1550
+ User: "sandbox"
1551
+ });
1552
+ await exec.start({ Detach: true });
1553
+ await exec.inspect();
1554
+ }
1555
+ const decodeExec = await container.exec({
1556
+ Cmd: ["sh", "-c", `base64 -d ${tempPath} > ${filePath} && rm ${tempPath}`],
1557
+ User: "sandbox"
1558
+ });
1559
+ await decodeExec.start({ Detach: true });
1560
+ let info = await decodeExec.inspect();
1561
+ while (info.Running) {
1562
+ await new Promise((r) => setTimeout(r, 5));
1563
+ info = await decodeExec.inspect();
1564
+ }
1565
+ if (info.ExitCode !== 0) {
1566
+ throw new Error(`Failed to write file ${filePath} in container (exit code ${info.ExitCode})`);
1567
+ }
1568
+ }
1569
+ async function readFileViaExec(container, filePath) {
1570
+ const exec = await container.exec({
1571
+ Cmd: ["base64", filePath],
1572
+ AttachStdout: true,
1573
+ AttachStderr: true,
1574
+ User: "sandbox"
1575
+ });
1576
+ const stream = await exec.start({ Tty: false });
1577
+ const chunks = [];
1578
+ const stderrChunks = [];
1579
+ const stdoutStream = new PassThrough;
1580
+ const stderrStream = new PassThrough;
1581
+ container.modem.demuxStream(stream, stdoutStream, stderrStream);
1582
+ stdoutStream.on("data", (chunk) => chunks.push(chunk));
1583
+ stderrStream.on("data", (chunk) => stderrChunks.push(chunk));
1584
+ await new Promise((resolve2, reject) => {
1585
+ stream.on("end", resolve2);
1586
+ stream.on("error", reject);
1587
+ });
1588
+ const inspectResult = await exec.inspect();
1589
+ if (inspectResult.ExitCode !== 0) {
1590
+ const stderr = Buffer.concat(stderrChunks).toString("utf-8").trim();
1591
+ throw new Error(`Failed to read file ${filePath} in container: ${stderr} (exit code ${inspectResult.ExitCode})`);
1592
+ }
1593
+ const b64Output = Buffer.concat(chunks).toString("utf-8").trim();
1594
+ return Buffer.from(b64Output, "base64");
1595
+ }
1596
+ var SANDBOX_WORKDIR = "/sandbox";
1597
+ var MAX_OUTPUT_BYTES = 1024 * 1024;
1598
+ var PROXY_PORT = 8118;
1599
+ var PROXY_STARTUP_TIMEOUT_MS = 5000;
1600
+ var PROXY_POLL_INTERVAL_MS = 100;
1601
+ async function startProxy(container, networkFilter) {
1602
+ const envParts = [];
1603
+ if (networkFilter) {
1604
+ envParts.push(`ISOL8_WHITELIST='${JSON.stringify(networkFilter.whitelist)}'`);
1605
+ envParts.push(`ISOL8_BLACKLIST='${JSON.stringify(networkFilter.blacklist)}'`);
1606
+ }
1607
+ const envPrefix = envParts.length > 0 ? `${envParts.join(" ")} ` : "";
1608
+ const startExec = await container.exec({
1609
+ Cmd: ["sh", "-c", `${envPrefix}bash /usr/local/bin/proxy.sh &`]
1610
+ });
1611
+ await startExec.start({ Detach: true });
1612
+ const deadline = Date.now() + PROXY_STARTUP_TIMEOUT_MS;
1613
+ while (Date.now() < deadline) {
1614
+ try {
1615
+ const checkExec = await container.exec({
1616
+ Cmd: ["sh", "-c", `nc -z 127.0.0.1 ${PROXY_PORT} 2>/dev/null`]
1617
+ });
1618
+ await checkExec.start({ Detach: true });
1619
+ let info = await checkExec.inspect();
1620
+ while (info.Running) {
1621
+ await new Promise((r) => setTimeout(r, 50));
1622
+ info = await checkExec.inspect();
1623
+ }
1624
+ if (info.ExitCode === 0) {
1625
+ return;
1626
+ }
1627
+ } catch {}
1628
+ await new Promise((r) => setTimeout(r, PROXY_POLL_INTERVAL_MS));
1629
+ }
1630
+ throw new Error("Proxy failed to start within timeout");
1631
+ }
1632
+ async function setupIptables(container) {
1633
+ const rules = [
1634
+ "/usr/sbin/iptables -A OUTPUT -o lo -j ACCEPT",
1635
+ "/usr/sbin/iptables -A OUTPUT -m state --state ESTABLISHED,RELATED -j ACCEPT",
1636
+ `/usr/sbin/iptables -A OUTPUT -p tcp -d 127.0.0.1 --dport ${PROXY_PORT} -m owner --uid-owner 100 -j ACCEPT`,
1637
+ "/usr/sbin/iptables -A OUTPUT -m owner --uid-owner 100 -j DROP"
1638
+ ].join(" && ");
1639
+ const exec = await container.exec({
1640
+ Cmd: ["sh", "-c", rules]
1641
+ });
1642
+ await exec.start({ Detach: true });
1643
+ let info = await exec.inspect();
1644
+ while (info.Running) {
1645
+ await new Promise((r) => setTimeout(r, 50));
1646
+ info = await exec.inspect();
1647
+ }
1648
+ if (info.ExitCode !== 0) {
1649
+ throw new Error(`Failed to set up iptables rules (exit code ${info.ExitCode})`);
1650
+ }
1651
+ logger.debug("[Filtered] iptables rules applied — sandbox user restricted to proxy only");
1652
+ }
1653
+ function wrapWithTimeout(cmd, timeoutSec) {
1654
+ return ["timeout", "-s", "KILL", String(timeoutSec), ...cmd];
1655
+ }
1656
+ function getInstallCommand(runtime, packages) {
1657
+ switch (runtime) {
1658
+ case "python":
1659
+ return [
1660
+ "pip",
1661
+ "install",
1662
+ "--user",
1663
+ "--no-cache-dir",
1664
+ "--break-system-packages",
1665
+ "--disable-pip-version-check",
1666
+ "--retries",
1667
+ "0",
1668
+ "--timeout",
1669
+ "15",
1670
+ ...packages
1671
+ ];
1672
+ case "node":
1673
+ return ["npm", "install", "--prefix", "/sandbox", ...packages];
1674
+ case "bun":
1675
+ return ["bun", "install", "-g", "--global-dir=/sandbox/.bun-global", ...packages];
1676
+ case "deno":
1677
+ return ["sh", "-c", packages.map((p) => `deno cache ${p}`).join(" && ")];
1678
+ case "bash":
1679
+ return ["apk", "add", "--no-cache", ...packages];
1680
+ default:
1681
+ throw new Error(`Unknown runtime for package install: ${runtime}`);
1682
+ }
1683
+ }
1684
+ async function installPackages(container, runtime, packages, timeoutMs) {
1685
+ const timeoutSec = Math.max(1, Math.ceil(timeoutMs / 1000));
1686
+ const cmd = wrapWithTimeout(getInstallCommand(runtime, packages), timeoutSec);
1687
+ logger.debug(`Installing packages: ${JSON.stringify(cmd)}`);
1688
+ const env = [
1689
+ "PATH=/sandbox/.local/bin:/sandbox/.npm-global/bin:/sandbox/.bun-global/bin:/usr/local/bin:/usr/bin:/bin:/usr/local/sbin:/usr/sbin:/sbin"
1690
+ ];
1691
+ if (runtime === "python") {
1692
+ env.push("PYTHONUSERBASE=/sandbox/.local");
1693
+ } else if (runtime === "node") {
1694
+ env.push("NPM_CONFIG_PREFIX=/sandbox/.npm-global");
1695
+ env.push("NPM_CONFIG_CACHE=/sandbox/.npm-cache");
1696
+ env.push("npm_config_cache=/sandbox/.npm-cache");
1697
+ env.push("NPM_CONFIG_FETCH_RETRIES=0");
1698
+ env.push("npm_config_fetch_retries=0");
1699
+ env.push("NPM_CONFIG_FETCH_RETRY_MINTIMEOUT=1000");
1700
+ env.push("npm_config_fetch_retry_mintimeout=1000");
1701
+ env.push("NPM_CONFIG_FETCH_RETRY_MAXTIMEOUT=2000");
1702
+ env.push("npm_config_fetch_retry_maxtimeout=2000");
1703
+ } else if (runtime === "bun") {
1704
+ env.push("BUN_INSTALL_GLOBAL_DIR=/sandbox/.bun-global");
1705
+ env.push("BUN_INSTALL_CACHE_DIR=/sandbox/.bun-cache");
1706
+ env.push("BUN_INSTALL_BIN=/sandbox/.bun-global/bin");
1707
+ } else if (runtime === "deno") {
1708
+ env.push("DENO_DIR=/sandbox/.deno");
1709
+ }
1710
+ const exec = await container.exec({
1711
+ Cmd: cmd,
1712
+ AttachStdout: true,
1713
+ AttachStderr: true,
1714
+ Env: env,
1715
+ User: runtime === "bash" ? "root" : "sandbox"
1716
+ });
1717
+ const stream = await exec.start({ Detach: false, Tty: false });
1718
+ return new Promise((resolve2, reject) => {
1719
+ let stderr = "";
1720
+ const stdoutStream = new PassThrough;
1721
+ const stderrStream = new PassThrough;
1722
+ container.modem.demuxStream(stream, stdoutStream, stderrStream);
1723
+ stderrStream.on("data", (chunk) => {
1724
+ const text = chunk.toString();
1725
+ stderr += text;
1726
+ logger.debug(`[install:${runtime}:stderr] ${text.trimEnd()}`);
1727
+ });
1728
+ stdoutStream.on("data", (chunk) => {
1729
+ const text = chunk.toString();
1730
+ logger.debug(`[install:${runtime}:stdout] ${text.trimEnd()}`);
1731
+ });
1732
+ stream.on("end", async () => {
1733
+ try {
1734
+ const info = await exec.inspect();
1735
+ if (info.ExitCode !== 0) {
1736
+ reject(new Error(`Package install failed (exit code ${info.ExitCode}): ${stderr}`));
1737
+ } else {
1738
+ resolve2();
1739
+ }
1740
+ } catch (err) {
1741
+ reject(err);
1742
+ }
1743
+ });
1744
+ stream.on("error", reject);
1745
+ });
1746
+ }
1747
+
1748
+ class DockerIsol8 {
1749
+ docker;
1750
+ mode;
1751
+ network;
1752
+ networkFilter;
1753
+ cpuLimit;
1754
+ memoryLimit;
1755
+ pidsLimit;
1756
+ readonlyRootFs;
1757
+ maxOutputSize;
1758
+ secrets;
1759
+ defaultTimeoutMs;
1760
+ overrideImage;
1761
+ semaphore;
1762
+ sandboxSize;
1763
+ tmpSize;
1764
+ security;
1765
+ persist;
1766
+ logNetwork;
1767
+ poolStrategy;
1768
+ poolSize;
1769
+ dependencies;
1770
+ auditLogger;
1771
+ remoteCodePolicy;
1772
+ container = null;
1773
+ persistentRuntime = null;
1774
+ pool = null;
1775
+ imageCache = new Map;
1776
+ async resolveExecutionRequest(req) {
1777
+ const inlineCode = req.code?.trim();
1778
+ const codeUrl = req.codeUrl?.trim();
1779
+ if (inlineCode && codeUrl) {
1780
+ throw new Error("ExecutionRequest.code and ExecutionRequest.codeUrl are mutually exclusive.");
1781
+ }
1782
+ if (!(inlineCode || codeUrl)) {
1783
+ throw new Error("ExecutionRequest must include either code or codeUrl.");
1784
+ }
1785
+ if (inlineCode) {
1786
+ return { ...req, code: req.code };
1787
+ }
1788
+ const fetched = await fetchRemoteCode({
1789
+ codeUrl,
1790
+ codeHash: req.codeHash,
1791
+ allowInsecureCodeUrl: req.allowInsecureCodeUrl
1792
+ }, this.remoteCodePolicy);
1793
+ return { ...req, code: fetched.code };
1794
+ }
1795
+ constructor(options = {}, maxConcurrent = 10) {
1796
+ this.docker = options.docker ?? new Docker;
1797
+ this.mode = options.mode ?? "ephemeral";
1798
+ this.network = options.network ?? "none";
1799
+ this.networkFilter = options.networkFilter;
1800
+ this.cpuLimit = options.cpuLimit ?? 1;
1801
+ this.memoryLimit = options.memoryLimit ?? "512m";
1802
+ this.pidsLimit = options.pidsLimit ?? 64;
1803
+ this.readonlyRootFs = options.readonlyRootFs ?? true;
1804
+ this.maxOutputSize = options.maxOutputSize ?? MAX_OUTPUT_BYTES;
1805
+ this.secrets = options.secrets ?? {};
1806
+ this.defaultTimeoutMs = options.timeoutMs ?? 30000;
1807
+ this.overrideImage = options.image;
1808
+ this.semaphore = new Semaphore(maxConcurrent);
1809
+ this.sandboxSize = options.sandboxSize ?? "512m";
1810
+ this.tmpSize = options.tmpSize ?? "256m";
1811
+ this.persist = options.persist ?? false;
1812
+ this.security = options.security ?? { seccomp: "strict" };
1813
+ this.logNetwork = options.logNetwork ?? false;
1814
+ this.poolStrategy = options.poolStrategy ?? "fast";
1815
+ this.poolSize = options.poolSize ?? { clean: 1, dirty: 1 };
1816
+ this.dependencies = options.dependencies ?? {};
1817
+ this.remoteCodePolicy = options.remoteCode ?? {
1818
+ enabled: false,
1819
+ allowedSchemes: ["https"],
1820
+ allowedHosts: [],
1821
+ blockedHosts: [],
1822
+ maxCodeSize: 10 * 1024 * 1024,
1823
+ fetchTimeoutMs: 30000,
1824
+ requireHash: false,
1825
+ enableCache: true,
1826
+ cacheTtl: 3600
1827
+ };
1828
+ if (options.audit) {
1829
+ this.auditLogger = new AuditLogger(options.audit);
1830
+ }
1831
+ if (options.debug) {
1832
+ logger.setDebug(true);
1833
+ }
1834
+ }
1835
+ async start(options = {}) {
1836
+ if (this.mode !== "ephemeral") {
1837
+ return;
1838
+ }
1839
+ const prewarm = options.prewarm;
1840
+ if (!prewarm) {
1841
+ return;
1842
+ }
1843
+ const pool = this.ensurePool();
1844
+ const images = new Set;
1845
+ const adapters2 = typeof prewarm === "object" && prewarm.runtimes?.length ? prewarm.runtimes.map((runtime) => RuntimeRegistry.get(runtime)) : RuntimeRegistry.list();
1846
+ for (const adapter of adapters2) {
1847
+ try {
1848
+ images.add(await this.resolveImage(adapter));
1849
+ } catch (err) {
1850
+ logger.debug(`[Pool] Pre-warm image resolution failed for ${adapter.name}: ${err}`);
1851
+ }
1852
+ }
1853
+ await Promise.all([...images].map(async (image) => {
1854
+ try {
1855
+ await pool.warm(image);
1856
+ logger.debug(`[Pool] Pre-warmed image: ${image}`);
1857
+ } catch (err) {
1858
+ logger.debug(`[Pool] Pre-warm failed for ${image}: ${err}`);
1859
+ }
1860
+ }));
1861
+ }
1862
+ async stop() {
1863
+ if (this.container) {
1864
+ try {
1865
+ await this.container.stop({ t: 2 });
1866
+ } catch {}
1867
+ try {
1868
+ await this.container.remove({ force: true });
1869
+ } catch {}
1870
+ this.container = null;
1871
+ this.persistentRuntime = null;
1872
+ }
1873
+ if (this.pool) {
1874
+ await this.pool.drain();
1875
+ this.pool = null;
1876
+ }
1877
+ }
1878
+ async execute(req) {
1879
+ await this.semaphore.acquire();
1880
+ const startTime = Date.now();
1881
+ try {
1882
+ const request = await this.resolveExecutionRequest(req);
1883
+ const result = this.mode === "persistent" ? await this.executePersistent(request, startTime) : await this.executeEphemeral(request, startTime);
1884
+ return result;
1885
+ } finally {
1886
+ this.semaphore.release();
1887
+ }
1888
+ }
1889
+ async recordAudit(req, result, startTime, container) {
1890
+ try {
1891
+ const enc = new TextEncoder;
1892
+ const data = enc.encode(req.code);
1893
+ const digest = await crypto.subtle.digest("SHA-256", data);
1894
+ const codeHash = Array.from(new Uint8Array(digest)).map((b) => b.toString(16).padStart(2, "0")).join("");
1895
+ let securityEvents;
1896
+ if (container && this.network === "filtered") {
1897
+ securityEvents = await this.collectSecurityEvents(container);
1898
+ if (securityEvents.length === 0) {
1899
+ securityEvents = undefined;
1900
+ }
1901
+ }
1902
+ let networkLogs;
1903
+ if (this.logNetwork && result.networkLogs) {
1904
+ networkLogs = result.networkLogs;
1905
+ }
1906
+ const audit = {
1907
+ executionId: result.executionId,
1908
+ userId: req.metadata?.userId || "",
1909
+ timestamp: new Date(startTime).toISOString(),
1910
+ runtime: result.runtime,
1911
+ codeHash,
1912
+ containerId: result.containerId || "",
1913
+ exitCode: result.exitCode,
1914
+ durationMs: result.durationMs,
1915
+ resourceUsage: result.resourceUsage,
1916
+ securityEvents,
1917
+ networkLogs,
1918
+ metadata: req.metadata
1919
+ };
1920
+ this.auditLogger.record(audit);
1921
+ } catch (err) {
1922
+ logger.error("Failed to record audit log:", err);
1923
+ }
1924
+ }
1925
+ async collectSecurityEvents(container) {
1926
+ const events = [];
1927
+ try {
1928
+ const exec = await container.exec({
1929
+ Cmd: ["cat", "/tmp/isol8-proxy/security-events.jsonl"],
1930
+ AttachStdout: true,
1931
+ AttachStderr: false,
1932
+ User: "root"
1933
+ });
1934
+ const stream = await exec.start({ Tty: false });
1935
+ const chunks = [];
1936
+ for await (const chunk of stream) {
1937
+ chunks.push(chunk);
1938
+ }
1939
+ const output = Buffer.concat(chunks).toString("utf-8").trim();
1940
+ if (output) {
1941
+ for (const line of output.split(`
1942
+ `)) {
1943
+ if (line.trim()) {
1944
+ try {
1945
+ const event = JSON.parse(line);
1946
+ events.push({
1947
+ type: event.type || "unknown",
1948
+ message: `Security event: ${event.type}`,
1949
+ details: event.details || {},
1950
+ timestamp: event.timestamp || new Date().toISOString()
1951
+ });
1952
+ } catch {}
1953
+ }
1954
+ }
1955
+ }
1956
+ } catch {}
1957
+ return events;
1958
+ }
1959
+ async collectNetworkLogs(container) {
1960
+ const logs = [];
1961
+ try {
1962
+ const exec = await container.exec({
1963
+ Cmd: ["cat", "/tmp/isol8-proxy/network.jsonl"],
1964
+ AttachStdout: true,
1965
+ AttachStderr: false,
1966
+ User: "root"
1967
+ });
1968
+ const stream = await exec.start({ Tty: false });
1969
+ const chunks = [];
1970
+ for await (const chunk of stream) {
1971
+ chunks.push(chunk);
1972
+ }
1973
+ const output = Buffer.concat(chunks).toString("utf-8").trim();
1974
+ logger.debug(`[NetworkLogs] Raw output length: ${output.length}, first 100 chars: ${output.substring(0, 100).replace(/\\n/g, "\\n")}`);
1975
+ const jsonLines = output.split(`
1976
+ `).filter((line) => line.includes("timestamp"));
1977
+ logger.debug(`[NetworkLogs] Found ${jsonLines.length} JSON lines out of ${output.split(`
1978
+ `).length} total lines`);
1979
+ for (const line of jsonLines) {
1980
+ const startIdx = line.indexOf("{");
1981
+ const endIdx = line.lastIndexOf("}");
1982
+ if (startIdx === -1 || endIdx === -1) {
1983
+ continue;
1984
+ }
1985
+ const jsonStr = line.substring(startIdx, endIdx + 1);
1986
+ try {
1987
+ const entry = JSON.parse(jsonStr);
1988
+ logs.push({
1989
+ timestamp: entry.timestamp || new Date().toISOString(),
1990
+ method: entry.method || "UNKNOWN",
1991
+ host: entry.host || "",
1992
+ path: entry.path,
1993
+ action: entry.action || "ALLOW",
1994
+ durationMs: entry.durationMs || 0
1995
+ });
1996
+ logger.debug(`[NetworkLogs] Successfully parsed line: ${JSON.stringify(entry)}`);
1997
+ } catch (e) {
1998
+ logger.debug(`[NetworkLogs] Failed to parse line: ${line.substring(0, 50)}..., error: ${e}`);
1999
+ }
2000
+ }
2001
+ logger.debug(`[NetworkLogs] Total parsed logs: ${logs.length}`);
2002
+ } catch {}
2003
+ return logs;
2004
+ }
2005
+ async putFile(path, content) {
2006
+ if (!this.container) {
2007
+ throw new Error("No active container. Call execute() first in persistent mode.");
2008
+ }
2009
+ if (this.readonlyRootFs) {
2010
+ await writeFileViaExec(this.container, path, content);
2011
+ } else {
2012
+ const tar = createTarBuffer(path, content);
2013
+ await this.container.putArchive(tar, { path: "/" });
2014
+ }
2015
+ }
2016
+ async getFile(path) {
2017
+ if (!this.container) {
2018
+ throw new Error("No active container. Call execute() first in persistent mode.");
2019
+ }
2020
+ if (this.readonlyRootFs) {
2021
+ return readFileViaExec(this.container, path);
2022
+ }
2023
+ const stream = await this.container.getArchive({ path });
2024
+ const chunks = [];
2025
+ for await (const chunk of stream) {
2026
+ chunks.push(chunk);
2027
+ }
2028
+ const tarBuffer = Buffer.concat(chunks);
2029
+ return extractFromTar(tarBuffer, path);
2030
+ }
2031
+ get containerId() {
2032
+ return this.container?.id ?? null;
2033
+ }
2034
+ async* executeStream(req) {
2035
+ await this.semaphore.acquire();
2036
+ try {
2037
+ const request = await this.resolveExecutionRequest(req);
2038
+ const adapter = this.getAdapter(request.runtime);
2039
+ const timeoutMs = request.timeoutMs ?? this.defaultTimeoutMs;
2040
+ const image = await this.resolveImage(adapter);
2041
+ const container = await this.docker.createContainer({
2042
+ Image: image,
2043
+ Cmd: ["sleep", "infinity"],
2044
+ WorkingDir: SANDBOX_WORKDIR,
2045
+ Env: this.buildEnv(),
2046
+ NetworkDisabled: this.network === "none",
2047
+ HostConfig: this.buildHostConfig(),
2048
+ StopTimeout: 2
2049
+ });
2050
+ try {
2051
+ await container.start();
2052
+ if (this.network === "filtered") {
2053
+ await startProxy(container, this.networkFilter);
2054
+ await setupIptables(container);
2055
+ }
2056
+ const ext = request.fileExtension ?? adapter.getFileExtension();
2057
+ const filePath = `${SANDBOX_WORKDIR}/main${ext}`;
2058
+ await writeFileViaExec(container, filePath, request.code);
2059
+ if (request.installPackages?.length) {
2060
+ await installPackages(container, request.runtime, request.installPackages, timeoutMs);
2061
+ }
2062
+ if (request.files) {
2063
+ for (const [fPath, fContent] of Object.entries(request.files)) {
2064
+ await writeFileViaExec(container, fPath, fContent);
2065
+ }
2066
+ }
2067
+ const rawCmd = adapter.getCommand(request.code, filePath);
2068
+ const timeoutSec = Math.ceil(timeoutMs / 1000);
2069
+ let cmd;
2070
+ if (request.stdin) {
2071
+ const stdinPath = `${SANDBOX_WORKDIR}/_stdin`;
2072
+ await writeFileViaExec(container, stdinPath, request.stdin);
2073
+ const cmdStr = rawCmd.map((a) => `'${a.replace(/'/g, "'\\''")}'`).join(" ");
2074
+ cmd = wrapWithTimeout(["sh", "-c", `cat ${stdinPath} | ${cmdStr}`], timeoutSec);
2075
+ } else {
2076
+ cmd = wrapWithTimeout(rawCmd, timeoutSec);
2077
+ }
2078
+ const exec = await container.exec({
2079
+ Cmd: cmd,
2080
+ Env: this.buildEnv(request.env),
2081
+ AttachStdout: true,
2082
+ AttachStderr: true,
2083
+ WorkingDir: SANDBOX_WORKDIR,
2084
+ User: "sandbox"
2085
+ });
2086
+ const execStream = await exec.start({ Tty: false });
2087
+ yield* this.streamExecOutput(execStream, exec, container, timeoutMs);
2088
+ } finally {
2089
+ if (this.persist) {
2090
+ logger.debug(`[Persist] Leaving container running for inspection: ${container.id}`);
2091
+ } else {
2092
+ try {
2093
+ await container.remove({ force: true });
2094
+ } catch {}
2095
+ }
2096
+ }
2097
+ } finally {
2098
+ this.semaphore.release();
2099
+ }
2100
+ }
2101
+ async resolveImage(adapter) {
2102
+ if (this.overrideImage) {
2103
+ return this.overrideImage;
2104
+ }
2105
+ const cacheKey = adapter.image;
2106
+ const cached = this.imageCache.get(cacheKey);
2107
+ if (cached) {
2108
+ return cached;
2109
+ }
2110
+ let resolvedImage = adapter.image;
2111
+ const configuredDeps = this.dependencies[adapter.name];
2112
+ const normalizedDeps = configuredDeps ? normalizePackages(configuredDeps) : [];
2113
+ if (normalizedDeps.length > 0) {
2114
+ const hashedCustomTag = getCustomImageTag(adapter.name, normalizedDeps);
2115
+ try {
2116
+ await this.docker.getImage(hashedCustomTag).inspect();
2117
+ resolvedImage = hashedCustomTag;
2118
+ } catch {
2119
+ logger.debug(`[ImageBuilder] Hashed custom image not found for ${adapter.name}: ${hashedCustomTag}`);
2120
+ }
2121
+ }
2122
+ if (resolvedImage === adapter.image) {
2123
+ const legacyCustomTag = `${adapter.image}-custom`;
2124
+ try {
2125
+ await this.docker.getImage(legacyCustomTag).inspect();
2126
+ resolvedImage = legacyCustomTag;
2127
+ } catch {}
2128
+ }
2129
+ try {
2130
+ await this.docker.getImage(resolvedImage).inspect();
2131
+ } catch {
2132
+ logger.debug(`[ImageBuilder] Image ${resolvedImage} not found. Building...`);
2133
+ const { buildBaseImages: buildBaseImages2, buildCustomImage: buildCustomImage2 } = await Promise.resolve().then(() => (init_image_builder(), exports_image_builder));
2134
+ if (resolvedImage !== adapter.image && normalizedDeps.length > 0) {
2135
+ try {
2136
+ await this.docker.getImage(adapter.image).inspect();
2137
+ } catch {
2138
+ logger.debug(`[ImageBuilder] Base image ${adapter.image} missing. Building...`);
2139
+ await buildBaseImages2(this.docker, undefined, false, [adapter.name]);
2140
+ }
2141
+ logger.debug(`[ImageBuilder] Building custom image for ${adapter.name}...`);
2142
+ await buildCustomImage2(this.docker, adapter.name, normalizedDeps);
2143
+ } else {
2144
+ logger.debug(`[ImageBuilder] Building base image for ${adapter.name}...`);
2145
+ await buildBaseImages2(this.docker, undefined, false, [adapter.name]);
2146
+ }
2147
+ }
2148
+ this.imageCache.set(cacheKey, resolvedImage);
2149
+ return resolvedImage;
2150
+ }
2151
+ ensurePool() {
2152
+ if (!this.pool) {
2153
+ this.pool = new ContainerPool({
2154
+ docker: this.docker,
2155
+ poolStrategy: this.poolStrategy,
2156
+ poolSize: this.poolSize,
2157
+ networkMode: this.network,
2158
+ securityMode: this.security.seccomp ?? "strict",
2159
+ createOptions: {
2160
+ Cmd: ["sleep", "infinity"],
2161
+ WorkingDir: SANDBOX_WORKDIR,
2162
+ Env: this.buildEnv(),
2163
+ NetworkDisabled: this.network === "none",
2164
+ HostConfig: this.buildHostConfig(),
2165
+ StopTimeout: 2
2166
+ }
2167
+ });
2168
+ }
2169
+ return this.pool;
2170
+ }
2171
+ async executeEphemeral(req, startTime) {
2172
+ const adapter = this.getAdapter(req.runtime);
2173
+ const timeoutMs = req.timeoutMs ?? this.defaultTimeoutMs;
2174
+ const image = await this.resolveImage(adapter);
2175
+ const pool = this.ensurePool();
2176
+ const container = await pool.acquire(image);
2177
+ let startStats;
2178
+ if (this.auditLogger) {
2179
+ try {
2180
+ startStats = await getContainerStats(container);
2181
+ } catch (err) {
2182
+ logger.debug("Failed to collect baseline stats:", err);
2183
+ }
2184
+ }
2185
+ try {
2186
+ if (this.network === "filtered") {
2187
+ await startProxy(container, this.networkFilter);
2188
+ await setupIptables(container);
2189
+ }
2190
+ const canUseInline = !(req.stdin || req.files || req.outputPaths) && (!req.installPackages || req.installPackages.length === 0);
2191
+ let rawCmd;
2192
+ if (canUseInline) {
2193
+ try {
2194
+ rawCmd = adapter.getCommand(req.code);
2195
+ } catch {
2196
+ const ext = req.fileExtension ?? adapter.getFileExtension();
2197
+ const filePath = `${SANDBOX_WORKDIR}/main${ext}`;
2198
+ await writeFileViaExec(container, filePath, req.code);
2199
+ rawCmd = adapter.getCommand(req.code, filePath);
2200
+ }
2201
+ } else {
2202
+ const ext = req.fileExtension ?? adapter.getFileExtension();
2203
+ const filePath = `${SANDBOX_WORKDIR}/main${ext}`;
2204
+ await writeFileViaExec(container, filePath, req.code);
2205
+ rawCmd = adapter.getCommand(req.code, filePath);
2206
+ }
2207
+ if (req.installPackages?.length) {
2208
+ await installPackages(container, req.runtime, req.installPackages, timeoutMs);
2209
+ }
2210
+ const timeoutSec = Math.ceil(timeoutMs / 1000);
2211
+ let cmd;
2212
+ if (req.stdin) {
2213
+ const stdinPath = `${SANDBOX_WORKDIR}/_stdin`;
2214
+ await writeFileViaExec(container, stdinPath, req.stdin);
2215
+ const cmdStr = rawCmd.map((a) => `'${a.replace(/'/g, "'\\''")}' `).join("");
2216
+ cmd = wrapWithTimeout(["sh", "-c", `cat ${stdinPath} | ${cmdStr}`], timeoutSec);
2217
+ } else {
2218
+ cmd = wrapWithTimeout(rawCmd, timeoutSec);
2219
+ }
2220
+ if (req.files) {
2221
+ for (const [fPath, fContent] of Object.entries(req.files)) {
2222
+ await writeFileViaExec(container, fPath, fContent);
2223
+ }
2224
+ }
2225
+ const exec = await container.exec({
2226
+ Cmd: cmd,
2227
+ Env: this.buildEnv(req.env),
2228
+ AttachStdout: true,
2229
+ AttachStderr: true,
2230
+ WorkingDir: SANDBOX_WORKDIR,
2231
+ User: "sandbox"
2232
+ });
2233
+ const start = performance.now();
2234
+ const execStream = await exec.start({ Tty: false });
2235
+ const { stdout, stderr, truncated } = await this.collectExecOutput(execStream, container, timeoutMs);
2236
+ const durationMs = Math.round(performance.now() - start);
2237
+ const inspectResult = await exec.inspect();
2238
+ let resourceUsage;
2239
+ if (startStats) {
2240
+ try {
2241
+ const endStats = await getContainerStats(container);
2242
+ resourceUsage = calculateResourceDelta(startStats, endStats);
2243
+ } catch (err) {
2244
+ logger.debug("Failed to collect final stats:", err);
2245
+ }
2246
+ }
2247
+ let networkLogs;
2248
+ if (this.logNetwork && this.network === "filtered") {
2249
+ try {
2250
+ networkLogs = await this.collectNetworkLogs(container);
2251
+ if (networkLogs.length === 0) {
2252
+ networkLogs = undefined;
2253
+ }
2254
+ } catch (err) {
2255
+ logger.debug("Failed to collect network logs:", err);
2256
+ }
2257
+ }
2258
+ const result = {
2259
+ stdout: this.postProcessOutput(stdout, truncated),
2260
+ stderr: this.postProcessOutput(stderr, false),
2261
+ exitCode: inspectResult.ExitCode ?? 1,
2262
+ durationMs,
2263
+ truncated,
2264
+ executionId: randomUUID(),
2265
+ runtime: req.runtime,
2266
+ timestamp: new Date().toISOString(),
2267
+ containerId: container.id,
2268
+ ...resourceUsage ? { resourceUsage } : {},
2269
+ ...networkLogs ? { networkLogs } : {},
2270
+ ...req.outputPaths ? { files: await this.retrieveFiles(container, req.outputPaths) } : {}
2271
+ };
2272
+ if (this.auditLogger) {
2273
+ await this.recordAudit(req, result, startTime, container);
2274
+ }
2275
+ return result;
2276
+ } finally {
2277
+ if (this.persist) {
2278
+ logger.debug(`[Persist] Leaving container running for inspection: ${container.id}`);
2279
+ } else {
2280
+ pool.release(container, image).catch((err) => {
2281
+ logger.debug(`[Pool] release failed: ${err}`);
2282
+ container.remove({ force: true }).catch(() => {});
2283
+ });
2284
+ }
2285
+ }
2286
+ }
2287
+ async executePersistent(req, startTime) {
2288
+ const adapter = this.getAdapter(req.runtime);
2289
+ const timeoutMs = req.timeoutMs ?? this.defaultTimeoutMs;
2290
+ if (!this.container) {
2291
+ await this.startPersistentContainer(adapter);
2292
+ } else if (this.persistentRuntime?.name !== adapter.name) {
2293
+ throw new Error(`Cannot switch runtime from "${this.persistentRuntime?.name}" to "${adapter.name}". Each persistent container supports a single runtime. Create a new Isol8 instance for a different runtime.`);
2294
+ }
2295
+ const ext = req.fileExtension ?? adapter.getFileExtension();
2296
+ const filePath = `${SANDBOX_WORKDIR}/exec_${Date.now()}${ext}`;
2297
+ if (this.readonlyRootFs) {
2298
+ await writeFileViaExec(this.container, filePath, req.code);
2299
+ } else {
2300
+ const tar = createTarBuffer(filePath, req.code);
2301
+ await this.container.putArchive(tar, { path: "/" });
2302
+ }
2303
+ if (req.files) {
2304
+ for (const [fPath, fContent] of Object.entries(req.files)) {
2305
+ if (this.readonlyRootFs) {
2306
+ await writeFileViaExec(this.container, fPath, fContent);
2307
+ } else {
2308
+ const tar = createTarBuffer(fPath, fContent);
2309
+ await this.container.putArchive(tar, { path: "/" });
2310
+ }
2311
+ }
2312
+ }
2313
+ const rawCmd = adapter.getCommand(req.code, filePath);
2314
+ const timeoutSec = Math.ceil(timeoutMs / 1000);
2315
+ if (req.installPackages?.length) {
2316
+ await installPackages(this.container, req.runtime, req.installPackages, timeoutMs);
2317
+ }
2318
+ let cmd;
2319
+ if (req.stdin) {
2320
+ const stdinPath = `${SANDBOX_WORKDIR}/_stdin_${Date.now()}`;
2321
+ await writeFileViaExec(this.container, stdinPath, req.stdin);
2322
+ const cmdStr = rawCmd.map((a) => `'${a.replace(/'/g, "'\\''")}' `).join("");
2323
+ cmd = wrapWithTimeout(["sh", "-c", `cat ${stdinPath} | ${cmdStr}`], timeoutSec);
2324
+ } else {
2325
+ cmd = wrapWithTimeout(rawCmd, timeoutSec);
2326
+ }
2327
+ const execEnv = this.buildEnv(req.env);
2328
+ const exec = await this.container.exec({
2329
+ Cmd: cmd,
2330
+ Env: execEnv,
2331
+ AttachStdout: true,
2332
+ AttachStderr: true,
2333
+ WorkingDir: SANDBOX_WORKDIR,
2334
+ User: "sandbox"
2335
+ });
2336
+ const start = performance.now();
2337
+ const execStream = await exec.start({ Tty: false });
2338
+ const { stdout, stderr, truncated } = await this.collectExecOutput(execStream, this.container, timeoutMs);
2339
+ const durationMs = Math.round(performance.now() - start);
2340
+ const inspectResult = await exec.inspect();
2341
+ let resourceUsage;
2342
+ if (this.auditLogger) {
2343
+ try {
2344
+ const endStats = await getContainerStats(this.container);
2345
+ resourceUsage = {
2346
+ cpuPercent: endStats.cpuPercent,
2347
+ memoryMB: endStats.memoryMB,
2348
+ networkBytesIn: endStats.networkBytesIn,
2349
+ networkBytesOut: endStats.networkBytesOut
2350
+ };
2351
+ } catch (err) {
2352
+ logger.debug("Failed to collect resource stats:", err);
2353
+ }
2354
+ }
2355
+ let networkLogs;
2356
+ if (this.logNetwork && this.network === "filtered") {
2357
+ try {
2358
+ networkLogs = await this.collectNetworkLogs(this.container);
2359
+ if (networkLogs.length === 0) {
2360
+ networkLogs = undefined;
2361
+ }
2362
+ } catch (err) {
2363
+ logger.debug("Failed to collect network logs:", err);
2364
+ }
2365
+ }
2366
+ const result = {
2367
+ stdout: this.postProcessOutput(stdout, truncated),
2368
+ stderr: this.postProcessOutput(stderr, false),
2369
+ exitCode: inspectResult.ExitCode ?? 1,
2370
+ durationMs,
2371
+ truncated,
2372
+ executionId: randomUUID(),
2373
+ runtime: req.runtime,
2374
+ timestamp: new Date().toISOString(),
2375
+ containerId: this.container?.id,
2376
+ ...resourceUsage ? { resourceUsage } : {},
2377
+ ...networkLogs ? { networkLogs } : {},
2378
+ ...req.outputPaths ? { files: await this.retrieveFiles(this.container, req.outputPaths) } : {}
2379
+ };
2380
+ if (this.auditLogger) {
2381
+ await this.recordAudit(req, result, startTime, this.container);
2382
+ }
2383
+ return result;
2384
+ }
2385
+ async retrieveFiles(container, paths) {
2386
+ const files = {};
2387
+ for (const p of paths) {
2388
+ try {
2389
+ const buf = this.readonlyRootFs ? await readFileViaExec(container, p) : await this.getFileFromContainer(container, p);
2390
+ files[p] = buf.toString("base64");
2391
+ } catch {}
2392
+ }
2393
+ return files;
2394
+ }
2395
+ async getFileFromContainer(container, path) {
2396
+ const stream = await container.getArchive({ path });
2397
+ const chunks = [];
2398
+ for await (const chunk of stream) {
2399
+ chunks.push(chunk);
2400
+ }
2401
+ return extractFromTar(Buffer.concat(chunks), path);
2402
+ }
2403
+ async startPersistentContainer(adapter) {
2404
+ const image = await this.resolveImage(adapter);
2405
+ this.container = await this.docker.createContainer({
2406
+ Image: image,
2407
+ Cmd: ["sleep", "infinity"],
2408
+ WorkingDir: SANDBOX_WORKDIR,
2409
+ Env: this.buildEnv(),
2410
+ NetworkDisabled: this.network === "none",
2411
+ HostConfig: this.buildHostConfig(),
2412
+ StopTimeout: 2,
2413
+ Labels: {
2414
+ "isol8.managed": "true",
2415
+ "isol8.runtime": adapter.name
2416
+ }
2417
+ });
2418
+ await this.container.start();
2419
+ if (this.network === "filtered") {
2420
+ await startProxy(this.container, this.networkFilter);
2421
+ await setupIptables(this.container);
2422
+ }
2423
+ this.persistentRuntime = adapter;
2424
+ }
2425
+ getAdapter(runtime) {
2426
+ return RuntimeRegistry.get(runtime);
2427
+ }
2428
+ buildHostConfig() {
2429
+ const config = {
2430
+ Memory: parseMemoryLimit(this.memoryLimit),
2431
+ NanoCpus: Math.floor(this.cpuLimit * 1e9),
2432
+ PidsLimit: this.pidsLimit,
2433
+ ReadonlyRootfs: this.readonlyRootFs,
2434
+ Tmpfs: {
2435
+ "/tmp": `rw,noexec,nosuid,nodev,size=${this.tmpSize}`,
2436
+ [SANDBOX_WORKDIR]: `rw,exec,nosuid,nodev,size=${this.sandboxSize},uid=100,gid=101`
2437
+ },
2438
+ SecurityOpt: this.buildSecurityOpts()
2439
+ };
2440
+ if (this.network === "filtered") {
2441
+ config.NetworkMode = "bridge";
2442
+ config.CapAdd = ["NET_ADMIN"];
2443
+ } else if (this.network === "host") {
2444
+ config.NetworkMode = "host";
2445
+ }
2446
+ return config;
2447
+ }
2448
+ buildSecurityOpts() {
2449
+ const opts = ["no-new-privileges"];
2450
+ if (this.security.seccomp === "unconfined") {
2451
+ opts.push("seccomp=unconfined");
2452
+ return opts;
2453
+ }
2454
+ if (this.security.seccomp === "custom" && this.security.customProfilePath) {
2455
+ try {
2456
+ const profile = readFileSync3(this.security.customProfilePath, "utf-8");
2457
+ opts.push(`seccomp=${profile}`);
2458
+ } catch (e) {
2459
+ throw new Error(`Failed to load custom seccomp profile at ${this.security.customProfilePath}: ${e}`);
2460
+ }
2461
+ return opts;
2462
+ }
2463
+ try {
2464
+ const profile = this.loadDefaultSeccompProfile();
2465
+ opts.push(`seccomp=${profile}`);
2466
+ } catch (e) {
2467
+ throw new Error(`Failed to load default seccomp profile: ${e}`);
2468
+ }
2469
+ return opts;
2470
+ }
2471
+ loadDefaultSeccompProfile() {
2472
+ const devPath = new URL("../../docker/seccomp-profile.json", import.meta.url);
2473
+ if (existsSync4(devPath)) {
2474
+ return readFileSync3(devPath, "utf-8");
2475
+ }
2476
+ const prodPath = new URL("./docker/seccomp-profile.json", import.meta.url);
2477
+ if (existsSync4(prodPath)) {
2478
+ return readFileSync3(prodPath, "utf-8");
2479
+ }
2480
+ if (EMBEDDED_DEFAULT_SECCOMP_PROFILE.length > 0) {
2481
+ logger.debug(`Default seccomp profile file not found. Using embedded profile. Tried: ${devPath.pathname}, ${prodPath.pathname}`);
2482
+ return EMBEDDED_DEFAULT_SECCOMP_PROFILE;
2483
+ }
2484
+ throw new Error("Embedded default seccomp profile is unavailable");
2485
+ }
2486
+ buildEnv(extra) {
2487
+ const env = [
2488
+ "PYTHONUNBUFFERED=1",
2489
+ "PYTHONUSERBASE=/sandbox/.local",
2490
+ "NPM_CONFIG_PREFIX=/sandbox/.npm-global",
2491
+ "DENO_DIR=/sandbox/.deno",
2492
+ "PATH=/sandbox/.local/bin:/sandbox/.npm-global/bin:/sandbox/.bun-global/bin:/usr/local/bin:/usr/bin:/bin",
2493
+ "NODE_PATH=/usr/local/lib/node_modules:/sandbox/.npm-global/lib/node_modules:/sandbox/node_modules"
2494
+ ];
2495
+ for (const [key, value] of Object.entries(this.secrets)) {
2496
+ env.push(`${key}=${value}`);
2497
+ }
2498
+ if (extra) {
2499
+ for (const [key, value] of Object.entries(extra)) {
2500
+ env.push(`${key}=${value}`);
2501
+ }
2502
+ }
2503
+ if (this.network === "filtered") {
2504
+ if (this.networkFilter) {
2505
+ env.push(`ISOL8_WHITELIST=${JSON.stringify(this.networkFilter.whitelist)}`);
2506
+ env.push(`ISOL8_BLACKLIST=${JSON.stringify(this.networkFilter.blacklist)}`);
2507
+ }
2508
+ env.push(`HTTP_PROXY=http://127.0.0.1:${PROXY_PORT}`);
2509
+ env.push(`HTTPS_PROXY=http://127.0.0.1:${PROXY_PORT}`);
2510
+ env.push(`http_proxy=http://127.0.0.1:${PROXY_PORT}`);
2511
+ env.push(`https_proxy=http://127.0.0.1:${PROXY_PORT}`);
2512
+ }
2513
+ return env;
2514
+ }
2515
+ async* streamExecOutput(stream, exec, container, timeoutMs) {
2516
+ const queue = [];
2517
+ let resolve2 = null;
2518
+ let done = false;
2519
+ const push = (event) => {
2520
+ queue.push(event);
2521
+ if (resolve2) {
2522
+ resolve2();
2523
+ resolve2 = null;
2524
+ }
2525
+ };
2526
+ const timer = setTimeout(() => {
2527
+ push({ type: "error", data: "EXECUTION TIMED OUT" });
2528
+ push({ type: "exit", data: "137" });
2529
+ done = true;
2530
+ }, timeoutMs);
2531
+ const stdoutStream = new PassThrough;
2532
+ const stderrStream = new PassThrough;
2533
+ container.modem.demuxStream(stream, stdoutStream, stderrStream);
2534
+ stdoutStream.on("data", (chunk) => {
2535
+ let text = chunk.toString("utf-8");
2536
+ if (Object.keys(this.secrets).length > 0) {
2537
+ text = maskSecrets(text, this.secrets);
2538
+ }
2539
+ push({ type: "stdout", data: text });
2540
+ });
2541
+ stderrStream.on("data", (chunk) => {
2542
+ let text = chunk.toString("utf-8");
2543
+ if (Object.keys(this.secrets).length > 0) {
2544
+ text = maskSecrets(text, this.secrets);
2545
+ }
2546
+ push({ type: "stderr", data: text });
2547
+ });
2548
+ stream.on("end", async () => {
2549
+ clearTimeout(timer);
2550
+ try {
2551
+ const info = await exec.inspect();
2552
+ push({ type: "exit", data: (info.ExitCode ?? 0).toString() });
2553
+ } catch {
2554
+ push({ type: "exit", data: "1" });
2555
+ }
2556
+ done = true;
2557
+ });
2558
+ stream.on("error", (err) => {
2559
+ clearTimeout(timer);
2560
+ push({ type: "error", data: err.message });
2561
+ push({ type: "exit", data: "1" });
2562
+ done = true;
2563
+ });
2564
+ while (!done || queue.length > 0) {
2565
+ if (queue.length > 0) {
2566
+ yield queue.shift();
2567
+ } else if (resolve2) {
2568
+ await new Promise((r) => {
2569
+ resolve2 = r;
2570
+ });
2571
+ } else {
2572
+ await new Promise((r) => setTimeout(r, 10));
2573
+ }
2574
+ }
2575
+ }
2576
+ async collectExecOutput(stream, container, timeoutMs) {
2577
+ return new Promise((resolve2, reject) => {
2578
+ let stdout = "";
2579
+ let stderr = "";
2580
+ let truncated = false;
2581
+ let settled = false;
2582
+ let stdoutEnded = false;
2583
+ let stderrEnded = false;
2584
+ const timer = setTimeout(() => {
2585
+ if (settled) {
2586
+ return;
2587
+ }
2588
+ settled = true;
2589
+ if (stream.destroy) {
2590
+ stream.destroy();
2591
+ }
2592
+ resolve2({ stdout, stderr: `${stderr}
2593
+ --- EXECUTION TIMED OUT ---`, truncated });
2594
+ }, timeoutMs);
2595
+ const stdoutStream = new PassThrough;
2596
+ const stderrStream = new PassThrough;
2597
+ container.modem.demuxStream(stream, stdoutStream, stderrStream);
2598
+ stdoutStream.on("data", (chunk) => {
2599
+ stdout += chunk.toString("utf-8");
2600
+ if (stdout.length > this.maxOutputSize) {
2601
+ const result = truncateOutput(stdout, this.maxOutputSize);
2602
+ stdout = result.text;
2603
+ truncated = true;
2604
+ }
2605
+ });
2606
+ stderrStream.on("data", (chunk) => {
2607
+ stderr += chunk.toString("utf-8");
2608
+ if (stderr.length > this.maxOutputSize) {
2609
+ const result = truncateOutput(stderr, this.maxOutputSize);
2610
+ stderr = result.text;
2611
+ truncated = true;
2612
+ }
2613
+ });
2614
+ const checkDone = () => {
2615
+ if (settled) {
2616
+ return;
2617
+ }
2618
+ if (stdoutEnded && stderrEnded) {
2619
+ settled = true;
2620
+ clearTimeout(timer);
2621
+ resolve2({ stdout, stderr, truncated });
2622
+ }
2623
+ };
2624
+ stdoutStream.on("end", () => {
2625
+ stdoutEnded = true;
2626
+ checkDone();
2627
+ });
2628
+ stderrStream.on("end", () => {
2629
+ stderrEnded = true;
2630
+ checkDone();
2631
+ });
2632
+ stream.on("error", (err) => {
2633
+ if (settled) {
2634
+ return;
2635
+ }
2636
+ settled = true;
2637
+ clearTimeout(timer);
2638
+ reject(err);
2639
+ });
2640
+ stream.on("end", () => {
2641
+ if (settled) {
2642
+ return;
2643
+ }
2644
+ setTimeout(() => {
2645
+ if (!settled) {
2646
+ stdoutEnded = true;
2647
+ stderrEnded = true;
2648
+ checkDone();
2649
+ }
2650
+ }, 100);
2651
+ });
2652
+ });
2653
+ }
2654
+ postProcessOutput(output, _truncated) {
2655
+ let result = output;
2656
+ if (Object.keys(this.secrets).length > 0) {
2657
+ result = maskSecrets(result, this.secrets);
2658
+ }
2659
+ return result.trimEnd();
2660
+ }
2661
+ static async cleanup(docker) {
2662
+ const dockerInstance = docker ?? new Docker;
2663
+ const containers = await dockerInstance.listContainers({ all: true });
2664
+ const isol8Containers = containers.filter((c) => c.Image.startsWith("isol8:"));
2665
+ let removed = 0;
2666
+ let failed = 0;
2667
+ const errors = [];
2668
+ for (const containerInfo of isol8Containers) {
2669
+ try {
2670
+ const container = dockerInstance.getContainer(containerInfo.Id);
2671
+ await container.remove({ force: true });
2672
+ removed++;
2673
+ } catch (err) {
2674
+ failed++;
2675
+ const errorMsg = err instanceof Error ? err.message : String(err);
2676
+ errors.push(`${containerInfo.Id.slice(0, 12)}: ${errorMsg}`);
2677
+ }
2678
+ }
2679
+ return { removed, failed, errors };
2680
+ }
2681
+ static async cleanupImages(docker) {
2682
+ const dockerInstance = docker ?? new Docker;
2683
+ const images = await dockerInstance.listImages({ all: true });
2684
+ const isol8Images = images.filter((img) => img.RepoTags?.some((tag) => tag.startsWith("isol8:")));
2685
+ let removed = 0;
2686
+ let failed = 0;
2687
+ const errors = [];
2688
+ for (const imageInfo of isol8Images) {
2689
+ try {
2690
+ const image = dockerInstance.getImage(imageInfo.Id);
2691
+ await image.remove({ force: true });
2692
+ removed++;
2693
+ } catch (err) {
2694
+ failed++;
2695
+ const errorMsg = err instanceof Error ? err.message : String(err);
2696
+ const imageRef = imageInfo.RepoTags?.[0] ?? imageInfo.Id.slice(0, 12);
2697
+ errors.push(`${imageRef}: ${errorMsg}`);
2698
+ }
2699
+ }
2700
+ return { removed, failed, errors };
2701
+ }
2702
+ }
2703
+
2704
+ // src/index.ts
2705
+ init_image_builder();
2706
+ init_runtime();
2707
+ init_logger();
2708
+ // package.json
2709
+ var package_default = {
2710
+ name: "@isol8/core",
2711
+ version: "0.13.0-alpha.0",
2712
+ description: "Core engine for isol8 secure code execution",
2713
+ author: "Illusion47586",
2714
+ license: "MIT",
2715
+ type: "module",
2716
+ main: "./dist/index.js",
2717
+ types: "./dist/index.d.ts",
2718
+ exports: {
2719
+ ".": {
2720
+ import: "./dist/index.js",
2721
+ types: "./dist/index.d.ts"
2722
+ },
2723
+ "./schema": "./schema/isol8.config.schema.json"
2724
+ },
2725
+ scripts: {
2726
+ build: "bun run scripts/build.ts",
2727
+ test: "bun test tests/unit/",
2728
+ "test:prod": "echo 'No production tests in core package'",
2729
+ "lint:check": "ultracite check",
2730
+ "lint:fix": "ultracite fix",
2731
+ schema: "ts-json-schema-generator --path src/types.ts --type Isol8UserConfig --tsconfig tsconfig.json -o schema/isol8.config.schema.json && ultracite fix schema/isol8.config.schema.json"
2732
+ },
2733
+ dependencies: {
2734
+ dockerode: "^4.0.9",
2735
+ hono: "^4.11.9"
2736
+ },
2737
+ devDependencies: {
2738
+ "@types/bun": "latest",
2739
+ "@types/dockerode": "^4.0.1",
2740
+ "@types/node": "^25.2.3",
2741
+ "ts-json-schema-generator": "^2.5.0",
2742
+ typescript: "^5.9.3",
2743
+ ultracite: "^7.2.0"
2744
+ },
2745
+ files: [
2746
+ "dist",
2747
+ "schema",
2748
+ "docker"
2749
+ ],
2750
+ jsonValidation: [
2751
+ {
2752
+ fileMatch: "isol8.config.json",
2753
+ url: "./schema/isol8.config.schema.json"
2754
+ }
2755
+ ]
2756
+ };
2757
+
2758
+ // src/version.ts
2759
+ var VERSION = package_default.version;
2760
+ export {
2761
+ logger,
2762
+ loadConfig,
2763
+ buildCustomImages,
2764
+ buildBaseImages,
2765
+ bashAdapter,
2766
+ VERSION,
2767
+ Semaphore,
2768
+ RuntimeRegistry,
2769
+ RemoteIsol8,
2770
+ PythonAdapter,
2771
+ NodeAdapter,
2772
+ DockerIsol8,
2773
+ DenoAdapter,
2774
+ BunAdapter
2775
+ };
2776
+
2777
+ //# debugId=22F12DAC58898F0F64756E2164756E21