@isol8/core 0.16.0 → 0.18.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,4 +1,3 @@
1
- import { createRequire } from "node:module";
2
1
  var __defProp = Object.defineProperty;
3
2
  var __returnValue = (v) => v;
4
3
  function __exportSetter(name, newValue) {
@@ -14,7 +13,6 @@ var __export = (target, all) => {
14
13
  });
15
14
  };
16
15
  var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
17
- var __require = /* @__PURE__ */ createRequire(import.meta.url);
18
16
 
19
17
  // src/runtime/adapter.ts
20
18
  var adapters, extensionMap, RuntimeRegistry;
@@ -200,11 +198,13 @@ var exports_utils = {};
200
198
  __export(exports_utils, {
201
199
  validatePackageName: () => validatePackageName,
202
200
  truncateOutput: () => truncateOutput,
201
+ resolveWorkdir: () => resolveWorkdir,
203
202
  parseMemoryLimit: () => parseMemoryLimit,
204
203
  maskSecrets: () => maskSecrets,
205
204
  extractFromTar: () => extractFromTar,
206
205
  createTarBuffer: () => createTarBuffer
207
206
  });
207
+ import path from "node:path";
208
208
  function parseMemoryLimit(limit) {
209
209
  const match = limit.match(/^(\d+(?:\.\d+)?)\s*([kmgt]?)b?$/i);
210
210
  if (!match) {
@@ -299,17 +299,24 @@ function validatePackageName(name) {
299
299
  }
300
300
  return name;
301
301
  }
302
+ function resolveWorkdir(workdir, sandboxRoot = "/sandbox") {
303
+ const resolved = path.posix.resolve(sandboxRoot, workdir);
304
+ if (resolved !== sandboxRoot && !resolved.startsWith(`${sandboxRoot}/`)) {
305
+ throw new Error("Working directory must be inside /sandbox");
306
+ }
307
+ return resolved;
308
+ }
309
+ var init_utils = () => {};
302
310
 
303
311
  // src/engine/image-builder.ts
304
312
  var exports_image_builder = {};
305
313
  __export(exports_image_builder, {
306
314
  normalizePackages: () => normalizePackages,
307
315
  imageExists: () => imageExists,
308
- getCustomImageTag: () => getCustomImageTag,
309
316
  ensureImages: () => ensureImages,
310
- buildCustomImages: () => buildCustomImages,
311
317
  buildCustomImage: () => buildCustomImage,
312
- buildBaseImages: () => buildBaseImages
318
+ buildBaseImages: () => buildBaseImages,
319
+ LABELS: () => LABELS
313
320
  });
314
321
  import { createHash as createHash2 } from "node:crypto";
315
322
  import { existsSync as existsSync3, readFileSync as readFileSync2, statSync as statSync2 } from "node:fs";
@@ -350,23 +357,21 @@ function computeDockerDirHash() {
350
357
  }
351
358
  return hash.digest("hex");
352
359
  }
353
- function computeDepsHash(runtime, packages) {
360
+ function computeDepsHash(runtime, packages, setupScript) {
354
361
  const hash = createHash2("sha256");
355
362
  hash.update(runtime);
356
363
  for (const pkg of [...packages].sort()) {
357
364
  hash.update(pkg);
358
365
  }
366
+ if (setupScript) {
367
+ hash.update("setup:");
368
+ hash.update(setupScript);
369
+ }
359
370
  return hash.digest("hex");
360
371
  }
361
372
  function normalizePackages(packages) {
362
373
  return [...new Set(packages.map((pkg) => pkg.trim()).filter(Boolean))].sort();
363
374
  }
364
- function getCustomImageTag(runtime, packages) {
365
- const normalizedPackages = normalizePackages(packages);
366
- const depsHash = computeDepsHash(runtime, normalizedPackages);
367
- const shortHash = depsHash.slice(0, 12);
368
- return `isol8:${runtime}-custom-${shortHash}`;
369
- }
370
375
  async function getImageLabels(docker, imageName) {
371
376
  try {
372
377
  const image = docker.getImage(imageName);
@@ -439,33 +444,9 @@ async function buildBaseImages(docker, onProgress, force = false, onlyRuntimes)
439
444
  }
440
445
  }
441
446
  }
