@isol8/core 0.17.0 → 0.19.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;
@@ -50,6 +48,37 @@ var init_adapter = __esm(() => {
50
48
  };
51
49
  });
52
50
 
51
+ // src/runtime/adapters/agent.ts
52
+ function shellQuote(s) {
53
+ return `'${s.replace(/'/g, "'\\''")}'`;
54
+ }
55
+ var SANDBOX_SYSTEM_PROMPT, AgentAdapter;
56
+ var init_agent = __esm(() => {
57
+ SANDBOX_SYSTEM_PROMPT = "You are running inside an isol8 sandbox — a Docker container with strict " + "resource limits and controlled network access. isol8 exists to execute " + "untrusted code safely: outbound network is filtered to a whitelist, the " + "filesystem is ephemeral, and some system calls are restricted. Work within " + "these constraints: do not assume open internet access, do not rely on " + "persistent state across runs, and do not attempt to escape the sandbox.";
58
+ AgentAdapter = {
59
+ name: "agent",
60
+ image: "isol8:agent",
61
+ getCommand(code) {
62
+ return [
63
+ "bash",
64
+ "-c",
65
+ `pi --no-session --append-system-prompt ${shellQuote(SANDBOX_SYSTEM_PROMPT)} -p ${shellQuote(code)}`
66
+ ];
67
+ },
68
+ getCommandWithOptions(code, options) {
69
+ const flags = options.agentFlags ? `${options.agentFlags} ` : "";
70
+ return [
71
+ "bash",
72
+ "-c",
73
+ `pi --no-session --append-system-prompt ${shellQuote(SANDBOX_SYSTEM_PROMPT)} ${flags}-p ${shellQuote(code)}`
74
+ ];
75
+ },
76
+ getFileExtension() {
77
+ return ".txt";
78
+ }
79
+ };
80
+ });
81
+
53
82
  // src/runtime/adapters/bash.ts
54
83
  var bashAdapter;
