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/README.md +8 -5
- package/dist/cli.js +332 -59
- package/dist/index.js +542 -111
- package/dist/src/config.d.ts.map +1 -1
- package/dist/src/engine/default-seccomp-profile.d.ts +8 -0
- package/dist/src/engine/default-seccomp-profile.d.ts.map +1 -0
- package/dist/src/engine/docker.d.ts.map +1 -1
- package/dist/src/engine/image-builder.d.ts +1 -1
- package/dist/src/engine/image-builder.d.ts.map +1 -1
- package/dist/src/server/index.d.ts +20 -0
- package/dist/src/server/index.d.ts.map +1 -1
- package/dist/src/types.d.ts +2 -0
- package/dist/src/types.d.ts.map +1 -1
- package/package.json +2 -2
- package/schema/isol8.config.schema.json +5 -0
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
|
-
|
|
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 [
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1853
|
-
opts.push(`seccomp=${profile}`);
|
|
1854
|
-
}
|
|
2214
|
+
opts.push(`seccomp=${profile}`);
|
|
1855
2215
|
} catch (e) {
|
|
1856
|
-
|
|
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
|
-
|
|
1870
|
-
|
|
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.
|
|
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=
|
|
3151
|
+
//# debugId=8EC327761CD2C45664756E2164756E21
|