isol8 0.10.3 → 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.2",
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
@@ -58449,7 +58687,7 @@ async function createServer(options) {
58449
58687
  app.post("/execute/stream", async (c) => {
58450
58688
  const body = await c.req.json();
58451
58689
  logger.debug(`[Server] POST /execute/stream runtime=${body.request.runtime}`);
58452
- 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)`}`);
58453
58691
  const engineOptions = {
58454
58692
  network: config.defaults.network,
58455
58693
  memoryLimit: config.defaults.memoryLimit,
@@ -58457,6 +58695,7 @@ async function createServer(options) {
58457
58695
  timeoutMs: config.defaults.timeoutMs,
58458
58696
  sandboxSize: config.defaults.sandboxSize,
58459
58697
  tmpSize: config.defaults.tmpSize,
58698
+ remoteCode: config.remoteCode,
58460
58699
  ...body.options,
58461
58700
  mode: "ephemeral"
58462
58701
  };
@@ -58581,13 +58820,13 @@ import {
58581
58820
  chmodSync,
58582
58821
  existsSync as existsSync5,
58583
58822
  mkdirSync as mkdirSync2,
58584
- readFileSync as readFileSync3,
58823
+ readFileSync as readFileSync4,
58585
58824
  renameSync,
58586
58825
  unlinkSync as unlinkSync2,
58587
58826
  writeFileSync
58588
58827
  } from "node:fs";
58589
58828
  import { arch, homedir as homedir2, platform } from "node:os";
58590
- import { join as join3, resolve as resolve2 } from "node:path";
58829
+ import { join as join4, resolve as resolve2 } from "node:path";
58591
58830
 
58592
58831
  // node_modules/commander/esm.mjs
58593
58832
  var import__ = __toESM(require_commander(), 1);
@@ -61981,7 +62220,10 @@ init_docker();
61981
62220
 
61982
62221
  // src/engine/image-builder.ts
61983
62222
  init_runtime();
61984
- 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";
61985
62227
  function resolveDockerDir() {
61986
62228
  const fromBundled = new URL("./docker", import.meta.url).pathname;
61987
62229
  if (existsSync4(fromBundled)) {
@@ -61990,16 +62232,82 @@ function resolveDockerDir() {
61990
62232
  return new URL("../../docker", import.meta.url).pathname;
61991
62233
  }
61992
62234
  var DOCKERFILE_DIR = resolveDockerDir();
61993
- 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) {
61994
62280
  const runtimes = RuntimeRegistry.list();
62281
+ const dockerHash = computeDockerDirHash();
62282
+ logger.debug(`[ImageBuilder] Docker directory hash: ${dockerHash.slice(0, 16)}...`);
61995
62283
  for (const adapter of runtimes) {
61996
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
+ }
61997
62302
  onProgress?.({ runtime: target, status: "building" });
61998
62303
  try {
61999
- const stream = await docker.buildImage({ context: DOCKERFILE_DIR, src: ["Dockerfile", "proxy.sh", "proxy-handler.sh"] }, {
62000
- t: adapter.image,
62304
+ const stream = await docker.buildImage({ context: DOCKERFILE_DIR, src: DOCKER_BUILD_FILES }, {
62305
+ t: imageName,
62001
62306
  target,
62002
- dockerfile: "Dockerfile"
62307
+ dockerfile: "Dockerfile",
62308
+ labels: {
62309
+ [LABELS.dockerHash]: dockerHash
62310
+ }
62003
62311
  });
62004
62312
  await new Promise((resolve2, reject) => {
62005
62313
  docker.modem.followProgress(stream, (err) => {
@@ -62010,6 +62318,9 @@ async function buildBaseImages(docker, onProgress) {
62010
62318
  }
62011
62319
  });
62012
62320
  });
62321
+ if (oldImageId) {
62322
+ await removeImage(docker, oldImageId);
62323
+ }
62013
62324
  onProgress?.({ runtime: target, status: "done" });
62014
62325
  } catch (err) {
62015
62326
  const message = err instanceof Error ? err.message : String(err);
@@ -62018,26 +62329,44 @@ async function buildBaseImages(docker, onProgress) {
62018
62329
  }
62019
62330
  }
62020
62331
  }
62021
- async function buildCustomImages(docker, config, onProgress) {
62332
+ async function buildCustomImages(docker, config, onProgress, force = false) {
62022
62333
  const deps = config.dependencies;
62023
62334
  if (deps.python?.length) {
62024
- await buildCustomImage(docker, "python", deps.python, onProgress);
62335
+ await buildCustomImage(docker, "python", deps.python, onProgress, force);
62025
62336
  }
62026
62337
  if (deps.node?.length) {
62027
- await buildCustomImage(docker, "node", deps.node, onProgress);
62338
+ await buildCustomImage(docker, "node", deps.node, onProgress, force);
62028
62339
  }
62029
62340
  if (deps.bun?.length) {
62030
- await buildCustomImage(docker, "bun", deps.bun, onProgress);
62341
+ await buildCustomImage(docker, "bun", deps.bun, onProgress, force);
62031
62342
  }
62032
62343
  if (deps.deno?.length) {
62033
- await buildCustomImage(docker, "deno", deps.deno, onProgress);
62344
+ await buildCustomImage(docker, "deno", deps.deno, onProgress, force);
62034
62345
  }
62035
62346
  if (deps.bash?.length) {
62036
- await buildCustomImage(docker, "bash", deps.bash, onProgress);
62347
+ await buildCustomImage(docker, "bash", deps.bash, onProgress, force);
62037
62348
  }
62038
62349
  }
62039
- async function buildCustomImage(docker, runtime, packages, onProgress) {
62350
+ async function buildCustomImage(docker, runtime, packages, onProgress, force = false) {
62040
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
+ }
62041
62370
  onProgress?.({ runtime, status: "building", message: `Custom: ${packages.join(", ")}` });
62042
62371
  let installCmd;
62043
62372
  switch (runtime) {
@@ -62069,7 +62398,10 @@ ${installCmd}
62069
62398
  const tarBuffer = createTarBuffer2("Dockerfile", dockerfileContent);
62070
62399
  const stream = await docker.buildImage(Readable.from(tarBuffer), {
62071
62400
  t: tag,
62072
- dockerfile: "Dockerfile"
62401
+ dockerfile: "Dockerfile",
62402
+ labels: {
62403
+ [LABELS.depsHash]: depsHash
62404
+ }
62073
62405
  });
62074
62406
  await new Promise((resolve2, reject) => {
62075
62407
  docker.modem.followProgress(stream, (err) => {
@@ -62080,6 +62412,9 @@ ${installCmd}
62080
62412
  }
62081
62413
  });
62082
62414
  });
62415
+ if (oldImageId) {
62416
+ await removeImage(docker, oldImageId);
62417
+ }
62083
62418
  onProgress?.({ runtime, status: "done" });
62084
62419
  }
62085
62420
 
@@ -62097,7 +62432,7 @@ program2.name("isol8").description("Secure code execution engine").version(VERSI
62097
62432
  logger.debug(`[CLI] Version: ${VERSION}`);
62098
62433
  logger.debug(`[CLI] Platform: ${platform()} ${arch()}`);
62099
62434
  });
62100
- 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) => {
62101
62436
  const docker = new import_dockerode2.default;
62102
62437
  logger.debug("[Setup] Connecting to Docker daemon");
62103
62438
  const spinner = ora("Checking Docker...").start();
@@ -62112,7 +62447,7 @@ program2.command("setup").description("Check Docker and build isol8 images").opt
62112
62447
  process.exit(1);
62113
62448
  }
62114
62449
  spinner.start("Building isol8 images...");
62115
- logger.debug("[Setup] Building base images");
62450
+ logger.debug(`[Setup] Building base images (force=${opts.force ?? false})`);
62116
62451
  await buildBaseImages(docker, (progress) => {
62117
62452
  const status = progress.status === "error" ? "[ERR]" : progress.status === "done" ? "[OK]" : "[..]";
62118
62453
  if (progress.status === "building") {
@@ -62128,7 +62463,7 @@ program2.command("setup").description("Check Docker and build isol8 images").opt
62128
62463
  spinner.start();
62129
62464
  }
62130
62465
  }
62131
- });
62466
+ }, opts.force ?? false);
62132
62467
  if (spinner.isSpinning) {
62133
62468
  spinner.stop();
62134
62469
  }
@@ -62176,7 +62511,7 @@ program2.command("setup").description("Check Docker and build isol8 images").opt
62176
62511
  spinner.start();
62177
62512
  }
