@isol8/core 0.17.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
  });
@@ -828,7 +835,6 @@ var DEFAULT_CONFIG = {
828
835
  },
829
836
  poolStrategy: "fast",
830
837
  poolSize: { clean: 1, dirty: 1 },
831
- dependencies: {},
832
838
  security: {
833
839
  seccomp: "strict"
834
840
  },
@@ -869,6 +875,7 @@ var DEFAULT_CONFIG = {
869
875
  defaultTtlMs: 86400000,
870
876
  cleanupIntervalMs: 3600000
871
877
  },
878
+ prebuiltImages: [],
872
879
  debug: false
873
880
  };
874
881
  function loadConfig(cwd) {
@@ -903,10 +910,6 @@ function mergeConfig(defaults, overrides) {
903
910
  },
904
911
  poolStrategy: overrides.poolStrategy ?? defaults.poolStrategy,
905
912
  poolSize: overrides.poolSize ?? defaults.poolSize,
906
- dependencies: {
907
- ...defaults.dependencies,
908
- ...overrides.dependencies
909
- },
910
913
  security: {
911
914
  seccomp: overrides.security?.seccomp ?? defaults.security.seccomp,
912
915
  customProfilePath: overrides.security?.customProfilePath ?? defaults.security.customProfilePath
@@ -926,6 +929,7 @@ function mergeConfig(defaults, overrides) {
926
929
  ...defaults.auth,
927
930
  ...overrides.auth
928
931
  },
932
+ prebuiltImages: overrides.prebuiltImages ?? defaults.prebuiltImages,
929
933
  debug: overrides.debug ?? defaults.debug
930
934
  };
931
935
  }
@@ -1345,11 +1349,9 @@ var EMBEDDED_DEFAULT_SECCOMP_PROFILE = JSON.stringify({
1345
1349
  ]
1346
1350
  });
1347
1351
 
1348
- // src/engine/docker.ts
1349
- init_image_builder();
1350
-
1351
1352
  // src/engine/managers/execution-manager.ts
1352
1353
  init_logger();
1354
+ init_utils();
1353
1355
  import { PassThrough } from "node:stream";
1354
1356
 
1355
1357
  class ExecutionManager {
@@ -1453,6 +1455,63 @@ class ExecutionManager {
1453
1455
  stream.on("error", reject);
1454
1456
  });
1455
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
+ }
1456
1515
  async* streamExecOutput(stream, exec, container, timeoutMs) {
1457
1516
  const queue = [];
1458
1517
  let resolve2 = null;
@@ -1707,6 +1766,7 @@ class NetworkManager {
1707
1766
  }
1708
1767
  }
1709
1768
  // src/engine/managers/volume-manager.ts
1769
+ init_utils();
1710
1770
  import { PassThrough as PassThrough2 } from "node:stream";
1711
1771
 