55
84
  var init_bash = __esm(() => {
@@ -151,12 +180,14 @@ var init_python = __esm(() => {
151
180
  // src/runtime/index.ts
152
181
  var init_runtime = __esm(() => {
153
182
  init_adapter();
183
+ init_agent();
154
184
  init_bash();
155
185
  init_bun();
156
186
  init_deno();
157
187
  init_node();
158
188
  init_python();
159
189
  init_adapter();
190
+ init_agent();
160
191
  init_bash();
161
192
  init_bun();
162
193
  init_deno();
@@ -167,6 +198,7 @@ var init_runtime = __esm(() => {
167
198
  RuntimeRegistry.register(BunAdapter);
168
199
  RuntimeRegistry.register(bashAdapter);
169
200
  RuntimeRegistry.register(DenoAdapter);
201
+ RuntimeRegistry.register(AgentAdapter);
170
202
  });
171
203
 
172
204
  // src/utils/logger.ts
@@ -200,11 +232,13 @@ var exports_utils = {};
200
232
  __export(exports_utils, {
201
233
  validatePackageName: () => validatePackageName,
202
234
  truncateOutput: () => truncateOutput,
235
+ resolveWorkdir: () => resolveWorkdir,
203
236
  parseMemoryLimit: () => parseMemoryLimit,
204
237
  maskSecrets: () => maskSecrets,
205
238
  extractFromTar: () => extractFromTar,
206
239
  createTarBuffer: () => createTarBuffer
207
240
  });
241
+ import path from "node:path";
208
242
  function parseMemoryLimit(limit) {
209
243
  const match = limit.match(/^(\d+(?:\.\d+)?)\s*([kmgt]?)b?$/i);
210
244
  if (!match) {
@@ -299,17 +333,24 @@ function validatePackageName(name) {
299
333
  }
300
334
  return name;
301
335
  }
336
+ function resolveWorkdir(workdir, sandboxRoot = "/sandbox") {
337
+ const resolved = path.posix.resolve(sandboxRoot, workdir);
338
+ if (resolved !== sandboxRoot && !resolved.startsWith(`${sandboxRoot}/`)) {
339
+ throw new Error("Working directory must be inside /sandbox");
340
+ }
341
+ return resolved;
342
+ }
343
+ var init_utils = () => {};
302
344
 
303
345
  // src/engine/image-builder.ts
304
346
  var exports_image_builder = {};
305
347
  __export(exports_image_builder, {
306
348
  normalizePackages: () => normalizePackages,
307
349
  imageExists: () => imageExists,
308
- getCustomImageTag: () => getCustomImageTag,
309
350
  ensureImages: () => ensureImages,
310
- buildCustomImages: () => buildCustomImages,
311
351
  buildCustomImage: () => buildCustomImage,
312
- buildBaseImages: () => buildBaseImages
352
+ buildBaseImages: () => buildBaseImages,
353
+ LABELS: () => LABELS
313
354
  });
314
355
  import { createHash as createHash2 } from "node:crypto";
315
356
  import { existsSync as existsSync3, readFileSync as readFileSync2, statSync as statSync2 } from "node:fs";
@@ -350,23 +391,21 @@ function computeDockerDirHash() {
350
391
  }
351
392
  return hash.digest("hex");
352
393
  }
353
- function computeDepsHash(runtime, packages) {
394
+ function computeDepsHash(runtime, packages, setupScript) {
354
395
  const hash = createHash2("sha256");
355
396
  hash.update(runtime);
356
397
  for (const pkg of [...packages].sort()) {
357
398
  hash.update(pkg);
358
399
  }
400
+ if (setupScript) {
401
+ hash.update("setup:");
402
+ hash.update(setupScript);
403
+ }
359
404
  return hash.digest("hex");
360
405
  }
361
406
  function normalizePackages(packages) {
362
407
  return [...new Set(packages.map((pkg) => pkg.trim()).filter(Boolean))].sort();
363
408
  }
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
409
  async function getImageLabels(docker, imageName) {
371
410
  try {
372
411
  const image = docker.getImage(imageName);
@@ -439,33 +478,9 @@ async function buildBaseImages(docker, onProgress, force = false, onlyRuntimes)
439
478
  }
440
479
  }
441
480
  }
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) {
481
+ async function buildCustomImage(docker, runtime, packages, tag, onProgress, force = false, setupScript) {
466
482
  const normalizedPackages = normalizePackages(packages);
467
- const tag = getCustomImageTag(runtime, normalizedPackages);
468
- const depsHash = computeDepsHash(runtime, normalizedPackages);
483
+ const depsHash = computeDepsHash(runtime, normalizedPackages, setupScript);
469
484
  logger.debug(`[ImageBuilder] ${runtime} custom deps hash: ${depsHash.slice(0, 16)}...`);
470
485
  if (!force) {
471
486
  const labels = await getImageLabels(docker, tag);
@@ -491,7 +506,7 @@ async function buildCustomImage(docker, runtime, packages, onProgress, force = f
491
506
  let installCmd;
492
507
  switch (runtime) {
493
508
  case "python":
494
- installCmd = `RUN pip install --no-cache-dir ${normalizedPackages.join(" ")}`;
509
+ installCmd = `RUN pip install --break-system-packages --no-cache-dir ${normalizedPackages.join(" ")}`;
495
510
  break;
496
511
  case "node":
497
512
  installCmd = `RUN npm install -g ${normalizedPackages.join(" ")}`;
@@ -509,27 +524,50 @@ async function buildCustomImage(docker, runtime, packages, onProgress, force = f
509
524
  default:
510
525
  throw new Error(`Unknown runtime: ${runtime}`);
511
526
  }
527
+ let setupLines = "";
528
+ if (setupScript) {
529
+ const escaped = setupScript.replace(/\\/g, "\\\\").replace(/'/g, "'\\''");
530
+ setupLines = [
531
+ `RUN printf '%s\\n' '${escaped}' > /sandbox/.isol8-setup.sh`,
532
+ "RUN chmod +x /sandbox/.isol8-setup.sh"
533
+ ].join(`
534
+ `);
535
+ }
512
536
  const dockerfileContent = `FROM isol8:${runtime}
513
537
  ${installCmd}
538
+ ${setupLines}
514
539
  `;
515
- const { createTarBuffer: createTarBuffer2, validatePackageName: validatePackageName2 } = await Promise.resolve().then(() => exports_utils);
516
- const { Readable } = await import("node:stream");
540
+ const { createTarBuffer: createTarBuffer2, validatePackageName: validatePackageName2 } = await Promise.resolve().then(() => (init_utils(), exports_utils));
517
541
  normalizedPackages.forEach(validatePackageName2);
518
542
  const tarBuffer = createTarBuffer2("Dockerfile", dockerfileContent);
519
- const stream = await docker.buildImage(Readable.from(tarBuffer), {
543
+ const imageLabels = {
544
+ [LABELS.depsHash]: depsHash,
545
+ [LABELS.runtime]: runtime.toString(),
546
+ [LABELS.dependencies]: normalizedPackages.join(",")
547
+ };
548
+ if (setupScript) {
549
+ imageLabels[LABELS.setupScript] = setupScript;
550
+ }
551
+ const stream = await docker.buildImage(tarBuffer, {
520
552
  t: tag,
521
553
  dockerfile: "Dockerfile",
522
- labels: {
523
- [LABELS.depsHash]: depsHash
524
- }
554
+ labels: imageLabels
525
555
  });
526
556
  await new Promise((resolve2, reject) => {
527
- docker.modem.followProgress(stream, (err) => {
557
+ docker.modem.followProgress(stream, (err, res) => {
528
558
  if (err) {
529
559
  reject(err);
560
+ } else if (res && res.length > 0 && res.at(-1).error) {
561
+ reject(new Error(res.at(-1).error));
530
562
  } else {
531
563
  resolve2();
532
564
  }
565
+ }, (event) => {
566
+ if (event.stream) {
567
+ onProgress?.({ runtime: String(runtime), status: "building", message: event.stream });
568
+ } else if (event.error) {
569
+ onProgress?.({ runtime: String(runtime), status: "error", message: event.error });
570
+ }
533
571
  });
534
572
  });
535
573
  if (oldImageId) {
@@ -564,7 +602,10 @@ var init_image_builder = __esm(() => {
564
602
  DOCKERFILE_DIR = resolveDockerDir();
565
603
  LABELS = {
566
604
  dockerHash: "org.isol8.build.hash",
567
- depsHash: "org.isol8.deps.hash"
605
+ depsHash: "org.isol8.deps.hash",
606
+ runtime: "org.isol8.runtime",
607
+ dependencies: "org.isol8.dependencies",
608
+ setupScript: "org.isol8.setup"
568
609
  };
569
610
  DOCKER_BUILD_FILES = ["Dockerfile", "proxy.sh", "proxy-handler.sh"];
570
611
  });
@@ -828,7 +869,6 @@ var DEFAULT_CONFIG = {
828
869
  },
829
870
  poolStrategy: "fast",
830
871
  poolSize: { clean: 1, dirty: 1 },
831
- dependencies: {},
832
872
  security: {
833
873
  seccomp: "strict"
834
874
  },
@@ -869,6 +909,7 @@ var DEFAULT_CONFIG = {
869
909
  defaultTtlMs: 86400000,
870
910
  cleanupIntervalMs: 3600000
871
911
  },
912
+ prebuiltImages: [],
872
913
  debug: false
873
914
  };
874
915
  function loadConfig(cwd) {
@@ -903,10 +944,6 @@ function mergeConfig(defaults, overrides) {
903
944
  },
904
945
  poolStrategy: overrides.poolStrategy ?? defaults.poolStrategy,
905
946
  poolSize: overrides.poolSize ?? defaults.poolSize,
906
- dependencies: {
907
- ...defaults.dependencies,
908
- ...overrides.dependencies
909
- },
910
947
  security: {
911
948
  seccomp: overrides.security?.seccomp ?? defaults.security.seccomp,
912
949
  customProfilePath: overrides.security?.customProfilePath ?? defaults.security.customProfilePath
@@ -926,6 +963,7 @@ function mergeConfig(defaults, overrides) {
926
963
  ...defaults.auth,
927
964
  ...overrides.auth
928
965
  },
966
+ prebuiltImages: overrides.prebuiltImages ?? defaults.prebuiltImages,
929
967
  debug: overrides.debug ?? defaults.debug
930
968
  };
931
969
  }
@@ -1345,11 +1383,9 @@ var EMBEDDED_DEFAULT_SECCOMP_PROFILE = JSON.stringify({
1345
1383
  ]
1346
1384
  });
1347
1385
 
1348
- // src/engine/docker.ts
1349
- init_image_builder();
1350
-
1351
1386
  // src/engine/managers/execution-manager.ts
1352
1387
  init_logger();
1388
+ init_utils();
1353
1389
  import { PassThrough } from "node:stream";
1354
1390
 
1355
1391
  class ExecutionManager {
@@ -1382,6 +1418,8 @@ class ExecutionManager {
1382
1418
  return ["npm", "install", "--prefix", "/sandbox", ...packages];
1383
1419
  case "bun":
1384
1420
  return ["bun", "install", "-g", "--global-dir=/sandbox/.bun-global", ...packages];
1421
+ case "agent":
1422
+ return ["bun", "install", "-g", "--global-dir=/sandbox/.bun-global", ...packages];
1385
1423
  case "deno":
1386
1424
  return ["sh", "-c", packages.map((p) => `deno cache ${p}`).join(" && ")];
1387
1425
  case "bash":
@@ -1409,7 +1447,7 @@ class ExecutionManager {
1409
1447
  env.push("npm_config_fetch_retry_mintimeout=1000");
1410
1448
  env.push("NPM_CONFIG_FETCH_RETRY_MAXTIMEOUT=2000");
1411
1449
  env.push("npm_config_fetch_retry_maxtimeout=2000");
1412
- } else if (runtime === "bun") {
1450
+ } else if (runtime === "bun" || runtime === "agent") {
1413
1451
  env.push("BUN_INSTALL_GLOBAL_DIR=/sandbox/.bun-global");
1414
1452
  env.push("BUN_INSTALL_CACHE_DIR=/sandbox/.bun-cache");
1415
1453
  env.push("BUN_INSTALL_BIN=/sandbox/.bun-global/bin");
@@ -1453,6 +1491,63 @@ class ExecutionManager {
1453
1491
  stream.on("error", reject);
1454
1492
  });
1455
1493
  }
1494
+ async runSetupScript(container, script, timeoutMs, volumeManager) {
1495
+ const scriptPath = "/sandbox/.isol8-setup.sh";
1496
+ await volumeManager.writeFileViaExec(container, scriptPath, script);
1497
+ const chmodExec = await container.exec({
1498
+ Cmd: ["chmod", "+x", scriptPath],
1499
+ User: "sandbox"
1500
+ });
1501
+ await chmodExec.start({ Detach: true });
1502
+ let chmodInfo = await chmodExec.inspect();
1503
+ while (chmodInfo.Running) {
1504
+ await new Promise((r) => setTimeout(r, 5));
1505
+ chmodInfo = await chmodExec.inspect();
1506
+ }
1507
+ const timeoutSec = Math.max(1, Math.ceil(timeoutMs / 1000));
1508
+ const cmd = this.wrapWithTimeout(["bash", scriptPath], timeoutSec);
1509
+ logger.debug(`Running setup script: ${JSON.stringify(cmd)}`);
1510
+ const env = [
1511
+ "PATH=/sandbox/.local/bin:/sandbox/.npm-global/bin:/sandbox/.bun-global/bin:/usr/local/bin:/usr/bin:/bin:/usr/local/sbin:/usr/sbin:/sbin"
1512
+ ];
1513
+ const exec = await container.exec({
1514
+ Cmd: cmd,
1515
+ AttachStdout: true,
1516
+ AttachStderr: true,
1517
+ Env: env,
1518
+ WorkingDir: "/sandbox",
1519
+ User: "sandbox"
1520
+ });
1521
+ const stream = await exec.start({ Detach: false, Tty: false });
1522
+ return new Promise((resolve2, reject) => {
1523
+ let stderr = "";
1524
+ const stdoutStream = new PassThrough;
1525
+ const stderrStream = new PassThrough;
1526
+ container.modem.demuxStream(stream, stdoutStream, stderrStream);
1527
+ stderrStream.on("data", (chunk) => {
1528
+ const text = chunk.toString();
1529
+ stderr += text;
1530
+ logger.debug(`[setup:stderr] ${text.trimEnd()}`);
1531
+ });
1532
+ stdoutStream.on("data", (chunk) => {
1533
+ const text = chunk.toString();
1534
+ logger.debug(`[setup:stdout] ${text.trimEnd()}`);
1535
+ });
1536
+ stream.on("end", async () => {
1537
+ try {
1538
+ const info = await exec.inspect();
1539
+ if (info.ExitCode !== 0) {
1540
+ reject(new Error(`Setup script failed (exit code ${info.ExitCode}): ${stderr}`));
1541
+ } else {
1542
+ resolve2();
1543
+ }
1544
+ } catch (err) {
1545
+ reject(err);
1546
+ }
1547
+ });
1548
+ stream.on("error", reject);
1549
+ });
1550
+ }
1456
1551
  async* streamExecOutput(stream, exec, container, timeoutMs) {
1457
1552
  const queue = [];
1458
1553
  let resolve2 = null;
@@ -1707,6 +1802,7 @@ class NetworkManager {
1707
1802
  }
1708
1803
  }
1709
1804
  // src/engine/managers/volume-manager.ts
1805
+ init_utils();
1710
1806
  import { PassThrough as PassThrough2 } from "node:stream";
1711
1807
 
1712
1808
  class VolumeManager {
@@ -1787,13 +1883,13 @@ class VolumeManager {
1787
1883
  const b64Output = Buffer.concat(chunks).toString("utf-8").trim();
1788
1884
  return Buffer.from(b64Output, "base64");
1789
1885
  }
1790
- async getFileFromContainer(container, path) {
1791
- const stream = await container.getArchive({ path });
1886
+ async getFileFromContainer(container, path2) {
1887
+ const stream = await container.getArchive({ path: path2 });
1792
1888
  const chunks = [];
1793
1889
  for await (const chunk of stream) {
1794
1890
  chunks.push(chunk);
1795
1891
  }
1796
- return extractFromTar(Buffer.concat(chunks), path);
1892
+ return extractFromTar(Buffer.concat(chunks), path2);
1797
1893
  }
1798
1894
  async retrieveFiles(container, paths) {
1799
1895
  const files = {};
@@ -1805,19 +1901,19 @@ class VolumeManager {
1805
1901
  }
1806
1902
  return files;
1807
1903
  }
1808
- async putFile(container, path, content) {
1904
+ async putFile(container, path2, content) {
1809
1905
  if (this.readonlyRootFs) {
1810
- await this.writeFileViaExec(container, path, content);
1906
+ await this.writeFileViaExec(container, path2, content);
1811
1907
  } else {
1812
- const tar = createTarBuffer(path, content);
1908
+ const tar = createTarBuffer(path2, content);
1813
1909
  await container.putArchive(tar, { path: "/" });
1814
1910
  }
1815
1911
  }
1816
- async getFile(container, path) {
1912
+ async getFile(container, path2) {
1817
1913
  if (this.readonlyRootFs) {
1818
- return this.readFileViaExec(container, path);
1914
+ return this.readFileViaExec(container, path2);
1819
1915
  }
1820
- return this.getFileFromContainer(container, path);
1916
+ return this.getFileFromContainer(container, path2);
1821
1917
  }
1822
1918
  }
1823
1919
  // src/engine/pool.ts
@@ -2099,6 +2195,7 @@ function calculateResourceDelta(before, after) {
2099
2195
  }
2100
2196
 
2101
2197
  // src/engine/docker.ts
2198
+ init_utils();
2102
2199
  var SANDBOX_WORKDIR = "/sandbox";
2103
2200
  var MAX_OUTPUT_BYTES = 1024 * 1024;
2104
2201
 
@@ -2123,7 +2220,6 @@ class DockerIsol8 {
2123
2220
  logNetwork;
2124
2221
  poolStrategy;
2125
2222
  poolSize;
2126
- dependencies;
2127
2223
  auditLogger;
2128
2224
  remoteCodePolicy;
2129
2225
  networkManager;
@@ -2173,7 +2269,6 @@ class DockerIsol8 {
2173
2269
  this.logNetwork = options.logNetwork ?? false;
2174
2270
  this.poolStrategy = options.poolStrategy ?? "fast";
2175
2271
  this.poolSize = options.poolSize ?? { clean: 1, dirty: 1 };
2176
- this.dependencies = options.dependencies ?? {};
2177
2272
  this.remoteCodePolicy = options.remoteCode ?? {
2178
2273
  enabled: false,
2179
2274
  allowedSchemes: ["https"],
@@ -2217,7 +2312,8 @@ class DockerIsol8 {
2217
2312
  const adapters2 = typeof prewarm === "object" && prewarm.runtimes?.length ? prewarm.runtimes.map((runtime) => RuntimeRegistry.get(runtime)) : RuntimeRegistry.list();
2218
2313
  for (const adapter of adapters2) {
2219
2314
  try {
2220
- images.add(await this.resolveImage(adapter));
2315
+ const resolved = await this.resolveImage(adapter);
2316
+ images.add(resolved.image);
2221
2317
  } catch (err) {
2222
2318
  logger.debug(`[Pool] Pre-warm image resolution failed for ${adapter.name}: ${err}`);
2223
2319
  }
@@ -2251,6 +2347,7 @@ class DockerIsol8 {
2251
2347
  await this.semaphore.acquire();
2252
2348
  const startTime = Date.now();
2253
2349
  try {
2350
+ this.validateAgentRuntime(req);
2254
2351
  const request = await this.resolveExecutionRequest(req);
2255
2352
  const result = this.mode === "persistent" ? await this.executePersistent(request, startTime) : await this.executeEphemeral(request, startTime);
2256
2353
  return result;
@@ -2374,17 +2471,17 @@ class DockerIsol8 {
2374
2471
  } catch {}
2375
2472
  return logs;
2376
2473
  }
2377
- async putFile(path, content) {
2474
+ async putFile(path2, content) {
2378
2475
  if (!this.container) {
2379
2476
  throw new Error("No active container. Call execute() first in persistent mode.");
2380
2477
  }
2381
- await this.volumeManager.putFile(this.container, path, content);
2478
+ await this.volumeManager.putFile(this.container, path2, content);
2382
2479
  }
2383
- async getFile(path) {
2480
+ async getFile(path2) {
2384
2481
  if (!this.container) {
2385
2482
  throw new Error("No active container. Call execute() first in persistent mode.");
2386
2483
  }
2387
- return this.volumeManager.getFile(this.container, path);
2484
+ return this.volumeManager.getFile(this.container, path2);
2388
2485
  }
2389
2486
  get containerId() {
2390
2487
  return this.container?.id ?? null;
@@ -2392,10 +2489,13 @@ class DockerIsol8 {
2392
2489
  async* executeStream(req) {
2393
2490
  await this.semaphore.acquire();
2394
2491
  try {
2492
+ this.validateAgentRuntime(req);
2395
2493
  const request = await this.resolveExecutionRequest(req);
2396
2494
  const adapter = this.getAdapter(request.runtime);
2397
2495
  const timeoutMs = request.timeoutMs ?? this.defaultTimeoutMs;
2398
- const image = await this.resolveImage(adapter);
2496
+ const resolved = await this.resolveImage(adapter, request.installPackages);
2497
+ const image = resolved.image;
2498
+ const execWorkdir = request.workdir ? resolveWorkdir(request.workdir) : SANDBOX_WORKDIR;
2399
2499
  const container = await this.docker.createContainer({
2400
2500
  Image: image,
2401
2501
  Cmd: ["sleep", "infinity"],
@@ -2412,15 +2512,21 @@ class DockerIsol8 {
2412
2512
  const ext = request.fileExtension ?? adapter.getFileExtension();
2413
2513
  const filePath = `${SANDBOX_WORKDIR}/main${ext}`;
2414
2514
  await this.volumeManager.writeFileViaExec(container, filePath, request.code);
2415
- if (request.installPackages?.length) {
2416
- await this.executionManager.installPackages(container, request.runtime, request.installPackages, timeoutMs);
2515
+ if (resolved.remainingPackages.length > 0) {
2516
+ await this.executionManager.installPackages(container, request.runtime, resolved.remainingPackages, timeoutMs);
2517
+ }
2518
+ if (resolved.imageSetupScript) {
2519
+ await this.executionManager.runSetupScript(container, resolved.imageSetupScript, timeoutMs, this.volumeManager);
2520
+ }
2521
+ if (request.setupScript) {
2522
+ await this.executionManager.runSetupScript(container, request.setupScript, timeoutMs, this.volumeManager);
2417
2523
  }
2418
2524
  if (request.files) {
2419
2525
  for (const [fPath, fContent] of Object.entries(request.files)) {
2420
2526
  await this.volumeManager.writeFileViaExec(container, fPath, fContent);
2421
2527
  }
2422
2528
  }
2423
- const rawCmd = adapter.getCommand(request.code, filePath);
2529
+ const rawCmd = this.buildAdapterCommand(adapter, request, filePath);
2424
2530
  const timeoutSec = Math.ceil(timeoutMs / 1000);
2425
2531
  let cmd;
2426
2532
  if (request.stdin) {
@@ -2436,7 +2542,7 @@ class DockerIsol8 {
2436
2542
  Env: this.executionManager.buildEnv(request.env, this.networkManager.proxyPort, this.network, this.networkFilter),
2437
2543
  AttachStdout: true,
2438
2544
  AttachStderr: true,
2439
- WorkingDir: SANDBOX_WORKDIR,
2545
+ WorkingDir: execWorkdir,
2440
2546
  User: "sandbox"
2441
2547
  });
2442
2548
  const execStream = await exec.start({ Tty: false });
@@ -2454,55 +2560,90 @@ class DockerIsol8 {
2454
2560
  this.semaphore.release();
2455
2561
  }
2456
2562
  }
2457
- async resolveImage(adapter) {
2563
+ async resolveImage(adapter, requestedPackages) {
2458
2564
  if (this.overrideImage) {
2459
- return this.overrideImage;
2565
+ let imageSetupScript2;
2566
+ try {
2567
+ const { LABELS: LABELS2 } = await Promise.resolve().then(() => (init_image_builder(), exports_image_builder));
2568
+ const inspect = await this.docker.getImage(this.overrideImage).inspect();
2569
+ const labels = inspect.Config?.Labels ?? {};
2570
+ imageSetupScript2 = labels[LABELS2.setupScript] || undefined;
2571
+ } catch {}
2572
+ return {
2573
+ image: this.overrideImage,
2574
+ remainingPackages: requestedPackages ?? [],
2575
+ imageSetupScript: imageSetupScript2
2576
+ };
2460
2577
  }
2461
- const cacheKey = adapter.image;
2578
+ const cacheKey = `${adapter.name}:${(requestedPackages ?? []).join(",")}`;
2462
2579
  const cached = this.imageCache.get(cacheKey);
2463
2580
  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);
2581
+ let imageSetupScript2;
2471
2582
  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}`);
2583
+ const { LABELS: LABELS2 } = await Promise.resolve().then(() => (init_image_builder(), exports_image_builder));
2584
+ const inspect = await this.docker.getImage(cached).inspect();
2585
+ const labels = inspect.Config?.Labels ?? {};
2586
+ imageSetupScript2 = labels[LABELS2.setupScript] || undefined;
2587
+ } catch {}
2588
+ return { image: cached, remainingPackages: [], imageSetupScript: imageSetupScript2 };
2589
+ }
2590
+ const baseImage = adapter.image;
2591
+ let bestImage = baseImage;
2592
+ let remainingPackages = requestedPackages ?? [];
2593
+ let imageSetupScript;
2594
+ if (requestedPackages && requestedPackages.length > 0) {
2595
+ const { LABELS: LABELS2, normalizePackages: normalizePackages2 } = await Promise.resolve().then(() => (init_image_builder(), exports_image_builder));
2596
+ const normalizedReq = normalizePackages2(requestedPackages);
2597
+ const images = await this.docker.listImages({
2598
+ filters: {
2599
+ label: [`${LABELS2.runtime}=${adapter.name}`]
2600
+ }
2601
+ });
2602
+ for (const img of images) {
2603
+ if (!img.RepoTags || img.RepoTags.length === 0) {
2604
+ continue;
2605
+ }
2606
+ const depsLabel = img.Labels?.[LABELS2.dependencies];
2607
+ if (!depsLabel) {
2608
+ continue;
2609
+ }
2610
+ const imgDeps = depsLabel.split(",");
2611
+ if (img.RepoTags[0] && normalizedReq.length === imgDeps.length && normalizedReq.every((p) => imgDeps.includes(p))) {
2612
+ bestImage = img.RepoTags[0];
2613
+ remainingPackages = [];
2614
+ imageSetupScript = img.Labels?.[LABELS2.setupScript] || undefined;
2615
+ logger.debug(`[Docker] Found exact custom image match: ${bestImage}`);
2616
+ break;
2617
+ }
2618
+ if (img.RepoTags[0] && normalizedReq.every((p) => imgDeps.includes(p))) {
2619
+ bestImage = img.RepoTags[0];
2620
+ remainingPackages = [];
2621
+ imageSetupScript = img.Labels?.[LABELS2.setupScript] || undefined;
2622
+ logger.debug(`[Docker] Found superset custom image match: ${bestImage}`);
2623
+ }
2476
2624
  }
2477
2625
  }
2478
- if (resolvedImage === adapter.image) {
2479
- const legacyCustomTag = `${adapter.image}-custom`;
2626
+ if (bestImage !== baseImage && imageSetupScript === undefined) {
2480
2627
  try {
2481
- await this.docker.getImage(legacyCustomTag).inspect();
2482
- resolvedImage = legacyCustomTag;
2628
+ const { LABELS: LABELS2 } = await Promise.resolve().then(() => (init_image_builder(), exports_image_builder));
2629
+ const inspect = await this.docker.getImage(bestImage).inspect();
2630
+ const labels = inspect.Config?.Labels ?? {};
2631
+ imageSetupScript = labels[LABELS2.setupScript] || undefined;
2483
2632
  } catch {}
2484
2633
  }
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}...`);
2634
+ if (bestImage === baseImage) {
2635
+ try {
2636
+ await this.docker.getImage(baseImage).inspect();
2637
+ } catch {
2638
+ logger.debug(`[Docker] Base image ${baseImage} not found. Building...`);
2639
+ const { buildBaseImages: buildBaseImages2 } = await Promise.resolve().then(() => (init_image_builder(), exports_image_builder));
2501
2640
  await buildBaseImages2(this.docker, undefined, false, [adapter.name]);
2502
2641
  }
2503
2642
  }
2504
- this.imageCache.set(cacheKey, resolvedImage);
2505
- return resolvedImage;
2643
+ if (remainingPackages.length === 0) {
2644
+ this.imageCache.set(cacheKey, bestImage);
2645
+ }
2646
+ return { image: bestImage, remainingPackages, imageSetupScript };
2506
2647
  }
2507
2648
  ensurePool() {
2508
2649
  if (!this.pool) {
@@ -2527,7 +2668,9 @@ class DockerIsol8 {
2527
2668
  async executeEphemeral(req, startTime) {
2528
2669
  const adapter = this.getAdapter(req.runtime);
2529
2670
  const timeoutMs = req.timeoutMs ?? this.defaultTimeoutMs;
2530
- const image = await this.resolveImage(adapter);
2671
+ const resolved = await this.resolveImage(adapter, req.installPackages);
2672
+ const image = resolved.image;
2673
+ const execWorkdir = req.workdir ? resolveWorkdir(req.workdir) : SANDBOX_WORKDIR;
2531
2674
  const pool = this.ensurePool();
2532
2675
  const container = await pool.acquire(image);
2533
2676
  let startStats;
@@ -2545,21 +2688,27 @@ class DockerIsol8 {
2545
2688
  let rawCmd;
2546
2689
  if (canUseInline) {
2547
2690
  try {
2548
- rawCmd = adapter.getCommand(req.code);
2691
+ rawCmd = this.buildAdapterCommand(adapter, req);
2549
2692
  } catch {
2550
2693
  const ext = req.fileExtension ?? adapter.getFileExtension();
2551
2694
  const filePath = `${SANDBOX_WORKDIR}/main${ext}`;
2552
2695
  await this.volumeManager.writeFileViaExec(container, filePath, req.code);
2553
- rawCmd = adapter.getCommand(req.code, filePath);
2696
+ rawCmd = this.buildAdapterCommand(adapter, req, filePath);
2554
2697
  }
2555
2698
  } else {
2556
2699
  const ext = req.fileExtension ?? adapter.getFileExtension();
2557
2700
  const filePath = `${SANDBOX_WORKDIR}/main${ext}`;
2558
2701
  await this.volumeManager.writeFileViaExec(container, filePath, req.code);
2559
- rawCmd = adapter.getCommand(req.code, filePath);
2702
+ rawCmd = this.buildAdapterCommand(adapter, req, filePath);
2703
+ }
2704
+ if (resolved.remainingPackages.length > 0) {
2705
+ await this.executionManager.installPackages(container, req.runtime, resolved.remainingPackages, timeoutMs);
2560
2706
  }
2561
- if (req.installPackages?.length) {
2562
- await this.executionManager.installPackages(container, req.runtime, req.installPackages, timeoutMs);
2707
+ if (resolved.imageSetupScript) {
2708
+ await this.executionManager.runSetupScript(container, resolved.imageSetupScript, timeoutMs, this.volumeManager);
2709
+ }
2710
+ if (req.setupScript) {
2711
+ await this.executionManager.runSetupScript(container, req.setupScript, timeoutMs, this.volumeManager);
2563
2712
  }
2564
2713
  const timeoutSec = Math.ceil(timeoutMs / 1000);
2565
2714
  let cmd;
@@ -2581,7 +2730,7 @@ class DockerIsol8 {
2581
2730
  Env: this.executionManager.buildEnv(req.env, this.networkManager.proxyPort, this.network, this.networkFilter),
2582
2731
  AttachStdout: true,
2583
2732
  AttachStderr: true,
2584
- WorkingDir: SANDBOX_WORKDIR,
2733
+ WorkingDir: execWorkdir,
2585
2734
  User: "sandbox"
2586
2735
  });
2587
2736
  const start = performance.now();
@@ -2641,8 +2790,13 @@ class DockerIsol8 {
2641
2790
  async executePersistent(req, startTime) {
2642
2791
  const adapter = this.getAdapter(req.runtime);
2643
2792
  const timeoutMs = req.timeoutMs ?? this.defaultTimeoutMs;
2793
+ const execWorkdir = req.workdir ? resolveWorkdir(req.workdir) : SANDBOX_WORKDIR;
2794
+ let remainingPackages = req.installPackages ?? [];
2795
+ let imageSetupScript;
2644
2796
  if (!this.container) {
2645
- await this.startPersistentContainer(adapter);
2797
+ const started = await this.startPersistentContainer(adapter, req.installPackages);
2798
+ remainingPackages = started.remainingPackages;
2799
+ imageSetupScript = started.imageSetupScript;
2646
2800
  } else if (this.persistentRuntime?.name !== adapter.name) {
2647
2801
  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
2802
  }
@@ -2654,10 +2808,16 @@ class DockerIsol8 {
2654
2808
  await this.volumeManager.putFile(this.container, fPath, fContent);
2655
2809
  }
2656
2810
  }
2657
- const rawCmd = adapter.getCommand(req.code, filePath);
2811
+ const rawCmd = this.buildAdapterCommand(adapter, req, filePath);
2658
2812
  const timeoutSec = Math.ceil(timeoutMs / 1000);
2659
- if (req.installPackages?.length) {
2660
- await this.executionManager.installPackages(this.container, req.runtime, req.installPackages, timeoutMs);
2813
+ if (remainingPackages.length > 0) {
2814
+ await this.executionManager.installPackages(this.container, req.runtime, remainingPackages, timeoutMs);
2815
+ }
2816
+ if (imageSetupScript) {
2817
+ await this.executionManager.runSetupScript(this.container, imageSetupScript, timeoutMs, this.volumeManager);
2818
+ }
2819
+ if (req.setupScript) {
2820
+ await this.executionManager.runSetupScript(this.container, req.setupScript, timeoutMs, this.volumeManager);
2661
2821
  }
2662
2822
  let cmd;
2663
2823
  if (req.stdin) {
@@ -2674,7 +2834,7 @@ class DockerIsol8 {
2674
2834
  Env: execEnv,
2675
2835
  AttachStdout: true,
2676
2836
  AttachStderr: true,
2677
- WorkingDir: SANDBOX_WORKDIR,
2837
+ WorkingDir: execWorkdir,
2678
2838
  User: "sandbox"
2679
2839
  });
2680
2840
  const start = performance.now();
@@ -2729,10 +2889,10 @@ class DockerIsol8 {
2729
2889
  async retrieveFiles(container, paths) {
2730
2890
  return this.volumeManager.retrieveFiles(container, paths);
2731
2891
  }
2732
- async startPersistentContainer(adapter) {
2733
- const image = await this.resolveImage(adapter);
2892
+ async startPersistentContainer(adapter, requestedPackages) {
2893
+ const resolved = await this.resolveImage(adapter, requestedPackages);
2734
2894
  this.container = await this.docker.createContainer({
2735
- Image: image,
2895
+ Image: resolved.image,
2736
2896
  Cmd: ["sleep", "infinity"],
2737
2897
  WorkingDir: SANDBOX_WORKDIR,
2738
2898
  Env: this.executionManager.buildEnv(undefined, this.networkManager.proxyPort, this.network, this.networkFilter),
@@ -2748,10 +2908,35 @@ class DockerIsol8 {
2748
2908
  await this.networkManager.startProxy(this.container);
2749
2909
  await this.networkManager.setupIptables(this.container);
2750
2910
  this.persistentRuntime = adapter;
2911
+ return {
2912
+ remainingPackages: resolved.remainingPackages,
2913
+ imageSetupScript: resolved.imageSetupScript
2914
+ };
2751
2915
  }
2752
2916
  getAdapter(runtime) {
2753
2917
  return RuntimeRegistry.get(runtime);
2754
2918
  }
2919
+ validateAgentRuntime(req) {
2920
+ if (req.runtime !== "agent") {
2921
+ return;
2922
+ }
2923
+ if (this.network !== "filtered") {
2924
+ throw new Error(`Agent runtime requires network mode "filtered". The AI coding agent needs network access to reach its LLM provider API. Use --net filtered --allow "api.anthropic.com" (or your provider's domain).`);
2925
+ }
2926
+ const whitelist = this.networkFilter?.whitelist ?? [];
2927
+ if (whitelist.length === 0) {
2928
+ throw new Error(`Agent runtime requires at least one network whitelist entry. The AI coding agent needs to reach its LLM provider API. Use --allow "api.anthropic.com" (or your provider's domain).`);
2929
+ }
2930
+ }
2931
+ buildAdapterCommand(adapter, req, filePath) {
2932
+ if (adapter.getCommandWithOptions) {
2933
+ return adapter.getCommandWithOptions(req.code, {
2934
+ filePath,
2935
+ agentFlags: req.agentFlags
2936
+ });
2937
+ }
2938
+ return adapter.getCommand(req.code, filePath);
2939
+ }
2755
2940
  buildHostConfig() {
2756
2941
  const config = {
2757
2942
  Memory: parseMemoryLimit(this.memoryLimit),
@@ -2860,7 +3045,7 @@ init_logger();
2860
3045
  // package.json
2861
3046
  var package_default = {
2862
3047
  name: "@isol8/core",
2863
- version: "0.17.0",
3048
+ version: "0.19.0",
2864
3049
  description: "Sandboxed code execution engine for AI agents and apps (Docker, runtime and network controls)",
2865
3050
  author: "Illusion47586",
2866
3051
  license: "MIT",
@@ -2928,8 +3113,7 @@ var VERSION = package_default.version;
2928
3113
  export {
2929
3114
  logger,
2930
3115
  loadConfig,
2931
- getCustomImageTag,
2932
- buildCustomImages,
3116
+ imageExists,
2933
3117
  buildCustomImage,
2934
3118
  buildBaseImages,
2935
3119
  bashAdapter,
@@ -2939,9 +3123,11 @@ export {
2939
3123
  RemoteIsol8,
2940
3124
  PythonAdapter,
2941
3125
  NodeAdapter,
3126
+ LABELS,
2942
3127
  DockerIsol8,
2943
3128
  DenoAdapter,
2944
- BunAdapter
3129
+ BunAdapter,
3130
+ AgentAdapter
2945
3131
  };
2946
3132
 
2947
- //# debugId=52A84C2338D09E5564756E2164756E21
3133
+ //# debugId=944C4599E61DE68D64756E2164756E21