62178
62513
  }
62179
- });
62514
+ }, opts.force ?? false);
62180
62515
  if (spinner.isSpinning) {
62181
62516
  spinner.stop();
62182
62517
  }
@@ -62184,12 +62519,25 @@ program2.command("setup").description("Check Docker and build isol8 images").opt
62184
62519
  console.log(`
62185
62520
  [DONE] Setup complete!`);
62186
62521
  });
62187
- 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) => {
62188
- 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);
62189
62534
  logger.debug(`[Run] Runtime: ${runtime}, mode: ${engineOptions.mode}`);
62190
62535
  logger.debug(`[Run] Network: ${engineOptions.network}, timeout: ${engineOptions.timeoutMs}ms`);
62191
62536
  logger.debug(`[Run] Memory: ${engineOptions.memoryLimit}, CPU: ${engineOptions.cpuLimit}`);
62192
- 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
+ }
62193
62541
  if (stdinData) {
62194
62542
  logger.debug(`[Run] Stdin data provided (${stdinData.length} chars)`);
62195
62543
  }
@@ -62215,9 +62563,12 @@ program2.command("run").description("Execute code in isol8").argument("[file]",
62215
62563
  logger.debug("[Run] Engine started");
62216
62564
  spinner.text = "Running code...";
62217
62565
  const req = {
62218
- code,
62219
62566
  runtime,
62220
62567
  timeoutMs: engineOptions.timeoutMs,
62568
+ ...code ? { code } : {},
62569
+ ...codeUrl ? { codeUrl } : {},
62570
+ ...codeHash ? { codeHash } : {},
62571
+ ...allowInsecureCodeUrl ? { allowInsecureCodeUrl } : {},
62221
62572
  ...stdinData ? { stdin: stdinData } : {},
62222
62573
  ...opts.install.length > 0 ? { installPackages: opts.install } : {},
62223
62574
  fileExtension
@@ -62377,7 +62728,7 @@ async function downloadServerBinary(binaryPath) {
62377
62728
  }
62378
62729
  process.exit(1);
62379
62730
  }
62380
- const binDir = join3(homedir2(), ".isol8", "bin");
62731
+ const binDir = join4(homedir2(), ".isol8", "bin");
62381
62732
  mkdirSync2(binDir, { recursive: true });
62382
62733
  const tmpPath = `${binaryPath}.tmp`;
62383
62734
  const buffer = Buffer.from(await response.arrayBuffer());
@@ -62409,8 +62760,8 @@ async function promptYesNo(question) {
62409
62760
  return normalized === "" || normalized === "y" || normalized === "yes";
62410
62761
  }
62411
62762
  async function ensureServerBinary(forceUpdate) {
62412
- const binDir = join3(homedir2(), ".isol8", "bin");
62413
- const binaryPath = join3(binDir, "isol8-server");
62763
+ const binDir = join4(homedir2(), ".isol8", "bin");
62764
+ const binaryPath = join4(binDir, "isol8-server");
62414
62765
  logger.debug(`[Serve] Binary path: ${binaryPath}, forceUpdate: ${forceUpdate}`);
62415
62766
  if (forceUpdate) {
62416
62767
  logger.debug("[Serve] Force update requested");
@@ -62440,8 +62791,8 @@ async function ensureServerBinary(forceUpdate) {
62440
62791
  program2.command("config").description("Show the resolved isol8 configuration").option("--json", "Output as raw JSON").action((opts) => {
62441
62792
  const config = loadConfig();
62442
62793
  const searchPaths = [
62443
- join3(resolve2(process.cwd()), "isol8.config.json"),
62444
- join3(homedir2(), ".isol8", "config.json")
62794
+ join4(resolve2(process.cwd()), "isol8.config.json"),
62795
+ join4(homedir2(), ".isol8", "config.json")
62445
62796
  ];
62446
62797
  const loadedFrom = searchPaths.find((p) => existsSync5(p));
62447
62798
  logger.debug(`[Config] Config source: ${loadedFrom ?? "defaults"}`);
@@ -62476,6 +62827,13 @@ Isol8 Configuration
62476
62827
  } else {
62477
62828
  console.log(" Whitelist: (none)");
62478
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"}`);
62479
62837
  if (config.network.blacklist.length > 0) {
62480
62838
  console.log(` Blacklist: ${config.network.blacklist.join(", ")}`);
62481
62839
  } else {
@@ -62570,8 +62928,25 @@ async function resolveRunInput(file, opts) {
62570
62928
  const config = loadConfig();
62571
62929
  logger.debug("[Run] Config loaded");
62572
62930
  let code;
62931
+ let codeUrl;
62932
+ let codeHash;
62933
+ let allowInsecureCodeUrl = false;
62573
62934
  let runtime;
62574
- 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) {
62575
62950
  code = opts.eval;
62576
62951
  runtime = opts.runtime ?? "python";
62577
62952
  logger.debug(`[Run] Inline eval, runtime: ${runtime}`);
@@ -62582,7 +62957,7 @@ async function resolveRunInput(file, opts) {
62582
62957
  console.error(`[ERR] File not found: ${file}`);
62583
62958
  process.exit(1);
62584
62959
  }
62585
- code = readFileSync3(filePath, "utf-8");
62960
+ code = readFileSync4(filePath, "utf-8");
62586
62961
  if (opts.runtime) {
62587
62962
  runtime = opts.runtime;
62588
62963
  logger.debug(`[Run] Runtime specified: ${runtime}`);
@@ -62622,6 +62997,7 @@ async function resolveRunInput(file, opts) {
62622
62997
  debug: opts.debug ?? config.debug,
62623
62998
  persist: opts.persist ?? false,
62624
62999
  ...opts.logNetwork ? { logNetwork: true } : {},
63000
+ remoteCode: config.remoteCode,
62625
63001
  poolStrategy: opts.poolStrategy === "secure" ? "secure" : "fast",
62626
63002
  poolSize: opts.poolSize ? opts.poolSize.includes(",") ? {
62627
63003
  clean: Number.parseInt(opts.poolSize.split(",")[0], 10),
@@ -62660,7 +63036,48 @@ async function resolveRunInput(file, opts) {
62660
63036
  logger.debug("[Run] Using local Docker engine");
62661
63037
  engine = new DockerIsol8(engineOptions, config.maxConcurrent);
62662
63038
  }
62663
- 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
+ }
62664
63081
  }
62665
63082
  function collect(value, previous) {
62666
63083
  return previous.concat([value]);
@@ -62671,4 +63088,4 @@ if (!process.argv.slice(2).length) {
62671
63088
  }
62672
63089
  program2.parse();
62673
63090
 
62674
- //# debugId=FB4CACFD75FE97A064756E2164756E21
63091
+ //# debugId=280ED4C71DBDC32964756E2164756E21