isol8 0.11.0 → 0.11.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +10 -1
- package/dist/cli.js +692 -540
- package/dist/index.js +196 -64
- package/dist/src/client/remote.d.ts +2 -2
- package/dist/src/client/remote.d.ts.map +1 -1
- package/dist/src/config.d.ts.map +1 -1
- package/dist/src/engine/docker.d.ts +20 -5
- package/dist/src/engine/docker.d.ts.map +1 -1
- package/dist/src/engine/image-builder.d.ts +14 -0
- package/dist/src/engine/image-builder.d.ts.map +1 -1
- package/dist/src/engine/pool.d.ts.map +1 -1
- package/dist/src/server/index.d.ts.map +1 -1
- package/dist/src/types.d.ts +48 -1
- package/dist/src/types.d.ts.map +1 -1
- package/package.json +3 -1
- package/schema/isol8.config.schema.json +31 -0
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 =
|
|
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,287 +55412,6 @@ class Semaphore {
|
|
|
55413
55412
|
}
|
|
55414
55413
|
}
|
|
55415
55414
|
|
|
55416
|
-
// src/engine/pool.ts
|
|
55417
|
-
class ContainerPool {
|
|
55418
|
-
docker;
|
|
55419
|
-
poolStrategy;
|
|
55420
|
-
cleanPoolSize;
|
|
55421
|
-
dirtyPoolSize;
|
|
55422
|
-
createOptions;
|
|
55423
|
-
networkMode;
|
|
55424
|
-
securityMode;
|
|
55425
|
-
pools = new Map;
|
|
55426
|
-
replenishing = new Set;
|
|
55427
|
-
pendingReplenishments = new Set;
|
|
55428
|
-
cleaningInterval = null;
|
|
55429
|
-
constructor(options) {
|
|
55430
|
-
this.docker = options.docker;
|
|
55431
|
-
this.poolStrategy = options.poolStrategy ?? "fast";
|
|
55432
|
-
this.createOptions = options.createOptions;
|
|
55433
|
-
this.networkMode = options.networkMode;
|
|
55434
|
-
this.securityMode = options.securityMode;
|
|
55435
|
-
if (typeof options.poolSize === "number") {
|
|
55436
|
-
this.cleanPoolSize = options.poolSize;
|
|
55437
|
-
this.dirtyPoolSize = options.poolSize;
|
|
55438
|
-
} else if (options.poolSize) {
|
|
55439
|
-
this.cleanPoolSize = options.poolSize.clean ?? 1;
|
|
55440
|
-
this.dirtyPoolSize = options.poolSize.dirty ?? 1;
|
|
55441
|
-
} else {
|
|
55442
|
-
this.cleanPoolSize = 1;
|
|
55443
|
-
this.dirtyPoolSize = 1;
|
|
55444
|
-
}
|
|
55445
|
-
if (this.poolStrategy === "fast") {
|
|
55446
|
-
this.startBackgroundCleaning();
|
|
55447
|
-
}
|
|
55448
|
-
}
|
|
55449
|
-
async acquire(image) {
|
|
55450
|
-
const pool = this.pools.get(image) ?? { clean: [], dirty: [] };
|
|
55451
|
-
if (this.poolStrategy === "fast") {
|
|
55452
|
-
if (pool.clean.length > 0) {
|
|
55453
|
-
const entry = pool.clean.shift();
|
|
55454
|
-
this.pools.set(image, pool);
|
|
55455
|
-
this.replenish(image);
|
|
55456
|
-
return entry.container;
|
|
55457
|
-
}
|
|
55458
|
-
if (pool.dirty.length > 0 && pool.clean.length < this.cleanPoolSize) {
|
|
55459
|
-
await this.cleanDirtyImmediate(image);
|
|
55460
|
-
const updatedPool = this.pools.get(image);
|
|
55461
|
-
if (updatedPool && updatedPool.clean.length > 0) {
|
|
55462
|
-
const entry = updatedPool.clean.shift();
|
|
55463
|
-
this.pools.set(image, updatedPool);
|
|
55464
|
-
this.replenish(image);
|
|
55465
|
-
return entry.container;
|
|
55466
|
-
}
|
|
55467
|
-
}
|
|
55468
|
-
return this.createContainer(image);
|
|
55469
|
-
}
|
|
55470
|
-
if (pool.clean && pool.clean.length > 0) {
|
|
55471
|
-
const entry = pool.clean.shift();
|
|
55472
|
-
this.pools.set(image, { clean: pool.clean, dirty: [] });
|
|
55473
|
-
await this.cleanupContainer(entry.container);
|
|
55474
|
-
this.replenish(image);
|
|
55475
|
-
return entry.container;
|
|
55476
|
-
}
|
|
55477
|
-
return this.createContainer(image);
|
|
55478
|
-
}
|
|
55479
|
-
async release(container, image) {
|
|
55480
|
-
let pool = this.pools.get(image);
|
|
55481
|
-
if (!pool) {
|
|
55482
|
-
pool = { clean: [], dirty: [] };
|
|
55483
|
-
this.pools.set(image, pool);
|
|
55484
|
-
}
|
|
55485
|
-
if (this.poolStrategy === "fast") {
|
|
55486
|
-
if (pool.dirty.length >= this.dirtyPoolSize) {
|
|
55487
|
-
await container.remove({ force: true }).catch(() => {});
|
|
55488
|
-
return;
|
|
55489
|
-
}
|
|
55490
|
-
pool.dirty.push({ container, createdAt: Date.now() });
|
|
55491
|
-
} else {
|
|
55492
|
-
if (pool.clean.length >= this.cleanPoolSize) {
|
|
55493
|
-
await container.remove({ force: true }).catch(() => {});
|
|
55494
|
-
return;
|
|
55495
|
-
}
|
|
55496
|
-
if (!pool.clean) {
|
|
55497
|
-
pool.clean = [];
|
|
55498
|
-
}
|
|
55499
|
-
pool.clean.push({ container, createdAt: Date.now() });
|
|
55500
|
-
}
|
|
55501
|
-
}
|
|
55502
|
-
startBackgroundCleaning() {
|
|
55503
|
-
this.cleaningInterval = setInterval(async () => {
|
|
55504
|
-
for (const [_image, pool] of this.pools) {
|
|
55505
|
-
for (let i = 0;i < this.dirtyPoolSize; i++) {
|
|
55506
|
-
if (pool.dirty.length > 0 && pool.clean.length < this.cleanPoolSize) {
|
|
55507
|
-
const entry = pool.dirty.shift();
|
|
55508
|
-
try {
|
|
55509
|
-
await this.cleanupContainer(entry.container);
|
|
55510
|
-
pool.clean.push(entry);
|
|
55511
|
-
} catch {
|
|
55512
|
-
entry.container.remove({ force: true }).catch(() => {});
|
|
55513
|
-
}
|
|
55514
|
-
}
|
|
55515
|
-
}
|
|
55516
|
-
}
|
|
55517
|
-
}, 5000);
|
|
55518
|
-
}
|
|
55519
|
-
async cleanDirtyImmediate(image) {
|
|
55520
|
-
const pool = this.pools.get(image);
|
|
55521
|
-
if (!pool || pool.dirty.length === 0 || pool.clean.length >= this.cleanPoolSize) {
|
|
55522
|
-
return;
|
|
55523
|
-
}
|
|
55524
|
-
const entry = pool.dirty.shift();
|
|
55525
|
-
try {
|
|
55526
|
-
await this.cleanupContainer(entry.container);
|
|
55527
|
-
pool.clean.push(entry);
|
|
55528
|
-
} catch {
|
|
55529
|
-
entry.container.remove({ force: true }).catch(() => {});
|
|
55530
|
-
}
|
|
55531
|
-
}
|
|
55532
|
-
async cleanupContainer(container) {
|
|
55533
|
-
const needsCleanup = this.securityMode === "strict";
|
|
55534
|
-
const needsIptables = this.networkMode === "filtered" && needsCleanup;
|
|
55535
|
-
if (!needsCleanup) {
|
|
55536
|
-
return;
|
|
55537
|
-
}
|
|
55538
|
-
try {
|
|
55539
|
-
const cleanupCmd = needsIptables ? "pkill -9 -u sandbox 2>/dev/null; /usr/sbin/iptables -F OUTPUT 2>/dev/null; rm -rf /sandbox/* /sandbox/.[!.]* 2>/dev/null; true" : "pkill -9 -u sandbox 2>/dev/null; rm -rf /sandbox/* /sandbox/.[!.]* 2>/dev/null; true";
|
|
55540
|
-
const cleanExec = await container.exec({
|
|
55541
|
-
Cmd: ["sh", "-c", cleanupCmd]
|
|
55542
|
-
});
|
|
55543
|
-
await cleanExec.start({ Detach: true });
|
|
55544
|
-
let info2 = await cleanExec.inspect();
|
|
55545
|
-
while (info2.Running) {
|
|
55546
|
-
await new Promise((r) => setTimeout(r, 5));
|
|
55547
|
-
info2 = await cleanExec.inspect();
|
|
55548
|
-
}
|
|
55549
|
-
} catch {}
|
|
55550
|
-
}
|
|
55551
|
-
async warm(image) {
|
|
55552
|
-
const pool = this.pools.get(image) ?? { clean: [], dirty: [] };
|
|
55553
|
-
this.pools.set(image, pool);
|
|
55554
|
-
const needed = this.poolStrategy === "fast" ? this.cleanPoolSize - pool.clean.length : this.cleanPoolSize - (pool.clean?.length ?? 0);
|
|
55555
|
-
if (needed <= 0) {
|
|
55556
|
-
return;
|
|
55557
|
-
}
|
|
55558
|
-
const promises = [];
|
|
55559
|
-
for (let i = 0;i < needed; i++) {
|
|
55560
|
-
promises.push(this.createContainer(image).then((container) => {
|
|
55561
|
-
if (this.poolStrategy === "fast") {
|
|
55562
|
-
pool.clean.push({ container, createdAt: Date.now() });
|
|
55563
|
-
} else {
|
|
55564
|
-
if (!pool.clean) {
|
|
55565
|
-
pool.clean = [];
|
|
55566
|
-
}
|
|
55567
|
-
pool.clean.push({ container, createdAt: Date.now() });
|
|
55568
|
-
}
|
|
55569
|
-
}));
|
|
55570
|
-
}
|
|
55571
|
-
await Promise.all(promises);
|
|
55572
|
-
}
|
|
55573
|
-
async stop() {
|
|
55574
|
-
return this.drain();
|
|
55575
|
-
}
|
|
55576
|
-
async drain() {
|
|
55577
|
-
if (this.cleaningInterval) {
|
|
55578
|
-
clearInterval(this.cleaningInterval);
|
|
55579
|
-
this.cleaningInterval = null;
|
|
55580
|
-
}
|
|
55581
|
-
await Promise.all(this.pendingReplenishments);
|
|
55582
|
-
const promises = [];
|
|
55583
|
-
for (const [, pool] of this.pools) {
|
|
55584
|
-
for (const entry of pool.clean ?? []) {
|
|
55585
|
-
promises.push(entry.container.remove({ force: true }).catch(() => {}));
|
|
55586
|
-
}
|
|
55587
|
-
for (const entry of pool.dirty) {
|
|
55588
|
-
promises.push(entry.container.remove({ force: true }).catch(() => {}));
|
|
55589
|
-
}
|
|
55590
|
-
}
|
|
55591
|
-
await Promise.all(promises);
|
|
55592
|
-
this.pools.clear();
|
|
55593
|
-
}
|
|
55594
|
-
async createContainer(image) {
|
|
55595
|
-
const container = await this.docker.createContainer({
|
|
55596
|
-
...this.createOptions,
|
|
55597
|
-
Image: image
|
|
55598
|
-
});
|
|
55599
|
-
logger.debug(`[Pool] Container ${container.id} created for image: ${image}`);
|
|
55600
|
-
await container.start();
|
|
55601
|
-
logger.debug(`[Pool] Container ${container.id} started`);
|
|
55602
|
-
return container;
|
|
55603
|
-
}
|
|
55604
|
-
replenish(image) {
|
|
55605
|
-
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) {
|
|
55613
|
-
return;
|
|
55614
|
-
}
|
|
55615
|
-
this.replenishing.add(image);
|
|
55616
|
-
const promise = this.createContainer(image).then((container) => {
|
|
55617
|
-
const p = this.pools.get(image);
|
|
55618
|
-
if (!p) {
|
|
55619
|
-
container.remove({ force: true }).catch(() => {});
|
|
55620
|
-
return;
|
|
55621
|
-
}
|
|
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
|
-
}
|
|
55628
|
-
} 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
|
-
}
|
|
55637
|
-
}
|
|
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
|
-
}
|
|
55646
|
-
}
|
|
55647
|
-
}
|
|
55648
|
-
var init_pool = __esm(() => {
|
|
55649
|
-
init_logger();
|
|
55650
|
-
});
|
|
55651
|
-
|
|
55652
|
-
// src/engine/stats.ts
|
|
55653
|
-
function calculateCPUPercent(stats) {
|
|
55654
|
-
const cpuDelta = stats.cpu_stats.cpu_usage.total_usage - stats.precpu_stats.cpu_usage.total_usage;
|
|
55655
|
-
const systemDelta = stats.cpu_stats.system_cpu_usage - stats.precpu_stats.system_cpu_usage;
|
|
55656
|
-
if (systemDelta === 0 || cpuDelta === 0) {
|
|
55657
|
-
return 0;
|
|
55658
|
-
}
|
|
55659
|
-
const numCores = stats.cpu_stats.online_cpus ?? stats.cpu_stats.cpu_usage.percpu_usage?.length ?? 1;
|
|
55660
|
-
return cpuDelta / systemDelta * numCores * 100;
|
|
55661
|
-
}
|
|
55662
|
-
function calculateNetworkStats(stats) {
|
|
55663
|
-
if (!stats.networks) {
|
|
55664
|
-
return { in: 0, out: 0 };
|
|
55665
|
-
}
|
|
55666
|
-
let rxBytes = 0;
|
|
55667
|
-
let txBytes = 0;
|
|
55668
|
-
for (const iface of Object.values(stats.networks)) {
|
|
55669
|
-
rxBytes += iface.rx_bytes;
|
|
55670
|
-
txBytes += iface.tx_bytes;
|
|
55671
|
-
}
|
|
55672
|
-
return { in: rxBytes, out: txBytes };
|
|
55673
|
-
}
|
|
55674
|
-
async function getContainerStats(container) {
|
|
55675
|
-
const stats = await container.stats({
|
|
55676
|
-
stream: false
|
|
55677
|
-
});
|
|
55678
|
-
const cpuPercent = calculateCPUPercent(stats);
|
|
55679
|
-
const memoryBytes = stats.memory_stats.usage;
|
|
55680
|
-
const network = calculateNetworkStats(stats);
|
|
55681
|
-
return {
|
|
55682
|
-
cpuPercent: Math.round(cpuPercent * 100) / 100,
|
|
55683
|
-
memoryMB: Math.round(memoryBytes / (1024 * 1024)),
|
|
55684
|
-
networkBytesIn: network.in,
|
|
55685
|
-
networkBytesOut: network.out
|
|
55686
|
-
};
|
|
55687
|
-
}
|
|
55688
|
-
function calculateResourceDelta(before, after) {
|
|
55689
|
-
return {
|
|
55690
|
-
cpuPercent: after.cpuPercent,
|
|
55691
|
-
memoryMB: after.memoryMB,
|
|
55692
|
-
networkBytesIn: after.networkBytesIn - before.networkBytesIn,
|
|
55693
|
-
networkBytesOut: after.networkBytesOut - before.networkBytesOut
|
|
55694
|
-
};
|
|
55695
|
-
}
|
|
55696
|
-
|
|
55697
55415
|
// src/engine/utils.ts
|
|
55698
55416
|
var exports_utils = {};
|
|
55699
55417
|
__export(exports_utils, {
|
|
@@ -55799,13 +55517,514 @@ function validatePackageName(name) {
|
|
|
55799
55517
|
return name;
|
|
55800
55518
|
}
|
|
55801
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
|
+
|
|
55742
|
+
// src/engine/pool.ts
|
|
55743
|
+
class ContainerPool {
|
|
55744
|
+
docker;
|
|
55745
|
+
poolStrategy;
|
|
55746
|
+
cleanPoolSize;
|
|
55747
|
+
dirtyPoolSize;
|
|
55748
|
+
createOptions;
|
|
55749
|
+
networkMode;
|
|
55750
|
+
securityMode;
|
|
55751
|
+
pools = new Map;
|
|
55752
|
+
replenishing = new Set;
|
|
55753
|
+
pendingReplenishments = new Set;
|
|
55754
|
+
cleaningInterval = null;
|
|
55755
|
+
constructor(options) {
|
|
55756
|
+
this.docker = options.docker;
|
|
55757
|
+
this.poolStrategy = options.poolStrategy ?? "fast";
|
|
55758
|
+
this.createOptions = options.createOptions;
|
|
55759
|
+
this.networkMode = options.networkMode;
|
|
55760
|
+
this.securityMode = options.securityMode;
|
|
55761
|
+
if (typeof options.poolSize === "number") {
|
|
55762
|
+
this.cleanPoolSize = options.poolSize;
|
|
55763
|
+
this.dirtyPoolSize = options.poolSize;
|
|
55764
|
+
} else if (options.poolSize) {
|
|
55765
|
+
this.cleanPoolSize = options.poolSize.clean ?? 1;
|
|
55766
|
+
this.dirtyPoolSize = options.poolSize.dirty ?? 1;
|
|
55767
|
+
} else {
|
|
55768
|
+
this.cleanPoolSize = 1;
|
|
55769
|
+
this.dirtyPoolSize = 1;
|
|
55770
|
+
}
|
|
55771
|
+
if (this.poolStrategy === "fast") {
|
|
55772
|
+
this.startBackgroundCleaning();
|
|
55773
|
+
}
|
|
55774
|
+
}
|
|
55775
|
+
async acquire(image) {
|
|
55776
|
+
const pool = this.pools.get(image) ?? { clean: [], dirty: [] };
|
|
55777
|
+
if (this.poolStrategy === "fast") {
|
|
55778
|
+
if (pool.clean.length > 0) {
|
|
55779
|
+
const entry = pool.clean.shift();
|
|
55780
|
+
this.pools.set(image, pool);
|
|
55781
|
+
this.replenish(image);
|
|
55782
|
+
return entry.container;
|
|
55783
|
+
}
|
|
55784
|
+
if (pool.dirty.length > 0 && pool.clean.length < this.cleanPoolSize) {
|
|
55785
|
+
await this.cleanDirtyImmediate(image);
|
|
55786
|
+
const updatedPool = this.pools.get(image);
|
|
55787
|
+
if (updatedPool && updatedPool.clean.length > 0) {
|
|
55788
|
+
const entry = updatedPool.clean.shift();
|
|
55789
|
+
this.pools.set(image, updatedPool);
|
|
55790
|
+
this.replenish(image);
|
|
55791
|
+
return entry.container;
|
|
55792
|
+
}
|
|
55793
|
+
}
|
|
55794
|
+
return this.createContainer(image);
|
|
55795
|
+
}
|
|
55796
|
+
if (pool.clean && pool.clean.length > 0) {
|
|
55797
|
+
const entry = pool.clean.shift();
|
|
55798
|
+
this.pools.set(image, { clean: pool.clean, dirty: [] });
|
|
55799
|
+
await this.cleanupContainer(entry.container);
|
|
55800
|
+
this.replenish(image);
|
|
55801
|
+
return entry.container;
|
|
55802
|
+
}
|
|
55803
|
+
return this.createContainer(image);
|
|
55804
|
+
}
|
|
55805
|
+
async release(container, image) {
|
|
55806
|
+
let pool = this.pools.get(image);
|
|
55807
|
+
if (!pool) {
|
|
55808
|
+
pool = { clean: [], dirty: [] };
|
|
55809
|
+
this.pools.set(image, pool);
|
|
55810
|
+
}
|
|
55811
|
+
if (this.poolStrategy === "fast") {
|
|
55812
|
+
if (pool.dirty.length >= this.dirtyPoolSize) {
|
|
55813
|
+
await container.remove({ force: true }).catch(() => {});
|
|
55814
|
+
return;
|
|
55815
|
+
}
|
|
55816
|
+
pool.dirty.push({ container, createdAt: Date.now() });
|
|
55817
|
+
} else {
|
|
55818
|
+
if (pool.clean.length >= this.cleanPoolSize) {
|
|
55819
|
+
await container.remove({ force: true }).catch(() => {});
|
|
55820
|
+
return;
|
|
55821
|
+
}
|
|
55822
|
+
if (!pool.clean) {
|
|
55823
|
+
pool.clean = [];
|
|
55824
|
+
}
|
|
55825
|
+
pool.clean.push({ container, createdAt: Date.now() });
|
|
55826
|
+
}
|
|
55827
|
+
}
|
|
55828
|
+
startBackgroundCleaning() {
|
|
55829
|
+
this.cleaningInterval = setInterval(async () => {
|
|
55830
|
+
for (const [_image, pool] of this.pools) {
|
|
55831
|
+
for (let i = 0;i < this.dirtyPoolSize; i++) {
|
|
55832
|
+
if (pool.dirty.length > 0 && pool.clean.length < this.cleanPoolSize) {
|
|
55833
|
+
const entry = pool.dirty.shift();
|
|
55834
|
+
try {
|
|
55835
|
+
await this.cleanupContainer(entry.container);
|
|
55836
|
+
pool.clean.push(entry);
|
|
55837
|
+
} catch {
|
|
55838
|
+
entry.container.remove({ force: true }).catch(() => {});
|
|
55839
|
+
}
|
|
55840
|
+
}
|
|
55841
|
+
}
|
|
55842
|
+
}
|
|
55843
|
+
}, 5000);
|
|
55844
|
+
}
|
|
55845
|
+
async cleanDirtyImmediate(image) {
|
|
55846
|
+
const pool = this.pools.get(image);
|
|
55847
|
+
if (!pool || pool.dirty.length === 0 || pool.clean.length >= this.cleanPoolSize) {
|
|
55848
|
+
return;
|
|
55849
|
+
}
|
|
55850
|
+
const entry = pool.dirty.shift();
|
|
55851
|
+
try {
|
|
55852
|
+
await this.cleanupContainer(entry.container);
|
|
55853
|
+
pool.clean.push(entry);
|
|
55854
|
+
} catch {
|
|
55855
|
+
entry.container.remove({ force: true }).catch(() => {});
|
|
55856
|
+
}
|
|
55857
|
+
}
|
|
55858
|
+
async cleanupContainer(container) {
|
|
55859
|
+
const needsCleanup = this.securityMode === "strict";
|
|
55860
|
+
const needsIptables = this.networkMode === "filtered" && needsCleanup;
|
|
55861
|
+
if (!needsCleanup) {
|
|
55862
|
+
return;
|
|
55863
|
+
}
|
|
55864
|
+
try {
|
|
55865
|
+
const cleanupCmd = needsIptables ? "pkill -9 -u sandbox 2>/dev/null; /usr/sbin/iptables -F OUTPUT 2>/dev/null; rm -rf /sandbox/* /sandbox/.[!.]* 2>/dev/null; true" : "pkill -9 -u sandbox 2>/dev/null; rm -rf /sandbox/* /sandbox/.[!.]* 2>/dev/null; true";
|
|
55866
|
+
const cleanExec = await container.exec({
|
|
55867
|
+
Cmd: ["sh", "-c", cleanupCmd]
|
|
55868
|
+
});
|
|
55869
|
+
await cleanExec.start({ Detach: true });
|
|
55870
|
+
let info2 = await cleanExec.inspect();
|
|
55871
|
+
while (info2.Running) {
|
|
55872
|
+
await new Promise((r) => setTimeout(r, 5));
|
|
55873
|
+
info2 = await cleanExec.inspect();
|
|
55874
|
+
}
|
|
55875
|
+
} catch {}
|
|
55876
|
+
}
|
|
55877
|
+
async warm(image) {
|
|
55878
|
+
const pool = this.pools.get(image) ?? { clean: [], dirty: [] };
|
|
55879
|
+
this.pools.set(image, pool);
|
|
55880
|
+
const needed = this.poolStrategy === "fast" ? this.cleanPoolSize - pool.clean.length : this.cleanPoolSize - (pool.clean?.length ?? 0);
|
|
55881
|
+
if (needed <= 0) {
|
|
55882
|
+
return;
|
|
55883
|
+
}
|
|
55884
|
+
const promises = [];
|
|
55885
|
+
for (let i = 0;i < needed; i++) {
|
|
55886
|
+
promises.push(this.createContainer(image).then((container) => {
|
|
55887
|
+
if (this.poolStrategy === "fast") {
|
|
55888
|
+
pool.clean.push({ container, createdAt: Date.now() });
|
|
55889
|
+
} else {
|
|
55890
|
+
if (!pool.clean) {
|
|
55891
|
+
pool.clean = [];
|
|
55892
|
+
}
|
|
55893
|
+
pool.clean.push({ container, createdAt: Date.now() });
|
|
55894
|
+
}
|
|
55895
|
+
}));
|
|
55896
|
+
}
|
|
55897
|
+
await Promise.all(promises);
|
|
55898
|
+
}
|
|
55899
|
+
async stop() {
|
|
55900
|
+
return this.drain();
|
|
55901
|
+
}
|
|
55902
|
+
async drain() {
|
|
55903
|
+
if (this.cleaningInterval) {
|
|
55904
|
+
clearInterval(this.cleaningInterval);
|
|
55905
|
+
this.cleaningInterval = null;
|
|
55906
|
+
}
|
|
55907
|
+
await Promise.all(this.pendingReplenishments);
|
|
55908
|
+
const promises = [];
|
|
55909
|
+
for (const [, pool] of this.pools) {
|
|
55910
|
+
for (const entry of pool.clean ?? []) {
|
|
55911
|
+
promises.push(entry.container.remove({ force: true }).catch(() => {}));
|
|
55912
|
+
}
|
|
55913
|
+
for (const entry of pool.dirty) {
|
|
55914
|
+
promises.push(entry.container.remove({ force: true }).catch(() => {}));
|
|
55915
|
+
}
|
|
55916
|
+
}
|
|
55917
|
+
await Promise.all(promises);
|
|
55918
|
+
this.pools.clear();
|
|
55919
|
+
}
|
|
55920
|
+
async createContainer(image) {
|
|
55921
|
+
const container = await this.docker.createContainer({
|
|
55922
|
+
...this.createOptions,
|
|
55923
|
+
Image: image
|
|
55924
|
+
});
|
|
55925
|
+
logger.debug(`[Pool] Container ${container.id} created for image: ${image}`);
|
|
55926
|
+
await container.start();
|
|
55927
|
+
logger.debug(`[Pool] Container ${container.id} started`);
|
|
55928
|
+
return container;
|
|
55929
|
+
}
|
|
55930
|
+
replenish(image) {
|
|
55931
|
+
if (this.replenishing.has(image)) {
|
|
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(() => {});
|
|
55945
|
+
return;
|
|
55946
|
+
}
|
|
55947
|
+
if (this.poolStrategy === "fast") {
|
|
55948
|
+
if (p.clean.length < this.cleanPoolSize) {
|
|
55949
|
+
p.clean.push({ container, createdAt: Date.now() });
|
|
55950
|
+
} else {
|
|
55951
|
+
container.remove({ force: true }).catch(() => {});
|
|
55952
|
+
}
|
|
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() });
|
|
55959
|
+
} else {
|
|
55960
|
+
container.remove({ force: true }).catch(() => {});
|
|
55961
|
+
}
|
|
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);
|
|
55970
|
+
}
|
|
55971
|
+
}
|
|
55972
|
+
var init_pool = __esm(() => {
|
|
55973
|
+
init_logger();
|
|
55974
|
+
});
|
|
55975
|
+
|
|
55976
|
+
// src/engine/stats.ts
|
|
55977
|
+
function calculateCPUPercent(stats) {
|
|
55978
|
+
const cpuDelta = stats.cpu_stats.cpu_usage.total_usage - stats.precpu_stats.cpu_usage.total_usage;
|
|
55979
|
+
const systemDelta = stats.cpu_stats.system_cpu_usage - stats.precpu_stats.system_cpu_usage;
|
|
55980
|
+
if (systemDelta === 0 || cpuDelta === 0) {
|
|
55981
|
+
return 0;
|
|
55982
|
+
}
|
|
55983
|
+
const numCores = stats.cpu_stats.online_cpus ?? stats.cpu_stats.cpu_usage.percpu_usage?.length ?? 1;
|
|
55984
|
+
return cpuDelta / systemDelta * numCores * 100;
|
|
55985
|
+
}
|
|
55986
|
+
function calculateNetworkStats(stats) {
|
|
55987
|
+
if (!stats.networks) {
|
|
55988
|
+
return { in: 0, out: 0 };
|
|
55989
|
+
}
|
|
55990
|
+
let rxBytes = 0;
|
|
55991
|
+
let txBytes = 0;
|
|
55992
|
+
for (const iface of Object.values(stats.networks)) {
|
|
55993
|
+
rxBytes += iface.rx_bytes;
|
|
55994
|
+
txBytes += iface.tx_bytes;
|
|
55995
|
+
}
|
|
55996
|
+
return { in: rxBytes, out: txBytes };
|
|
55997
|
+
}
|
|
55998
|
+
async function getContainerStats(container) {
|
|
55999
|
+
const stats = await container.stats({
|
|
56000
|
+
stream: false
|
|
56001
|
+
});
|
|
56002
|
+
const cpuPercent = calculateCPUPercent(stats);
|
|
56003
|
+
const memoryBytes = stats.memory_stats.usage;
|
|
56004
|
+
const network = calculateNetworkStats(stats);
|
|
56005
|
+
return {
|
|
56006
|
+
cpuPercent: Math.round(cpuPercent * 100) / 100,
|
|
56007
|
+
memoryMB: Math.round(memoryBytes / (1024 * 1024)),
|
|
56008
|
+
networkBytesIn: network.in,
|
|
56009
|
+
networkBytesOut: network.out
|
|
56010
|
+
};
|
|
56011
|
+
}
|
|
56012
|
+
function calculateResourceDelta(before, after) {
|
|
56013
|
+
return {
|
|
56014
|
+
cpuPercent: after.cpuPercent,
|
|
56015
|
+
memoryMB: after.memoryMB,
|
|
56016
|
+
networkBytesIn: after.networkBytesIn - before.networkBytesIn,
|
|
56017
|
+
networkBytesOut: after.networkBytesOut - before.networkBytesOut
|
|
56018
|
+
};
|
|
56019
|
+
}
|
|
56020
|
+
|
|
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
|
|
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
|
-
|
|
56338
|
-
|
|
56339
|
-
|
|
56340
|
-
|
|
56341
|
-
|
|
56342
|
-
|
|
56343
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
56384
|
-
|
|
56385
|
-
|
|
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
|
-
|
|
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 =
|
|
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 (
|
|
56656
|
-
return
|
|
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 (
|
|
56660
|
-
return
|
|
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:")
|
|
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.
|
|
57174
|
+
version: "0.11.0",
|
|
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
|
-
...
|
|
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
|
-
...
|
|
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)").
|
|
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,
|
|
@@ -62843,6 +62957,11 @@ Isol8 Configuration
|
|
|
62843
62957
|
console.log(" ── Cleanup ──");
|
|
62844
62958
|
console.log(` Auto-prune: ${config.cleanup.autoPrune ? "yes" : "no"}`);
|
|
62845
62959
|
console.log(` Max idle time: ${config.cleanup.maxContainerAgeMs}ms (${Math.round(config.cleanup.maxContainerAgeMs / 60000)}min)`);
|
|
62960
|
+
console.log("");
|
|
62961
|
+
console.log(" ── Pool Defaults (Serve) ──");
|
|
62962
|
+
console.log(` Pool strategy: ${config.poolStrategy}`);
|
|
62963
|
+
const poolSize = typeof config.poolSize === "number" ? String(config.poolSize) : `${config.poolSize.clean},${config.poolSize.dirty}`;
|
|
62964
|
+
console.log(` Pool size: ${poolSize}`);
|
|
62846
62965
|
const deps = config.dependencies;
|
|
62847
62966
|
const hasDeps = Object.values(deps).some((pkgs) => pkgs && pkgs.length > 0);
|
|
62848
62967
|
console.log("");
|
|
@@ -62865,7 +62984,7 @@ Isol8 Configuration
|
|
|
62865
62984
|
}
|
|
62866
62985
|
console.log("");
|
|
62867
62986
|
});
|
|
62868
|
-
program2.command("cleanup").description("Remove orphaned isol8 containers").option("--force", "Skip confirmation prompt").action(async (opts) => {
|
|
62987
|
+
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
62988
|
const docker = new import_dockerode2.default;
|
|
62870
62989
|
logger.debug("[Cleanup] Connecting to Docker daemon");
|
|
62871
62990
|
const spinner = ora("Checking Docker...").start();
|
|
@@ -62881,17 +63000,33 @@ program2.command("cleanup").description("Remove orphaned isol8 containers").opti
|
|
|
62881
63000
|
const containers = await docker.listContainers({ all: true });
|
|
62882
63001
|
const isol8Containers = containers.filter((c) => c.Image.startsWith("isol8:") || c.Image.startsWith("isol8-custom:"));
|
|
62883
63002
|
logger.debug(`[Cleanup] Found ${containers.length} total containers, ${isol8Containers.length} isol8 containers`);
|
|
62884
|
-
|
|
62885
|
-
|
|
63003
|
+
let isol8Images = [];
|
|
63004
|
+
if (opts.images) {
|
|
63005
|
+
spinner.start("Finding isol8 images...");
|
|
63006
|
+
const images = await docker.listImages({ all: true });
|
|
63007
|
+
isol8Images = images.filter((img) => img.RepoTags?.some((tag) => tag.startsWith("isol8:"))).map((img) => ({ id: img.Id, tags: img.RepoTags ?? [] }));
|
|
63008
|
+
logger.debug(`[Cleanup] Found ${images.length} total images, ${isol8Images.length} isol8 images`);
|
|
63009
|
+
}
|
|
63010
|
+
if (isol8Containers.length === 0 && (!opts.images || isol8Images.length === 0)) {
|
|
63011
|
+
spinner.info(opts.images ? "No isol8 containers or images found" : "No isol8 containers found");
|
|
62886
63012
|
return;
|
|
62887
63013
|
}
|
|
62888
|
-
spinner.succeed(`Found ${isol8Containers.length} isol8 container(s)`);
|
|
63014
|
+
spinner.succeed(`Found ${isol8Containers.length} isol8 container(s)` + (opts.images ? ` and ${isol8Images.length} image(s)` : ""));
|
|
62889
63015
|
console.log("");
|
|
62890
63016
|
for (const c of isol8Containers) {
|
|
62891
63017
|
const status = c.State === "running" ? "\uD83D\uDFE2 running" : "⚪ stopped";
|
|
62892
63018
|
const created = new Date(c.Created * 1000).toLocaleString();
|
|
62893
63019
|
console.log(` ${status} ${c.Id.slice(0, 12)} | ${c.Image} | created ${created}`);
|
|
62894
63020
|
}
|
|
63021
|
+
if (opts.images && isol8Images.length > 0) {
|
|
63022
|
+
if (isol8Containers.length > 0) {
|
|
63023
|
+
console.log("");
|
|
63024
|
+
}
|
|
63025
|
+
for (const image of isol8Images) {
|
|
63026
|
+
const tagText = image.tags.length > 0 ? image.tags.join(", ") : "<untagged>";
|
|
63027
|
+
console.log(` \uD83D\uDDBC️ image ${image.id.slice(0, 12)} | ${tagText}`);
|
|
63028
|
+
}
|
|
63029
|
+
}
|
|
62895
63030
|
console.log("");
|
|
62896
63031
|
if (!opts.force) {
|
|
62897
63032
|
const readline = await import("node:readline");
|
|
@@ -62900,7 +63035,8 @@ program2.command("cleanup").description("Remove orphaned isol8 containers").opti
|
|
|
62900
63035
|
output: process.stdout
|
|
62901
63036
|
});
|
|
62902
63037
|
const answer = await new Promise((resolve3) => {
|
|
62903
|
-
|
|
63038
|
+
const targetLabel = opts.images ? "containers and images" : "containers";
|
|
63039
|
+
rl.question(`Remove all these ${targetLabel}? [y/N] `, resolve3);
|
|
62904
63040
|
});
|
|
62905
63041
|
rl.close();
|
|
62906
63042
|
if (answer.toLowerCase() !== "y" && answer.toLowerCase() !== "yes") {
|
|
@@ -62908,20 +63044,40 @@ program2.command("cleanup").description("Remove orphaned isol8 containers").opti
|
|
|
62908
63044
|
return;
|
|
62909
63045
|
}
|
|
62910
63046
|
}
|
|
62911
|
-
|
|
62912
|
-
|
|
62913
|
-
|
|
62914
|
-
|
|
62915
|
-
|
|
63047
|
+
let containerResult = { removed: 0, failed: 0, errors: [] };
|
|
63048
|
+
if (isol8Containers.length > 0) {
|
|
63049
|
+
spinner.start("Removing containers...");
|
|
63050
|
+
logger.debug("[Cleanup] Removing containers");
|
|
63051
|
+
containerResult = await DockerIsol8.cleanup(docker);
|
|
63052
|
+
logger.debug(`[Cleanup] Containers removed: ${containerResult.removed}, failed: ${containerResult.failed}`);
|
|
63053
|
+
}
|
|
63054
|
+
if (containerResult.errors.length > 0) {
|
|
62916
63055
|
console.log("");
|
|
62917
|
-
for (const err of
|
|
63056
|
+
for (const err of containerResult.errors) {
|
|
62918
63057
|
console.error(` Failed to remove ${err}`);
|
|
62919
63058
|
}
|
|
62920
63059
|
}
|
|
62921
|
-
if (
|
|
62922
|
-
spinner.succeed(`Removed ${
|
|
63060
|
+
if (containerResult.failed === 0) {
|
|
63061
|
+
spinner.succeed(`Removed ${containerResult.removed} container(s)`);
|
|
62923
63062
|
} else {
|
|
62924
|
-
spinner.warn(`Removed ${
|
|
63063
|
+
spinner.warn(`Removed ${containerResult.removed} container(s), ${containerResult.failed} failed`);
|
|
63064
|
+
}
|
|
63065
|
+
if (opts.images && isol8Images.length > 0) {
|
|
63066
|
+
spinner.start("Removing images...");
|
|
63067
|
+
logger.debug("[Cleanup] Removing images");
|
|
63068
|
+
const imageResult = await DockerIsol8.cleanupImages(docker);
|
|
63069
|
+
logger.debug(`[Cleanup] Images removed: ${imageResult.removed}, failed: ${imageResult.failed}`);
|
|
63070
|
+
if (imageResult.errors.length > 0) {
|
|
63071
|
+
console.log("");
|
|
63072
|
+
for (const err of imageResult.errors) {
|
|
63073
|
+
console.error(` Failed to remove image ${err}`);
|
|
63074
|
+
}
|
|
63075
|
+
}
|
|
63076
|
+
if (imageResult.failed === 0) {
|
|
63077
|
+
spinner.succeed(`Removed ${imageResult.removed} image(s)`);
|
|
63078
|
+
} else {
|
|
63079
|
+
spinner.warn(`Removed ${imageResult.removed} image(s), ${imageResult.failed} failed`);
|
|
63080
|
+
}
|
|
62925
63081
|
}
|
|
62926
63082
|
});
|
|
62927
63083
|
async function resolveRunInput(file, opts) {
|
|
@@ -62997,12 +63153,8 @@ async function resolveRunInput(file, opts) {
|
|
|
62997
63153
|
debug: opts.debug ?? config.debug,
|
|
62998
63154
|
persist: opts.persist ?? false,
|
|
62999
63155
|
...opts.logNetwork ? { logNetwork: true } : {},
|
|
63000
|
-
|
|
63001
|
-
|
|
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 }
|
|
63156
|
+
dependencies: config.dependencies,
|
|
63157
|
+
remoteCode: config.remoteCode
|
|
63006
63158
|
};
|
|
63007
63159
|
logger.debug(`[Run] Engine options: mode=${engineOptions.mode}, network=${engineOptions.network}`);
|
|
63008
63160
|
let fileExtension;
|
|
@@ -63088,4 +63240,4 @@ if (!process.argv.slice(2).length) {
|
|
|
63088
63240
|
}
|
|
63089
63241
|
program2.parse();
|
|
63090
63242
|
|
|
63091
|
-
//# debugId=
|
|
63243
|
+
//# debugId=6B3AFECE6CC836BD64756E2164756E21
|