1712
1772
  class VolumeManager {
@@ -1787,13 +1847,13 @@ class VolumeManager {
1787
1847
  const b64Output = Buffer.concat(chunks).toString("utf-8").trim();
1788
1848
  return Buffer.from(b64Output, "base64");
1789
1849
  }
1790
- async getFileFromContainer(container, path) {
1791
- const stream = await container.getArchive({ path });
1850
+ async getFileFromContainer(container, path2) {
1851
+ const stream = await container.getArchive({ path: path2 });
1792
1852
  const chunks = [];
1793
1853
  for await (const chunk of stream) {
1794
1854
  chunks.push(chunk);
1795
1855
  }
1796
- return extractFromTar(Buffer.concat(chunks), path);
1856
+ return extractFromTar(Buffer.concat(chunks), path2);
1797
1857
  }
1798
1858
  async retrieveFiles(container, paths) {
1799
1859
  const files = {};
@@ -1805,19 +1865,19 @@ class VolumeManager {
1805
1865
  }
1806
1866
  return files;
1807
1867
  }
1808
- async putFile(container, path, content) {
1868
+ async putFile(container, path2, content) {
1809
1869
  if (this.readonlyRootFs) {
1810
- await this.writeFileViaExec(container, path, content);
1870
+ await this.writeFileViaExec(container, path2, content);
1811
1871
  } else {
1812
- const tar = createTarBuffer(path, content);
1872
+ const tar = createTarBuffer(path2, content);
1813
1873
  await container.putArchive(tar, { path: "/" });
1814
1874
  }
1815
1875
  }
1816
- async getFile(container, path) {
1876
+ async getFile(container, path2) {
1817
1877
  if (this.readonlyRootFs) {
1818
- return this.readFileViaExec(container, path);
1878
+ return this.readFileViaExec(container, path2);
1819
1879
  }
1820
- return this.getFileFromContainer(container, path);
1880
+ return this.getFileFromContainer(container, path2);
1821
1881
  }
1822
1882
  }
1823
1883
  // src/engine/pool.ts
@@ -2099,6 +2159,7 @@ function calculateResourceDelta(before, after) {
2099
2159
  }
2100
2160
 
2101
2161
  // src/engine/docker.ts
2162
+ init_utils();
2102
2163
  var SANDBOX_WORKDIR = "/sandbox";
2103
2164
  var MAX_OUTPUT_BYTES = 1024 * 1024;
2104
2165
 
@@ -2123,7 +2184,6 @@ class DockerIsol8 {
2123
2184
  logNetwork;
2124
2185
  poolStrategy;
2125
2186
  poolSize;
2126
- dependencies;
2127
2187
  auditLogger;
2128
2188
  remoteCodePolicy;
2129
2189
  networkManager;
@@ -2173,7 +2233,6 @@ class DockerIsol8 {
2173
2233
  this.logNetwork = options.logNetwork ?? false;
2174
2234
  this.poolStrategy = options.poolStrategy ?? "fast";
2175
2235
  this.poolSize = options.poolSize ?? { clean: 1, dirty: 1 };
2176
- this.dependencies = options.dependencies ?? {};
2177
2236
  this.remoteCodePolicy = options.remoteCode ?? {
2178
2237
  enabled: false,
2179
2238
  allowedSchemes: ["https"],
@@ -2217,7 +2276,8 @@ class DockerIsol8 {
2217
2276
  const adapters2 = typeof prewarm === "object" && prewarm.runtimes?.length ? prewarm.runtimes.map((runtime) => RuntimeRegistry.get(runtime)) : RuntimeRegistry.list();
2218
2277
  for (const adapter of adapters2) {
2219
2278
  try {
2220
- images.add(await this.resolveImage(adapter));
2279
+ const resolved = await this.resolveImage(adapter);
2280
+ images.add(resolved.image);
2221
2281
  } catch (err) {
2222
2282
  logger.debug(`[Pool] Pre-warm image resolution failed for ${adapter.name}: ${err}`);
2223
2283
  }
@@ -2374,17 +2434,17 @@ class DockerIsol8 {
2374
2434
  } catch {}
2375
2435
  return logs;
2376
2436
  }
2377
- async putFile(path, content) {
2437
+ async putFile(path2, content) {
2378
2438
  if (!this.container) {
2379
2439
  throw new Error("No active container. Call execute() first in persistent mode.");
2380
2440
  }
2381
- await this.volumeManager.putFile(this.container, path, content);
2441
+ await this.volumeManager.putFile(this.container, path2, content);
2382
2442
  }
2383
- async getFile(path) {
2443
+ async getFile(path2) {
2384
2444
  if (!this.container) {
2385
2445
  throw new Error("No active container. Call execute() first in persistent mode.");
2386
2446
  }
2387
- return this.volumeManager.getFile(this.container, path);
2447
+ return this.volumeManager.getFile(this.container, path2);
2388
2448
  }
2389
2449
  get containerId() {
2390
2450
  return this.container?.id ?? null;
@@ -2395,7 +2455,9 @@ class DockerIsol8 {
2395
2455
  const request = await this.resolveExecutionRequest(req);
2396
2456
  const adapter = this.getAdapter(request.runtime);
2397
2457
  const timeoutMs = request.timeoutMs ?? this.defaultTimeoutMs;
2398
- 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;
2399
2461
  const container = await this.docker.createContainer({
2400
2462
  Image: image,
2401
2463
  Cmd: ["sleep", "infinity"],
@@ -2412,8 +2474,14 @@ class DockerIsol8 {
2412
2474
  const ext = request.fileExtension ?? adapter.getFileExtension();
2413
2475
  const filePath = `${SANDBOX_WORKDIR}/main${ext}`;
2414
2476
  await this.volumeManager.writeFileViaExec(container, filePath, request.code);
2415
- if (request.installPackages?.length) {
2416
- 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);
2417
2485
  }
2418
2486
  if (request.files) {
2419
2487
  for (const [fPath, fContent] of Object.entries(request.files)) {
@@ -2436,7 +2504,7 @@ class DockerIsol8 {
2436
2504
  Env: this.executionManager.buildEnv(request.env, this.networkManager.proxyPort, this.network, this.networkFilter),
2437
2505
  AttachStdout: true,
2438
2506
  AttachStderr: true,
2439
- WorkingDir: SANDBOX_WORKDIR,
2507
+ WorkingDir: execWorkdir,
2440
2508
  User: "sandbox"
2441
2509
  });
2442
2510
  const execStream = await exec.start({ Tty: false });
@@ -2454,55 +2522,90 @@ class DockerIsol8 {
2454
2522
  this.semaphore.release();
2455
2523
  }
2456
2524
  }
2457
- async resolveImage(adapter) {
2525
+ async resolveImage(adapter, requestedPackages) {
2458
2526
  if (this.overrideImage) {
2459
- 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
+ };
2460
2539
  }
2461
- const cacheKey = adapter.image;
2540
+ const cacheKey = `${adapter.name}:${(requestedPackages ?? []).join(",")}`;
2462
2541
  const cached = this.imageCache.get(cacheKey);
2463
2542
  if (cached) {
2464
- return cached;
2465
- }
2466
- let resolvedImage = adapter.image;
2467
- const configuredDeps = this.dependencies[adapter.name];
2468
- const normalizedDeps = configuredDeps ? normalizePackages(configuredDeps) : [];
2469
- if (normalizedDeps.length > 0) {
2470
- const hashedCustomTag = getCustomImageTag(adapter.name, normalizedDeps);
2543
+ let imageSetupScript2;
2471
2544
  try {
2472
- await this.docker.getImage(hashedCustomTag).inspect();
2473
- resolvedImage = hashedCustomTag;
2474
- } catch {
2475
- 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
+ }
2476
2586
  }
2477
2587
  }
2478
- if (resolvedImage === adapter.image) {
2479
- const legacyCustomTag = `${adapter.image}-custom`;
2588
+ if (bestImage !== baseImage && imageSetupScript === undefined) {
2480
2589
  try {
2481
- await this.docker.getImage(legacyCustomTag).inspect();
2482
- 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;
2483
2594
  } catch {}
2484
2595
  }
2485
- try {
2486
- await this.docker.getImage(resolvedImage).inspect();
2487
- } catch {
2488
- logger.debug(`[ImageBuilder] Image ${resolvedImage} not found. Building...`);
2489
- const { buildBaseImages: buildBaseImages2, buildCustomImage: buildCustomImage2 } = await Promise.resolve().then(() => (init_image_builder(), exports_image_builder));
2490
- if (resolvedImage !== adapter.image && normalizedDeps.length > 0) {
2491
- try {
2492
- await this.docker.getImage(adapter.image).inspect();
2493
- } catch {
2494
- logger.debug(`[ImageBuilder] Base image ${adapter.image} missing. Building...`);
2495
- await buildBaseImages2(this.docker, undefined, false, [adapter.name]);
2496
- }
2497
- logger.debug(`[ImageBuilder] Building custom image for ${adapter.name}...`);
2498
- await buildCustomImage2(this.docker, adapter.name, normalizedDeps);
2499
- } else {
2500
- 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));
2501
2602
  await buildBaseImages2(this.docker, undefined, false, [adapter.name]);
2502
2603
  }
2503
2604
  }
2504
- this.imageCache.set(cacheKey, resolvedImage);
2505
- return resolvedImage;
2605
+ if (remainingPackages.length === 0) {
2606
+ this.imageCache.set(cacheKey, bestImage);
2607
+ }
2608
+ return { image: bestImage, remainingPackages, imageSetupScript };
2506
2609
  }
2507
2610
  ensurePool() {
2508
2611
  if (!this.pool) {
@@ -2527,7 +2630,9 @@ class DockerIsol8 {
2527
2630
  async executeEphemeral(req, startTime) {
2528
2631
  const adapter = this.getAdapter(req.runtime);
2529
2632
  const timeoutMs = req.timeoutMs ?? this.defaultTimeoutMs;
2530
- 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;
2531
2636
  const pool = this.ensurePool();
2532
2637
  const container = await pool.acquire(image);
2533
2638
  let startStats;
@@ -2558,8 +2663,14 @@ class DockerIsol8 {
2558
2663
  await this.volumeManager.writeFileViaExec(container, filePath, req.code);
2559
2664
  rawCmd = adapter.getCommand(req.code, filePath);
2560
2665
  }
2561
- if (req.installPackages?.length) {
2562
- 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);
2563
2674
  }
2564
2675
  const timeoutSec = Math.ceil(timeoutMs / 1000);
2565
2676
  let cmd;
@@ -2581,7 +2692,7 @@ class DockerIsol8 {
2581
2692
  Env: this.executionManager.buildEnv(req.env, this.networkManager.proxyPort, this.network, this.networkFilter),
2582
2693
  AttachStdout: true,
2583
2694
  AttachStderr: true,
2584
- WorkingDir: SANDBOX_WORKDIR,
2695
+ WorkingDir: execWorkdir,
2585
2696
  User: "sandbox"
2586
2697
  });
2587
2698
  const start = performance.now();
@@ -2641,8 +2752,13 @@ class DockerIsol8 {
2641
2752
  async executePersistent(req, startTime) {
2642
2753
  const adapter = this.getAdapter(req.runtime);
2643
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;
2644
2758
  if (!this.container) {
2645
- await this.startPersistentContainer(adapter);
2759
+ const started = await this.startPersistentContainer(adapter, req.installPackages);
2760
+ remainingPackages = started.remainingPackages;
2761
+ imageSetupScript = started.imageSetupScript;
2646
2762
  } else if (this.persistentRuntime?.name !== adapter.name) {
2647
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.`);
2648
2764
  }
@@ -2656,8 +2772,14 @@ class DockerIsol8 {
2656
2772
  }
2657
2773
  const rawCmd = adapter.getCommand(req.code, filePath);
2658
2774
  const timeoutSec = Math.ceil(timeoutMs / 1000);
2659
- if (req.installPackages?.length) {
2660
- 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);
2661
2783
  }
2662
2784
  let cmd;
2663
2785
  if (req.stdin) {
@@ -2674,7 +2796,7 @@ class DockerIsol8 {
2674
2796
  Env: execEnv,
2675
2797
  AttachStdout: true,
2676
2798
  AttachStderr: true,
2677
- WorkingDir: SANDBOX_WORKDIR,
2799
+ WorkingDir: execWorkdir,
2678
2800
  User: "sandbox"
2679
2801
  });
2680
2802
  const start = performance.now();
@@ -2729,10 +2851,10 @@ class DockerIsol8 {
2729
2851
  async retrieveFiles(container, paths) {
2730
2852
  return this.volumeManager.retrieveFiles(container, paths);
2731
2853
  }
2732
- async startPersistentContainer(adapter) {
2733
- const image = await this.resolveImage(adapter);
2854
+ async startPersistentContainer(adapter, requestedPackages) {
2855
+ const resolved = await this.resolveImage(adapter, requestedPackages);
2734
2856
  this.container = await this.docker.createContainer({
2735
- Image: image,
2857
+ Image: resolved.image,
2736
2858
  Cmd: ["sleep", "infinity"],
2737
2859
  WorkingDir: SANDBOX_WORKDIR,
2738
2860
  Env: this.executionManager.buildEnv(undefined, this.networkManager.proxyPort, this.network, this.networkFilter),
@@ -2748,6 +2870,10 @@ class DockerIsol8 {
2748
2870
  await this.networkManager.startProxy(this.container);
2749
2871
  await this.networkManager.setupIptables(this.container);
2750
2872
  this.persistentRuntime = adapter;
2873
+ return {
2874
+ remainingPackages: resolved.remainingPackages,
2875
+ imageSetupScript: resolved.imageSetupScript
2876
+ };
2751
2877
  }
2752
2878
  getAdapter(runtime) {
2753
2879
  return RuntimeRegistry.get(runtime);
@@ -2860,7 +2986,7 @@ init_logger();
2860
2986
  // package.json
2861
2987
  var package_default = {
2862
2988
  name: "@isol8/core",
2863
- version: "0.17.0",
2989
+ version: "0.18.0",
2864
2990
  description: "Sandboxed code execution engine for AI agents and apps (Docker, runtime and network controls)",
2865
2991
  author: "Illusion47586",
2866
2992
  license: "MIT",
@@ -2928,8 +3054,7 @@ var VERSION = package_default.version;
2928
3054
  export {
2929
3055
  logger,
2930
3056
  loadConfig,
2931
- getCustomImageTag,
2932
- buildCustomImages,
3057
+ imageExists,
2933
3058
  buildCustomImage,
2934
3059
  buildBaseImages,
2935
3060
  bashAdapter,
@@ -2939,9 +3064,10 @@ export {
2939
3064
  RemoteIsol8,
2940
3065
  PythonAdapter,
2941
3066
  NodeAdapter,
3067
+ LABELS,
2942
3068
  DockerIsol8,
2943
3069
  DenoAdapter,
2944
3070
  BunAdapter
2945
3071
  };
2946
3072
 
2947
- //# debugId=52A84C2338D09E5564756E2164756E21
3073
+ //# debugId=014F8E5DF8C3A76364756E2164756E21