isol8 0.11.2 → 0.12.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.
package/dist/index.js CHANGED
@@ -1,4 +1,20 @@
1
+ import { createRequire } from "node:module";
2
+ var __create = Object.create;
3
+ var __getProtoOf = Object.getPrototypeOf;
1
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
+ };
2
18
  var __export = (target, all) => {
3
19
  for (var name in all)
4
20
  __defProp(target, name, {
@@ -9,6 +25,7 @@ var __export = (target, all) => {
9
25
  });
10
26
  };
11
27
  var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
28
+ var __require = /* @__PURE__ */ createRequire(import.meta.url);
12
29
 
13
30
  // src/runtime/adapter.ts
14
31
  var adapters, extensionMap, RuntimeRegistry;
@@ -546,9 +563,191 @@ class Semaphore {
546
563
  }
547
564
  }
548
565
 
566
+ // src/engine/default-seccomp-profile.ts
567
+ var EMBEDDED_DEFAULT_SECCOMP_PROFILE;
568
+ var init_default_seccomp_profile = __esm(() => {
569
+ EMBEDDED_DEFAULT_SECCOMP_PROFILE = JSON.stringify({
570
+ defaultAction: "SCMP_ACT_ALLOW",
571
+ architectures: ["SCMP_ARCH_X86_64", "SCMP_ARCH_X86", "SCMP_ARCH_X32", "SCMP_ARCH_AARCH64"],
572
+ syscalls: [
573
+ {
574
+ names: [
575
+ "acct",
576
+ "add_key",
577
+ "bpf",
578
+ "clock_adjtime",
579
+ "clock_settime",
580
+ "create_module",
581
+ "delete_module",
582
+ "finit_module",
583
+ "get_mempolicy",
584
+ "init_module",
585
+ "ioperm",
586
+ "iopl",
587
+ "kcmp",
588
+ "kexec_file_load",
589
+ "kexec_load",
590
+ "keyctl",
591
+ "lookup_dcookie",
592
+ "mbind",
593
+ "mount",
594
+ "move_pages",
595
+ "name_to_handle_at",
596
+ "open_by_handle_at",
597
+ "perf_event_open",
598
+ "pivot_root",
599
+ "process_vm_readv",
600
+ "process_vm_writev",
601
+ "ptrace",
602
+ "query_module",
603
+ "quotactl",
604
+ "reboot",
605
+ "request_key",
606
+ "set_mempolicy",
607
+ "setns",
608
+ "settimeofday",
609
+ "stime",
610
+ "swapon",
611
+ "swapoff",
612
+ "sysfs",
613
+ "syslog",
614
+ "umount",
615
+ "umount2",
616
+ "unshare",
617
+ "uselib",
618
+ "userfaultfd",
619
+ "ustat",
620
+ "vm86",
621
+ "vm86old"
622
+ ],
623
+ action: "SCMP_ACT_ERRNO",
624
+ args: [],
625
+ comment: "",
626
+ includes: {},
627
+ excludes: {}
628
+ }
629
+ ]
630
+ });
631
+ });
632
+
633
+ // src/engine/utils.ts
634
+ var exports_utils = {};
635
+ __export(exports_utils, {
636
+ validatePackageName: () => validatePackageName,
637
+ truncateOutput: () => truncateOutput,
638
+ parseMemoryLimit: () => parseMemoryLimit,
639
+ maskSecrets: () => maskSecrets,
640
+ extractFromTar: () => extractFromTar,
641
+ createTarBuffer: () => createTarBuffer
642
+ });
643
+ function parseMemoryLimit(limit) {
644
+ const match = limit.match(/^(\d+(?:\.\d+)?)\s*([kmgt]?)b?$/i);
645
+ if (!match) {
646
+ throw new Error(`Invalid memory limit format: "${limit}". Use e.g. "512m", "1g".`);
647
+ }
648
+ const value = Number.parseFloat(match[1]);
649
+ const unit = (match[2] || "b").toLowerCase();
650
+ const multipliers = {
651
+ b: 1,
652
+ k: 1024,
653
+ m: 1024 ** 2,
654
+ g: 1024 ** 3,
655
+ t: 1024 ** 4
656
+ };
657
+ return Math.floor(value * (multipliers[unit] ?? 1));
658
+ }
659
+ function truncateOutput(output, maxBytes) {
660
+ const encoder = new TextEncoder;
661
+ const bytes = encoder.encode(output);
662
+ if (bytes.length <= maxBytes) {
663
+ return { text: output, truncated: false };
664
+ }
665
+ const decoder = new TextDecoder("utf-8", { fatal: false });
666
+ const truncated = decoder.decode(bytes.slice(0, maxBytes));
667
+ return {
668
+ text: `${truncated}
669
+
670
+ --- OUTPUT TRUNCATED (${bytes.length} bytes, limit: ${maxBytes}) ---`,
671
+ truncated: true
672
+ };
673
+ }
674
+ function maskSecrets(text, secrets) {
675
+ let result = text;
676
+ for (const value of Object.values(secrets)) {
677
+ if (value.length > 0) {
678
+ result = result.replaceAll(value, "***");
679
+ }
680
+ }
681
+ return result;
682
+ }
683
+ function createTarBuffer(filePath, content) {
684
+ const data = typeof content === "string" ? Buffer.from(content, "utf-8") : content;
685
+ const headerSize = 512;
686
+ const dataBlocks = Math.ceil(data.length / 512);
687
+ const totalSize = headerSize + dataBlocks * 512 + 1024;
688
+ const buf = Buffer.alloc(totalSize);
689
+ buf.write(filePath.replace(/^\//, ""), 0, 100, "utf-8");
690
+ buf.write("0000644\x00", 100, 8, "utf-8");
691
+ buf.write("0000000\x00", 108, 8, "utf-8");
692
+ buf.write("0000000\x00", 116, 8, "utf-8");
693
+ buf.write(`${data.length.toString(8).padStart(11, "0")}\x00`, 124, 12, "utf-8");
694
+ buf.write(`${Math.floor(Date.now() / 1000).toString(8).padStart(11, "0")}\x00`, 136, 12, "utf-8");
695
+ buf.write("0", 156, 1, "utf-8");
696
+ buf.write("ustar\x00", 257, 6, "utf-8");
697
+ buf.write("00", 263, 2, "utf-8");
698
+ buf.write(" ", 148, 8, "utf-8");
699
+ let checksum = 0;
700
+ for (let i = 0;i < headerSize; i++) {
701
+ checksum += buf[i];
702
+ }
703
+ buf.write(`${checksum.toString(8).padStart(6, "0")}\x00 `, 148, 8, "utf-8");
704
+ data.copy(buf, headerSize);
705
+ return buf;
706
+ }
707
+ function extractFromTar(tarBuffer, targetPath) {
708
+ const normalizedTarget = targetPath.replace(/^\//, "");
709
+ const basename = targetPath.split("/").pop() ?? targetPath;
710
+ let offset = 0;
711
+ while (offset < tarBuffer.length - 512) {
712
+ const nameEnd = tarBuffer.indexOf(0, offset);
713
+ const name = tarBuffer.subarray(offset, Math.min(nameEnd, offset + 100)).toString("utf-8");
714
+ if (name.length === 0) {
715
+ break;
716
+ }
717
+ const sizeStr = tarBuffer.subarray(offset + 124, offset + 136).toString("utf-8").trim();
718
+ const size = Number.parseInt(sizeStr, 8);
719
+ if (Number.isNaN(size)) {
720
+ break;
721
+ }
722
+ const dataStart = offset + 512;
723
+ const dataBlocks = Math.ceil(size / 512);
724
+ if (name === normalizedTarget || name.endsWith(`/${normalizedTarget}`) || name === basename) {
725
+ return Buffer.from(tarBuffer.subarray(dataStart, dataStart + size));
726
+ }
727
+ offset = dataStart + dataBlocks * 512;
728
+ }
729
+ throw new Error(`File "${targetPath}" not found in tar archive`);
730
+ }
731
+ function validatePackageName(name) {
732
+ if (!/^[@a-zA-Z0-9_./\-=]+$/.test(name)) {
733
+ throw new Error(`Invalid package name: "${name}". Only alphanumeric, -, _, ., /, @, and = are allowed.`);
734
+ }
735
+ return name;
736
+ }
737
+
549
738
  // src/engine/image-builder.ts
739
+ var exports_image_builder = {};
740
+ __export(exports_image_builder, {
741
+ normalizePackages: () => normalizePackages,
742
+ imageExists: () => imageExists,
743
+ getCustomImageTag: () => getCustomImageTag,
744
+ ensureImages: () => ensureImages,
745
+ buildCustomImages: () => buildCustomImages,
746
+ buildBaseImages: () => buildBaseImages
747
+ });
550
748
  import { createHash as createHash2 } from "node:crypto";
551
749
  import { existsSync as existsSync3, readFileSync as readFileSync2 } from "node:fs";
750
+ import { join as join3 } from "node:path";
552
751
  function resolveDockerDir() {
553
752
  const fromBundled = new URL("./docker", import.meta.url).pathname;
554
753
  if (existsSync3(fromBundled)) {
@@ -556,6 +755,19 @@ function resolveDockerDir() {
556
755
  }
557
756
  return new URL("../../docker", import.meta.url).pathname;
558
757
  }
758
+ function computeDockerDirHash() {
759
+ const hash = createHash2("sha256");
760
+ const files = [...DOCKER_BUILD_FILES].sort();
761
+ for (const file of files) {
762
+ const filePath = join3(DOCKERFILE_DIR, file);
763
+ if (existsSync3(filePath)) {
764
+ const content = readFileSync2(filePath);
765
+ hash.update(file);
766
+ hash.update(content);
767
+ }
768
+ }
769
+ return hash.digest("hex");
770
+ }
559
771
  function computeDepsHash(runtime, packages) {
560
772
  const hash = createHash2("sha256");
561
773
  hash.update(runtime);
@@ -573,11 +785,206 @@ function getCustomImageTag(runtime, packages) {
573
785
  const shortHash = depsHash.slice(0, 12);
574
786
  return `isol8:${runtime}-custom-${shortHash}`;
575
787
  }
576
- var DOCKERFILE_DIR;
788
+ async function getImageLabels(docker, imageName) {
789
+ try {
790
+ const image = docker.getImage(imageName);
791
+ const inspect = await image.inspect();
792
+ return inspect.Config?.Labels ?? {};
793
+ } catch {
794
+ return null;
795
+ }
796
+ }
797
+ async function removeImage(docker, imageId) {
798
+ try {
799
+ const image = docker.getImage(imageId);
800
+ await image.remove();
801
+ logger.debug(`[ImageBuilder] Removed old image: ${imageId.slice(0, 12)}`);
802
+ } catch (err) {
803
+ logger.debug(`[ImageBuilder] Could not remove image ${imageId.slice(0, 12)}: ${err}`);
804
+ }
805
+ }
806
+ async function buildBaseImages(docker, onProgress, force = false, onlyRuntimes) {
807
+ const allRuntimes = RuntimeRegistry.list();
808
+ const runtimes = onlyRuntimes ? allRuntimes.filter((r) => onlyRuntimes.includes(r.name)) : allRuntimes;
809
+ const dockerHash = computeDockerDirHash();
810
+ logger.debug(`[ImageBuilder] Docker directory hash: ${dockerHash.slice(0, 16)}...`);
811
+ for (const adapter of runtimes) {
812
+ const target = adapter.name;
813
+ const imageName = adapter.image;
814
+ if (!force) {
815
+ const labels = await getImageLabels(docker, imageName);
816
+ if (labels && labels[LABELS.dockerHash] === dockerHash) {
817
+ logger.debug(`[ImageBuilder] Base image ${target} is up to date, skipping build`);
818
+ onProgress?.({ runtime: target, status: "done", message: "Up to date" });
819
+ continue;
820
+ }
821
+ }
822
+ let oldImageId = null;
823
+ try {
824
+ const oldImage = await docker.getImage(imageName).inspect();
825
+ oldImageId = oldImage.Id;
826
+ logger.debug(`[ImageBuilder] Existing image ${target} ID: ${oldImageId.slice(0, 12)}`);
827
+ } catch {
828
+ logger.debug(`[ImageBuilder] No existing image for ${target}`);
829
+ }
830
+ onProgress?.({ runtime: target, status: "building" });
831
+ try {
832
+ const stream = await docker.buildImage({ context: DOCKERFILE_DIR, src: DOCKER_BUILD_FILES }, {
833
+ t: imageName,
834
+ target,
835
+ dockerfile: "Dockerfile",
836
+ labels: {
837
+ [LABELS.dockerHash]: dockerHash
838
+ }
839
+ });
840
+ await new Promise((resolve2, reject) => {
841
+ docker.modem.followProgress(stream, (err) => {
842
+ if (err) {
843
+ reject(err);
844
+ } else {
845
+ resolve2();
846
+ }
847
+ });
848
+ });
849
+ if (oldImageId) {
850
+ await removeImage(docker, oldImageId);
851
+ }
852
+ onProgress?.({ runtime: target, status: "done" });
853
+ } catch (err) {
854
+ const message = err instanceof Error ? err.message : String(err);
855
+ onProgress?.({ runtime: target, status: "error", message });
856
+ throw new Error(`Failed to build image for ${target}: ${message}`);
857
+ }
858
+ }
859
+ }
860
+ async function buildCustomImages(docker, config, onProgress, force = false) {
861
+ const deps = config.dependencies;
862
+ const python = deps.python ? normalizePackages(deps.python) : [];
863
+ const node = deps.node ? normalizePackages(deps.node) : [];
864
+ const bun = deps.bun ? normalizePackages(deps.bun) : [];
865
+ const deno = deps.deno ? normalizePackages(deps.deno) : [];
866
+ const bash = deps.bash ? normalizePackages(deps.bash) : [];
867
+ if (python.length) {
868
+ await buildCustomImage(docker, "python", python, onProgress, force);
869
+ }
870
+ if (node.length) {
871
+ await buildCustomImage(docker, "node", node, onProgress, force);
872
+ }
873
+ if (bun.length) {
874
+ await buildCustomImage(docker, "bun", bun, onProgress, force);
875
+ }
876
+ if (deno.length) {
877
+ await buildCustomImage(docker, "deno", deno, onProgress, force);
878
+ }
879
+ if (bash.length) {
880
+ await buildCustomImage(docker, "bash", bash, onProgress, force);
881
+ }
882
+ }
883
+ async function buildCustomImage(docker, runtime, packages, onProgress, force = false) {
884
+ const normalizedPackages = normalizePackages(packages);
885
+ const tag = getCustomImageTag(runtime, normalizedPackages);
886
+ const depsHash = computeDepsHash(runtime, normalizedPackages);
887
+ logger.debug(`[ImageBuilder] ${runtime} custom deps hash: ${depsHash.slice(0, 16)}...`);
888
+ if (!force) {
889
+ const labels = await getImageLabels(docker, tag);
890
+ if (labels && labels[LABELS.depsHash] === depsHash) {
891
+ logger.debug(`[ImageBuilder] Custom image ${runtime} is up to date, skipping build`);
892
+ onProgress?.({ runtime, status: "done", message: "Up to date" });
893
+ return;
894
+ }
895
+ }
896
+ let oldImageId = null;
897
+ try {
898
+ const oldImage = await docker.getImage(tag).inspect();
899
+ oldImageId = oldImage.Id;
900
+ logger.debug(`[ImageBuilder] Existing custom image ${runtime} ID: ${oldImageId.slice(0, 12)}`);
901
+ } catch {
902
+ logger.debug(`[ImageBuilder] No existing custom image for ${runtime}`);
903
+ }
904
+ onProgress?.({
905
+ runtime,
906
+ status: "building",
907
+ message: `Custom: ${normalizedPackages.join(", ")}`
908
+ });
909
+ let installCmd;
910
+ switch (runtime) {
911
+ case "python":
912
+ installCmd = `RUN pip install --no-cache-dir ${normalizedPackages.join(" ")}`;
913
+ break;
914
+ case "node":
915
+ installCmd = `RUN npm install -g ${normalizedPackages.join(" ")}`;
916
+ break;
917
+ case "bun":
918
+ installCmd = `RUN bun install -g ${normalizedPackages.join(" ")}`;
919
+ break;
920
+ case "deno":
921
+ installCmd = normalizedPackages.map((p) => `RUN deno cache ${p}`).join(`
922
+ `);
923
+ break;
924
+ case "bash":
925
+ installCmd = `RUN apk add --no-cache ${normalizedPackages.join(" ")}`;
926
+ break;
927
+ default:
928
+ throw new Error(`Unknown runtime: ${runtime}`);
929
+ }
930
+ const dockerfileContent = `FROM isol8:${runtime}
931
+ ${installCmd}
932
+ `;
933
+ const { createTarBuffer: createTarBuffer2, validatePackageName: validatePackageName2 } = await Promise.resolve().then(() => exports_utils);
934
+ const { Readable } = await import("node:stream");
935
+ normalizedPackages.forEach(validatePackageName2);
936
+ const tarBuffer = createTarBuffer2("Dockerfile", dockerfileContent);
937
+ const stream = await docker.buildImage(Readable.from(tarBuffer), {
938
+ t: tag,
939
+ dockerfile: "Dockerfile",
940
+ labels: {
941
+ [LABELS.depsHash]: depsHash
942
+ }
943
+ });
944
+ await new Promise((resolve2, reject) => {
945
+ docker.modem.followProgress(stream, (err) => {
946
+ if (err) {
947
+ reject(err);
948
+ } else {
949
+ resolve2();
950
+ }
951
+ });
952
+ });
953
+ if (oldImageId) {
954
+ await removeImage(docker, oldImageId);
955
+ }
956
+ onProgress?.({ runtime, status: "done" });
957
+ }
958
+ async function imageExists(docker, imageName) {
959
+ try {
960
+ await docker.getImage(imageName).inspect();
961
+ return true;
962
+ } catch {
963
+ return false;
964
+ }
965
+ }
966
+ async function ensureImages(docker, onProgress) {
967
+ const runtimes = RuntimeRegistry.list();
968
+ const missing = [];
969
+ for (const adapter of runtimes) {
970
+ if (!await imageExists(docker, adapter.image)) {
971
+ missing.push(adapter.name);
972
+ }
973
+ }
974
+ if (missing.length > 0) {
975
+ await buildBaseImages(docker, onProgress, false, missing);
976
+ }
977
+ }
978
+ var DOCKERFILE_DIR, LABELS, DOCKER_BUILD_FILES;
577
979
  var init_image_builder = __esm(() => {
578
980
  init_runtime();
579
981
  init_logger();
580
982
  DOCKERFILE_DIR = resolveDockerDir();
983
+ LABELS = {
984
+ dockerHash: "org.isol8.build.hash",
985
+ depsHash: "org.isol8.deps.hash"
986
+ };
987
+ DOCKER_BUILD_FILES = ["Dockerfile", "proxy.sh", "proxy-handler.sh"];
581
988
  });
582
989
 
583
990
  // src/engine/pool.ts
@@ -859,96 +1266,6 @@ function calculateResourceDelta(before, after) {
859
1266
  };
860
1267
  }
861
1268
 
862
- // src/engine/utils.ts
863
- function parseMemoryLimit(limit) {
864
- const match = limit.match(/^(\d+(?:\.\d+)?)\s*([kmgt]?)b?$/i);
865
- if (!match) {
866
- throw new Error(`Invalid memory limit format: "${limit}". Use e.g. "512m", "1g".`);
867
- }
868
- const value = Number.parseFloat(match[1]);
869
- const unit = (match[2] || "b").toLowerCase();
870
- const multipliers = {
871
- b: 1,
872
- k: 1024,
873
- m: 1024 ** 2,
874
- g: 1024 ** 3,
875
- t: 1024 ** 4
876
- };
877
- return Math.floor(value * (multipliers[unit] ?? 1));
878
- }
879
- function truncateOutput(output, maxBytes) {
880
- const encoder = new TextEncoder;
881
- const bytes = encoder.encode(output);
882
- if (bytes.length <= maxBytes) {
883
- return { text: output, truncated: false };
884
- }
885
- const decoder = new TextDecoder("utf-8", { fatal: false });
886
- const truncated = decoder.decode(bytes.slice(0, maxBytes));
887
- return {
888
- text: `${truncated}
889
-
890
- --- OUTPUT TRUNCATED (${bytes.length} bytes, limit: ${maxBytes}) ---`,
891
- truncated: true
892
- };
893
- }
894
- function maskSecrets(text, secrets) {
895
- let result = text;
896
- for (const value of Object.values(secrets)) {
897
- if (value.length > 0) {
898
- result = result.replaceAll(value, "***");
899
- }
900
- }
901
- return result;
902
- }
903
- function createTarBuffer(filePath, content) {
904
- const data = typeof content === "string" ? Buffer.from(content, "utf-8") : content;
905
- const headerSize = 512;
906
- const dataBlocks = Math.ceil(data.length / 512);
907
- const totalSize = headerSize + dataBlocks * 512 + 1024;
908
- const buf = Buffer.alloc(totalSize);
909
- buf.write(filePath.replace(/^\//, ""), 0, 100, "utf-8");
910
- buf.write("0000644\x00", 100, 8, "utf-8");
911
- buf.write("0000000\x00", 108, 8, "utf-8");
912
- buf.write("0000000\x00", 116, 8, "utf-8");
913
- buf.write(`${data.length.toString(8).padStart(11, "0")}\x00`, 124, 12, "utf-8");
914
- buf.write(`${Math.floor(Date.now() / 1000).toString(8).padStart(11, "0")}\x00`, 136, 12, "utf-8");
915
- buf.write("0", 156, 1, "utf-8");
916
- buf.write("ustar\x00", 257, 6, "utf-8");
917
- buf.write("00", 263, 2, "utf-8");
918
- buf.write(" ", 148, 8, "utf-8");
919
- let checksum = 0;
920
- for (let i = 0;i < headerSize; i++) {
921
- checksum += buf[i];
922
- }
923
- buf.write(`${checksum.toString(8).padStart(6, "0")}\x00 `, 148, 8, "utf-8");
924
- data.copy(buf, headerSize);
925
- return buf;
926
- }
927
- function extractFromTar(tarBuffer, targetPath) {
928
- const normalizedTarget = targetPath.replace(/^\//, "");
929
- const basename = targetPath.split("/").pop() ?? targetPath;
930
- let offset = 0;
931
- while (offset < tarBuffer.length - 512) {
932
- const nameEnd = tarBuffer.indexOf(0, offset);
933
- const name = tarBuffer.subarray(offset, Math.min(nameEnd, offset + 100)).toString("utf-8");
934
- if (name.length === 0) {
935
- break;
936
- }
937
- const sizeStr = tarBuffer.subarray(offset + 124, offset + 136).toString("utf-8").trim();
938
- const size = Number.parseInt(sizeStr, 8);
939
- if (Number.isNaN(size)) {
940
- break;
941
- }
942
- const dataStart = offset + 512;
943
- const dataBlocks = Math.ceil(size / 512);
944
- if (name === normalizedTarget || name.endsWith(`/${normalizedTarget}`) || name === basename) {
945
- return Buffer.from(tarBuffer.subarray(dataStart, dataStart + size));
946
- }
947
- offset = dataStart + dataBlocks * 512;
948
- }
949
- throw new Error(`File "${targetPath}" not found in tar archive`);
950
- }
951
-
952
1269
  // src/engine/docker.ts
953
1270
  var exports_docker = {};
954
1271
  __export(exports_docker, {
@@ -1087,7 +1404,19 @@ function wrapWithTimeout(cmd, timeoutSec) {
1087
1404
  function getInstallCommand(runtime, packages) {
1088
1405
  switch (runtime) {
1089
1406
  case "python":
1090
- return ["pip", "install", "--user", "--no-cache-dir", "--break-system-packages", ...packages];
1407
+ return [
1408
+ "pip",
1409
+ "install",
1410
+ "--user",
1411
+ "--no-cache-dir",
1412
+ "--break-system-packages",
1413
+ "--disable-pip-version-check",
1414
+ "--retries",
1415
+ "0",
1416
+ "--timeout",
1417
+ "15",
1418
+ ...packages
1419
+ ];
1091
1420
  case "node":
1092
1421
  return ["npm", "install", "--prefix", "/sandbox", ...packages];
1093
1422
  case "bun":
@@ -1100,8 +1429,9 @@ function getInstallCommand(runtime, packages) {
1100
1429
  throw new Error(`Unknown runtime for package install: ${runtime}`);
1101
1430
  }
1102
1431
  }
1103
- async function installPackages(container, runtime, packages) {
1104
- const cmd = getInstallCommand(runtime, packages);
1432
+ async function installPackages(container, runtime, packages, timeoutMs) {
1433
+ const timeoutSec = Math.max(1, Math.ceil(timeoutMs / 1000));
1434
+ const cmd = wrapWithTimeout(getInstallCommand(runtime, packages), timeoutSec);
1105
1435
  logger.debug(`Installing packages: ${JSON.stringify(cmd)}`);
1106
1436
  const env = [
1107
1437
  "PATH=/sandbox/.local/bin:/sandbox/.npm-global/bin:/sandbox/.bun-global/bin:/usr/local/bin:/usr/bin:/bin:/usr/local/sbin:/usr/sbin:/sbin"
@@ -1112,6 +1442,12 @@ async function installPackages(container, runtime, packages) {
1112
1442
  env.push("NPM_CONFIG_PREFIX=/sandbox/.npm-global");
1113
1443
  env.push("NPM_CONFIG_CACHE=/sandbox/.npm-cache");
1114
1444
  env.push("npm_config_cache=/sandbox/.npm-cache");
1445
+ env.push("NPM_CONFIG_FETCH_RETRIES=0");
1446
+ env.push("npm_config_fetch_retries=0");
1447
+ env.push("NPM_CONFIG_FETCH_RETRY_MINTIMEOUT=1000");
1448
+ env.push("npm_config_fetch_retry_mintimeout=1000");
1449
+ env.push("NPM_CONFIG_FETCH_RETRY_MAXTIMEOUT=2000");
1450
+ env.push("npm_config_fetch_retry_maxtimeout=2000");
1115
1451
  } else if (runtime === "bun") {
1116
1452
  env.push("BUN_INSTALL_GLOBAL_DIR=/sandbox/.bun-global");
1117
1453
  env.push("BUN_INSTALL_CACHE_DIR=/sandbox/.bun-cache");
@@ -1133,7 +1469,13 @@ async function installPackages(container, runtime, packages) {
1133
1469
  const stderrStream = new PassThrough;
1134
1470
  container.modem.demuxStream(stream, stdoutStream, stderrStream);
1135
1471
  stderrStream.on("data", (chunk) => {
1136
- stderr += chunk.toString();
1472
+ const text = chunk.toString();
1473
+ stderr += text;
1474
+ logger.debug(`[install:${runtime}:stderr] ${text.trimEnd()}`);
1475
+ });
1476
+ stdoutStream.on("data", (chunk) => {
1477
+ const text = chunk.toString();
1478
+ logger.debug(`[install:${runtime}:stdout] ${text.trimEnd()}`);
1137
1479
  });
1138
1480
  stream.on("end", async () => {
1139
1481
  try {
@@ -1463,7 +1805,7 @@ class DockerIsol8 {
1463
1805
  const filePath = `${SANDBOX_WORKDIR}/main${ext}`;
1464
1806
  await writeFileViaExec(container, filePath, request.code);
1465
1807
  if (request.installPackages?.length) {
1466
- await installPackages(container, request.runtime, request.installPackages);
1808
+ await installPackages(container, request.runtime, request.installPackages, timeoutMs);
1467
1809
  }
1468
1810
  if (request.files) {
1469
1811
  for (const [fPath, fContent] of Object.entries(request.files)) {
@@ -1532,6 +1874,26 @@ class DockerIsol8 {
1532
1874
  resolvedImage = legacyCustomTag;
1533
1875
  } catch {}
1534
1876
  }
1877
+ try {
1878
+ await this.docker.getImage(resolvedImage).inspect();
1879
+ } catch {
1880
+ logger.debug(`[ImageBuilder] Image ${resolvedImage} not found. Building...`);
1881
+ const { buildBaseImages: buildBaseImages2, buildCustomImages: buildCustomImages2 } = await Promise.resolve().then(() => (init_image_builder(), exports_image_builder));
1882
+ if (resolvedImage !== adapter.image && normalizedDeps.length > 0) {
1883
+ try {
1884
+ await this.docker.getImage(adapter.image).inspect();
1885
+ } catch {
1886
+ logger.debug(`[ImageBuilder] Base image ${adapter.image} missing. Building...`);
1887
+ await buildBaseImages2(this.docker, undefined, false, [adapter.name]);
1888
+ }
1889
+ logger.debug(`[ImageBuilder] Building custom image for ${adapter.name}...`);
1890
+ const dummyConfig = { dependencies: { [adapter.name]: normalizedDeps } };
1891
+ await buildCustomImages2(this.docker, dummyConfig, undefined, false);
1892
+ } else {
1893
+ logger.debug(`[ImageBuilder] Building base image for ${adapter.name}...`);
1894
+ await buildBaseImages2(this.docker, undefined, false, [adapter.name]);
1895
+ }
1896
+ }
1535
1897
  this.imageCache.set(cacheKey, resolvedImage);
1536
1898
  return resolvedImage;
1537
1899
  }
@@ -1592,7 +1954,7 @@ class DockerIsol8 {
1592
1954
  rawCmd = adapter.getCommand(req.code, filePath);
1593
1955
  }
1594
1956
  if (req.installPackages?.length) {
1595
- await installPackages(container, req.runtime, req.installPackages);
1957
+ await installPackages(container, req.runtime, req.installPackages, timeoutMs);
1596
1958
  }
1597
1959
  const timeoutSec = Math.ceil(timeoutMs / 1000);
1598
1960
  let cmd;
@@ -1700,7 +2062,7 @@ class DockerIsol8 {
1700
2062
  const rawCmd = adapter.getCommand(req.code, filePath);
1701
2063
  const timeoutSec = Math.ceil(timeoutMs / 1000);
1702
2064
  if (req.installPackages?.length) {
1703
- await installPackages(this.container, req.runtime, req.installPackages);
2065
+ await installPackages(this.container, req.runtime, req.installPackages, timeoutMs);
1704
2066
  }
1705
2067
  let cmd;
1706
2068
  if (req.stdin) {
@@ -1843,17 +2205,15 @@ class DockerIsol8 {
1843
2205
  const profile = readFileSync3(this.security.customProfilePath, "utf-8");
1844
2206
  opts.push(`seccomp=${profile}`);
1845
2207
  } catch (e) {
1846
- logger.error(`Failed to load custom seccomp profile: ${e}`);
2208
+ throw new Error(`Failed to load custom seccomp profile at ${this.security.customProfilePath}: ${e}`);
1847
2209
  }
1848
2210
  return opts;
1849
2211
  }
1850
2212
  try {
1851
2213
  const profile = this.loadDefaultSeccompProfile();
1852
- if (profile) {
1853
- opts.push(`seccomp=${profile}`);
1854
- }
2214
+ opts.push(`seccomp=${profile}`);
1855
2215
  } catch (e) {
1856
- logger.error(`Failed to load default seccomp profile: ${e}`);
2216
+ throw new Error(`Failed to load default seccomp profile: ${e}`);
1857
2217
  }
1858
2218
  return opts;
1859
2219
  }
@@ -1866,8 +2226,11 @@ class DockerIsol8 {
1866
2226
  if (existsSync4(prodPath)) {
1867
2227
  return readFileSync3(prodPath, "utf-8");
1868
2228
  }
1869
- logger.warn("Could not locate default seccomp profile. Running without seccomp filter.");
1870
- return null;
2229
+ if (EMBEDDED_DEFAULT_SECCOMP_PROFILE.length > 0) {
2230
+ logger.debug(`Default seccomp profile file not found. Using embedded profile. Tried: ${devPath.pathname}, ${prodPath.pathname}`);
2231
+ return EMBEDDED_DEFAULT_SECCOMP_PROFILE;
2232
+ }
2233
+ throw new Error("Embedded default seccomp profile is unavailable");
1871
2234
  }
1872
2235
  buildEnv(extra) {
1873
2236
  const env = [
@@ -2092,6 +2455,7 @@ var init_docker = __esm(() => {
2092
2455
  init_logger();
2093
2456
  init_audit();
2094
2457
  init_code_fetcher();
2458
+ init_default_seccomp_profile();
2095
2459
  init_image_builder();
2096
2460
  init_pool();
2097
2461
  MAX_OUTPUT_BYTES = 1024 * 1024;
@@ -2235,7 +2599,8 @@ var DEFAULT_CONFIG = {
2235
2599
  cpuLimit: 1,
2236
2600
  network: "none",
2237
2601
  sandboxSize: "512m",
2238
- tmpSize: "256m"
2602
+ tmpSize: "256m",
2603
+ readonlyRootFs: true
2239
2604
  },
2240
2605
  network: {
2241
2606
  whitelist: [],
@@ -2304,7 +2669,8 @@ function mergeConfig(defaults, overrides) {
2304
2669
  maxConcurrent: overrides.maxConcurrent ?? defaults.maxConcurrent,
2305
2670
  defaults: {
2306
2671
  ...defaults.defaults,
2307
- ...overrides.defaults
2672
+ ...overrides.defaults,
2673
+ readonlyRootFs: overrides.defaults?.readonlyRootFs ?? defaults.defaults.readonlyRootFs
2308
2674
  },
2309
2675
  network: {
2310
2676
  whitelist: overrides.network?.whitelist ?? defaults.network.whitelist,
@@ -2349,7 +2715,7 @@ init_logger();
2349
2715
  // package.json
2350
2716
  var package_default = {
2351
2717
  name: "isol8",
2352
- version: "0.11.1",
2718
+ version: "0.12.0-alpha.0",
2353
2719
  description: "Secure code execution engine for AI agents",
2354
2720
  author: "Illusion47586",
2355
2721
  license: "MIT",
@@ -2501,6 +2867,50 @@ async function createServer(options) {
2501
2867
  logger.debug(`[Server] Auto-prune: ${config.cleanup.autoPrune}`);
2502
2868
  const app = new Hono;
2503
2869
  const globalSemaphore = new Semaphore(config.maxConcurrent);
2870
+ let pruneInterval;
2871
+ let cleanupInFlight = null;
2872
+ const cleanupSessions = async () => {
2873
+ let removed = 0;
2874
+ let failed = 0;
2875
+ const errors = [];
2876
+ for (const [id, session] of sessions) {
2877
+ try {
2878
+ await session.engine.stop();
2879
+ removed++;
2880
+ } catch (err) {
2881
+ failed++;
2882
+ const errorMsg = err instanceof Error ? err.message : String(err);
2883
+ errors.push(`${id}: ${errorMsg}`);
2884
+ } finally {
2885
+ sessions.delete(id);
2886
+ }
2887
+ }
2888
+ return { removed, failed, errors };
2889
+ };
2890
+ const runCleanup = async (includeImages) => {
2891
+ if (cleanupInFlight) {
2892
+ return cleanupInFlight;
2893
+ }
2894
+ cleanupInFlight = (async () => {
2895
+ logger.info(`[Server] Starting cleanup (sessions=true containers=true images=${includeImages})`);
2896
+ const sessionsResult = await cleanupSessions();
2897
+ const containersResult = await DockerIsol82.cleanup();
2898
+ const result = {
2899
+ sessions: sessionsResult,
2900
+ containers: containersResult
2901
+ };
2902
+ if (includeImages) {
2903
+ result.images = await DockerIsol82.cleanupImages();
2904
+ }
2905
+ logger.info(`[Server] Cleanup complete: sessions=${result.sessions.removed}/${result.sessions.failed} containers=${result.containers.removed}/${result.containers.failed}${result.images ? ` images=${result.images.removed}/${result.images.failed}` : ""}`);
2906
+ return result;
2907
+ })();
2908
+ try {
2909
+ return await cleanupInFlight;
2910
+ } finally {
2911
+ cleanupInFlight = null;
2912
+ }
2913
+ };
2504
2914
  app.use("*", authMiddleware(options.apiKey));
2505
2915
  app.get("/health", (c) => c.json({ status: "ok", version: VERSION }));
2506
2916
  app.post("/execute", async (c) => {
@@ -2681,8 +3091,21 @@ async function createServer(options) {
2681
3091
  }
2682
3092
  return c.json({ ok: true });
2683
3093
  });
3094
+ app.post("/cleanup", async (c) => {
3095
+ const body = await c.req.json().catch(() => ({}));
3096
+ const includeImages = body.images ?? true;
3097
+ logger.debug(`[Server] POST /cleanup images=${includeImages}`);
3098
+ try {
3099
+ const result = await runCleanup(includeImages);
3100
+ return c.json({ ok: true, ...result });
3101
+ } catch (err) {
3102
+ const message = err instanceof Error ? err.message : String(err);
3103
+ logger.error(`[Server] Cleanup failed: ${message}`);
3104
+ return c.json({ error: message }, 500);
3105
+ }
3106
+ });
2684
3107
  if (config.cleanup.autoPrune) {
2685
- setInterval(async () => {
3108
+ pruneInterval = setInterval(async () => {
2686
3109
  const maxAge = config.cleanup.maxContainerAgeMs;
2687
3110
  const now = Date.now();
2688
3111
  for (const [id, session] of sessions) {
@@ -2700,7 +3123,15 @@ async function createServer(options) {
2700
3123
  return {
2701
3124
  app,
2702
3125
  fetch: app.fetch,
2703
- port: options.port
3126
+ port: options.port,
3127
+ cleanup: async (includeImages = true) => runCleanup(includeImages),
3128
+ shutdown: async (includeImages = true) => {
3129
+ if (pruneInterval) {
3130
+ clearInterval(pruneInterval);
3131
+ pruneInterval = undefined;
3132
+ }
3133
+ await runCleanup(includeImages);
3134
+ }
2704
3135
  };
2705
3136
  }
2706
3137
  export {
@@ -2717,4 +3148,4 @@ export {
2717
3148
  BunAdapter
2718
3149
  };
2719
3150
 
2720
- //# debugId=E6910E46B07952A064756E2164756E21
3151
+ //# debugId=8EC327761CD2C45664756E2164756E21