@isol8/core 0.14.7 → 0.16.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,12 +1,16 @@
1
1
  import { createRequire } from "node:module";
2
2
  var __defProp = Object.defineProperty;
3
+ var __returnValue = (v) => v;
4
+ function __exportSetter(name, newValue) {
5
+ this[name] = __returnValue.bind(null, newValue);
6
+ }
3
7
  var __export = (target, all) => {
4
8
  for (var name in all)
5
9
  __defProp(target, name, {
6
10
  get: all[name],
7
11
  enumerable: true,
8
12
  configurable: true,
9
- set: (newValue) => all[name] = () => newValue
13
+ set: __exportSetter.bind(all, name)
10
14
  });
11
15
  };
12
16
  var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
@@ -571,6 +575,7 @@ class RemoteIsol8 {
571
575
  apiKey;
572
576
  sessionId;
573
577
  isol8Options;
578
+ wsAvailable = null;
574
579
  constructor(options, isol8Options) {
575
580
  this.host = options.host.replace(/\/$/, "");
576
581
  this.apiKey = options.apiKey;
@@ -604,6 +609,97 @@ class RemoteIsol8 {
604
609
  return res.json();
605
610
  }
606
611
  async* executeStream(req) {
612
+ if (this.wsAvailable !== false) {
613
+ try {
614
+ yield* this.executeStreamWs(req);
615
+ return;
616
+ } catch (err) {
617
+ if (this.wsAvailable === null) {
618
+ this.wsAvailable = false;
619
+ } else {
620
+ throw err;
621
+ }
622
+ }
623
+ }
624
+ yield* this.executeStreamSse(req);
625
+ }
626
+ async* executeStreamWs(req) {
627
+ const wsUrl = `${this.host.replace(/^http/, "ws")}/execute/ws`;
628
+ const events = [];
629
+ let resolve = null;
630
+ let done = false;
631
+ let wsError = null;
632
+ const ws = new WebSocket(wsUrl, {
633
+ headers: {
634
+ Authorization: `Bearer ${this.apiKey}`
635
+ }
636
+ });
637
+ const waitForEvent = () => new Promise((r) => {
638
+ if (events.length > 0 || done) {
639
+ r();
640
+ } else {
641
+ resolve = r;
642
+ }
643
+ });
644
+ ws.onopen = () => {
645
+ this.wsAvailable = true;
646
+ const msg = {
647
+ type: "execute",
648
+ request: req,
649
+ ...this.isol8Options ? { options: this.isol8Options } : {}
650
+ };
651
+ ws.send(JSON.stringify(msg));
652
+ };
653
+ ws.onmessage = (evt) => {
654
+ try {
655
+ const event = JSON.parse(typeof evt.data === "string" ? evt.data : String(evt.data));
656
+ events.push(event);
657
+ if (resolve) {
658
+ const r = resolve;
659
+ resolve = null;
660
+ r();
661
+ }
662
+ } catch {}
663
+ };
664
+ ws.onerror = () => {
665
+ if (!done) {
666
+ wsError = new Error("WebSocket connection failed");
667
+ done = true;
668
+ if (resolve) {
669
+ const r = resolve;
670
+ resolve = null;
671
+ r();
672
+ }
673
+ }
674
+ };
675
+ ws.onclose = () => {
676
+ done = true;
677
+ if (resolve) {
678
+ const r = resolve;
679
+ resolve = null;
680
+ r();
681
+ }
682
+ };
683
+ try {
684
+ while (true) {
685
+ await waitForEvent();
686
+ if (wsError) {
687
+ throw wsError;
688
+ }
689
+ while (events.length > 0) {
690
+ yield events.shift();
691
+ }
692
+ if (done) {
693
+ break;
694
+ }
695
+ }
696
+ } finally {
697
+ if (ws.readyState === WebSocket.OPEN || ws.readyState === WebSocket.CONNECTING) {
698
+ ws.close();
699
+ }
700
+ }
701
+ }
702
+ async* executeStreamSse(req) {
607
703
  const res = await this.fetch("/execute/stream", {
608
704
  method: "POST",
609
705
  body: JSON.stringify({
@@ -752,6 +848,11 @@ var DEFAULT_CONFIG = {
752
848
  includeCode: false,
753
849
  includeOutput: false
754
850
  },
851
+ auth: {
852
+ enabled: false,
853
+ defaultTtlMs: 86400000,
854
+ cleanupIntervalMs: 3600000
855
+ },
755
856
  debug: false
756
857
  };
757
858
  function loadConfig(cwd) {
@@ -805,6 +906,10 @@ function mergeConfig(defaults, overrides) {
805
906
  ...defaults.audit,
806
907
  ...overrides.audit
807
908
  },
909
+ auth: {
910
+ ...defaults.auth,
911
+ ...overrides.auth
912
+ },
808
913
  debug: overrides.debug ?? defaults.debug
809
914
  };
810
915
  }
@@ -848,7 +953,6 @@ init_runtime();
848
953
  init_logger();
849
954
  import { randomUUID } from "node:crypto";
850
955
  import { existsSync as existsSync4, readFileSync as readFileSync3 } from "node:fs";
851
- import { PassThrough } from "node:stream";
852
956
  import Docker from "dockerode";
853
957
 
854
958
  // src/engine/audit.ts
@@ -1228,137 +1332,609 @@ var EMBEDDED_DEFAULT_SECCOMP_PROFILE = JSON.stringify({
1228
1332
  // src/engine/docker.ts
1229
1333
  init_image_builder();
1230
1334
 
1231
- // src/engine/pool.ts
1335
+ // src/engine/managers/execution-manager.ts
1232
1336
  init_logger();
1337
+ import { PassThrough } from "node:stream";
1233
1338
 
1234
- class ContainerPool {
1235
- docker;
1236
- poolStrategy;
1237
- cleanPoolSize;
1238
- dirtyPoolSize;
1239
- createOptions;
1240
- networkMode;
1241
- securityMode;
1242
- pools = new Map;
1243
- replenishing = new Set;
1244
- pendingReplenishments = new Set;
1245
- cleaningInterval = null;
1339
+ class ExecutionManager {
1340
+ secrets;
1341
+ maxOutputSize;
1246
1342
  constructor(options) {
1247
- this.docker = options.docker;
1248
- this.poolStrategy = options.poolStrategy ?? "fast";
1249
- this.createOptions = options.createOptions;
1250
- this.networkMode = options.networkMode;
1251
- this.securityMode = options.securityMode;
1252
- if (typeof options.poolSize === "number") {
1253
- this.cleanPoolSize = options.poolSize;
1254
- this.dirtyPoolSize = options.poolSize;
1255
- } else if (options.poolSize) {
1256
- this.cleanPoolSize = options.poolSize.clean ?? 1;
1257
- this.dirtyPoolSize = options.poolSize.dirty ?? 1;
1258
- } else {
1259
- this.cleanPoolSize = 1;
1260
- this.dirtyPoolSize = 1;
1261
- }
1262
- if (this.poolStrategy === "fast") {
1263
- this.startBackgroundCleaning();
1343
+ this.secrets = options.secrets;
1344
+ this.maxOutputSize = options.maxOutputSize;
1345
+ }
1346
+ wrapWithTimeout(cmd, timeoutSec) {
1347
+ return ["timeout", "-s", "KILL", String(timeoutSec), ...cmd];
1348
+ }
1349
+ getInstallCommand(runtime, packages) {
1350
+ switch (runtime) {
1351
+ case "python":
1352
+ return [
1353
+ "pip",
1354
+ "install",
1355
+ "--user",
1356
+ "--no-cache-dir",
1357
+ "--break-system-packages",
1358
+ "--disable-pip-version-check",
1359
+ "--retries",
1360
+ "0",
1361
+ "--timeout",
1362
+ "15",
1363
+ ...packages
1364
+ ];
1365
+ case "node":
1366
+ return ["npm", "install", "--prefix", "/sandbox", ...packages];
1367
+ case "bun":
1368
+ return ["bun", "install", "-g", "--global-dir=/sandbox/.bun-global", ...packages];
1369
+ case "deno":
1370
+ return ["sh", "-c", packages.map((p) => `deno cache ${p}`).join(" && ")];
1371
+ case "bash":
1372
+ return ["apk", "add", "--no-cache", ...packages];
1373
+ default:
1374
+ throw new Error(`Unknown runtime for package install: ${runtime}`);
1375
+ }
1376
+ }
1377
+ async installPackages(container, runtime, packages, timeoutMs) {
1378
+ const timeoutSec = Math.max(1, Math.ceil(timeoutMs / 1000));
1379
+ const cmd = this.wrapWithTimeout(this.getInstallCommand(runtime, packages), timeoutSec);
1380
+ logger.debug(`Installing packages: ${JSON.stringify(cmd)}`);
1381
+ const env = [
1382
+ "PATH=/sandbox/.local/bin:/sandbox/.npm-global/bin:/sandbox/.bun-global/bin:/usr/local/bin:/usr/bin:/bin:/usr/local/sbin:/usr/sbin:/sbin"
1383
+ ];
1384
+ if (runtime === "python") {
1385
+ env.push("PYTHONUSERBASE=/sandbox/.local");
1386
+ } else if (runtime === "node") {
1387
+ env.push("NPM_CONFIG_PREFIX=/sandbox/.npm-global");
1388
+ env.push("NPM_CONFIG_CACHE=/sandbox/.npm-cache");
1389
+ env.push("npm_config_cache=/sandbox/.npm-cache");
1390
+ env.push("NPM_CONFIG_FETCH_RETRIES=0");
1391
+ env.push("npm_config_fetch_retries=0");
1392
+ env.push("NPM_CONFIG_FETCH_RETRY_MINTIMEOUT=1000");
1393
+ env.push("npm_config_fetch_retry_mintimeout=1000");
1394
+ env.push("NPM_CONFIG_FETCH_RETRY_MAXTIMEOUT=2000");
1395
+ env.push("npm_config_fetch_retry_maxtimeout=2000");
1396
+ } else if (runtime === "bun") {
1397
+ env.push("BUN_INSTALL_GLOBAL_DIR=/sandbox/.bun-global");
1398
+ env.push("BUN_INSTALL_CACHE_DIR=/sandbox/.bun-cache");
1399
+ env.push("BUN_INSTALL_BIN=/sandbox/.bun-global/bin");
1400
+ } else if (runtime === "deno") {
1401
+ env.push("DENO_DIR=/sandbox/.deno");
1264
1402
  }
1403
+ const exec = await container.exec({
1404
+ Cmd: cmd,
1405
+ AttachStdout: true,
1406
+ AttachStderr: true,
1407
+ Env: env,
1408
+ User: runtime === "bash" ? "root" : "sandbox"
1409
+ });
1410
+ const stream = await exec.start({ Detach: false, Tty: false });
1411
+ return new Promise((resolve2, reject) => {
1412
+ let stderr = "";
1413
+ const stdoutStream = new PassThrough;
1414
+ const stderrStream = new PassThrough;
1415
+ container.modem.demuxStream(stream, stdoutStream, stderrStream);
1416
+ stderrStream.on("data", (chunk) => {
1417
+ const text = chunk.toString();
1418
+ stderr += text;
1419
+ logger.debug(`[install:${runtime}:stderr] ${text.trimEnd()}`);
1420
+ });
1421
+ stdoutStream.on("data", (chunk) => {
1422
+ const text = chunk.toString();
1423
+ logger.debug(`[install:${runtime}:stdout] ${text.trimEnd()}`);
1424
+ });
1425
+ stream.on("end", async () => {
1426
+ try {
1427
+ const info = await exec.inspect();
1428
+ if (info.ExitCode !== 0) {
1429
+ reject(new Error(`Package install failed (exit code ${info.ExitCode}): ${stderr}`));
1430
+ } else {
1431
+ resolve2();
1432
+ }
1433
+ } catch (err) {
1434
+ reject(err);
1435
+ }
1436
+ });
1437
+ stream.on("error", reject);
1438
+ });
1265
1439
  }
1266
- async acquire(image) {
1267
- const pool = this.pools.get(image) ?? { clean: [], dirty: [] };
1268
- if (this.poolStrategy === "fast") {
1269
- if (pool.clean.length > 0) {
1270
- const entry = pool.clean.shift();
1271
- this.pools.set(image, pool);
1272
- this.replenish(image);
1273
- return entry.container;
1440
+ async* streamExecOutput(stream, exec, container, timeoutMs) {
1441
+ const queue = [];
1442
+ let resolve2 = null;
1443
+ let done = false;
1444
+ const push = (event) => {
1445
+ queue.push(event);
1446
+ if (resolve2) {
1447
+ resolve2();
1448
+ resolve2 = null;
1274
1449
  }
1275
- if (pool.dirty.length > 0 && pool.clean.length < this.cleanPoolSize) {
1276
- await this.cleanDirtyImmediate(image);
1277
- const updatedPool = this.pools.get(image);
1278
- if (updatedPool && updatedPool.clean.length > 0) {
1279
- const entry = updatedPool.clean.shift();
1280
- this.pools.set(image, updatedPool);
1281
- this.replenish(image);
1282
- return entry.container;
1283
- }
1450
+ };
1451
+ const timer = setTimeout(() => {
1452
+ push({ type: "error", data: "EXECUTION TIMED OUT" });
1453
+ push({ type: "exit", data: "137" });
1454
+ done = true;
1455
+ }, timeoutMs);
1456
+ const stdoutStream = new PassThrough;
1457
+ const stderrStream = new PassThrough;
1458
+ container.modem.demuxStream(stream, stdoutStream, stderrStream);
1459
+ stdoutStream.on("data", (chunk) => {
1460
+ let text = chunk.toString("utf-8");
1461
+ if (Object.keys(this.secrets).length > 0) {
1462
+ text = maskSecrets(text, this.secrets);
1284
1463
  }
1285
- return this.createContainer(image);
1286
- }
1287
- if (pool.clean && pool.clean.length > 0) {
1288
- const entry = pool.clean.shift();
1289
- this.pools.set(image, { clean: pool.clean, dirty: [] });
1290
- await this.cleanupContainer(entry.container);
1291
- this.replenish(image);
1292
- return entry.container;
1293
- }
1294
- return this.createContainer(image);
1295
- }
1296
- async release(container, image) {
1297
- let pool = this.pools.get(image);
1298
- if (!pool) {
1299
- pool = { clean: [], dirty: [] };
1300
- this.pools.set(image, pool);
1301
- }
1302
- if (this.poolStrategy === "fast") {
1303
- if (pool.dirty.length >= this.dirtyPoolSize) {
1304
- await container.remove({ force: true }).catch(() => {});
1305
- return;
1464
+ push({ type: "stdout", data: text });
1465
+ });
1466
+ stderrStream.on("data", (chunk) => {
1467
+ let text = chunk.toString("utf-8");
1468
+ if (Object.keys(this.secrets).length > 0) {
1469
+ text = maskSecrets(text, this.secrets);
1306
1470
  }
1307
- pool.dirty.push({ container, createdAt: Date.now() });
1308
- } else {
1309
- if (pool.clean.length >= this.cleanPoolSize) {
1310
- await container.remove({ force: true }).catch(() => {});
1311
- return;
1471
+ push({ type: "stderr", data: text });
1472
+ });
1473
+ stream.on("end", async () => {
1474
+ clearTimeout(timer);
1475
+ try {
1476
+ const info = await exec.inspect();
1477
+ push({ type: "exit", data: (info.ExitCode ?? 0).toString() });
1478
+ } catch {
1479
+ push({ type: "exit", data: "1" });
1312
1480
  }
1313
- if (!pool.clean) {
1314
- pool.clean = [];
1481
+ done = true;
1482
+ });
1483
+ stream.on("error", (err) => {
1484
+ clearTimeout(timer);
1485
+ push({ type: "error", data: err.message });
1486
+ push({ type: "exit", data: "1" });
1487
+ done = true;
1488
+ });
1489
+ while (!done || queue.length > 0) {
1490
+ if (queue.length > 0) {
1491
+ yield queue.shift();
1492
+ } else if (resolve2) {
1493
+ await new Promise((r) => {
1494
+ resolve2 = r;
1495
+ });
1496
+ } else {
1497
+ await new Promise((r) => setTimeout(r, 10));
1315
1498
  }
1316
- pool.clean.push({ container, createdAt: Date.now() });
1317
1499
  }
1318
1500
  }
1319
- startBackgroundCleaning() {
1320
- this.cleaningInterval = setInterval(async () => {
1321
- for (const [_image, pool] of this.pools) {
1322
- for (let i = 0;i < this.dirtyPoolSize; i++) {
1323
- if (pool.dirty.length > 0 && pool.clean.length < this.cleanPoolSize) {
1324
- const entry = pool.dirty.shift();
1325
- try {
1326
- await this.cleanupContainer(entry.container);
1327
- pool.clean.push(entry);
1328
- } catch {
1329
- entry.container.remove({ force: true }).catch(() => {});
1330
- }
1331
- }
1501
+ async collectExecOutput(stream, container, timeoutMs) {
1502
+ return new Promise((resolve2, reject) => {
1503
+ let stdout = "";
1504
+ let stderr = "";
1505
+ let truncated = false;
1506
+ let settled = false;
1507
+ let stdoutEnded = false;
1508
+ let stderrEnded = false;
1509
+ const timer = setTimeout(() => {
1510
+ if (settled) {
1511
+ return;
1332
1512
  }
1333
- }
1334
- }, 5000);
1335
- }
1336
- async cleanDirtyImmediate(image) {
1337
- const pool = this.pools.get(image);
1338
- if (!pool || pool.dirty.length === 0 || pool.clean.length >= this.cleanPoolSize) {
1339
- return;
1340
- }
1341
- const entry = pool.dirty.shift();
1342
- try {
1343
- await this.cleanupContainer(entry.container);
1344
- pool.clean.push(entry);
1345
- } catch {
1346
- entry.container.remove({ force: true }).catch(() => {});
1347
- }
1348
- }
1349
- async cleanupContainer(container) {
1350
- const needsCleanup = this.securityMode === "strict";
1351
- const needsIptables = this.networkMode === "filtered" && needsCleanup;
1352
- if (!needsCleanup) {
1353
- return;
1354
- }
1355
- try {
1356
- const cleanupCmd = needsIptables ? "pkill -9 -u sandbox 2>/dev/null; /usr/sbin/iptables -F OUTPUT 2>/dev/null; rm -rf /sandbox/* /sandbox/.[!.]* 2>/dev/null; true" : "pkill -9 -u sandbox 2>/dev/null; rm -rf /sandbox/* /sandbox/.[!.]* 2>/dev/null; true";
1357
- const cleanExec = await container.exec({
1358
- Cmd: ["sh", "-c", cleanupCmd]
1359
- });
1360
- await cleanExec.start({ Detach: true });
1361
- let info = await cleanExec.inspect();
1513
+ settled = true;
1514
+ if (stream.destroy) {
1515
+ stream.destroy();
1516
+ }
1517
+ resolve2({ stdout, stderr: `${stderr}
1518
+ --- EXECUTION TIMED OUT ---`, truncated });
1519
+ }, timeoutMs);
1520
+ const stdoutStream = new PassThrough;
1521
+ const stderrStream = new PassThrough;
1522
+ container.modem.demuxStream(stream, stdoutStream, stderrStream);
1523
+ stdoutStream.on("data", (chunk) => {
1524
+ stdout += chunk.toString("utf-8");
1525
+ if (stdout.length > this.maxOutputSize) {
1526
+ const result = truncateOutput(stdout, this.maxOutputSize);
1527
+ stdout = result.text;
1528
+ truncated = true;
1529
+ }
1530
+ });
1531
+ stderrStream.on("data", (chunk) => {
1532
+ stderr += chunk.toString("utf-8");
1533
+ if (stderr.length > this.maxOutputSize) {
1534
+ const result = truncateOutput(stderr, this.maxOutputSize);
1535
+ stderr = result.text;
1536
+ truncated = true;
1537
+ }
1538
+ });
1539
+ const checkDone = () => {
1540
+ if (settled) {
1541
+ return;
1542
+ }
1543
+ if (stdoutEnded && stderrEnded) {
1544
+ settled = true;
1545
+ clearTimeout(timer);
1546
+ resolve2({ stdout, stderr, truncated });
1547
+ }
1548
+ };
1549
+ stdoutStream.on("end", () => {
1550
+ stdoutEnded = true;
1551
+ checkDone();
1552
+ });
1553
+ stderrStream.on("end", () => {
1554
+ stderrEnded = true;
1555
+ checkDone();
1556
+ });
1557
+ stream.on("error", (err) => {
1558
+ if (settled) {
1559
+ return;
1560
+ }
1561
+ settled = true;
1562
+ clearTimeout(timer);
1563
+ reject(err);
1564
+ });
1565
+ stream.on("end", () => {
1566
+ if (settled) {
1567
+ return;
1568
+ }
1569
+ setTimeout(() => {
1570
+ if (!settled) {
1571
+ stdoutEnded = true;
1572
+ stderrEnded = true;
1573
+ checkDone();
1574
+ }
1575
+ }, 100);
1576
+ });
1577
+ });
1578
+ }
1579
+ postProcessOutput(output, _truncated) {
1580
+ let result = output;
1581
+ if (Object.keys(this.secrets).length > 0) {
1582
+ result = maskSecrets(result, this.secrets);
1583
+ }
1584
+ return result.trimEnd();
1585
+ }
1586
+ buildEnv(extra, proxyPort, networkMode, networkFilter) {
1587
+ const env = [
1588
+ "PYTHONUNBUFFERED=1",
1589
+ "PYTHONUSERBASE=/sandbox/.local",
1590
+ "NPM_CONFIG_PREFIX=/sandbox/.npm-global",
1591
+ "DENO_DIR=/sandbox/.deno",
1592
+ "PATH=/sandbox/.local/bin:/sandbox/.npm-global/bin:/sandbox/.bun-global/bin:/usr/local/bin:/usr/bin:/bin",
1593
+ "NODE_PATH=/usr/local/lib/node_modules:/sandbox/.npm-global/lib/node_modules:/sandbox/node_modules"
1594
+ ];
1595
+ for (const [key, value] of Object.entries(this.secrets)) {
1596
+ env.push(`${key}=${value}`);
1597
+ }
1598
+ if (extra) {
1599
+ for (const [key, value] of Object.entries(extra)) {
1600
+ env.push(`${key}=${value}`);
1601
+ }
1602
+ }
1603
+ if (networkMode === "filtered") {
1604
+ if (networkFilter) {
1605
+ env.push(`ISOL8_WHITELIST=${JSON.stringify(networkFilter.whitelist)}`);
1606
+ env.push(`ISOL8_BLACKLIST=${JSON.stringify(networkFilter.blacklist)}`);
1607
+ }
1608
+ if (proxyPort) {
1609
+ env.push(`HTTP_PROXY=http://127.0.0.1:${proxyPort}`);
1610
+ env.push(`HTTPS_PROXY=http://127.0.0.1:${proxyPort}`);
1611
+ env.push(`http_proxy=http://127.0.0.1:${proxyPort}`);
1612
+ env.push(`https_proxy=http://127.0.0.1:${proxyPort}`);
1613
+ }
1614
+ }
1615
+ return env;
1616
+ }
1617
+ }
1618
+ // src/engine/managers/network-manager.ts
1619
+ init_logger();
1620
+ var PROXY_PORT = 8118;
1621
+ var PROXY_STARTUP_TIMEOUT_MS = 5000;
1622
+ var PROXY_POLL_INTERVAL_MS = 100;
1623
+
1624
+ class NetworkManager {
1625
+ network;
1626
+ networkFilter;
1627
+ constructor(options) {
1628
+ this.network = options.network;
1629
+ this.networkFilter = options.networkFilter;
1630
+ }
1631
+ async startProxy(container) {
1632
+ if (this.network !== "filtered") {
1633
+ return;
1634
+ }
1635
+ const envParts = [];
1636
+ if (this.networkFilter) {
1637
+ envParts.push(`ISOL8_WHITELIST='${JSON.stringify(this.networkFilter.whitelist)}'`);
1638
+ envParts.push(`ISOL8_BLACKLIST='${JSON.stringify(this.networkFilter.blacklist)}'`);
1639
+ }
1640
+ const envPrefix = envParts.length > 0 ? `${envParts.join(" ")} ` : "";
1641
+ const startExec = await container.exec({
1642
+ Cmd: ["sh", "-c", `${envPrefix}bash /usr/local/bin/proxy.sh &`]
1643
+ });
1644
+ await startExec.start({ Detach: true });
1645
+ const deadline = Date.now() + PROXY_STARTUP_TIMEOUT_MS;
1646
+ while (Date.now() < deadline) {
1647
+ try {
1648
+ const checkExec = await container.exec({
1649
+ Cmd: ["sh", "-c", `nc -z 127.0.0.1 ${PROXY_PORT} 2>/dev/null`]
1650
+ });
1651
+ await checkExec.start({ Detach: true });
1652
+ let info = await checkExec.inspect();
1653
+ while (info.Running) {
1654
+ await new Promise((r) => setTimeout(r, 50));
1655
+ info = await checkExec.inspect();
1656
+ }
1657
+ if (info.ExitCode === 0) {
1658
+ return;
1659
+ }
1660
+ } catch {}
1661
+ await new Promise((r) => setTimeout(r, PROXY_POLL_INTERVAL_MS));
1662
+ }
1663
+ throw new Error("Proxy failed to start within timeout");
1664
+ }
1665
+ async setupIptables(container) {
1666
+ if (this.network !== "filtered") {
1667
+ return;
1668
+ }
1669
+ const rules = [
1670
+ "/usr/sbin/iptables -A OUTPUT -o lo -j ACCEPT",
1671
+ "/usr/sbin/iptables -A OUTPUT -m state --state ESTABLISHED,RELATED -j ACCEPT",
1672
+ `/usr/sbin/iptables -A OUTPUT -p tcp -d 127.0.0.1 --dport ${PROXY_PORT} -m owner --uid-owner 100 -j ACCEPT`,
1673
+ "/usr/sbin/iptables -A OUTPUT -m owner --uid-owner 100 -j DROP"
1674
+ ].join(" && ");
1675
+ const exec = await container.exec({
1676
+ Cmd: ["sh", "-c", rules]
1677
+ });
1678
+ await exec.start({ Detach: true });
1679
+ let info = await exec.inspect();
1680
+ while (info.Running) {
1681
+ await new Promise((r) => setTimeout(r, 50));
1682
+ info = await exec.inspect();
1683
+ }
1684
+ if (info.ExitCode !== 0) {
1685
+ throw new Error(`Failed to set up iptables rules (exit code ${info.ExitCode})`);
1686
+ }
1687
+ logger.debug("[Filtered] iptables rules applied — sandbox user restricted to proxy only");
1688
+ }
1689
+ get proxyPort() {
1690
+ return PROXY_PORT;
1691
+ }
1692
+ }
1693
+ // src/engine/managers/volume-manager.ts
1694
+ import { PassThrough as PassThrough2 } from "node:stream";
1695
+
1696
+ class VolumeManager {
1697
+ readonlyRootFs;
1698
+ sandboxWorkdir;
1699
+ constructor(options) {
1700
+ this.readonlyRootFs = options.readonlyRootFs;
1701
+ this.sandboxWorkdir = options.sandboxWorkdir ?? "/sandbox";
1702
+ }
1703
+ async writeFileViaExec(container, filePath, content) {
1704
+ const data = typeof content === "string" ? Buffer.from(content, "utf-8") : content;
1705
+ const b64 = data.toString("base64");
1706
+ if (b64.length < 20000) {
1707
+ const exec = await container.exec({
1708
+ Cmd: ["sh", "-c", `printf '%s' '${b64}' | base64 -d > ${filePath}`],
1709
+ User: "sandbox"
1710
+ });
1711
+ await exec.start({ Detach: true });
1712
+ let info2 = await exec.inspect();
1713
+ while (info2.Running) {
1714
+ await new Promise((r) => setTimeout(r, 5));
1715
+ info2 = await exec.inspect();
1716
+ }
1717
+ if (info2.ExitCode !== 0) {
1718
+ throw new Error(`Failed to write file ${filePath} in container (exit code ${info2.ExitCode})`);
1719
+ }
1720
+ return;
1721
+ }
1722
+ const tempPath = `/tmp/b64_${Date.now()}.tmp`;
1723
+ const chunkSize = 8000;
1724
+ for (let i = 0;i < b64.length; i += chunkSize) {
1725
+ const chunk = b64.slice(i, i + chunkSize);
1726
+ const exec = await container.exec({
1727
+ Cmd: ["sh", "-c", `printf '%s' '${chunk}' >> ${tempPath}`],
1728
+ User: "sandbox"
1729
+ });
1730
+ await exec.start({ Detach: true });
1731
+ await exec.inspect();
1732
+ }
1733
+ const decodeExec = await container.exec({
1734
+ Cmd: ["sh", "-c", `base64 -d ${tempPath} > ${filePath} && rm ${tempPath}`],
1735
+ User: "sandbox"
1736
+ });
1737
+ await decodeExec.start({ Detach: true });
1738
+ let info = await decodeExec.inspect();
1739
+ while (info.Running) {
1740
+ await new Promise((r) => setTimeout(r, 5));
1741
+ info = await decodeExec.inspect();
1742
+ }
1743
+ if (info.ExitCode !== 0) {
1744
+ throw new Error(`Failed to write file ${filePath} in container (exit code ${info.ExitCode})`);
1745
+ }
1746
+ }
1747
+ async readFileViaExec(container, filePath) {
1748
+ const exec = await container.exec({
1749
+ Cmd: ["base64", filePath],
1750
+ AttachStdout: true,
1751
+ AttachStderr: true,
1752
+ User: "sandbox"
1753
+ });
1754
+ const stream = await exec.start({ Tty: false });
1755
+ const chunks = [];
1756
+ const stderrChunks = [];
1757
+ const stdoutStream = new PassThrough2;
1758
+ const stderrStream = new PassThrough2;
1759
+ container.modem.demuxStream(stream, stdoutStream, stderrStream);
1760
+ stdoutStream.on("data", (chunk) => chunks.push(chunk));
1761
+ stderrStream.on("data", (chunk) => stderrChunks.push(chunk));
1762
+ await new Promise((resolve2, reject) => {
1763
+ stream.on("end", resolve2);
1764
+ stream.on("error", reject);
1765
+ });
1766
+ const inspectResult = await exec.inspect();
1767
+ if (inspectResult.ExitCode !== 0) {
1768
+ const stderr = Buffer.concat(stderrChunks).toString("utf-8").trim();
1769
+ throw new Error(`Failed to read file ${filePath} in container: ${stderr} (exit code ${inspectResult.ExitCode})`);
1770
+ }
1771
+ const b64Output = Buffer.concat(chunks).toString("utf-8").trim();
1772
+ return Buffer.from(b64Output, "base64");
1773
+ }
1774
+ async getFileFromContainer(container, path) {
1775
+ const stream = await container.getArchive({ path });
1776
+ const chunks = [];
1777
+ for await (const chunk of stream) {
1778
+ chunks.push(chunk);
1779
+ }
1780
+ return extractFromTar(Buffer.concat(chunks), path);
1781
+ }
1782
+ async retrieveFiles(container, paths) {
1783
+ const files = {};
1784
+ for (const p of paths) {
1785
+ try {
1786
+ const buf = this.readonlyRootFs ? await this.readFileViaExec(container, p) : await this.getFileFromContainer(container, p);
1787
+ files[p] = buf.toString("base64");
1788
+ } catch {}
1789
+ }
1790
+ return files;
1791
+ }
1792
+ async putFile(container, path, content) {
1793
+ if (this.readonlyRootFs) {
1794
+ await this.writeFileViaExec(container, path, content);
1795
+ } else {
1796
+ const tar = createTarBuffer(path, content);
1797
+ await container.putArchive(tar, { path: "/" });
1798
+ }
1799
+ }
1800
+ async getFile(container, path) {
1801
+ if (this.readonlyRootFs) {
1802
+ return this.readFileViaExec(container, path);
1803
+ }
1804
+ return this.getFileFromContainer(container, path);
1805
+ }
1806
+ }
1807
+ // src/engine/pool.ts
1808
+ init_logger();
1809
+
1810
+ class ContainerPool {
1811
+ docker;
1812
+ poolStrategy;
1813
+ cleanPoolSize;
1814
+ dirtyPoolSize;
1815
+ createOptions;
1816
+ networkMode;
1817
+ securityMode;
1818
+ pools = new Map;
1819
+ replenishing = new Set;
1820
+ pendingReplenishments = new Set;
1821
+ cleaningInterval = null;
1822
+ constructor(options) {
1823
+ this.docker = options.docker;
1824
+ this.poolStrategy = options.poolStrategy ?? "fast";
1825
+ this.createOptions = options.createOptions;
1826
+ this.networkMode = options.networkMode;
1827
+ this.securityMode = options.securityMode;
1828
+ if (typeof options.poolSize === "number") {
1829
+ this.cleanPoolSize = options.poolSize;
1830
+ this.dirtyPoolSize = options.poolSize;
1831
+ } else if (options.poolSize) {
1832
+ this.cleanPoolSize = options.poolSize.clean ?? 1;
1833
+ this.dirtyPoolSize = options.poolSize.dirty ?? 1;
1834
+ } else {
1835
+ this.cleanPoolSize = 1;
1836
+ this.dirtyPoolSize = 1;
1837
+ }
1838
+ if (this.poolStrategy === "fast") {
1839
+ this.startBackgroundCleaning();
1840
+ }
1841
+ }
1842
+ async acquire(image) {
1843
+ const pool = this.pools.get(image) ?? { clean: [], dirty: [] };
1844
+ if (this.poolStrategy === "fast") {
1845
+ if (pool.clean.length > 0) {
1846
+ const entry = pool.clean.shift();
1847
+ this.pools.set(image, pool);
1848
+ this.replenish(image);
1849
+ return entry.container;
1850
+ }
1851
+ if (pool.dirty.length > 0 && pool.clean.length < this.cleanPoolSize) {
1852
+ await this.cleanDirtyImmediate(image);
1853
+ const updatedPool = this.pools.get(image);
1854
+ if (updatedPool && updatedPool.clean.length > 0) {
1855
+ const entry = updatedPool.clean.shift();
1856
+ this.pools.set(image, updatedPool);
1857
+ this.replenish(image);
1858
+ return entry.container;
1859
+ }
1860
+ }
1861
+ return this.createContainer(image);
1862
+ }
1863
+ if (pool.clean && pool.clean.length > 0) {
1864
+ const entry = pool.clean.shift();
1865
+ this.pools.set(image, { clean: pool.clean, dirty: [] });
1866
+ await this.cleanupContainer(entry.container);
1867
+ this.replenish(image);
1868
+ return entry.container;
1869
+ }
1870
+ return this.createContainer(image);
1871
+ }
1872
+ async release(container, image) {
1873
+ let pool = this.pools.get(image);
1874
+ if (!pool) {
1875
+ pool = { clean: [], dirty: [] };
1876
+ this.pools.set(image, pool);
1877
+ }
1878
+ if (this.poolStrategy === "fast") {
1879
+ if (pool.dirty.length >= this.dirtyPoolSize) {
1880
+ await container.remove({ force: true }).catch(() => {});
1881
+ return;
1882
+ }
1883
+ pool.dirty.push({ container, createdAt: Date.now() });
1884
+ } else {
1885
+ if (pool.clean.length >= this.cleanPoolSize) {
1886
+ await container.remove({ force: true }).catch(() => {});
1887
+ return;
1888
+ }
1889
+ if (!pool.clean) {
1890
+ pool.clean = [];
1891
+ }
1892
+ pool.clean.push({ container, createdAt: Date.now() });
1893
+ }
1894
+ }
1895
+ startBackgroundCleaning() {
1896
+ this.cleaningInterval = setInterval(async () => {
1897
+ for (const [_image, pool] of this.pools) {
1898
+ for (let i = 0;i < this.dirtyPoolSize; i++) {
1899
+ if (pool.dirty.length > 0 && pool.clean.length < this.cleanPoolSize) {
1900
+ const entry = pool.dirty.shift();
1901
+ try {
1902
+ await this.cleanupContainer(entry.container);
1903
+ pool.clean.push(entry);
1904
+ } catch {
1905
+ entry.container.remove({ force: true }).catch(() => {});
1906
+ }
1907
+ }
1908
+ }
1909
+ }
1910
+ }, 5000);
1911
+ }
1912
+ async cleanDirtyImmediate(image) {
1913
+ const pool = this.pools.get(image);
1914
+ if (!pool || pool.dirty.length === 0 || pool.clean.length >= this.cleanPoolSize) {
1915
+ return;
1916
+ }
1917
+ const entry = pool.dirty.shift();
1918
+ try {
1919
+ await this.cleanupContainer(entry.container);
1920
+ pool.clean.push(entry);
1921
+ } catch {
1922
+ entry.container.remove({ force: true }).catch(() => {});
1923
+ }
1924
+ }
1925
+ async cleanupContainer(container) {
1926
+ const needsCleanup = this.securityMode === "strict";
1927
+ const needsIptables = this.networkMode === "filtered" && needsCleanup;
1928
+ if (!needsCleanup) {
1929
+ return;
1930
+ }
1931
+ try {
1932
+ const cleanupCmd = needsIptables ? "pkill -9 -u sandbox 2>/dev/null; /usr/sbin/iptables -F OUTPUT 2>/dev/null; rm -rf /sandbox/* /sandbox/.[!.]* 2>/dev/null; true" : "pkill -9 -u sandbox 2>/dev/null; rm -rf /sandbox/* /sandbox/.[!.]* 2>/dev/null; true";
1933
+ const cleanExec = await container.exec({
1934
+ Cmd: ["sh", "-c", cleanupCmd]
1935
+ });
1936
+ await cleanExec.start({ Detach: true });
1937
+ let info = await cleanExec.inspect();
1362
1938
  while (info.Running) {
1363
1939
  await new Promise((r) => setTimeout(r, 5));
1364
1940
  info = await cleanExec.inspect();
@@ -1471,265 +2047,45 @@ function calculateCPUPercent(stats) {
1471
2047
  const numCores = stats.cpu_stats.online_cpus ?? stats.cpu_stats.cpu_usage.percpu_usage?.length ?? 1;
1472
2048
  return cpuDelta / systemDelta * numCores * 100;
1473
2049
  }
1474
- function calculateNetworkStats(stats) {
1475
- if (!stats.networks) {
1476
- return { in: 0, out: 0 };
1477
- }
1478
- let rxBytes = 0;
1479
- let txBytes = 0;
1480
- for (const iface of Object.values(stats.networks)) {
1481
- rxBytes += iface.rx_bytes;
1482
- txBytes += iface.tx_bytes;
1483
- }
1484
- return { in: rxBytes, out: txBytes };
1485
- }
1486
- async function getContainerStats(container) {
1487
- const stats = await container.stats({
1488
- stream: false
1489
- });
1490
- const cpuPercent = calculateCPUPercent(stats);
1491
- const memoryBytes = stats.memory_stats.usage;
1492
- const network = calculateNetworkStats(stats);
1493
- return {
1494
- cpuPercent: Math.round(cpuPercent * 100) / 100,
1495
- memoryMB: Math.round(memoryBytes / (1024 * 1024)),
1496
- networkBytesIn: network.in,
1497
- networkBytesOut: network.out
1498
- };
1499
- }
1500
- function calculateResourceDelta(before, after) {
1501
- return {
1502
- cpuPercent: after.cpuPercent,
1503
- memoryMB: after.memoryMB,
1504
- networkBytesIn: after.networkBytesIn - before.networkBytesIn,
1505
- networkBytesOut: after.networkBytesOut - before.networkBytesOut
1506
- };
1507
- }
1508
-
1509
- // src/engine/docker.ts
1510
- async function writeFileViaExec(container, filePath, content) {
1511
- const data = typeof content === "string" ? Buffer.from(content, "utf-8") : content;
1512
- const b64 = data.toString("base64");
1513
- if (b64.length < 20000) {
1514
- const exec = await container.exec({
1515
- Cmd: ["sh", "-c", `printf '%s' '${b64}' | base64 -d > ${filePath}`],
1516
- User: "sandbox"
1517
- });
1518
- await exec.start({ Detach: true });
1519
- let info2 = await exec.inspect();
1520
- while (info2.Running) {
1521
- await new Promise((r) => setTimeout(r, 5));
1522
- info2 = await exec.inspect();
1523
- }
1524
- if (info2.ExitCode !== 0) {
1525
- throw new Error(`Failed to write file ${filePath} in container (exit code ${info2.ExitCode})`);
1526
- }
1527
- return;
1528
- }
1529
- const tempPath = `/tmp/b64_${Date.now()}.tmp`;
1530
- const chunkSize = 8000;
1531
- for (let i = 0;i < b64.length; i += chunkSize) {
1532
- const chunk = b64.slice(i, i + chunkSize);
1533
- const exec = await container.exec({
1534
- Cmd: ["sh", "-c", `printf '%s' '${chunk}' >> ${tempPath}`],
1535
- User: "sandbox"
1536
- });
1537
- await exec.start({ Detach: true });
1538
- await exec.inspect();
1539
- }
1540
- const decodeExec = await container.exec({
1541
- Cmd: ["sh", "-c", `base64 -d ${tempPath} > ${filePath} && rm ${tempPath}`],
1542
- User: "sandbox"
1543
- });
1544
- await decodeExec.start({ Detach: true });
1545
- let info = await decodeExec.inspect();
1546
- while (info.Running) {
1547
- await new Promise((r) => setTimeout(r, 5));
1548
- info = await decodeExec.inspect();
1549
- }
1550
- if (info.ExitCode !== 0) {
1551
- throw new Error(`Failed to write file ${filePath} in container (exit code ${info.ExitCode})`);
1552
- }
1553
- }
1554
- async function readFileViaExec(container, filePath) {
1555
- const exec = await container.exec({
1556
- Cmd: ["base64", filePath],
1557
- AttachStdout: true,
1558
- AttachStderr: true,
1559
- User: "sandbox"
1560
- });
1561
- const stream = await exec.start({ Tty: false });
1562
- const chunks = [];
1563
- const stderrChunks = [];
1564
- const stdoutStream = new PassThrough;
1565
- const stderrStream = new PassThrough;
1566
- container.modem.demuxStream(stream, stdoutStream, stderrStream);
1567
- stdoutStream.on("data", (chunk) => chunks.push(chunk));
1568
- stderrStream.on("data", (chunk) => stderrChunks.push(chunk));
1569
- await new Promise((resolve2, reject) => {
1570
- stream.on("end", resolve2);
1571
- stream.on("error", reject);
1572
- });
1573
- const inspectResult = await exec.inspect();
1574
- if (inspectResult.ExitCode !== 0) {
1575
- const stderr = Buffer.concat(stderrChunks).toString("utf-8").trim();
1576
- throw new Error(`Failed to read file ${filePath} in container: ${stderr} (exit code ${inspectResult.ExitCode})`);
1577
- }
1578
- const b64Output = Buffer.concat(chunks).toString("utf-8").trim();
1579
- return Buffer.from(b64Output, "base64");
1580
- }
1581
- var SANDBOX_WORKDIR = "/sandbox";
1582
- var MAX_OUTPUT_BYTES = 1024 * 1024;
1583
- var PROXY_PORT = 8118;
1584
- var PROXY_STARTUP_TIMEOUT_MS = 5000;
1585
- var PROXY_POLL_INTERVAL_MS = 100;
1586
- async function startProxy(container, networkFilter) {
1587
- const envParts = [];
1588
- if (networkFilter) {
1589
- envParts.push(`ISOL8_WHITELIST='${JSON.stringify(networkFilter.whitelist)}'`);
1590
- envParts.push(`ISOL8_BLACKLIST='${JSON.stringify(networkFilter.blacklist)}'`);
1591
- }
1592
- const envPrefix = envParts.length > 0 ? `${envParts.join(" ")} ` : "";
1593
- const startExec = await container.exec({
1594
- Cmd: ["sh", "-c", `${envPrefix}bash /usr/local/bin/proxy.sh &`]
1595
- });
1596
- await startExec.start({ Detach: true });
1597
- const deadline = Date.now() + PROXY_STARTUP_TIMEOUT_MS;
1598
- while (Date.now() < deadline) {
1599
- try {
1600
- const checkExec = await container.exec({
1601
- Cmd: ["sh", "-c", `nc -z 127.0.0.1 ${PROXY_PORT} 2>/dev/null`]
1602
- });
1603
- await checkExec.start({ Detach: true });
1604
- let info = await checkExec.inspect();
1605
- while (info.Running) {
1606
- await new Promise((r) => setTimeout(r, 50));
1607
- info = await checkExec.inspect();
1608
- }
1609
- if (info.ExitCode === 0) {
1610
- return;
1611
- }
1612
- } catch {}
1613
- await new Promise((r) => setTimeout(r, PROXY_POLL_INTERVAL_MS));
1614
- }
1615
- throw new Error("Proxy failed to start within timeout");
1616
- }
1617
- async function setupIptables(container) {
1618
- const rules = [
1619
- "/usr/sbin/iptables -A OUTPUT -o lo -j ACCEPT",
1620
- "/usr/sbin/iptables -A OUTPUT -m state --state ESTABLISHED,RELATED -j ACCEPT",
1621
- `/usr/sbin/iptables -A OUTPUT -p tcp -d 127.0.0.1 --dport ${PROXY_PORT} -m owner --uid-owner 100 -j ACCEPT`,
1622
- "/usr/sbin/iptables -A OUTPUT -m owner --uid-owner 100 -j DROP"
1623
- ].join(" && ");
1624
- const exec = await container.exec({
1625
- Cmd: ["sh", "-c", rules]
1626
- });
1627
- await exec.start({ Detach: true });
1628
- let info = await exec.inspect();
1629
- while (info.Running) {
1630
- await new Promise((r) => setTimeout(r, 50));
1631
- info = await exec.inspect();
1632
- }
1633
- if (info.ExitCode !== 0) {
1634
- throw new Error(`Failed to set up iptables rules (exit code ${info.ExitCode})`);
1635
- }
1636
- logger.debug("[Filtered] iptables rules applied — sandbox user restricted to proxy only");
1637
- }
1638
- function wrapWithTimeout(cmd, timeoutSec) {
1639
- return ["timeout", "-s", "KILL", String(timeoutSec), ...cmd];
1640
- }
1641
- function getInstallCommand(runtime, packages) {
1642
- switch (runtime) {
1643
- case "python":
1644
- return [
1645
- "pip",
1646
- "install",
1647
- "--user",
1648
- "--no-cache-dir",
1649
- "--break-system-packages",
1650
- "--disable-pip-version-check",
1651
- "--retries",
1652
- "0",
1653
- "--timeout",
1654
- "15",
1655
- ...packages
1656
- ];
1657
- case "node":
1658
- return ["npm", "install", "--prefix", "/sandbox", ...packages];
1659
- case "bun":
1660
- return ["bun", "install", "-g", "--global-dir=/sandbox/.bun-global", ...packages];
1661
- case "deno":
1662
- return ["sh", "-c", packages.map((p) => `deno cache ${p}`).join(" && ")];
1663
- case "bash":
1664
- return ["apk", "add", "--no-cache", ...packages];
1665
- default:
1666
- throw new Error(`Unknown runtime for package install: ${runtime}`);
2050
+ function calculateNetworkStats(stats) {
2051
+ if (!stats.networks) {
2052
+ return { in: 0, out: 0 };
2053
+ }
2054
+ let rxBytes = 0;
2055
+ let txBytes = 0;
2056
+ for (const iface of Object.values(stats.networks)) {
2057
+ rxBytes += iface.rx_bytes;
2058
+ txBytes += iface.tx_bytes;
1667
2059
  }
2060
+ return { in: rxBytes, out: txBytes };
1668
2061
  }
1669
- async function installPackages(container, runtime, packages, timeoutMs) {
1670
- const timeoutSec = Math.max(1, Math.ceil(timeoutMs / 1000));
1671
- const cmd = wrapWithTimeout(getInstallCommand(runtime, packages), timeoutSec);
1672
- logger.debug(`Installing packages: ${JSON.stringify(cmd)}`);
1673
- const env = [
1674
- "PATH=/sandbox/.local/bin:/sandbox/.npm-global/bin:/sandbox/.bun-global/bin:/usr/local/bin:/usr/bin:/bin:/usr/local/sbin:/usr/sbin:/sbin"
1675
- ];
1676
- if (runtime === "python") {
1677
- env.push("PYTHONUSERBASE=/sandbox/.local");
1678
- } else if (runtime === "node") {
1679
- env.push("NPM_CONFIG_PREFIX=/sandbox/.npm-global");
1680
- env.push("NPM_CONFIG_CACHE=/sandbox/.npm-cache");
1681
- env.push("npm_config_cache=/sandbox/.npm-cache");
1682
- env.push("NPM_CONFIG_FETCH_RETRIES=0");
1683
- env.push("npm_config_fetch_retries=0");
1684
- env.push("NPM_CONFIG_FETCH_RETRY_MINTIMEOUT=1000");
1685
- env.push("npm_config_fetch_retry_mintimeout=1000");
1686
- env.push("NPM_CONFIG_FETCH_RETRY_MAXTIMEOUT=2000");
1687
- env.push("npm_config_fetch_retry_maxtimeout=2000");
1688
- } else if (runtime === "bun") {
1689
- env.push("BUN_INSTALL_GLOBAL_DIR=/sandbox/.bun-global");
1690
- env.push("BUN_INSTALL_CACHE_DIR=/sandbox/.bun-cache");
1691
- env.push("BUN_INSTALL_BIN=/sandbox/.bun-global/bin");
1692
- } else if (runtime === "deno") {
1693
- env.push("DENO_DIR=/sandbox/.deno");
1694
- }
1695
- const exec = await container.exec({
1696
- Cmd: cmd,
1697
- AttachStdout: true,
1698
- AttachStderr: true,
1699
- Env: env,
1700
- User: runtime === "bash" ? "root" : "sandbox"
1701
- });
1702
- const stream = await exec.start({ Detach: false, Tty: false });
1703
- return new Promise((resolve2, reject) => {
1704
- let stderr = "";
1705
- const stdoutStream = new PassThrough;
1706
- const stderrStream = new PassThrough;
1707
- container.modem.demuxStream(stream, stdoutStream, stderrStream);
1708
- stderrStream.on("data", (chunk) => {
1709
- const text = chunk.toString();
1710
- stderr += text;
1711
- logger.debug(`[install:${runtime}:stderr] ${text.trimEnd()}`);
1712
- });
1713
- stdoutStream.on("data", (chunk) => {
1714
- const text = chunk.toString();
1715
- logger.debug(`[install:${runtime}:stdout] ${text.trimEnd()}`);
1716
- });
1717
- stream.on("end", async () => {
1718
- try {
1719
- const info = await exec.inspect();
1720
- if (info.ExitCode !== 0) {
1721
- reject(new Error(`Package install failed (exit code ${info.ExitCode}): ${stderr}`));
1722
- } else {
1723
- resolve2();
1724
- }
1725
- } catch (err) {
1726
- reject(err);
1727
- }
1728
- });
1729
- stream.on("error", reject);
2062
+ async function getContainerStats(container) {
2063
+ const stats = await container.stats({
2064
+ stream: false
1730
2065
  });
2066
+ const cpuPercent = calculateCPUPercent(stats);
2067
+ const memoryBytes = stats.memory_stats.usage;
2068
+ const network = calculateNetworkStats(stats);
2069
+ return {
2070
+ cpuPercent: Math.round(cpuPercent * 100) / 100,
2071
+ memoryMB: Math.round(memoryBytes / (1024 * 1024)),
2072
+ networkBytesIn: network.in,
2073
+ networkBytesOut: network.out
2074
+ };
2075
+ }
2076
+ function calculateResourceDelta(before, after) {
2077
+ return {
2078
+ cpuPercent: after.cpuPercent,
2079
+ memoryMB: after.memoryMB,
2080
+ networkBytesIn: after.networkBytesIn - before.networkBytesIn,
2081
+ networkBytesOut: after.networkBytesOut - before.networkBytesOut
2082
+ };
1731
2083
  }
1732
2084
 
2085
+ // src/engine/docker.ts
2086
+ var SANDBOX_WORKDIR = "/sandbox";
2087
+ var MAX_OUTPUT_BYTES = 1024 * 1024;
2088
+
1733
2089
  class DockerIsol8 {
1734
2090
  docker;
1735
2091
  mode;
@@ -1754,6 +2110,9 @@ class DockerIsol8 {
1754
2110
  dependencies;
1755
2111
  auditLogger;
1756
2112
  remoteCodePolicy;
2113
+ networkManager;
2114
+ executionManager;
2115
+ volumeManager;
1757
2116
  container = null;
1758
2117
  persistentRuntime = null;
1759
2118
  pool = null;
@@ -1813,6 +2172,18 @@ class DockerIsol8 {
1813
2172
  if (options.audit) {
1814
2173
  this.auditLogger = new AuditLogger(options.audit);
1815
2174
  }
2175
+ this.networkManager = new NetworkManager({
2176
+ network: this.network,
2177
+ networkFilter: this.networkFilter
2178
+ });
2179
+ this.executionManager = new ExecutionManager({
2180
+ secrets: this.secrets,
2181
+ maxOutputSize: this.maxOutputSize
2182
+ });
2183
+ this.volumeManager = new VolumeManager({
2184
+ readonlyRootFs: this.readonlyRootFs,
2185
+ sandboxWorkdir: SANDBOX_WORKDIR
2186
+ });
1816
2187
  if (options.debug) {
1817
2188
  logger.setDebug(true);
1818
2189
  }
@@ -1991,27 +2362,13 @@ class DockerIsol8 {
1991
2362
  if (!this.container) {
1992
2363
  throw new Error("No active container. Call execute() first in persistent mode.");
1993
2364
  }
1994
- if (this.readonlyRootFs) {
1995
- await writeFileViaExec(this.container, path, content);
1996
- } else {
1997
- const tar = createTarBuffer(path, content);
1998
- await this.container.putArchive(tar, { path: "/" });
1999
- }
2365
+ await this.volumeManager.putFile(this.container, path, content);
2000
2366
  }
2001
2367
  async getFile(path) {
2002
2368
  if (!this.container) {
2003
2369
  throw new Error("No active container. Call execute() first in persistent mode.");
2004
2370
  }
2005
- if (this.readonlyRootFs) {
2006
- return readFileViaExec(this.container, path);
2007
- }
2008
- const stream = await this.container.getArchive({ path });
2009
- const chunks = [];
2010
- for await (const chunk of stream) {
2011
- chunks.push(chunk);
2012
- }
2013
- const tarBuffer = Buffer.concat(chunks);
2014
- return extractFromTar(tarBuffer, path);
2371
+ return this.volumeManager.getFile(this.container, path);
2015
2372
  }
2016
2373
  get containerId() {
2017
2374
  return this.container?.id ?? null;
@@ -2027,26 +2384,24 @@ class DockerIsol8 {
2027
2384
  Image: image,
2028
2385
  Cmd: ["sleep", "infinity"],
2029
2386
  WorkingDir: SANDBOX_WORKDIR,
2030
- Env: this.buildEnv(),
2387
+ Env: this.executionManager.buildEnv(undefined, this.networkManager.proxyPort, this.network, this.networkFilter),
2031
2388
  NetworkDisabled: this.network === "none",
2032
2389
  HostConfig: this.buildHostConfig(),
2033
2390
  StopTimeout: 2
2034
2391
  });
2035
2392
  try {
2036
2393
  await container.start();
2037
- if (this.network === "filtered") {
2038
- await startProxy(container, this.networkFilter);
2039
- await setupIptables(container);
2040
- }
2394
+ await this.networkManager.startProxy(container);
2395
+ await this.networkManager.setupIptables(container);
2041
2396
  const ext = request.fileExtension ?? adapter.getFileExtension();
2042
2397
  const filePath = `${SANDBOX_WORKDIR}/main${ext}`;
2043
- await writeFileViaExec(container, filePath, request.code);
2398
+ await this.volumeManager.writeFileViaExec(container, filePath, request.code);
2044
2399
  if (request.installPackages?.length) {
2045
- await installPackages(container, request.runtime, request.installPackages, timeoutMs);
2400
+ await this.executionManager.installPackages(container, request.runtime, request.installPackages, timeoutMs);
2046
2401
  }
2047
2402
  if (request.files) {
2048
2403
  for (const [fPath, fContent] of Object.entries(request.files)) {
2049
- await writeFileViaExec(container, fPath, fContent);
2404
+ await this.volumeManager.writeFileViaExec(container, fPath, fContent);
2050
2405
  }
2051
2406
  }
2052
2407
  const rawCmd = adapter.getCommand(request.code, filePath);
@@ -2054,22 +2409,22 @@ class DockerIsol8 {
2054
2409
  let cmd;
2055
2410
  if (request.stdin) {
2056
2411
  const stdinPath = `${SANDBOX_WORKDIR}/_stdin`;
2057
- await writeFileViaExec(container, stdinPath, request.stdin);
2412
+ await this.volumeManager.writeFileViaExec(container, stdinPath, request.stdin);
2058
2413
  const cmdStr = rawCmd.map((a) => `'${a.replace(/'/g, "'\\''")}'`).join(" ");
2059
- cmd = wrapWithTimeout(["sh", "-c", `cat ${stdinPath} | ${cmdStr}`], timeoutSec);
2414
+ cmd = this.executionManager.wrapWithTimeout(["sh", "-c", `cat ${stdinPath} | ${cmdStr}`], timeoutSec);
2060
2415
  } else {
2061
- cmd = wrapWithTimeout(rawCmd, timeoutSec);
2416
+ cmd = this.executionManager.wrapWithTimeout(rawCmd, timeoutSec);
2062
2417
  }
2063
2418
  const exec = await container.exec({
2064
2419
  Cmd: cmd,
2065
- Env: this.buildEnv(request.env),
2420
+ Env: this.executionManager.buildEnv(request.env, this.networkManager.proxyPort, this.network, this.networkFilter),
2066
2421
  AttachStdout: true,
2067
2422
  AttachStderr: true,
2068
2423
  WorkingDir: SANDBOX_WORKDIR,
2069
2424
  User: "sandbox"
2070
2425
  });
2071
2426
  const execStream = await exec.start({ Tty: false });
2072
- yield* this.streamExecOutput(execStream, exec, container, timeoutMs);
2427
+ yield* this.executionManager.streamExecOutput(execStream, exec, container, timeoutMs);
2073
2428
  } finally {
2074
2429
  if (this.persist) {
2075
2430
  logger.debug(`[Persist] Leaving container running for inspection: ${container.id}`);
@@ -2144,7 +2499,7 @@ class DockerIsol8 {
2144
2499
  createOptions: {
2145
2500
  Cmd: ["sleep", "infinity"],
2146
2501
  WorkingDir: SANDBOX_WORKDIR,
2147
- Env: this.buildEnv(),
2502
+ Env: this.executionManager.buildEnv(undefined, this.networkManager.proxyPort, this.network, this.networkFilter),
2148
2503
  NetworkDisabled: this.network === "none",
2149
2504
  HostConfig: this.buildHostConfig(),
2150
2505
  StopTimeout: 2
@@ -2168,10 +2523,8 @@ class DockerIsol8 {
2168
2523
  }
2169
2524
  }
2170
2525
  try {
2171
- if (this.network === "filtered") {
2172
- await startProxy(container, this.networkFilter);
2173
- await setupIptables(container);
2174
- }
2526
+ await this.networkManager.startProxy(container);
2527
+ await this.networkManager.setupIptables(container);
2175
2528
  const canUseInline = !(req.stdin || req.files || req.outputPaths) && (!req.installPackages || req.installPackages.length === 0);
2176
2529
  let rawCmd;
2177
2530
  if (canUseInline) {
@@ -2180,36 +2533,36 @@ class DockerIsol8 {
2180
2533
  } catch {
2181
2534
  const ext = req.fileExtension ?? adapter.getFileExtension();
2182
2535
  const filePath = `${SANDBOX_WORKDIR}/main${ext}`;
2183
- await writeFileViaExec(container, filePath, req.code);
2536
+ await this.volumeManager.writeFileViaExec(container, filePath, req.code);
2184
2537
  rawCmd = adapter.getCommand(req.code, filePath);
2185
2538
  }
2186
2539
  } else {
2187
2540
  const ext = req.fileExtension ?? adapter.getFileExtension();
2188
2541
  const filePath = `${SANDBOX_WORKDIR}/main${ext}`;
2189
- await writeFileViaExec(container, filePath, req.code);
2542
+ await this.volumeManager.writeFileViaExec(container, filePath, req.code);
2190
2543
  rawCmd = adapter.getCommand(req.code, filePath);
2191
2544
  }
2192
2545
  if (req.installPackages?.length) {
2193
- await installPackages(container, req.runtime, req.installPackages, timeoutMs);
2546
+ await this.executionManager.installPackages(container, req.runtime, req.installPackages, timeoutMs);
2194
2547
  }
2195
2548
  const timeoutSec = Math.ceil(timeoutMs / 1000);
2196
2549
  let cmd;
2197
2550
  if (req.stdin) {
2198
2551
  const stdinPath = `${SANDBOX_WORKDIR}/_stdin`;
2199
- await writeFileViaExec(container, stdinPath, req.stdin);
2552
+ await this.volumeManager.writeFileViaExec(container, stdinPath, req.stdin);
2200
2553
  const cmdStr = rawCmd.map((a) => `'${a.replace(/'/g, "'\\''")}' `).join("");
2201
- cmd = wrapWithTimeout(["sh", "-c", `cat ${stdinPath} | ${cmdStr}`], timeoutSec);
2554
+ cmd = this.executionManager.wrapWithTimeout(["sh", "-c", `cat ${stdinPath} | ${cmdStr}`], timeoutSec);
2202
2555
  } else {
2203
- cmd = wrapWithTimeout(rawCmd, timeoutSec);
2556
+ cmd = this.executionManager.wrapWithTimeout(rawCmd, timeoutSec);
2204
2557
  }
2205
2558
  if (req.files) {
2206
2559
  for (const [fPath, fContent] of Object.entries(req.files)) {
2207
- await writeFileViaExec(container, fPath, fContent);
2560
+ await this.volumeManager.writeFileViaExec(container, fPath, fContent);
2208
2561
  }
2209
2562
  }
2210
2563
  const exec = await container.exec({
2211
2564
  Cmd: cmd,
2212
- Env: this.buildEnv(req.env),
2565
+ Env: this.executionManager.buildEnv(req.env, this.networkManager.proxyPort, this.network, this.networkFilter),
2213
2566
  AttachStdout: true,
2214
2567
  AttachStderr: true,
2215
2568
  WorkingDir: SANDBOX_WORKDIR,
@@ -2217,7 +2570,7 @@ class DockerIsol8 {
2217
2570
  });
2218
2571
  const start = performance.now();
2219
2572
  const execStream = await exec.start({ Tty: false });
2220
- const { stdout, stderr, truncated } = await this.collectExecOutput(execStream, container, timeoutMs);
2573
+ const { stdout, stderr, truncated } = await this.executionManager.collectExecOutput(execStream, container, timeoutMs);
2221
2574
  const durationMs = Math.round(performance.now() - start);
2222
2575
  const inspectResult = await exec.inspect();
2223
2576
  let resourceUsage;
@@ -2241,8 +2594,8 @@ class DockerIsol8 {
2241
2594
  }
2242
2595
  }
2243
2596
  const result = {
2244
- stdout: this.postProcessOutput(stdout, truncated),
2245
- stderr: this.postProcessOutput(stderr, false),
2597
+ stdout: this.executionManager.postProcessOutput(stdout, truncated),
2598
+ stderr: this.executionManager.postProcessOutput(stderr, false),
2246
2599
  exitCode: inspectResult.ExitCode ?? 1,
2247
2600
  durationMs,
2248
2601
  truncated,
@@ -2252,7 +2605,7 @@ class DockerIsol8 {
2252
2605
  containerId: container.id,
2253
2606
  ...resourceUsage ? { resourceUsage } : {},
2254
2607
  ...networkLogs ? { networkLogs } : {},
2255
- ...req.outputPaths ? { files: await this.retrieveFiles(container, req.outputPaths) } : {}
2608
+ ...req.outputPaths ? { files: await this.volumeManager.retrieveFiles(container, req.outputPaths) } : {}
2256
2609
  };
2257
2610
  if (this.auditLogger) {
2258
2611
  await this.recordAudit(req, result, startTime, container);
@@ -2279,37 +2632,27 @@ class DockerIsol8 {
2279
2632
  }
2280
2633
  const ext = req.fileExtension ?? adapter.getFileExtension();
2281
2634
  const filePath = `${SANDBOX_WORKDIR}/exec_${Date.now()}${ext}`;
2282
- if (this.readonlyRootFs) {
2283
- await writeFileViaExec(this.container, filePath, req.code);
2284
- } else {
2285
- const tar = createTarBuffer(filePath, req.code);
2286
- await this.container.putArchive(tar, { path: "/" });
2287
- }
2635
+ await this.volumeManager.putFile(this.container, filePath, req.code);
2288
2636
  if (req.files) {
2289
2637
  for (const [fPath, fContent] of Object.entries(req.files)) {
2290
- if (this.readonlyRootFs) {
2291
- await writeFileViaExec(this.container, fPath, fContent);
2292
- } else {
2293
- const tar = createTarBuffer(fPath, fContent);
2294
- await this.container.putArchive(tar, { path: "/" });
2295
- }
2638
+ await this.volumeManager.putFile(this.container, fPath, fContent);
2296
2639
  }
2297
2640
  }
2298
2641
  const rawCmd = adapter.getCommand(req.code, filePath);
2299
2642
  const timeoutSec = Math.ceil(timeoutMs / 1000);
2300
2643
  if (req.installPackages?.length) {
2301
- await installPackages(this.container, req.runtime, req.installPackages, timeoutMs);
2644
+ await this.executionManager.installPackages(this.container, req.runtime, req.installPackages, timeoutMs);
2302
2645
  }
2303
2646
  let cmd;
2304
2647
  if (req.stdin) {
2305
2648
  const stdinPath = `${SANDBOX_WORKDIR}/_stdin_${Date.now()}`;
2306
- await writeFileViaExec(this.container, stdinPath, req.stdin);
2649
+ await this.volumeManager.writeFileViaExec(this.container, stdinPath, req.stdin);
2307
2650
  const cmdStr = rawCmd.map((a) => `'${a.replace(/'/g, "'\\''")}' `).join("");
2308
- cmd = wrapWithTimeout(["sh", "-c", `cat ${stdinPath} | ${cmdStr}`], timeoutSec);
2651
+ cmd = this.executionManager.wrapWithTimeout(["sh", "-c", `cat ${stdinPath} | ${cmdStr}`], timeoutSec);
2309
2652
  } else {
2310
- cmd = wrapWithTimeout(rawCmd, timeoutSec);
2653
+ cmd = this.executionManager.wrapWithTimeout(rawCmd, timeoutSec);
2311
2654
  }
2312
- const execEnv = this.buildEnv(req.env);
2655
+ const execEnv = this.executionManager.buildEnv(req.env, this.networkManager.proxyPort, this.network, this.networkFilter);
2313
2656
  const exec = await this.container.exec({
2314
2657
  Cmd: cmd,
2315
2658
  Env: execEnv,
@@ -2320,7 +2663,7 @@ class DockerIsol8 {
2320
2663
  });
2321
2664
  const start = performance.now();
2322
2665
  const execStream = await exec.start({ Tty: false });
2323
- const { stdout, stderr, truncated } = await this.collectExecOutput(execStream, this.container, timeoutMs);
2666
+ const { stdout, stderr, truncated } = await this.executionManager.collectExecOutput(execStream, this.container, timeoutMs);
2324
2667
  const durationMs = Math.round(performance.now() - start);
2325
2668
  const inspectResult = await exec.inspect();
2326
2669
  let resourceUsage;
@@ -2349,8 +2692,8 @@ class DockerIsol8 {
2349
2692
  }
2350
2693
  }
2351
2694
  const result = {
2352
- stdout: this.postProcessOutput(stdout, truncated),
2353
- stderr: this.postProcessOutput(stderr, false),
2695
+ stdout: this.executionManager.postProcessOutput(stdout, truncated),
2696
+ stderr: this.executionManager.postProcessOutput(stderr, false),
2354
2697
  exitCode: inspectResult.ExitCode ?? 1,
2355
2698
  durationMs,
2356
2699
  truncated,
@@ -2368,22 +2711,7 @@ class DockerIsol8 {
2368
2711
  return result;
2369
2712
  }
2370
2713
  async retrieveFiles(container, paths) {
2371
- const files = {};
2372
- for (const p of paths) {
2373
- try {
2374
- const buf = this.readonlyRootFs ? await readFileViaExec(container, p) : await this.getFileFromContainer(container, p);
2375
- files[p] = buf.toString("base64");
2376
- } catch {}
2377
- }
2378
- return files;
2379
- }
2380
- async getFileFromContainer(container, path) {
2381
- const stream = await container.getArchive({ path });
2382
- const chunks = [];
2383
- for await (const chunk of stream) {
2384
- chunks.push(chunk);
2385
- }
2386
- return extractFromTar(Buffer.concat(chunks), path);
2714
+ return this.volumeManager.retrieveFiles(container, paths);
2387
2715
  }
2388
2716
  async startPersistentContainer(adapter) {
2389
2717
  const image = await this.resolveImage(adapter);
@@ -2391,7 +2719,7 @@ class DockerIsol8 {
2391
2719
  Image: image,
2392
2720
  Cmd: ["sleep", "infinity"],
2393
2721
  WorkingDir: SANDBOX_WORKDIR,
2394
- Env: this.buildEnv(),
2722
+ Env: this.executionManager.buildEnv(undefined, this.networkManager.proxyPort, this.network, this.networkFilter),
2395
2723
  NetworkDisabled: this.network === "none",
2396
2724
  HostConfig: this.buildHostConfig(),
2397
2725
  StopTimeout: 2,
@@ -2401,10 +2729,8 @@ class DockerIsol8 {
2401
2729
  }
2402
2730
  });
2403
2731
  await this.container.start();
2404
- if (this.network === "filtered") {
2405
- await startProxy(this.container, this.networkFilter);
2406
- await setupIptables(this.container);
2407
- }
2732
+ await this.networkManager.startProxy(this.container);
2733
+ await this.networkManager.setupIptables(this.container);
2408
2734
  this.persistentRuntime = adapter;
2409
2735
  }
2410
2736
  getAdapter(runtime) {
@@ -2468,181 +2794,6 @@ class DockerIsol8 {
2468
2794
  }
2469
2795
  throw new Error("Embedded default seccomp profile is unavailable");
2470
2796
  }
2471
- buildEnv(extra) {
2472
- const env = [
2473
- "PYTHONUNBUFFERED=1",
2474
- "PYTHONUSERBASE=/sandbox/.local",
2475
- "NPM_CONFIG_PREFIX=/sandbox/.npm-global",
2476
- "DENO_DIR=/sandbox/.deno",
2477
- "PATH=/sandbox/.local/bin:/sandbox/.npm-global/bin:/sandbox/.bun-global/bin:/usr/local/bin:/usr/bin:/bin",
2478
- "NODE_PATH=/usr/local/lib/node_modules:/sandbox/.npm-global/lib/node_modules:/sandbox/node_modules"
2479
- ];
2480
- for (const [key, value] of Object.entries(this.secrets)) {
2481
- env.push(`${key}=${value}`);
2482
- }
2483
- if (extra) {
2484
- for (const [key, value] of Object.entries(extra)) {
2485
- env.push(`${key}=${value}`);
2486
- }
2487
- }
2488
- if (this.network === "filtered") {
2489
- if (this.networkFilter) {
2490
- env.push(`ISOL8_WHITELIST=${JSON.stringify(this.networkFilter.whitelist)}`);
2491
- env.push(`ISOL8_BLACKLIST=${JSON.stringify(this.networkFilter.blacklist)}`);
2492
- }
2493
- env.push(`HTTP_PROXY=http://127.0.0.1:${PROXY_PORT}`);
2494
- env.push(`HTTPS_PROXY=http://127.0.0.1:${PROXY_PORT}`);
2495
- env.push(`http_proxy=http://127.0.0.1:${PROXY_PORT}`);
2496
- env.push(`https_proxy=http://127.0.0.1:${PROXY_PORT}`);
2497
- }
2498
- return env;
2499
- }
2500
- async* streamExecOutput(stream, exec, container, timeoutMs) {
2501
- const queue = [];
2502
- let resolve2 = null;
2503
- let done = false;
2504
- const push = (event) => {
2505
- queue.push(event);
2506
- if (resolve2) {
2507
- resolve2();
2508
- resolve2 = null;
2509
- }
2510
- };
2511
- const timer = setTimeout(() => {
2512
- push({ type: "error", data: "EXECUTION TIMED OUT" });
2513
- push({ type: "exit", data: "137" });
2514
- done = true;
2515
- }, timeoutMs);
2516
- const stdoutStream = new PassThrough;
2517
- const stderrStream = new PassThrough;
2518
- container.modem.demuxStream(stream, stdoutStream, stderrStream);
2519
- stdoutStream.on("data", (chunk) => {
2520
- let text = chunk.toString("utf-8");
2521
- if (Object.keys(this.secrets).length > 0) {
2522
- text = maskSecrets(text, this.secrets);
2523
- }
2524
- push({ type: "stdout", data: text });
2525
- });
2526
- stderrStream.on("data", (chunk) => {
2527
- let text = chunk.toString("utf-8");
2528
- if (Object.keys(this.secrets).length > 0) {
2529
- text = maskSecrets(text, this.secrets);
2530
- }
2531
- push({ type: "stderr", data: text });
2532
- });
2533
- stream.on("end", async () => {
2534
- clearTimeout(timer);
2535
- try {
2536
- const info = await exec.inspect();
2537
- push({ type: "exit", data: (info.ExitCode ?? 0).toString() });
2538
- } catch {
2539
- push({ type: "exit", data: "1" });
2540
- }
2541
- done = true;
2542
- });
2543
- stream.on("error", (err) => {
2544
- clearTimeout(timer);
2545
- push({ type: "error", data: err.message });
2546
- push({ type: "exit", data: "1" });
2547
- done = true;
2548
- });
2549
- while (!done || queue.length > 0) {
2550
- if (queue.length > 0) {
2551
- yield queue.shift();
2552
- } else if (resolve2) {
2553
- await new Promise((r) => {
2554
- resolve2 = r;
2555
- });
2556
- } else {
2557
- await new Promise((r) => setTimeout(r, 10));
2558
- }
2559
- }
2560
- }
2561
- async collectExecOutput(stream, container, timeoutMs) {
2562
- return new Promise((resolve2, reject) => {
2563
- let stdout = "";
2564
- let stderr = "";
2565
- let truncated = false;
2566
- let settled = false;
2567
- let stdoutEnded = false;
2568
- let stderrEnded = false;
2569
- const timer = setTimeout(() => {
2570
- if (settled) {
2571
- return;
2572
- }
2573
- settled = true;
2574
- if (stream.destroy) {
2575
- stream.destroy();
2576
- }
2577
- resolve2({ stdout, stderr: `${stderr}
2578
- --- EXECUTION TIMED OUT ---`, truncated });
2579
- }, timeoutMs);
2580
- const stdoutStream = new PassThrough;
2581
- const stderrStream = new PassThrough;
2582
- container.modem.demuxStream(stream, stdoutStream, stderrStream);
2583
- stdoutStream.on("data", (chunk) => {
2584
- stdout += chunk.toString("utf-8");
2585
- if (stdout.length > this.maxOutputSize) {
2586
- const result = truncateOutput(stdout, this.maxOutputSize);
2587
- stdout = result.text;
2588
- truncated = true;
2589
- }
2590
- });
2591
- stderrStream.on("data", (chunk) => {
2592
- stderr += chunk.toString("utf-8");
2593
- if (stderr.length > this.maxOutputSize) {
2594
- const result = truncateOutput(stderr, this.maxOutputSize);
2595
- stderr = result.text;
2596
- truncated = true;
2597
- }
2598
- });
2599
- const checkDone = () => {
2600
- if (settled) {
2601
- return;
2602
- }
2603
- if (stdoutEnded && stderrEnded) {
2604
- settled = true;
2605
- clearTimeout(timer);
2606
- resolve2({ stdout, stderr, truncated });
2607
- }
2608
- };
2609
- stdoutStream.on("end", () => {
2610
- stdoutEnded = true;
2611
- checkDone();
2612
- });
2613
- stderrStream.on("end", () => {
2614
- stderrEnded = true;
2615
- checkDone();
2616
- });
2617
- stream.on("error", (err) => {
2618
- if (settled) {
2619
- return;
2620
- }
2621
- settled = true;
2622
- clearTimeout(timer);
2623
- reject(err);
2624
- });
2625
- stream.on("end", () => {
2626
- if (settled) {
2627
- return;
2628
- }
2629
- setTimeout(() => {
2630
- if (!settled) {
2631
- stdoutEnded = true;
2632
- stderrEnded = true;
2633
- checkDone();
2634
- }
2635
- }, 100);
2636
- });
2637
- });
2638
- }
2639
- postProcessOutput(output, _truncated) {
2640
- let result = output;
2641
- if (Object.keys(this.secrets).length > 0) {
2642
- result = maskSecrets(result, this.secrets);
2643
- }
2644
- return result.trimEnd();
2645
- }
2646
2797
  static async cleanup(docker) {
2647
2798
  const dockerInstance = docker ?? new Docker;
2648
2799
  const containers = await dockerInstance.listContainers({ all: true });
@@ -2693,7 +2844,7 @@ init_logger();
2693
2844
  // package.json
2694
2845
  var package_default = {
2695
2846
  name: "@isol8/core",
2696
- version: "0.14.7",
2847
+ version: "0.16.0",
2697
2848
  description: "Sandboxed code execution engine for AI agents and apps (Docker, runtime and network controls)",
2698
2849
  author: "Illusion47586",
2699
2850
  license: "MIT",
@@ -2721,7 +2872,7 @@ var package_default = {
2721
2872
  "test:prod": "echo 'No production tests in core package'",
2722
2873
  "lint:check": "ultracite check",
2723
2874
  "lint:fix": "ultracite fix",
2724
- schema: "ts-json-schema-generator --path src/types.ts --type Isol8UserConfig --tsconfig tsconfig.json -o schema/isol8.config.schema.json && ultracite fix schema/isol8.config.schema.json"
2875
+ schema: "ts-json-schema-generator --path src/types.ts --type Isol8UserConfig --tsconfig tsconfig.json --no-type-check -o schema/isol8.config.schema.json && ultracite fix schema/isol8.config.schema.json"
2725
2876
  },
2726
2877
  dependencies: {
2727
2878
  dockerode: "^4.0.9",
@@ -2777,4 +2928,4 @@ export {
2777
2928
  BunAdapter
2778
2929
  };
2779
2930
 
2780
- //# debugId=07DD5AE18A61308A64756E2164756E21
2931
+ //# debugId=A775CBFF7103831D64756E2164756E21