isol8 0.10.2 → 0.11.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/cli.js CHANGED
@@ -54803,6 +54803,13 @@ function mergeConfig(defaults, overrides) {
54803
54803
  seccomp: overrides.security?.seccomp ?? defaults.security.seccomp,
54804
54804
  customProfilePath: overrides.security?.customProfilePath ?? defaults.security.customProfilePath
54805
54805
  },
54806
+ remoteCode: {
54807
+ ...defaults.remoteCode,
54808
+ ...overrides.remoteCode,
54809
+ allowedSchemes: overrides.remoteCode?.allowedSchemes ?? defaults.remoteCode.allowedSchemes,
54810
+ allowedHosts: overrides.remoteCode?.allowedHosts ?? defaults.remoteCode.allowedHosts,
54811
+ blockedHosts: overrides.remoteCode?.blockedHosts ?? defaults.remoteCode.blockedHosts
54812
+ },
54806
54813
  audit: {
54807
54814
  ...defaults.audit,
54808
54815
  ...overrides.audit
@@ -54834,6 +54841,28 @@ var init_config = __esm(() => {
54834
54841
  security: {
54835
54842
  seccomp: "strict"
54836
54843
  },
54844
+ remoteCode: {
54845
+ enabled: false,
54846
+ allowedSchemes: ["https"],
54847
+ allowedHosts: [],
54848
+ blockedHosts: [
54849
+ "^localhost$",
54850
+ "^127(?:\\.[0-9]{1,3}){3}$",
54851
+ "^\\[::1\\]$",
54852
+ "^::1$",
54853
+ "^10(?:\\.[0-9]{1,3}){3}$",
54854
+ "^172\\.(?:1[6-9]|2[0-9]|3[0-1])(?:\\.[0-9]{1,3}){2}$",
54855
+ "^192\\.168(?:\\.[0-9]{1,3}){2}$",
54856
+ "^169\\.254(?:\\.[0-9]{1,3}){2}$",
54857
+ "^metadata\\.google\\.internal$",
54858
+ "^169\\.254\\.169\\.254$"
54859
+ ],
54860
+ maxCodeSize: 10 * 1024 * 1024,
54861
+ fetchTimeoutMs: 30000,
54862
+ requireHash: false,
54863
+ enableCache: true,
54864
+ cacheTtl: 3600
54865
+ },
54837
54866
  audit: {
54838
54867
  enabled: false,
54839
54868
  destination: "filesystem",
@@ -55174,6 +55203,180 @@ var init_audit = __esm(() => {
55174
55203
  init_logger();
55175
55204
  });
55176
55205
 
55206
+ // src/engine/code-fetcher.ts
55207
+ import { createHash } from "node:crypto";
55208
+ import { lookup as dnsLookup } from "node:dns/promises";
55209
+ import { isIP } from "node:net";
55210
+ function sha256Hex(input) {
55211
+ return createHash("sha256").update(input, "utf-8").digest("hex");
55212
+ }
55213
+ function normalizeScheme(url) {
55214
+ return url.protocol.replace(/:$/, "").toLowerCase();
55215
+ }
55216
+ function isBlockedByPattern(host, patterns) {
55217
+ return patterns.some((pattern) => new RegExp(pattern, "i").test(host));
55218
+ }
55219
+ function isAllowedByPattern(host, patterns) {
55220
+ if (patterns.length === 0) {
55221
+ return true;
55222
+ }
55223
+ return patterns.some((pattern) => new RegExp(pattern, "i").test(host));
55224
+ }
55225
+ function isPrivateIpv4(ip) {
55226
+ const parts = ip.split(IPV4_SEPARATOR).map((v) => Number.parseInt(v, 10));
55227
+ if (parts.length !== 4 || parts.some((p) => Number.isNaN(p))) {
55228
+ return false;
55229
+ }
55230
+ const a = parts[0];
55231
+ const b = parts[1];
55232
+ if (a === 10 || a === 127 || a === 0) {
55233
+ return true;
55234
+ }
55235
+ if (a === 169 && b === 254) {
55236
+ return true;
55237
+ }
55238
+ if (a === 172 && b >= 16 && b <= 31) {
55239
+ return true;
55240
+ }
55241
+ if (a === 192 && b === 168) {
55242
+ return true;
55243
+ }
55244
+ if (a === 100 && b >= 64 && b <= 127) {
55245
+ return true;
55246
+ }
55247
+ return false;
55248
+ }
55249
+ function isPrivateIpv6(ip) {
55250
+ const normalized = ip.toLowerCase();
55251
+ if (normalized === IPV6_LOOPBACK) {
55252
+ return true;
55253
+ }
55254
+ return normalized.startsWith("fc") || normalized.startsWith("fd") || normalized.startsWith("fe8") || normalized.startsWith("fe9") || normalized.startsWith("fea") || normalized.startsWith("feb");
55255
+ }
55256
+ function isPrivateIp(ip) {
55257
+ const family = isIP(ip);
55258
+ if (family === 4) {
55259
+ return isPrivateIpv4(ip);
55260
+ }
55261
+ if (family === 6) {
55262
+ return isPrivateIpv6(ip);
55263
+ }
55264
+ return false;
55265
+ }
55266
+ async function assertHostResolvesPublic(host, lookupFn) {
55267
+ if (isIP(host) && isPrivateIp(host)) {
55268
+ throw new Error(`Blocked code URL host: ${host}`);
55269
+ }
55270
+ try {
55271
+ const records = await lookupFn(host);
55272
+ for (const record of records) {
55273
+ if (isPrivateIp(record.address)) {
55274
+ throw new Error(`Blocked code URL host: ${host}`);
55275
+ }
55276
+ }
55277
+ } catch (err) {
55278
+ if (err instanceof Error && err.message.startsWith("Blocked code URL host:")) {
55279
+ throw err;
55280
+ }
55281
+ throw new Error(`Failed to resolve code URL host: ${host}`);
55282
+ }
55283
+ }
55284
+ function decodeUtf8(content) {
55285
+ const decoder = new TextDecoder("utf-8", { fatal: true });
55286
+ const text = decoder.decode(content);
55287
+ if (text.includes("\x00")) {
55288
+ throw new Error("Fetched code appears to be binary content");
55289
+ }
55290
+ return text;
55291
+ }
55292
+ async function fetchRemoteCode(request, policy, deps = {}) {
55293
+ if (!policy.enabled) {
55294
+ throw new Error("Remote code fetching is disabled. Set remoteCode.enabled=true to allow it.");
55295
+ }
55296
+ const fetchFn = deps.fetchFn ?? globalThis.fetch;
55297
+ const lookupFn = deps.lookupFn ?? (async (hostname) => {
55298
+ const records = await dnsLookup(hostname, { all: true, verbatim: true });
55299
+ return records;
55300
+ });
55301
+ if (!request.codeUrl) {
55302
+ throw new Error("codeUrl is required for remote code fetching");
55303
+ }
55304
+ const url = new URL(request.codeUrl);
55305
+ const scheme = normalizeScheme(url);
55306
+ if (scheme === "http" && !request.allowInsecureCodeUrl) {
55307
+ throw new Error("Insecure code URL blocked. Use allowInsecureCodeUrl=true to allow HTTP.");
55308
+ }
55309
+ if (!policy.allowedSchemes.map((s) => s.toLowerCase()).includes(scheme)) {
55310
+ throw new Error(`URL scheme not allowed: ${scheme}`);
55311
+ }
55312
+ const host = url.hostname.toLowerCase();
55313
+ if (!isAllowedByPattern(host, policy.allowedHosts) || isBlockedByPattern(host, policy.blockedHosts)) {
55314
+ throw new Error(`Blocked code URL host: ${host}`);
55315
+ }
55316
+ await assertHostResolvesPublic(host, lookupFn);
55317
+ if (policy.requireHash && !request.codeHash) {
55318
+ throw new Error("Hash verification required: provide codeHash for remote code execution.");
55319
+ }
55320
+ const controller = new AbortController;
55321
+ const timeout = setTimeout(() => controller.abort(), policy.fetchTimeoutMs);
55322
+ let response;
55323
+ try {
55324
+ response = await fetchFn(url.toString(), {
55325
+ method: "GET",
55326
+ redirect: "follow",
55327
+ signal: controller.signal
55328
+ });
55329
+ } catch (err) {
55330
+ throw new Error(err instanceof Error && err.name === "AbortError" ? `Remote code fetch timed out after ${policy.fetchTimeoutMs}ms` : `Failed to fetch remote code: ${err instanceof Error ? err.message : String(err)}`);
55331
+ } finally {
55332
+ clearTimeout(timeout);
55333
+ }
55334
+ if (!response.ok) {
55335
+ throw new Error(`Failed to fetch remote code: HTTP ${response.status}`);
55336
+ }
55337
+ const contentLengthHeader = response.headers.get("content-length");
55338
+ if (contentLengthHeader) {
55339
+ const parsedLength = Number.parseInt(contentLengthHeader, 10);
55340
+ if (!Number.isNaN(parsedLength) && parsedLength > policy.maxCodeSize) {
55341
+ throw new Error(`Remote code exceeds maxCodeSize (${policy.maxCodeSize} bytes): ${parsedLength} bytes`);
55342
+ }
55343
+ }
55344
+ if (!response.body) {
55345
+ throw new Error("Remote code response body is empty");
55346
+ }
55347
+ const reader = response.body.getReader();
55348
+ const chunks = [];
55349
+ let totalBytes = 0;
55350
+ while (true) {
55351
+ const { done, value } = await reader.read();
55352
+ if (done) {
55353
+ break;
55354
+ }
55355
+ if (!value) {
55356
+ continue;
55357
+ }
55358
+ totalBytes += value.byteLength;
55359
+ if (totalBytes > policy.maxCodeSize) {
55360
+ throw new Error(`Remote code exceeds maxCodeSize (${policy.maxCodeSize} bytes)`);
55361
+ }
55362
+ chunks.push(value);
55363
+ }
55364
+ const buffer = new Uint8Array(totalBytes);
55365
+ let offset = 0;
55366
+ for (const chunk of chunks) {
55367
+ buffer.set(chunk, offset);
55368
+ offset += chunk.byteLength;
55369
+ }
55370
+ const code = decodeUtf8(buffer);
55371
+ const hash = sha256Hex(code);
55372
+ if (request.codeHash && hash.toLowerCase() !== request.codeHash.toLowerCase()) {
55373
+ throw new Error("Remote code hash mismatch");
55374
+ }
55375
+ return { code, url: url.toString(), hash };
55376
+ }
55377
+ var IPV4_SEPARATOR = ".", IPV6_LOOPBACK = "::1";
55378
+ var init_code_fetcher = () => {};
55379
+
55177
55380
  // src/engine/concurrency.ts
55178
55381
  class Semaphore {
55179
55382
  max;
@@ -55819,10 +56022,30 @@ class DockerIsol8 {
55819
56022
  poolStrategy;
55820
56023
  poolSize;
55821
56024
  auditLogger;
56025
+ remoteCodePolicy;
55822
56026
  container = null;
55823
56027
  persistentRuntime = null;
55824
56028
  pool = null;
55825
56029
  imageCache = new Map;
56030
+ async resolveExecutionRequest(req) {
56031
+ const inlineCode = req.code?.trim();
56032
+ const codeUrl = req.codeUrl?.trim();
56033
+ if (inlineCode && codeUrl) {
56034
+ throw new Error("ExecutionRequest.code and ExecutionRequest.codeUrl are mutually exclusive.");
56035
+ }
56036
+ if (!(inlineCode || codeUrl)) {
56037
+ throw new Error("ExecutionRequest must include either code or codeUrl.");
56038
+ }
56039
+ if (inlineCode) {
56040
+ return { ...req, code: req.code };
56041
+ }
56042
+ const fetched = await fetchRemoteCode({
56043
+ codeUrl,
56044
+ codeHash: req.codeHash,
56045
+ allowInsecureCodeUrl: req.allowInsecureCodeUrl
56046
+ }, this.remoteCodePolicy);
56047
+ return { ...req, code: fetched.code };
56048
+ }
55826
56049
  constructor(options = {}, maxConcurrent = 10) {
55827
56050
  this.docker = options.docker ?? new import_dockerode.default;
55828
56051
  this.mode = options.mode ?? "ephemeral";
@@ -55844,6 +56067,17 @@ class DockerIsol8 {
55844
56067
  this.logNetwork = options.logNetwork ?? false;
55845
56068
  this.poolStrategy = options.poolStrategy ?? "fast";
55846
56069
  this.poolSize = options.poolSize ?? { clean: 1, dirty: 1 };
56070
+ this.remoteCodePolicy = options.remoteCode ?? {
56071
+ enabled: false,
56072
+ allowedSchemes: ["https"],
56073
+ allowedHosts: [],
56074
+ blockedHosts: [],
56075
+ maxCodeSize: 10 * 1024 * 1024,
56076
+ fetchTimeoutMs: 30000,
56077
+ requireHash: false,
56078
+ enableCache: true,
56079
+ cacheTtl: 3600
56080
+ };
55847
56081
  if (options.audit) {
55848
56082
  this.auditLogger = new AuditLogger(options.audit);
55849
56083
  }
@@ -55872,7 +56106,8 @@ class DockerIsol8 {
55872
56106
  await this.semaphore.acquire();
55873
56107
  const startTime = Date.now();
55874
56108
  try {
55875
- const result = this.mode === "persistent" ? await this.executePersistent(req, startTime) : await this.executeEphemeral(req, startTime);
56109
+ const request = await this.resolveExecutionRequest(req);
56110
+ const result = this.mode === "persistent" ? await this.executePersistent(request, startTime) : await this.executeEphemeral(request, startTime);
55876
56111
  return result;
55877
56112
  } finally {
55878
56113
  this.semaphore.release();
@@ -56026,8 +56261,9 @@ class DockerIsol8 {
56026
56261
  async* executeStream(req) {
56027
56262
  await this.semaphore.acquire();
56028
56263
  try {
56029
- const adapter = this.getAdapter(req.runtime);
56030
- const timeoutMs = req.timeoutMs ?? this.defaultTimeoutMs;
56264
+ const request = await this.resolveExecutionRequest(req);
56265
+ const adapter = this.getAdapter(request.runtime);
56266
+ const timeoutMs = request.timeoutMs ?? this.defaultTimeoutMs;
56031
56267
  const image = await this.resolveImage(adapter);
56032
56268
  const container = await this.docker.createContainer({
56033
56269
  Image: image,
@@ -56044,23 +56280,23 @@ class DockerIsol8 {
56044
56280
  await startProxy(container, this.networkFilter);
56045
56281
  await setupIptables(container);
56046
56282
  }
56047
- const ext = req.fileExtension ?? adapter.getFileExtension();
56283
+ const ext = request.fileExtension ?? adapter.getFileExtension();
56048
56284
  const filePath = `${SANDBOX_WORKDIR}/main${ext}`;
56049
- await writeFileViaExec(container, filePath, req.code);
56050
- if (req.installPackages?.length) {
56051
- await installPackages(container, req.runtime, req.installPackages);
56285
+ await writeFileViaExec(container, filePath, request.code);
56286
+ if (request.installPackages?.length) {
56287
+ await installPackages(container, request.runtime, request.installPackages);
56052
56288
  }
56053
- if (req.files) {
56054
- for (const [fPath, fContent] of Object.entries(req.files)) {
56289
+ if (request.files) {
56290
+ for (const [fPath, fContent] of Object.entries(request.files)) {
56055
56291
  await writeFileViaExec(container, fPath, fContent);
56056
56292
  }
56057
56293
  }
56058
- const rawCmd = adapter.getCommand(req.code, filePath);
56294
+ const rawCmd = adapter.getCommand(request.code, filePath);
56059
56295
  const timeoutSec = Math.ceil(timeoutMs / 1000);
56060
56296
  let cmd;
56061
- if (req.stdin) {
56297
+ if (request.stdin) {
56062
56298
  const stdinPath = `${SANDBOX_WORKDIR}/_stdin`;
56063
- await writeFileViaExec(container, stdinPath, req.stdin);
56299
+ await writeFileViaExec(container, stdinPath, request.stdin);
56064
56300
  const cmdStr = rawCmd.map((a) => `'${a.replace(/'/g, "'\\''")}'`).join(" ");
56065
56301
  cmd = wrapWithTimeout(["sh", "-c", `cat ${stdinPath} | ${cmdStr}`], timeoutSec);
56066
56302
  } else {
@@ -56068,7 +56304,7 @@ class DockerIsol8 {
56068
56304
  }
56069
56305
  const exec = await container.exec({
56070
56306
  Cmd: cmd,
56071
- Env: this.buildEnv(req.env),
56307
+ Env: this.buildEnv(request.env),
56072
56308
  AttachStdout: true,
56073
56309
  AttachStderr: true,
56074
56310
  WorkingDir: SANDBOX_WORKDIR,
@@ -56627,6 +56863,7 @@ var init_docker = __esm(() => {
56627
56863
  init_runtime();
56628
56864
  init_logger();
56629
56865
  init_audit();
56866
+ init_code_fetcher();
56630
56867
  init_pool();
56631
56868
  import_dockerode = __toESM(require_docker(), 1);
56632
56869
  MAX_OUTPUT_BYTES = 1024 * 1024;
@@ -56637,7 +56874,7 @@ var package_default;
56637
56874
  var init_package = __esm(() => {
56638
56875
  package_default = {
56639
56876
  name: "isol8",
56640
- version: "0.10.1",
56877
+ version: "0.10.3",
56641
56878
  description: "Secure code execution engine for AI agents",
56642
56879
  author: "Illusion47586",
56643
56880
  license: "MIT",
@@ -58388,7 +58625,7 @@ async function createServer(options) {
58388
58625
  app.post("/execute", async (c) => {
58389
58626
  const body = await c.req.json();
58390
58627
  logger.debug(`[Server] POST /execute runtime=${body.request.runtime} sessionId=${body.sessionId ?? "ephemeral"}`);
58391
- logger.debug(`[Server] Code length: ${body.request.code.length} chars`);
58628
+ logger.debug(`[Server] Code source: ${body.request.codeUrl ? `url=${body.request.codeUrl}` : `inline (${body.request.code?.length ?? 0} chars)`}`);
58392
58629
  const engineOptions = {
58393
58630
  network: config.defaults.network,
58394
58631
  memoryLimit: config.defaults.memoryLimit,
@@ -58396,6 +58633,7 @@ async function createServer(options) {
58396
58633
  timeoutMs: config.defaults.timeoutMs,
58397
58634
  sandboxSize: config.defaults.sandboxSize,
58398
58635
  tmpSize: config.defaults.tmpSize,
58636
+ remoteCode: config.remoteCode,
58399
58637
  ...body.options,
58400
58638
  mode: body.sessionId ? "persistent" : "ephemeral",
58401
58639
  audit: config.audit
@@ -58407,11 +58645,12 @@ async function createServer(options) {
58407
58645
  logger.debug(`[Server] Reusing existing session: ${body.sessionId}`);
58408
58646
  engine = session.engine;
58409
58647
  session.lastAccessedAt = Date.now();
58648
+ session.isActive = true;
58410
58649
  } else {
58411
58650
  logger.debug(`[Server] Creating new session: ${body.sessionId}`);
58412
58651
  engine = new DockerIsol82(engineOptions, config.maxConcurrent);
58413
58652
  await engine.start();
58414
- sessions.set(body.sessionId, { engine, lastAccessedAt: Date.now() });
58653
+ sessions.set(body.sessionId, { engine, lastAccessedAt: Date.now(), isActive: true });
58415
58654
  }
58416
58655
  } else {
58417
58656
  logger.debug("[Server] Creating ephemeral engine");
@@ -58433,7 +58672,13 @@ async function createServer(options) {
58433
58672
  logger.debug(`[Server] Execution error: ${message}`);
58434
58673
  return c.json({ error: message }, 500);
58435
58674
  } finally {
58436
- if (!body.sessionId) {
58675
+ if (body.sessionId) {
58676
+ const session = sessions.get(body.sessionId);
58677
+ if (session) {
58678
+ session.isActive = false;
58679
+ session.lastAccessedAt = Date.now();
58680
+ }
58681
+ } else {
58437
58682
  logger.debug("[Server] Cleaning up ephemeral engine");
58438
58683
  await engine.stop();
58439
58684
  }
@@ -58442,7 +58687,7 @@ async function createServer(options) {
58442
58687
  app.post("/execute/stream", async (c) => {
58443
58688
  const body = await c.req.json();
58444
58689
  logger.debug(`[Server] POST /execute/stream runtime=${body.request.runtime}`);
58445
- logger.debug(`[Server] Code length: ${body.request.code.length} chars`);
58690
+ logger.debug(`[Server] Code source: ${body.request.codeUrl ? `url=${body.request.codeUrl}` : `inline (${body.request.code?.length ?? 0} chars)`}`);
58446
58691
  const engineOptions = {
58447
58692
  network: config.defaults.network,
58448
58693
  memoryLimit: config.defaults.memoryLimit,
@@ -58450,6 +58695,7 @@ async function createServer(options) {
58450
58695
  timeoutMs: config.defaults.timeoutMs,
58451
58696
  sandboxSize: config.defaults.sandboxSize,
58452
58697
  tmpSize: config.defaults.tmpSize,
58698
+ remoteCode: config.remoteCode,
58453
58699
  ...body.options,
58454
58700
  mode: "ephemeral"
58455
58701
  };
@@ -58543,6 +58789,9 @@ async function createServer(options) {
58543
58789
  const maxAge = config.cleanup.maxContainerAgeMs;
58544
58790
  const now = Date.now();
58545
58791
  for (const [id, session] of sessions) {
58792
+ if (session.isActive) {
58793
+ continue;
58794
+ }
58546
58795
  if (now - session.lastAccessedAt > maxAge) {
58547
58796
  logger.debug(`[Server] Auto-pruning stale session: ${id}`);
58548
58797
  await session.engine.stop();
@@ -58571,13 +58820,13 @@ import {
58571
58820
  chmodSync,
58572
58821
  existsSync as existsSync5,
58573
58822
  mkdirSync as mkdirSync2,
58574
- readFileSync as readFileSync3,
58823
+ readFileSync as readFileSync4,
58575
58824
  renameSync,
58576
58825
  unlinkSync as unlinkSync2,
58577
58826
  writeFileSync
58578
58827
  } from "node:fs";
58579
58828
  import { arch, homedir as homedir2, platform } from "node:os";
58580
- import { join as join3, resolve as resolve2 } from "node:path";
58829
+ import { join as join4, resolve as resolve2 } from "node:path";
58581
58830
 
58582
58831
  // node_modules/commander/esm.mjs
58583
58832
  var import__ = __toESM(require_commander(), 1);
@@ -61971,7 +62220,10 @@ init_docker();
61971
62220
 
61972
62221
  // src/engine/image-builder.ts
61973
62222
  init_runtime();
61974
- import { existsSync as existsSync4 } from "node:fs";
62223
+ init_logger();
62224
+ import { createHash as createHash2 } from "node:crypto";
62225
+ import { existsSync as existsSync4, readFileSync as readFileSync3 } from "node:fs";
62226
+ import { join as join3 } from "node:path";
61975
62227
  function resolveDockerDir() {
61976
62228
  const fromBundled = new URL("./docker", import.meta.url).pathname;
61977
62229
  if (existsSync4(fromBundled)) {
@@ -61980,16 +62232,82 @@ function resolveDockerDir() {
61980
62232
  return new URL("../../docker", import.meta.url).pathname;
61981
62233
  }
61982
62234
  var DOCKERFILE_DIR = resolveDockerDir();
61983
- async function buildBaseImages(docker, onProgress) {
62235
+ var LABELS = {
62236
+ dockerHash: "org.isol8.build.hash",
62237
+ depsHash: "org.isol8.deps.hash"
62238
+ };
62239
+ var DOCKER_BUILD_FILES = ["Dockerfile", "proxy.sh", "proxy-handler.sh"];
62240
+ function computeDockerDirHash() {
62241
+ const hash = createHash2("sha256");
62242
+ const files = [...DOCKER_BUILD_FILES].sort();
62243
+ for (const file of files) {
62244
+ const filePath = join3(DOCKERFILE_DIR, file);
62245
+ if (existsSync4(filePath)) {
62246
+ const content = readFileSync3(filePath);
62247
+ hash.update(file);
62248
+ hash.update(content);
62249
+ }
62250
+ }
62251
+ return hash.digest("hex");
62252
+ }
62253
+ function computeDepsHash(runtime, packages) {
62254
+ const hash = createHash2("sha256");
62255
+ hash.update(runtime);
62256
+ for (const pkg of [...packages].sort()) {
62257
+ hash.update(pkg);
62258
+ }
62259
+ return hash.digest("hex");
62260
+ }
62261
+ async function getImageLabels(docker, imageName) {
62262
+ try {
62263
+ const image = docker.getImage(imageName);
62264
+ const inspect = await image.inspect();
62265
+ return inspect.Config?.Labels ?? {};
62266
+ } catch {
62267
+ return null;
62268
+ }
62269
+ }
62270
+ async function removeImage(docker, imageId) {
62271
+ try {
62272
+ const image = docker.getImage(imageId);
62273
+ await image.remove();
62274
+ logger.debug(`[ImageBuilder] Removed old image: ${imageId.slice(0, 12)}`);
62275
+ } catch (err) {
62276
+ logger.debug(`[ImageBuilder] Could not remove image ${imageId.slice(0, 12)}: ${err}`);
62277
+ }
62278
+ }
62279
+ async function buildBaseImages(docker, onProgress, force = false) {
61984
62280
  const runtimes = RuntimeRegistry.list();
62281
+ const dockerHash = computeDockerDirHash();
62282
+ logger.debug(`[ImageBuilder] Docker directory hash: ${dockerHash.slice(0, 16)}...`);
61985
62283
  for (const adapter of runtimes) {
61986
62284
  const target = adapter.name;
62285
+ const imageName = adapter.image;
62286
+ if (!force) {
62287
+ const labels = await getImageLabels(docker, imageName);
62288
+ if (labels && labels[LABELS.dockerHash] === dockerHash) {
62289
+ logger.debug(`[ImageBuilder] Base image ${target} is up to date, skipping build`);
62290
+ onProgress?.({ runtime: target, status: "done", message: "Up to date" });
62291
+ continue;
62292
+ }
62293
+ }
62294
+ let oldImageId = null;
62295
+ try {
62296
+ const oldImage = await docker.getImage(imageName).inspect();
62297
+ oldImageId = oldImage.Id;
62298
+ logger.debug(`[ImageBuilder] Existing image ${target} ID: ${oldImageId.slice(0, 12)}`);
62299
+ } catch {
62300
+ logger.debug(`[ImageBuilder] No existing image for ${target}`);
62301
+ }
61987
62302
  onProgress?.({ runtime: target, status: "building" });
61988
62303
  try {
61989
- const stream = await docker.buildImage({ context: DOCKERFILE_DIR, src: ["Dockerfile", "proxy.sh", "proxy-handler.sh"] }, {
61990
- t: adapter.image,
62304
+ const stream = await docker.buildImage({ context: DOCKERFILE_DIR, src: DOCKER_BUILD_FILES }, {
62305
+ t: imageName,
61991
62306
  target,
61992
- dockerfile: "Dockerfile"
62307
+ dockerfile: "Dockerfile",
62308
+ labels: {
62309
+ [LABELS.dockerHash]: dockerHash
62310
+ }
61993
62311
  });
61994
62312
  await new Promise((resolve2, reject) => {
61995
62313
  docker.modem.followProgress(stream, (err) => {
@@ -62000,6 +62318,9 @@ async function buildBaseImages(docker, onProgress) {
62000
62318
  }
62001
62319
  });
62002
62320
  });
62321
+ if (oldImageId) {
62322
+ await removeImage(docker, oldImageId);
62323
+ }
62003
62324
  onProgress?.({ runtime: target, status: "done" });
62004
62325
  } catch (err) {
62005
62326
  const message = err instanceof Error ? err.message : String(err);
@@ -62008,26 +62329,44 @@ async function buildBaseImages(docker, onProgress) {
62008
62329
  }
62009
62330
  }
62010
62331
  }
62011
- async function buildCustomImages(docker, config, onProgress) {
62332
+ async function buildCustomImages(docker, config, onProgress, force = false) {
62012
62333
  const deps = config.dependencies;
62013
62334
  if (deps.python?.length) {
62014
- await buildCustomImage(docker, "python", deps.python, onProgress);
62335
+ await buildCustomImage(docker, "python", deps.python, onProgress, force);
62015
62336
  }
62016
62337
  if (deps.node?.length) {
62017
- await buildCustomImage(docker, "node", deps.node, onProgress);
62338
+ await buildCustomImage(docker, "node", deps.node, onProgress, force);
62018
62339
  }
62019
62340
  if (deps.bun?.length) {
62020
- await buildCustomImage(docker, "bun", deps.bun, onProgress);
62341
+ await buildCustomImage(docker, "bun", deps.bun, onProgress, force);
62021
62342
  }
62022
62343
  if (deps.deno?.length) {
62023
- await buildCustomImage(docker, "deno", deps.deno, onProgress);
62344
+ await buildCustomImage(docker, "deno", deps.deno, onProgress, force);
62024
62345
  }
62025
62346
  if (deps.bash?.length) {
62026
- await buildCustomImage(docker, "bash", deps.bash, onProgress);
62347
+ await buildCustomImage(docker, "bash", deps.bash, onProgress, force);
62027
62348
  }
62028
62349
  }
62029
- async function buildCustomImage(docker, runtime, packages, onProgress) {
62350
+ async function buildCustomImage(docker, runtime, packages, onProgress, force = false) {
62030
62351
  const tag = `isol8:${runtime}-custom`;
62352
+ const depsHash = computeDepsHash(runtime, packages);
62353
+ logger.debug(`[ImageBuilder] ${runtime} custom deps hash: ${depsHash.slice(0, 16)}...`);
62354
+ if (!force) {
62355
+ const labels = await getImageLabels(docker, tag);
62356
+ if (labels && labels[LABELS.depsHash] === depsHash) {
62357
+ logger.debug(`[ImageBuilder] Custom image ${runtime} is up to date, skipping build`);
62358
+ onProgress?.({ runtime, status: "done", message: "Up to date" });
62359
+ return;
62360
+ }
62361
+ }
62362
+ let oldImageId = null;
62363
+ try {
62364
+ const oldImage = await docker.getImage(tag).inspect();
62365
+ oldImageId = oldImage.Id;
62366
+ logger.debug(`[ImageBuilder] Existing custom image ${runtime} ID: ${oldImageId.slice(0, 12)}`);
62367
+ } catch {
62368
+ logger.debug(`[ImageBuilder] No existing custom image for ${runtime}`);
62369
+ }
62031
62370
  onProgress?.({ runtime, status: "building", message: `Custom: ${packages.join(", ")}` });
62032
62371
  let installCmd;
62033
62372
  switch (runtime) {
@@ -62059,7 +62398,10 @@ ${installCmd}
62059
62398
  const tarBuffer = createTarBuffer2("Dockerfile", dockerfileContent);
62060
62399
  const stream = await docker.buildImage(Readable.from(tarBuffer), {
62061
62400
  t: tag,
62062
- dockerfile: "Dockerfile"
62401
+ dockerfile: "Dockerfile",
62402
+ labels: {
62403
+ [LABELS.depsHash]: depsHash
62404
+ }
62063
62405
  });
62064
62406
  await new Promise((resolve2, reject) => {
62065
62407
  docker.modem.followProgress(stream, (err) => {
@@ -62070,6 +62412,9 @@ ${installCmd}
62070
62412
  }
62071
62413
  });
62072
62414
  });
62415
+ if (oldImageId) {
62416
+ await removeImage(docker, oldImageId);
62417
+ }
62073
62418
  onProgress?.({ runtime, status: "done" });
62074
62419
  }
62075
62420
 
@@ -62087,7 +62432,7 @@ program2.name("isol8").description("Secure code execution engine").version(VERSI
62087
62432
  logger.debug(`[CLI] Version: ${VERSION}`);
62088
62433
  logger.debug(`[CLI] Platform: ${platform()} ${arch()}`);
62089
62434
  });
62090
- program2.command("setup").description("Check Docker and build isol8 images").option("--python <packages>", "Additional Python packages (comma-separated)").option("--node <packages>", "Additional Node.js packages (comma-separated)").option("--bun <packages>", "Additional Bun packages (comma-separated)").option("--deno <packages>", "Additional Deno packages (comma-separated)").option("--bash <packages>", "Additional Bash packages (comma-separated)").action(async (opts) => {
62435
+ program2.command("setup").description("Check Docker and build isol8 images").option("--python <packages>", "Additional Python packages (comma-separated)").option("--node <packages>", "Additional Node.js packages (comma-separated)").option("--bun <packages>", "Additional Bun packages (comma-separated)").option("--deno <packages>", "Additional Deno packages (comma-separated)").option("--bash <packages>", "Additional Bash packages (comma-separated)").option("--force", "Force rebuild even if images are up to date").action(async (opts) => {
62091
62436
  const docker = new import_dockerode2.default;
62092
62437
  logger.debug("[Setup] Connecting to Docker daemon");
62093
62438
  const spinner = ora("Checking Docker...").start();
@@ -62102,7 +62447,7 @@ program2.command("setup").description("Check Docker and build isol8 images").opt
62102
62447
  process.exit(1);
62103
62448
  }
62104
62449
  spinner.start("Building isol8 images...");
62105
- logger.debug("[Setup] Building base images");
62450
+ logger.debug(`[Setup] Building base images (force=${opts.force ?? false})`);
62106
62451
  await buildBaseImages(docker, (progress) => {
62107
62452
  const status = progress.status === "error" ? "[ERR]" : progress.status === "done" ? "[OK]" : "[..]";
62108
62453
  if (progress.status === "building") {
@@ -62118,7 +62463,7 @@ program2.command("setup").description("Check Docker and build isol8 images").opt
62118
62463
  spinner.start();
62119
62464
  }
62120
62465
  }
62121
- });
62466
+ }, opts.force ?? false);
62122
62467
  if (spinner.isSpinning) {
62123
62468
  spinner.stop();
62124
62469
  }
@@ -62166,7 +62511,7 @@ program2.command("setup").description("Check Docker and build isol8 images").opt
62166
62511
  spinner.start();
62167
62512
  }
62168
62513
  }
62169
- });
62514
+ }, opts.force ?? false);
62170
62515
  if (spinner.isSpinning) {
62171
62516
  spinner.stop();
62172
62517
  }
@@ -62174,12 +62519,25 @@ program2.command("setup").description("Check Docker and build isol8 images").opt
62174
62519
  console.log(`
62175
62520
  [DONE] Setup complete!`);
62176
62521
  });
62177
- program2.command("run").description("Execute code in isol8").argument("[file]", "Script file to execute").option("-e, --eval <code>", "Execute inline code string").option("-r, --runtime <name>", "Force runtime (python, node, bun, deno, bash)").option("--net <mode>", "Network mode: none, host, filtered", "none").option("--allow <regex>", "Whitelist regex for filtered mode (repeatable)", collect, []).option("--deny <regex>", "Blacklist regex for filtered mode (repeatable)", collect, []).option("--out <file>", "Write output to file").option("--persistent", "Use persistent container").option("--timeout <ms>", "Execution timeout in milliseconds").option("--memory <limit>", "Memory limit (e.g. 512m, 1g)").option("--cpu <limit>", "CPU limit as fraction (e.g. 0.5, 2.0)").option("--image <name>", "Override Docker image").option("--pids-limit <n>", "Maximum number of processes").option("--writable", "Disable read-only root filesystem").option("--max-output <bytes>", "Maximum output size in bytes").option("--secret <KEY=VALUE>", "Secret env var (repeatable, values masked)", collect, []).option("--sandbox-size <size>", "Sandbox tmpfs size (e.g. 128m, 512m)").option("--tmp-size <size>", "Tmp tmpfs size (e.g. 256m, 512m)").option("--stdin <data>", "Data to pipe to stdin").option("--install <package>", "Install package for runtime (repeatable)", collect, []).option("--host <url>", "Execute on remote server").option("--key <key>", "API key for remote server").option("--no-stream", "Disable real-time output streaming").option("--debug", "Enable debug logging").option("--persist", "Keep container running after execution for inspection").option("--log-network", "Log all network requests (requires --net filtered)").option("--pool-strategy <mode>", "Pool strategy: fast (default) or secure", "fast").option("--pool-size <size>", "Pool size (number or 'clean,dirty' for fast mode)", "1,1").action(async (file, opts) => {
62178
- const { code, runtime, engineOptions, engine, stdinData, fileExtension } = await resolveRunInput(file, opts);
62522
+ program2.command("run").description("Execute code in isol8").argument("[file]", "Script file to execute").option("-e, --eval <code>", "Execute inline code string").option("-r, --runtime <name>", "Force runtime (python, node, bun, deno, bash)").option("--net <mode>", "Network mode: none, host, filtered", "none").option("--allow <regex>", "Whitelist regex for filtered mode (repeatable)", collect, []).option("--deny <regex>", "Blacklist regex for filtered mode (repeatable)", collect, []).option("--out <file>", "Write output to file").option("--persistent", "Use persistent container").option("--timeout <ms>", "Execution timeout in milliseconds").option("--memory <limit>", "Memory limit (e.g. 512m, 1g)").option("--cpu <limit>", "CPU limit as fraction (e.g. 0.5, 2.0)").option("--image <name>", "Override Docker image").option("--pids-limit <n>", "Maximum number of processes").option("--writable", "Disable read-only root filesystem").option("--max-output <bytes>", "Maximum output size in bytes").option("--secret <KEY=VALUE>", "Secret env var (repeatable, values masked)", collect, []).option("--sandbox-size <size>", "Sandbox tmpfs size (e.g. 128m, 512m)").option("--tmp-size <size>", "Tmp tmpfs size (e.g. 256m, 512m)").option("--stdin <data>", "Data to pipe to stdin").option("--install <package>", "Install package for runtime (repeatable)", collect, []).option("--url <url>", "Fetch code from URL").option("--github <path>", "GitHub shorthand: owner/repo/ref/path/to/file").option("--gist <path>", "Gist shorthand: gistId/file.ext").option("--hash <sha256>", "Expected SHA-256 hash of fetched code").option("--allow-insecure-code-url", "Allow insecure HTTP code URLs").option("--host <url>", "Execute on remote server").option("--key <key>", "API key for remote server").option("--no-stream", "Disable real-time output streaming").option("--debug", "Enable debug logging").option("--persist", "Keep container running after execution for inspection").option("--log-network", "Log all network requests (requires --net filtered)").option("--pool-strategy <mode>", "Pool strategy: fast (default) or secure", "fast").option("--pool-size <size>", "Pool size (number or 'clean,dirty' for fast mode)", "1,1").action(async (file, opts) => {
62523
+ const {
62524
+ code,
62525
+ codeUrl,
62526
+ codeHash,
62527
+ allowInsecureCodeUrl,
62528
+ runtime,
62529
+ engineOptions,
62530
+ engine,
62531
+ stdinData,
62532
+ fileExtension
62533
+ } = await resolveRunInput(file, opts);
62179
62534
  logger.debug(`[Run] Runtime: ${runtime}, mode: ${engineOptions.mode}`);
62180
62535
  logger.debug(`[Run] Network: ${engineOptions.network}, timeout: ${engineOptions.timeoutMs}ms`);
62181
62536
  logger.debug(`[Run] Memory: ${engineOptions.memoryLimit}, CPU: ${engineOptions.cpuLimit}`);
62182
- logger.debug(`[Run] Code length: ${code.length} chars`);
62537
+ logger.debug(`[Run] Code source: ${codeUrl ? `url=${codeUrl}` : "inline/file/stdin"}`);
62538
+ if (code) {
62539
+ logger.debug(`[Run] Code length: ${code.length} chars`);
62540
+ }
62183
62541
  if (stdinData) {
62184
62542
  logger.debug(`[Run] Stdin data provided (${stdinData.length} chars)`);
62185
62543
  }
@@ -62205,9 +62563,12 @@ program2.command("run").description("Execute code in isol8").argument("[file]",
62205
62563
  logger.debug("[Run] Engine started");
62206
62564
  spinner.text = "Running code...";
62207
62565
  const req = {
62208
- code,
62209
62566
  runtime,
62210
62567
  timeoutMs: engineOptions.timeoutMs,
62568
+ ...code ? { code } : {},
62569
+ ...codeUrl ? { codeUrl } : {},
62570
+ ...codeHash ? { codeHash } : {},
62571
+ ...allowInsecureCodeUrl ? { allowInsecureCodeUrl } : {},
62211
62572
  ...stdinData ? { stdin: stdinData } : {},
62212
62573
  ...opts.install.length > 0 ? { installPackages: opts.install } : {},
62213
62574
  fileExtension
@@ -62367,7 +62728,7 @@ async function downloadServerBinary(binaryPath) {
62367
62728
  }
62368
62729
  process.exit(1);
62369
62730
  }
62370
- const binDir = join3(homedir2(), ".isol8", "bin");
62731
+ const binDir = join4(homedir2(), ".isol8", "bin");
62371
62732
  mkdirSync2(binDir, { recursive: true });
62372
62733
  const tmpPath = `${binaryPath}.tmp`;
62373
62734
  const buffer = Buffer.from(await response.arrayBuffer());
@@ -62399,8 +62760,8 @@ async function promptYesNo(question) {
62399
62760
  return normalized === "" || normalized === "y" || normalized === "yes";
62400
62761
  }
62401
62762
  async function ensureServerBinary(forceUpdate) {
62402
- const binDir = join3(homedir2(), ".isol8", "bin");
62403
- const binaryPath = join3(binDir, "isol8-server");
62763
+ const binDir = join4(homedir2(), ".isol8", "bin");
62764
+ const binaryPath = join4(binDir, "isol8-server");
62404
62765
  logger.debug(`[Serve] Binary path: ${binaryPath}, forceUpdate: ${forceUpdate}`);
62405
62766
  if (forceUpdate) {
62406
62767
  logger.debug("[Serve] Force update requested");
@@ -62430,8 +62791,8 @@ async function ensureServerBinary(forceUpdate) {
62430
62791
  program2.command("config").description("Show the resolved isol8 configuration").option("--json", "Output as raw JSON").action((opts) => {
62431
62792
  const config = loadConfig();
62432
62793
  const searchPaths = [
62433
- join3(resolve2(process.cwd()), "isol8.config.json"),
62434
- join3(homedir2(), ".isol8", "config.json")
62794
+ join4(resolve2(process.cwd()), "isol8.config.json"),
62795
+ join4(homedir2(), ".isol8", "config.json")
62435
62796
  ];
62436
62797
  const loadedFrom = searchPaths.find((p) => existsSync5(p));
62437
62798
  logger.debug(`[Config] Config source: ${loadedFrom ?? "defaults"}`);
@@ -62466,6 +62827,13 @@ Isol8 Configuration
62466
62827
  } else {
62467
62828
  console.log(" Whitelist: (none)");
62468
62829
  }
62830
+ console.log("");
62831
+ console.log(" ── Remote Code ──");
62832
+ console.log(` Enabled: ${config.remoteCode.enabled ? "yes" : "no"}`);
62833
+ console.log(` Schemes: ${config.remoteCode.allowedSchemes.join(", ")}`);
62834
+ console.log(` Max code size: ${config.remoteCode.maxCodeSize} bytes`);
62835
+ console.log(` Fetch timeout: ${config.remoteCode.fetchTimeoutMs}ms`);
62836
+ console.log(` Require hash: ${config.remoteCode.requireHash ? "yes" : "no"}`);
62469
62837
  if (config.network.blacklist.length > 0) {
62470
62838
  console.log(` Blacklist: ${config.network.blacklist.join(", ")}`);
62471
62839
  } else {
@@ -62560,8 +62928,25 @@ async function resolveRunInput(file, opts) {
62560
62928
  const config = loadConfig();
62561
62929
  logger.debug("[Run] Config loaded");
62562
62930
  let code;
62931
+ let codeUrl;
62932
+ let codeHash;
62933
+ let allowInsecureCodeUrl = false;
62563
62934
  let runtime;
62564
- if (opts.eval) {
62935
+ if (opts.url || opts.github || opts.gist) {
62936
+ if (file || opts.eval) {
62937
+ console.error("[ERR] --url/--github/--gist cannot be used with file input or --eval.");
62938
+ process.exit(1);
62939
+ }
62940
+ codeUrl = resolveCodeUrl(opts);
62941
+ codeHash = opts.hash ?? undefined;
62942
+ allowInsecureCodeUrl = opts.allowInsecureCodeUrl ?? false;
62943
+ runtime = opts.runtime ?? detectRuntimeFromPath(new URL(codeUrl).pathname);
62944
+ if (!runtime) {
62945
+ console.error("[ERR] Cannot detect runtime from URL path. Use --runtime to specify.");
62946
+ process.exit(1);
62947
+ }
62948
+ logger.debug(`[Run] Remote code URL: ${codeUrl}`);
62949
+ } else if (opts.eval) {
62565
62950
  code = opts.eval;
62566
62951
  runtime = opts.runtime ?? "python";
62567
62952
  logger.debug(`[Run] Inline eval, runtime: ${runtime}`);
@@ -62572,7 +62957,7 @@ async function resolveRunInput(file, opts) {
62572
62957
  console.error(`[ERR] File not found: ${file}`);
62573
62958
  process.exit(1);
62574
62959
  }
62575
- code = readFileSync3(filePath, "utf-8");
62960
+ code = readFileSync4(filePath, "utf-8");
62576
62961
  if (opts.runtime) {
62577
62962
  runtime = opts.runtime;
62578
62963
  logger.debug(`[Run] Runtime specified: ${runtime}`);
@@ -62612,6 +62997,7 @@ async function resolveRunInput(file, opts) {
62612
62997
  debug: opts.debug ?? config.debug,
62613
62998
  persist: opts.persist ?? false,
62614
62999
  ...opts.logNetwork ? { logNetwork: true } : {},
63000
+ remoteCode: config.remoteCode,
62615
63001
  poolStrategy: opts.poolStrategy === "secure" ? "secure" : "fast",
62616
63002
  poolSize: opts.poolSize ? opts.poolSize.includes(",") ? {
62617
63003
  clean: Number.parseInt(opts.poolSize.split(",")[0], 10),
@@ -62650,7 +63036,48 @@ async function resolveRunInput(file, opts) {
62650
63036
  logger.debug("[Run] Using local Docker engine");
62651
63037
  engine = new DockerIsol8(engineOptions, config.maxConcurrent);
62652
63038
  }
62653
- return { code, runtime, engineOptions, engine, stdinData, fileExtension };
63039
+ return {
63040
+ code,
63041
+ codeUrl,
63042
+ codeHash,
63043
+ allowInsecureCodeUrl,
63044
+ runtime,
63045
+ engineOptions,
63046
+ engine,
63047
+ stdinData,
63048
+ fileExtension
63049
+ };
63050
+ }
63051
+ function resolveCodeUrl(opts) {
63052
+ if (typeof opts.url === "string") {
63053
+ return opts.url;
63054
+ }
63055
+ if (typeof opts.github === "string") {
63056
+ const parts = opts.github.split("/");
63057
+ if (parts.length < 4) {
63058
+ console.error("[ERR] --github format must be owner/repo/ref/path/to/file");
63059
+ process.exit(1);
63060
+ }
63061
+ const [owner, repo, ref, ...pathParts] = parts;
63062
+ return `https://raw.githubusercontent.com/${owner}/${repo}/${ref}/${pathParts.join("/")}`;
63063
+ }
63064
+ if (typeof opts.gist === "string") {
63065
+ const [gistId, ...fileParts] = opts.gist.split("/");
63066
+ if (!gistId || fileParts.length === 0) {
63067
+ console.error("[ERR] --gist format must be gistId/file.ext");
63068
+ process.exit(1);
63069
+ }
63070
+ return `https://gist.githubusercontent.com/${gistId}/raw/${fileParts.join("/")}`;
63071
+ }
63072
+ console.error("[ERR] Missing code URL source.");
63073
+ process.exit(1);
63074
+ }
63075
+ function detectRuntimeFromPath(pathValue) {
63076
+ try {
63077
+ return RuntimeRegistry.detect(pathValue).name;
63078
+ } catch {
63079
+ return;
63080
+ }
62654
63081
  }
62655
63082
  function collect(value, previous) {
62656
63083
  return previous.concat([value]);
@@ -62661,4 +63088,4 @@ if (!process.argv.slice(2).length) {
62661
63088
  }
62662
63089
  program2.parse();
62663
63090
 
62664
- //# debugId=92C54A0066AB3BEC64756E2164756E21
63091
+ //# debugId=280ED4C71DBDC32964756E2164756E21