@vm0/runner 3.6.0 → 3.6.1

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.
Files changed (2) hide show
  1. package/index.js +471 -354
  2. package/package.json +1 -1
package/index.js CHANGED
@@ -450,7 +450,30 @@ import { exec } from "child_process";
450
450
  import { promisify } from "util";
451
451
  import * as fs2 from "fs";
452
452
  import * as path from "path";
453
+
454
+ // src/lib/logger.ts
455
+ var _log = null;
456
+ var _error = null;
457
+ function getLog() {
458
+ return _log ?? console.log.bind(console);
459
+ }
460
+ function getError() {
461
+ return _error ?? console.error.bind(console);
462
+ }
463
+ function setGlobalLogger(log, error) {
464
+ _log = log;
465
+ _error = error ?? log;
466
+ }
467
+ function createLogger(prefix) {
468
+ return {
469
+ log: (message) => getLog()(`[${prefix}] ${message}`),
470
+ error: (message) => getError()(`[${prefix}] ${message}`)
471
+ };
472
+ }
473
+
474
+ // src/lib/firecracker/ip-pool.ts
453
475
  var execAsync = promisify(exec);
476
+ var logger = createLogger("IP Pool");
454
477
  var VM0_RUN_DIR = "/var/run/vm0";
455
478
  var REGISTRY_FILE_PATH = path.join(VM0_RUN_DIR, "ip-registry.json");
456
479
  var BRIDGE_NAME = "vm0br0";
@@ -564,8 +587,8 @@ function reconcileRegistry(registry, activeTaps) {
564
587
  } else if (isWithinGracePeriod) {
565
588
  reconciled.allocations[ip] = allocation;
566
589
  } else {
567
- console.log(
568
- `[IP Pool] Removing stale allocation for ${ip} (TAP ${allocation.tapDevice} no longer exists)`
590
+ logger.log(
591
+ `Removing stale allocation for ${ip} (TAP ${allocation.tapDevice} no longer exists)`
569
592
  );
570
593
  }
571
594
  }
@@ -593,8 +616,8 @@ async function allocateIP(vmId) {
593
616
  }
594
617
  const allocatedCount = Object.keys(registry.allocations).length;
595
618
  const allocatedIPs = Object.keys(registry.allocations).sort();
596
- console.log(
597
- `[IP Pool] Current state: ${allocatedCount} IPs allocated [${allocatedIPs.join(", ")}], assigning ${ip}`
619
+ logger.log(
620
+ `Current state: ${allocatedCount} IPs allocated [${allocatedIPs.join(", ")}], assigning ${ip}`
598
621
  );
599
622
  registry.allocations[ip] = {
600
623
  vmId,
@@ -602,7 +625,7 @@ async function allocateIP(vmId) {
602
625
  allocatedAt: (/* @__PURE__ */ new Date()).toISOString()
603
626
  };
604
627
  writeRegistry(registry);
605
- console.log(`[IP Pool] Allocated ${ip} for VM ${vmId} (TAP ${tapDevice})`);
628
+ logger.log(`Allocated ${ip} for VM ${vmId} (TAP ${tapDevice})`);
606
629
  return ip;
607
630
  });
608
631
  }
@@ -613,42 +636,39 @@ async function releaseIP(ip) {
613
636
  const allocation = registry.allocations[ip];
614
637
  delete registry.allocations[ip];
615
638
  writeRegistry(registry);
616
- console.log(
617
- `[IP Pool] Released ${ip} (was allocated to VM ${allocation.vmId})`
618
- );
639
+ logger.log(`Released ${ip} (was allocated to VM ${allocation.vmId})`);
619
640
  } else {
620
- console.log(`[IP Pool] IP ${ip} was not in registry, nothing to release`);
641
+ logger.log(`IP ${ip} was not in registry, nothing to release`);
621
642
  }
622
643
  });
623
644
  }