442
- async function buildCustomImages(docker, config, onProgress, force = false) {
443
- const deps = config.dependencies;
444
- const python = deps.python ? normalizePackages(deps.python) : [];
445
- const node = deps.node ? normalizePackages(deps.node) : [];
446
- const bun = deps.bun ? normalizePackages(deps.bun) : [];
447
- const deno = deps.deno ? normalizePackages(deps.deno) : [];
448
- const bash = deps.bash ? normalizePackages(deps.bash) : [];
449
- if (python.length) {
450
- await buildCustomImage(docker, "python", python, onProgress, force);
451
- }
452
- if (node.length) {
453
- await buildCustomImage(docker, "node", node, onProgress, force);
454
- }
455
- if (bun.length) {
456
- await buildCustomImage(docker, "bun", bun, onProgress, force);
457
- }
458
- if (deno.length) {
459
- await buildCustomImage(docker, "deno", deno, onProgress, force);
460
- }
461
- if (bash.length) {
462
- await buildCustomImage(docker, "bash", bash, onProgress, force);
463
- }
464
- }
465
- async function buildCustomImage(docker, runtime, packages, onProgress, force = false) {
447
+ async function buildCustomImage(docker, runtime, packages, tag, onProgress, force = false, setupScript) {
466
448
  const normalizedPackages = normalizePackages(packages);
467
- const tag = getCustomImageTag(runtime, normalizedPackages);
468
- const depsHash = computeDepsHash(runtime, normalizedPackages);
449
+ const depsHash = computeDepsHash(runtime, normalizedPackages, setupScript);
469
450
  logger.debug(`[ImageBuilder] ${runtime} custom deps hash: ${depsHash.slice(0, 16)}...`);
470
451
  if (!force) {
471
452
  const labels = await getImageLabels(docker, tag);
@@ -491,7 +472,7 @@ async function buildCustomImage(docker, runtime, packages, onProgress, force = f
491
472
  let installCmd;
492
473
  switch (runtime) {
493
474
  case "python":
494
- installCmd = `RUN pip install --no-cache-dir ${normalizedPackages.join(" ")}`;
475
+ installCmd = `RUN pip install --break-system-packages --no-cache-dir ${normalizedPackages.join(" ")}`;
495
476
  break;
496
477
  case "node":
497
478
  installCmd = `RUN npm install -g ${normalizedPackages.join(" ")}`;
@@ -509,27 +490,50 @@ async function buildCustomImage(docker, runtime, packages, onProgress, force = f
509
490
  default:
510
491
  throw new Error(`Unknown runtime: ${runtime}`);
511
492
  }
493
+ let setupLines = "";
494
+ if (setupScript) {
495
+ const escaped = setupScript.replace(/\\/g, "\\\\").replace(/'/g, "'\\''");
496
+ setupLines = [
497
+ `RUN printf '%s\\n' '${escaped}' > /sandbox/.isol8-setup.sh`,
498
+ "RUN chmod +x /sandbox/.isol8-setup.sh"
499
+ ].join(`
500
+ `);
501
+ }
512
502
  const dockerfileContent = `FROM isol8:${runtime}
513
503
  ${installCmd}
504
+ ${setupLines}
514
505
  `;
515
- const { createTarBuffer: createTarBuffer2, validatePackageName: validatePackageName2 } = await Promise.resolve().then(() => exports_utils);
516
- const { Readable } = await import("node:stream");
506
+ const { createTarBuffer: createTarBuffer2, validatePackageName: validatePackageName2 } = await Promise.resolve().then(() => (init_utils(), exports_utils));
517
507
  normalizedPackages.forEach(validatePackageName2);
518
508
  const tarBuffer = createTarBuffer2("Dockerfile", dockerfileContent);
519
- const stream = await docker.buildImage(Readable.from(tarBuffer), {
509
+ const imageLabels = {
510
+ [LABELS.depsHash]: depsHash,
511
+ [LABELS.runtime]: runtime.toString(),
512
+ [LABELS.dependencies]: normalizedPackages.join(",")
513
+ };
514
+ if (setupScript) {
515
+ imageLabels[LABELS.setupScript] = setupScript;
516
+ }
517
+ const stream = await docker.buildImage(tarBuffer, {
520
518
  t: tag,
521
519
  dockerfile: "Dockerfile",
522
- labels: {
523
- [LABELS.depsHash]: depsHash
524
- }
520
+ labels: imageLabels
525
521
  });
526
522
  await new Promise((resolve2, reject) => {
527
- docker.modem.followProgress(stream, (err) => {
523
+ docker.modem.followProgress(stream, (err, res) => {
528
524
  if (err) {
529
525
  reject(err);
526
+ } else if (res && res.length > 0 && res.at(-1).error) {
527
+ reject(new Error(res.at(-1).error));
530
528
  } else {
531
529
  resolve2();
532
530
  }
531
+ }, (event) => {
532
+ if (event.stream) {
533
+ process.stdout.write(event.stream);
534
+ } else if (event.error) {
535
+ console.error(event.error);
536
+ }
533
537
  });
534
538
  });
535
539
  if (oldImageId) {
@@ -564,7 +568,10 @@ var init_image_builder = __esm(() => {
564
568
  DOCKERFILE_DIR = resolveDockerDir();
565
569
  LABELS = {
566
570
  dockerHash: "org.isol8.build.hash",
567
- depsHash: "org.isol8.deps.hash"
571
+ depsHash: "org.isol8.deps.hash",
572
+ runtime: "org.isol8.runtime",
573
+ dependencies: "org.isol8.dependencies",
574
+ setupScript: "org.isol8.setup"
568
575
  };
569
576
  DOCKER_BUILD_FILES = ["Dockerfile", "proxy.sh", "proxy-handler.sh"];
570
577
  });
@@ -776,6 +783,22 @@ class RemoteIsol8 {
776
783
  const body = await res.json();
777
784
  return Buffer.from(body.content, "base64");
778
785
  }
786
+ async listSessions() {
787
+ const res = await this.fetch("/sessions");
788
+ if (!res.ok) {
789
+ const body2 = await res.json().catch(() => ({}));
790
+ throw new Error(`Failed to list sessions: ${body2.error ?? res.statusText}`);
791
+ }
792
+ const body = await res.json();
793
+ return body.sessions;
794
+ }
795
+ async deleteSession(sessionId) {
796
+ const res = await this.fetch(`/session/${sessionId}`, { method: "DELETE" });
797
+ if (!res.ok) {
798
+ const body = await res.json().catch(() => ({}));
799
+ throw new Error(`Failed to delete session: ${body.error ?? res.statusText}`);
800
+ }
801
+ }
779
802
  async fetch(path, init) {
780
803
  return globalThis.fetch(`${this.host}${path}`, {
781
804
  ...init,
@@ -812,7 +835,6 @@ var DEFAULT_CONFIG = {
812
835
  },
813
836
  poolStrategy: "fast",
814
837
  poolSize: { clean: 1, dirty: 1 },
815
- dependencies: {},
816
838
  security: {
817
839
  seccomp: "strict"
818
840
  },
@@ -853,6 +875,7 @@ var DEFAULT_CONFIG = {
853
875
  defaultTtlMs: 86400000,
854
876
  cleanupIntervalMs: 3600000
855
877
  },
878
+ prebuiltImages: [],
856
879
  debug: false
857
880
  };
858
881
  function loadConfig(cwd) {
@@ -887,10 +910,6 @@ function mergeConfig(defaults, overrides) {
887
910
  },
888
911
  poolStrategy: overrides.poolStrategy ?? defaults.poolStrategy,
889
912
  poolSize: overrides.poolSize ?? defaults.poolSize,
890
- dependencies: {
891
- ...defaults.dependencies,
892
- ...overrides.dependencies
893
- },
894
913
  security: {
895
914
  seccomp: overrides.security?.seccomp ?? defaults.security.seccomp,
896
915
  customProfilePath: overrides.security?.customProfilePath ?? defaults.security.customProfilePath
@@ -910,6 +929,7 @@ function mergeConfig(defaults, overrides) {
910
929
  ...defaults.auth,
911
930
  ...overrides.auth
912
931
  },
932
+ prebuiltImages: overrides.prebuiltImages ?? defaults.prebuiltImages,
913
933
  debug: overrides.debug ?? defaults.debug
914
934
  };
915
935
  }
@@ -1329,11 +1349,9 @@ var EMBEDDED_DEFAULT_SECCOMP_PROFILE = JSON.stringify({
1329
1349
  ]
1330
1350
  });
1331
1351
 
1332
- // src/engine/docker.ts
1333
- init_image_builder();
1334
-
1335
1352
  // src/engine/managers/execution-manager.ts
1336
1353
  init_logger();
1354
+ init_utils();
1337
1355
  import { PassThrough } from "node:stream";
1338
1356
 
1339
1357
  class ExecutionManager {
@@ -1437,6 +1455,63 @@ class ExecutionManager {
1437
1455
  stream.on("error", reject);
1438
1456
  });
1439
1457
  }
1458
+ async runSetupScript(container, script, timeoutMs, volumeManager) {
1459
+ const scriptPath = "/sandbox/.isol8-setup.sh";
1460
+ await volumeManager.writeFileViaExec(container, scriptPath, script);
1461
+ const chmodExec = await container.exec({
1462
+ Cmd: ["chmod", "+x", scriptPath],
1463
+ User: "sandbox"
1464
+ });
1465
+ await chmodExec.start({ Detach: true });
1466
+ let chmodInfo = await chmodExec.inspect();
1467
+ while (chmodInfo.Running) {
1468
+ await new Promise((r) => setTimeout(r, 5));
1469
+ chmodInfo = await chmodExec.inspect();
1470
+ }
1471
+ const timeoutSec = Math.max(1, Math.ceil(timeoutMs / 1000));
1472
+ const cmd = this.wrapWithTimeout(["bash", scriptPath], timeoutSec);
1473
+ logger.debug(`Running setup script: ${JSON.stringify(cmd)}`);
1474
+ const env = [
1475
+ "PATH=/sandbox/.local/bin:/sandbox/.npm-global/bin:/sandbox/.bun-global/bin:/usr/local/bin:/usr/bin:/bin:/usr/local/sbin:/usr/sbin:/sbin"
1476
+ ];
1477
+ const exec = await container.exec({
1478
+ Cmd: cmd,
1479
+ AttachStdout: true,
1480
+ AttachStderr: true,
1481
+ Env: env,
1482
+ WorkingDir: "/sandbox",
1483
+ User: "sandbox"
1484
+ });
1485
+ const stream = await exec.start({ Detach: false, Tty: false });
1486
+ return new Promise((resolve2, reject) => {
1487
+ let stderr = "";
1488
+ const stdoutStream = new PassThrough;
1489
+ const stderrStream = new PassThrough;
1490
+ container.modem.demuxStream(stream, stdoutStream, stderrStream);
1491
+ stderrStream.on("data", (chunk) => {
1492
+ const text = chunk.toString();
1493
+ stderr += text;
1494
+ logger.debug(`[setup:stderr] ${text.trimEnd()}`);
1495
+ });
1496
+ stdoutStream.on("data", (chunk) => {
1497
+ const text = chunk.toString();
1498
+ logger.debug(`[setup:stdout] ${text.trimEnd()}`);
1499
+ });
1500
+ stream.on("end", async () => {
1501
+ try {
1502
+ const info = await exec.inspect();
1503
+ if (info.ExitCode !== 0) {
1504
+ reject(new Error(`Setup script failed (exit code ${info.ExitCode}): ${stderr}`));
1505
+ } else {
1506
+ resolve2();
1507
+ }
1508
+ } catch (err) {
1509
+ reject(err);
1510
+ }
1511
+ });
1512
+ stream.on("error", reject);
1513
+ });
1514
+ }
1440
1515
  async* streamExecOutput(stream, exec, container, timeoutMs) {
1441
1516
  const queue = [];
1442
1517
  let resolve2 = null;
@@ -1691,6 +1766,7 @@ class NetworkManager {
1691
1766
  }
1692
1767
  }
1693
1768
  // src/engine/managers/volume-manager.ts
1769
+ init_utils();
1694
1770
  import { PassThrough as PassThrough2 } from "node:stream";
1695
1771
 
1696
1772
  class VolumeManager {
@@ -1771,13 +1847,13 @@ class VolumeManager {
1771
1847
  const b64Output = Buffer.concat(chunks).toString("utf-8").trim();
1772
1848
  return Buffer.from(b64Output, "base64");
1773
1849
  }
1774
- async getFileFromContainer(container, path) {
1775
- const stream = await container.getArchive({ path });
1850
+ async getFileFromContainer(container, path2) {
1851
+ const stream = await container.getArchive({ path: path2 });
1776
1852
  const chunks = [];
1777
1853
  for await (const chunk of stream) {
1778
1854
  chunks.push(chunk);
1779
1855
  }
1780
- return extractFromTar(Buffer.concat(chunks), path);
1856
+ return extractFromTar(Buffer.concat(chunks), path2);
1781
1857
  }
1782
1858
  async retrieveFiles(container, paths) {
1783
1859
  const files = {};
@@ -1789,19 +1865,19 @@ class VolumeManager {
1789
1865
  }
1790
1866
  return files;
1791
1867
  }
1792
- async putFile(container, path, content) {
1868
+ async putFile(container, path2, content) {
1793
1869
  if (this.readonlyRootFs) {
1794
- await this.writeFileViaExec(container, path, content);
1870
+ await this.writeFileViaExec(container, path2, content);
1795
1871
  } else {
1796
- const tar = createTarBuffer(path, content);
1872
+ const tar = createTarBuffer(path2, content);
1797
1873
  await container.putArchive(tar, { path: "/" });
1798
1874
  }
1799
1875
  }
1800
- async getFile(container, path) {
1876
+ async getFile(container, path2) {
1801
1877
  if (this.readonlyRootFs) {
1802
- return this.readFileViaExec(container, path);
1878
+ return this.readFileViaExec(container, path2);
1803
1879
  }
1804
- return this.getFileFromContainer(container, path);
1880
+ return this.getFileFromContainer(container, path2);
1805
1881
  }
1806
1882
  }
1807
1883
  // src/engine/pool.ts
@@ -2083,6 +2159,7 @@ function calculateResourceDelta(before, after) {
2083
2159
  }
2084
2160
 
2085
2161
  // src/engine/docker.ts
2162
+ init_utils();
2086
2163
  var SANDBOX_WORKDIR = "/sandbox";
2087
2164
  var MAX_OUTPUT_BYTES = 1024 * 1024;
2088
2165
 
@@ -2107,7 +2184,6 @@ class DockerIsol8 {
2107
2184
  logNetwork;
2108
2185
  poolStrategy;
2109
2186
  poolSize;
2110
- dependencies;
2111
2187
  auditLogger;
2112
2188
  remoteCodePolicy;
2113
2189
  networkManager;
@@ -2157,7 +2233,6 @@ class DockerIsol8 {
2157
2233
  this.logNetwork = options.logNetwork ?? false;
2158
2234
  this.poolStrategy = options.poolStrategy ?? "fast";
2159
2235
  this.poolSize = options.poolSize ?? { clean: 1, dirty: 1 };
2160
- this.dependencies = options.dependencies ?? {};
2161
2236
  this.remoteCodePolicy = options.remoteCode ?? {
2162
2237
  enabled: false,
2163
2238
  allowedSchemes: ["https"],
@@ -2201,7 +2276,8 @@ class DockerIsol8 {
2201
2276
  const adapters2 = typeof prewarm === "object" && prewarm.runtimes?.length ? prewarm.runtimes.map((runtime) => RuntimeRegistry.get(runtime)) : RuntimeRegistry.list();
2202
2277
  for (const adapter of adapters2) {
2203
2278
  try {
2204
- images.add(await this.resolveImage(adapter));
2279
+ const resolved = await this.resolveImage(adapter);
2280
+ images.add(resolved.image);
2205
2281
  } catch (err) {
2206
2282
  logger.debug(`[Pool] Pre-warm image resolution failed for ${adapter.name}: ${err}`);
2207
2283
  }
@@ -2358,17 +2434,17 @@ class DockerIsol8 {
2358
2434
  } catch {}
2359
2435
  return logs;
2360
2436
  }
2361
- async putFile(path, content) {
2437
+ async putFile(path2, content) {
2362
2438
  if (!this.container) {
2363
2439
  throw new Error("No active container. Call execute() first in persistent mode.");
2364
2440
  }
2365
- await this.volumeManager.putFile(this.container, path, content);
2441
+ await this.volumeManager.putFile(this.container, path2, content);
2366
2442
  }
2367
- async getFile(path) {
2443
+ async getFile(path2) {
2368
2444
  if (!this.container) {
2369
2445
  throw new Error("No active container. Call execute() first in persistent mode.");
2370
2446
  }
2371
- return this.volumeManager.getFile(this.container, path);
2447
+ return this.volumeManager.getFile(this.container, path2);
2372
2448
  }
2373
2449
  get containerId() {
2374
2450
  return this.container?.id ?? null;
@@ -2379,7 +2455,9 @@ class DockerIsol8 {
2379
2455
  const request = await this.resolveExecutionRequest(req);
2380
2456
  const adapter = this.getAdapter(request.runtime);
2381
2457
  const timeoutMs = request.timeoutMs ?? this.defaultTimeoutMs;
2382
- const image = await this.resolveImage(adapter);
2458
+ const resolved = await this.resolveImage(adapter, request.installPackages);
2459
+ const image = resolved.image;
2460
+ const execWorkdir = request.workdir ? resolveWorkdir(request.workdir) : SANDBOX_WORKDIR;
2383
2461
  const container = await this.docker.createContainer({
2384
2462
  Image: image,
2385
2463
  Cmd: ["sleep", "infinity"],
@@ -2396,8 +2474,14 @@ class DockerIsol8 {
2396
2474
  const ext = request.fileExtension ?? adapter.getFileExtension();
2397
2475
  const filePath = `${SANDBOX_WORKDIR}/main${ext}`;
2398
2476
  await this.volumeManager.writeFileViaExec(container, filePath, request.code);
2399
- if (request.installPackages?.length) {
2400
- await this.executionManager.installPackages(container, request.runtime, request.installPackages, timeoutMs);
2477
+ if (resolved.remainingPackages.length > 0) {
2478
+ await this.executionManager.installPackages(container, request.runtime, resolved.remainingPackages, timeoutMs);
2479
+ }
2480
+ if (resolved.imageSetupScript) {
2481
+ await this.executionManager.runSetupScript(container, resolved.imageSetupScript, timeoutMs, this.volumeManager);
2482
+ }
2483
+ if (request.setupScript) {
2484
+ await this.executionManager.runSetupScript(container, request.setupScript, timeoutMs, this.volumeManager);
2401
2485
  }
2402
2486
  if (request.files) {
2403
2487
  for (const [fPath, fContent] of Object.entries(request.files)) {
@@ -2420,7 +2504,7 @@ class DockerIsol8 {
2420
2504
  Env: this.executionManager.buildEnv(request.env, this.networkManager.proxyPort, this.network, this.networkFilter),
2421
2505
  AttachStdout: true,
2422
2506
  AttachStderr: true,
2423
- WorkingDir: SANDBOX_WORKDIR,
2507
+ WorkingDir: execWorkdir,
2424
2508
  User: "sandbox"
2425
2509
  });
2426
2510
  const execStream = await exec.start({ Tty: false });
@@ -2438,55 +2522,90 @@ class DockerIsol8 {
2438
2522
  this.semaphore.release();
2439
2523
  }
2440
2524
  }
2441
- async resolveImage(adapter) {
2525
+ async resolveImage(adapter, requestedPackages) {
2442
2526
  if (this.overrideImage) {
2443
- return this.overrideImage;
2527
+ let imageSetupScript2;
2528
+ try {
2529
+ const { LABELS: LABELS2 } = await Promise.resolve().then(() => (init_image_builder(), exports_image_builder));
2530
+ const inspect = await this.docker.getImage(this.overrideImage).inspect();
2531
+ const labels = inspect.Config?.Labels ?? {};
2532
+ imageSetupScript2 = labels[LABELS2.setupScript] || undefined;
2533
+ } catch {}
2534
+ return {
2535
+ image: this.overrideImage,
2536
+ remainingPackages: requestedPackages ?? [],
2537
+ imageSetupScript: imageSetupScript2
2538
+ };
2444
2539
  }
2445
- const cacheKey = adapter.image;
2540
+ const cacheKey = `${adapter.name}:${(requestedPackages ?? []).join(",")}`;
2446
2541
  const cached = this.imageCache.get(cacheKey);
2447
2542
  if (cached) {
2448
- return cached;
2449
- }
2450
- let resolvedImage = adapter.image;
2451
- const configuredDeps = this.dependencies[adapter.name];
2452
- const normalizedDeps = configuredDeps ? normalizePackages(configuredDeps) : [];
2453
- if (normalizedDeps.length > 0) {
2454
- const hashedCustomTag = getCustomImageTag(adapter.name, normalizedDeps);
2543
+ let imageSetupScript2;
2455
2544
  try {
2456
- await this.docker.getImage(hashedCustomTag).inspect();
2457
- resolvedImage = hashedCustomTag;
2458
- } catch {
2459
- logger.debug(`[ImageBuilder] Hashed custom image not found for ${adapter.name}: ${hashedCustomTag}`);
2545
+ const { LABELS: LABELS2 } = await Promise.resolve().then(() => (init_image_builder(), exports_image_builder));
2546
+ const inspect = await this.docker.getImage(cached).inspect();
2547
+ const labels = inspect.Config?.Labels ?? {};
2548
+ imageSetupScript2 = labels[LABELS2.setupScript] || undefined;
2549
+ } catch {}
2550
+ return { image: cached, remainingPackages: [], imageSetupScript: imageSetupScript2 };
2551
+ }
2552
+ const baseImage = adapter.image;
2553
+ let bestImage = baseImage;
2554
+ let remainingPackages = requestedPackages ?? [];
2555
+ let imageSetupScript;
2556
+ if (requestedPackages && requestedPackages.length > 0) {
2557
+ const { LABELS: LABELS2, normalizePackages: normalizePackages2 } = await Promise.resolve().then(() => (init_image_builder(), exports_image_builder));
2558
+ const normalizedReq = normalizePackages2(requestedPackages);
2559
+ const images = await this.docker.listImages({
2560
+ filters: {
2561
+ label: [`${LABELS2.runtime}=${adapter.name}`]
2562
+ }
2563
+ });
2564
+ for (const img of images) {
2565
+ if (!img.RepoTags || img.RepoTags.length === 0) {
2566
+ continue;
2567
+ }
2568
+ const depsLabel = img.Labels?.[LABELS2.dependencies];
2569
+ if (!depsLabel) {
2570
+ continue;
2571
+ }
2572
+ const imgDeps = depsLabel.split(",");
2573
+ if (img.RepoTags[0] && normalizedReq.length === imgDeps.length && normalizedReq.every((p) => imgDeps.includes(p))) {
2574
+ bestImage = img.RepoTags[0];
2575
+ remainingPackages = [];
2576
+ imageSetupScript = img.Labels?.[LABELS2.setupScript] || undefined;
2577
+ logger.debug(`[Docker] Found exact custom image match: ${bestImage}`);
2578
+ break;
2579
+ }
2580
+ if (img.RepoTags[0] && normalizedReq.every((p) => imgDeps.includes(p))) {
2581
+ bestImage = img.RepoTags[0];
2582
+ remainingPackages = [];
2583
+ imageSetupScript = img.Labels?.[LABELS2.setupScript] || undefined;
2584
+ logger.debug(`[Docker] Found superset custom image match: ${bestImage}`);
2585
+ }
2460
2586
  }
2461
2587
  }
2462
- if (resolvedImage === adapter.image) {
2463
- const legacyCustomTag = `${adapter.image}-custom`;
2588
+ if (bestImage !== baseImage && imageSetupScript === undefined) {
2464
2589
  try {
2465
- await this.docker.getImage(legacyCustomTag).inspect();
2466
- resolvedImage = legacyCustomTag;
2590
+ const { LABELS: LABELS2 } = await Promise.resolve().then(() => (init_image_builder(), exports_image_builder));
2591
+ const inspect = await this.docker.getImage(bestImage).inspect();
2592
+ const labels = inspect.Config?.Labels ?? {};
2593
+ imageSetupScript = labels[LABELS2.setupScript] || undefined;
2467
2594
  } catch {}
2468
2595
  }
2469
- try {
2470
- await this.docker.getImage(resolvedImage).inspect();
2471
- } catch {
2472
- logger.debug(`[ImageBuilder] Image ${resolvedImage} not found. Building...`);
2473
- const { buildBaseImages: buildBaseImages2, buildCustomImage: buildCustomImage2 } = await Promise.resolve().then(() => (init_image_builder(), exports_image_builder));
2474
- if (resolvedImage !== adapter.image && normalizedDeps.length > 0) {
2475
- try {
2476
- await this.docker.getImage(adapter.image).inspect();
2477
- } catch {
2478
- logger.debug(`[ImageBuilder] Base image ${adapter.image} missing. Building...`);
2479
- await buildBaseImages2(this.docker, undefined, false, [adapter.name]);
2480
- }
2481
- logger.debug(`[ImageBuilder] Building custom image for ${adapter.name}...`);
2482
- await buildCustomImage2(this.docker, adapter.name, normalizedDeps);
2483
- } else {
2484
- logger.debug(`[ImageBuilder] Building base image for ${adapter.name}...`);
2596
+ if (bestImage === baseImage) {
2597
+ try {
2598
+ await this.docker.getImage(baseImage).inspect();
2599
+ } catch {
2600
+ logger.debug(`[Docker] Base image ${baseImage} not found. Building...`);
2601
+ const { buildBaseImages: buildBaseImages2 } = await Promise.resolve().then(() => (init_image_builder(), exports_image_builder));
2485
2602
  await buildBaseImages2(this.docker, undefined, false, [adapter.name]);
2486
2603
  }
2487
2604
  }
2488
- this.imageCache.set(cacheKey, resolvedImage);
2489
- return resolvedImage;
2605
+ if (remainingPackages.length === 0) {
2606
+ this.imageCache.set(cacheKey, bestImage);
2607
+ }
2608
+ return { image: bestImage, remainingPackages, imageSetupScript };
2490
2609
  }
2491
2610
  ensurePool() {
2492
2611
  if (!this.pool) {
@@ -2511,7 +2630,9 @@ class DockerIsol8 {
2511
2630
  async executeEphemeral(req, startTime) {
2512
2631
  const adapter = this.getAdapter(req.runtime);
2513
2632
  const timeoutMs = req.timeoutMs ?? this.defaultTimeoutMs;
2514
- const image = await this.resolveImage(adapter);
2633
+ const resolved = await this.resolveImage(adapter, req.installPackages);
2634
+ const image = resolved.image;
2635
+ const execWorkdir = req.workdir ? resolveWorkdir(req.workdir) : SANDBOX_WORKDIR;
2515
2636
  const pool = this.ensurePool();
2516
2637
  const container = await pool.acquire(image);
2517
2638
  let startStats;
@@ -2542,8 +2663,14 @@ class DockerIsol8 {
2542
2663
  await this.volumeManager.writeFileViaExec(container, filePath, req.code);
2543
2664
  rawCmd = adapter.getCommand(req.code, filePath);
2544
2665
  }
2545
- if (req.installPackages?.length) {
2546
- await this.executionManager.installPackages(container, req.runtime, req.installPackages, timeoutMs);
2666
+ if (resolved.remainingPackages.length > 0) {
2667
+ await this.executionManager.installPackages(container, req.runtime, resolved.remainingPackages, timeoutMs);
2668
+ }
2669
+ if (resolved.imageSetupScript) {
2670
+ await this.executionManager.runSetupScript(container, resolved.imageSetupScript, timeoutMs, this.volumeManager);
2671
+ }
2672
+ if (req.setupScript) {
2673
+ await this.executionManager.runSetupScript(container, req.setupScript, timeoutMs, this.volumeManager);
2547
2674
  }
2548
2675
  const timeoutSec = Math.ceil(timeoutMs / 1000);
2549
2676
  let cmd;
@@ -2565,7 +2692,7 @@ class DockerIsol8 {
2565
2692
  Env: this.executionManager.buildEnv(req.env, this.networkManager.proxyPort, this.network, this.networkFilter),
2566
2693
  AttachStdout: true,
2567
2694
  AttachStderr: true,
2568
- WorkingDir: SANDBOX_WORKDIR,
2695
+ WorkingDir: execWorkdir,
2569
2696
  User: "sandbox"
2570
2697
  });
2571
2698
  const start = performance.now();
@@ -2625,8 +2752,13 @@ class DockerIsol8 {
2625
2752
  async executePersistent(req, startTime) {
2626
2753
  const adapter = this.getAdapter(req.runtime);
2627
2754
  const timeoutMs = req.timeoutMs ?? this.defaultTimeoutMs;
2755
+ const execWorkdir = req.workdir ? resolveWorkdir(req.workdir) : SANDBOX_WORKDIR;
2756
+ let remainingPackages = req.installPackages ?? [];
2757
+ let imageSetupScript;
2628
2758
  if (!this.container) {
2629
- await this.startPersistentContainer(adapter);
2759
+ const started = await this.startPersistentContainer(adapter, req.installPackages);
2760
+ remainingPackages = started.remainingPackages;
2761
+ imageSetupScript = started.imageSetupScript;
2630
2762
  } else if (this.persistentRuntime?.name !== adapter.name) {
2631
2763
  throw new Error(`Cannot switch runtime from "${this.persistentRuntime?.name}" to "${adapter.name}". Each persistent container supports a single runtime. Create a new Isol8 instance for a different runtime.`);
2632
2764
  }
@@ -2640,8 +2772,14 @@ class DockerIsol8 {
2640
2772
  }
2641
2773
  const rawCmd = adapter.getCommand(req.code, filePath);
2642
2774
  const timeoutSec = Math.ceil(timeoutMs / 1000);
2643
- if (req.installPackages?.length) {
2644
- await this.executionManager.installPackages(this.container, req.runtime, req.installPackages, timeoutMs);
2775
+ if (remainingPackages.length > 0) {
2776
+ await this.executionManager.installPackages(this.container, req.runtime, remainingPackages, timeoutMs);
2777
+ }
2778
+ if (imageSetupScript) {
2779
+ await this.executionManager.runSetupScript(this.container, imageSetupScript, timeoutMs, this.volumeManager);
2780
+ }
2781
+ if (req.setupScript) {
2782
+ await this.executionManager.runSetupScript(this.container, req.setupScript, timeoutMs, this.volumeManager);
2645
2783
  }
2646
2784
  let cmd;
2647
2785
  if (req.stdin) {
@@ -2658,7 +2796,7 @@ class DockerIsol8 {
2658
2796
  Env: execEnv,
2659
2797
  AttachStdout: true,
2660
2798
  AttachStderr: true,
2661
- WorkingDir: SANDBOX_WORKDIR,
2799
+ WorkingDir: execWorkdir,
2662
2800
  User: "sandbox"
2663
2801
  });
2664
2802
  const start = performance.now();
@@ -2713,10 +2851,10 @@ class DockerIsol8 {
2713
2851
  async retrieveFiles(container, paths) {
2714
2852
  return this.volumeManager.retrieveFiles(container, paths);
2715
2853
  }
2716
- async startPersistentContainer(adapter) {
2717
- const image = await this.resolveImage(adapter);
2854
+ async startPersistentContainer(adapter, requestedPackages) {
2855
+ const resolved = await this.resolveImage(adapter, requestedPackages);
2718
2856
  this.container = await this.docker.createContainer({
2719
- Image: image,
2857
+ Image: resolved.image,
2720
2858
  Cmd: ["sleep", "infinity"],
2721
2859
  WorkingDir: SANDBOX_WORKDIR,
2722
2860
  Env: this.executionManager.buildEnv(undefined, this.networkManager.proxyPort, this.network, this.networkFilter),
@@ -2732,6 +2870,10 @@ class DockerIsol8 {
2732
2870
  await this.networkManager.startProxy(this.container);
2733
2871
  await this.networkManager.setupIptables(this.container);
2734
2872
  this.persistentRuntime = adapter;
2873
+ return {
2874
+ remainingPackages: resolved.remainingPackages,
2875
+ imageSetupScript: resolved.imageSetupScript
2876
+ };
2735
2877
  }
2736
2878
  getAdapter(runtime) {
2737
2879
  return RuntimeRegistry.get(runtime);
@@ -2844,7 +2986,7 @@ init_logger();
2844
2986
  // package.json
2845
2987
  var package_default = {
2846
2988
  name: "@isol8/core",
2847
- version: "0.16.0",
2989
+ version: "0.18.0",
2848
2990
  description: "Sandboxed code execution engine for AI agents and apps (Docker, runtime and network controls)",
2849
2991
  author: "Illusion47586",
2850
2992
  license: "MIT",
@@ -2912,8 +3054,7 @@ var VERSION = package_default.version;
2912
3054
  export {
2913
3055
  logger,
2914
3056
  loadConfig,
2915
- getCustomImageTag,
2916
- buildCustomImages,
3057
+ imageExists,
2917
3058
  buildCustomImage,
2918
3059
  buildBaseImages,
2919
3060
  bashAdapter,
@@ -2923,9 +3064,10 @@ export {
2923
3064
  RemoteIsol8,
2924
3065
  PythonAdapter,
2925
3066
  NodeAdapter,
3067
+ LABELS,
2926
3068
  DockerIsol8,
2927
3069
  DenoAdapter,
2928
3070
  BunAdapter
2929
3071
  };
2930
3072
 
2931
- //# debugId=A775CBFF7103831D64756E2164756E21
3073
+ //# debugId=014F8E5DF8C3A76364756E2164756E21