isol8 0.10.3 → 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 +26 -1
- package/dist/cli.js +902 -333
- package/dist/index.js +450 -79
- 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/code-fetcher.d.ts +21 -0
- package/dist/src/engine/code-fetcher.d.ts.map +1 -0
- package/dist/src/engine/docker.d.ts +22 -5
- package/dist/src/engine/docker.d.ts.map +1 -1
- package/dist/src/engine/image-builder.d.ts +26 -2
- package/dist/src/engine/image-builder.d.ts.map +1 -1
- package/dist/src/engine/pool.d.ts.map +1 -1
- package/dist/src/index.d.ts +1 -1
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/server/index.d.ts.map +1 -1
- package/dist/src/types.d.ts +92 -3
- package/dist/src/types.d.ts.map +1 -1
- package/package.json +3 -1
- package/schema/isol8.config.schema.json +90 -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
|
|
@@ -54803,6 +54800,13 @@ function mergeConfig(defaults, overrides) {
|
|
|
54803
54800
|
seccomp: overrides.security?.seccomp ?? defaults.security.seccomp,
|
|
54804
54801
|
customProfilePath: overrides.security?.customProfilePath ?? defaults.security.customProfilePath
|
|
54805
54802
|
},
|
|
54803
|
+
remoteCode: {
|
|
54804
|
+
...defaults.remoteCode,
|
|
54805
|
+
...overrides.remoteCode,
|
|
54806
|
+
allowedSchemes: overrides.remoteCode?.allowedSchemes ?? defaults.remoteCode.allowedSchemes,
|
|
54807
|
+
allowedHosts: overrides.remoteCode?.allowedHosts ?? defaults.remoteCode.allowedHosts,
|
|
54808
|
+
blockedHosts: overrides.remoteCode?.blockedHosts ?? defaults.remoteCode.blockedHosts
|
|
54809
|
+
},
|
|
54806
54810
|
audit: {
|
|
54807
54811
|
...defaults.audit,
|
|
54808
54812
|
...overrides.audit
|
|
@@ -54830,10 +54834,34 @@ var init_config = __esm(() => {
|
|
|
54830
54834
|
autoPrune: true,
|
|
54831
54835
|
maxContainerAgeMs: 3600000
|
|
54832
54836
|
},
|
|
54837
|
+
poolStrategy: "fast",
|
|
54838
|
+
poolSize: { clean: 1, dirty: 1 },
|
|
54833
54839
|
dependencies: {},
|
|
54834
54840
|
security: {
|
|
54835
54841
|
seccomp: "strict"
|
|
54836
54842
|
},
|
|
54843
|
+
remoteCode: {
|
|
54844
|
+
enabled: false,
|
|
54845
|
+
allowedSchemes: ["https"],
|
|
54846
|
+
allowedHosts: [],
|
|
54847
|
+
blockedHosts: [
|
|
54848
|
+
"^localhost$",
|
|
54849
|
+
"^127(?:\\.[0-9]{1,3}){3}$",
|
|
54850
|
+
"^\\[::1\\]$",
|
|
54851
|
+
"^::1$",
|
|
54852
|
+
"^10(?:\\.[0-9]{1,3}){3}$",
|
|
54853
|
+
"^172\\.(?:1[6-9]|2[0-9]|3[0-1])(?:\\.[0-9]{1,3}){2}$",
|
|
54854
|
+
"^192\\.168(?:\\.[0-9]{1,3}){2}$",
|
|
54855
|
+
"^169\\.254(?:\\.[0-9]{1,3}){2}$",
|
|
54856
|
+
"^metadata\\.google\\.internal$",
|
|
54857
|
+
"^169\\.254\\.169\\.254$"
|
|
54858
|
+
],
|
|
54859
|
+
maxCodeSize: 10 * 1024 * 1024,
|
|
54860
|
+
fetchTimeoutMs: 30000,
|
|
54861
|
+
requireHash: false,
|
|
54862
|
+
enableCache: true,
|
|
54863
|
+
cacheTtl: 3600
|
|
54864
|
+
},
|
|
54837
54865
|
audit: {
|
|
54838
54866
|
enabled: false,
|
|
54839
54867
|
destination: "filesystem",
|
|
@@ -55174,6 +55202,180 @@ var init_audit = __esm(() => {
|
|
|
55174
55202
|
init_logger();
|
|
55175
55203
|
});
|
|
55176
55204
|
|
|
55205
|
+
// src/engine/code-fetcher.ts
|
|
55206
|
+
import { createHash } from "node:crypto";
|
|
55207
|
+
import { lookup as dnsLookup } from "node:dns/promises";
|
|
55208
|
+
import { isIP } from "node:net";
|
|
55209
|
+
function sha256Hex(input) {
|
|
55210
|
+
return createHash("sha256").update(input, "utf-8").digest("hex");
|
|
55211
|
+
}
|
|
55212
|
+
function normalizeScheme(url) {
|
|
55213
|
+
return url.protocol.replace(/:$/, "").toLowerCase();
|
|
55214
|
+
}
|
|
55215
|
+
function isBlockedByPattern(host, patterns) {
|
|
55216
|
+
return patterns.some((pattern) => new RegExp(pattern, "i").test(host));
|
|
55217
|
+
}
|
|
55218
|
+
function isAllowedByPattern(host, patterns) {
|
|
55219
|
+
if (patterns.length === 0) {
|
|
55220
|
+
return true;
|
|
55221
|
+
}
|
|
55222
|
+
return patterns.some((pattern) => new RegExp(pattern, "i").test(host));
|
|
55223
|
+
}
|
|
55224
|
+
function isPrivateIpv4(ip) {
|
|
55225
|
+
const parts = ip.split(IPV4_SEPARATOR).map((v) => Number.parseInt(v, 10));
|
|
55226
|
+
if (parts.length !== 4 || parts.some((p) => Number.isNaN(p))) {
|
|
55227
|
+
return false;
|
|
55228
|
+
}
|
|
55229
|
+
const a = parts[0];
|
|
55230
|
+
const b = parts[1];
|
|
55231
|
+
if (a === 10 || a === 127 || a === 0) {
|
|
55232
|
+
return true;
|
|
55233
|
+
}
|
|
55234
|
+
if (a === 169 && b === 254) {
|
|
55235
|
+
return true;
|
|
55236
|
+
}
|
|
55237
|
+
if (a === 172 && b >= 16 && b <= 31) {
|
|
55238
|
+
return true;
|
|
55239
|
+
}
|
|
55240
|
+
if (a === 192 && b === 168) {
|
|
55241
|
+
return true;
|
|
55242
|
+
}
|
|
55243
|
+
if (a === 100 && b >= 64 && b <= 127) {
|
|
55244
|
+
return true;
|
|
55245
|
+
}
|
|
55246
|
+
return false;
|
|
55247
|
+
}
|
|
55248
|
+
function isPrivateIpv6(ip) {
|
|
55249
|
+
const normalized = ip.toLowerCase();
|
|
55250
|
+
if (normalized === IPV6_LOOPBACK) {
|
|
55251
|
+
return true;
|
|
55252
|
+
}
|
|
55253
|
+
return normalized.startsWith("fc") || normalized.startsWith("fd") || normalized.startsWith("fe8") || normalized.startsWith("fe9") || normalized.startsWith("fea") || normalized.startsWith("feb");
|
|
55254
|
+
}
|
|
55255
|
+
function isPrivateIp(ip) {
|
|
55256
|
+
const family = isIP(ip);
|
|
55257
|
+
if (family === 4) {
|
|
55258
|
+
return isPrivateIpv4(ip);
|
|
55259
|
+
}
|
|
55260
|
+
if (family === 6) {
|
|
55261
|
+
return isPrivateIpv6(ip);
|
|
55262
|
+
}
|
|
55263
|
+
return false;
|
|
55264
|
+
}
|
|
55265
|
+
async function assertHostResolvesPublic(host, lookupFn) {
|
|
55266
|
+
if (isIP(host) && isPrivateIp(host)) {
|
|
55267
|
+
throw new Error(`Blocked code URL host: ${host}`);
|
|
55268
|
+
}
|
|
55269
|
+
try {
|
|
55270
|
+
const records = await lookupFn(host);
|
|
55271
|
+
for (const record of records) {
|
|
55272
|
+
if (isPrivateIp(record.address)) {
|
|
55273
|
+
throw new Error(`Blocked code URL host: ${host}`);
|
|
55274
|
+
}
|
|
55275
|
+
}
|
|
55276
|
+
} catch (err) {
|
|
55277
|
+
if (err instanceof Error && err.message.startsWith("Blocked code URL host:")) {
|
|
55278
|
+
throw err;
|
|
55279
|
+
}
|
|
55280
|
+
throw new Error(`Failed to resolve code URL host: ${host}`);
|
|
55281
|
+
}
|
|
55282
|
+
}
|
|
55283
|
+
function decodeUtf8(content) {
|
|
55284
|
+
const decoder = new TextDecoder("utf-8", { fatal: true });
|
|
55285
|
+
const text = decoder.decode(content);
|
|
55286
|
+
if (text.includes("\x00")) {
|
|
55287
|
+
throw new Error("Fetched code appears to be binary content");
|
|
55288
|
+
}
|
|
55289
|
+
return text;
|
|
55290
|
+
}
|
|
55291
|
+
async function fetchRemoteCode(request, policy, deps = {}) {
|
|
55292
|
+
if (!policy.enabled) {
|
|
55293
|
+
throw new Error("Remote code fetching is disabled. Set remoteCode.enabled=true to allow it.");
|
|
55294
|
+
}
|
|
55295
|
+
const fetchFn = deps.fetchFn ?? globalThis.fetch;
|
|
55296
|
+
const lookupFn = deps.lookupFn ?? (async (hostname) => {
|
|
55297
|
+
const records = await dnsLookup(hostname, { all: true, verbatim: true });
|
|
55298
|
+
return records;
|
|
55299
|
+
});
|
|
55300
|
+
if (!request.codeUrl) {
|
|
55301
|
+
throw new Error("codeUrl is required for remote code fetching");
|
|
55302
|
+
}
|
|
55303
|
+
const url = new URL(request.codeUrl);
|
|
55304
|
+
const scheme = normalizeScheme(url);
|
|
55305
|
+
if (scheme === "http" && !request.allowInsecureCodeUrl) {
|
|
55306
|
+
throw new Error("Insecure code URL blocked. Use allowInsecureCodeUrl=true to allow HTTP.");
|
|
55307
|
+
}
|
|
55308
|
+
if (!policy.allowedSchemes.map((s) => s.toLowerCase()).includes(scheme)) {
|
|
55309
|
+
throw new Error(`URL scheme not allowed: ${scheme}`);
|
|
55310
|
+
}
|
|
55311
|
+
const host = url.hostname.toLowerCase();
|
|
55312
|
+
if (!isAllowedByPattern(host, policy.allowedHosts) || isBlockedByPattern(host, policy.blockedHosts)) {
|
|
55313
|
+
throw new Error(`Blocked code URL host: ${host}`);
|
|
55314
|
+
}
|
|
55315
|
+
await assertHostResolvesPublic(host, lookupFn);
|
|
55316
|
+
if (policy.requireHash && !request.codeHash) {
|
|
55317
|
+
throw new Error("Hash verification required: provide codeHash for remote code execution.");
|
|
55318
|
+
}
|
|
55319
|
+
const controller = new AbortController;
|
|
55320
|
+
const timeout = setTimeout(() => controller.abort(), policy.fetchTimeoutMs);
|
|
55321
|
+
let response;
|
|
55322
|
+
try {
|
|
55323
|
+
response = await fetchFn(url.toString(), {
|
|
55324
|
+
method: "GET",
|
|
55325
|
+
redirect: "follow",
|
|
55326
|
+
signal: controller.signal
|
|
55327
|
+
});
|
|
55328
|
+
} catch (err) {
|
|
55329
|
+
throw new Error(err instanceof Error && err.name === "AbortError" ? `Remote code fetch timed out after ${policy.fetchTimeoutMs}ms` : `Failed to fetch remote code: ${err instanceof Error ? err.message : String(err)}`);
|
|
55330
|
+
} finally {
|
|
55331
|
+
clearTimeout(timeout);
|
|
55332
|
+
}
|
|
55333
|
+
if (!response.ok) {
|
|
55334
|
+
throw new Error(`Failed to fetch remote code: HTTP ${response.status}`);
|
|
55335
|
+
}
|
|
55336
|
+
const contentLengthHeader = response.headers.get("content-length");
|
|
55337
|
+
if (contentLengthHeader) {
|
|
55338
|
+
const parsedLength = Number.parseInt(contentLengthHeader, 10);
|
|
55339
|
+
if (!Number.isNaN(parsedLength) && parsedLength > policy.maxCodeSize) {
|
|
55340
|
+
throw new Error(`Remote code exceeds maxCodeSize (${policy.maxCodeSize} bytes): ${parsedLength} bytes`);
|
|
55341
|
+
}
|
|
55342
|
+
}
|
|
55343
|
+
if (!response.body) {
|
|
55344
|
+
throw new Error("Remote code response body is empty");
|
|
55345
|
+
}
|
|
55346
|
+
const reader = response.body.getReader();
|
|
55347
|
+
const chunks = [];
|
|
55348
|
+
let totalBytes = 0;
|
|
55349
|
+
while (true) {
|
|
55350
|
+
const { done, value } = await reader.read();
|
|
55351
|
+
if (done) {
|
|
55352
|
+
break;
|
|
55353
|
+
}
|
|
55354
|
+
if (!value) {
|
|
55355
|
+
continue;
|
|
55356
|
+
}
|
|
55357
|
+
totalBytes += value.byteLength;
|
|
55358
|
+
if (totalBytes > policy.maxCodeSize) {
|
|
55359
|
+
throw new Error(`Remote code exceeds maxCodeSize (${policy.maxCodeSize} bytes)`);
|
|
55360
|
+
}
|
|
55361
|
+
chunks.push(value);
|
|
55362
|
+
}
|
|
55363
|
+
const buffer = new Uint8Array(totalBytes);
|
|
55364
|
+
let offset = 0;
|
|
55365
|
+
for (const chunk of chunks) {
|
|
55366
|
+
buffer.set(chunk, offset);
|
|
55367
|
+
offset += chunk.byteLength;
|
|
55368
|
+
}
|
|
55369
|
+
const code = decodeUtf8(buffer);
|
|
55370
|
+
const hash = sha256Hex(code);
|
|
55371
|
+
if (request.codeHash && hash.toLowerCase() !== request.codeHash.toLowerCase()) {
|
|
55372
|
+
throw new Error("Remote code hash mismatch");
|
|
55373
|
+
}
|
|
55374
|
+
return { code, url: url.toString(), hash };
|
|
55375
|
+
}
|
|
55376
|
+
var IPV4_SEPARATOR = ".", IPV6_LOOPBACK = "::1";
|
|
55377
|
+
var init_code_fetcher = () => {};
|
|
55378
|
+
|
|
55177
55379
|
// src/engine/concurrency.ts
|
|
55178
55380
|
class Semaphore {
|
|
55179
55381
|
max;
|
|
@@ -55210,6 +55412,333 @@ class Semaphore {
|
|
|
55210
55412
|
}
|
|
55211
55413
|
}
|
|
55212
55414
|
|
|
55415
|
+
// src/engine/utils.ts
|
|
55416
|
+
var exports_utils = {};
|
|
55417
|
+
__export(exports_utils, {
|
|
55418
|
+
validatePackageName: () => validatePackageName,
|
|
55419
|
+
truncateOutput: () => truncateOutput,
|
|
55420
|
+
parseMemoryLimit: () => parseMemoryLimit,
|
|
55421
|
+
maskSecrets: () => maskSecrets,
|
|
55422
|
+
extractFromTar: () => extractFromTar,
|
|
55423
|
+
createTarBuffer: () => createTarBuffer
|
|
55424
|
+
});
|
|
55425
|
+
function parseMemoryLimit(limit) {
|
|
55426
|
+
const match = limit.match(/^(\d+(?:\.\d+)?)\s*([kmgt]?)b?$/i);
|
|
55427
|
+
if (!match) {
|
|
55428
|
+
throw new Error(`Invalid memory limit format: "${limit}". Use e.g. "512m", "1g".`);
|
|
55429
|
+
}
|
|
55430
|
+
const value = Number.parseFloat(match[1]);
|
|
55431
|
+
const unit = (match[2] || "b").toLowerCase();
|
|
55432
|
+
const multipliers = {
|
|
55433
|
+
b: 1,
|
|
55434
|
+
k: 1024,
|
|
55435
|
+
m: 1024 ** 2,
|
|
55436
|
+
g: 1024 ** 3,
|
|
55437
|
+
t: 1024 ** 4
|
|
55438
|
+
};
|
|
55439
|
+
return Math.floor(value * (multipliers[unit] ?? 1));
|
|
55440
|
+
}
|
|
55441
|
+
function truncateOutput(output, maxBytes) {
|
|
55442
|
+
const encoder = new TextEncoder;
|
|
55443
|
+
const bytes = encoder.encode(output);
|
|
55444
|
+
if (bytes.length <= maxBytes) {
|
|
55445
|
+
return { text: output, truncated: false };
|
|
55446
|
+
}
|
|
55447
|
+
const decoder = new TextDecoder("utf-8", { fatal: false });
|
|
55448
|
+
const truncated = decoder.decode(bytes.slice(0, maxBytes));
|
|
55449
|
+
return {
|
|
55450
|
+
text: `${truncated}
|
|
55451
|
+
|
|
55452
|
+
--- OUTPUT TRUNCATED (${bytes.length} bytes, limit: ${maxBytes}) ---`,
|
|
55453
|
+
truncated: true
|
|
55454
|
+
};
|
|
55455
|
+
}
|
|
55456
|
+
function maskSecrets(text, secrets) {
|
|
55457
|
+
let result = text;
|
|
55458
|
+
for (const value of Object.values(secrets)) {
|
|
55459
|
+
if (value.length > 0) {
|
|
55460
|
+
result = result.replaceAll(value, "***");
|
|
55461
|
+
}
|
|
55462
|
+
}
|
|
55463
|
+
return result;
|
|
55464
|
+
}
|
|
55465
|
+
function createTarBuffer(filePath, content) {
|
|
55466
|
+
const data = typeof content === "string" ? Buffer.from(content, "utf-8") : content;
|
|
55467
|
+
const headerSize = 512;
|
|
55468
|
+
const dataBlocks = Math.ceil(data.length / 512);
|
|
55469
|
+
const totalSize = headerSize + dataBlocks * 512 + 1024;
|
|
55470
|
+
const buf = Buffer.alloc(totalSize);
|
|
55471
|
+
buf.write(filePath.replace(/^\//, ""), 0, 100, "utf-8");
|
|
55472
|
+
buf.write("0000644\x00", 100, 8, "utf-8");
|
|
55473
|
+
buf.write("0000000\x00", 108, 8, "utf-8");
|
|
55474
|
+
buf.write("0000000\x00", 116, 8, "utf-8");
|
|
55475
|
+
buf.write(`${data.length.toString(8).padStart(11, "0")}\x00`, 124, 12, "utf-8");
|
|
55476
|
+
buf.write(`${Math.floor(Date.now() / 1000).toString(8).padStart(11, "0")}\x00`, 136, 12, "utf-8");
|
|
55477
|
+
buf.write("0", 156, 1, "utf-8");
|
|
55478
|
+
buf.write("ustar\x00", 257, 6, "utf-8");
|
|
55479
|
+
buf.write("00", 263, 2, "utf-8");
|
|
55480
|
+
buf.write(" ", 148, 8, "utf-8");
|
|
55481
|
+
let checksum = 0;
|
|
55482
|
+
for (let i = 0;i < headerSize; i++) {
|
|
55483
|
+
checksum += buf[i];
|
|
55484
|
+
}
|
|
55485
|
+
buf.write(`${checksum.toString(8).padStart(6, "0")}\x00 `, 148, 8, "utf-8");
|
|
55486
|
+
data.copy(buf, headerSize);
|
|
55487
|
+
return buf;
|
|
55488
|
+
}
|
|
55489
|
+
function extractFromTar(tarBuffer, targetPath) {
|
|
55490
|
+
const normalizedTarget = targetPath.replace(/^\//, "");
|
|
55491
|
+
const basename = targetPath.split("/").pop() ?? targetPath;
|
|
55492
|
+
let offset = 0;
|
|
55493
|
+
while (offset < tarBuffer.length - 512) {
|
|
55494
|
+
const nameEnd = tarBuffer.indexOf(0, offset);
|
|
55495
|
+
const name = tarBuffer.subarray(offset, Math.min(nameEnd, offset + 100)).toString("utf-8");
|
|
55496
|
+
if (name.length === 0) {
|
|
55497
|
+
break;
|
|
55498
|
+
}
|
|
55499
|
+
const sizeStr = tarBuffer.subarray(offset + 124, offset + 136).toString("utf-8").trim();
|
|
55500
|
+
const size = Number.parseInt(sizeStr, 8);
|
|
55501
|
+
if (Number.isNaN(size)) {
|
|
55502
|
+
break;
|
|
55503
|
+
}
|
|
55504
|
+
const dataStart = offset + 512;
|
|
55505
|
+
const dataBlocks = Math.ceil(size / 512);
|
|
55506
|
+
if (name === normalizedTarget || name.endsWith(`/${normalizedTarget}`) || name === basename) {
|
|
55507
|
+
return Buffer.from(tarBuffer.subarray(dataStart, dataStart + size));
|
|
55508
|
+
}
|
|
55509
|
+
offset = dataStart + dataBlocks * 512;
|
|
55510
|
+
}
|
|
55511
|
+
throw new Error(`File "${targetPath}" not found in tar archive`);
|
|
55512
|
+
}
|
|
55513
|
+
function validatePackageName(name) {
|
|
55514
|
+
if (!/^[@a-zA-Z0-9_./\-=]+$/.test(name)) {
|
|
55515
|
+
throw new Error(`Invalid package name: "${name}". Only alphanumeric, -, _, ., /, @, and = are allowed.`);
|
|
55516
|
+
}
|
|
55517
|
+
return name;
|
|
55518
|
+
}
|
|
55519
|
+
|
|
55520
|
+
// src/engine/image-builder.ts
|
|
55521
|
+
import { createHash as createHash2 } from "node:crypto";
|
|
55522
|
+
import { existsSync as existsSync3, readFileSync as readFileSync2 } from "node:fs";
|
|
55523
|
+
import { join as join3 } from "node:path";
|
|
55524
|
+
function resolveDockerDir() {
|
|
55525
|
+
const fromBundled = new URL("./docker", import.meta.url).pathname;
|
|
55526
|
+
if (existsSync3(fromBundled)) {
|
|
55527
|
+
return fromBundled;
|
|
55528
|
+
}
|
|
55529
|
+
return new URL("../../docker", import.meta.url).pathname;
|
|
55530
|
+
}
|
|
55531
|
+
function computeDockerDirHash() {
|
|
55532
|
+
const hash = createHash2("sha256");
|
|
55533
|
+
const files = [...DOCKER_BUILD_FILES].sort();
|
|
55534
|
+
for (const file of files) {
|
|
55535
|
+
const filePath = join3(DOCKERFILE_DIR, file);
|
|
55536
|
+
if (existsSync3(filePath)) {
|
|
55537
|
+
const content = readFileSync2(filePath);
|
|
55538
|
+
hash.update(file);
|
|
55539
|
+
hash.update(content);
|
|
55540
|
+
}
|
|
55541
|
+
}
|
|
55542
|
+
return hash.digest("hex");
|
|
55543
|
+
}
|
|
55544
|
+
function computeDepsHash(runtime, packages) {
|
|
55545
|
+
const hash = createHash2("sha256");
|
|
55546
|
+
hash.update(runtime);
|
|
55547
|
+
for (const pkg of [...packages].sort()) {
|
|
55548
|
+
hash.update(pkg);
|
|
55549
|
+
}
|
|
55550
|
+
return hash.digest("hex");
|
|
55551
|
+
}
|
|
55552
|
+
function normalizePackages(packages) {
|
|
55553
|
+
return [...new Set(packages.map((pkg) => pkg.trim()).filter(Boolean))].sort();
|
|
55554
|
+
}
|
|
55555
|
+
function getCustomImageTag(runtime, packages) {
|
|
55556
|
+
const normalizedPackages = normalizePackages(packages);
|
|
55557
|
+
const depsHash = computeDepsHash(runtime, normalizedPackages);
|
|
55558
|
+
const shortHash = depsHash.slice(0, 12);
|
|
55559
|
+
return `isol8:${runtime}-custom-${shortHash}`;
|
|
55560
|
+
}
|
|
55561
|
+
async function getImageLabels(docker, imageName) {
|
|
55562
|
+
try {
|
|
55563
|
+
const image = docker.getImage(imageName);
|
|
55564
|
+
const inspect = await image.inspect();
|
|
55565
|
+
return inspect.Config?.Labels ?? {};
|
|
55566
|
+
} catch {
|
|
55567
|
+
return null;
|
|
55568
|
+
}
|
|
55569
|
+
}
|
|
55570
|
+
async function removeImage(docker, imageId) {
|
|
55571
|
+
try {
|
|
55572
|
+
const image = docker.getImage(imageId);
|
|
55573
|
+
await image.remove();
|
|
55574
|
+
logger.debug(`[ImageBuilder] Removed old image: ${imageId.slice(0, 12)}`);
|
|
55575
|
+
} catch (err) {
|
|
55576
|
+
logger.debug(`[ImageBuilder] Could not remove image ${imageId.slice(0, 12)}: ${err}`);
|
|
55577
|
+
}
|
|
55578
|
+
}
|
|
55579
|
+
async function buildBaseImages(docker, onProgress, force = false) {
|
|
55580
|
+
const runtimes = RuntimeRegistry.list();
|
|
55581
|
+
const dockerHash = computeDockerDirHash();
|
|
55582
|
+
logger.debug(`[ImageBuilder] Docker directory hash: ${dockerHash.slice(0, 16)}...`);
|
|
55583
|
+
for (const adapter of runtimes) {
|
|
55584
|
+
const target = adapter.name;
|
|
55585
|
+
const imageName = adapter.image;
|
|
55586
|
+
if (!force) {
|
|
55587
|
+
const labels = await getImageLabels(docker, imageName);
|
|
55588
|
+
if (labels && labels[LABELS.dockerHash] === dockerHash) {
|
|
55589
|
+
logger.debug(`[ImageBuilder] Base image ${target} is up to date, skipping build`);
|
|
55590
|
+
onProgress?.({ runtime: target, status: "done", message: "Up to date" });
|
|
55591
|
+
continue;
|
|
55592
|
+
}
|
|
55593
|
+
}
|
|
55594
|
+
let oldImageId = null;
|
|
55595
|
+
try {
|
|
55596
|
+
const oldImage = await docker.getImage(imageName).inspect();
|
|
55597
|
+
oldImageId = oldImage.Id;
|
|
55598
|
+
logger.debug(`[ImageBuilder] Existing image ${target} ID: ${oldImageId.slice(0, 12)}`);
|
|
55599
|
+
} catch {
|
|
55600
|
+
logger.debug(`[ImageBuilder] No existing image for ${target}`);
|
|
55601
|
+
}
|
|
55602
|
+
onProgress?.({ runtime: target, status: "building" });
|
|
55603
|
+
try {
|
|
55604
|
+
const stream = await docker.buildImage({ context: DOCKERFILE_DIR, src: DOCKER_BUILD_FILES }, {
|
|
55605
|
+
t: imageName,
|
|
55606
|
+
target,
|
|
55607
|
+
dockerfile: "Dockerfile",
|
|
55608
|
+
labels: {
|
|
55609
|
+
[LABELS.dockerHash]: dockerHash
|
|
55610
|
+
}
|
|
55611
|
+
});
|
|
55612
|
+
await new Promise((resolve2, reject) => {
|
|
55613
|
+
docker.modem.followProgress(stream, (err) => {
|
|
55614
|
+
if (err) {
|
|
55615
|
+
reject(err);
|
|
55616
|
+
} else {
|
|
55617
|
+
resolve2();
|
|
55618
|
+
}
|
|
55619
|
+
});
|
|
55620
|
+
});
|
|
55621
|
+
if (oldImageId) {
|
|
55622
|
+
await removeImage(docker, oldImageId);
|
|
55623
|
+
}
|
|
55624
|
+
onProgress?.({ runtime: target, status: "done" });
|
|
55625
|
+
} catch (err) {
|
|
55626
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
55627
|
+
onProgress?.({ runtime: target, status: "error", message });
|
|
55628
|
+
throw new Error(`Failed to build image for ${target}: ${message}`);
|
|
55629
|
+
}
|
|
55630
|
+
}
|
|
55631
|
+
}
|
|
55632
|
+
async function buildCustomImages(docker, config, onProgress, force = false) {
|
|
55633
|
+
const deps = config.dependencies;
|
|
55634
|
+
const python = deps.python ? normalizePackages(deps.python) : [];
|
|
55635
|
+
const node = deps.node ? normalizePackages(deps.node) : [];
|
|
55636
|
+
const bun = deps.bun ? normalizePackages(deps.bun) : [];
|
|
55637
|
+
const deno = deps.deno ? normalizePackages(deps.deno) : [];
|
|
55638
|
+
const bash = deps.bash ? normalizePackages(deps.bash) : [];
|
|
55639
|
+
if (python.length) {
|
|
55640
|
+
await buildCustomImage(docker, "python", python, onProgress, force);
|
|
55641
|
+
}
|
|
55642
|
+
if (node.length) {
|
|
55643
|
+
await buildCustomImage(docker, "node", node, onProgress, force);
|
|
55644
|
+
}
|
|
55645
|
+
if (bun.length) {
|
|
55646
|
+
await buildCustomImage(docker, "bun", bun, onProgress, force);
|
|
55647
|
+
}
|
|
55648
|
+
if (deno.length) {
|
|
55649
|
+
await buildCustomImage(docker, "deno", deno, onProgress, force);
|
|
55650
|
+
}
|
|
55651
|
+
if (bash.length) {
|
|
55652
|
+
await buildCustomImage(docker, "bash", bash, onProgress, force);
|
|
55653
|
+
}
|
|
55654
|
+
}
|
|
55655
|
+
async function buildCustomImage(docker, runtime, packages, onProgress, force = false) {
|
|
55656
|
+
const normalizedPackages = normalizePackages(packages);
|
|
55657
|
+
const tag = getCustomImageTag(runtime, normalizedPackages);
|
|
55658
|
+
const depsHash = computeDepsHash(runtime, normalizedPackages);
|
|
55659
|
+
logger.debug(`[ImageBuilder] ${runtime} custom deps hash: ${depsHash.slice(0, 16)}...`);
|
|
55660
|
+
if (!force) {
|
|
55661
|
+
const labels = await getImageLabels(docker, tag);
|
|
55662
|
+
if (labels && labels[LABELS.depsHash] === depsHash) {
|
|
55663
|
+
logger.debug(`[ImageBuilder] Custom image ${runtime} is up to date, skipping build`);
|
|
55664
|
+
onProgress?.({ runtime, status: "done", message: "Up to date" });
|
|
55665
|
+
return;
|
|
55666
|
+
}
|
|
55667
|
+
}
|
|
55668
|
+
let oldImageId = null;
|
|
55669
|
+
try {
|
|
55670
|
+
const oldImage = await docker.getImage(tag).inspect();
|
|
55671
|
+
oldImageId = oldImage.Id;
|
|
55672
|
+
logger.debug(`[ImageBuilder] Existing custom image ${runtime} ID: ${oldImageId.slice(0, 12)}`);
|
|
55673
|
+
} catch {
|
|
55674
|
+
logger.debug(`[ImageBuilder] No existing custom image for ${runtime}`);
|
|
55675
|
+
}
|
|
55676
|
+
onProgress?.({
|
|
55677
|
+
runtime,
|
|
55678
|
+
status: "building",
|
|
55679
|
+
message: `Custom: ${normalizedPackages.join(", ")}`
|
|
55680
|
+
});
|
|
55681
|
+
let installCmd;
|
|
55682
|
+
switch (runtime) {
|
|
55683
|
+
case "python":
|
|
55684
|
+
installCmd = `RUN pip install --no-cache-dir ${normalizedPackages.join(" ")}`;
|
|
55685
|
+
break;
|
|
55686
|
+
case "node":
|
|
55687
|
+
installCmd = `RUN npm install -g ${normalizedPackages.join(" ")}`;
|
|
55688
|
+
break;
|
|
55689
|
+
case "bun":
|
|
55690
|
+
installCmd = `RUN bun install -g ${normalizedPackages.join(" ")}`;
|
|
55691
|
+
break;
|
|
55692
|
+
case "deno":
|
|
55693
|
+
installCmd = normalizedPackages.map((p) => `RUN deno cache ${p}`).join(`
|
|
55694
|
+
`);
|
|
55695
|
+
break;
|
|
55696
|
+
case "bash":
|
|
55697
|
+
installCmd = `RUN apk add --no-cache ${normalizedPackages.join(" ")}`;
|
|
55698
|
+
break;
|
|
55699
|
+
default:
|
|
55700
|
+
throw new Error(`Unknown runtime: ${runtime}`);
|
|
55701
|
+
}
|
|
55702
|
+
const dockerfileContent = `FROM isol8:${runtime}
|
|
55703
|
+
${installCmd}
|
|
55704
|
+
`;
|
|
55705
|
+
const { createTarBuffer: createTarBuffer2, validatePackageName: validatePackageName2 } = await Promise.resolve().then(() => exports_utils);
|
|
55706
|
+
const { Readable } = await import("node:stream");
|
|
55707
|
+
normalizedPackages.forEach(validatePackageName2);
|
|
55708
|
+
const tarBuffer = createTarBuffer2("Dockerfile", dockerfileContent);
|
|
55709
|
+
const stream = await docker.buildImage(Readable.from(tarBuffer), {
|
|
55710
|
+
t: tag,
|
|
55711
|
+
dockerfile: "Dockerfile",
|
|
55712
|
+
labels: {
|
|
55713
|
+
[LABELS.depsHash]: depsHash
|
|
55714
|
+
}
|
|
55715
|
+
});
|
|
55716
|
+
await new Promise((resolve2, reject) => {
|
|
55717
|
+
docker.modem.followProgress(stream, (err) => {
|
|
55718
|
+
if (err) {
|
|
55719
|
+
reject(err);
|
|
55720
|
+
} else {
|
|
55721
|
+
resolve2();
|
|
55722
|
+
}
|
|
55723
|
+
});
|
|
55724
|
+
});
|
|
55725
|
+
if (oldImageId) {
|
|
55726
|
+
await removeImage(docker, oldImageId);
|
|
55727
|
+
}
|
|
55728
|
+
onProgress?.({ runtime, status: "done" });
|
|
55729
|
+
}
|
|
55730
|
+
var DOCKERFILE_DIR, LABELS, DOCKER_BUILD_FILES;
|
|
55731
|
+
var init_image_builder = __esm(() => {
|
|
55732
|
+
init_runtime();
|
|
55733
|
+
init_logger();
|
|
55734
|
+
DOCKERFILE_DIR = resolveDockerDir();
|
|
55735
|
+
LABELS = {
|
|
55736
|
+
dockerHash: "org.isol8.build.hash",
|
|
55737
|
+
depsHash: "org.isol8.deps.hash"
|
|
55738
|
+
};
|
|
55739
|
+
DOCKER_BUILD_FILES = ["Dockerfile", "proxy.sh", "proxy-handler.sh"];
|
|
55740
|
+
});
|
|
55741
|
+
|
|
55213
55742
|
// src/engine/pool.ts
|
|
55214
55743
|
class ContainerPool {
|
|
55215
55744
|
docker;
|
|
@@ -55400,46 +55929,44 @@ class ContainerPool {
|
|
|
55400
55929
|
}
|
|
55401
55930
|
replenish(image) {
|
|
55402
55931
|
if (this.replenishing.has(image)) {
|
|
55403
|
-
|
|
55404
|
-
|
|
55405
|
-
|
|
55406
|
-
|
|
55407
|
-
|
|
55408
|
-
|
|
55409
|
-
|
|
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(() => {});
|
|
55410
55945
|
return;
|
|
55411
55946
|
}
|
|
55412
|
-
this.
|
|
55413
|
-
|
|
55414
|
-
|
|
55415
|
-
|
|
55947
|
+
if (this.poolStrategy === "fast") {
|
|
55948
|
+
if (p.clean.length < this.cleanPoolSize) {
|
|
55949
|
+
p.clean.push({ container, createdAt: Date.now() });
|
|
55950
|
+
} else {
|
|
55416
55951
|
container.remove({ force: true }).catch(() => {});
|
|
55417
|
-
return;
|
|
55418
55952
|
}
|
|
55419
|
-
|
|
55420
|
-
|
|
55421
|
-
|
|
55422
|
-
|
|
55423
|
-
|
|
55424
|
-
}
|
|
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() });
|
|
55425
55959
|
} else {
|
|
55426
|
-
|
|
55427
|
-
p.clean = [];
|
|
55428
|
-
}
|
|
55429
|
-
if (p.clean.length < this.cleanPoolSize) {
|
|
55430
|
-
p.clean.push({ container, createdAt: Date.now() });
|
|
55431
|
-
} else {
|
|
55432
|
-
container.remove({ force: true }).catch(() => {});
|
|
55433
|
-
}
|
|
55960
|
+
container.remove({ force: true }).catch(() => {});
|
|
55434
55961
|
}
|
|
55435
|
-
}
|
|
55436
|
-
|
|
55437
|
-
|
|
55438
|
-
|
|
55439
|
-
|
|
55440
|
-
|
|
55441
|
-
|
|
55442
|
-
|
|
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);
|
|
55443
55970
|
}
|
|
55444
55971
|
}
|
|
55445
55972
|
var init_pool = __esm(() => {
|
|
@@ -55491,118 +56018,13 @@ function calculateResourceDelta(before, after) {
|
|
|
55491
56018
|
};
|
|
55492
56019
|
}
|
|
55493
56020
|
|
|
55494
|
-
// src/engine/utils.ts
|
|
55495
|
-
var exports_utils = {};
|
|
55496
|
-
__export(exports_utils, {
|
|
55497
|
-
validatePackageName: () => validatePackageName,
|
|
55498
|
-
truncateOutput: () => truncateOutput,
|
|
55499
|
-
parseMemoryLimit: () => parseMemoryLimit,
|
|
55500
|
-
maskSecrets: () => maskSecrets,
|
|
55501
|
-
extractFromTar: () => extractFromTar,
|
|
55502
|
-
createTarBuffer: () => createTarBuffer
|
|
55503
|
-
});
|
|
55504
|
-
function parseMemoryLimit(limit) {
|
|
55505
|
-
const match = limit.match(/^(\d+(?:\.\d+)?)\s*([kmgt]?)b?$/i);
|
|
55506
|
-
if (!match) {
|
|
55507
|
-
throw new Error(`Invalid memory limit format: "${limit}". Use e.g. "512m", "1g".`);
|
|
55508
|
-
}
|
|
55509
|
-
const value = Number.parseFloat(match[1]);
|
|
55510
|
-
const unit = (match[2] || "b").toLowerCase();
|
|
55511
|
-
const multipliers = {
|
|
55512
|
-
b: 1,
|
|
55513
|
-
k: 1024,
|
|
55514
|
-
m: 1024 ** 2,
|
|
55515
|
-
g: 1024 ** 3,
|
|
55516
|
-
t: 1024 ** 4
|
|
55517
|
-
};
|
|
55518
|
-
return Math.floor(value * (multipliers[unit] ?? 1));
|
|
55519
|
-
}
|
|
55520
|
-
function truncateOutput(output, maxBytes) {
|
|
55521
|
-
const encoder = new TextEncoder;
|
|
55522
|
-
const bytes = encoder.encode(output);
|
|
55523
|
-
if (bytes.length <= maxBytes) {
|
|
55524
|
-
return { text: output, truncated: false };
|
|
55525
|
-
}
|
|
55526
|
-
const decoder = new TextDecoder("utf-8", { fatal: false });
|
|
55527
|
-
const truncated = decoder.decode(bytes.slice(0, maxBytes));
|
|
55528
|
-
return {
|
|
55529
|
-
text: `${truncated}
|
|
55530
|
-
|
|
55531
|
-
--- OUTPUT TRUNCATED (${bytes.length} bytes, limit: ${maxBytes}) ---`,
|
|
55532
|
-
truncated: true
|
|
55533
|
-
};
|
|
55534
|
-
}
|
|
55535
|
-
function maskSecrets(text, secrets) {
|
|
55536
|
-
let result = text;
|
|
55537
|
-
for (const value of Object.values(secrets)) {
|
|
55538
|
-
if (value.length > 0) {
|
|
55539
|
-
result = result.replaceAll(value, "***");
|
|
55540
|
-
}
|
|
55541
|
-
}
|
|
55542
|
-
return result;
|
|
55543
|
-
}
|
|
55544
|
-
function createTarBuffer(filePath, content) {
|
|
55545
|
-
const data = typeof content === "string" ? Buffer.from(content, "utf-8") : content;
|
|
55546
|
-
const headerSize = 512;
|
|
55547
|
-
const dataBlocks = Math.ceil(data.length / 512);
|
|
55548
|
-
const totalSize = headerSize + dataBlocks * 512 + 1024;
|
|
55549
|
-
const buf = Buffer.alloc(totalSize);
|
|
55550
|
-
buf.write(filePath.replace(/^\//, ""), 0, 100, "utf-8");
|
|
55551
|
-
buf.write("0000644\x00", 100, 8, "utf-8");
|
|
55552
|
-
buf.write("0000000\x00", 108, 8, "utf-8");
|
|
55553
|
-
buf.write("0000000\x00", 116, 8, "utf-8");
|
|
55554
|
-
buf.write(`${data.length.toString(8).padStart(11, "0")}\x00`, 124, 12, "utf-8");
|
|
55555
|
-
buf.write(`${Math.floor(Date.now() / 1000).toString(8).padStart(11, "0")}\x00`, 136, 12, "utf-8");
|
|
55556
|
-
buf.write("0", 156, 1, "utf-8");
|
|
55557
|
-
buf.write("ustar\x00", 257, 6, "utf-8");
|
|
55558
|
-
buf.write("00", 263, 2, "utf-8");
|
|
55559
|
-
buf.write(" ", 148, 8, "utf-8");
|
|
55560
|
-
let checksum = 0;
|
|
55561
|
-
for (let i = 0;i < headerSize; i++) {
|
|
55562
|
-
checksum += buf[i];
|
|
55563
|
-
}
|
|
55564
|
-
buf.write(`${checksum.toString(8).padStart(6, "0")}\x00 `, 148, 8, "utf-8");
|
|
55565
|
-
data.copy(buf, headerSize);
|
|
55566
|
-
return buf;
|
|
55567
|
-
}
|
|
55568
|
-
function extractFromTar(tarBuffer, targetPath) {
|
|
55569
|
-
const normalizedTarget = targetPath.replace(/^\//, "");
|
|
55570
|
-
const basename = targetPath.split("/").pop() ?? targetPath;
|
|
55571
|
-
let offset = 0;
|
|
55572
|
-
while (offset < tarBuffer.length - 512) {
|
|
55573
|
-
const nameEnd = tarBuffer.indexOf(0, offset);
|
|
55574
|
-
const name = tarBuffer.subarray(offset, Math.min(nameEnd, offset + 100)).toString("utf-8");
|
|
55575
|
-
if (name.length === 0) {
|
|
55576
|
-
break;
|
|
55577
|
-
}
|
|
55578
|
-
const sizeStr = tarBuffer.subarray(offset + 124, offset + 136).toString("utf-8").trim();
|
|
55579
|
-
const size = Number.parseInt(sizeStr, 8);
|
|
55580
|
-
if (Number.isNaN(size)) {
|
|
55581
|
-
break;
|
|
55582
|
-
}
|
|
55583
|
-
const dataStart = offset + 512;
|
|
55584
|
-
const dataBlocks = Math.ceil(size / 512);
|
|
55585
|
-
if (name === normalizedTarget || name.endsWith(`/${normalizedTarget}`) || name === basename) {
|
|
55586
|
-
return Buffer.from(tarBuffer.subarray(dataStart, dataStart + size));
|
|
55587
|
-
}
|
|
55588
|
-
offset = dataStart + dataBlocks * 512;
|
|
55589
|
-
}
|
|
55590
|
-
throw new Error(`File "${targetPath}" not found in tar archive`);
|
|
55591
|
-
}
|
|
55592
|
-
function validatePackageName(name) {
|
|
55593
|
-
if (!/^[@a-zA-Z0-9_./\-=]+$/.test(name)) {
|
|
55594
|
-
throw new Error(`Invalid package name: "${name}". Only alphanumeric, -, _, ., /, @, and = are allowed.`);
|
|
55595
|
-
}
|
|
55596
|
-
return name;
|
|
55597
|
-
}
|
|
55598
|
-
|
|
55599
56021
|
// src/engine/docker.ts
|
|
55600
56022
|
var exports_docker = {};
|
|
55601
56023
|
__export(exports_docker, {
|
|
55602
56024
|
DockerIsol8: () => DockerIsol8
|
|
55603
56025
|
});
|
|
55604
56026
|
import { randomUUID } from "node:crypto";
|
|
55605
|
-
import { existsSync as
|
|
56027
|
+
import { existsSync as existsSync4, readFileSync as readFileSync3 } from "node:fs";
|
|
55606
56028
|
import { PassThrough } from "node:stream";
|
|
55607
56029
|
async function writeFileViaExec(container, filePath, content) {
|
|
55608
56030
|
const data = typeof content === "string" ? Buffer.from(content, "utf-8") : content;
|
|
@@ -55818,11 +56240,32 @@ class DockerIsol8 {
|
|
|
55818
56240
|
logNetwork;
|
|
55819
56241
|
poolStrategy;
|
|
55820
56242
|
poolSize;
|
|
56243
|
+
dependencies;
|
|
55821
56244
|
auditLogger;
|
|
56245
|
+
remoteCodePolicy;
|
|
55822
56246
|
container = null;
|
|
55823
56247
|
persistentRuntime = null;
|
|
55824
56248
|
pool = null;
|
|
55825
56249
|
imageCache = new Map;
|
|
56250
|
+
async resolveExecutionRequest(req) {
|
|
56251
|
+
const inlineCode = req.code?.trim();
|
|
56252
|
+
const codeUrl = req.codeUrl?.trim();
|
|
56253
|
+
if (inlineCode && codeUrl) {
|
|
56254
|
+
throw new Error("ExecutionRequest.code and ExecutionRequest.codeUrl are mutually exclusive.");
|
|
56255
|
+
}
|
|
56256
|
+
if (!(inlineCode || codeUrl)) {
|
|
56257
|
+
throw new Error("ExecutionRequest must include either code or codeUrl.");
|
|
56258
|
+
}
|
|
56259
|
+
if (inlineCode) {
|
|
56260
|
+
return { ...req, code: req.code };
|
|
56261
|
+
}
|
|
56262
|
+
const fetched = await fetchRemoteCode({
|
|
56263
|
+
codeUrl,
|
|
56264
|
+
codeHash: req.codeHash,
|
|
56265
|
+
allowInsecureCodeUrl: req.allowInsecureCodeUrl
|
|
56266
|
+
}, this.remoteCodePolicy);
|
|
56267
|
+
return { ...req, code: fetched.code };
|
|
56268
|
+
}
|
|
55826
56269
|
constructor(options = {}, maxConcurrent = 10) {
|
|
55827
56270
|
this.docker = options.docker ?? new import_dockerode.default;
|
|
55828
56271
|
this.mode = options.mode ?? "ephemeral";
|
|
@@ -55844,6 +56287,18 @@ class DockerIsol8 {
|
|
|
55844
56287
|
this.logNetwork = options.logNetwork ?? false;
|
|
55845
56288
|
this.poolStrategy = options.poolStrategy ?? "fast";
|
|
55846
56289
|
this.poolSize = options.poolSize ?? { clean: 1, dirty: 1 };
|
|
56290
|
+
this.dependencies = options.dependencies ?? {};
|
|
56291
|
+
this.remoteCodePolicy = options.remoteCode ?? {
|
|
56292
|
+
enabled: false,
|
|
56293
|
+
allowedSchemes: ["https"],
|
|
56294
|
+
allowedHosts: [],
|
|
56295
|
+
blockedHosts: [],
|
|
56296
|
+
maxCodeSize: 10 * 1024 * 1024,
|
|
56297
|
+
fetchTimeoutMs: 30000,
|
|
56298
|
+
requireHash: false,
|
|
56299
|
+
enableCache: true,
|
|
56300
|
+
cacheTtl: 3600
|
|
56301
|
+
};
|
|
55847
56302
|
if (options.audit) {
|
|
55848
56303
|
this.auditLogger = new AuditLogger(options.audit);
|
|
55849
56304
|
}
|
|
@@ -55851,7 +56306,33 @@ class DockerIsol8 {
|
|
|
55851
56306
|
logger.setDebug(true);
|
|
55852
56307
|
}
|
|
55853
56308
|
}
|
|
55854
|
-
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
|
+
}
|
|
55855
56336
|
async stop() {
|
|
55856
56337
|
if (this.container) {
|
|
55857
56338
|
try {
|
|
@@ -55872,7 +56353,8 @@ class DockerIsol8 {
|
|
|
55872
56353
|
await this.semaphore.acquire();
|
|
55873
56354
|
const startTime = Date.now();
|
|
55874
56355
|
try {
|
|
55875
|
-
const
|
|
56356
|
+
const request = await this.resolveExecutionRequest(req);
|
|
56357
|
+
const result = this.mode === "persistent" ? await this.executePersistent(request, startTime) : await this.executeEphemeral(request, startTime);
|
|
55876
56358
|
return result;
|
|
55877
56359
|
} finally {
|
|
55878
56360
|
this.semaphore.release();
|
|
@@ -56026,8 +56508,9 @@ class DockerIsol8 {
|
|
|
56026
56508
|
async* executeStream(req) {
|
|
56027
56509
|
await this.semaphore.acquire();
|
|
56028
56510
|
try {
|
|
56029
|
-
const
|
|
56030
|
-
const
|
|
56511
|
+
const request = await this.resolveExecutionRequest(req);
|
|
56512
|
+
const adapter = this.getAdapter(request.runtime);
|
|
56513
|
+
const timeoutMs = request.timeoutMs ?? this.defaultTimeoutMs;
|
|
56031
56514
|
const image = await this.resolveImage(adapter);
|
|
56032
56515
|
const container = await this.docker.createContainer({
|
|
56033
56516
|
Image: image,
|
|
@@ -56044,23 +56527,23 @@ class DockerIsol8 {
|
|
|
56044
56527
|
await startProxy(container, this.networkFilter);
|
|
56045
56528
|
await setupIptables(container);
|
|
56046
56529
|
}
|
|
56047
|
-
const ext =
|
|
56530
|
+
const ext = request.fileExtension ?? adapter.getFileExtension();
|
|
56048
56531
|
const filePath = `${SANDBOX_WORKDIR}/main${ext}`;
|
|
56049
|
-
await writeFileViaExec(container, filePath,
|
|
56050
|
-
if (
|
|
56051
|
-
await installPackages(container,
|
|
56532
|
+
await writeFileViaExec(container, filePath, request.code);
|
|
56533
|
+
if (request.installPackages?.length) {
|
|
56534
|
+
await installPackages(container, request.runtime, request.installPackages);
|
|
56052
56535
|
}
|
|
56053
|
-
if (
|
|
56054
|
-
for (const [fPath, fContent] of Object.entries(
|
|
56536
|
+
if (request.files) {
|
|
56537
|
+
for (const [fPath, fContent] of Object.entries(request.files)) {
|
|
56055
56538
|
await writeFileViaExec(container, fPath, fContent);
|
|
56056
56539
|
}
|
|
56057
56540
|
}
|
|
56058
|
-
const rawCmd = adapter.getCommand(
|
|
56541
|
+
const rawCmd = adapter.getCommand(request.code, filePath);
|
|
56059
56542
|
const timeoutSec = Math.ceil(timeoutMs / 1000);
|
|
56060
56543
|
let cmd;
|
|
56061
|
-
if (
|
|
56544
|
+
if (request.stdin) {
|
|
56062
56545
|
const stdinPath = `${SANDBOX_WORKDIR}/_stdin`;
|
|
56063
|
-
await writeFileViaExec(container, stdinPath,
|
|
56546
|
+
await writeFileViaExec(container, stdinPath, request.stdin);
|
|
56064
56547
|
const cmdStr = rawCmd.map((a) => `'${a.replace(/'/g, "'\\''")}'`).join(" ");
|
|
56065
56548
|
cmd = wrapWithTimeout(["sh", "-c", `cat ${stdinPath} | ${cmdStr}`], timeoutSec);
|
|
56066
56549
|
} else {
|
|
@@ -56068,7 +56551,7 @@ class DockerIsol8 {
|
|
|
56068
56551
|
}
|
|
56069
56552
|
const exec = await container.exec({
|
|
56070
56553
|
Cmd: cmd,
|
|
56071
|
-
Env: this.buildEnv(
|
|
56554
|
+
Env: this.buildEnv(request.env),
|
|
56072
56555
|
AttachStdout: true,
|
|
56073
56556
|
AttachStderr: true,
|
|
56074
56557
|
WorkingDir: SANDBOX_WORKDIR,
|
|
@@ -56098,21 +56581,29 @@ class DockerIsol8 {
|
|
|
56098
56581
|
if (cached) {
|
|
56099
56582
|
return cached;
|
|
56100
56583
|
}
|
|
56101
|
-
|
|
56102
|
-
|
|
56103
|
-
|
|
56104
|
-
|
|
56105
|
-
|
|
56106
|
-
|
|
56107
|
-
|
|
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 {}
|
|
56108
56602
|
}
|
|
56109
56603
|
this.imageCache.set(cacheKey, resolvedImage);
|
|
56110
56604
|
return resolvedImage;
|
|
56111
56605
|
}
|
|
56112
|
-
|
|
56113
|
-
const adapter = this.getAdapter(req.runtime);
|
|
56114
|
-
const timeoutMs = req.timeoutMs ?? this.defaultTimeoutMs;
|
|
56115
|
-
const image = await this.resolveImage(adapter);
|
|
56606
|
+
ensurePool() {
|
|
56116
56607
|
if (!this.pool) {
|
|
56117
56608
|
this.pool = new ContainerPool({
|
|
56118
56609
|
docker: this.docker,
|
|
@@ -56130,7 +56621,14 @@ class DockerIsol8 {
|
|
|
56130
56621
|
}
|
|
56131
56622
|
});
|
|
56132
56623
|
}
|
|
56133
|
-
|
|
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);
|
|
56134
56632
|
let startStats;
|
|
56135
56633
|
if (this.auditLogger) {
|
|
56136
56634
|
try {
|
|
@@ -56144,13 +56642,26 @@ class DockerIsol8 {
|
|
|
56144
56642
|
await startProxy(container, this.networkFilter);
|
|
56145
56643
|
await setupIptables(container);
|
|
56146
56644
|
}
|
|
56147
|
-
const
|
|
56148
|
-
|
|
56149
|
-
|
|
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
|
+
}
|
|
56150
56662
|
if (req.installPackages?.length) {
|
|
56151
56663
|
await installPackages(container, req.runtime, req.installPackages);
|
|
56152
56664
|
}
|
|
56153
|
-
const rawCmd = adapter.getCommand(req.code, filePath);
|
|
56154
56665
|
const timeoutSec = Math.ceil(timeoutMs / 1000);
|
|
56155
56666
|
let cmd;
|
|
56156
56667
|
if (req.stdin) {
|
|
@@ -56221,7 +56732,7 @@ class DockerIsol8 {
|
|
|
56221
56732
|
if (this.persist) {
|
|
56222
56733
|
logger.debug(`[Persist] Leaving container running for inspection: ${container.id}`);
|
|
56223
56734
|
} else {
|
|
56224
|
-
|
|
56735
|
+
pool.release(container, image).catch((err) => {
|
|
56225
56736
|
logger.debug(`[Pool] release failed: ${err}`);
|
|
56226
56737
|
container.remove({ force: true }).catch(() => {});
|
|
56227
56738
|
});
|
|
@@ -56397,7 +56908,7 @@ class DockerIsol8 {
|
|
|
56397
56908
|
}
|
|
56398
56909
|
if (this.security.seccomp === "custom" && this.security.customProfilePath) {
|
|
56399
56910
|
try {
|
|
56400
|
-
const profile =
|
|
56911
|
+
const profile = readFileSync3(this.security.customProfilePath, "utf-8");
|
|
56401
56912
|
opts.push(`seccomp=${profile}`);
|
|
56402
56913
|
} catch (e) {
|
|
56403
56914
|
logger.error(`Failed to load custom seccomp profile: ${e}`);
|
|
@@ -56416,12 +56927,12 @@ class DockerIsol8 {
|
|
|
56416
56927
|
}
|
|
56417
56928
|
loadDefaultSeccompProfile() {
|
|
56418
56929
|
const devPath = new URL("../../docker/seccomp-profile.json", import.meta.url);
|
|
56419
|
-
if (
|
|
56420
|
-
return
|
|
56930
|
+
if (existsSync4(devPath)) {
|
|
56931
|
+
return readFileSync3(devPath, "utf-8");
|
|
56421
56932
|
}
|
|
56422
56933
|
const prodPath = new URL("./docker/seccomp-profile.json", import.meta.url);
|
|
56423
|
-
if (
|
|
56424
|
-
return
|
|
56934
|
+
if (existsSync4(prodPath)) {
|
|
56935
|
+
return readFileSync3(prodPath, "utf-8");
|
|
56425
56936
|
}
|
|
56426
56937
|
logger.warn("Could not locate default seccomp profile. Running without seccomp filter.");
|
|
56427
56938
|
return null;
|
|
@@ -56604,7 +57115,7 @@ class DockerIsol8 {
|
|
|
56604
57115
|
static async cleanup(docker) {
|
|
56605
57116
|
const dockerInstance = docker ?? new import_dockerode.default;
|
|
56606
57117
|
const containers = await dockerInstance.listContainers({ all: true });
|
|
56607
|
-
const isol8Containers = containers.filter((c) => c.Image.startsWith("isol8:")
|
|
57118
|
+
const isol8Containers = containers.filter((c) => c.Image.startsWith("isol8:"));
|
|
56608
57119
|
let removed = 0;
|
|
56609
57120
|
let failed = 0;
|
|
56610
57121
|
const errors = [];
|
|
@@ -56621,12 +57132,35 @@ class DockerIsol8 {
|
|
|
56621
57132
|
}
|
|
56622
57133
|
return { removed, failed, errors };
|
|
56623
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
|
+
}
|
|
56624
57156
|
}
|
|
56625
57157
|
var import_dockerode, SANDBOX_WORKDIR = "/sandbox", MAX_OUTPUT_BYTES, PROXY_PORT = 8118, PROXY_STARTUP_TIMEOUT_MS = 5000, PROXY_POLL_INTERVAL_MS = 100;
|
|
56626
57158
|
var init_docker = __esm(() => {
|
|
56627
57159
|
init_runtime();
|
|
56628
57160
|
init_logger();
|
|
56629
57161
|
init_audit();
|
|
57162
|
+
init_code_fetcher();
|
|
57163
|
+
init_image_builder();
|
|
56630
57164
|
init_pool();
|
|
56631
57165
|
import_dockerode = __toESM(require_docker(), 1);
|
|
56632
57166
|
MAX_OUTPUT_BYTES = 1024 * 1024;
|
|
@@ -56637,7 +57171,7 @@ var package_default;
|
|
|
56637
57171
|
var init_package = __esm(() => {
|
|
56638
57172
|
package_default = {
|
|
56639
57173
|
name: "isol8",
|
|
56640
|
-
version: "0.
|
|
57174
|
+
version: "0.11.0",
|
|
56641
57175
|
description: "Secure code execution engine for AI agents",
|
|
56642
57176
|
author: "Illusion47586",
|
|
56643
57177
|
license: "MIT",
|
|
@@ -56682,6 +57216,8 @@ var init_package = __esm(() => {
|
|
|
56682
57216
|
"lint:check": "ultracite check",
|
|
56683
57217
|
"lint:fix": "ultracite fix",
|
|
56684
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",
|
|
56685
57221
|
"bench:pool": "bunx tsx benchmarks/spawn-pool.ts",
|
|
56686
57222
|
"bench:detailed": "bunx tsx benchmarks/spawn-detailed.ts",
|
|
56687
57223
|
"bench:cli": "bun run tests/production/bench-cli.ts",
|
|
@@ -58388,7 +58924,12 @@ async function createServer(options) {
|
|
|
58388
58924
|
app.post("/execute", async (c) => {
|
|
58389
58925
|
const body = await c.req.json();
|
|
58390
58926
|
logger.debug(`[Server] POST /execute runtime=${body.request.runtime} sessionId=${body.sessionId ?? "ephemeral"}`);
|
|
58391
|
-
logger.debug(`[Server] Code
|
|
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 ?? {};
|
|
58392
58933
|
const engineOptions = {
|
|
58393
58934
|
network: config.defaults.network,
|
|
58394
58935
|
memoryLimit: config.defaults.memoryLimit,
|
|
@@ -58396,7 +58937,11 @@ async function createServer(options) {
|
|
|
58396
58937
|
timeoutMs: config.defaults.timeoutMs,
|
|
58397
58938
|
sandboxSize: config.defaults.sandboxSize,
|
|
58398
58939
|
tmpSize: config.defaults.tmpSize,
|
|
58399
|
-
|
|
58940
|
+
poolStrategy: config.poolStrategy,
|
|
58941
|
+
poolSize: config.poolSize,
|
|
58942
|
+
dependencies: config.dependencies,
|
|
58943
|
+
remoteCode: config.remoteCode,
|
|
58944
|
+
...requestOptions,
|
|
58400
58945
|
mode: body.sessionId ? "persistent" : "ephemeral",
|
|
58401
58946
|
audit: config.audit
|
|
58402
58947
|
};
|
|
@@ -58449,7 +58994,12 @@ async function createServer(options) {
|
|
|
58449
58994
|
app.post("/execute/stream", async (c) => {
|
|
58450
58995
|
const body = await c.req.json();
|
|
58451
58996
|
logger.debug(`[Server] POST /execute/stream runtime=${body.request.runtime}`);
|
|
58452
|
-
logger.debug(`[Server] Code
|
|
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 ?? {};
|
|
58453
59003
|
const engineOptions = {
|
|
58454
59004
|
network: config.defaults.network,
|
|
58455
59005
|
memoryLimit: config.defaults.memoryLimit,
|
|
@@ -58457,7 +59007,11 @@ async function createServer(options) {
|
|
|
58457
59007
|
timeoutMs: config.defaults.timeoutMs,
|
|
58458
59008
|
sandboxSize: config.defaults.sandboxSize,
|
|
58459
59009
|
tmpSize: config.defaults.tmpSize,
|
|
58460
|
-
|
|
59010
|
+
poolStrategy: config.poolStrategy,
|
|
59011
|
+
poolSize: config.poolSize,
|
|
59012
|
+
dependencies: config.dependencies,
|
|
59013
|
+
remoteCode: config.remoteCode,
|
|
59014
|
+
...requestOptions,
|
|
58461
59015
|
mode: "ephemeral"
|
|
58462
59016
|
};
|
|
58463
59017
|
const engine = new DockerIsol82(engineOptions, config.maxConcurrent);
|
|
@@ -58581,13 +59135,13 @@ import {
|
|
|
58581
59135
|
chmodSync,
|
|
58582
59136
|
existsSync as existsSync5,
|
|
58583
59137
|
mkdirSync as mkdirSync2,
|
|
58584
|
-
readFileSync as
|
|
59138
|
+
readFileSync as readFileSync4,
|
|
58585
59139
|
renameSync,
|
|
58586
59140
|
unlinkSync as unlinkSync2,
|
|
58587
59141
|
writeFileSync
|
|
58588
59142
|
} from "node:fs";
|
|
58589
59143
|
import { arch, homedir as homedir2, platform } from "node:os";
|
|
58590
|
-
import { join as
|
|
59144
|
+
import { join as join4, resolve as resolve2 } from "node:path";
|
|
58591
59145
|
|
|
58592
59146
|
// node_modules/commander/esm.mjs
|
|
58593
59147
|
var import__ = __toESM(require_commander(), 1);
|
|
@@ -61860,7 +62414,7 @@ class RemoteIsol8 {
|
|
|
61860
62414
|
this.sessionId = options.sessionId;
|
|
61861
62415
|
this.isol8Options = isol8Options;
|
|
61862
62416
|
}
|
|
61863
|
-
async start() {
|
|
62417
|
+
async start(_options) {
|
|
61864
62418
|
const res = await this.fetch("/health");
|
|
61865
62419
|
if (!res.ok) {
|
|
61866
62420
|
throw new Error(`Remote server health check failed: ${res.status}`);
|
|
@@ -61978,112 +62532,7 @@ class RemoteIsol8 {
|
|
|
61978
62532
|
// src/cli.ts
|
|
61979
62533
|
init_config();
|
|
61980
62534
|
init_docker();
|
|
61981
|
-
|
|
61982
|
-
// src/engine/image-builder.ts
|
|
61983
|
-
init_runtime();
|
|
61984
|
-
import { existsSync as existsSync4 } from "node:fs";
|
|
61985
|
-
function resolveDockerDir() {
|
|
61986
|
-
const fromBundled = new URL("./docker", import.meta.url).pathname;
|
|
61987
|
-
if (existsSync4(fromBundled)) {
|
|
61988
|
-
return fromBundled;
|
|
61989
|
-
}
|
|
61990
|
-
return new URL("../../docker", import.meta.url).pathname;
|
|
61991
|
-
}
|
|
61992
|
-
var DOCKERFILE_DIR = resolveDockerDir();
|
|
61993
|
-
async function buildBaseImages(docker, onProgress) {
|
|
61994
|
-
const runtimes = RuntimeRegistry.list();
|
|
61995
|
-
for (const adapter of runtimes) {
|
|
61996
|
-
const target = adapter.name;
|
|
61997
|
-
onProgress?.({ runtime: target, status: "building" });
|
|
61998
|
-
try {
|
|
61999
|
-
const stream = await docker.buildImage({ context: DOCKERFILE_DIR, src: ["Dockerfile", "proxy.sh", "proxy-handler.sh"] }, {
|
|
62000
|
-
t: adapter.image,
|
|
62001
|
-
target,
|
|
62002
|
-
dockerfile: "Dockerfile"
|
|
62003
|
-
});
|
|
62004
|
-
await new Promise((resolve2, reject) => {
|
|
62005
|
-
docker.modem.followProgress(stream, (err) => {
|
|
62006
|
-
if (err) {
|
|
62007
|
-
reject(err);
|
|
62008
|
-
} else {
|
|
62009
|
-
resolve2();
|
|
62010
|
-
}
|
|
62011
|
-
});
|
|
62012
|
-
});
|
|
62013
|
-
onProgress?.({ runtime: target, status: "done" });
|
|
62014
|
-
} catch (err) {
|
|
62015
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
62016
|
-
onProgress?.({ runtime: target, status: "error", message });
|
|
62017
|
-
throw new Error(`Failed to build image for ${target}: ${message}`);
|
|
62018
|
-
}
|
|
62019
|
-
}
|
|
62020
|
-
}
|
|
62021
|
-
async function buildCustomImages(docker, config, onProgress) {
|
|
62022
|
-
const deps = config.dependencies;
|
|
62023
|
-
if (deps.python?.length) {
|
|
62024
|
-
await buildCustomImage(docker, "python", deps.python, onProgress);
|
|
62025
|
-
}
|
|
62026
|
-
if (deps.node?.length) {
|
|
62027
|
-
await buildCustomImage(docker, "node", deps.node, onProgress);
|
|
62028
|
-
}
|
|
62029
|
-
if (deps.bun?.length) {
|
|
62030
|
-
await buildCustomImage(docker, "bun", deps.bun, onProgress);
|
|
62031
|
-
}
|
|
62032
|
-
if (deps.deno?.length) {
|
|
62033
|
-
await buildCustomImage(docker, "deno", deps.deno, onProgress);
|
|
62034
|
-
}
|
|
62035
|
-
if (deps.bash?.length) {
|
|
62036
|
-
await buildCustomImage(docker, "bash", deps.bash, onProgress);
|
|
62037
|
-
}
|
|
62038
|
-
}
|
|
62039
|
-
async function buildCustomImage(docker, runtime, packages, onProgress) {
|
|
62040
|
-
const tag = `isol8:${runtime}-custom`;
|
|
62041
|
-
onProgress?.({ runtime, status: "building", message: `Custom: ${packages.join(", ")}` });
|
|
62042
|
-
let installCmd;
|
|
62043
|
-
switch (runtime) {
|
|
62044
|
-
case "python":
|
|
62045
|
-
installCmd = `RUN pip install --no-cache-dir ${packages.join(" ")}`;
|
|
62046
|
-
break;
|
|
62047
|
-
case "node":
|
|
62048
|
-
installCmd = `RUN npm install -g ${packages.join(" ")}`;
|
|
62049
|
-
break;
|
|
62050
|
-
case "bun":
|
|
62051
|
-
installCmd = `RUN bun install -g ${packages.join(" ")}`;
|
|
62052
|
-
break;
|
|
62053
|
-
case "deno":
|
|
62054
|
-
installCmd = packages.map((p) => `RUN deno cache ${p}`).join(`
|
|
62055
|
-
`);
|
|
62056
|
-
break;
|
|
62057
|
-
case "bash":
|
|
62058
|
-
installCmd = `RUN apk add --no-cache ${packages.join(" ")}`;
|
|
62059
|
-
break;
|
|
62060
|
-
default:
|
|
62061
|
-
throw new Error(`Unknown runtime: ${runtime}`);
|
|
62062
|
-
}
|
|
62063
|
-
const dockerfileContent = `FROM isol8:${runtime}
|
|
62064
|
-
${installCmd}
|
|
62065
|
-
`;
|
|
62066
|
-
const { createTarBuffer: createTarBuffer2, validatePackageName: validatePackageName2 } = await Promise.resolve().then(() => exports_utils);
|
|
62067
|
-
const { Readable } = await import("node:stream");
|
|
62068
|
-
packages.forEach(validatePackageName2);
|
|
62069
|
-
const tarBuffer = createTarBuffer2("Dockerfile", dockerfileContent);
|
|
62070
|
-
const stream = await docker.buildImage(Readable.from(tarBuffer), {
|
|
62071
|
-
t: tag,
|
|
62072
|
-
dockerfile: "Dockerfile"
|
|
62073
|
-
});
|
|
62074
|
-
await new Promise((resolve2, reject) => {
|
|
62075
|
-
docker.modem.followProgress(stream, (err) => {
|
|
62076
|
-
if (err) {
|
|
62077
|
-
reject(err);
|
|
62078
|
-
} else {
|
|
62079
|
-
resolve2();
|
|
62080
|
-
}
|
|
62081
|
-
});
|
|
62082
|
-
});
|
|
62083
|
-
onProgress?.({ runtime, status: "done" });
|
|
62084
|
-
}
|
|
62085
|
-
|
|
62086
|
-
// src/cli.ts
|
|
62535
|
+
init_image_builder();
|
|
62087
62536
|
init_runtime();
|
|
62088
62537
|
init_logger();
|
|
62089
62538
|
init_version();
|
|
@@ -62097,7 +62546,7 @@ program2.name("isol8").description("Secure code execution engine").version(VERSI
|
|
|
62097
62546
|
logger.debug(`[CLI] Version: ${VERSION}`);
|
|
62098
62547
|
logger.debug(`[CLI] Platform: ${platform()} ${arch()}`);
|
|
62099
62548
|
});
|
|
62100
|
-
program2.command("setup").description("Check Docker and build isol8 images").option("--python <packages>", "Additional Python packages (comma-separated)").option("--node <packages>", "Additional Node.js packages (comma-separated)").option("--bun <packages>", "Additional Bun packages (comma-separated)").option("--deno <packages>", "Additional Deno packages (comma-separated)").option("--bash <packages>", "Additional Bash packages (comma-separated)").action(async (opts) => {
|
|
62549
|
+
program2.command("setup").description("Check Docker and build isol8 images").option("--python <packages>", "Additional Python packages (comma-separated)").option("--node <packages>", "Additional Node.js packages (comma-separated)").option("--bun <packages>", "Additional Bun packages (comma-separated)").option("--deno <packages>", "Additional Deno packages (comma-separated)").option("--bash <packages>", "Additional Bash packages (comma-separated)").option("--force", "Force rebuild even if images are up to date").action(async (opts) => {
|
|
62101
62550
|
const docker = new import_dockerode2.default;
|
|
62102
62551
|
logger.debug("[Setup] Connecting to Docker daemon");
|
|
62103
62552
|
const spinner = ora("Checking Docker...").start();
|
|
@@ -62112,7 +62561,7 @@ program2.command("setup").description("Check Docker and build isol8 images").opt
|
|
|
62112
62561
|
process.exit(1);
|
|
62113
62562
|
}
|
|
62114
62563
|
spinner.start("Building isol8 images...");
|
|
62115
|
-
logger.debug(
|
|
62564
|
+
logger.debug(`[Setup] Building base images (force=${opts.force ?? false})`);
|
|
62116
62565
|
await buildBaseImages(docker, (progress) => {
|
|
62117
62566
|
const status = progress.status === "error" ? "[ERR]" : progress.status === "done" ? "[OK]" : "[..]";
|
|
62118
62567
|
if (progress.status === "building") {
|
|
@@ -62128,7 +62577,7 @@ program2.command("setup").description("Check Docker and build isol8 images").opt
|
|
|
62128
62577
|
spinner.start();
|
|
62129
62578
|
}
|
|
62130
62579
|
}
|
|
62131
|
-
});
|
|
62580
|
+
}, opts.force ?? false);
|
|
62132
62581
|
if (spinner.isSpinning) {
|
|
62133
62582
|
spinner.stop();
|
|
62134
62583
|
}
|
|
@@ -62176,7 +62625,7 @@ program2.command("setup").description("Check Docker and build isol8 images").opt
|
|
|
62176
62625
|
spinner.start();
|
|
62177
62626
|
}
|
|
62178
62627
|
}
|
|
62179
|
-
});
|
|
62628
|
+
}, opts.force ?? false);
|
|
62180
62629
|
if (spinner.isSpinning) {
|
|
62181
62630
|
spinner.stop();
|
|
62182
62631
|
}
|
|
@@ -62184,12 +62633,25 @@ program2.command("setup").description("Check Docker and build isol8 images").opt
|
|
|
62184
62633
|
console.log(`
|
|
62185
62634
|
[DONE] Setup complete!`);
|
|
62186
62635
|
});
|
|
62187
|
-
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("--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)").
|
|
62188
|
-
const {
|
|
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) => {
|
|
62637
|
+
const {
|
|
62638
|
+
code,
|
|
62639
|
+
codeUrl,
|
|
62640
|
+
codeHash,
|
|
62641
|
+
allowInsecureCodeUrl,
|
|
62642
|
+
runtime,
|
|
62643
|
+
engineOptions,
|
|
62644
|
+
engine,
|
|
62645
|
+
stdinData,
|
|
62646
|
+
fileExtension
|
|
62647
|
+
} = await resolveRunInput(file, opts);
|
|
62189
62648
|
logger.debug(`[Run] Runtime: ${runtime}, mode: ${engineOptions.mode}`);
|
|
62190
62649
|
logger.debug(`[Run] Network: ${engineOptions.network}, timeout: ${engineOptions.timeoutMs}ms`);
|
|
62191
62650
|
logger.debug(`[Run] Memory: ${engineOptions.memoryLimit}, CPU: ${engineOptions.cpuLimit}`);
|
|
62192
|
-
logger.debug(`[Run] Code
|
|
62651
|
+
logger.debug(`[Run] Code source: ${codeUrl ? `url=${codeUrl}` : "inline/file/stdin"}`);
|
|
62652
|
+
if (code) {
|
|
62653
|
+
logger.debug(`[Run] Code length: ${code.length} chars`);
|
|
62654
|
+
}
|
|
62193
62655
|
if (stdinData) {
|
|
62194
62656
|
logger.debug(`[Run] Stdin data provided (${stdinData.length} chars)`);
|
|
62195
62657
|
}
|
|
@@ -62215,9 +62677,12 @@ program2.command("run").description("Execute code in isol8").argument("[file]",
|
|
|
62215
62677
|
logger.debug("[Run] Engine started");
|
|
62216
62678
|
spinner.text = "Running code...";
|
|
62217
62679
|
const req = {
|
|
62218
|
-
code,
|
|
62219
62680
|
runtime,
|
|
62220
62681
|
timeoutMs: engineOptions.timeoutMs,
|
|
62682
|
+
...code ? { code } : {},
|
|
62683
|
+
...codeUrl ? { codeUrl } : {},
|
|
62684
|
+
...codeHash ? { codeHash } : {},
|
|
62685
|
+
...allowInsecureCodeUrl ? { allowInsecureCodeUrl } : {},
|
|
62221
62686
|
...stdinData ? { stdin: stdinData } : {},
|
|
62222
62687
|
...opts.install.length > 0 ? { installPackages: opts.install } : {},
|
|
62223
62688
|
fileExtension
|
|
@@ -62377,7 +62842,7 @@ async function downloadServerBinary(binaryPath) {
|
|
|
62377
62842
|
}
|
|
62378
62843
|
process.exit(1);
|
|
62379
62844
|
}
|
|
62380
|
-
const binDir =
|
|
62845
|
+
const binDir = join4(homedir2(), ".isol8", "bin");
|
|
62381
62846
|
mkdirSync2(binDir, { recursive: true });
|
|
62382
62847
|
const tmpPath = `${binaryPath}.tmp`;
|
|
62383
62848
|
const buffer = Buffer.from(await response.arrayBuffer());
|
|
@@ -62409,8 +62874,8 @@ async function promptYesNo(question) {
|
|
|
62409
62874
|
return normalized === "" || normalized === "y" || normalized === "yes";
|
|
62410
62875
|
}
|
|
62411
62876
|
async function ensureServerBinary(forceUpdate) {
|
|
62412
|
-
const binDir =
|
|
62413
|
-
const binaryPath =
|
|
62877
|
+
const binDir = join4(homedir2(), ".isol8", "bin");
|
|
62878
|
+
const binaryPath = join4(binDir, "isol8-server");
|
|
62414
62879
|
logger.debug(`[Serve] Binary path: ${binaryPath}, forceUpdate: ${forceUpdate}`);
|
|
62415
62880
|
if (forceUpdate) {
|
|
62416
62881
|
logger.debug("[Serve] Force update requested");
|
|
@@ -62440,8 +62905,8 @@ async function ensureServerBinary(forceUpdate) {
|
|
|
62440
62905
|
program2.command("config").description("Show the resolved isol8 configuration").option("--json", "Output as raw JSON").action((opts) => {
|
|
62441
62906
|
const config = loadConfig();
|
|
62442
62907
|
const searchPaths = [
|
|
62443
|
-
|
|
62444
|
-
|
|
62908
|
+
join4(resolve2(process.cwd()), "isol8.config.json"),
|
|
62909
|
+
join4(homedir2(), ".isol8", "config.json")
|
|
62445
62910
|
];
|
|
62446
62911
|
const loadedFrom = searchPaths.find((p) => existsSync5(p));
|
|
62447
62912
|
logger.debug(`[Config] Config source: ${loadedFrom ?? "defaults"}`);
|
|
@@ -62476,6 +62941,13 @@ Isol8 Configuration
|
|
|
62476
62941
|
} else {
|
|
62477
62942
|
console.log(" Whitelist: (none)");
|
|
62478
62943
|
}
|
|
62944
|
+
console.log("");
|
|
62945
|
+
console.log(" ── Remote Code ──");
|
|
62946
|
+
console.log(` Enabled: ${config.remoteCode.enabled ? "yes" : "no"}`);
|
|
62947
|
+
console.log(` Schemes: ${config.remoteCode.allowedSchemes.join(", ")}`);
|
|
62948
|
+
console.log(` Max code size: ${config.remoteCode.maxCodeSize} bytes`);
|
|
62949
|
+
console.log(` Fetch timeout: ${config.remoteCode.fetchTimeoutMs}ms`);
|
|
62950
|
+
console.log(` Require hash: ${config.remoteCode.requireHash ? "yes" : "no"}`);
|
|
62479
62951
|
if (config.network.blacklist.length > 0) {
|
|
62480
62952
|
console.log(` Blacklist: ${config.network.blacklist.join(", ")}`);
|
|
62481
62953
|
} else {
|
|
@@ -62485,6 +62957,11 @@ Isol8 Configuration
|
|
|
62485
62957
|
console.log(" ── Cleanup ──");
|
|
62486
62958
|
console.log(` Auto-prune: ${config.cleanup.autoPrune ? "yes" : "no"}`);
|
|
62487
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}`);
|
|
62488
62965
|
const deps = config.dependencies;
|
|
62489
62966
|
const hasDeps = Object.values(deps).some((pkgs) => pkgs && pkgs.length > 0);
|
|
62490
62967
|
console.log("");
|
|
@@ -62507,7 +62984,7 @@ Isol8 Configuration
|
|
|
62507
62984
|
}
|
|
62508
62985
|
console.log("");
|
|
62509
62986
|
});
|
|
62510
|
-
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) => {
|
|
62511
62988
|
const docker = new import_dockerode2.default;
|
|
62512
62989
|
logger.debug("[Cleanup] Connecting to Docker daemon");
|
|
62513
62990
|
const spinner = ora("Checking Docker...").start();
|
|
@@ -62523,17 +63000,33 @@ program2.command("cleanup").description("Remove orphaned isol8 containers").opti
|
|
|
62523
63000
|
const containers = await docker.listContainers({ all: true });
|
|
62524
63001
|
const isol8Containers = containers.filter((c) => c.Image.startsWith("isol8:") || c.Image.startsWith("isol8-custom:"));
|
|
62525
63002
|
logger.debug(`[Cleanup] Found ${containers.length} total containers, ${isol8Containers.length} isol8 containers`);
|
|
62526
|
-
|
|
62527
|
-
|
|
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");
|
|
62528
63012
|
return;
|
|
62529
63013
|
}
|
|
62530
|
-
spinner.succeed(`Found ${isol8Containers.length} isol8 container(s)`);
|
|
63014
|
+
spinner.succeed(`Found ${isol8Containers.length} isol8 container(s)` + (opts.images ? ` and ${isol8Images.length} image(s)` : ""));
|
|
62531
63015
|
console.log("");
|
|
62532
63016
|
for (const c of isol8Containers) {
|
|
62533
63017
|
const status = c.State === "running" ? "\uD83D\uDFE2 running" : "⚪ stopped";
|
|
62534
63018
|
const created = new Date(c.Created * 1000).toLocaleString();
|
|
62535
63019
|
console.log(` ${status} ${c.Id.slice(0, 12)} | ${c.Image} | created ${created}`);
|
|
62536
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
|
+
}
|
|
62537
63030
|
console.log("");
|
|
62538
63031
|
if (!opts.force) {
|
|
62539
63032
|
const readline = await import("node:readline");
|
|
@@ -62542,7 +63035,8 @@ program2.command("cleanup").description("Remove orphaned isol8 containers").opti
|
|
|
62542
63035
|
output: process.stdout
|
|
62543
63036
|
});
|
|
62544
63037
|
const answer = await new Promise((resolve3) => {
|
|
62545
|
-
|
|
63038
|
+
const targetLabel = opts.images ? "containers and images" : "containers";
|
|
63039
|
+
rl.question(`Remove all these ${targetLabel}? [y/N] `, resolve3);
|
|
62546
63040
|
});
|
|
62547
63041
|
rl.close();
|
|
62548
63042
|
if (answer.toLowerCase() !== "y" && answer.toLowerCase() !== "yes") {
|
|
@@ -62550,28 +63044,65 @@ program2.command("cleanup").description("Remove orphaned isol8 containers").opti
|
|
|
62550
63044
|
return;
|
|
62551
63045
|
}
|
|
62552
63046
|
}
|
|
62553
|
-
|
|
62554
|
-
|
|
62555
|
-
|
|
62556
|
-
|
|
62557
|
-
|
|
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) {
|
|
62558
63055
|
console.log("");
|
|
62559
|
-
for (const err of
|
|
63056
|
+
for (const err of containerResult.errors) {
|
|
62560
63057
|
console.error(` Failed to remove ${err}`);
|
|
62561
63058
|
}
|
|
62562
63059
|
}
|
|
62563
|
-
if (
|
|
62564
|
-
spinner.succeed(`Removed ${
|
|
63060
|
+
if (containerResult.failed === 0) {
|
|
63061
|
+
spinner.succeed(`Removed ${containerResult.removed} container(s)`);
|
|
62565
63062
|
} else {
|
|
62566
|
-
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
|
+
}
|
|
62567
63081
|
}
|
|
62568
63082
|
});
|
|
62569
63083
|
async function resolveRunInput(file, opts) {
|
|
62570
63084
|
const config = loadConfig();
|
|
62571
63085
|
logger.debug("[Run] Config loaded");
|
|
62572
63086
|
let code;
|
|
63087
|
+
let codeUrl;
|
|
63088
|
+
let codeHash;
|
|
63089
|
+
let allowInsecureCodeUrl = false;
|
|
62573
63090
|
let runtime;
|
|
62574
|
-
if (opts.
|
|
63091
|
+
if (opts.url || opts.github || opts.gist) {
|
|
63092
|
+
if (file || opts.eval) {
|
|
63093
|
+
console.error("[ERR] --url/--github/--gist cannot be used with file input or --eval.");
|
|
63094
|
+
process.exit(1);
|
|
63095
|
+
}
|
|
63096
|
+
codeUrl = resolveCodeUrl(opts);
|
|
63097
|
+
codeHash = opts.hash ?? undefined;
|
|
63098
|
+
allowInsecureCodeUrl = opts.allowInsecureCodeUrl ?? false;
|
|
63099
|
+
runtime = opts.runtime ?? detectRuntimeFromPath(new URL(codeUrl).pathname);
|
|
63100
|
+
if (!runtime) {
|
|
63101
|
+
console.error("[ERR] Cannot detect runtime from URL path. Use --runtime to specify.");
|
|
63102
|
+
process.exit(1);
|
|
63103
|
+
}
|
|
63104
|
+
logger.debug(`[Run] Remote code URL: ${codeUrl}`);
|
|
63105
|
+
} else if (opts.eval) {
|
|
62575
63106
|
code = opts.eval;
|
|
62576
63107
|
runtime = opts.runtime ?? "python";
|
|
62577
63108
|
logger.debug(`[Run] Inline eval, runtime: ${runtime}`);
|
|
@@ -62582,7 +63113,7 @@ async function resolveRunInput(file, opts) {
|
|
|
62582
63113
|
console.error(`[ERR] File not found: ${file}`);
|
|
62583
63114
|
process.exit(1);
|
|
62584
63115
|
}
|
|
62585
|
-
code =
|
|
63116
|
+
code = readFileSync4(filePath, "utf-8");
|
|
62586
63117
|
if (opts.runtime) {
|
|
62587
63118
|
runtime = opts.runtime;
|
|
62588
63119
|
logger.debug(`[Run] Runtime specified: ${runtime}`);
|
|
@@ -62622,11 +63153,8 @@ async function resolveRunInput(file, opts) {
|
|
|
62622
63153
|
debug: opts.debug ?? config.debug,
|
|
62623
63154
|
persist: opts.persist ?? false,
|
|
62624
63155
|
...opts.logNetwork ? { logNetwork: true } : {},
|
|
62625
|
-
|
|
62626
|
-
|
|
62627
|
-
clean: Number.parseInt(opts.poolSize.split(",")[0], 10),
|
|
62628
|
-
dirty: Number.parseInt(opts.poolSize.split(",")[1], 10)
|
|
62629
|
-
} : Number.parseInt(opts.poolSize, 10) : { clean: 1, dirty: 1 }
|
|
63156
|
+
dependencies: config.dependencies,
|
|
63157
|
+
remoteCode: config.remoteCode
|
|
62630
63158
|
};
|
|
62631
63159
|
logger.debug(`[Run] Engine options: mode=${engineOptions.mode}, network=${engineOptions.network}`);
|
|
62632
63160
|
let fileExtension;
|
|
@@ -62660,7 +63188,48 @@ async function resolveRunInput(file, opts) {
|
|
|
62660
63188
|
logger.debug("[Run] Using local Docker engine");
|
|
62661
63189
|
engine = new DockerIsol8(engineOptions, config.maxConcurrent);
|
|
62662
63190
|
}
|
|
62663
|
-
return {
|
|
63191
|
+
return {
|
|
63192
|
+
code,
|
|
63193
|
+
codeUrl,
|
|
63194
|
+
codeHash,
|
|
63195
|
+
allowInsecureCodeUrl,
|
|
63196
|
+
runtime,
|
|
63197
|
+
engineOptions,
|
|
63198
|
+
engine,
|
|
63199
|
+
stdinData,
|
|
63200
|
+
fileExtension
|
|
63201
|
+
};
|
|
63202
|
+
}
|
|
63203
|
+
function resolveCodeUrl(opts) {
|
|
63204
|
+
if (typeof opts.url === "string") {
|
|
63205
|
+
return opts.url;
|
|
63206
|
+
}
|
|
63207
|
+
if (typeof opts.github === "string") {
|
|
63208
|
+
const parts = opts.github.split("/");
|
|
63209
|
+
if (parts.length < 4) {
|
|
63210
|
+
console.error("[ERR] --github format must be owner/repo/ref/path/to/file");
|
|
63211
|
+
process.exit(1);
|
|
63212
|
+
}
|
|
63213
|
+
const [owner, repo, ref, ...pathParts] = parts;
|
|
63214
|
+
return `https://raw.githubusercontent.com/${owner}/${repo}/${ref}/${pathParts.join("/")}`;
|
|
63215
|
+
}
|
|
63216
|
+
if (typeof opts.gist === "string") {
|
|
63217
|
+
const [gistId, ...fileParts] = opts.gist.split("/");
|
|
63218
|
+
if (!gistId || fileParts.length === 0) {
|
|
63219
|
+
console.error("[ERR] --gist format must be gistId/file.ext");
|
|
63220
|
+
process.exit(1);
|
|
63221
|
+
}
|
|
63222
|
+
return `https://gist.githubusercontent.com/${gistId}/raw/${fileParts.join("/")}`;
|
|
63223
|
+
}
|
|
63224
|
+
console.error("[ERR] Missing code URL source.");
|
|
63225
|
+
process.exit(1);
|
|
63226
|
+
}
|
|
63227
|
+
function detectRuntimeFromPath(pathValue) {
|
|
63228
|
+
try {
|
|
63229
|
+
return RuntimeRegistry.detect(pathValue).name;
|
|
63230
|
+
} catch {
|
|
63231
|
+
return;
|
|
63232
|
+
}
|
|
62664
63233
|
}
|
|
62665
63234
|
function collect(value, previous) {
|
|
62666
63235
|
return previous.concat([value]);
|
|
@@ -62671,4 +63240,4 @@ if (!process.argv.slice(2).length) {
|
|
|
62671
63240
|
}
|
|
62672
63241
|
program2.parse();
|
|
62673
63242
|
|
|
62674
|
-
//# debugId=
|
|
63243
|
+
//# debugId=6B3AFECE6CC836BD64756E2164756E21
|