@vm0/runner 3.6.0 → 3.6.2
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/index.js +471 -354
- 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
|
-
|
|
568
|
-
`
|
|
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
|
-
|
|
597
|
-
`
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
651
|
+
logger.log("No allocations in registry, nothing to clean up");
|
|
631
652
|
return;
|
|
632
653
|
}
|
|
633
654
|
const activeTaps = await scanTapDevices();
|
|
634
|
-
|
|
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
|
-
|
|
642
|
-
`
|
|
660
|
+
logger.log(
|
|
661
|
+
`Cleaned up ${beforeCount - afterCount} orphaned allocation(s)`
|
|
643
662
|
);
|
|
644
663
|
} else {
|
|
645
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
757
|
+
logger2.log(`Bridge ${BRIDGE_NAME2} already exists`);
|
|
738
758
|
await setupForwardRules();
|
|
739
759
|
return;
|
|
740
760
|
}
|
|
741
|
-
|
|
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
|
-
|
|
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
|
-
|
|
777
|
+
logger2.log("NAT rule added");
|
|
758
778
|
}
|
|
759
779
|
await setupForwardRules();
|
|
760
|
-
|
|
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
|
-
|
|
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
|
|
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(
|
|
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(
|
|
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
|
-
|
|
844
|
+
logger2.log(`TAP device ${tapDevice} does not exist, skipping delete`);
|
|
822
845
|
} else {
|
|
823
846
|
await execCommand(`ip link delete ${tapDevice}`);
|
|
824
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1004
|
+
logger2.log(`Flushing ARP cache on bridge ${BRIDGE_NAME2}...`);
|
|
982
1005
|
try {
|
|
983
1006
|
if (!await bridgeExists()) {
|
|
984
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1030
|
+
logger2.log(`Cleared ${cleared} ARP entries from bridge`);
|
|
1008
1031
|
} catch (error) {
|
|
1009
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1044
|
+
logger2.log("No orphaned proxy rules found");
|
|
1022
1045
|
return;
|
|
1023
1046
|
}
|
|
1024
|
-
|
|
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
|
-
|
|
1052
|
+
logger2.log(`Deleted orphaned rule: ${rule.substring(0, 80)}...`);
|
|
1030
1053
|
} catch {
|
|
1031
|
-
|
|
1054
|
+
logger2.log(
|
|
1032
1055
|
`Failed to delete rule (may already be gone): ${rule.substring(0, 80)}...`
|
|
1033
1056
|
);
|
|
1034
1057
|
}
|
|
1035
1058
|
}
|
|
1036
|
-
|
|
1059
|
+
logger2.log("Orphaned proxy rules cleanup complete");
|
|
1037
1060
|
} catch (error) {
|
|
1038
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
1143
|
+
createTapDevice(this.config.vmId)
|
|
1123
1144
|
]);
|
|
1124
1145
|
this.networkConfig = networkConfig;
|
|
1125
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1184
|
+
logger3.log(`[VM ${this.config.vmId}] stderr: ${line}`);
|
|
1164
1185
|
}
|
|
1165
1186
|
});
|
|
1166
1187
|
}
|
|
1167
1188
|
this.client = new FirecrackerClient(this.socketPath);
|
|
1168
|
-
|
|
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
|
-
|
|
1193
|
+
logger3.log(`[VM ${this.config.vmId}] Booting...`);
|
|
1173
1194
|
await this.client.start();
|
|
1174
1195
|
this.state = "running";
|
|
1175
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1262
|
+
logger3.log(`[VM ${this.config.vmId}] Not running, state: ${this.state}`);
|
|
1240
1263
|
return;
|
|
1241
1264
|
}
|
|
1242
1265
|
this.state = "stopping";
|
|
1243
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 <
|
|
1365
|
-
return {
|
|
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 {
|
|
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
|
-
|
|
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
|
|
1762
|
+
const pendingReqs = Array.from(this.pendingRequests.values());
|
|
1658
1763
|
this.pendingRequests.clear();
|
|
1659
|
-
for (const req of
|
|
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
|
|
1862
|
+
const pendingRequests = Array.from(this.pendingRequests.values());
|
|
1698
1863
|
this.pendingRequests.clear();
|
|
1699
|
-
for (const req of
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
-
|
|
8388
|
-
`
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
9010
|
-
|
|
9011
|
-
|
|
9012
|
-
|
|
9013
|
-
|
|
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
|
-
|
|
9207
|
+
mitmLogger.log(data.toString().trim());
|
|
9037
9208
|
});
|
|
9038
9209
|
this.process.stderr?.on("data", (data) => {
|
|
9039
|
-
|
|
9210
|
+
mitmLogger.log(data.toString().trim());
|
|
9040
9211
|
});
|
|
9041
9212
|
this.process.on("close", (code) => {
|
|
9042
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
9250
|
+
logger5.log("Proxy not running");
|
|
9080
9251
|
return;
|
|
9081
9252
|
}
|
|
9082
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
9303
|
-
`
|
|
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
|
-
|
|
9316
|
-
`
|
|
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
|
-
|
|
9534
|
+
logger7.log(`No network logs to upload for ${runId}`);
|
|
9324
9535
|
return;
|
|
9325
9536
|
}
|
|
9326
|
-
|
|
9327
|
-
`
|
|
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
|
-
|
|
9558
|
+
logger7.error(`Failed to upload network logs: ${errorText}`);
|
|
9348
9559
|
return;
|
|
9349
9560
|
}
|
|
9350
|
-
|
|
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
|
-
|
|
9461
|
-
`
|
|
9612
|
+
logger8.error(
|
|
9613
|
+
`Failed to report preflight failure: HTTP ${response.status}`
|
|
9462
9614
|
);
|
|
9463
9615
|
}
|
|
9464
9616
|
} catch (err) {
|
|
9465
|
-
|
|
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
|
-
|
|
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(`
|
|
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(`
|
|
9655
|
+
logger8.log(`VM ${vmId} started, guest IP: ${guestIp}`);
|
|
9506
9656
|
const vsockPath = vm.getVsockPath();
|
|
9507
9657
|
const guest = new VsockClient(vsockPath);
|
|
9508
|
-
log(`
|
|
9509
|
-
log(`
|
|
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(`
|
|
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
|
-
`
|
|
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
|
-
`
|
|
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(`
|
|
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(`
|
|
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(`
|
|
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(`
|
|
9599
|
-
|
|
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(`
|
|
9605
|
-
|
|
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
|
|
9611
|
-
|
|
9748
|
+
const { pid } = await guest.spawnAndWatch(command, maxWaitMs);
|
|
9749
|
+
logger8.log(`Process started with pid=${pid}`);
|
|
9612
9750
|
let exitCode = 1;
|
|
9613
|
-
let
|
|
9614
|
-
|
|
9615
|
-
|
|
9616
|
-
|
|
9617
|
-
|
|
9618
|
-
|
|
9619
|
-
|
|
9620
|
-
|
|
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 ${
|
|
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(`
|
|
9680
|
-
|
|
9681
|
-
|
|
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 ?
|
|
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
|
-
|
|
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(`
|
|
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
|
-
|
|
9712
|
-
`
|
|
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(`
|
|
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
|
-
|
|
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
|
-
|
|
9872
|
+
logger10.error("Network prerequisites not met:");
|
|
9760
9873
|
for (const error of networkCheck.errors) {
|
|
9761
|
-
|
|
9874
|
+
logger10.error(` - ${error}`);
|
|
9762
9875
|
}
|
|
9763
9876
|
process.exit(1);
|
|
9764
9877
|
}
|
|
9765
|
-
|
|
9878
|
+
logger10.log("Setting up network bridge...");
|
|
9766
9879
|
await setupBridge();
|
|
9767
|
-
|
|
9880
|
+
logger10.log("Flushing bridge ARP cache...");
|
|
9768
9881
|
await flushBridgeArpCache();
|
|
9769
|
-
|
|
9882
|
+
logger10.log("Cleaning up orphaned proxy rules...");
|
|
9770
9883
|
await cleanupOrphanedProxyRules(config.name);
|
|
9771
|
-
|
|
9884
|
+
logger10.log("Cleaning up orphaned IP allocations...");
|
|
9772
9885
|
await cleanupOrphanedAllocations();
|
|
9773
|
-
|
|
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
|
-
|
|
9785
|
-
|
|
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
|
-
|
|
9901
|
+
logger10.log(
|
|
9789
9902
|
`Network proxy not available: ${err instanceof Error ? err.message : "Unknown error"}`
|
|
9790
9903
|
);
|
|
9791
|
-
|
|
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
|
-
|
|
9912
|
+
logger10.log("Cleaning up CIDR proxy rules...");
|
|
9800
9913
|
await cleanupCIDRProxyRules(resources.proxyPort);
|
|
9801
9914
|
}
|
|
9802
9915
|
if (resources.proxyEnabled) {
|
|
9803
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
9825
|
-
|
|
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
|
-
|
|
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
|
-
|
|
9995
|
+
logger12.log(
|
|
9881
9996
|
`Starting runner '${this.config.name}' for group '${this.config.group}'...`
|
|
9882
9997
|
);
|
|
9883
|
-
|
|
9884
|
-
|
|
9885
|
-
|
|
9886
|
-
|
|
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
|
-
|
|
10003
|
+
logger12.log("Checking for pending jobs...");
|
|
9889
10004
|
await this.pollFallback();
|
|
9890
|
-
|
|
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
|
-
|
|
10010
|
+
logger12.log(`Ably notification: ${notification.runId}`);
|
|
9896
10011
|
this.processJob(notification.runId).catch(console.error);
|
|
9897
10012
|
},
|
|
9898
10013
|
(connectionState, reason) => {
|
|
9899
|
-
|
|
10014
|
+
logger12.log(
|
|
9900
10015
|
`Ably connection: ${connectionState}${reason ? ` (${reason})` : ""}`
|
|
9901
10016
|
);
|
|
9902
10017
|
}
|
|
9903
10018
|
);
|
|
9904
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
10059
|
+
logger12.log(`Poll fallback found job: ${job.runId}`);
|
|
9945
10060
|
await this.processJob(job.runId);
|
|
9946
10061
|
}
|
|
9947
10062
|
} catch (error) {
|
|
9948
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
10015
|
-
|
|
10016
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
10759
|
+
var version = true ? "3.6.2" : "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);
|