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/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
- if (this.replenishing.has(image)) {
566
- return;
567
- }
568
- const pool = this.pools.get(image);
569
- const currentSize = pool ? this.poolStrategy === "fast" ? pool.clean.length : pool.clean?.length ?? 0 : 0;
570
- const targetSize = this.poolStrategy === "fast" ? this.cleanPoolSize : this.cleanPoolSize;
571
- if (currentSize >= targetSize) {
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.replenishing.add(image);
575
- const promise = this.createContainer(image).then((container) => {
576
- const p = this.pools.get(image);
577
- if (!p) {
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
- if (this.poolStrategy === "fast") {
582
- if (p.clean.length < this.cleanPoolSize) {
583
- p.clean.push({ container, createdAt: Date.now() });
584
- } else {
585
- container.remove({ force: true }).catch(() => {});
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
- if (!p.clean) {
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
- }).catch((err) => {
598
- logger.error(`[Pool] Error during replenishment for ${image}:`, err);
599
- }).finally(() => {
600
- this.replenishing.delete(image);
601
- this.pendingReplenishments.delete(promise);
602
- });
603
- this.pendingReplenishments.add(promise);
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 existsSync3, readFileSync as readFileSync2 } from "node:fs";
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 result = this.mode === "persistent" ? await this.executePersistent(req, startTime) : await this.executeEphemeral(req, startTime);
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 adapter = this.getAdapter(req.runtime);
1178
- const timeoutMs = req.timeoutMs ?? this.defaultTimeoutMs;
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 = req.fileExtension ?? adapter.getFileExtension();
1462
+ const ext = request.fileExtension ?? adapter.getFileExtension();
1196
1463
  const filePath = `${SANDBOX_WORKDIR}/main${ext}`;
1197
- await writeFileViaExec(container, filePath, req.code);
1198
- if (req.installPackages?.length) {
1199
- await installPackages(container, req.runtime, req.installPackages);
1464
+ await writeFileViaExec(container, filePath, request.code);
1465
+ if (request.installPackages?.length) {
1466
+ await installPackages(container, request.runtime, request.installPackages);
1200
1467
  }
1201
- if (req.files) {
1202
- for (const [fPath, fContent] of Object.entries(req.files)) {
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(req.code, filePath);
1473
+ const rawCmd = adapter.getCommand(request.code, filePath);
1207
1474
  const timeoutSec = Math.ceil(timeoutMs / 1000);
1208
1475
  let cmd;
1209
- if (req.stdin) {
1476
+ if (request.stdin) {
1210
1477
  const stdinPath = `${SANDBOX_WORKDIR}/_stdin`;
1211
- await writeFileViaExec(container, stdinPath, req.stdin);
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(req.env),
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
- const customTag = `${adapter.image}-custom`;
1250
- let resolvedImage;
1251
- try {
1252
- await this.docker.getImage(customTag).inspect();
1253
- resolvedImage = customTag;
1254
- } catch {
1255
- resolvedImage = adapter.image;
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
- async executeEphemeral(req, startTime) {
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
- const container = await this.pool.acquire(image);
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 ext = req.fileExtension ?? adapter.getFileExtension();
1296
- const filePath = `${SANDBOX_WORKDIR}/main${ext}`;
1297
- await writeFileViaExec(container, filePath, req.code);
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
- this.pool.release(container, image).catch((err) => {
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 = readFileSync2(this.security.customProfilePath, "utf-8");
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 (existsSync3(devPath)) {
1568
- return readFileSync2(devPath, "utf-8");
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 (existsSync3(prodPath)) {
1572
- return readFileSync2(prodPath, "utf-8");
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:") || c.Image.startsWith("isol8-custom:"));
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.10.2",
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 length: ${body.request.code.length} chars`);
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
- ...body.options,
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 length: ${body.request.code.length} chars`);
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
- ...body.options,
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=B5951587CB2FCE7364756E2164756E21
2720
+ //# debugId=C48E982CAC86EB5964756E2164756E21