isol8 0.11.0 → 0.11.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -6929,11 +6929,6 @@ var require_utils2 = __commonJS((exports, module) => {
6929
6929
  };
6930
6930
  });
6931
6931
 
6932
- // node_modules/ssh2/lib/protocol/crypto/build/Release/sshcrypto.node
6933
- var require_sshcrypto = __commonJS((exports, module) => {
6934
- module.exports = __require("./sshcrypto-0209sx47.node");
6935
- });
6936
-
6937
6932
  // node_modules/ssh2/lib/protocol/crypto/poly1305.js
6938
6933
  var require_poly1305 = __commonJS((exports, module) => {
6939
6934
  var __dirname = "/home/runner/work/isol8/isol8/node_modules/ssh2/lib/protocol/crypto", __filename = "/home/runner/work/isol8/isol8/node_modules/ssh2/lib/protocol/crypto/poly1305.js";
@@ -7420,7 +7415,7 @@ var require_crypto = __commonJS((exports, module) => {
7420
7415
  var ChaChaPolyDecipher;
7421
7416
  var GenericDecipher;
7422
7417
  try {
7423
- binding = require_sshcrypto();
7418
+ binding = (()=>{throw new Error("Cannot require module "+"./crypto/build/Release/sshcrypto.node");})();
7424
7419
  ({
7425
7420
  AESGCMCipher,
7426
7421
  ChaChaPolyCipher,
@@ -54795,6 +54790,8 @@ function mergeConfig(defaults, overrides) {
54795
54790
  ...defaults.cleanup,
54796
54791
  ...overrides.cleanup
54797
54792
  },
54793
+ poolStrategy: overrides.poolStrategy ?? defaults.poolStrategy,
54794
+ poolSize: overrides.poolSize ?? defaults.poolSize,
54798
54795
  dependencies: {
54799
54796
  ...defaults.dependencies,
54800
54797
  ...overrides.dependencies
@@ -54837,6 +54834,8 @@ var init_config = __esm(() => {
54837
54834
  autoPrune: true,
54838
54835
  maxContainerAgeMs: 3600000
54839
54836
  },
54837
+ poolStrategy: "fast",
54838
+ poolSize: { clean: 1, dirty: 1 },
54840
54839
  dependencies: {},
54841
54840
  security: {
54842
54841
  seccomp: "strict"
@@ -55413,6 +55412,333 @@ class Semaphore {
55413
55412
  }
55414
55413
  }
55415
55414
 
55415
+ // src/engine/utils.ts
55416
+ var exports_utils = {};
55417
+ __export(exports_utils, {
55418
+ validatePackageName: () => validatePackageName,
55419
+ truncateOutput: () => truncateOutput,
55420
+ parseMemoryLimit: () => parseMemoryLimit,
55421
+ maskSecrets: () => maskSecrets,
55422
+ extractFromTar: () => extractFromTar,
55423
+ createTarBuffer: () => createTarBuffer
55424
+ });
55425
+ function parseMemoryLimit(limit) {
55426
+ const match = limit.match(/^(\d+(?:\.\d+)?)\s*([kmgt]?)b?$/i);
55427
+ if (!match) {
55428
+ throw new Error(`Invalid memory limit format: "${limit}". Use e.g. "512m", "1g".`);
55429
+ }
55430
+ const value = Number.parseFloat(match[1]);
55431
+ const unit = (match[2] || "b").toLowerCase();
55432
+ const multipliers = {
55433
+ b: 1,
55434
+ k: 1024,
55435
+ m: 1024 ** 2,
55436
+ g: 1024 ** 3,
55437
+ t: 1024 ** 4
55438
+ };
55439
+ return Math.floor(value * (multipliers[unit] ?? 1));
55440
+ }
55441
+ function truncateOutput(output, maxBytes) {
55442
+ const encoder = new TextEncoder;
55443
+ const bytes = encoder.encode(output);
55444
+ if (bytes.length <= maxBytes) {
55445
+ return { text: output, truncated: false };
55446
+ }
55447
+ const decoder = new TextDecoder("utf-8", { fatal: false });
55448
+ const truncated = decoder.decode(bytes.slice(0, maxBytes));
55449
+ return {
55450
+ text: `${truncated}
55451
+
55452
+ --- OUTPUT TRUNCATED (${bytes.length} bytes, limit: ${maxBytes}) ---`,
55453
+ truncated: true
55454
+ };
55455
+ }
55456
+ function maskSecrets(text, secrets) {
55457
+ let result = text;
55458
+ for (const value of Object.values(secrets)) {
55459
+ if (value.length > 0) {
55460
+ result = result.replaceAll(value, "***");
55461
+ }
55462
+ }
55463
+ return result;
55464
+ }
55465
+ function createTarBuffer(filePath, content) {
55466
+ const data = typeof content === "string" ? Buffer.from(content, "utf-8") : content;
55467
+ const headerSize = 512;
55468
+ const dataBlocks = Math.ceil(data.length / 512);
55469
+ const totalSize = headerSize + dataBlocks * 512 + 1024;
55470
+ const buf = Buffer.alloc(totalSize);
55471
+ buf.write(filePath.replace(/^\//, ""), 0, 100, "utf-8");
55472
+ buf.write("0000644\x00", 100, 8, "utf-8");
55473
+ buf.write("0000000\x00", 108, 8, "utf-8");
55474
+ buf.write("0000000\x00", 116, 8, "utf-8");
55475
+ buf.write(`${data.length.toString(8).padStart(11, "0")}\x00`, 124, 12, "utf-8");
55476
+ buf.write(`${Math.floor(Date.now() / 1000).toString(8).padStart(11, "0")}\x00`, 136, 12, "utf-8");
55477
+ buf.write("0", 156, 1, "utf-8");
55478
+ buf.write("ustar\x00", 257, 6, "utf-8");
55479
+ buf.write("00", 263, 2, "utf-8");
55480
+ buf.write(" ", 148, 8, "utf-8");
55481
+ let checksum = 0;
55482
+ for (let i = 0;i < headerSize; i++) {
55483
+ checksum += buf[i];
55484
+ }
55485
+ buf.write(`${checksum.toString(8).padStart(6, "0")}\x00 `, 148, 8, "utf-8");
55486
+ data.copy(buf, headerSize);
55487
+ return buf;
55488
+ }
55489
+ function extractFromTar(tarBuffer, targetPath) {
55490
+ const normalizedTarget = targetPath.replace(/^\//, "");
55491
+ const basename = targetPath.split("/").pop() ?? targetPath;
55492
+ let offset = 0;
55493
+ while (offset < tarBuffer.length - 512) {
55494
+ const nameEnd = tarBuffer.indexOf(0, offset);
55495
+ const name = tarBuffer.subarray(offset, Math.min(nameEnd, offset + 100)).toString("utf-8");
55496
+ if (name.length === 0) {
55497
+ break;
55498
+ }
55499
+ const sizeStr = tarBuffer.subarray(offset + 124, offset + 136).toString("utf-8").trim();
55500
+ const size = Number.parseInt(sizeStr, 8);
55501
+ if (Number.isNaN(size)) {
55502
+ break;
55503
+ }
55504
+ const dataStart = offset + 512;
55505
+ const dataBlocks = Math.ceil(size / 512);
55506
+ if (name === normalizedTarget || name.endsWith(`/${normalizedTarget}`) || name === basename) {
55507
+ return Buffer.from(tarBuffer.subarray(dataStart, dataStart + size));
55508
+ }
55509
+ offset = dataStart + dataBlocks * 512;
55510
+ }
55511
+ throw new Error(`File "${targetPath}" not found in tar archive`);
55512
+ }
55513
+ function validatePackageName(name) {
55514
+ if (!/^[@a-zA-Z0-9_./\-=]+$/.test(name)) {
55515
+ throw new Error(`Invalid package name: "${name}". Only alphanumeric, -, _, ., /, @, and = are allowed.`);
55516
+ }
55517
+ return name;
55518
+ }
55519
+
55520
+ // src/engine/image-builder.ts
55521
+ import { createHash as createHash2 } from "node:crypto";
55522
+ import { existsSync as existsSync3, readFileSync as readFileSync2 } from "node:fs";
55523
+ import { join as join3 } from "node:path";
55524
+ function resolveDockerDir() {
55525
+ const fromBundled = new URL("./docker", import.meta.url).pathname;
55526
+ if (existsSync3(fromBundled)) {
55527
+ return fromBundled;
55528
+ }
55529
+ return new URL("../../docker", import.meta.url).pathname;
55530
+ }
55531
+ function computeDockerDirHash() {
55532
+ const hash = createHash2("sha256");
55533
+ const files = [...DOCKER_BUILD_FILES].sort();
55534
+ for (const file of files) {
55535
+ const filePath = join3(DOCKERFILE_DIR, file);
55536
+ if (existsSync3(filePath)) {
55537
+ const content = readFileSync2(filePath);
55538
+ hash.update(file);
55539
+ hash.update(content);
55540
+ }
55541
+ }
55542
+ return hash.digest("hex");
55543
+ }
55544
+ function computeDepsHash(runtime, packages) {
55545
+ const hash = createHash2("sha256");
55546
+ hash.update(runtime);
55547
+ for (const pkg of [...packages].sort()) {
55548
+ hash.update(pkg);
55549
+ }
55550
+ return hash.digest("hex");
55551
+ }
55552
+ function normalizePackages(packages) {
55553
+ return [...new Set(packages.map((pkg) => pkg.trim()).filter(Boolean))].sort();
55554
+ }
55555
+ function getCustomImageTag(runtime, packages) {
55556
+ const normalizedPackages = normalizePackages(packages);
55557
+ const depsHash = computeDepsHash(runtime, normalizedPackages);
55558
+ const shortHash = depsHash.slice(0, 12);
55559
+ return `isol8:${runtime}-custom-${shortHash}`;
55560
+ }
55561
+ async function getImageLabels(docker, imageName) {
55562
+ try {
55563
+ const image = docker.getImage(imageName);
55564
+ const inspect = await image.inspect();
55565
+ return inspect.Config?.Labels ?? {};
55566
+ } catch {
55567
+ return null;
55568
+ }
55569
+ }
55570
+ async function removeImage(docker, imageId) {
55571
+ try {
55572
+ const image = docker.getImage(imageId);
55573
+ await image.remove();
55574
+ logger.debug(`[ImageBuilder] Removed old image: ${imageId.slice(0, 12)}`);
55575
+ } catch (err) {
55576
+ logger.debug(`[ImageBuilder] Could not remove image ${imageId.slice(0, 12)}: ${err}`);
55577
+ }
55578
+ }
55579
+ async function buildBaseImages(docker, onProgress, force = false) {
55580
+ const runtimes = RuntimeRegistry.list();
55581
+ const dockerHash = computeDockerDirHash();
55582
+ logger.debug(`[ImageBuilder] Docker directory hash: ${dockerHash.slice(0, 16)}...`);
55583
+ for (const adapter of runtimes) {
55584
+ const target = adapter.name;
55585
+ const imageName = adapter.image;
55586
+ if (!force) {
55587
+ const labels = await getImageLabels(docker, imageName);
55588
+ if (labels && labels[LABELS.dockerHash] === dockerHash) {
55589
+ logger.debug(`[ImageBuilder] Base image ${target} is up to date, skipping build`);
55590
+ onProgress?.({ runtime: target, status: "done", message: "Up to date" });
55591
+ continue;
55592
+ }
55593
+ }
55594
+ let oldImageId = null;
55595
+ try {
55596
+ const oldImage = await docker.getImage(imageName).inspect();
55597
+ oldImageId = oldImage.Id;
55598
+ logger.debug(`[ImageBuilder] Existing image ${target} ID: ${oldImageId.slice(0, 12)}`);
55599
+ } catch {
55600
+ logger.debug(`[ImageBuilder] No existing image for ${target}`);
55601
+ }
55602
+ onProgress?.({ runtime: target, status: "building" });
55603
+ try {
55604
+ const stream = await docker.buildImage({ context: DOCKERFILE_DIR, src: DOCKER_BUILD_FILES }, {
55605
+ t: imageName,
55606
+ target,
55607
+ dockerfile: "Dockerfile",
55608
+ labels: {
55609
+ [LABELS.dockerHash]: dockerHash
55610
+ }
55611
+ });
55612
+ await new Promise((resolve2, reject) => {
55613
+ docker.modem.followProgress(stream, (err) => {
55614
+ if (err) {
55615
+ reject(err);
55616
+ } else {
55617
+ resolve2();
55618
+ }
55619
+ });
55620
+ });
55621
+ if (oldImageId) {
55622
+ await removeImage(docker, oldImageId);
55623
+ }
55624
+ onProgress?.({ runtime: target, status: "done" });
55625
+ } catch (err) {
55626
+ const message = err instanceof Error ? err.message : String(err);
55627
+ onProgress?.({ runtime: target, status: "error", message });
55628
+ throw new Error(`Failed to build image for ${target}: ${message}`);
55629
+ }
55630
+ }
55631
+ }
55632
+ async function buildCustomImages(docker, config, onProgress, force = false) {
55633
+ const deps = config.dependencies;
55634
+ const python = deps.python ? normalizePackages(deps.python) : [];
55635
+ const node = deps.node ? normalizePackages(deps.node) : [];
55636
+ const bun = deps.bun ? normalizePackages(deps.bun) : [];
55637
+ const deno = deps.deno ? normalizePackages(deps.deno) : [];
55638
+ const bash = deps.bash ? normalizePackages(deps.bash) : [];
55639
+ if (python.length) {
55640
+ await buildCustomImage(docker, "python", python, onProgress, force);
55641
+ }
55642
+ if (node.length) {
55643
+ await buildCustomImage(docker, "node", node, onProgress, force);
55644
+ }
55645
+ if (bun.length) {
55646
+ await buildCustomImage(docker, "bun", bun, onProgress, force);
55647
+ }
55648
+ if (deno.length) {
55649
+ await buildCustomImage(docker, "deno", deno, onProgress, force);
55650
+ }
55651
+ if (bash.length) {
55652
+ await buildCustomImage(docker, "bash", bash, onProgress, force);
55653
+ }
55654
+ }
55655
+ async function buildCustomImage(docker, runtime, packages, onProgress, force = false) {
55656
+ const normalizedPackages = normalizePackages(packages);
55657
+ const tag = getCustomImageTag(runtime, normalizedPackages);
55658
+ const depsHash = computeDepsHash(runtime, normalizedPackages);
55659
+ logger.debug(`[ImageBuilder] ${runtime} custom deps hash: ${depsHash.slice(0, 16)}...`);
55660
+ if (!force) {
55661
+ const labels = await getImageLabels(docker, tag);
55662
+ if (labels && labels[LABELS.depsHash] === depsHash) {
55663
+ logger.debug(`[ImageBuilder] Custom image ${runtime} is up to date, skipping build`);
55664
+ onProgress?.({ runtime, status: "done", message: "Up to date" });
55665
+ return;
55666
+ }
55667
+ }
55668
+ let oldImageId = null;
55669
+ try {
55670
+ const oldImage = await docker.getImage(tag).inspect();
55671
+ oldImageId = oldImage.Id;
55672
+ logger.debug(`[ImageBuilder] Existing custom image ${runtime} ID: ${oldImageId.slice(0, 12)}`);
55673
+ } catch {
55674
+ logger.debug(`[ImageBuilder] No existing custom image for ${runtime}`);
55675
+ }
55676
+ onProgress?.({
55677
+ runtime,
55678
+ status: "building",
55679
+ message: `Custom: ${normalizedPackages.join(", ")}`
55680
+ });
55681
+ let installCmd;
55682
+ switch (runtime) {
55683
+ case "python":
55684
+ installCmd = `RUN pip install --no-cache-dir ${normalizedPackages.join(" ")}`;
55685
+ break;
55686
+ case "node":
55687
+ installCmd = `RUN npm install -g ${normalizedPackages.join(" ")}`;
55688
+ break;
55689
+ case "bun":
55690
+ installCmd = `RUN bun install -g ${normalizedPackages.join(" ")}`;
55691
+ break;
55692
+ case "deno":
55693
+ installCmd = normalizedPackages.map((p) => `RUN deno cache ${p}`).join(`
55694
+ `);
55695
+ break;
55696
+ case "bash":
55697
+ installCmd = `RUN apk add --no-cache ${normalizedPackages.join(" ")}`;
55698
+ break;
55699
+ default:
55700
+ throw new Error(`Unknown runtime: ${runtime}`);
55701
+ }
55702
+ const dockerfileContent = `FROM isol8:${runtime}
55703
+ ${installCmd}
55704
+ `;
55705
+ const { createTarBuffer: createTarBuffer2, validatePackageName: validatePackageName2 } = await Promise.resolve().then(() => exports_utils);
55706
+ const { Readable } = await import("node:stream");
55707
+ normalizedPackages.forEach(validatePackageName2);
55708
+ const tarBuffer = createTarBuffer2("Dockerfile", dockerfileContent);
55709
+ const stream = await docker.buildImage(Readable.from(tarBuffer), {
55710
+ t: tag,
55711
+ dockerfile: "Dockerfile",
55712
+ labels: {
55713
+ [LABELS.depsHash]: depsHash
55714
+ }
55715
+ });
55716
+ await new Promise((resolve2, reject) => {
55717
+ docker.modem.followProgress(stream, (err) => {
55718
+ if (err) {
55719
+ reject(err);
55720
+ } else {
55721
+ resolve2();
55722
+ }
55723
+ });
55724
+ });
55725
+ if (oldImageId) {
55726
+ await removeImage(docker, oldImageId);
55727
+ }
55728
+ onProgress?.({ runtime, status: "done" });
55729
+ }
55730
+ var DOCKERFILE_DIR, LABELS, DOCKER_BUILD_FILES;
55731
+ var init_image_builder = __esm(() => {
55732
+ init_runtime();
55733
+ init_logger();
55734
+ DOCKERFILE_DIR = resolveDockerDir();
55735
+ LABELS = {
55736
+ dockerHash: "org.isol8.build.hash",
55737
+ depsHash: "org.isol8.deps.hash"
55738
+ };
55739
+ DOCKER_BUILD_FILES = ["Dockerfile", "proxy.sh", "proxy-handler.sh"];
55740
+ });
55741
+
55416
55742
  // src/engine/pool.ts
55417
55743
  class ContainerPool {
55418
55744
  docker;
@@ -55603,46 +55929,44 @@ class ContainerPool {
55603
55929
  }
55604
55930
  replenish(image) {
55605
55931
  if (this.replenishing.has(image)) {
55606
- if (this.replenishing.has(image)) {
55607
- return;
55608
- }
55609
- const pool = this.pools.get(image);
55610
- const currentSize = pool ? this.poolStrategy === "fast" ? pool.clean.length : pool.clean?.length ?? 0 : 0;
55611
- const targetSize = this.poolStrategy === "fast" ? this.cleanPoolSize : this.cleanPoolSize;
55612
- if (currentSize >= targetSize) {
55932
+ return;
55933
+ }
55934
+ const pool = this.pools.get(image);
55935
+ const currentSize = pool ? this.poolStrategy === "fast" ? pool.clean.length : pool.clean?.length ?? 0 : 0;
55936
+ const targetSize = this.cleanPoolSize;
55937
+ if (currentSize >= targetSize) {
55938
+ return;
55939
+ }
55940
+ this.replenishing.add(image);
55941
+ const promise = this.createContainer(image).then((container) => {
55942
+ const p = this.pools.get(image);
55943
+ if (!p) {
55944
+ container.remove({ force: true }).catch(() => {});
55613
55945
  return;
55614
55946
  }
55615
- this.replenishing.add(image);
55616
- const promise = this.createContainer(image).then((container) => {
55617
- const p = this.pools.get(image);
55618
- if (!p) {
55947
+ if (this.poolStrategy === "fast") {
55948
+ if (p.clean.length < this.cleanPoolSize) {
55949
+ p.clean.push({ container, createdAt: Date.now() });
55950
+ } else {
55619
55951
  container.remove({ force: true }).catch(() => {});
55620
- return;
55621
55952
  }
55622
- if (this.poolStrategy === "fast") {
55623
- if (p.clean.length < this.cleanPoolSize) {
55624
- p.clean.push({ container, createdAt: Date.now() });
55625
- } else {
55626
- container.remove({ force: true }).catch(() => {});
55627
- }
55953
+ } else {
55954
+ if (!p.clean) {
55955
+ p.clean = [];
55956
+ }
55957
+ if (p.clean.length < this.cleanPoolSize) {
55958
+ p.clean.push({ container, createdAt: Date.now() });
55628
55959
  } else {
55629
- if (!p.clean) {
55630
- p.clean = [];
55631
- }
55632
- if (p.clean.length < this.cleanPoolSize) {
55633
- p.clean.push({ container, createdAt: Date.now() });
55634
- } else {
55635
- container.remove({ force: true }).catch(() => {});
55636
- }
55960
+ container.remove({ force: true }).catch(() => {});
55637
55961
  }
55638
- }).catch((err) => {
55639
- logger.error(`[Pool] Error during replenishment for ${image}:`, err);
55640
- }).finally(() => {
55641
- this.replenishing.delete(image);
55642
- this.pendingReplenishments.delete(promise);
55643
- });
55644
- this.pendingReplenishments.add(promise);
55645
- }
55962
+ }
55963
+ }).catch((err) => {
55964
+ logger.error(`[Pool] Error during replenishment for ${image}:`, err);
55965
+ }).finally(() => {
55966
+ this.replenishing.delete(image);
55967
+ this.pendingReplenishments.delete(promise);
55968
+ });
55969
+ this.pendingReplenishments.add(promise);
55646
55970
  }
55647
55971
  }
55648
55972
  var init_pool = __esm(() => {
@@ -55694,118 +56018,13 @@ function calculateResourceDelta(before, after) {
55694
56018
  };
55695
56019
  }
55696
56020
 
55697
- // src/engine/utils.ts
55698
- var exports_utils = {};
55699
- __export(exports_utils, {
55700
- validatePackageName: () => validatePackageName,
55701
- truncateOutput: () => truncateOutput,
55702
- parseMemoryLimit: () => parseMemoryLimit,
55703
- maskSecrets: () => maskSecrets,
55704
- extractFromTar: () => extractFromTar,
55705
- createTarBuffer: () => createTarBuffer
55706
- });
55707
- function parseMemoryLimit(limit) {
55708
- const match = limit.match(/^(\d+(?:\.\d+)?)\s*([kmgt]?)b?$/i);
55709
- if (!match) {
55710
- throw new Error(`Invalid memory limit format: "${limit}". Use e.g. "512m", "1g".`);
55711
- }
55712
- const value = Number.parseFloat(match[1]);
55713
- const unit = (match[2] || "b").toLowerCase();
55714
- const multipliers = {
55715
- b: 1,
55716
- k: 1024,
55717
- m: 1024 ** 2,
55718
- g: 1024 ** 3,
55719
- t: 1024 ** 4
55720
- };
55721
- return Math.floor(value * (multipliers[unit] ?? 1));
55722
- }
55723
- function truncateOutput(output, maxBytes) {
55724
- const encoder = new TextEncoder;
55725
- const bytes = encoder.encode(output);
55726
- if (bytes.length <= maxBytes) {
55727
- return { text: output, truncated: false };
55728
- }
55729
- const decoder = new TextDecoder("utf-8", { fatal: false });
55730
- const truncated = decoder.decode(bytes.slice(0, maxBytes));
55731
- return {
55732
- text: `${truncated}
55733
-
55734
- --- OUTPUT TRUNCATED (${bytes.length} bytes, limit: ${maxBytes}) ---`,
55735
- truncated: true
55736
- };
55737
- }
55738
- function maskSecrets(text, secrets) {
55739
- let result = text;
55740
- for (const value of Object.values(secrets)) {
55741
- if (value.length > 0) {
55742
- result = result.replaceAll(value, "***");
55743
- }
55744
- }
55745
- return result;
55746
- }
55747
- function createTarBuffer(filePath, content) {
55748
- const data = typeof content === "string" ? Buffer.from(content, "utf-8") : content;
55749
- const headerSize = 512;
55750
- const dataBlocks = Math.ceil(data.length / 512);
55751
- const totalSize = headerSize + dataBlocks * 512 + 1024;
55752
- const buf = Buffer.alloc(totalSize);
55753
- buf.write(filePath.replace(/^\//, ""), 0, 100, "utf-8");
55754
- buf.write("0000644\x00", 100, 8, "utf-8");
55755
- buf.write("0000000\x00", 108, 8, "utf-8");
55756
- buf.write("0000000\x00", 116, 8, "utf-8");
55757
- buf.write(`${data.length.toString(8).padStart(11, "0")}\x00`, 124, 12, "utf-8");
55758
- buf.write(`${Math.floor(Date.now() / 1000).toString(8).padStart(11, "0")}\x00`, 136, 12, "utf-8");
55759
- buf.write("0", 156, 1, "utf-8");
55760
- buf.write("ustar\x00", 257, 6, "utf-8");
55761
- buf.write("00", 263, 2, "utf-8");
55762
- buf.write(" ", 148, 8, "utf-8");
55763
- let checksum = 0;
55764
- for (let i = 0;i < headerSize; i++) {
55765
- checksum += buf[i];
55766
- }
55767
- buf.write(`${checksum.toString(8).padStart(6, "0")}\x00 `, 148, 8, "utf-8");
55768
- data.copy(buf, headerSize);
55769
- return buf;
55770
- }
55771
- function extractFromTar(tarBuffer, targetPath) {
55772
- const normalizedTarget = targetPath.replace(/^\//, "");
55773
- const basename = targetPath.split("/").pop() ?? targetPath;
55774
- let offset = 0;
55775
- while (offset < tarBuffer.length - 512) {
55776
- const nameEnd = tarBuffer.indexOf(0, offset);
55777
- const name = tarBuffer.subarray(offset, Math.min(nameEnd, offset + 100)).toString("utf-8");
55778
- if (name.length === 0) {
55779
- break;
55780
- }
55781
- const sizeStr = tarBuffer.subarray(offset + 124, offset + 136).toString("utf-8").trim();
55782
- const size = Number.parseInt(sizeStr, 8);
55783
- if (Number.isNaN(size)) {
55784
- break;
55785
- }
55786
- const dataStart = offset + 512;
55787
- const dataBlocks = Math.ceil(size / 512);
55788
- if (name === normalizedTarget || name.endsWith(`/${normalizedTarget}`) || name === basename) {
55789
- return Buffer.from(tarBuffer.subarray(dataStart, dataStart + size));
55790
- }
55791
- offset = dataStart + dataBlocks * 512;
55792
- }
55793
- throw new Error(`File "${targetPath}" not found in tar archive`);
55794
- }
55795
- function validatePackageName(name) {
55796
- if (!/^[@a-zA-Z0-9_./\-=]+$/.test(name)) {
55797
- throw new Error(`Invalid package name: "${name}". Only alphanumeric, -, _, ., /, @, and = are allowed.`);
55798
- }
55799
- return name;
55800
- }
55801
-
55802
56021
  // src/engine/docker.ts
55803
56022
  var exports_docker = {};
55804
56023
  __export(exports_docker, {
55805
56024
  DockerIsol8: () => DockerIsol8
55806
56025
  });
55807
56026
  import { randomUUID } from "node:crypto";
55808
- import { existsSync as existsSync3, readFileSync as readFileSync2 } from "node:fs";
56027
+ import { existsSync as existsSync4, readFileSync as readFileSync3 } from "node:fs";
55809
56028
  import { PassThrough } from "node:stream";
55810
56029
  async function writeFileViaExec(container, filePath, content) {
55811
56030
  const data = typeof content === "string" ? Buffer.from(content, "utf-8") : content;
@@ -56021,6 +56240,7 @@ class DockerIsol8 {
56021
56240
  logNetwork;
56022
56241
  poolStrategy;
56023
56242
  poolSize;
56243
+ dependencies;
56024
56244
  auditLogger;
56025
56245
  remoteCodePolicy;
56026
56246
  container = null;
@@ -56067,6 +56287,7 @@ class DockerIsol8 {
56067
56287
  this.logNetwork = options.logNetwork ?? false;
56068
56288
  this.poolStrategy = options.poolStrategy ?? "fast";
56069
56289
  this.poolSize = options.poolSize ?? { clean: 1, dirty: 1 };
56290
+ this.dependencies = options.dependencies ?? {};
56070
56291
  this.remoteCodePolicy = options.remoteCode ?? {
56071
56292
  enabled: false,
56072
56293
  allowedSchemes: ["https"],
@@ -56085,7 +56306,33 @@ class DockerIsol8 {
56085
56306
  logger.setDebug(true);
56086
56307
  }
56087
56308
  }
56088
- async start() {}
56309
+ async start(options = {}) {
56310
+ if (this.mode !== "ephemeral") {
56311
+ return;
56312
+ }
56313
+ const prewarm = options.prewarm;
56314
+ if (!prewarm) {
56315
+ return;
56316
+ }
56317
+ const pool = this.ensurePool();
56318
+ const images = new Set;
56319
+ const adapters2 = typeof prewarm === "object" && prewarm.runtimes?.length ? prewarm.runtimes.map((runtime) => RuntimeRegistry.get(runtime)) : RuntimeRegistry.list();
56320
+ for (const adapter of adapters2) {
56321
+ try {
56322
+ images.add(await this.resolveImage(adapter));
56323
+ } catch (err) {
56324
+ logger.debug(`[Pool] Pre-warm image resolution failed for ${adapter.name}: ${err}`);
56325
+ }
56326
+ }
56327
+ await Promise.all([...images].map(async (image) => {
56328
+ try {
56329
+ await pool.warm(image);
56330
+ logger.debug(`[Pool] Pre-warmed image: ${image}`);
56331
+ } catch (err) {
56332
+ logger.debug(`[Pool] Pre-warm failed for ${image}: ${err}`);
56333
+ }
56334
+ }));
56335
+ }
56089
56336
  async stop() {
56090
56337
  if (this.container) {
56091
56338
  try {
@@ -56334,21 +56581,29 @@ class DockerIsol8 {
56334
56581
  if (cached) {
56335
56582
  return cached;
56336
56583
  }
56337
- const customTag = `${adapter.image}-custom`;
56338
- let resolvedImage;
56339
- try {
56340
- await this.docker.getImage(customTag).inspect();
56341
- resolvedImage = customTag;
56342
- } catch {
56343
- resolvedImage = adapter.image;
56584
+ let resolvedImage = adapter.image;
56585
+ const configuredDeps = this.dependencies[adapter.name];
56586
+ const normalizedDeps = configuredDeps ? normalizePackages(configuredDeps) : [];
56587
+ if (normalizedDeps.length > 0) {
56588
+ const hashedCustomTag = getCustomImageTag(adapter.name, normalizedDeps);
56589
+ try {
56590
+ await this.docker.getImage(hashedCustomTag).inspect();
56591
+ resolvedImage = hashedCustomTag;
56592
+ } catch {
56593
+ logger.debug(`[ImageBuilder] Hashed custom image not found for ${adapter.name}: ${hashedCustomTag}`);
56594
+ }
56595
+ }
56596
+ if (resolvedImage === adapter.image) {
56597
+ const legacyCustomTag = `${adapter.image}-custom`;
56598
+ try {
56599
+ await this.docker.getImage(legacyCustomTag).inspect();
56600
+ resolvedImage = legacyCustomTag;
56601
+ } catch {}
56344
56602
  }
56345
56603
  this.imageCache.set(cacheKey, resolvedImage);
56346
56604
  return resolvedImage;
56347
56605
  }
56348
- async executeEphemeral(req, startTime) {
56349
- const adapter = this.getAdapter(req.runtime);
56350
- const timeoutMs = req.timeoutMs ?? this.defaultTimeoutMs;
56351
- const image = await this.resolveImage(adapter);
56606
+ ensurePool() {
56352
56607
  if (!this.pool) {
56353
56608
  this.pool = new ContainerPool({
56354
56609
  docker: this.docker,
@@ -56366,7 +56621,14 @@ class DockerIsol8 {
56366
56621
  }
56367
56622
  });
56368
56623
  }
56369
- const container = await this.pool.acquire(image);
56624
+ return this.pool;
56625
+ }
56626
+ async executeEphemeral(req, startTime) {
56627
+ const adapter = this.getAdapter(req.runtime);
56628
+ const timeoutMs = req.timeoutMs ?? this.defaultTimeoutMs;
56629
+ const image = await this.resolveImage(adapter);
56630
+ const pool = this.ensurePool();
56631
+ const container = await pool.acquire(image);
56370
56632
  let startStats;
56371
56633
  if (this.auditLogger) {
56372
56634
  try {
@@ -56380,13 +56642,26 @@ class DockerIsol8 {
56380
56642
  await startProxy(container, this.networkFilter);
56381
56643
  await setupIptables(container);
56382
56644
  }
56383
- const ext = req.fileExtension ?? adapter.getFileExtension();
56384
- const filePath = `${SANDBOX_WORKDIR}/main${ext}`;
56385
- await writeFileViaExec(container, filePath, req.code);
56645
+ const canUseInline = !(req.stdin || req.files || req.outputPaths) && (!req.installPackages || req.installPackages.length === 0);
56646
+ let rawCmd;
56647
+ if (canUseInline) {
56648
+ try {
56649
+ rawCmd = adapter.getCommand(req.code);
56650
+ } catch {
56651
+ const ext = req.fileExtension ?? adapter.getFileExtension();
56652
+ const filePath = `${SANDBOX_WORKDIR}/main${ext}`;
56653
+ await writeFileViaExec(container, filePath, req.code);
56654
+ rawCmd = adapter.getCommand(req.code, filePath);
56655
+ }
56656
+ } else {
56657
+ const ext = req.fileExtension ?? adapter.getFileExtension();
56658
+ const filePath = `${SANDBOX_WORKDIR}/main${ext}`;
56659
+ await writeFileViaExec(container, filePath, req.code);
56660
+ rawCmd = adapter.getCommand(req.code, filePath);
56661
+ }
56386
56662
  if (req.installPackages?.length) {
56387
56663
  await installPackages(container, req.runtime, req.installPackages);
56388
56664
  }
56389
- const rawCmd = adapter.getCommand(req.code, filePath);
56390
56665
  const timeoutSec = Math.ceil(timeoutMs / 1000);
56391
56666
  let cmd;
56392
56667
  if (req.stdin) {
@@ -56457,7 +56732,7 @@ class DockerIsol8 {
56457
56732
  if (this.persist) {
56458
56733
  logger.debug(`[Persist] Leaving container running for inspection: ${container.id}`);
56459
56734
  } else {
56460
- this.pool.release(container, image).catch((err) => {
56735
+ pool.release(container, image).catch((err) => {
56461
56736
  logger.debug(`[Pool] release failed: ${err}`);
56462
56737
  container.remove({ force: true }).catch(() => {});
56463
56738
  });
@@ -56633,7 +56908,7 @@ class DockerIsol8 {
56633
56908
  }
56634
56909
  if (this.security.seccomp === "custom" && this.security.customProfilePath) {
56635
56910
  try {
56636
- const profile = readFileSync2(this.security.customProfilePath, "utf-8");
56911
+ const profile = readFileSync3(this.security.customProfilePath, "utf-8");
56637
56912
  opts.push(`seccomp=${profile}`);
56638
56913
  } catch (e) {
56639
56914
  logger.error(`Failed to load custom seccomp profile: ${e}`);
@@ -56652,12 +56927,12 @@ class DockerIsol8 {
56652
56927
  }
56653
56928
  loadDefaultSeccompProfile() {
56654
56929
  const devPath = new URL("../../docker/seccomp-profile.json", import.meta.url);
56655
- if (existsSync3(devPath)) {
56656
- return readFileSync2(devPath, "utf-8");
56930
+ if (existsSync4(devPath)) {
56931
+ return readFileSync3(devPath, "utf-8");
56657
56932
  }
56658
56933
  const prodPath = new URL("./docker/seccomp-profile.json", import.meta.url);
56659
- if (existsSync3(prodPath)) {
56660
- return readFileSync2(prodPath, "utf-8");
56934
+ if (existsSync4(prodPath)) {
56935
+ return readFileSync3(prodPath, "utf-8");
56661
56936
  }
56662
56937
  logger.warn("Could not locate default seccomp profile. Running without seccomp filter.");
56663
56938
  return null;
@@ -56840,7 +57115,7 @@ class DockerIsol8 {
56840
57115
  static async cleanup(docker) {
56841
57116
  const dockerInstance = docker ?? new import_dockerode.default;
56842
57117
  const containers = await dockerInstance.listContainers({ all: true });
56843
- const isol8Containers = containers.filter((c) => c.Image.startsWith("isol8:") || c.Image.startsWith("isol8-custom:"));
57118
+ const isol8Containers = containers.filter((c) => c.Image.startsWith("isol8:"));
56844
57119
  let removed = 0;
56845
57120
  let failed = 0;
56846
57121
  const errors = [];
@@ -56857,6 +57132,27 @@ class DockerIsol8 {
56857
57132
  }
56858
57133
  return { removed, failed, errors };
56859
57134
  }
57135
+ static async cleanupImages(docker) {
57136
+ const dockerInstance = docker ?? new import_dockerode.default;
57137
+ const images = await dockerInstance.listImages({ all: true });
57138
+ const isol8Images = images.filter((img) => img.RepoTags?.some((tag) => tag.startsWith("isol8:")));
57139
+ let removed = 0;
57140
+ let failed = 0;
57141
+ const errors = [];
57142
+ for (const imageInfo of isol8Images) {
57143
+ try {
57144
+ const image = dockerInstance.getImage(imageInfo.Id);
57145
+ await image.remove({ force: true });
57146
+ removed++;
57147
+ } catch (err) {
57148
+ failed++;
57149
+ const errorMsg = err instanceof Error ? err.message : String(err);
57150
+ const imageRef = imageInfo.RepoTags?.[0] ?? imageInfo.Id.slice(0, 12);
57151
+ errors.push(`${imageRef}: ${errorMsg}`);
57152
+ }
57153
+ }
57154
+ return { removed, failed, errors };
57155
+ }
56860
57156
  }
56861
57157
  var import_dockerode, SANDBOX_WORKDIR = "/sandbox", MAX_OUTPUT_BYTES, PROXY_PORT = 8118, PROXY_STARTUP_TIMEOUT_MS = 5000, PROXY_POLL_INTERVAL_MS = 100;
56862
57158
  var init_docker = __esm(() => {
@@ -56864,6 +57160,7 @@ var init_docker = __esm(() => {
56864
57160
  init_logger();
56865
57161
  init_audit();
56866
57162
  init_code_fetcher();
57163
+ init_image_builder();
56867
57164
  init_pool();
56868
57165
  import_dockerode = __toESM(require_docker(), 1);
56869
57166
  MAX_OUTPUT_BYTES = 1024 * 1024;
@@ -56874,7 +57171,7 @@ var package_default;
56874
57171
  var init_package = __esm(() => {
56875
57172
  package_default = {
56876
57173
  name: "isol8",
56877
- version: "0.10.3",
57174
+ version: "0.11.1",
56878
57175
  description: "Secure code execution engine for AI agents",
56879
57176
  author: "Illusion47586",
56880
57177
  license: "MIT",
@@ -56919,6 +57216,8 @@ var init_package = __esm(() => {
56919
57216
  "lint:check": "ultracite check",
56920
57217
  "lint:fix": "ultracite fix",
56921
57218
  bench: "bunx tsx benchmarks/spawn.ts",
57219
+ "bench:tti": "bunx tsx benchmarks/tti.ts",
57220
+ "bench:tti:pool": "bunx tsx benchmarks/tti.ts --warm-pool --iterations 5",
56922
57221
  "bench:pool": "bunx tsx benchmarks/spawn-pool.ts",
56923
57222
  "bench:detailed": "bunx tsx benchmarks/spawn-detailed.ts",
56924
57223
  "bench:cli": "bun run tests/production/bench-cli.ts",
@@ -58626,6 +58925,11 @@ async function createServer(options) {
58626
58925
  const body = await c.req.json();
58627
58926
  logger.debug(`[Server] POST /execute runtime=${body.request.runtime} sessionId=${body.sessionId ?? "ephemeral"}`);
58628
58927
  logger.debug(`[Server] Code source: ${body.request.codeUrl ? `url=${body.request.codeUrl}` : `inline (${body.request.code?.length ?? 0} chars)`}`);
58928
+ const {
58929
+ poolStrategy: _ignoredPoolStrategy,
58930
+ poolSize: _ignoredPoolSize,
58931
+ ...requestOptions
58932
+ } = body.options ?? {};
58629
58933
  const engineOptions = {
58630
58934
  network: config.defaults.network,
58631
58935
  memoryLimit: config.defaults.memoryLimit,
@@ -58633,8 +58937,11 @@ async function createServer(options) {
58633
58937
  timeoutMs: config.defaults.timeoutMs,
58634
58938
  sandboxSize: config.defaults.sandboxSize,
58635
58939
  tmpSize: config.defaults.tmpSize,
58940
+ poolStrategy: config.poolStrategy,
58941
+ poolSize: config.poolSize,
58942
+ dependencies: config.dependencies,
58636
58943
  remoteCode: config.remoteCode,
58637
- ...body.options,
58944
+ ...requestOptions,
58638
58945
  mode: body.sessionId ? "persistent" : "ephemeral",
58639
58946
  audit: config.audit
58640
58947
  };
@@ -58688,6 +58995,11 @@ async function createServer(options) {
58688
58995
  const body = await c.req.json();
58689
58996
  logger.debug(`[Server] POST /execute/stream runtime=${body.request.runtime}`);
58690
58997
  logger.debug(`[Server] Code source: ${body.request.codeUrl ? `url=${body.request.codeUrl}` : `inline (${body.request.code?.length ?? 0} chars)`}`);
58998
+ const {
58999
+ poolStrategy: _ignoredPoolStrategy,
59000
+ poolSize: _ignoredPoolSize,
59001
+ ...requestOptions
59002
+ } = body.options ?? {};
58691
59003
  const engineOptions = {
58692
59004
  network: config.defaults.network,
58693
59005
  memoryLimit: config.defaults.memoryLimit,
@@ -58695,8 +59007,11 @@ async function createServer(options) {
58695
59007
  timeoutMs: config.defaults.timeoutMs,
58696
59008
  sandboxSize: config.defaults.sandboxSize,
58697
59009
  tmpSize: config.defaults.tmpSize,
59010
+ poolStrategy: config.poolStrategy,
59011
+ poolSize: config.poolSize,
59012
+ dependencies: config.dependencies,
58698
59013
  remoteCode: config.remoteCode,
58699
- ...body.options,
59014
+ ...requestOptions,
58700
59015
  mode: "ephemeral"
58701
59016
  };
58702
59017
  const engine = new DockerIsol82(engineOptions, config.maxConcurrent);
@@ -62099,7 +62414,7 @@ class RemoteIsol8 {
62099
62414
  this.sessionId = options.sessionId;
62100
62415
  this.isol8Options = isol8Options;
62101
62416
  }
62102
- async start() {
62417
+ async start(_options) {
62103
62418
  const res = await this.fetch("/health");
62104
62419
  if (!res.ok) {
62105
62420
  throw new Error(`Remote server health check failed: ${res.status}`);
@@ -62217,208 +62532,7 @@ class RemoteIsol8 {
62217
62532
  // src/cli.ts
62218
62533
  init_config();
62219
62534
  init_docker();
62220
-
62221
- // src/engine/image-builder.ts
62222
- init_runtime();
62223
- init_logger();
62224
- import { createHash as createHash2 } from "node:crypto";
62225
- import { existsSync as existsSync4, readFileSync as readFileSync3 } from "node:fs";
62226
- import { join as join3 } from "node:path";
62227
- function resolveDockerDir() {
62228
- const fromBundled = new URL("./docker", import.meta.url).pathname;
62229
- if (existsSync4(fromBundled)) {
62230
- return fromBundled;
62231
- }
62232
- return new URL("../../docker", import.meta.url).pathname;
62233
- }
62234
- var DOCKERFILE_DIR = resolveDockerDir();
62235
- var LABELS = {
62236
- dockerHash: "org.isol8.build.hash",
62237
- depsHash: "org.isol8.deps.hash"
62238
- };
62239
- var DOCKER_BUILD_FILES = ["Dockerfile", "proxy.sh", "proxy-handler.sh"];
62240
- function computeDockerDirHash() {
62241
- const hash = createHash2("sha256");
62242
- const files = [...DOCKER_BUILD_FILES].sort();
62243
- for (const file of files) {
62244
- const filePath = join3(DOCKERFILE_DIR, file);
62245
- if (existsSync4(filePath)) {
62246
- const content = readFileSync3(filePath);
62247
- hash.update(file);
62248
- hash.update(content);
62249
- }
62250
- }
62251
- return hash.digest("hex");
62252
- }
62253
- function computeDepsHash(runtime, packages) {
62254
- const hash = createHash2("sha256");
62255
- hash.update(runtime);
62256
- for (const pkg of [...packages].sort()) {
62257
- hash.update(pkg);
62258
- }
62259
- return hash.digest("hex");
62260
- }
62261
- async function getImageLabels(docker, imageName) {
62262
- try {
62263
- const image = docker.getImage(imageName);
62264
- const inspect = await image.inspect();
62265
- return inspect.Config?.Labels ?? {};
62266
- } catch {
62267
- return null;
62268
- }
62269
- }
62270
- async function removeImage(docker, imageId) {
62271
- try {
62272
- const image = docker.getImage(imageId);
62273
- await image.remove();
62274
- logger.debug(`[ImageBuilder] Removed old image: ${imageId.slice(0, 12)}`);
62275
- } catch (err) {
62276
- logger.debug(`[ImageBuilder] Could not remove image ${imageId.slice(0, 12)}: ${err}`);
62277
- }
62278
- }
62279
- async function buildBaseImages(docker, onProgress, force = false) {
62280
- const runtimes = RuntimeRegistry.list();
62281
- const dockerHash = computeDockerDirHash();
62282
- logger.debug(`[ImageBuilder] Docker directory hash: ${dockerHash.slice(0, 16)}...`);
62283
- for (const adapter of runtimes) {
62284
- const target = adapter.name;
62285
- const imageName = adapter.image;
62286
- if (!force) {
62287
- const labels = await getImageLabels(docker, imageName);
62288
- if (labels && labels[LABELS.dockerHash] === dockerHash) {
62289
- logger.debug(`[ImageBuilder] Base image ${target} is up to date, skipping build`);
62290
- onProgress?.({ runtime: target, status: "done", message: "Up to date" });
62291
- continue;
62292
- }
62293
- }
62294
- let oldImageId = null;
62295
- try {
62296
- const oldImage = await docker.getImage(imageName).inspect();
62297
- oldImageId = oldImage.Id;
62298
- logger.debug(`[ImageBuilder] Existing image ${target} ID: ${oldImageId.slice(0, 12)}`);
62299
- } catch {
62300
- logger.debug(`[ImageBuilder] No existing image for ${target}`);
62301
- }
62302
- onProgress?.({ runtime: target, status: "building" });
62303
- try {
62304
- const stream = await docker.buildImage({ context: DOCKERFILE_DIR, src: DOCKER_BUILD_FILES }, {
62305
- t: imageName,
62306
- target,
62307
- dockerfile: "Dockerfile",
62308
- labels: {
62309
- [LABELS.dockerHash]: dockerHash
62310
- }
62311
- });
62312
- await new Promise((resolve2, reject) => {
62313
- docker.modem.followProgress(stream, (err) => {
62314
- if (err) {
62315
- reject(err);
62316
- } else {
62317
- resolve2();
62318
- }
62319
- });
62320
- });
62321
- if (oldImageId) {
62322
- await removeImage(docker, oldImageId);
62323
- }
62324
- onProgress?.({ runtime: target, status: "done" });
62325
- } catch (err) {
62326
- const message = err instanceof Error ? err.message : String(err);
62327
- onProgress?.({ runtime: target, status: "error", message });
62328
- throw new Error(`Failed to build image for ${target}: ${message}`);
62329
- }
62330
- }
62331
- }
62332
- async function buildCustomImages(docker, config, onProgress, force = false) {
62333
- const deps = config.dependencies;
62334
- if (deps.python?.length) {
62335
- await buildCustomImage(docker, "python", deps.python, onProgress, force);
62336
- }
62337
- if (deps.node?.length) {
62338
- await buildCustomImage(docker, "node", deps.node, onProgress, force);
62339
- }
62340
- if (deps.bun?.length) {
62341
- await buildCustomImage(docker, "bun", deps.bun, onProgress, force);
62342
- }
62343
- if (deps.deno?.length) {
62344
- await buildCustomImage(docker, "deno", deps.deno, onProgress, force);
62345
- }
62346
- if (deps.bash?.length) {
62347
- await buildCustomImage(docker, "bash", deps.bash, onProgress, force);
62348
- }
62349
- }
62350
- async function buildCustomImage(docker, runtime, packages, onProgress, force = false) {
62351
- const tag = `isol8:${runtime}-custom`;
62352
- const depsHash = computeDepsHash(runtime, packages);
62353
- logger.debug(`[ImageBuilder] ${runtime} custom deps hash: ${depsHash.slice(0, 16)}...`);
62354
- if (!force) {
62355
- const labels = await getImageLabels(docker, tag);
62356
- if (labels && labels[LABELS.depsHash] === depsHash) {
62357
- logger.debug(`[ImageBuilder] Custom image ${runtime} is up to date, skipping build`);
62358
- onProgress?.({ runtime, status: "done", message: "Up to date" });
62359
- return;
62360
- }
62361
- }
62362
- let oldImageId = null;
62363
- try {
62364
- const oldImage = await docker.getImage(tag).inspect();
62365
- oldImageId = oldImage.Id;
62366
- logger.debug(`[ImageBuilder] Existing custom image ${runtime} ID: ${oldImageId.slice(0, 12)}`);
62367
- } catch {
62368
- logger.debug(`[ImageBuilder] No existing custom image for ${runtime}`);
62369
- }
62370
- onProgress?.({ runtime, status: "building", message: `Custom: ${packages.join(", ")}` });
62371
- let installCmd;
62372
- switch (runtime) {
62373
- case "python":
62374
- installCmd = `RUN pip install --no-cache-dir ${packages.join(" ")}`;
62375
- break;
62376
- case "node":
62377
- installCmd = `RUN npm install -g ${packages.join(" ")}`;
62378
- break;
62379
- case "bun":
62380
- installCmd = `RUN bun install -g ${packages.join(" ")}`;
62381
- break;
62382
- case "deno":
62383
- installCmd = packages.map((p) => `RUN deno cache ${p}`).join(`
62384
- `);
62385
- break;
62386
- case "bash":
62387
- installCmd = `RUN apk add --no-cache ${packages.join(" ")}`;
62388
- break;
62389
- default:
62390
- throw new Error(`Unknown runtime: ${runtime}`);
62391
- }
62392
- const dockerfileContent = `FROM isol8:${runtime}
62393
- ${installCmd}
62394
- `;
62395
- const { createTarBuffer: createTarBuffer2, validatePackageName: validatePackageName2 } = await Promise.resolve().then(() => exports_utils);
62396
- const { Readable } = await import("node:stream");
62397
- packages.forEach(validatePackageName2);
62398
- const tarBuffer = createTarBuffer2("Dockerfile", dockerfileContent);
62399
- const stream = await docker.buildImage(Readable.from(tarBuffer), {
62400
- t: tag,
62401
- dockerfile: "Dockerfile",
62402
- labels: {
62403
- [LABELS.depsHash]: depsHash
62404
- }
62405
- });
62406
- await new Promise((resolve2, reject) => {
62407
- docker.modem.followProgress(stream, (err) => {
62408
- if (err) {
62409
- reject(err);
62410
- } else {
62411
- resolve2();
62412
- }
62413
- });
62414
- });
62415
- if (oldImageId) {
62416
- await removeImage(docker, oldImageId);
62417
- }
62418
- onProgress?.({ runtime, status: "done" });
62419
- }
62420
-
62421
- // src/cli.ts
62535
+ init_image_builder();
62422
62536
  init_runtime();
62423
62537
  init_logger();
62424
62538
  init_version();
@@ -62519,7 +62633,7 @@ program2.command("setup").description("Check Docker and build isol8 images").opt
62519
62633
  console.log(`
62520
62634
  [DONE] Setup complete!`);
62521
62635
  });
62522
- program2.command("run").description("Execute code in isol8").argument("[file]", "Script file to execute").option("-e, --eval <code>", "Execute inline code string").option("-r, --runtime <name>", "Force runtime (python, node, bun, deno, bash)").option("--net <mode>", "Network mode: none, host, filtered", "none").option("--allow <regex>", "Whitelist regex for filtered mode (repeatable)", collect, []).option("--deny <regex>", "Blacklist regex for filtered mode (repeatable)", collect, []).option("--out <file>", "Write output to file").option("--persistent", "Use persistent container").option("--timeout <ms>", "Execution timeout in milliseconds").option("--memory <limit>", "Memory limit (e.g. 512m, 1g)").option("--cpu <limit>", "CPU limit as fraction (e.g. 0.5, 2.0)").option("--image <name>", "Override Docker image").option("--pids-limit <n>", "Maximum number of processes").option("--writable", "Disable read-only root filesystem").option("--max-output <bytes>", "Maximum output size in bytes").option("--secret <KEY=VALUE>", "Secret env var (repeatable, values masked)", collect, []).option("--sandbox-size <size>", "Sandbox tmpfs size (e.g. 128m, 512m)").option("--tmp-size <size>", "Tmp tmpfs size (e.g. 256m, 512m)").option("--stdin <data>", "Data to pipe to stdin").option("--install <package>", "Install package for runtime (repeatable)", collect, []).option("--url <url>", "Fetch code from URL").option("--github <path>", "GitHub shorthand: owner/repo/ref/path/to/file").option("--gist <path>", "Gist shorthand: gistId/file.ext").option("--hash <sha256>", "Expected SHA-256 hash of fetched code").option("--allow-insecure-code-url", "Allow insecure HTTP code URLs").option("--host <url>", "Execute on remote server").option("--key <key>", "API key for remote server").option("--no-stream", "Disable real-time output streaming").option("--debug", "Enable debug logging").option("--persist", "Keep container running after execution for inspection").option("--log-network", "Log all network requests (requires --net filtered)").option("--pool-strategy <mode>", "Pool strategy: fast (default) or secure", "fast").option("--pool-size <size>", "Pool size (number or 'clean,dirty' for fast mode)", "1,1").action(async (file, opts) => {
62636
+ program2.command("run").description("Execute code in isol8").argument("[file]", "Script file to execute").option("-e, --eval <code>", "Execute inline code string").option("-r, --runtime <name>", "Force runtime (python, node, bun, deno, bash)").option("--net <mode>", "Network mode: none, host, filtered", "none").option("--allow <regex>", "Whitelist regex for filtered mode (repeatable)", collect, []).option("--deny <regex>", "Blacklist regex for filtered mode (repeatable)", collect, []).option("--out <file>", "Write output to file").option("--persistent", "Use persistent container").option("--timeout <ms>", "Execution timeout in milliseconds").option("--memory <limit>", "Memory limit (e.g. 512m, 1g)").option("--cpu <limit>", "CPU limit as fraction (e.g. 0.5, 2.0)").option("--image <name>", "Override Docker image").option("--pids-limit <n>", "Maximum number of processes").option("--writable", "Disable read-only root filesystem").option("--max-output <bytes>", "Maximum output size in bytes").option("--secret <KEY=VALUE>", "Secret env var (repeatable, values masked)", collect, []).option("--sandbox-size <size>", "Sandbox tmpfs size (e.g. 128m, 512m)").option("--tmp-size <size>", "Tmp tmpfs size (e.g. 256m, 512m)").option("--stdin <data>", "Data to pipe to stdin").option("--install <package>", "Install package for runtime (repeatable)", collect, []).option("--url <url>", "Fetch code from URL").option("--github <path>", "GitHub shorthand: owner/repo/ref/path/to/file").option("--gist <path>", "Gist shorthand: gistId/file.ext").option("--hash <sha256>", "Expected SHA-256 hash of fetched code").option("--allow-insecure-code-url", "Allow insecure HTTP code URLs").option("--host <url>", "Execute on remote server").option("--key <key>", "API key for remote server").option("--no-stream", "Disable real-time output streaming").option("--debug", "Enable debug logging").option("--persist", "Keep container running after execution for inspection").option("--log-network", "Log all network requests (requires --net filtered)").action(async (file, opts) => {
62523
62637
  const {
62524
62638
  code,
62525
62639
  codeUrl,
@@ -62633,14 +62747,16 @@ program2.command("run").description("Execute code in isol8").argument("[file]",
62633
62747
  process.exit(exitCode);
62634
62748
  }
62635
62749
  });
62636
- program2.command("serve").description("Start the isol8 remote server").option("-p, --port <port>", "Port to listen on", "3000").option("-k, --key <key>", "API key for authentication").option("--update", "Force re-download the server binary").option("--debug", "Enable debug logging").action(async (opts) => {
62750
+ program2.command("serve").description("Start the isol8 remote server").option("-p, --port <port>", "Port to listen on").option("-k, --key <key>", "API key for authentication").option("--update", "Force re-download the server binary").option("--debug", "Enable debug logging").action(async (opts) => {
62637
62751
  const apiKey = opts.key ?? process.env.ISOL8_API_KEY;
62638
62752
  if (!apiKey) {
62639
62753
  console.error("[ERR] API key required. Use --key or ISOL8_API_KEY env var.");
62640
62754
  process.exit(1);
62641
62755
  }
62642
- const port = Number.parseInt(opts.port, 10);
62643
- logger.debug(`[Serve] Port: ${port}`);
62756
+ const requestedPort = resolveServePort(opts.port);
62757
+ const port = await resolveAvailableServePort(requestedPort);
62758
+ logger.debug(`[Serve] Requested port: ${requestedPort}`);
62759
+ logger.debug(`[Serve] Using port: ${port}`);
62644
62760
  logger.debug(`[Serve] API key: ${"*".repeat(apiKey.length)}`);
62645
62761
  if (typeof globalThis.Bun !== "undefined") {
62646
62762
  logger.debug("[Serve] Running under Bun, starting server in-process");
@@ -62747,6 +62863,11 @@ async function downloadServerBinary(binaryPath) {
62747
62863
  }
62748
62864
  }
62749
62865
  async function promptYesNo(question) {
62866
+ const answer = await promptText(question);
62867
+ const normalized = answer.trim().toLowerCase();
62868
+ return normalized === "" || normalized === "y" || normalized === "yes";
62869
+ }
62870
+ async function promptText(question) {
62750
62871
  const readline = await import("node:readline");
62751
62872
  const rl = readline.createInterface({
62752
62873
  input: process.stdin,
@@ -62756,8 +62877,103 @@ async function promptYesNo(question) {
62756
62877
  rl.question(question, resolve3);
62757
62878
  });
62758
62879
  rl.close();
62759
- const normalized = answer.trim().toLowerCase();
62760
- return normalized === "" || normalized === "y" || normalized === "yes";
62880
+ return answer;
62881
+ }
62882
+ function parsePort(raw2, source) {
62883
+ const parsed = Number(raw2);
62884
+ if (!Number.isInteger(parsed) || parsed < 1 || parsed > 65535) {
62885
+ console.error(`[ERR] Invalid port from ${source}: ${raw2}. Expected 1-65535.`);
62886
+ process.exit(1);
62887
+ }
62888
+ return parsed;
62889
+ }
62890
+ function resolveServePort(portFlag) {
62891
+ if (typeof portFlag === "string") {
62892
+ return parsePort(portFlag, "--port");
62893
+ }
62894
+ if (process.env.ISOL8_PORT) {
62895
+ return parsePort(process.env.ISOL8_PORT, "ISOL8_PORT");
62896
+ }
62897
+ if (process.env.PORT) {
62898
+ return parsePort(process.env.PORT, "PORT");
62899
+ }
62900
+ return 3000;
62901
+ }
62902
+ async function isPortAvailable(port) {
62903
+ const { createServer: createServer2 } = await import("node:net");
62904
+ return await new Promise((resolve3) => {
62905
+ const server = createServer2();
62906
+ server.once("error", () => {
62907
+ resolve3(false);
62908
+ });
62909
+ server.once("listening", () => {
62910
+ server.close(() => resolve3(true));
62911
+ });
62912
+ server.listen(port);
62913
+ });
62914
+ }
62915
+ async function findAvailablePort() {
62916
+ const { createServer: createServer2 } = await import("node:net");
62917
+ return await new Promise((resolve3, reject) => {
62918
+ const server = createServer2();
62919
+ server.once("error", reject);
62920
+ server.once("listening", () => {
62921
+ const address = server.address();
62922
+ if (!address || typeof address === "string") {
62923
+ server.close(() => reject(new Error("Failed to determine available port")));
62924
+ return;
62925
+ }
62926
+ server.close((closeErr) => {
62927
+ if (closeErr) {
62928
+ reject(closeErr);
62929
+ return;
62930
+ }
62931
+ resolve3(address.port);
62932
+ });
62933
+ });
62934
+ server.listen(0);
62935
+ });
62936
+ }
62937
+ async function resolveAvailableServePort(port) {
62938
+ if (await isPortAvailable(port)) {
62939
+ return port;
62940
+ }
62941
+ if (!(process.stdin.isTTY && process.stdout.isTTY)) {
62942
+ const autoPort = await findAvailablePort();
62943
+ console.warn(`[WARN] Port ${port} is in use. Falling back to available port ${autoPort}.`);
62944
+ return autoPort;
62945
+ }
62946
+ let candidate = port;
62947
+ while (true) {
62948
+ console.warn(`[WARN] Port ${candidate} is already in use.`);
62949
+ const choice = (await promptText("Choose: [1] Enter another port [2] Find an available port [3] Exit (default: 2): ")).trim().toLowerCase();
62950
+ if (choice === "" || choice === "2") {
62951
+ const autoPort = await findAvailablePort();
62952
+ console.log(`[INFO] Using available port ${autoPort}`);
62953
+ return autoPort;
62954
+ }
62955
+ if (choice === "1") {
62956
+ const rawPort = (await promptText("Enter port (1-65535): ")).trim();
62957
+ if (!rawPort) {
62958
+ continue;
62959
+ }
62960
+ const parsed = Number(rawPort);
62961
+ if (!Number.isInteger(parsed) || parsed < 1 || parsed > 65535) {
62962
+ console.error(`[ERR] Invalid port: ${rawPort}. Expected 1-65535.`);
62963
+ continue;
62964
+ }
62965
+ candidate = parsed;
62966
+ if (await isPortAvailable(candidate)) {
62967
+ return candidate;
62968
+ }
62969
+ continue;
62970
+ }
62971
+ if (choice === "3") {
62972
+ console.error("[ERR] Server startup cancelled.");
62973
+ process.exit(1);
62974
+ }
62975
+ console.error("[ERR] Invalid selection. Enter 1, 2, or 3.");
62976
+ }
62761
62977
  }
62762
62978
  async function ensureServerBinary(forceUpdate) {
62763
62979
  const binDir = join4(homedir2(), ".isol8", "bin");
@@ -62843,6 +63059,11 @@ Isol8 Configuration
62843
63059
  console.log(" ── Cleanup ──");
62844
63060
  console.log(` Auto-prune: ${config.cleanup.autoPrune ? "yes" : "no"}`);
62845
63061
  console.log(` Max idle time: ${config.cleanup.maxContainerAgeMs}ms (${Math.round(config.cleanup.maxContainerAgeMs / 60000)}min)`);
63062
+ console.log("");
63063
+ console.log(" ── Pool Defaults (Serve) ──");
63064
+ console.log(` Pool strategy: ${config.poolStrategy}`);
63065
+ const poolSize = typeof config.poolSize === "number" ? String(config.poolSize) : `${config.poolSize.clean},${config.poolSize.dirty}`;
63066
+ console.log(` Pool size: ${poolSize}`);
62846
63067
  const deps = config.dependencies;
62847
63068
  const hasDeps = Object.values(deps).some((pkgs) => pkgs && pkgs.length > 0);
62848
63069
  console.log("");
@@ -62865,7 +63086,7 @@ Isol8 Configuration
62865
63086
  }
62866
63087
  console.log("");
62867
63088
  });
62868
- program2.command("cleanup").description("Remove orphaned isol8 containers").option("--force", "Skip confirmation prompt").action(async (opts) => {
63089
+ program2.command("cleanup").description("Remove orphaned isol8 containers (and optionally images)").option("--force", "Skip confirmation prompt").option("--images", "Also remove isol8 Docker images").action(async (opts) => {
62869
63090
  const docker = new import_dockerode2.default;
62870
63091
  logger.debug("[Cleanup] Connecting to Docker daemon");
62871
63092
  const spinner = ora("Checking Docker...").start();
@@ -62881,17 +63102,33 @@ program2.command("cleanup").description("Remove orphaned isol8 containers").opti
62881
63102
  const containers = await docker.listContainers({ all: true });
62882
63103
  const isol8Containers = containers.filter((c) => c.Image.startsWith("isol8:") || c.Image.startsWith("isol8-custom:"));
62883
63104
  logger.debug(`[Cleanup] Found ${containers.length} total containers, ${isol8Containers.length} isol8 containers`);
62884
- if (isol8Containers.length === 0) {
62885
- spinner.info("No isol8 containers found");
63105
+ let isol8Images = [];
63106
+ if (opts.images) {
63107
+ spinner.start("Finding isol8 images...");
63108
+ const images = await docker.listImages({ all: true });
63109
+ isol8Images = images.filter((img) => img.RepoTags?.some((tag) => tag.startsWith("isol8:"))).map((img) => ({ id: img.Id, tags: img.RepoTags ?? [] }));
63110
+ logger.debug(`[Cleanup] Found ${images.length} total images, ${isol8Images.length} isol8 images`);
63111
+ }
63112
+ if (isol8Containers.length === 0 && (!opts.images || isol8Images.length === 0)) {
63113
+ spinner.info(opts.images ? "No isol8 containers or images found" : "No isol8 containers found");
62886
63114
  return;
62887
63115
  }
62888
- spinner.succeed(`Found ${isol8Containers.length} isol8 container(s)`);
63116
+ spinner.succeed(`Found ${isol8Containers.length} isol8 container(s)` + (opts.images ? ` and ${isol8Images.length} image(s)` : ""));
62889
63117
  console.log("");
62890
63118
  for (const c of isol8Containers) {
62891
63119
  const status = c.State === "running" ? "\uD83D\uDFE2 running" : "⚪ stopped";
62892
63120
  const created = new Date(c.Created * 1000).toLocaleString();
62893
63121
  console.log(` ${status} ${c.Id.slice(0, 12)} | ${c.Image} | created ${created}`);
62894
63122
  }
63123
+ if (opts.images && isol8Images.length > 0) {
63124
+ if (isol8Containers.length > 0) {
63125
+ console.log("");
63126
+ }
63127
+ for (const image of isol8Images) {
63128
+ const tagText = image.tags.length > 0 ? image.tags.join(", ") : "<untagged>";
63129
+ console.log(` \uD83D\uDDBC️ image ${image.id.slice(0, 12)} | ${tagText}`);
63130
+ }
63131
+ }
62895
63132
  console.log("");
62896
63133
  if (!opts.force) {
62897
63134
  const readline = await import("node:readline");
@@ -62900,7 +63137,8 @@ program2.command("cleanup").description("Remove orphaned isol8 containers").opti
62900
63137
  output: process.stdout
62901
63138
  });
62902
63139
  const answer = await new Promise((resolve3) => {
62903
- rl.question("Remove all these containers? [y/N] ", resolve3);
63140
+ const targetLabel = opts.images ? "containers and images" : "containers";
63141
+ rl.question(`Remove all these ${targetLabel}? [y/N] `, resolve3);
62904
63142
  });
62905
63143
  rl.close();
62906
63144
  if (answer.toLowerCase() !== "y" && answer.toLowerCase() !== "yes") {
@@ -62908,20 +63146,40 @@ program2.command("cleanup").description("Remove orphaned isol8 containers").opti
62908
63146
  return;
62909
63147
  }
62910
63148
  }
62911
- spinner.start("Removing containers...");
62912
- logger.debug("[Cleanup] Removing containers");
62913
- const result = await DockerIsol8.cleanup(docker);
62914
- logger.debug(`[Cleanup] Removed: ${result.removed}, failed: ${result.failed}`);
62915
- if (result.errors.length > 0) {
63149
+ let containerResult = { removed: 0, failed: 0, errors: [] };
63150
+ if (isol8Containers.length > 0) {
63151
+ spinner.start("Removing containers...");
63152
+ logger.debug("[Cleanup] Removing containers");
63153
+ containerResult = await DockerIsol8.cleanup(docker);
63154
+ logger.debug(`[Cleanup] Containers removed: ${containerResult.removed}, failed: ${containerResult.failed}`);
63155
+ }
63156
+ if (containerResult.errors.length > 0) {
62916
63157
  console.log("");
62917
- for (const err of result.errors) {
63158
+ for (const err of containerResult.errors) {
62918
63159
  console.error(` Failed to remove ${err}`);
62919
63160
  }
62920
63161
  }
62921
- if (result.failed === 0) {
62922
- spinner.succeed(`Removed ${result.removed} container(s)`);
63162
+ if (containerResult.failed === 0) {
63163
+ spinner.succeed(`Removed ${containerResult.removed} container(s)`);
62923
63164
  } else {
62924
- spinner.warn(`Removed ${result.removed} container(s), ${result.failed} failed`);
63165
+ spinner.warn(`Removed ${containerResult.removed} container(s), ${containerResult.failed} failed`);
63166
+ }
63167
+ if (opts.images && isol8Images.length > 0) {
63168
+ spinner.start("Removing images...");
63169
+ logger.debug("[Cleanup] Removing images");
63170
+ const imageResult = await DockerIsol8.cleanupImages(docker);
63171
+ logger.debug(`[Cleanup] Images removed: ${imageResult.removed}, failed: ${imageResult.failed}`);
63172
+ if (imageResult.errors.length > 0) {
63173
+ console.log("");
63174
+ for (const err of imageResult.errors) {
63175
+ console.error(` Failed to remove image ${err}`);
63176
+ }
63177
+ }
63178
+ if (imageResult.failed === 0) {
63179
+ spinner.succeed(`Removed ${imageResult.removed} image(s)`);
63180
+ } else {
63181
+ spinner.warn(`Removed ${imageResult.removed} image(s), ${imageResult.failed} failed`);
63182
+ }
62925
63183
  }
62926
63184
  });
62927
63185
  async function resolveRunInput(file, opts) {
@@ -62997,12 +63255,8 @@ async function resolveRunInput(file, opts) {
62997
63255
  debug: opts.debug ?? config.debug,
62998
63256
  persist: opts.persist ?? false,
62999
63257
  ...opts.logNetwork ? { logNetwork: true } : {},
63000
- remoteCode: config.remoteCode,
63001
- poolStrategy: opts.poolStrategy === "secure" ? "secure" : "fast",
63002
- poolSize: opts.poolSize ? opts.poolSize.includes(",") ? {
63003
- clean: Number.parseInt(opts.poolSize.split(",")[0], 10),
63004
- dirty: Number.parseInt(opts.poolSize.split(",")[1], 10)
63005
- } : Number.parseInt(opts.poolSize, 10) : { clean: 1, dirty: 1 }
63258
+ dependencies: config.dependencies,
63259
+ remoteCode: config.remoteCode
63006
63260
  };
63007
63261
  logger.debug(`[Run] Engine options: mode=${engineOptions.mode}, network=${engineOptions.network}`);
63008
63262
  let fileExtension;
@@ -63088,4 +63342,4 @@ if (!process.argv.slice(2).length) {
63088
63342
  }
63089
63343
  program2.parse();
63090
63344
 
63091
- //# debugId=280ED4C71DBDC32964756E2164756E21
63345
+ //# debugId=33A00A6A263B687D64756E2164756E21