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/index.js
CHANGED
|
@@ -336,6 +336,180 @@ var init_audit = __esm(() => {
|
|
|
336
336
|
init_logger();
|
|
337
337
|
});
|
|
338
338
|
|
|
339
|
+
// src/engine/code-fetcher.ts
|
|
340
|
+
import { createHash } from "node:crypto";
|
|
341
|
+
import { lookup as dnsLookup } from "node:dns/promises";
|
|
342
|
+
import { isIP } from "node:net";
|
|
343
|
+
function sha256Hex(input) {
|
|
344
|
+
return createHash("sha256").update(input, "utf-8").digest("hex");
|
|
345
|
+
}
|
|
346
|
+
function normalizeScheme(url) {
|
|
347
|
+
return url.protocol.replace(/:$/, "").toLowerCase();
|
|
348
|
+
}
|
|
349
|
+
function isBlockedByPattern(host, patterns) {
|
|
350
|
+
return patterns.some((pattern) => new RegExp(pattern, "i").test(host));
|
|
351
|
+
}
|
|
352
|
+
function isAllowedByPattern(host, patterns) {
|
|
353
|
+
if (patterns.length === 0) {
|
|
354
|
+
return true;
|
|
355
|
+
}
|
|
356
|
+
return patterns.some((pattern) => new RegExp(pattern, "i").test(host));
|
|
357
|
+
}
|
|
358
|
+
function isPrivateIpv4(ip) {
|
|
359
|
+
const parts = ip.split(IPV4_SEPARATOR).map((v) => Number.parseInt(v, 10));
|
|
360
|
+
if (parts.length !== 4 || parts.some((p) => Number.isNaN(p))) {
|
|
361
|
+
return false;
|
|
362
|
+
}
|
|
363
|
+
const a = parts[0];
|
|
364
|
+
const b = parts[1];
|
|
365
|
+
if (a === 10 || a === 127 || a === 0) {
|
|
366
|
+
return true;
|
|
367
|
+
}
|
|
368
|
+
if (a === 169 && b === 254) {
|
|
369
|
+
return true;
|
|
370
|
+
}
|
|
371
|
+
if (a === 172 && b >= 16 && b <= 31) {
|
|
372
|
+
return true;
|
|
373
|
+
}
|
|
374
|
+
if (a === 192 && b === 168) {
|
|
375
|
+
return true;
|
|
376
|
+
}
|
|
377
|
+
if (a === 100 && b >= 64 && b <= 127) {
|
|
378
|
+
return true;
|
|
379
|
+
}
|
|
380
|
+
return false;
|
|
381
|
+
}
|
|
382
|
+
function isPrivateIpv6(ip) {
|
|
383
|
+
const normalized = ip.toLowerCase();
|
|
384
|
+
if (normalized === IPV6_LOOPBACK) {
|
|
385
|
+
return true;
|
|
386
|
+
}
|
|
387
|
+
return normalized.startsWith("fc") || normalized.startsWith("fd") || normalized.startsWith("fe8") || normalized.startsWith("fe9") || normalized.startsWith("fea") || normalized.startsWith("feb");
|
|
388
|
+
}
|
|
389
|
+
function isPrivateIp(ip) {
|
|
390
|
+
const family = isIP(ip);
|
|
391
|
+
if (family === 4) {
|
|
392
|
+
return isPrivateIpv4(ip);
|
|
393
|
+
}
|
|
394
|
+
if (family === 6) {
|
|
395
|
+
return isPrivateIpv6(ip);
|
|
396
|
+
}
|
|
397
|
+
return false;
|
|
398
|
+
}
|
|
399
|
+
async function assertHostResolvesPublic(host, lookupFn) {
|
|
400
|
+
if (isIP(host) && isPrivateIp(host)) {
|
|
401
|
+
throw new Error(`Blocked code URL host: ${host}`);
|
|
402
|
+
}
|
|
403
|
+
try {
|
|
404
|
+
const records = await lookupFn(host);
|
|
405
|
+
for (const record of records) {
|
|
406
|
+
if (isPrivateIp(record.address)) {
|
|
407
|
+
throw new Error(`Blocked code URL host: ${host}`);
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
} catch (err) {
|
|
411
|
+
if (err instanceof Error && err.message.startsWith("Blocked code URL host:")) {
|
|
412
|
+
throw err;
|
|
413
|
+
}
|
|
414
|
+
throw new Error(`Failed to resolve code URL host: ${host}`);
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
function decodeUtf8(content) {
|
|
418
|
+
const decoder = new TextDecoder("utf-8", { fatal: true });
|
|
419
|
+
const text = decoder.decode(content);
|
|
420
|
+
if (text.includes("\x00")) {
|
|
421
|
+
throw new Error("Fetched code appears to be binary content");
|
|
422
|
+
}
|
|
423
|
+
return text;
|
|
424
|
+
}
|
|
425
|
+
async function fetchRemoteCode(request, policy, deps = {}) {
|
|
426
|
+
if (!policy.enabled) {
|
|
427
|
+
throw new Error("Remote code fetching is disabled. Set remoteCode.enabled=true to allow it.");
|
|
428
|
+
}
|
|
429
|
+
const fetchFn = deps.fetchFn ?? globalThis.fetch;
|
|
430
|
+
const lookupFn = deps.lookupFn ?? (async (hostname) => {
|
|
431
|
+
const records = await dnsLookup(hostname, { all: true, verbatim: true });
|
|
432
|
+
return records;
|
|
433
|
+
});
|
|
434
|
+
if (!request.codeUrl) {
|
|
435
|
+
throw new Error("codeUrl is required for remote code fetching");
|
|
436
|
+
}
|
|
437
|
+
const url = new URL(request.codeUrl);
|
|
438
|
+
const scheme = normalizeScheme(url);
|
|
439
|
+
if (scheme === "http" && !request.allowInsecureCodeUrl) {
|
|
440
|
+
throw new Error("Insecure code URL blocked. Use allowInsecureCodeUrl=true to allow HTTP.");
|
|
441
|
+
}
|
|
442
|
+
if (!policy.allowedSchemes.map((s) => s.toLowerCase()).includes(scheme)) {
|
|
443
|
+
throw new Error(`URL scheme not allowed: ${scheme}`);
|
|
444
|
+
}
|
|
445
|
+
const host = url.hostname.toLowerCase();
|
|
446
|
+
if (!isAllowedByPattern(host, policy.allowedHosts) || isBlockedByPattern(host, policy.blockedHosts)) {
|
|
447
|
+
throw new Error(`Blocked code URL host: ${host}`);
|
|
448
|
+
}
|
|
449
|
+
await assertHostResolvesPublic(host, lookupFn);
|
|
450
|
+
if (policy.requireHash && !request.codeHash) {
|
|
451
|
+
throw new Error("Hash verification required: provide codeHash for remote code execution.");
|
|
452
|
+
}
|
|
453
|
+
const controller = new AbortController;
|
|
454
|
+
const timeout = setTimeout(() => controller.abort(), policy.fetchTimeoutMs);
|
|
455
|
+
let response;
|
|
456
|
+
try {
|
|
457
|
+
response = await fetchFn(url.toString(), {
|
|
458
|
+
method: "GET",
|
|
459
|
+
redirect: "follow",
|
|
460
|
+
signal: controller.signal
|
|
461
|
+
});
|
|
462
|
+
} catch (err) {
|
|
463
|
+
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)}`);
|
|
464
|
+
} finally {
|
|
465
|
+
clearTimeout(timeout);
|
|
466
|
+
}
|
|
467
|
+
if (!response.ok) {
|
|
468
|
+
throw new Error(`Failed to fetch remote code: HTTP ${response.status}`);
|
|
469
|
+
}
|
|
470
|
+
const contentLengthHeader = response.headers.get("content-length");
|
|
471
|
+
if (contentLengthHeader) {
|
|
472
|
+
const parsedLength = Number.parseInt(contentLengthHeader, 10);
|
|
473
|
+
if (!Number.isNaN(parsedLength) && parsedLength > policy.maxCodeSize) {
|
|
474
|
+
throw new Error(`Remote code exceeds maxCodeSize (${policy.maxCodeSize} bytes): ${parsedLength} bytes`);
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
if (!response.body) {
|
|
478
|
+
throw new Error("Remote code response body is empty");
|
|
479
|
+
}
|
|
480
|
+
const reader = response.body.getReader();
|
|
481
|
+
const chunks = [];
|
|
482
|
+
let totalBytes = 0;
|
|
483
|
+
while (true) {
|
|
484
|
+
const { done, value } = await reader.read();
|
|
485
|
+
if (done) {
|
|
486
|
+
break;
|
|
487
|
+
}
|
|
488
|
+
if (!value) {
|
|
489
|
+
continue;
|
|
490
|
+
}
|
|
491
|
+
totalBytes += value.byteLength;
|
|
492
|
+
if (totalBytes > policy.maxCodeSize) {
|
|
493
|
+
throw new Error(`Remote code exceeds maxCodeSize (${policy.maxCodeSize} bytes)`);
|
|
494
|
+
}
|
|
495
|
+
chunks.push(value);
|
|
496
|
+
}
|
|
497
|
+
const buffer = new Uint8Array(totalBytes);
|
|
498
|
+
let offset = 0;
|
|
499
|
+
for (const chunk of chunks) {
|
|
500
|
+
buffer.set(chunk, offset);
|
|
501
|
+
offset += chunk.byteLength;
|
|
502
|
+
}
|
|
503
|
+
const code = decodeUtf8(buffer);
|
|
504
|
+
const hash = sha256Hex(code);
|
|
505
|
+
if (request.codeHash && hash.toLowerCase() !== request.codeHash.toLowerCase()) {
|
|
506
|
+
throw new Error("Remote code hash mismatch");
|
|
507
|
+
}
|
|
508
|
+
return { code, url: url.toString(), hash };
|
|
509
|
+
}
|
|
510
|
+
var IPV4_SEPARATOR = ".", IPV6_LOOPBACK = "::1";
|
|
511
|
+
var init_code_fetcher = () => {};
|
|
512
|
+
|
|
339
513
|
// src/engine/concurrency.ts
|
|
340
514
|
class Semaphore {
|
|
341
515
|
max;
|
|
@@ -372,6 +546,40 @@ class Semaphore {
|
|
|
372
546
|
}
|
|
373
547
|
}
|
|
374
548
|
|
|
549
|
+
// src/engine/image-builder.ts
|
|
550
|
+
import { createHash as createHash2 } from "node:crypto";
|
|
551
|
+
import { existsSync as existsSync3, readFileSync as readFileSync2 } from "node:fs";
|
|
552
|
+
function resolveDockerDir() {
|
|
553
|
+
const fromBundled = new URL("./docker", import.meta.url).pathname;
|
|
554
|
+
if (existsSync3(fromBundled)) {
|
|
555
|
+
return fromBundled;
|
|
556
|
+
}
|
|
557
|
+
return new URL("../../docker", import.meta.url).pathname;
|
|
558
|
+
}
|
|
559
|
+
function computeDepsHash(runtime, packages) {
|
|
560
|
+
const hash = createHash2("sha256");
|
|
561
|
+
hash.update(runtime);
|
|
562
|
+
for (const pkg of [...packages].sort()) {
|
|
563
|
+
hash.update(pkg);
|
|
564
|
+
}
|
|
565
|
+
return hash.digest("hex");
|
|
566
|
+
}
|
|
567
|
+
function normalizePackages(packages) {
|
|
568
|
+
return [...new Set(packages.map((pkg) => pkg.trim()).filter(Boolean))].sort();
|
|
569
|
+
}
|
|
570
|
+
function getCustomImageTag(runtime, packages) {
|
|
571
|
+
const normalizedPackages = normalizePackages(packages);
|
|
572
|
+
const depsHash = computeDepsHash(runtime, normalizedPackages);
|
|
573
|
+
const shortHash = depsHash.slice(0, 12);
|
|
574
|
+
return `isol8:${runtime}-custom-${shortHash}`;
|
|
575
|
+
}
|
|
576
|
+
var DOCKERFILE_DIR;
|
|
577
|
+
var init_image_builder = __esm(() => {
|
|
578
|
+
init_runtime();
|
|
579
|
+
init_logger();
|
|
580
|
+
DOCKERFILE_DIR = resolveDockerDir();
|
|
581
|
+
});
|
|
582
|
+
|
|
375
583
|
// src/engine/pool.ts
|
|
376
584
|
class ContainerPool {
|
|
377
585
|
docker;
|
|
@@ -562,46 +770,44 @@ class ContainerPool {
|
|
|
562
770
|
}
|
|
563
771
|
replenish(image) {
|
|
564
772
|
if (this.replenishing.has(image)) {
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
773
|
+
return;
|
|
774
|
+
}
|
|
775
|
+
const pool = this.pools.get(image);
|
|
776
|
+
const currentSize = pool ? this.poolStrategy === "fast" ? pool.clean.length : pool.clean?.length ?? 0 : 0;
|
|
777
|
+
const targetSize = this.cleanPoolSize;
|
|
778
|
+
if (currentSize >= targetSize) {
|
|
779
|
+
return;
|
|
780
|
+
}
|
|
781
|
+
this.replenishing.add(image);
|
|
782
|
+
const promise = this.createContainer(image).then((container) => {
|
|
783
|
+
const p = this.pools.get(image);
|
|
784
|
+
if (!p) {
|
|
785
|
+
container.remove({ force: true }).catch(() => {});
|
|
572
786
|
return;
|
|
573
787
|
}
|
|
574
|
-
this.
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
788
|
+
if (this.poolStrategy === "fast") {
|
|
789
|
+
if (p.clean.length < this.cleanPoolSize) {
|
|
790
|
+
p.clean.push({ container, createdAt: Date.now() });
|
|
791
|
+
} else {
|
|
578
792
|
container.remove({ force: true }).catch(() => {});
|
|
579
|
-
return;
|
|
580
793
|
}
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
}
|
|
794
|
+
} else {
|
|
795
|
+
if (!p.clean) {
|
|
796
|
+
p.clean = [];
|
|
797
|
+
}
|
|
798
|
+
if (p.clean.length < this.cleanPoolSize) {
|
|
799
|
+
p.clean.push({ container, createdAt: Date.now() });
|
|
587
800
|
} else {
|
|
588
|
-
|
|
589
|
-
p.clean = [];
|
|
590
|
-
}
|
|
591
|
-
if (p.clean.length < this.cleanPoolSize) {
|
|
592
|
-
p.clean.push({ container, createdAt: Date.now() });
|
|
593
|
-
} else {
|
|
594
|
-
container.remove({ force: true }).catch(() => {});
|
|
595
|
-
}
|
|
801
|
+
container.remove({ force: true }).catch(() => {});
|
|
596
802
|
}
|
|
597
|
-
}
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
803
|
+
}
|
|
804
|
+
}).catch((err) => {
|
|
805
|
+
logger.error(`[Pool] Error during replenishment for ${image}:`, err);
|
|
806
|
+
}).finally(() => {
|
|
807
|
+
this.replenishing.delete(image);
|
|
808
|
+
this.pendingReplenishments.delete(promise);
|
|
809
|
+
});
|
|
810
|
+
this.pendingReplenishments.add(promise);
|
|
605
811
|
}
|
|
606
812
|
}
|
|
607
813
|
var init_pool = __esm(() => {
|
|
@@ -749,7 +955,7 @@ __export(exports_docker, {
|
|
|
749
955
|
DockerIsol8: () => DockerIsol8
|
|
750
956
|
});
|
|
751
957
|
import { randomUUID } from "node:crypto";
|
|
752
|
-
import { existsSync as
|
|
958
|
+
import { existsSync as existsSync4, readFileSync as readFileSync3 } from "node:fs";
|
|
753
959
|
import { PassThrough } from "node:stream";
|
|
754
960
|
import Docker from "dockerode";
|
|
755
961
|
async function writeFileViaExec(container, filePath, content) {
|
|
@@ -966,11 +1172,32 @@ class DockerIsol8 {
|
|
|
966
1172
|
logNetwork;
|
|
967
1173
|
poolStrategy;
|
|
968
1174
|
poolSize;
|
|
1175
|
+
dependencies;
|
|
969
1176
|
auditLogger;
|
|
1177
|
+
remoteCodePolicy;
|
|
970
1178
|
container = null;
|
|
971
1179
|
persistentRuntime = null;
|
|
972
1180
|
pool = null;
|
|
973
1181
|
imageCache = new Map;
|
|
1182
|
+
async resolveExecutionRequest(req) {
|
|
1183
|
+
const inlineCode = req.code?.trim();
|
|
1184
|
+
const codeUrl = req.codeUrl?.trim();
|
|
1185
|
+
if (inlineCode && codeUrl) {
|
|
1186
|
+
throw new Error("ExecutionRequest.code and ExecutionRequest.codeUrl are mutually exclusive.");
|
|
1187
|
+
}
|
|
1188
|
+
if (!(inlineCode || codeUrl)) {
|
|
1189
|
+
throw new Error("ExecutionRequest must include either code or codeUrl.");
|
|
1190
|
+
}
|
|
1191
|
+
if (inlineCode) {
|
|
1192
|
+
return { ...req, code: req.code };
|
|
1193
|
+
}
|
|
1194
|
+
const fetched = await fetchRemoteCode({
|
|
1195
|
+
codeUrl,
|
|
1196
|
+
codeHash: req.codeHash,
|
|
1197
|
+
allowInsecureCodeUrl: req.allowInsecureCodeUrl
|
|
1198
|
+
}, this.remoteCodePolicy);
|
|
1199
|
+
return { ...req, code: fetched.code };
|
|
1200
|
+
}
|
|
974
1201
|
constructor(options = {}, maxConcurrent = 10) {
|
|
975
1202
|
this.docker = options.docker ?? new Docker;
|
|
976
1203
|
this.mode = options.mode ?? "ephemeral";
|
|
@@ -992,6 +1219,18 @@ class DockerIsol8 {
|
|
|
992
1219
|
this.logNetwork = options.logNetwork ?? false;
|
|
993
1220
|
this.poolStrategy = options.poolStrategy ?? "fast";
|
|
994
1221
|
this.poolSize = options.poolSize ?? { clean: 1, dirty: 1 };
|
|
1222
|
+
this.dependencies = options.dependencies ?? {};
|
|
1223
|
+
this.remoteCodePolicy = options.remoteCode ?? {
|
|
1224
|
+
enabled: false,
|
|
1225
|
+
allowedSchemes: ["https"],
|
|
1226
|
+
allowedHosts: [],
|
|
1227
|
+
blockedHosts: [],
|
|
1228
|
+
maxCodeSize: 10 * 1024 * 1024,
|
|
1229
|
+
fetchTimeoutMs: 30000,
|
|
1230
|
+
requireHash: false,
|
|
1231
|
+
enableCache: true,
|
|
1232
|
+
cacheTtl: 3600
|
|
1233
|
+
};
|
|
995
1234
|
if (options.audit) {
|
|
996
1235
|
this.auditLogger = new AuditLogger(options.audit);
|
|
997
1236
|
}
|
|
@@ -999,7 +1238,33 @@ class DockerIsol8 {
|
|
|
999
1238
|
logger.setDebug(true);
|
|
1000
1239
|
}
|
|
1001
1240
|
}
|
|
1002
|
-
async start() {
|
|
1241
|
+
async start(options = {}) {
|
|
1242
|
+
if (this.mode !== "ephemeral") {
|
|
1243
|
+
return;
|
|
1244
|
+
}
|
|
1245
|
+
const prewarm = options.prewarm;
|
|
1246
|
+
if (!prewarm) {
|
|
1247
|
+
return;
|
|
1248
|
+
}
|
|
1249
|
+
const pool = this.ensurePool();
|
|
1250
|
+
const images = new Set;
|
|
1251
|
+
const adapters2 = typeof prewarm === "object" && prewarm.runtimes?.length ? prewarm.runtimes.map((runtime) => RuntimeRegistry.get(runtime)) : RuntimeRegistry.list();
|
|
1252
|
+
for (const adapter of adapters2) {
|
|
1253
|
+
try {
|
|
1254
|
+
images.add(await this.resolveImage(adapter));
|
|
1255
|
+
} catch (err) {
|
|
1256
|
+
logger.debug(`[Pool] Pre-warm image resolution failed for ${adapter.name}: ${err}`);
|
|
1257
|
+
}
|
|
1258
|
+
}
|
|
1259
|
+
await Promise.all([...images].map(async (image) => {
|
|
1260
|
+
try {
|
|
1261
|
+
await pool.warm(image);
|
|
1262
|
+
logger.debug(`[Pool] Pre-warmed image: ${image}`);
|
|
1263
|
+
} catch (err) {
|
|
1264
|
+
logger.debug(`[Pool] Pre-warm failed for ${image}: ${err}`);
|
|
1265
|
+
}
|
|
1266
|
+
}));
|
|
1267
|
+
}
|
|
1003
1268
|
async stop() {
|
|
1004
1269
|
if (this.container) {
|
|
1005
1270
|
try {
|
|
@@ -1020,7 +1285,8 @@ class DockerIsol8 {
|
|
|
1020
1285
|
await this.semaphore.acquire();
|
|
1021
1286
|
const startTime = Date.now();
|
|
1022
1287
|
try {
|
|
1023
|
-
const
|
|
1288
|
+
const request = await this.resolveExecutionRequest(req);
|
|
1289
|
+
const result = this.mode === "persistent" ? await this.executePersistent(request, startTime) : await this.executeEphemeral(request, startTime);
|
|
1024
1290
|
return result;
|
|
1025
1291
|
} finally {
|
|
1026
1292
|
this.semaphore.release();
|
|
@@ -1174,8 +1440,9 @@ class DockerIsol8 {
|
|
|
1174
1440
|
async* executeStream(req) {
|
|
1175
1441
|
await this.semaphore.acquire();
|
|
1176
1442
|
try {
|
|
1177
|
-
const
|
|
1178
|
-
const
|
|
1443
|
+
const request = await this.resolveExecutionRequest(req);
|
|
1444
|
+
const adapter = this.getAdapter(request.runtime);
|
|
1445
|
+
const timeoutMs = request.timeoutMs ?? this.defaultTimeoutMs;
|
|
1179
1446
|
const image = await this.resolveImage(adapter);
|
|
1180
1447
|
const container = await this.docker.createContainer({
|
|
1181
1448
|
Image: image,
|
|
@@ -1192,23 +1459,23 @@ class DockerIsol8 {
|
|
|
1192
1459
|
await startProxy(container, this.networkFilter);
|
|
1193
1460
|
await setupIptables(container);
|
|
1194
1461
|
}
|
|
1195
|
-
const ext =
|
|
1462
|
+
const ext = request.fileExtension ?? adapter.getFileExtension();
|
|
1196
1463
|
const filePath = `${SANDBOX_WORKDIR}/main${ext}`;
|
|
1197
|
-
await writeFileViaExec(container, filePath,
|
|
1198
|
-
if (
|
|
1199
|
-
await installPackages(container,
|
|
1464
|
+
await writeFileViaExec(container, filePath, request.code);
|
|
1465
|
+
if (request.installPackages?.length) {
|
|
1466
|
+
await installPackages(container, request.runtime, request.installPackages);
|
|
1200
1467
|
}
|
|
1201
|
-
if (
|
|
1202
|
-
for (const [fPath, fContent] of Object.entries(
|
|
1468
|
+
if (request.files) {
|
|
1469
|
+
for (const [fPath, fContent] of Object.entries(request.files)) {
|
|
1203
1470
|
await writeFileViaExec(container, fPath, fContent);
|
|
1204
1471
|
}
|
|
1205
1472
|
}
|
|
1206
|
-
const rawCmd = adapter.getCommand(
|
|
1473
|
+
const rawCmd = adapter.getCommand(request.code, filePath);
|
|
1207
1474
|
const timeoutSec = Math.ceil(timeoutMs / 1000);
|
|
1208
1475
|
let cmd;
|
|
1209
|
-
if (
|
|
1476
|
+
if (request.stdin) {
|
|
1210
1477
|
const stdinPath = `${SANDBOX_WORKDIR}/_stdin`;
|
|
1211
|
-
await writeFileViaExec(container, stdinPath,
|
|
1478
|
+
await writeFileViaExec(container, stdinPath, request.stdin);
|
|
1212
1479
|
const cmdStr = rawCmd.map((a) => `'${a.replace(/'/g, "'\\''")}'`).join(" ");
|
|
1213
1480
|
cmd = wrapWithTimeout(["sh", "-c", `cat ${stdinPath} | ${cmdStr}`], timeoutSec);
|
|
1214
1481
|
} else {
|
|
@@ -1216,7 +1483,7 @@ class DockerIsol8 {
|
|
|
1216
1483
|
}
|
|
1217
1484
|
const exec = await container.exec({
|
|
1218
1485
|
Cmd: cmd,
|
|
1219
|
-
Env: this.buildEnv(
|
|
1486
|
+
Env: this.buildEnv(request.env),
|
|
1220
1487
|
AttachStdout: true,
|
|
1221
1488
|
AttachStderr: true,
|
|
1222
1489
|
WorkingDir: SANDBOX_WORKDIR,
|
|
@@ -1246,21 +1513,29 @@ class DockerIsol8 {
|
|
|
1246
1513
|
if (cached) {
|
|
1247
1514
|
return cached;
|
|
1248
1515
|
}
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1516
|
+
let resolvedImage = adapter.image;
|
|
1517
|
+
const configuredDeps = this.dependencies[adapter.name];
|
|
1518
|
+
const normalizedDeps = configuredDeps ? normalizePackages(configuredDeps) : [];
|
|
1519
|
+
if (normalizedDeps.length > 0) {
|
|
1520
|
+
const hashedCustomTag = getCustomImageTag(adapter.name, normalizedDeps);
|
|
1521
|
+
try {
|
|
1522
|
+
await this.docker.getImage(hashedCustomTag).inspect();
|
|
1523
|
+
resolvedImage = hashedCustomTag;
|
|
1524
|
+
} catch {
|
|
1525
|
+
logger.debug(`[ImageBuilder] Hashed custom image not found for ${adapter.name}: ${hashedCustomTag}`);
|
|
1526
|
+
}
|
|
1527
|
+
}
|
|
1528
|
+
if (resolvedImage === adapter.image) {
|
|
1529
|
+
const legacyCustomTag = `${adapter.image}-custom`;
|
|
1530
|
+
try {
|
|
1531
|
+
await this.docker.getImage(legacyCustomTag).inspect();
|
|
1532
|
+
resolvedImage = legacyCustomTag;
|
|
1533
|
+
} catch {}
|
|
1256
1534
|
}
|
|
1257
1535
|
this.imageCache.set(cacheKey, resolvedImage);
|
|
1258
1536
|
return resolvedImage;
|
|
1259
1537
|
}
|
|
1260
|
-
|
|
1261
|
-
const adapter = this.getAdapter(req.runtime);
|
|
1262
|
-
const timeoutMs = req.timeoutMs ?? this.defaultTimeoutMs;
|
|
1263
|
-
const image = await this.resolveImage(adapter);
|
|
1538
|
+
ensurePool() {
|
|
1264
1539
|
if (!this.pool) {
|
|
1265
1540
|
this.pool = new ContainerPool({
|
|
1266
1541
|
docker: this.docker,
|
|
@@ -1278,7 +1553,14 @@ class DockerIsol8 {
|
|
|
1278
1553
|
}
|
|
1279
1554
|
});
|
|
1280
1555
|
}
|
|
1281
|
-
|
|
1556
|
+
return this.pool;
|
|
1557
|
+
}
|
|
1558
|
+
async executeEphemeral(req, startTime) {
|
|
1559
|
+
const adapter = this.getAdapter(req.runtime);
|
|
1560
|
+
const timeoutMs = req.timeoutMs ?? this.defaultTimeoutMs;
|
|
1561
|
+
const image = await this.resolveImage(adapter);
|
|
1562
|
+
const pool = this.ensurePool();
|
|
1563
|
+
const container = await pool.acquire(image);
|
|
1282
1564
|
let startStats;
|
|
1283
1565
|
if (this.auditLogger) {
|
|
1284
1566
|
try {
|
|
@@ -1292,13 +1574,26 @@ class DockerIsol8 {
|
|
|
1292
1574
|
await startProxy(container, this.networkFilter);
|
|
1293
1575
|
await setupIptables(container);
|
|
1294
1576
|
}
|
|
1295
|
-
const
|
|
1296
|
-
|
|
1297
|
-
|
|
1577
|
+
const canUseInline = !(req.stdin || req.files || req.outputPaths) && (!req.installPackages || req.installPackages.length === 0);
|
|
1578
|
+
let rawCmd;
|
|
1579
|
+
if (canUseInline) {
|
|
1580
|
+
try {
|
|
1581
|
+
rawCmd = adapter.getCommand(req.code);
|
|
1582
|
+
} catch {
|
|
1583
|
+
const ext = req.fileExtension ?? adapter.getFileExtension();
|
|
1584
|
+
const filePath = `${SANDBOX_WORKDIR}/main${ext}`;
|
|
1585
|
+
await writeFileViaExec(container, filePath, req.code);
|
|
1586
|
+
rawCmd = adapter.getCommand(req.code, filePath);
|
|
1587
|
+
}
|
|
1588
|
+
} else {
|
|
1589
|
+
const ext = req.fileExtension ?? adapter.getFileExtension();
|
|
1590
|
+
const filePath = `${SANDBOX_WORKDIR}/main${ext}`;
|
|
1591
|
+
await writeFileViaExec(container, filePath, req.code);
|
|
1592
|
+
rawCmd = adapter.getCommand(req.code, filePath);
|
|
1593
|
+
}
|
|
1298
1594
|
if (req.installPackages?.length) {
|
|
1299
1595
|
await installPackages(container, req.runtime, req.installPackages);
|
|
1300
1596
|
}
|
|
1301
|
-
const rawCmd = adapter.getCommand(req.code, filePath);
|
|
1302
1597
|
const timeoutSec = Math.ceil(timeoutMs / 1000);
|
|
1303
1598
|
let cmd;
|
|
1304
1599
|
if (req.stdin) {
|
|
@@ -1369,7 +1664,7 @@ class DockerIsol8 {
|
|
|
1369
1664
|
if (this.persist) {
|
|
1370
1665
|
logger.debug(`[Persist] Leaving container running for inspection: ${container.id}`);
|
|
1371
1666
|
} else {
|
|
1372
|
-
|
|
1667
|
+
pool.release(container, image).catch((err) => {
|
|
1373
1668
|
logger.debug(`[Pool] release failed: ${err}`);
|
|
1374
1669
|
container.remove({ force: true }).catch(() => {});
|
|
1375
1670
|
});
|
|
@@ -1545,7 +1840,7 @@ class DockerIsol8 {
|
|
|
1545
1840
|
}
|
|
1546
1841
|
if (this.security.seccomp === "custom" && this.security.customProfilePath) {
|
|
1547
1842
|
try {
|
|
1548
|
-
const profile =
|
|
1843
|
+
const profile = readFileSync3(this.security.customProfilePath, "utf-8");
|
|
1549
1844
|
opts.push(`seccomp=${profile}`);
|
|
1550
1845
|
} catch (e) {
|
|
1551
1846
|
logger.error(`Failed to load custom seccomp profile: ${e}`);
|
|
@@ -1564,12 +1859,12 @@ class DockerIsol8 {
|
|
|
1564
1859
|
}
|
|
1565
1860
|
loadDefaultSeccompProfile() {
|
|
1566
1861
|
const devPath = new URL("../../docker/seccomp-profile.json", import.meta.url);
|
|
1567
|
-
if (
|
|
1568
|
-
return
|
|
1862
|
+
if (existsSync4(devPath)) {
|
|
1863
|
+
return readFileSync3(devPath, "utf-8");
|
|
1569
1864
|
}
|
|
1570
1865
|
const prodPath = new URL("./docker/seccomp-profile.json", import.meta.url);
|
|
1571
|
-
if (
|
|
1572
|
-
return
|
|
1866
|
+
if (existsSync4(prodPath)) {
|
|
1867
|
+
return readFileSync3(prodPath, "utf-8");
|
|
1573
1868
|
}
|
|
1574
1869
|
logger.warn("Could not locate default seccomp profile. Running without seccomp filter.");
|
|
1575
1870
|
return null;
|
|
@@ -1752,7 +2047,7 @@ class DockerIsol8 {
|
|
|
1752
2047
|
static async cleanup(docker) {
|
|
1753
2048
|
const dockerInstance = docker ?? new Docker;
|
|
1754
2049
|
const containers = await dockerInstance.listContainers({ all: true });
|
|
1755
|
-
const isol8Containers = containers.filter((c) => c.Image.startsWith("isol8:")
|
|
2050
|
+
const isol8Containers = containers.filter((c) => c.Image.startsWith("isol8:"));
|
|
1756
2051
|
let removed = 0;
|
|
1757
2052
|
let failed = 0;
|
|
1758
2053
|
const errors = [];
|
|
@@ -1769,12 +2064,35 @@ class DockerIsol8 {
|
|
|
1769
2064
|
}
|
|
1770
2065
|
return { removed, failed, errors };
|
|
1771
2066
|
}
|
|
2067
|
+
static async cleanupImages(docker) {
|
|
2068
|
+
const dockerInstance = docker ?? new Docker;
|
|
2069
|
+
const images = await dockerInstance.listImages({ all: true });
|
|
2070
|
+
const isol8Images = images.filter((img) => img.RepoTags?.some((tag) => tag.startsWith("isol8:")));
|
|
2071
|
+
let removed = 0;
|
|
2072
|
+
let failed = 0;
|
|
2073
|
+
const errors = [];
|
|
2074
|
+
for (const imageInfo of isol8Images) {
|
|
2075
|
+
try {
|
|
2076
|
+
const image = dockerInstance.getImage(imageInfo.Id);
|
|
2077
|
+
await image.remove({ force: true });
|
|
2078
|
+
removed++;
|
|
2079
|
+
} catch (err) {
|
|
2080
|
+
failed++;
|
|
2081
|
+
const errorMsg = err instanceof Error ? err.message : String(err);
|
|
2082
|
+
const imageRef = imageInfo.RepoTags?.[0] ?? imageInfo.Id.slice(0, 12);
|
|
2083
|
+
errors.push(`${imageRef}: ${errorMsg}`);
|
|
2084
|
+
}
|
|
2085
|
+
}
|
|
2086
|
+
return { removed, failed, errors };
|
|
2087
|
+
}
|
|
1772
2088
|
}
|
|
1773
2089
|
var SANDBOX_WORKDIR = "/sandbox", MAX_OUTPUT_BYTES, PROXY_PORT = 8118, PROXY_STARTUP_TIMEOUT_MS = 5000, PROXY_POLL_INTERVAL_MS = 100;
|
|
1774
2090
|
var init_docker = __esm(() => {
|
|
1775
2091
|
init_runtime();
|
|
1776
2092
|
init_logger();
|
|
1777
2093
|
init_audit();
|
|
2094
|
+
init_code_fetcher();
|
|
2095
|
+
init_image_builder();
|
|
1778
2096
|
init_pool();
|
|
1779
2097
|
MAX_OUTPUT_BYTES = 1024 * 1024;
|
|
1780
2098
|
});
|
|
@@ -1791,7 +2109,7 @@ class RemoteIsol8 {
|
|
|
1791
2109
|
this.sessionId = options.sessionId;
|
|
1792
2110
|
this.isol8Options = isol8Options;
|
|
1793
2111
|
}
|
|
1794
|
-
async start() {
|
|
2112
|
+
async start(_options) {
|
|
1795
2113
|
const res = await this.fetch("/health");
|
|
1796
2114
|
if (!res.ok) {
|
|
1797
2115
|
throw new Error(`Remote server health check failed: ${res.status}`);
|
|
@@ -1927,10 +2245,34 @@ var DEFAULT_CONFIG = {
|
|
|
1927
2245
|
autoPrune: true,
|
|
1928
2246
|
maxContainerAgeMs: 3600000
|
|
1929
2247
|
},
|
|
2248
|
+
poolStrategy: "fast",
|
|
2249
|
+
poolSize: { clean: 1, dirty: 1 },
|
|
1930
2250
|
dependencies: {},
|
|
1931
2251
|
security: {
|
|
1932
2252
|
seccomp: "strict"
|
|
1933
2253
|
},
|
|
2254
|
+
remoteCode: {
|
|
2255
|
+
enabled: false,
|
|
2256
|
+
allowedSchemes: ["https"],
|
|
2257
|
+
allowedHosts: [],
|
|
2258
|
+
blockedHosts: [
|
|
2259
|
+
"^localhost$",
|
|
2260
|
+
"^127(?:\\.[0-9]{1,3}){3}$",
|
|
2261
|
+
"^\\[::1\\]$",
|
|
2262
|
+
"^::1$",
|
|
2263
|
+
"^10(?:\\.[0-9]{1,3}){3}$",
|
|
2264
|
+
"^172\\.(?:1[6-9]|2[0-9]|3[0-1])(?:\\.[0-9]{1,3}){2}$",
|
|
2265
|
+
"^192\\.168(?:\\.[0-9]{1,3}){2}$",
|
|
2266
|
+
"^169\\.254(?:\\.[0-9]{1,3}){2}$",
|
|
2267
|
+
"^metadata\\.google\\.internal$",
|
|
2268
|
+
"^169\\.254\\.169\\.254$"
|
|
2269
|
+
],
|
|
2270
|
+
maxCodeSize: 10 * 1024 * 1024,
|
|
2271
|
+
fetchTimeoutMs: 30000,
|
|
2272
|
+
requireHash: false,
|
|
2273
|
+
enableCache: true,
|
|
2274
|
+
cacheTtl: 3600
|
|
2275
|
+
},
|
|
1934
2276
|
audit: {
|
|
1935
2277
|
enabled: false,
|
|
1936
2278
|
destination: "filesystem",
|
|
@@ -1972,6 +2314,8 @@ function mergeConfig(defaults, overrides) {
|
|
|
1972
2314
|
...defaults.cleanup,
|
|
1973
2315
|
...overrides.cleanup
|
|
1974
2316
|
},
|
|
2317
|
+
poolStrategy: overrides.poolStrategy ?? defaults.poolStrategy,
|
|
2318
|
+
poolSize: overrides.poolSize ?? defaults.poolSize,
|
|
1975
2319
|
dependencies: {
|
|
1976
2320
|
...defaults.dependencies,
|
|
1977
2321
|
...overrides.dependencies
|
|
@@ -1980,6 +2324,13 @@ function mergeConfig(defaults, overrides) {
|
|
|
1980
2324
|
seccomp: overrides.security?.seccomp ?? defaults.security.seccomp,
|
|
1981
2325
|
customProfilePath: overrides.security?.customProfilePath ?? defaults.security.customProfilePath
|
|
1982
2326
|
},
|
|
2327
|
+
remoteCode: {
|
|
2328
|
+
...defaults.remoteCode,
|
|
2329
|
+
...overrides.remoteCode,
|
|
2330
|
+
allowedSchemes: overrides.remoteCode?.allowedSchemes ?? defaults.remoteCode.allowedSchemes,
|
|
2331
|
+
allowedHosts: overrides.remoteCode?.allowedHosts ?? defaults.remoteCode.allowedHosts,
|
|
2332
|
+
blockedHosts: overrides.remoteCode?.blockedHosts ?? defaults.remoteCode.blockedHosts
|
|
2333
|
+
},
|
|
1983
2334
|
audit: {
|
|
1984
2335
|
...defaults.audit,
|
|
1985
2336
|
...overrides.audit
|
|
@@ -1998,7 +2349,7 @@ init_logger();
|
|
|
1998
2349
|
// package.json
|
|
1999
2350
|
var package_default = {
|
|
2000
2351
|
name: "isol8",
|
|
2001
|
-
version: "0.
|
|
2352
|
+
version: "0.11.0",
|
|
2002
2353
|
description: "Secure code execution engine for AI agents",
|
|
2003
2354
|
author: "Illusion47586",
|
|
2004
2355
|
license: "MIT",
|
|
@@ -2043,6 +2394,8 @@ var package_default = {
|
|
|
2043
2394
|
"lint:check": "ultracite check",
|
|
2044
2395
|
"lint:fix": "ultracite fix",
|
|
2045
2396
|
bench: "bunx tsx benchmarks/spawn.ts",
|
|
2397
|
+
"bench:tti": "bunx tsx benchmarks/tti.ts",
|
|
2398
|
+
"bench:tti:pool": "bunx tsx benchmarks/tti.ts --warm-pool --iterations 5",
|
|
2046
2399
|
"bench:pool": "bunx tsx benchmarks/spawn-pool.ts",
|
|
2047
2400
|
"bench:detailed": "bunx tsx benchmarks/spawn-detailed.ts",
|
|
2048
2401
|
"bench:cli": "bun run tests/production/bench-cli.ts",
|
|
@@ -2153,7 +2506,12 @@ async function createServer(options) {
|
|
|
2153
2506
|
app.post("/execute", async (c) => {
|
|
2154
2507
|
const body = await c.req.json();
|
|
2155
2508
|
logger.debug(`[Server] POST /execute runtime=${body.request.runtime} sessionId=${body.sessionId ?? "ephemeral"}`);
|
|
2156
|
-
logger.debug(`[Server] Code
|
|
2509
|
+
logger.debug(`[Server] Code source: ${body.request.codeUrl ? `url=${body.request.codeUrl}` : `inline (${body.request.code?.length ?? 0} chars)`}`);
|
|
2510
|
+
const {
|
|
2511
|
+
poolStrategy: _ignoredPoolStrategy,
|
|
2512
|
+
poolSize: _ignoredPoolSize,
|
|
2513
|
+
...requestOptions
|
|
2514
|
+
} = body.options ?? {};
|
|
2157
2515
|
const engineOptions = {
|
|
2158
2516
|
network: config.defaults.network,
|
|
2159
2517
|
memoryLimit: config.defaults.memoryLimit,
|
|
@@ -2161,7 +2519,11 @@ async function createServer(options) {
|
|
|
2161
2519
|
timeoutMs: config.defaults.timeoutMs,
|
|
2162
2520
|
sandboxSize: config.defaults.sandboxSize,
|
|
2163
2521
|
tmpSize: config.defaults.tmpSize,
|
|
2164
|
-
|
|
2522
|
+
poolStrategy: config.poolStrategy,
|
|
2523
|
+
poolSize: config.poolSize,
|
|
2524
|
+
dependencies: config.dependencies,
|
|
2525
|
+
remoteCode: config.remoteCode,
|
|
2526
|
+
...requestOptions,
|
|
2165
2527
|
mode: body.sessionId ? "persistent" : "ephemeral",
|
|
2166
2528
|
audit: config.audit
|
|
2167
2529
|
};
|
|
@@ -2214,7 +2576,12 @@ async function createServer(options) {
|
|
|
2214
2576
|
app.post("/execute/stream", async (c) => {
|
|
2215
2577
|
const body = await c.req.json();
|
|
2216
2578
|
logger.debug(`[Server] POST /execute/stream runtime=${body.request.runtime}`);
|
|
2217
|
-
logger.debug(`[Server] Code
|
|
2579
|
+
logger.debug(`[Server] Code source: ${body.request.codeUrl ? `url=${body.request.codeUrl}` : `inline (${body.request.code?.length ?? 0} chars)`}`);
|
|
2580
|
+
const {
|
|
2581
|
+
poolStrategy: _ignoredPoolStrategy,
|
|
2582
|
+
poolSize: _ignoredPoolSize,
|
|
2583
|
+
...requestOptions
|
|
2584
|
+
} = body.options ?? {};
|
|
2218
2585
|
const engineOptions = {
|
|
2219
2586
|
network: config.defaults.network,
|
|
2220
2587
|
memoryLimit: config.defaults.memoryLimit,
|
|
@@ -2222,7 +2589,11 @@ async function createServer(options) {
|
|
|
2222
2589
|
timeoutMs: config.defaults.timeoutMs,
|
|
2223
2590
|
sandboxSize: config.defaults.sandboxSize,
|
|
2224
2591
|
tmpSize: config.defaults.tmpSize,
|
|
2225
|
-
|
|
2592
|
+
poolStrategy: config.poolStrategy,
|
|
2593
|
+
poolSize: config.poolSize,
|
|
2594
|
+
dependencies: config.dependencies,
|
|
2595
|
+
remoteCode: config.remoteCode,
|
|
2596
|
+
...requestOptions,
|
|
2226
2597
|
mode: "ephemeral"
|
|
2227
2598
|
};
|
|
2228
2599
|
const engine = new DockerIsol82(engineOptions, config.maxConcurrent);
|
|
@@ -2346,4 +2717,4 @@ export {
|
|
|
2346
2717
|
BunAdapter
|
|
2347
2718
|
};
|
|
2348
2719
|
|
|
2349
|
-
//# debugId=
|
|
2720
|
+
//# debugId=C48E982CAC86EB5964756E2164756E21
|