624
645
  async function cleanupOrphanedAllocations() {
625
646
  return withLock(async () => {
626
- console.log("[IP Pool] Cleaning up orphaned allocations...");
647
+ logger.log("Cleaning up orphaned allocations...");
627
648
  const registry = readRegistry();
628
649
  const beforeCount = Object.keys(registry.allocations).length;
629
650
  if (beforeCount === 0) {
630
- console.log("[IP Pool] No allocations in registry, nothing to clean up");
651
+ logger.log("No allocations in registry, nothing to clean up");
631
652
  return;
632
653
  }
633
654
  const activeTaps = await scanTapDevices();
634
- console.log(
635
- `[IP Pool] Found ${activeTaps.size} active TAP device(s) on bridge`
636
- );
655
+ logger.log(`Found ${activeTaps.size} active TAP device(s) on bridge`);
637
656
  const reconciled = reconcileRegistry(registry, activeTaps);
638
657
  const afterCount = Object.keys(reconciled.allocations).length;
639
658
  if (afterCount !== beforeCount) {
640
659
  writeRegistry(reconciled);
641
- console.log(
642
- `[IP Pool] Cleaned up ${beforeCount - afterCount} orphaned allocation(s)`
660
+ logger.log(
661
+ `Cleaned up ${beforeCount - afterCount} orphaned allocation(s)`
643
662
  );
644
663
  } else {
645
- console.log("[IP Pool] No orphaned allocations found");
664
+ logger.log("No orphaned allocations found");
646
665
  }
647
666
  });
648
667
  }
649
668
 
650
669
  // src/lib/firecracker/network.ts
651
670
  var execAsync2 = promisify2(exec2);
671
+ var logger2 = createLogger("Network");
652
672
  var BRIDGE_NAME2 = "vm0br0";
653
673
  var BRIDGE_IP = "172.16.0.1";
654
674
  var BRIDGE_NETMASK = "255.255.255.0";
@@ -700,28 +720,28 @@ async function getDefaultInterface() {
700
720
  }
701
721
  async function setupForwardRules() {
702
722
  const extIface = await getDefaultInterface();
703
- console.log(`Setting up FORWARD rules for ${BRIDGE_NAME2} <-> ${extIface}`);
723
+ logger2.log(`Setting up FORWARD rules for ${BRIDGE_NAME2} <-> ${extIface}`);
704
724
  try {
705
725
  await execCommand(
706
726
  `iptables -C FORWARD -i ${BRIDGE_NAME2} -o ${extIface} -j ACCEPT`
707
727
  );
708
- console.log("FORWARD outbound rule already exists");
728
+ logger2.log("FORWARD outbound rule already exists");
709
729
  } catch {
710
730
  await execCommand(
711
731
  `iptables -I FORWARD -i ${BRIDGE_NAME2} -o ${extIface} -j ACCEPT`
712
732
  );
713
- console.log("FORWARD outbound rule added");
733
+ logger2.log("FORWARD outbound rule added");
714
734
  }
715
735
  try {
716
736
  await execCommand(
717
737
  `iptables -C FORWARD -i ${extIface} -o ${BRIDGE_NAME2} -m state --state RELATED,ESTABLISHED -j ACCEPT`
718
738
  );
719
- console.log("FORWARD inbound rule already exists");
739
+ logger2.log("FORWARD inbound rule already exists");
720
740
  } catch {
721
741
  await execCommand(
722
742
  `iptables -I FORWARD -i ${extIface} -o ${BRIDGE_NAME2} -m state --state RELATED,ESTABLISHED -j ACCEPT`
723
743
  );
724
- console.log("FORWARD inbound rule added");
744
+ logger2.log("FORWARD inbound rule added");
725
745
  }
726
746
  }
727
747
  async function bridgeExists() {
@@ -734,11 +754,11 @@ async function bridgeExists() {
734
754
  }
735
755
  async function setupBridge() {
736
756
  if (await bridgeExists()) {
737
- console.log(`Bridge ${BRIDGE_NAME2} already exists`);
757
+ logger2.log(`Bridge ${BRIDGE_NAME2} already exists`);
738
758
  await setupForwardRules();
739
759
  return;
740
760
  }
741
- console.log(`Creating bridge ${BRIDGE_NAME2}...`);
761
+ logger2.log(`Creating bridge ${BRIDGE_NAME2}...`);
742
762
  await execCommand(`ip link add name ${BRIDGE_NAME2} type bridge`);
743
763
  await execCommand(
744
764
  `ip addr add ${BRIDGE_IP}/${BRIDGE_NETMASK} dev ${BRIDGE_NAME2}`
@@ -749,15 +769,15 @@ async function setupBridge() {
749
769
  await execCommand(
750
770
  `iptables -t nat -C POSTROUTING -s ${BRIDGE_CIDR} -j MASQUERADE`
751
771
  );
752
- console.log("NAT rule already exists");
772
+ logger2.log("NAT rule already exists");
753
773
  } catch {
754
774
  await execCommand(
755
775
  `iptables -t nat -A POSTROUTING -s ${BRIDGE_CIDR} -j MASQUERADE`
756
776
  );
757
- console.log("NAT rule added");
777
+ logger2.log("NAT rule added");
758
778
  }
759
779
  await setupForwardRules();
760
- console.log(`Bridge ${BRIDGE_NAME2} configured with IP ${BRIDGE_IP}`);
780
+ logger2.log(`Bridge ${BRIDGE_NAME2} configured with IP ${BRIDGE_IP}`);
761
781
  }
762
782
  async function tapDeviceExists(tapDevice) {
763
783
  try {
@@ -777,7 +797,7 @@ async function clearStaleIptablesRulesForIP(ip) {
777
797
  if (rulesForIP.length === 0) {
778
798
  return;
779
799
  }
780
- console.log(
800
+ logger2.log(
781
801
  `Clearing ${rulesForIP.length} stale iptables rule(s) for IP ${ip}`
782
802
  );
783
803
  for (const rule of rulesForIP) {
@@ -790,24 +810,27 @@ async function clearStaleIptablesRulesForIP(ip) {
790
810
  } catch {
791
811
  }
792
812
  }
793
- async function createTapDevice(vmId, logger) {
794
- const log = logger ?? console.log;
813
+ async function createTapDevice(vmId) {
795
814
  const tapDevice = `tap${vmId.substring(0, 8)}`;
796
815
  const guestMac = generateMacAddress(vmId);
797
816
  const guestIp = await allocateIP(vmId);
798
- log(`[VM ${vmId}] IP allocated: ${guestIp}`);
817
+ logger2.log(`[VM ${vmId}] IP allocated: ${guestIp}`);
799
818
  await clearStaleIptablesRulesForIP(guestIp);
800
- log(`[VM ${vmId}] Stale iptables cleared`);
819
+ logger2.log(`[VM ${vmId}] Stale iptables cleared`);
801
820
  if (await tapDeviceExists(tapDevice)) {
802
- log(`[VM ${vmId}] TAP device ${tapDevice} already exists, deleting...`);
821
+ logger2.log(
822
+ `[VM ${vmId}] TAP device ${tapDevice} already exists, deleting...`
823
+ );
803
824
  await deleteTapDevice(tapDevice);
804
825
  }
805
826
  await execCommand(`ip tuntap add ${tapDevice} mode tap`);
806
- log(`[VM ${vmId}] TAP device created`);
827
+ logger2.log(`[VM ${vmId}] TAP device created`);
807
828
  await execCommand(`ip link set ${tapDevice} master ${BRIDGE_NAME2}`);
808
- log(`[VM ${vmId}] TAP added to bridge`);
829
+ logger2.log(`[VM ${vmId}] TAP added to bridge`);
809
830
  await execCommand(`ip link set ${tapDevice} up`);
810
- log(`[VM ${vmId}] TAP created: ${tapDevice}, MAC ${guestMac}, IP ${guestIp}`);
831
+ logger2.log(
832
+ `[VM ${vmId}] TAP created: ${tapDevice}, MAC ${guestMac}, IP ${guestIp}`
833
+ );
811
834
  return {
812
835
  tapDevice,
813
836
  guestMac,
@@ -818,15 +841,15 @@ async function createTapDevice(vmId, logger) {
818
841
  }
819
842
  async function deleteTapDevice(tapDevice, guestIp) {
820
843
  if (!await tapDeviceExists(tapDevice)) {
821
- console.log(`TAP device ${tapDevice} does not exist, skipping delete`);
844
+ logger2.log(`TAP device ${tapDevice} does not exist, skipping delete`);
822
845
  } else {
823
846
  await execCommand(`ip link delete ${tapDevice}`);
824
- console.log(`TAP device ${tapDevice} deleted`);
847
+ logger2.log(`TAP device ${tapDevice} deleted`);
825
848
  }
826
849
  if (guestIp) {
827
850
  try {
828
851
  await execCommand(`ip neigh del ${guestIp} dev ${BRIDGE_NAME2}`, true);
829
- console.log(`ARP entry cleared for ${guestIp}`);
852
+ logger2.log(`ARP entry cleared for ${guestIp}`);
830
853
  } catch {
831
854
  }
832
855
  }
@@ -859,47 +882,47 @@ function checkNetworkPrerequisites() {
859
882
  }
860
883
  async function setupCIDRProxyRules(proxyPort) {
861
884
  const comment = "vm0:cidr-proxy";
862
- console.log(
885
+ logger2.log(
863
886
  `Setting up CIDR proxy rules for ${BRIDGE_CIDR} -> port ${proxyPort}`
864
887
  );
865
888
  try {
866
889
  await execCommand(
867
890
  `iptables -t nat -C PREROUTING -s ${BRIDGE_CIDR} -p tcp --dport 80 -j REDIRECT --to-port ${proxyPort} -m comment --comment "${comment}"`
868
891
  );
869
- console.log("CIDR proxy rule for port 80 already exists");
892
+ logger2.log("CIDR proxy rule for port 80 already exists");
870
893
  } catch {
871
894
  await execCommand(
872
895
  `iptables -t nat -A PREROUTING -s ${BRIDGE_CIDR} -p tcp --dport 80 -j REDIRECT --to-port ${proxyPort} -m comment --comment "${comment}"`
873
896
  );
874
- console.log("CIDR proxy rule for port 80 added");
897
+ logger2.log("CIDR proxy rule for port 80 added");
875
898
  }
876
899
  try {
877
900
  await execCommand(
878
901
  `iptables -t nat -C PREROUTING -s ${BRIDGE_CIDR} -p tcp --dport 443 -j REDIRECT --to-port ${proxyPort} -m comment --comment "${comment}"`
879
902
  );
880
- console.log("CIDR proxy rule for port 443 already exists");
903
+ logger2.log("CIDR proxy rule for port 443 already exists");
881
904
  } catch {
882
905
  await execCommand(
883
906
  `iptables -t nat -A PREROUTING -s ${BRIDGE_CIDR} -p tcp --dport 443 -j REDIRECT --to-port ${proxyPort} -m comment --comment "${comment}"`
884
907
  );
885
- console.log("CIDR proxy rule for port 443 added");
908
+ logger2.log("CIDR proxy rule for port 443 added");
886
909
  }
887
910
  }
888
911
  async function cleanupCIDRProxyRules(proxyPort) {
889
912
  const comment = "vm0:cidr-proxy";
890
- console.log("Cleaning up CIDR proxy rules...");
913
+ logger2.log("Cleaning up CIDR proxy rules...");
891
914
  try {
892
915
  await execCommand(
893
916
  `iptables -t nat -D PREROUTING -s ${BRIDGE_CIDR} -p tcp --dport 80 -j REDIRECT --to-port ${proxyPort} -m comment --comment "${comment}"`
894
917
  );
895
- console.log("CIDR proxy rule for port 80 removed");
918
+ logger2.log("CIDR proxy rule for port 80 removed");
896
919
  } catch {
897
920
  }
898
921
  try {
899
922
  await execCommand(
900
923
  `iptables -t nat -D PREROUTING -s ${BRIDGE_CIDR} -p tcp --dport 443 -j REDIRECT --to-port ${proxyPort} -m comment --comment "${comment}"`
901
924
  );
902
- console.log("CIDR proxy rule for port 443 removed");
925
+ logger2.log("CIDR proxy rule for port 443 removed");
903
926
  } catch {
904
927
  }
905
928
  }
@@ -978,17 +1001,17 @@ async function findOrphanedIptablesRules(rules, activeVmIps, expectedProxyPort)
978
1001
  return orphaned;
979
1002
  }
980
1003
  async function flushBridgeArpCache() {
981
- console.log(`Flushing ARP cache on bridge ${BRIDGE_NAME2}...`);
1004
+ logger2.log(`Flushing ARP cache on bridge ${BRIDGE_NAME2}...`);
982
1005
  try {
983
1006
  if (!await bridgeExists()) {
984
- console.log("Bridge does not exist, skipping ARP flush");
1007
+ logger2.log("Bridge does not exist, skipping ARP flush");
985
1008
  return;
986
1009
  }
987
1010
  const { stdout } = await execAsync2(
988
1011
  `ip neigh show dev ${BRIDGE_NAME2} 2>/dev/null || true`
989
1012
  );
990
1013
  if (!stdout.trim()) {
991
- console.log("No ARP entries on bridge");
1014
+ logger2.log("No ARP entries on bridge");
992
1015
  return;
993
1016
  }
994
1017
  const lines = stdout.split("\n").filter((line) => line.trim());
@@ -1004,38 +1027,38 @@ async function flushBridgeArpCache() {
1004
1027
  }
1005
1028
  }
1006
1029
  }
1007
- console.log(`Cleared ${cleared} ARP entries from bridge`);
1030
+ logger2.log(`Cleared ${cleared} ARP entries from bridge`);
1008
1031
  } catch (error) {
1009
- console.log(
1032
+ logger2.log(
1010
1033
  `Warning: Could not flush ARP cache: ${error instanceof Error ? error.message : "Unknown error"}`
1011
1034
  );
1012
1035
  }
1013
1036
  }
1014
1037
  async function cleanupOrphanedProxyRules(runnerName) {
1015
1038
  const comment = `vm0:runner:${runnerName}`;
1016
- console.log(`Cleaning up orphaned proxy rules for runner '${runnerName}'...`);
1039
+ logger2.log(`Cleaning up orphaned proxy rules for runner '${runnerName}'...`);
1017
1040
  try {
1018
1041
  const rules = await execCommand("iptables -t nat -S PREROUTING", false);
1019
1042
  const ourRules = rules.split("\n").filter((rule) => rule.includes(comment));
1020
1043
  if (ourRules.length === 0) {
1021
- console.log("No orphaned proxy rules found");
1044
+ logger2.log("No orphaned proxy rules found");
1022
1045
  return;
1023
1046
  }
1024
- console.log(`Found ${ourRules.length} orphaned rule(s) to clean up`);
1047
+ logger2.log(`Found ${ourRules.length} orphaned rule(s) to clean up`);
1025
1048
  for (const rule of ourRules) {
1026
1049
  const deleteRule = rule.replace("-A ", "-D ");
1027
1050
  try {
1028
1051
  await execCommand(`iptables -t nat ${deleteRule}`);
1029
- console.log(`Deleted orphaned rule: ${rule.substring(0, 80)}...`);
1052
+ logger2.log(`Deleted orphaned rule: ${rule.substring(0, 80)}...`);
1030
1053
  } catch {
1031
- console.log(
1054
+ logger2.log(
1032
1055
  `Failed to delete rule (may already be gone): ${rule.substring(0, 80)}...`
1033
1056
  );
1034
1057
  }
1035
1058
  }
1036
- console.log("Orphaned proxy rules cleanup complete");
1059
+ logger2.log("Orphaned proxy rules cleanup complete");
1037
1060
  } catch (error) {
1038
- console.log(
1061
+ logger2.log(
1039
1062
  `Warning: Could not clean up orphaned rules: ${error instanceof Error ? error.message : "Unknown error"}`
1040
1063
  );
1041
1064
  }
@@ -1043,6 +1066,7 @@ async function cleanupOrphanedProxyRules(runnerName) {
1043
1066
 
1044
1067
  // src/lib/firecracker/vm.ts
1045
1068
  var execAsync3 = promisify3(exec3);
1069
+ var logger3 = createLogger("VM");
1046
1070
  var FirecrackerVM = class {
1047
1071
  config;
1048
1072
  process = null;
@@ -1062,9 +1086,6 @@ var FirecrackerVM = class {
1062
1086
  this.vmOverlayPath = path2.join(this.workDir, "overlay.ext4");
1063
1087
  this.vsockPath = path2.join(this.workDir, "vsock.sock");
1064
1088
  }
1065
- log(msg) {
1066
- (this.config.logger ?? console.log)(msg);
1067
- }
1068
1089
  /**
1069
1090
  * Get current VM state
1070
1091
  */
@@ -1108,21 +1129,21 @@ var FirecrackerVM = class {
1108
1129
  if (fs3.existsSync(this.socketPath)) {
1109
1130
  fs3.unlinkSync(this.socketPath);
1110
1131
  }
1111
- this.log(`[VM ${this.config.vmId}] Setting up overlay and network...`);
1132
+ logger3.log(`[VM ${this.config.vmId}] Setting up overlay and network...`);
1112
1133
  const createOverlay = async () => {
1113
1134
  const overlaySize = 2 * 1024 * 1024 * 1024;
1114
1135
  const fd = fs3.openSync(this.vmOverlayPath, "w");
1115
1136
  fs3.ftruncateSync(fd, overlaySize);
1116
1137
  fs3.closeSync(fd);
1117
1138
  await execAsync3(`mkfs.ext4 -F -q "${this.vmOverlayPath}"`);
1118
- this.log(`[VM ${this.config.vmId}] Overlay created`);
1139
+ logger3.log(`[VM ${this.config.vmId}] Overlay created`);
1119
1140
  };
1120
1141
  const [, networkConfig] = await Promise.all([
1121
1142
  createOverlay(),
1122
- createTapDevice(this.config.vmId, this.log.bind(this))
1143
+ createTapDevice(this.config.vmId)
1123
1144
  ]);
1124
1145
  this.networkConfig = networkConfig;
1125
- this.log(`[VM ${this.config.vmId}] Starting Firecracker...`);
1146
+ logger3.log(`[VM ${this.config.vmId}] Starting Firecracker...`);
1126
1147
  this.process = spawn(
1127
1148
  this.config.firecrackerBinary,
1128
1149
  ["--api-sock", this.socketPath],
@@ -1133,11 +1154,11 @@ var FirecrackerVM = class {
1133
1154
  }
1134
1155
  );
1135
1156
  this.process.on("error", (err) => {
1136
- this.log(`[VM ${this.config.vmId}] Firecracker error: ${err}`);
1157
+ logger3.log(`[VM ${this.config.vmId}] Firecracker error: ${err}`);
1137
1158
  this.state = "error";
1138
1159
  });
1139
1160
  this.process.on("exit", (code, signal) => {
1140
- this.log(
1161
+ logger3.log(
1141
1162
  `[VM ${this.config.vmId}] Firecracker exited: code=${code}, signal=${signal}`
1142
1163
  );
1143
1164
  if (this.state !== "stopped") {
@@ -1150,7 +1171,7 @@ var FirecrackerVM = class {
1150
1171
  });
1151
1172
  stdoutRL.on("line", (line) => {
1152
1173
  if (line.trim()) {
1153
- this.log(`[VM ${this.config.vmId}] ${line}`);
1174
+ logger3.log(`[VM ${this.config.vmId}] ${line}`);
1154
1175
  }
1155
1176
  });
1156
1177
  }
@@ -1160,19 +1181,19 @@ var FirecrackerVM = class {
1160
1181
  });
1161
1182
  stderrRL.on("line", (line) => {
1162
1183
  if (line.trim()) {
1163
- this.log(`[VM ${this.config.vmId}] stderr: ${line}`);
1184
+ logger3.log(`[VM ${this.config.vmId}] stderr: ${line}`);
1164
1185
  }
1165
1186
  });
1166
1187
  }
1167
1188
  this.client = new FirecrackerClient(this.socketPath);
1168
- this.log(`[VM ${this.config.vmId}] Waiting for API...`);
1189
+ logger3.log(`[VM ${this.config.vmId}] Waiting for API...`);
1169
1190
  await this.client.waitUntilReady(1e4, 100);
1170
1191
  this.state = "configuring";
1171
1192
  await this.configure();
1172
- this.log(`[VM ${this.config.vmId}] Booting...`);
1193
+ logger3.log(`[VM ${this.config.vmId}] Booting...`);
1173
1194
  await this.client.start();
1174
1195
  this.state = "running";
1175
- this.log(
1196
+ logger3.log(
1176
1197
  `[VM ${this.config.vmId}] Running at ${this.networkConfig.guestIp}`
1177
1198
  );
1178
1199
  } catch (error) {
@@ -1188,7 +1209,7 @@ var FirecrackerVM = class {
1188
1209
  if (!this.client || !this.networkConfig) {
1189
1210
  throw new Error("VM not properly initialized");
1190
1211
  }
1191
- this.log(
1212
+ logger3.log(
1192
1213
  `[VM ${this.config.vmId}] Configuring: ${this.config.vcpus} vCPUs, ${this.config.memoryMb}MB RAM`
1193
1214
  );
1194
1215
  await this.client.setMachineConfig({
@@ -1197,26 +1218,28 @@ var FirecrackerVM = class {
1197
1218
  });
1198
1219
  const networkBootArgs = generateNetworkBootArgs(this.networkConfig);
1199
1220
  const bootArgs = `console=ttyS0 reboot=k panic=1 pci=off nomodules random.trust_cpu=on quiet loglevel=0 nokaslr audit=0 numa=off mitigations=off noresume init=/sbin/vm-init ${networkBootArgs}`;
1200
- this.log(`[VM ${this.config.vmId}] Boot args: ${bootArgs}`);
1221
+ logger3.log(`[VM ${this.config.vmId}] Boot args: ${bootArgs}`);
1201
1222
  await this.client.setBootSource({
1202
1223
  kernel_image_path: this.config.kernelPath,
1203
1224
  boot_args: bootArgs
1204
1225
  });
1205
- this.log(`[VM ${this.config.vmId}] Base rootfs: ${this.config.rootfsPath}`);
1226
+ logger3.log(
1227
+ `[VM ${this.config.vmId}] Base rootfs: ${this.config.rootfsPath}`
1228
+ );
1206
1229
  await this.client.setDrive({
1207
1230
  drive_id: "rootfs",
1208
1231
  path_on_host: this.config.rootfsPath,
1209
1232
  is_root_device: true,
1210
1233
  is_read_only: true
1211
1234
  });
1212
- this.log(`[VM ${this.config.vmId}] Overlay: ${this.vmOverlayPath}`);
1235
+ logger3.log(`[VM ${this.config.vmId}] Overlay: ${this.vmOverlayPath}`);
1213
1236
  await this.client.setDrive({
1214
1237
  drive_id: "overlay",
1215
1238
  path_on_host: this.vmOverlayPath,
1216
1239
  is_root_device: false,
1217
1240
  is_read_only: false
1218
1241
  });
1219
- this.log(
1242
+ logger3.log(
1220
1243
  `[VM ${this.config.vmId}] Network: ${this.networkConfig.tapDevice}`
1221
1244
  );
1222
1245
  await this.client.setNetworkInterface({
@@ -1224,7 +1247,7 @@ var FirecrackerVM = class {
1224
1247
  guest_mac: this.networkConfig.guestMac,
1225
1248
  host_dev_name: this.networkConfig.tapDevice
1226
1249
  });
1227
- this.log(`[VM ${this.config.vmId}] Vsock: ${this.vsockPath}`);
1250
+ logger3.log(`[VM ${this.config.vmId}] Vsock: ${this.vsockPath}`);
1228
1251
  await this.client.setVsock({
1229
1252
  vsock_id: "vsock0",
1230
1253
  guest_cid: 3,
@@ -1236,15 +1259,15 @@ var FirecrackerVM = class {
1236
1259
  */
1237
1260
  async stop() {
1238
1261
  if (this.state !== "running") {
1239
- this.log(`[VM ${this.config.vmId}] Not running, state: ${this.state}`);
1262
+ logger3.log(`[VM ${this.config.vmId}] Not running, state: ${this.state}`);
1240
1263
  return;
1241
1264
  }
1242
1265
  this.state = "stopping";
1243
- this.log(`[VM ${this.config.vmId}] Stopping...`);
1266
+ logger3.log(`[VM ${this.config.vmId}] Stopping...`);
1244
1267
  try {
1245
1268
  if (this.client) {
1246
1269
  await this.client.sendCtrlAltDel().catch((error) => {
1247
- this.log(
1270
+ logger3.log(
1248
1271
  `[VM ${this.config.vmId}] Graceful shutdown signal failed (VM may already be stopping): ${error instanceof Error ? error.message : error}`
1249
1272
  );
1250
1273
  });
@@ -1257,7 +1280,7 @@ var FirecrackerVM = class {
1257
1280
  * Force kill the VM
1258
1281
  */
1259
1282
  async kill() {
1260
- this.log(`[VM ${this.config.vmId}] Force killing...`);
1283
+ logger3.log(`[VM ${this.config.vmId}] Force killing...`);
1261
1284
  await this.cleanup();
1262
1285
  }
1263
1286
  /**
@@ -1282,7 +1305,7 @@ var FirecrackerVM = class {
1282
1305
  }
1283
1306
  this.client = null;
1284
1307
  this.state = "stopped";
1285
- this.log(`[VM ${this.config.vmId}] Stopped`);
1308
+ logger3.log(`[VM ${this.config.vmId}] Stopped`);
1286
1309
  }
1287
1310
  /**
1288
1311
  * Wait for the VM process to exit
@@ -1323,6 +1346,9 @@ var MSG_PING = 1;
1323
1346
  var MSG_PONG = 2;
1324
1347
  var MSG_EXEC = 3;
1325
1348
  var MSG_WRITE_FILE = 5;
1349
+ var MSG_SPAWN_WATCH = 7;
1350
+ var MSG_SPAWN_WATCH_RESULT = 8;
1351
+ var MSG_PROCESS_EXIT = 9;
1326
1352
  var MSG_ERROR = 255;
1327
1353
  var FLAG_SUDO = 1;
1328
1354
  function encode(type, seq, payload = Buffer.alloc(0)) {
@@ -1361,33 +1387,90 @@ function encodeWriteFilePayload(path6, content, sudo) {
1361
1387
  return payload;
1362
1388
  }
1363
1389
  function decodeExecResult(payload) {
1364
- if (payload.length < 8) {
1365
- return { exitCode: 1, stdout: "", stderr: "Invalid exec_result payload" };
1390
+ if (payload.length < 12) {
1391
+ return {
1392
+ exitCode: 1,
1393
+ stdout: "",
1394
+ stderr: "Invalid exec_result payload: too short"
1395
+ };
1366
1396
  }
1367
1397
  const exitCode = payload.readInt32BE(0);
1368
1398
  const stdoutLen = payload.readUInt32BE(4);
1369
- const stdout = payload.subarray(8, 8 + stdoutLen).toString("utf-8");
1370
1399
  const stderrLenOffset = 8 + stdoutLen;
1400
+ if (payload.length < stderrLenOffset + 4) {
1401
+ return {
1402
+ exitCode: 1,
1403
+ stdout: "",
1404
+ stderr: "Invalid exec_result payload: stdout truncated"
1405
+ };
1406
+ }
1407
+ const stdout = payload.subarray(8, 8 + stdoutLen).toString("utf-8");
1371
1408
  const stderrLen = payload.readUInt32BE(stderrLenOffset);
1409
+ const expectedLen = stderrLenOffset + 4 + stderrLen;
1410
+ if (payload.length < expectedLen) {
1411
+ return {
1412
+ exitCode: 1,
1413
+ stdout,
1414
+ stderr: "Invalid exec_result payload: stderr truncated"
1415
+ };
1416
+ }
1372
1417
  const stderr = payload.subarray(stderrLenOffset + 4, stderrLenOffset + 4 + stderrLen).toString("utf-8");
1373
1418
  return { exitCode, stdout, stderr };
1374
1419
  }
1375
1420
  function decodeWriteFileResult(payload) {
1376
1421
  if (payload.length < 3) {
1377
- return { success: false, error: "Invalid write_file_result payload" };
1422
+ return {
1423
+ success: false,
1424
+ error: "Invalid write_file_result payload: too short"
1425
+ };
1378
1426
  }
1379
1427
  const success = payload.readUInt8(0) === 1;
1380
1428
  const errorLen = payload.readUInt16BE(1);
1429
+ if (payload.length < 3 + errorLen) {
1430
+ return {
1431
+ success: false,
1432
+ error: "Invalid write_file_result payload: error truncated"
1433
+ };
1434
+ }
1381
1435
  const error = payload.subarray(3, 3 + errorLen).toString("utf-8");
1382
1436
  return { success, error };
1383
1437
  }
1384
1438
  function decodeError(payload) {
1385
1439
  if (payload.length < 2) {
1386
- return "Invalid error payload";
1440
+ return "Invalid error payload: too short";
1387
1441
  }
1388
1442
  const errorLen = payload.readUInt16BE(0);
1443
+ if (payload.length < 2 + errorLen) {
1444
+ return "Invalid error payload: message truncated";
1445
+ }
1389
1446
  return payload.subarray(2, 2 + errorLen).toString("utf-8");
1390
1447
  }
1448
+ function decodeSpawnWatchResult(payload) {
1449
+ if (payload.length < 4) {
1450
+ throw new Error("Invalid spawn_watch_result payload");
1451
+ }
1452
+ return { pid: payload.readUInt32BE(0) };
1453
+ }
1454
+ function decodeProcessExit(payload) {
1455
+ if (payload.length < 16) {
1456
+ throw new Error("Invalid process_exit payload: too short");
1457
+ }
1458
+ const pid = payload.readUInt32BE(0);
1459
+ const exitCode = payload.readInt32BE(4);
1460
+ const stdoutLen = payload.readUInt32BE(8);
1461
+ const stderrLenOffset = 12 + stdoutLen;
1462
+ if (payload.length < stderrLenOffset + 4) {
1463
+ throw new Error("Invalid process_exit payload: stdout truncated");
1464
+ }
1465
+ const stdout = payload.subarray(12, 12 + stdoutLen).toString("utf-8");
1466
+ const stderrLen = payload.readUInt32BE(stderrLenOffset);
1467
+ const expectedLen = stderrLenOffset + 4 + stderrLen;
1468
+ if (payload.length < expectedLen) {
1469
+ throw new Error("Invalid process_exit payload: stderr truncated");
1470
+ }
1471
+ const stderr = payload.subarray(stderrLenOffset + 4, stderrLenOffset + 4 + stderrLen).toString("utf-8");
1472
+ return { pid, exitCode, stdout, stderr };
1473
+ }
1391
1474
  var Decoder = class {
1392
1475
  buf = Buffer.alloc(0);
1393
1476
  decode(data) {
@@ -1419,6 +1502,9 @@ var VsockClient = class {
1419
1502
  connected = false;
1420
1503
  nextSeq = 1;
1421
1504
  pendingRequests = /* @__PURE__ */ new Map();
1505
+ pendingExits = /* @__PURE__ */ new Map();
1506
+ // Cache for exit events that arrive before waitForExit is called
1507
+ cachedExits = /* @__PURE__ */ new Map();
1422
1508
  constructor(vsockPath) {
1423
1509
  this.vsockPath = vsockPath;
1424
1510
  }
@@ -1435,6 +1521,21 @@ var VsockClient = class {
1435
1521
  * Handle incoming message and route to pending request
1436
1522
  */
1437
1523
  handleMessage(msg) {
1524
+ if (msg.type === MSG_PROCESS_EXIT && msg.seq === 0) {
1525
+ const event = decodeProcessExit(msg.payload);
1526
+ const pending2 = this.pendingExits.get(event.pid);
1527
+ if (pending2) {
1528
+ if (pending2.timeout) clearTimeout(pending2.timeout);
1529
+ this.pendingExits.delete(event.pid);
1530
+ pending2.resolve(event);
1531
+ } else if (!this.cachedExits.has(event.pid)) {
1532
+ this.cachedExits.set(event.pid, {
1533
+ event,
1534
+ timestamp: Date.now()
1535
+ });
1536
+ }
1537
+ return;
1538
+ }
1438
1539
  const pending = this.pendingRequests.get(msg.seq);
1439
1540
  if (pending) {
1440
1541
  clearTimeout(pending.timeout);
@@ -1638,7 +1739,11 @@ var VsockClient = class {
1638
1739
  this.connected = true;
1639
1740
  resolve();
1640
1741
  } else if (state === 2 /* Connected */) {
1641
- this.handleMessage(msg);
1742
+ try {
1743
+ this.handleMessage(msg);
1744
+ } catch (msgErr) {
1745
+ console.error(`[vsock] Error handling message: ${msgErr}`);
1746
+ }
1642
1747
  }
1643
1748
  }
1644
1749
  } catch (e) {
@@ -1654,12 +1759,19 @@ var VsockClient = class {
1654
1759
  }
1655
1760
  this.connected = false;
1656
1761
  this.socket = null;
1657
- const pending = Array.from(this.pendingRequests.values());
1762
+ const pendingReqs = Array.from(this.pendingRequests.values());
1658
1763
  this.pendingRequests.clear();
1659
- for (const req of pending) {
1764
+ for (const req of pendingReqs) {
1660
1765
  clearTimeout(req.timeout);
1661
1766
  req.reject(new Error("Connection closed"));
1662
1767
  }
1768
+ const pendingExits = Array.from(this.pendingExits.values());
1769
+ this.pendingExits.clear();
1770
+ for (const exit of pendingExits) {
1771
+ if (exit.timeout) clearTimeout(exit.timeout);
1772
+ exit.reject(new Error("Connection closed"));
1773
+ }
1774
+ this.cachedExits.clear();
1663
1775
  });
1664
1776
  });
1665
1777
  server.listen(listenerPath, () => {
@@ -1679,6 +1791,59 @@ var VsockClient = class {
1679
1791
  const result = await this.exec(`test -e '${remotePath}'`);
1680
1792
  return result.exitCode === 0;
1681
1793
  }
1794
+ /**
1795
+ * Spawn a process and monitor for exit (event-driven mode)
1796
+ *
1797
+ * Returns immediately with the PID. Use waitForExit() to wait for completion.
1798
+ * When the process exits, the agent sends an unsolicited notification.
1799
+ */
1800
+ async spawnAndWatch(command, timeoutMs = 0) {
1801
+ const payload = encodeExecPayload(command, timeoutMs);
1802
+ const response = await this.request(
1803
+ MSG_SPAWN_WATCH,
1804
+ payload,
1805
+ 3e4
1806
+ // 30s timeout for spawn acknowledgment
1807
+ );
1808
+ if (response.type === MSG_ERROR) {
1809
+ throw new Error(`spawnAndWatch failed: ${decodeError(response.payload)}`);
1810
+ }
1811
+ if (response.type !== MSG_SPAWN_WATCH_RESULT) {
1812
+ throw new Error(
1813
+ `Unexpected response type: 0x${response.type.toString(16)}`
1814
+ );
1815
+ }
1816
+ return decodeSpawnWatchResult(response.payload);
1817
+ }
1818
+ /**
1819
+ * Wait for a spawned process to exit
1820
+ *
1821
+ * Blocks until the process exits or timeout is reached.
1822
+ * The exit event is pushed by the guest agent (no polling).
1823
+ */
1824
+ async waitForExit(pid, timeoutMs = 0) {
1825
+ if (!this.connected || !this.socket) {
1826
+ throw new Error("Not connected - cannot wait for process exit");
1827
+ }
1828
+ if (this.pendingExits.has(pid)) {
1829
+ throw new Error(`Already waiting for process ${pid} to exit`);
1830
+ }
1831
+ const cached = this.cachedExits.get(pid);
1832
+ if (cached) {
1833
+ this.cachedExits.delete(pid);
1834
+ return cached.event;
1835
+ }
1836
+ return new Promise((resolve, reject) => {
1837
+ const pending = { resolve, reject };
1838
+ if (timeoutMs > 0) {
1839
+ pending.timeout = setTimeout(() => {
1840
+ this.pendingExits.delete(pid);
1841
+ reject(new Error(`Timeout waiting for process ${pid} to exit`));
1842
+ }, timeoutMs);
1843
+ }
1844
+ this.pendingExits.set(pid, pending);
1845
+ });
1846
+ }
1682
1847
  /**
1683
1848
  * Get the vsock path (for logging/debugging)
1684
1849
  */
@@ -1694,12 +1859,19 @@ var VsockClient = class {
1694
1859
  this.socket = null;
1695
1860
  }
1696
1861
  this.connected = false;
1697
- const pending = Array.from(this.pendingRequests.values());
1862
+ const pendingRequests = Array.from(this.pendingRequests.values());
1698
1863
  this.pendingRequests.clear();
1699
- for (const req of pending) {
1864
+ for (const req of pendingRequests) {
1700
1865
  clearTimeout(req.timeout);
1701
1866
  req.reject(new Error("Connection closed"));
1702
1867
  }
1868
+ const pendingExits = Array.from(this.pendingExits.values());
1869
+ this.pendingExits.clear();
1870
+ for (const exit of pendingExits) {
1871
+ if (exit.timeout) clearTimeout(exit.timeout);
1872
+ exit.reject(new Error("Connection closed"));
1873
+ }
1874
+ this.cachedExits.clear();
1703
1875
  }
1704
1876
  };
1705
1877
 
@@ -7112,7 +7284,7 @@ var credentialsByNameContract = c10.router({
7112
7284
  name: credentialNameSchema
7113
7285
  }),
7114
7286
  responses: {
7115
- 204: z16.undefined(),
7287
+ 204: c10.noBody(),
7116
7288
  401: apiErrorSchema,
7117
7289
  404: apiErrorSchema,
7118
7290
  500: apiErrorSchema
@@ -7208,7 +7380,7 @@ var modelProvidersByTypeContract = c11.router({
7208
7380
  type: modelProviderTypeSchema
7209
7381
  }),
7210
7382
  responses: {
7211
- 204: z17.undefined(),
7383
+ 204: c11.noBody(),
7212
7384
  401: apiErrorSchema,
7213
7385
  404: apiErrorSchema,
7214
7386
  500: apiErrorSchema
@@ -7493,7 +7665,7 @@ var schedulesByNameContract = c13.router({
7493
7665
  composeId: z19.string().uuid("Compose ID required")
7494
7666
  }),
7495
7667
  responses: {
7496
- 204: z19.undefined(),
7668
+ 204: c13.noBody(),
7497
7669
  401: apiErrorSchema,
7498
7670
  404: apiErrorSchema
7499
7671
  },
@@ -8338,6 +8510,7 @@ var ENV_LOADER_PATH = "/usr/local/bin/vm0-agent/env-loader.mjs";
8338
8510
 
8339
8511
  // src/lib/proxy/vm-registry.ts
8340
8512
  import fs5 from "fs";
8513
+ var logger4 = createLogger("VMRegistry");
8341
8514
  var DEFAULT_REGISTRY_PATH = "/tmp/vm0-vm-registry.json";
8342
8515
  var VMRegistry = class {
8343
8516
  registryPath;
@@ -8384,8 +8557,8 @@ var VMRegistry = class {
8384
8557
  this.save();
8385
8558
  const firewallInfo = options?.firewallRules ? ` with ${options.firewallRules.length} firewall rules` : "";
8386
8559
  const mitmInfo = options?.mitmEnabled ? ", MITM enabled" : "";
8387
- console.log(
8388
- `[VMRegistry] Registered VM ${vmIp} for run ${runId}${firewallInfo}${mitmInfo}`
8560
+ logger4.log(
8561
+ `Registered VM ${vmIp} for run ${runId}${firewallInfo}${mitmInfo}`
8389
8562
  );
8390
8563
  }
8391
8564
  /**
@@ -8396,9 +8569,7 @@ var VMRegistry = class {
8396
8569
  const registration = this.data.vms[vmIp];
8397
8570
  delete this.data.vms[vmIp];
8398
8571
  this.save();
8399
- console.log(
8400
- `[VMRegistry] Unregistered VM ${vmIp} (run ${registration.runId})`
8401
- );
8572
+ logger4.log(`Unregistered VM ${vmIp} (run ${registration.runId})`);
8402
8573
  }
8403
8574
  }
8404
8575
  /**
@@ -8419,7 +8590,7 @@ var VMRegistry = class {
8419
8590
  clear() {
8420
8591
  this.data.vms = {};
8421
8592
  this.save();
8422
- console.log("[VMRegistry] Cleared all registrations");
8593
+ logger4.log("Cleared all registrations");
8423
8594
  }
8424
8595
  /**
8425
8596
  * Get the path to the registry file
@@ -8929,6 +9100,7 @@ addons = [tls_clienthello, request, response]
8929
9100
  `;
8930
9101
 
8931
9102
  // src/lib/proxy/proxy-manager.ts
9103
+ var logger5 = createLogger("ProxyManager");
8932
9104
  var DEFAULT_PROXY_OPTIONS = {
8933
9105
  port: 8080,
8934
9106
  registryPath: DEFAULT_REGISTRY_PATH,
@@ -8973,9 +9145,7 @@ var ProxyManager = class {
8973
9145
  fs6.writeFileSync(this.config.addonPath, RUNNER_MITM_ADDON_SCRIPT, {
8974
9146
  mode: 493
8975
9147
  });
8976
- console.log(
8977
- `[ProxyManager] Addon script written to ${this.config.addonPath}`
8978
- );
9148
+ logger5.log(`Addon script written to ${this.config.addonPath}`);
8979
9149
  }
8980
9150
  /**
8981
9151
  * Validate proxy configuration
@@ -8995,7 +9165,7 @@ var ProxyManager = class {
8995
9165
  */
8996
9166
  async start() {
8997
9167
  if (this.isRunning) {
8998
- console.log("[ProxyManager] Proxy already running");
9168
+ logger5.log("Proxy already running");
8999
9169
  return;
9000
9170
  }
9001
9171
  const mitmproxyInstalled = await this.checkMitmproxyInstalled();
@@ -9006,11 +9176,11 @@ var ProxyManager = class {
9006
9176
  }
9007
9177
  this.validateConfig();
9008
9178
  getVMRegistry();
9009
- console.log("[ProxyManager] Starting mitmproxy...");
9010
- console.log(` Port: ${this.config.port}`);
9011
- console.log(` CA Dir: ${this.config.caDir}`);
9012
- console.log(` Addon: ${this.config.addonPath}`);
9013
- console.log(` Registry: ${this.config.registryPath}`);
9179
+ logger5.log("Starting mitmproxy...");
9180
+ logger5.log(` Port: ${this.config.port}`);
9181
+ logger5.log(` CA Dir: ${this.config.caDir}`);
9182
+ logger5.log(` Addon: ${this.config.addonPath}`);
9183
+ logger5.log(` Registry: ${this.config.registryPath}`);
9014
9184
  const args = [
9015
9185
  "--mode",
9016
9186
  "transparent",
@@ -9032,25 +9202,26 @@ var ProxyManager = class {
9032
9202
  stdio: ["ignore", "pipe", "pipe"],
9033
9203
  detached: false
9034
9204
  });
9205
+ const mitmLogger = createLogger("mitmproxy");
9035
9206
  this.process.stdout?.on("data", (data) => {
9036
- console.log(`[mitmproxy] ${data.toString().trim()}`);
9207
+ mitmLogger.log(data.toString().trim());
9037
9208
  });
9038
9209
  this.process.stderr?.on("data", (data) => {
9039
- console.error(`[mitmproxy] ${data.toString().trim()}`);
9210
+ mitmLogger.log(data.toString().trim());
9040
9211
  });
9041
9212
  this.process.on("close", (code) => {
9042
- console.log(`[ProxyManager] mitmproxy exited with code ${code}`);
9213
+ logger5.log(`mitmproxy exited with code ${code}`);
9043
9214
  this.isRunning = false;
9044
9215
  this.process = null;
9045
9216
  });
9046
9217
  this.process.on("error", (err) => {
9047
- console.error(`[ProxyManager] mitmproxy error: ${err.message}`);
9218
+ logger5.error(`mitmproxy error: ${err.message}`);
9048
9219
  this.isRunning = false;
9049
9220
  this.process = null;
9050
9221
  });
9051
9222
  await this.waitForReady();
9052
9223
  this.isRunning = true;
9053
- console.log("[ProxyManager] mitmproxy started successfully");
9224
+ logger5.log("mitmproxy started successfully");
9054
9225
  }
9055
9226
  /**
9056
9227
  * Wait for proxy to be ready
@@ -9076,24 +9247,24 @@ var ProxyManager = class {
9076
9247
  */
9077
9248
  async stop() {
9078
9249
  if (!this.process || !this.isRunning) {
9079
- console.log("[ProxyManager] Proxy not running");
9250
+ logger5.log("Proxy not running");
9080
9251
  return;
9081
9252
  }
9082
- console.log("[ProxyManager] Stopping mitmproxy...");
9253
+ logger5.log("Stopping mitmproxy...");
9083
9254
  return new Promise((resolve) => {
9084
9255
  if (!this.process) {
9085
9256
  resolve();
9086
9257
  return;
9087
9258
  }
9088
9259
  const timeout = setTimeout(() => {
9089
- console.log("[ProxyManager] Force killing mitmproxy...");
9260
+ logger5.log("Force killing mitmproxy...");
9090
9261
  this.process?.kill("SIGKILL");
9091
9262
  }, 5e3);
9092
9263
  this.process.on("close", () => {
9093
9264
  clearTimeout(timeout);
9094
9265
  this.isRunning = false;
9095
9266
  this.process = null;
9096
- console.log("[ProxyManager] mitmproxy stopped");
9267
+ logger5.log("mitmproxy stopped");
9097
9268
  resolve();
9098
9269
  });
9099
9270
  this.process.kill("SIGTERM");
@@ -9237,6 +9408,45 @@ async function withSandboxTiming(actionType, fn) {
9237
9408
  }
9238
9409
  }
9239
9410
 
9411
+ // src/lib/vm-setup/vm-setup.ts
9412
+ var logger6 = createLogger("VMSetup");
9413
+ var VM_PROXY_CA_PATH = "/usr/local/share/ca-certificates/vm0-proxy-ca.crt";
9414
+ async function downloadStorages(guest, manifest) {
9415
+ const totalArchives = manifest.storages.filter((s) => s.archiveUrl).length + (manifest.artifact?.archiveUrl ? 1 : 0);
9416
+ if (totalArchives === 0) {
9417
+ logger6.log(`No archives to download`);
9418
+ return;
9419
+ }
9420
+ logger6.log(`Downloading ${totalArchives} archive(s)...`);
9421
+ const manifestJson = JSON.stringify(manifest);
9422
+ await guest.writeFile("/tmp/storage-manifest.json", manifestJson);
9423
+ const result = await guest.exec(
9424
+ `node ${SCRIPT_PATHS.download} /tmp/storage-manifest.json`
9425
+ );
9426
+ if (result.exitCode !== 0) {
9427
+ throw new Error(`Storage download failed: ${result.stderr}`);
9428
+ }
9429
+ logger6.log(`Storage download completed`);
9430
+ }
9431
+ async function restoreSessionHistory(guest, resumeSession, workingDir, cliAgentType) {
9432
+ const { sessionId, sessionHistory } = resumeSession;
9433
+ let sessionPath;
9434
+ if (cliAgentType === "codex") {
9435
+ logger6.log(`Codex resume session will be handled by checkpoint.py`);
9436
+ return;
9437
+ } else {
9438
+ const projectName = workingDir.replace(/^\//, "").replace(/\//g, "-");
9439
+ sessionPath = `/home/user/.claude/projects/-${projectName}/${sessionId}.jsonl`;
9440
+ }
9441
+ logger6.log(`Restoring session history to ${sessionPath}`);
9442
+ const dirPath = sessionPath.substring(0, sessionPath.lastIndexOf("/"));
9443
+ await guest.execOrThrow(`mkdir -p "${dirPath}"`);
9444
+ await guest.writeFile(sessionPath, sessionHistory);
9445
+ logger6.log(
9446
+ `Session history restored (${sessionHistory.split("\n").length} lines)`
9447
+ );
9448
+ }
9449
+
9240
9450
  // src/lib/executor-env.ts
9241
9451
  var ENV_JSON_PATH = "/tmp/vm0-env.json";
9242
9452
  function buildEnvironmentVariables(context, apiUrl) {
@@ -9279,13 +9489,14 @@ function buildEnvironmentVariables(context, apiUrl) {
9279
9489
  }
9280
9490
  }
9281
9491
  if (context.experimentalFirewall?.experimental_mitm) {
9282
- envVars.NODE_EXTRA_CA_CERTS = "/usr/local/share/ca-certificates/vm0-proxy-ca.crt";
9492
+ envVars.NODE_EXTRA_CA_CERTS = VM_PROXY_CA_PATH;
9283
9493
  }
9284
9494
  return envVars;
9285
9495
  }
9286
9496
 
9287
9497
  // src/lib/network-logs/network-logs.ts
9288
9498
  import fs7 from "fs";
9499
+ var logger7 = createLogger("NetworkLogs");
9289
9500
  function getNetworkLogPath(runId) {
9290
9501
  return `/tmp/vm0-network-${runId}.jsonl`;
9291
9502
  }
@@ -9299,8 +9510,8 @@ function readNetworkLogs(runId) {
9299
9510
  const lines = content.split("\n").filter((line) => line.trim());
9300
9511
  return lines.map((line) => JSON.parse(line));
9301
9512
  } catch (err) {
9302
- console.error(
9303
- `[Executor] Failed to read network logs: ${err instanceof Error ? err.message : "Unknown error"}`
9513
+ logger7.error(
9514
+ `Failed to read network logs: ${err instanceof Error ? err.message : "Unknown error"}`
9304
9515
  );
9305
9516
  return [];
9306
9517
  }
@@ -9312,19 +9523,19 @@ function cleanupNetworkLogs(runId) {
9312
9523
  fs7.unlinkSync(logPath);
9313
9524
  }
9314
9525
  } catch (err) {
9315
- console.error(
9316
- `[Executor] Failed to cleanup network logs: ${err instanceof Error ? err.message : "Unknown error"}`
9526
+ logger7.error(
9527
+ `Failed to cleanup network logs: ${err instanceof Error ? err.message : "Unknown error"}`
9317
9528
  );
9318
9529
  }
9319
9530
  }
9320
9531
  async function uploadNetworkLogs(apiUrl, sandboxToken, runId) {
9321
9532
  const networkLogs = readNetworkLogs(runId);
9322
9533
  if (networkLogs.length === 0) {
9323
- console.log(`[Executor] No network logs to upload for ${runId}`);
9534
+ logger7.log(`No network logs to upload for ${runId}`);
9324
9535
  return;
9325
9536
  }
9326
- console.log(
9327
- `[Executor] Uploading ${networkLogs.length} network log entries for ${runId}`
9537
+ logger7.log(
9538
+ `Uploading ${networkLogs.length} network log entries for ${runId}`
9328
9539
  );
9329
9540
  const headers = {
9330
9541
  Authorization: `Bearer ${sandboxToken}`,
@@ -9344,74 +9555,15 @@ async function uploadNetworkLogs(apiUrl, sandboxToken, runId) {
9344
9555
  });
9345
9556
  if (!response.ok) {
9346
9557
  const errorText = await response.text();
9347
- console.error(`[Executor] Failed to upload network logs: ${errorText}`);
9558
+ logger7.error(`Failed to upload network logs: ${errorText}`);
9348
9559
  return;
9349
9560
  }
9350
- console.log(`[Executor] Network logs uploaded successfully for ${runId}`);
9561
+ logger7.log(`Network logs uploaded successfully for ${runId}`);
9351
9562
  cleanupNetworkLogs(runId);
9352
9563
  }
9353
9564
 
9354
- // src/lib/vm-setup/vm-setup.ts
9355
- import fs8 from "fs";
9356
- async function downloadStorages(guest, manifest) {
9357
- const totalArchives = manifest.storages.filter((s) => s.archiveUrl).length + (manifest.artifact?.archiveUrl ? 1 : 0);
9358
- if (totalArchives === 0) {
9359
- console.log(`[Executor] No archives to download`);
9360
- return;
9361
- }
9362
- console.log(`[Executor] Downloading ${totalArchives} archive(s)...`);
9363
- const manifestJson = JSON.stringify(manifest);
9364
- await guest.writeFile("/tmp/storage-manifest.json", manifestJson);
9365
- const result = await guest.exec(
9366
- `node ${SCRIPT_PATHS.download} /tmp/storage-manifest.json`
9367
- );
9368
- if (result.exitCode !== 0) {
9369
- throw new Error(`Storage download failed: ${result.stderr}`);
9370
- }
9371
- console.log(`[Executor] Storage download completed`);
9372
- }
9373
- async function restoreSessionHistory(guest, resumeSession, workingDir, cliAgentType) {
9374
- const { sessionId, sessionHistory } = resumeSession;
9375
- let sessionPath;
9376
- if (cliAgentType === "codex") {
9377
- console.log(
9378
- `[Executor] Codex resume session will be handled by checkpoint.py`
9379
- );
9380
- return;
9381
- } else {
9382
- const projectName = workingDir.replace(/^\//, "").replace(/\//g, "-");
9383
- sessionPath = `/home/user/.claude/projects/-${projectName}/${sessionId}.jsonl`;
9384
- }
9385
- console.log(`[Executor] Restoring session history to ${sessionPath}`);
9386
- const dirPath = sessionPath.substring(0, sessionPath.lastIndexOf("/"));
9387
- await guest.execOrThrow(`mkdir -p "${dirPath}"`);
9388
- await guest.writeFile(sessionPath, sessionHistory);
9389
- console.log(
9390
- `[Executor] Session history restored (${sessionHistory.split("\n").length} lines)`
9391
- );
9392
- }
9393
- async function installProxyCA(guest, caCertPath) {
9394
- if (!fs8.existsSync(caCertPath)) {
9395
- throw new Error(
9396
- `Proxy CA certificate not found at ${caCertPath}. Run generate-proxy-ca.sh first.`
9397
- );
9398
- }
9399
- const caCert = fs8.readFileSync(caCertPath, "utf-8");
9400
- const certWithNewline = caCert.endsWith("\n") ? caCert : caCert + "\n";
9401
- console.log(
9402
- `[Executor] Installing proxy CA certificate (${certWithNewline.length} bytes)`
9403
- );
9404
- await guest.writeFileWithSudo(
9405
- "/usr/local/share/ca-certificates/vm0-proxy-ca.crt",
9406
- certWithNewline
9407
- );
9408
- await guest.execOrThrow(
9409
- "cat /usr/local/share/ca-certificates/vm0-proxy-ca.crt | sudo tee -a /etc/ssl/certs/ca-certificates.crt > /dev/null"
9410
- );
9411
- console.log("[Executor] Proxy CA certificate installed successfully");
9412
- }
9413
-
9414
9565
  // src/lib/executor.ts
9566
+ var logger8 = createLogger("Executor");
9415
9567
  function getVmIdFromRunId(runId) {
9416
9568
  return runId.split("-")[0] || runId.substring(0, 8);
9417
9569
  }
@@ -9457,12 +9609,12 @@ async function reportPreflightFailure(apiUrl, runId, sandboxToken, error, bypass
9457
9609
  })
9458
9610
  });
9459
9611
  if (!response.ok) {
9460
- console.error(
9461
- `[Executor] Failed to report preflight failure: HTTP ${response.status}`
9612
+ logger8.error(
9613
+ `Failed to report preflight failure: HTTP ${response.status}`
9462
9614
  );
9463
9615
  }
9464
9616
  } catch (err) {
9465
- console.error(`[Executor] Failed to report preflight failure: ${err}`);
9617
+ logger8.error(`Failed to report preflight failure: ${err}`);
9466
9618
  }
9467
9619
  }
9468
9620
  async function executeJob(context, config, options = {}) {
@@ -9481,8 +9633,7 @@ async function executeJob(context, config, options = {}) {
9481
9633
  const vmId = getVmIdFromRunId(context.runId);
9482
9634
  let vm = null;
9483
9635
  let guestIp = null;
9484
- const log = options.logger ?? ((msg) => console.log(msg));
9485
- log(`[Executor] Starting job ${context.runId} in VM ${vmId}`);
9636
+ logger8.log(`Starting job ${context.runId} in VM ${vmId}`);
9486
9637
  try {
9487
9638
  const workspacesDir = path4.join(process.cwd(), "workspaces");
9488
9639
  const vmConfig = {
@@ -9492,32 +9643,31 @@ async function executeJob(context, config, options = {}) {
9492
9643
  kernelPath: config.firecracker.kernel,
9493
9644
  rootfsPath: config.firecracker.rootfs,
9494
9645
  firecrackerBinary: config.firecracker.binary,
9495
- workDir: path4.join(workspacesDir, `vm0-${vmId}`),
9496
- logger: log
9646
+ workDir: path4.join(workspacesDir, `vm0-${vmId}`)
9497
9647
  };
9498
- log(`[Executor] Creating VM ${vmId}...`);
9648
+ logger8.log(`Creating VM ${vmId}...`);
9499
9649
  vm = new FirecrackerVM(vmConfig);
9500
9650
  await withSandboxTiming("vm_create", () => vm.start());
9501
9651
  guestIp = vm.getGuestIp();
9502
9652
  if (!guestIp) {
9503
9653
  throw new Error("VM started but no IP address available");
9504
9654
  }
9505
- log(`[Executor] VM ${vmId} started, guest IP: ${guestIp}`);
9655
+ logger8.log(`VM ${vmId} started, guest IP: ${guestIp}`);
9506
9656
  const vsockPath = vm.getVsockPath();
9507
9657
  const guest = new VsockClient(vsockPath);
9508
- log(`[Executor] Using vsock for guest communication: ${vsockPath}`);
9509
- log(`[Executor] Waiting for guest connection...`);
9658
+ logger8.log(`Using vsock for guest communication: ${vsockPath}`);
9659
+ logger8.log(`Waiting for guest connection...`);
9510
9660
  await withSandboxTiming(
9511
9661
  "guest_wait",
9512
9662
  () => guest.waitForGuestConnection(3e4)
9513
9663
  );
9514
- log(`[Executor] Guest client ready`);
9664
+ logger8.log(`Guest client ready`);
9515
9665
  const firewallConfig = context.experimentalFirewall;
9516
9666
  if (firewallConfig?.enabled) {
9517
9667
  const mitmEnabled = firewallConfig.experimental_mitm ?? false;
9518
9668
  const sealSecretsEnabled = firewallConfig.experimental_seal_secrets ?? false;
9519
- log(
9520
- `[Executor] Setting up network security for VM ${guestIp} (mitm=${mitmEnabled}, sealSecrets=${sealSecretsEnabled})`
9669
+ logger8.log(
9670
+ `Setting up network security for VM ${guestIp} (mitm=${mitmEnabled}, sealSecrets=${sealSecretsEnabled})`
9521
9671
  );
9522
9672
  await withSandboxTiming("network_setup", async () => {
9523
9673
  getVMRegistry().register(
@@ -9530,13 +9680,6 @@ async function executeJob(context, config, options = {}) {
9530
9680
  sealSecretsEnabled
9531
9681
  }
9532
9682
  );
9533
- if (mitmEnabled) {
9534
- const caCertPath = path4.join(
9535
- config.proxy.ca_dir,
9536
- "mitmproxy-ca-cert.pem"
9537
- );
9538
- await installProxyCA(guest, caCertPath);
9539
- }
9540
9683
  });
9541
9684
  }
9542
9685
  if (context.storageManifest) {
@@ -9558,12 +9701,12 @@ async function executeJob(context, config, options = {}) {
9558
9701
  }
9559
9702
  const envVars = buildEnvironmentVariables(context, config.server.url);
9560
9703
  const envJson = JSON.stringify(envVars);
9561
- log(
9562
- `[Executor] Writing env JSON (${envJson.length} bytes) to ${ENV_JSON_PATH}`
9704
+ logger8.log(
9705
+ `Writing env JSON (${envJson.length} bytes) to ${ENV_JSON_PATH}`
9563
9706
  );
9564
9707
  await guest.writeFile(ENV_JSON_PATH, envJson);
9565
9708
  if (!options.benchmarkMode) {
9566
- log(`[Executor] Running preflight connectivity check...`);
9709
+ logger8.log(`Running preflight connectivity check...`);
9567
9710
  const bypassSecret = process.env.VERCEL_AUTOMATION_BYPASS_SECRET;
9568
9711
  const preflight = await withSandboxTiming(
9569
9712
  "preflight_check",
@@ -9576,7 +9719,7 @@ async function executeJob(context, config, options = {}) {
9576
9719
  )
9577
9720
  );
9578
9721
  if (!preflight.success) {
9579
- log(`[Executor] Preflight check failed: ${preflight.error}`);
9722
+ logger8.log(`Preflight check failed: ${preflight.error}`);
9580
9723
  await reportPreflightFailure(
9581
9724
  config.server.url,
9582
9725
  context.runId,
@@ -9589,116 +9732,84 @@ async function executeJob(context, config, options = {}) {
9589
9732
  error: preflight.error
9590
9733
  };
9591
9734
  }
9592
- log(`[Executor] Preflight check passed`);
9735
+ logger8.log(`Preflight check passed`);
9593
9736
  }
9594
9737
  const systemLogFile = `/tmp/vm0-main-${context.runId}.log`;
9595
- const exitCodeFile = `/tmp/vm0-exit-${context.runId}`;
9596
9738
  const startTime = Date.now();
9739
+ const maxWaitMs = 2 * 60 * 60 * 1e3;
9740
+ let command;
9597
9741
  if (options.benchmarkMode) {
9598
- log(`[Executor] Running command directly (benchmark mode)...`);
9599
- await guest.exec(
9600
- `nohup sh -c '${context.prompt}; echo $? > ${exitCodeFile}' > ${systemLogFile} 2>&1 &`
9601
- );
9602
- log(`[Executor] Command started in background`);
9742
+ logger8.log(`Running command directly (benchmark mode)...`);
9743
+ command = `${context.prompt} > ${systemLogFile} 2>&1`;
9603
9744
  } else {
9604
- log(`[Executor] Running agent via env-loader (background)...`);
9605
- await guest.exec(
9606
- `nohup sh -c 'node ${ENV_LOADER_PATH}; echo $? > ${exitCodeFile}' > ${systemLogFile} 2>&1 &`
9607
- );
9608
- log(`[Executor] Agent started in background`);
9745
+ logger8.log(`Running agent via env-loader...`);
9746
+ command = `node ${ENV_LOADER_PATH} > ${systemLogFile} 2>&1`;
9609
9747
  }
9610
- const pollIntervalMs = 2e3;
9611
- const maxWaitMs = 2 * 60 * 60 * 1e3;
9748
+ const { pid } = await guest.spawnAndWatch(command, maxWaitMs);
9749
+ logger8.log(`Process started with pid=${pid}`);
9612
9750
  let exitCode = 1;
9613
- let completed = false;
9614
- while (Date.now() - startTime < maxWaitMs) {
9615
- await new Promise((resolve) => setTimeout(resolve, pollIntervalMs));
9616
- const checkResult = await guest.exec(`cat ${exitCodeFile} 2>/dev/null`);
9617
- if (checkResult.exitCode === 0 && checkResult.stdout.trim()) {
9618
- const parsed = parseInt(checkResult.stdout.trim(), 10);
9619
- exitCode = Number.isNaN(parsed) ? 1 : parsed;
9620
- completed = true;
9621
- break;
9622
- }
9623
- if (!options.benchmarkMode) {
9624
- const processCheck = await guest.exec(
9625
- `pgrep -f "env-loader.mjs" > /dev/null 2>&1 && echo "RUNNING" || echo "DEAD"`
9626
- );
9627
- if (processCheck.stdout.trim() === "DEAD") {
9628
- log(
9629
- `[Executor] Agent process died unexpectedly without writing exit code`
9630
- );
9631
- const logContent = await guest.exec(
9632
- `tail -50 ${systemLogFile} 2>/dev/null`
9633
- );
9634
- const dmesgCheck = await guest.exec(
9635
- `dmesg | tail -20 | grep -iE "killed|oom" 2>/dev/null`
9636
- );
9637
- let errorMsg = "Agent process terminated unexpectedly";
9638
- if (dmesgCheck.stdout.toLowerCase().includes("oom") || dmesgCheck.stdout.toLowerCase().includes("killed")) {
9639
- errorMsg = "Agent process killed by OOM killer";
9640
- log(`[Executor] OOM detected: ${dmesgCheck.stdout}`);
9641
- }
9642
- if (logContent.stdout) {
9643
- log(
9644
- `[Executor] Last log output: ${logContent.stdout.substring(0, 500)}`
9645
- );
9646
- }
9647
- const durationMs2 = Date.now() - startTime;
9648
- recordOperation({
9649
- actionType: "agent_execute",
9650
- durationMs: durationMs2,
9651
- success: false
9652
- });
9653
- return {
9654
- exitCode: 1,
9655
- error: errorMsg
9656
- };
9657
- }
9658
- }
9659
- }
9660
- const durationMs = Date.now() - startTime;
9661
- const duration = Math.round(durationMs / 1e3);
9662
- if (!completed) {
9663
- log(`[Executor] Agent timed out after ${duration}s`);
9751
+ let exitEvent;
9752
+ try {
9753
+ exitEvent = await guest.waitForExit(pid, maxWaitMs + 5e3);
9754
+ exitCode = exitEvent.exitCode;
9755
+ } catch {
9756
+ const durationMs2 = Date.now() - startTime;
9757
+ const duration2 = Math.round(durationMs2 / 1e3);
9758
+ logger8.log(`Agent timed out after ${duration2}s`);
9664
9759
  recordOperation({
9665
9760
  actionType: "agent_execute",
9666
- durationMs,
9761
+ durationMs: durationMs2,
9667
9762
  success: false
9668
9763
  });
9669
9764
  return {
9670
9765
  exitCode: 1,
9671
- error: `Agent execution timed out after ${duration}s`
9766
+ error: `Agent execution timed out after ${duration2}s`
9672
9767
  };
9673
9768
  }
9769
+ const durationMs = Date.now() - startTime;
9770
+ const duration = Math.round(durationMs / 1e3);
9771
+ if (exitCode === 137 || exitCode === 9) {
9772
+ const dmesgCheck = await guest.exec(
9773
+ `dmesg | tail -20 | grep -iE "killed|oom" 2>/dev/null`
9774
+ );
9775
+ if (dmesgCheck.stdout.toLowerCase().includes("oom") || dmesgCheck.stdout.toLowerCase().includes("killed")) {
9776
+ logger8.log(`OOM detected: ${dmesgCheck.stdout}`);
9777
+ recordOperation({
9778
+ actionType: "agent_execute",
9779
+ durationMs,
9780
+ success: false
9781
+ });
9782
+ return {
9783
+ exitCode: 1,
9784
+ error: "Agent process killed by OOM killer"
9785
+ };
9786
+ }
9787
+ }
9674
9788
  recordOperation({
9675
9789
  actionType: "agent_execute",
9676
9790
  durationMs,
9677
9791
  success: exitCode === 0
9678
9792
  });
9679
- log(`[Executor] Agent finished in ${duration}s with exit code ${exitCode}`);
9680
- const logResult = await guest.exec(
9681
- `tail -100 ${systemLogFile} 2>/dev/null`
9682
- );
9683
- if (logResult.stdout) {
9684
- log(
9685
- `[Executor] Log output (${logResult.stdout.length} chars): ${logResult.stdout.substring(0, 500)}`
9793
+ logger8.log(`Agent finished in ${duration}s with exit code ${exitCode}`);
9794
+ if (exitEvent.stderr) {
9795
+ logger8.log(
9796
+ `Stderr (${exitEvent.stderr.length} chars): ${exitEvent.stderr.substring(0, 500)}`
9686
9797
  );
9687
9798
  }
9688
9799
  return {
9689
9800
  exitCode,
9690
- error: exitCode !== 0 ? logResult.stdout || void 0 : void 0
9801
+ error: exitCode !== 0 ? exitEvent.stderr || void 0 : void 0
9691
9802
  };
9692
9803
  } catch (error) {
9693
9804
  const errorMsg = error instanceof Error ? error.message : "Unknown error";
9694
- console.error(`[Executor] Job ${context.runId} failed: ${errorMsg}`);
9805
+ logger8.error(`Job ${context.runId} failed: ${errorMsg}`);
9695
9806
  return {
9696
9807
  exitCode: 1,
9697
9808
  error: errorMsg
9698
9809
  };
9699
9810
  } finally {
9700
9811
  if (context.experimentalFirewall?.enabled && guestIp) {
9701
- log(`[Executor] Cleaning up network security for VM ${guestIp}`);
9812
+ logger8.log(`Cleaning up network security for VM ${guestIp}`);
9702
9813
  getVMRegistry().unregister(guestIp);
9703
9814
  if (!options.benchmarkMode) {
9704
9815
  try {
@@ -9708,14 +9819,14 @@ async function executeJob(context, config, options = {}) {
9708
9819
  context.runId
9709
9820
  );
9710
9821
  } catch (err) {
9711
- console.error(
9712
- `[Executor] Failed to upload network logs: ${err instanceof Error ? err.message : "Unknown error"}`
9822
+ logger8.error(
9823
+ `Failed to upload network logs: ${err instanceof Error ? err.message : "Unknown error"}`
9713
9824
  );
9714
9825
  }
9715
9826
  }
9716
9827
  }
9717
9828
  if (vm) {
9718
- log(`[Executor] Cleaning up VM ${vmId}...`);
9829
+ logger8.log(`Cleaning up VM ${vmId}...`);
9719
9830
  await withSandboxTiming("cleanup", () => vm.kill());
9720
9831
  }
9721
9832
  await clearSandboxContext();
@@ -9724,6 +9835,7 @@ async function executeJob(context, config, options = {}) {
9724
9835
 
9725
9836
  // src/lib/runner/status.ts
9726
9837
  import { writeFileSync as writeFileSync2 } from "fs";
9838
+ var logger9 = createLogger("Runner");
9727
9839
  function writeStatusFile(statusFilePath, mode, activeRuns, startedAt) {
9728
9840
  const status = {
9729
9841
  mode,
@@ -9735,7 +9847,7 @@ function writeStatusFile(statusFilePath, mode, activeRuns, startedAt) {
9735
9847
  try {
9736
9848
  writeFileSync2(statusFilePath, JSON.stringify(status, null, 2));
9737
9849
  } catch (err) {
9738
- console.error(
9850
+ logger9.error(
9739
9851
  `Failed to write status file: ${err instanceof Error ? err.message : "Unknown error"}`
9740
9852
  );
9741
9853
  }
@@ -9752,25 +9864,26 @@ function createStatusUpdater(statusFilePath, state) {
9752
9864
  }
9753
9865
 
9754
9866
  // src/lib/runner/setup.ts
9867
+ var logger10 = createLogger("Runner");
9755
9868
  async function setupEnvironment(options) {
9756
9869
  const { config } = options;
9757
9870
  const networkCheck = checkNetworkPrerequisites();
9758
9871
  if (!networkCheck.ok) {
9759
- console.error("Network prerequisites not met:");
9872
+ logger10.error("Network prerequisites not met:");
9760
9873
  for (const error of networkCheck.errors) {
9761
- console.error(` - ${error}`);
9874
+ logger10.error(` - ${error}`);
9762
9875
  }
9763
9876
  process.exit(1);
9764
9877
  }
9765
- console.log("Setting up network bridge...");
9878
+ logger10.log("Setting up network bridge...");
9766
9879
  await setupBridge();
9767
- console.log("Flushing bridge ARP cache...");
9880
+ logger10.log("Flushing bridge ARP cache...");
9768
9881
  await flushBridgeArpCache();
9769
- console.log("Cleaning up orphaned proxy rules...");
9882
+ logger10.log("Cleaning up orphaned proxy rules...");
9770
9883
  await cleanupOrphanedProxyRules(config.name);
9771
- console.log("Cleaning up orphaned IP allocations...");
9884
+ logger10.log("Cleaning up orphaned IP allocations...");
9772
9885
  await cleanupOrphanedAllocations();
9773
- console.log("Initializing network proxy...");
9886
+ logger10.log("Initializing network proxy...");
9774
9887
  initVMRegistry();
9775
9888
  const proxyManager = initProxyManager({
9776
9889
  apiUrl: config.server.url,
@@ -9781,14 +9894,14 @@ async function setupEnvironment(options) {
9781
9894
  try {
9782
9895
  await proxyManager.start();
9783
9896
  proxyEnabled = true;
9784
- console.log("Network proxy initialized successfully");
9785
- console.log("Setting up CIDR proxy rules...");
9897
+ logger10.log("Network proxy initialized successfully");
9898
+ logger10.log("Setting up CIDR proxy rules...");
9786
9899
  await setupCIDRProxyRules(config.proxy.port);
9787
9900
  } catch (err) {
9788
- console.warn(
9901
+ logger10.log(
9789
9902
  `Network proxy not available: ${err instanceof Error ? err.message : "Unknown error"}`
9790
9903
  );
9791
- console.warn(
9904
+ logger10.log(
9792
9905
  "Jobs with experimentalFirewall enabled will run without network interception"
9793
9906
  );
9794
9907
  }
@@ -9796,33 +9909,34 @@ async function setupEnvironment(options) {
9796
9909
  }
9797
9910
  async function cleanupEnvironment(resources) {
9798
9911
  if (resources.proxyEnabled) {
9799
- console.log("Cleaning up CIDR proxy rules...");
9912
+ logger10.log("Cleaning up CIDR proxy rules...");
9800
9913
  await cleanupCIDRProxyRules(resources.proxyPort);
9801
9914
  }
9802
9915
  if (resources.proxyEnabled) {
9803
- console.log("Stopping network proxy...");
9916
+ logger10.log("Stopping network proxy...");
9804
9917
  await getProxyManager().stop();
9805
9918
  }
9806
9919
  }
9807
9920
 
9808
9921
  // src/lib/runner/signals.ts
9922
+ var logger11 = createLogger("Runner");
9809
9923
  function setupSignalHandlers(state, handlers) {
9810
9924
  process.on("SIGINT", () => {
9811
- console.log("\nShutting down...");
9925
+ logger11.log("\nShutting down...");
9812
9926
  handlers.onShutdown();
9813
9927
  state.mode = "stopped";
9814
9928
  handlers.updateStatus();
9815
9929
  });
9816
9930
  process.on("SIGTERM", () => {
9817
- console.log("\nShutting down...");
9931
+ logger11.log("\nShutting down...");
9818
9932
  handlers.onShutdown();
9819
9933
  state.mode = "stopped";
9820
9934
  handlers.updateStatus();
9821
9935
  });
9822
9936
  process.on("SIGUSR1", () => {
9823
9937
  if (state.mode === "running") {
9824
- console.log("\n[Maintenance] Entering drain mode...");
9825
- console.log(
9938
+ logger11.log("\n[Maintenance] Entering drain mode...");
9939
+ logger11.log(
9826
9940
  `[Maintenance] Active jobs: ${state.activeRuns.size} (will wait for completion)`
9827
9941
  );
9828
9942
  state.mode = "draining";
@@ -9833,6 +9947,7 @@ function setupSignalHandlers(state, handlers) {
9833
9947
  }
9834
9948
 
9835
9949
  // src/lib/runner/runner.ts
9950
+ var logger12 = createLogger("Runner");
9836
9951
  var Runner = class _Runner {
9837
9952
  config;
9838
9953
  statusFilePath;
@@ -9871,41 +9986,41 @@ var Runner = class _Runner {
9871
9986
  onDrain: () => {
9872
9987
  this.pendingJobs.length = 0;
9873
9988
  if (this.state.activeRuns.size === 0) {
9874
- console.log("[Maintenance] No active jobs, exiting immediately");
9989
+ logger12.log("[Maintenance] No active jobs, exiting immediately");
9875
9990
  this.resolveShutdown?.();
9876
9991
  }
9877
9992
  },
9878
9993
  updateStatus: this.updateStatus
9879
9994
  });
9880
- console.log(
9995
+ logger12.log(
9881
9996
  `Starting runner '${this.config.name}' for group '${this.config.group}'...`
9882
9997
  );
9883
- console.log(`Max concurrent jobs: ${this.config.sandbox.max_concurrent}`);
9884
- console.log(`Status file: ${this.statusFilePath}`);
9885
- console.log("Press Ctrl+C to stop");
9886
- console.log("");
9998
+ logger12.log(`Max concurrent jobs: ${this.config.sandbox.max_concurrent}`);
9999
+ logger12.log(`Status file: ${this.statusFilePath}`);
10000
+ logger12.log("Press Ctrl+C to stop");
10001
+ logger12.log("");
9887
10002
  this.updateStatus();
9888
- console.log("Checking for pending jobs...");
10003
+ logger12.log("Checking for pending jobs...");
9889
10004
  await this.pollFallback();
9890
- console.log("Connecting to realtime job notifications...");
10005
+ logger12.log("Connecting to realtime job notifications...");
9891
10006
  this.subscription = await subscribeToJobs(
9892
10007
  this.config.server,
9893
10008
  this.config.group,
9894
10009
  (notification) => {
9895
- console.log(`Ably notification: ${notification.runId}`);
10010
+ logger12.log(`Ably notification: ${notification.runId}`);
9896
10011
  this.processJob(notification.runId).catch(console.error);
9897
10012
  },
9898
10013
  (connectionState, reason) => {
9899
- console.log(
10014
+ logger12.log(
9900
10015
  `Ably connection: ${connectionState}${reason ? ` (${reason})` : ""}`
9901
10016
  );
9902
10017
  }
9903
10018
  );
9904
- console.log("Connected to realtime job notifications");
10019
+ logger12.log("Connected to realtime job notifications");
9905
10020
  this.pollInterval = setInterval(() => {
9906
10021
  this.pollFallback().catch(console.error);
9907
10022
  }, this.config.sandbox.poll_interval_ms);
9908
- console.log(
10023
+ logger12.log(
9909
10024
  `Polling fallback enabled (every ${this.config.sandbox.poll_interval_ms / 1e3}s)`
9910
10025
  );
9911
10026
  await shutdownPromise;
@@ -9916,7 +10031,7 @@ var Runner = class _Runner {
9916
10031
  this.subscription.cleanup();
9917
10032
  }
9918
10033
  if (this.state.jobPromises.size > 0) {
9919
- console.log(
10034
+ logger12.log(
9920
10035
  `Waiting for ${this.state.jobPromises.size} active job(s) to complete...`
9921
10036
  );
9922
10037
  await Promise.all(this.state.jobPromises);
@@ -9924,7 +10039,7 @@ var Runner = class _Runner {
9924
10039
  await cleanupEnvironment(this.resources);
9925
10040
  this.state.mode = "stopped";
9926
10041
  this.updateStatus();
9927
- console.log("Runner stopped");
10042
+ logger12.log("Runner stopped");
9928
10043
  process.exit(0);
9929
10044
  }
9930
10045
  /**
@@ -9941,13 +10056,12 @@ var Runner = class _Runner {
9941
10056
  () => pollForJob(this.config.server, this.config.group)
9942
10057
  );
9943
10058
  if (job) {
9944
- console.log(`Poll fallback found job: ${job.runId}`);
10059
+ logger12.log(`Poll fallback found job: ${job.runId}`);
9945
10060
  await this.processJob(job.runId);
9946
10061
  }
9947
10062
  } catch (error) {
9948
- console.error(
9949
- `Poll fallback error:`,
9950
- error instanceof Error ? error.message : "Unknown error"
10063
+ logger12.error(
10064
+ `Poll fallback error: ${error instanceof Error ? error.message : "Unknown error"}`
9951
10065
  );
9952
10066
  }
9953
10067
  }
@@ -9956,7 +10070,7 @@ var Runner = class _Runner {
9956
10070
  */
9957
10071
  async processJob(runId) {
9958
10072
  if (this.state.mode !== "running") {
9959
- console.log(`Not running (${this.state.mode}), ignoring job ${runId}`);
10073
+ logger12.log(`Not running (${this.state.mode}), ignoring job ${runId}`);
9960
10074
  return;
9961
10075
  }
9962
10076
  if (this.state.activeRuns.has(runId)) {
@@ -9964,10 +10078,10 @@ var Runner = class _Runner {
9964
10078
  }
9965
10079
  if (this.state.activeRuns.size >= this.config.sandbox.max_concurrent) {
9966
10080
  if (!this.pendingJobs.includes(runId) && this.pendingJobs.length < _Runner.MAX_PENDING_QUEUE_SIZE) {
9967
- console.log(`At capacity, queueing job ${runId}`);
10081
+ logger12.log(`At capacity, queueing job ${runId}`);
9968
10082
  this.pendingJobs.push(runId);
9969
10083
  } else if (this.pendingJobs.length >= _Runner.MAX_PENDING_QUEUE_SIZE) {
9970
- console.log(
10084
+ logger12.log(
9971
10085
  `Pending queue full (${_Runner.MAX_PENDING_QUEUE_SIZE}), dropping job ${runId}`
9972
10086
  );
9973
10087
  }
@@ -9978,20 +10092,19 @@ var Runner = class _Runner {
9978
10092
  "claim",
9979
10093
  () => claimJob(this.config.server, runId)
9980
10094
  );
9981
- console.log(`Claimed job: ${context.runId}`);
10095
+ logger12.log(`Claimed job: ${context.runId}`);
9982
10096
  this.state.activeRuns.add(context.runId);
9983
10097
  this.updateStatus();
9984
10098
  const jobPromise = this.executeJob(context).catch((error) => {
9985
- console.error(
9986
- `Job ${context.runId} failed:`,
9987
- error instanceof Error ? error.message : "Unknown error"
10099
+ logger12.error(
10100
+ `Job ${context.runId} failed: ${error instanceof Error ? error.message : "Unknown error"}`
9988
10101
  );
9989
10102
  }).finally(() => {
9990
10103
  this.state.activeRuns.delete(context.runId);
9991
10104
  this.state.jobPromises.delete(jobPromise);
9992
10105
  this.updateStatus();
9993
10106
  if (this.state.mode === "draining" && this.state.activeRuns.size === 0) {
9994
- console.log("[Maintenance] All jobs completed, exiting");
10107
+ logger12.log("[Maintenance] All jobs completed, exiting");
9995
10108
  this.resolveShutdown?.();
9996
10109
  return;
9997
10110
  }
@@ -10004,34 +10117,33 @@ var Runner = class _Runner {
10004
10117
  });
10005
10118
  this.state.jobPromises.add(jobPromise);
10006
10119
  } catch (error) {
10007
- console.log(
10008
- `Could not claim job ${runId}:`,
10009
- error instanceof Error ? error.message : "Unknown error"
10120
+ logger12.log(
10121
+ `Could not claim job ${runId}: ${error instanceof Error ? error.message : "Unknown error"}`
10010
10122
  );
10011
10123
  }
10012
10124
  }
10013
10125
  async executeJob(context) {
10014
- console.log(` Executing job ${context.runId}...`);
10015
- console.log(` Prompt: ${context.prompt.substring(0, 100)}...`);
10016
- console.log(` Compose version: ${context.agentComposeVersionId}`);
10126
+ logger12.log(` Executing job ${context.runId}...`);
10127
+ logger12.log(` Prompt: ${context.prompt.substring(0, 100)}...`);
10128
+ logger12.log(` Compose version: ${context.agentComposeVersionId}`);
10017
10129
  try {
10018
10130
  const result = await executeJob(context, this.config);
10019
- console.log(
10131
+ logger12.log(
10020
10132
  ` Job ${context.runId} execution completed with exit code ${result.exitCode}`
10021
10133
  );
10022
10134
  if (result.exitCode !== 0 && result.error) {
10023
- console.log(` Job ${context.runId} failed: ${result.error}`);
10135
+ logger12.error(` Job ${context.runId} failed: ${result.error}`);
10024
10136
  }
10025
10137
  } catch (err) {
10026
10138
  const error = err instanceof Error ? err.message : "Unknown execution error";
10027
- console.error(` Job ${context.runId} execution failed: ${error}`);
10139
+ logger12.error(` Job ${context.runId} execution failed: ${error}`);
10028
10140
  const result = await completeJob(
10029
10141
  this.config.server.url,
10030
10142
  context,
10031
10143
  1,
10032
10144
  error
10033
10145
  );
10034
- console.log(` Job ${context.runId} reported as ${result.status}`);
10146
+ logger12.log(` Job ${context.runId} reported as ${result.status}`);
10035
10147
  }
10036
10148
  }
10037
10149
  };
@@ -10596,13 +10708,19 @@ function createBenchmarkContext(prompt, options) {
10596
10708
  environment: null,
10597
10709
  resumeSession: null,
10598
10710
  secretValues: null,
10599
- cliAgentType: options.agentType
10711
+ cliAgentType: options.agentType,
10712
+ // Enable firewall and MITM by default for benchmark to test proxy flow
10713
+ experimentalFirewall: {
10714
+ enabled: true,
10715
+ experimental_mitm: true
10716
+ }
10600
10717
  };
10601
10718
  }
10602
10719
  var benchmarkCommand = new Command4("benchmark").description(
10603
10720
  "Run a VM performance benchmark (executes bash command directly)"
10604
10721
  ).argument("<prompt>", "The bash command to execute in the VM").option("--config <path>", "Config file path", "./runner.yaml").option("--working-dir <path>", "Working directory in VM", "/home/user").option("--agent-type <type>", "Agent type", "claude-code").action(async (prompt, options) => {
10605
10722
  const timer = new Timer();
10723
+ setGlobalLogger(timer.log.bind(timer));
10606
10724
  try {
10607
10725
  timer.log("Loading configuration...");
10608
10726
  const config = loadDebugConfig(options.config);
@@ -10621,8 +10739,7 @@ var benchmarkCommand = new Command4("benchmark").description(
10621
10739
  timer.log(`Executing command: ${prompt}`);
10622
10740
  const context = createBenchmarkContext(prompt, options);
10623
10741
  const result = await executeJob(context, config, {
10624
- benchmarkMode: true,
10625
- logger: timer.log.bind(timer)
10742
+ benchmarkMode: true
10626
10743
  });
10627
10744
  timer.log(`Exit code: ${result.exitCode}`);
10628
10745
  if (result.error) {
@@ -10639,7 +10756,7 @@ var benchmarkCommand = new Command4("benchmark").description(
10639
10756
  });
10640
10757
 
10641
10758
  // src/index.ts
10642
- var version = true ? "3.6.0" : "0.1.0";
10759
+ var version = true ? "3.6.1" : "0.1.0";
10643
10760
  program.name("vm0-runner").version(version).description("Self-hosted runner for VM0 agents");
10644
10761
  program.addCommand(startCommand);
10645
10762
  program.addCommand(doctorCommand);