@vm0/runner 3.11.0 → 3.11.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 +192 -157
- package/package.json +1 -1
package/index.js
CHANGED
|
@@ -33,6 +33,8 @@ var runtimePaths = {
|
|
|
33
33
|
};
|
|
34
34
|
var VM_WORKSPACE_PREFIX = "vm0-";
|
|
35
35
|
var runnerPaths = {
|
|
36
|
+
/** Base directory used for snapshot generation (baseDir + /snapshot-gen) */
|
|
37
|
+
snapshotBaseDir: (baseDir) => path.join(baseDir, "snapshot-gen"),
|
|
36
38
|
/** Overlay pool directory for pre-warmed VM overlays */
|
|
37
39
|
overlayPool: (baseDir) => path.join(baseDir, "overlay-pool"),
|
|
38
40
|
/** Workspaces directory for VM work directories */
|
|
@@ -41,16 +43,24 @@ var runnerPaths = {
|
|
|
41
43
|
vmWorkDir: (baseDir, vmId) => path.join(baseDir, "workspaces", `${VM_WORKSPACE_PREFIX}${vmId}`),
|
|
42
44
|
/** Runner status file */
|
|
43
45
|
statusFile: (baseDir) => path.join(baseDir, "status.json"),
|
|
46
|
+
/** Snapshot generation work directory */
|
|
47
|
+
snapshotWorkDir: (baseDir) => path.join(baseDir, "workspaces", ".snapshot-work"),
|
|
44
48
|
/** Check if a directory name is a VM workspace */
|
|
45
49
|
isVmWorkspace: (dirname) => dirname.startsWith(VM_WORKSPACE_PREFIX),
|
|
46
50
|
/** Extract vmId from workspace directory name */
|
|
47
51
|
extractVmId: (dirname) => createVmId(dirname.replace(VM_WORKSPACE_PREFIX, ""))
|
|
48
52
|
};
|
|
49
53
|
var vmPaths = {
|
|
50
|
-
/** Firecracker config file (used with --config-file
|
|
54
|
+
/** Firecracker config file (used with --config-file) */
|
|
51
55
|
config: (workDir) => path.join(workDir, "config.json"),
|
|
52
|
-
/** Vsock
|
|
53
|
-
|
|
56
|
+
/** Vsock directory for host-guest communication */
|
|
57
|
+
vsockDir: (workDir) => path.join(workDir, "vsock"),
|
|
58
|
+
/** Vsock UDS path for host-guest communication */
|
|
59
|
+
vsock: (workDir) => path.join(workDir, "vsock", "vsock.sock"),
|
|
60
|
+
/** Firecracker API socket (used with --api-sock) */
|
|
61
|
+
apiSock: (workDir) => path.join(workDir, "api.sock"),
|
|
62
|
+
/** Overlay filesystem for VM writes */
|
|
63
|
+
overlay: (workDir) => path.join(workDir, "overlay.ext4")
|
|
54
64
|
};
|
|
55
65
|
var tempPaths = {
|
|
56
66
|
/** Default proxy CA directory */
|
|
@@ -325,15 +335,16 @@ async function subscribeToJobs(server, group, onJob, onConnectionChange) {
|
|
|
325
335
|
};
|
|
326
336
|
}
|
|
327
337
|
|
|
338
|
+
// src/lib/executor.ts
|
|
339
|
+
import fs9 from "fs";
|
|
340
|
+
|
|
328
341
|
// src/lib/firecracker/vm.ts
|
|
329
342
|
import { spawn } from "child_process";
|
|
330
343
|
import fs4 from "fs";
|
|
331
344
|
import readline from "readline";
|
|
332
345
|
|
|
333
346
|
// src/lib/firecracker/netns-pool.ts
|
|
334
|
-
import { exec } from "child_process";
|
|
335
347
|
import path2 from "path";
|
|
336
|
-
import { promisify } from "util";
|
|
337
348
|
import { existsSync, readFileSync, writeFileSync, mkdirSync } from "fs";
|
|
338
349
|
import { z as z3 } from "zod";
|
|
339
350
|
|
|
@@ -377,9 +388,25 @@ async function withFileLock(path7, fn, options) {
|
|
|
377
388
|
}
|
|
378
389
|
}
|
|
379
390
|
|
|
380
|
-
// src/lib/
|
|
391
|
+
// src/lib/utils/exec.ts
|
|
392
|
+
import { exec } from "child_process";
|
|
393
|
+
import { promisify } from "util";
|
|
381
394
|
var execAsync = promisify(exec);
|
|
382
|
-
|
|
395
|
+
async function execCommand(cmd, sudo = true) {
|
|
396
|
+
const fullCmd = sudo ? `sudo ${cmd}` : cmd;
|
|
397
|
+
try {
|
|
398
|
+
const { stdout } = await execAsync(fullCmd);
|
|
399
|
+
return stdout.trim();
|
|
400
|
+
} catch (error) {
|
|
401
|
+
const execError = error;
|
|
402
|
+
throw new Error(
|
|
403
|
+
`Command failed: ${fullCmd}
|
|
404
|
+
${execError.stderr || execError.message}`
|
|
405
|
+
);
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
// src/lib/firecracker/netns.ts
|
|
383
410
|
var SNAPSHOT_NETWORK = {
|
|
384
411
|
/** TAP device name inside namespace (must match Firecracker config) */
|
|
385
412
|
tapName: "vm0-tap",
|
|
@@ -394,14 +421,28 @@ var SNAPSHOT_NETWORK = {
|
|
|
394
421
|
/** CIDR prefix length (for ip commands) */
|
|
395
422
|
prefixLen: 29
|
|
396
423
|
};
|
|
397
|
-
var VETH_NS = "veth0";
|
|
398
|
-
var NS_PREFIX = "vm0-ns-";
|
|
399
|
-
var VETH_PREFIX = "vm0-ve-";
|
|
400
|
-
var VETH_IP_PREFIX = "10.200";
|
|
401
424
|
function generateSnapshotNetworkBootArgs() {
|
|
402
425
|
const { guestIp, gatewayIp, netmask } = SNAPSHOT_NETWORK;
|
|
403
426
|
return `ip=${guestIp}::${gatewayIp}:${netmask}:vm0-guest:eth0:off`;
|
|
404
427
|
}
|
|
428
|
+
async function createNetnsWithTap(nsName, tap) {
|
|
429
|
+
await execCommand(`ip netns add ${nsName}`);
|
|
430
|
+
await execCommand(
|
|
431
|
+
`ip netns exec ${nsName} ip tuntap add ${tap.tapName} mode tap`
|
|
432
|
+
);
|
|
433
|
+
await execCommand(
|
|
434
|
+
`ip netns exec ${nsName} ip addr add ${tap.gatewayIpWithPrefix} dev ${tap.tapName}`
|
|
435
|
+
);
|
|
436
|
+
await execCommand(`ip netns exec ${nsName} ip link set ${tap.tapName} up`);
|
|
437
|
+
await execCommand(`ip netns exec ${nsName} ip link set lo up`);
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
// src/lib/firecracker/netns-pool.ts
|
|
441
|
+
var logger = createLogger("NetnsPool");
|
|
442
|
+
var VETH_NS = "veth0";
|
|
443
|
+
var NS_PREFIX = "vm0-ns-";
|
|
444
|
+
var VETH_PREFIX = "vm0-ve-";
|
|
445
|
+
var VETH_IP_PREFIX = "10.200";
|
|
405
446
|
var MAX_RUNNERS = 64;
|
|
406
447
|
var MAX_NAMESPACES_PER_RUNNER = 256;
|
|
407
448
|
var NamespaceEntrySchema = z3.object({
|
|
@@ -459,19 +500,6 @@ var RegistryFile = class {
|
|
|
459
500
|
});
|
|
460
501
|
}
|
|
461
502
|
};
|
|
462
|
-
async function execCommand(cmd, sudo = true) {
|
|
463
|
-
const fullCmd = sudo ? `sudo ${cmd}` : cmd;
|
|
464
|
-
try {
|
|
465
|
-
const { stdout } = await execAsync(fullCmd);
|
|
466
|
-
return stdout.trim();
|
|
467
|
-
} catch (error) {
|
|
468
|
-
const execError = error;
|
|
469
|
-
throw new Error(
|
|
470
|
-
`Command failed: ${fullCmd}
|
|
471
|
-
${execError.stderr || execError.message}`
|
|
472
|
-
);
|
|
473
|
-
}
|
|
474
|
-
}
|
|
475
503
|
async function getDefaultInterface() {
|
|
476
504
|
const result = await execCommand("ip route get 8.8.8.8", false);
|
|
477
505
|
const match = result.match(/dev\s+(\S+)/);
|
|
@@ -694,16 +722,10 @@ var NetnsPool = class _NetnsPool {
|
|
|
694
722
|
});
|
|
695
723
|
logger.log(`Creating namespace ${name}...`);
|
|
696
724
|
try {
|
|
697
|
-
await
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
);
|
|
701
|
-
await execCommand(
|
|
702
|
-
`ip netns exec ${name} ip addr add ${SNAPSHOT_NETWORK.gatewayIp}/${SNAPSHOT_NETWORK.prefixLen} dev ${SNAPSHOT_NETWORK.tapName}`
|
|
703
|
-
);
|
|
704
|
-
await execCommand(
|
|
705
|
-
`ip netns exec ${name} ip link set ${SNAPSHOT_NETWORK.tapName} up`
|
|
706
|
-
);
|
|
725
|
+
await createNetnsWithTap(name, {
|
|
726
|
+
tapName: SNAPSHOT_NETWORK.tapName,
|
|
727
|
+
gatewayIpWithPrefix: `${SNAPSHOT_NETWORK.gatewayIp}/${SNAPSHOT_NETWORK.prefixLen}`
|
|
728
|
+
});
|
|
707
729
|
await execCommand(
|
|
708
730
|
`ip link add ${vethHost} type veth peer name ${VETH_NS} netns ${name}`
|
|
709
731
|
);
|
|
@@ -711,7 +733,6 @@ var NetnsPool = class _NetnsPool {
|
|
|
711
733
|
`ip netns exec ${name} ip addr add ${vethNsIp}/30 dev ${VETH_NS}`
|
|
712
734
|
);
|
|
713
735
|
await execCommand(`ip netns exec ${name} ip link set ${VETH_NS} up`);
|
|
714
|
-
await execCommand(`ip netns exec ${name} ip link set lo up`);
|
|
715
736
|
await execCommand(`ip addr add ${vethHostIp}/30 dev ${vethHost}`);
|
|
716
737
|
await execCommand(`ip link set ${vethHost} up`);
|
|
717
738
|
await execCommand(
|
|
@@ -878,7 +899,7 @@ import { promisify as promisify2 } from "util";
|
|
|
878
899
|
var execAsync2 = promisify2(exec2);
|
|
879
900
|
var logger2 = createLogger("OverlayPool");
|
|
880
901
|
var OVERLAY_SIZE = 2 * 1024 * 1024 * 1024;
|
|
881
|
-
async function
|
|
902
|
+
async function createOverlayFile(filePath) {
|
|
882
903
|
const fd = fs2.openSync(filePath, "w");
|
|
883
904
|
fs2.ftruncateSync(fd, OVERLAY_SIZE);
|
|
884
905
|
fs2.closeSync(fd);
|
|
@@ -894,7 +915,7 @@ var OverlayPool = class {
|
|
|
894
915
|
size: config.size,
|
|
895
916
|
replenishThreshold: config.replenishThreshold,
|
|
896
917
|
poolDir: config.poolDir,
|
|
897
|
-
createFile: config.createFile ??
|
|
918
|
+
createFile: config.createFile ?? createOverlayFile
|
|
898
919
|
};
|
|
899
920
|
}
|
|
900
921
|
/**
|
|
@@ -903,19 +924,6 @@ var OverlayPool = class {
|
|
|
903
924
|
generateFileName() {
|
|
904
925
|
return `overlay-${randomUUID()}.ext4`;
|
|
905
926
|
}
|
|
906
|
-
/**
|
|
907
|
-
* Ensure the pool directory exists
|
|
908
|
-
*/
|
|
909
|
-
async ensurePoolDir() {
|
|
910
|
-
const parentDir = path3.dirname(this.config.poolDir);
|
|
911
|
-
if (!fs2.existsSync(parentDir)) {
|
|
912
|
-
await execAsync2(`sudo mkdir -p ${parentDir}`);
|
|
913
|
-
await execAsync2(`sudo chmod 777 ${parentDir}`);
|
|
914
|
-
}
|
|
915
|
-
if (!fs2.existsSync(this.config.poolDir)) {
|
|
916
|
-
fs2.mkdirSync(this.config.poolDir, { recursive: true });
|
|
917
|
-
}
|
|
918
|
-
}
|
|
919
927
|
/**
|
|
920
928
|
* Scan pool directory for overlay files
|
|
921
929
|
*/
|
|
@@ -969,7 +977,7 @@ var OverlayPool = class {
|
|
|
969
977
|
logger2.log(
|
|
970
978
|
`Initializing overlay pool (size=${this.config.size}, threshold=${this.config.replenishThreshold})...`
|
|
971
979
|
);
|
|
972
|
-
|
|
980
|
+
fs2.mkdirSync(this.config.poolDir, { recursive: true });
|
|
973
981
|
const existing = this.scanPoolDir();
|
|
974
982
|
if (existing.length > 0) {
|
|
975
983
|
logger2.log(`Cleaning up ${existing.length} stale overlay(s)`);
|
|
@@ -1081,6 +1089,36 @@ var FirecrackerClient = class {
|
|
|
1081
1089
|
constructor(socketPath) {
|
|
1082
1090
|
this.socketPath = socketPath;
|
|
1083
1091
|
}
|
|
1092
|
+
/**
|
|
1093
|
+
* Configure machine settings (vCPUs, memory)
|
|
1094
|
+
*/
|
|
1095
|
+
async configureMachine(config) {
|
|
1096
|
+
await this.put("/machine-config", config);
|
|
1097
|
+
}
|
|
1098
|
+
/**
|
|
1099
|
+
* Configure boot source (kernel, boot args)
|
|
1100
|
+
*/
|
|
1101
|
+
async configureBootSource(config) {
|
|
1102
|
+
await this.put("/boot-source", config);
|
|
1103
|
+
}
|
|
1104
|
+
/**
|
|
1105
|
+
* Configure network interface
|
|
1106
|
+
*/
|
|
1107
|
+
async configureNetworkInterface(config) {
|
|
1108
|
+
await this.put(`/network-interfaces/${config.iface_id}`, config);
|
|
1109
|
+
}
|
|
1110
|
+
/**
|
|
1111
|
+
* Configure vsock device
|
|
1112
|
+
*/
|
|
1113
|
+
async configureVsock(config) {
|
|
1114
|
+
await this.put("/vsock", config);
|
|
1115
|
+
}
|
|
1116
|
+
/**
|
|
1117
|
+
* Start the VM instance
|
|
1118
|
+
*/
|
|
1119
|
+
async startInstance() {
|
|
1120
|
+
await this.put("/actions", { action_type: "InstanceStart" });
|
|
1121
|
+
}
|
|
1084
1122
|
/**
|
|
1085
1123
|
* Pause the VM
|
|
1086
1124
|
*
|
|
@@ -1177,17 +1215,27 @@ var FirecrackerClient = class {
|
|
|
1177
1215
|
*/
|
|
1178
1216
|
request(method, path7, body, timeoutMs = 3e4) {
|
|
1179
1217
|
return new Promise((resolve, reject) => {
|
|
1218
|
+
const bodyStr = body !== void 0 ? JSON.stringify(body) : void 0;
|
|
1219
|
+
const headers = {
|
|
1220
|
+
Accept: "application/json",
|
|
1221
|
+
// Disable keep-alive to prevent pipelining issues
|
|
1222
|
+
Connection: "close"
|
|
1223
|
+
};
|
|
1224
|
+
if (bodyStr !== void 0) {
|
|
1225
|
+
headers["Content-Type"] = "application/json";
|
|
1226
|
+
headers["Content-Length"] = Buffer.byteLength(bodyStr);
|
|
1227
|
+
}
|
|
1180
1228
|
const options = {
|
|
1181
1229
|
socketPath: this.socketPath,
|
|
1182
|
-
path:
|
|
1230
|
+
path: path7,
|
|
1183
1231
|
method,
|
|
1184
|
-
headers
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1232
|
+
headers,
|
|
1233
|
+
timeout: timeoutMs,
|
|
1234
|
+
// Disable agent to ensure fresh connection for each request
|
|
1235
|
+
// Firecracker's single-threaded API can have issues with pipelined requests
|
|
1236
|
+
agent: false
|
|
1189
1237
|
};
|
|
1190
|
-
logger3.log(`${method} ${path7}${
|
|
1238
|
+
logger3.log(`${method} ${path7}${bodyStr ? " " + bodyStr : ""}`);
|
|
1191
1239
|
const req = http.request(options, (res) => {
|
|
1192
1240
|
let data = "";
|
|
1193
1241
|
res.on("data", (chunk) => {
|
|
@@ -1217,8 +1265,8 @@ var FirecrackerClient = class {
|
|
|
1217
1265
|
req.on("error", (err) => {
|
|
1218
1266
|
reject(err);
|
|
1219
1267
|
});
|
|
1220
|
-
if (
|
|
1221
|
-
req.write(
|
|
1268
|
+
if (bodyStr !== void 0) {
|
|
1269
|
+
req.write(bodyStr);
|
|
1222
1270
|
}
|
|
1223
1271
|
req.end();
|
|
1224
1272
|
});
|
|
@@ -1231,6 +1279,58 @@ var FirecrackerClient = class {
|
|
|
1231
1279
|
}
|
|
1232
1280
|
};
|
|
1233
1281
|
|
|
1282
|
+
// src/lib/firecracker/config.ts
|
|
1283
|
+
function buildBootArgs() {
|
|
1284
|
+
const networkBootArgs = generateSnapshotNetworkBootArgs();
|
|
1285
|
+
return `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}`;
|
|
1286
|
+
}
|
|
1287
|
+
function buildFirecrackerConfig(params) {
|
|
1288
|
+
const bootArgs = buildBootArgs();
|
|
1289
|
+
return {
|
|
1290
|
+
"boot-source": {
|
|
1291
|
+
kernel_image_path: params.kernelPath,
|
|
1292
|
+
boot_args: bootArgs
|
|
1293
|
+
},
|
|
1294
|
+
drives: [
|
|
1295
|
+
// Base drive (squashfs, read-only, shared across VMs)
|
|
1296
|
+
// Mounted as /dev/vda inside the VM
|
|
1297
|
+
{
|
|
1298
|
+
drive_id: "rootfs",
|
|
1299
|
+
path_on_host: params.rootfsPath,
|
|
1300
|
+
is_root_device: true,
|
|
1301
|
+
is_read_only: true
|
|
1302
|
+
},
|
|
1303
|
+
// Overlay drive (ext4, read-write, per-VM)
|
|
1304
|
+
// Mounted as /dev/vdb inside the VM
|
|
1305
|
+
// The vm-init script combines these using overlayfs
|
|
1306
|
+
{
|
|
1307
|
+
drive_id: "overlay",
|
|
1308
|
+
path_on_host: params.overlayPath,
|
|
1309
|
+
is_root_device: false,
|
|
1310
|
+
is_read_only: false
|
|
1311
|
+
}
|
|
1312
|
+
],
|
|
1313
|
+
"machine-config": {
|
|
1314
|
+
vcpu_count: params.vcpus,
|
|
1315
|
+
mem_size_mib: params.memoryMb
|
|
1316
|
+
},
|
|
1317
|
+
"network-interfaces": [
|
|
1318
|
+
{
|
|
1319
|
+
// Network interface uses fixed config from SNAPSHOT_NETWORK
|
|
1320
|
+
// TAP device is inside the namespace, created by netns-pool
|
|
1321
|
+
iface_id: "eth0",
|
|
1322
|
+
guest_mac: SNAPSHOT_NETWORK.guestMac,
|
|
1323
|
+
host_dev_name: SNAPSHOT_NETWORK.tapName
|
|
1324
|
+
}
|
|
1325
|
+
],
|
|
1326
|
+
// Guest CID 3 is the standard guest identifier (CID 0=hypervisor, 1=local, 2=host)
|
|
1327
|
+
vsock: {
|
|
1328
|
+
guest_cid: 3,
|
|
1329
|
+
uds_path: params.vsockPath
|
|
1330
|
+
}
|
|
1331
|
+
};
|
|
1332
|
+
}
|
|
1333
|
+
|
|
1234
1334
|
// src/lib/firecracker/vm.ts
|
|
1235
1335
|
var logger4 = createLogger("VM");
|
|
1236
1336
|
var FirecrackerVM = class {
|
|
@@ -1295,7 +1395,6 @@ var FirecrackerVM = class {
|
|
|
1295
1395
|
throw new Error(`Cannot start VM in state: ${this.state}`);
|
|
1296
1396
|
}
|
|
1297
1397
|
try {
|
|
1298
|
-
fs4.mkdirSync(this.workDir, { recursive: true });
|
|
1299
1398
|
logger4.log(`[VM ${this.config.vmId}] Acquiring resources...`);
|
|
1300
1399
|
const results = await Promise.allSettled([
|
|
1301
1400
|
acquireOverlay(),
|
|
@@ -1471,74 +1570,23 @@ var FirecrackerVM = class {
|
|
|
1471
1570
|
}
|
|
1472
1571
|
/**
|
|
1473
1572
|
* Build Firecracker configuration object
|
|
1474
|
-
*
|
|
1475
|
-
* Creates the JSON configuration for Firecracker's --config-file option.
|
|
1476
|
-
* Boot args:
|
|
1477
|
-
* - console=ttyS0: serial console output
|
|
1478
|
-
* - reboot=k: use keyboard controller for reboot
|
|
1479
|
-
* - panic=1: reboot after 1 second on kernel panic
|
|
1480
|
-
* - pci=off: disable PCI bus (not needed in microVM)
|
|
1481
|
-
* - nomodules: skip module loading (not needed in microVM)
|
|
1482
|
-
* - random.trust_cpu=on: trust CPU RNG, skip entropy wait
|
|
1483
|
-
* - quiet loglevel=0: minimize kernel log output
|
|
1484
|
-
* - nokaslr: disable kernel address space randomization
|
|
1485
|
-
* - audit=0: disable kernel auditing
|
|
1486
|
-
* - numa=off: disable NUMA (single node)
|
|
1487
|
-
* - mitigations=off: disable CPU vulnerability mitigations
|
|
1488
|
-
* - noresume: skip hibernation resume check
|
|
1489
|
-
* - init=/sbin/vm-init: use vm-init (Rust binary) for filesystem setup and vsock-agent
|
|
1490
|
-
* - ip=...: network configuration (fixed IPs from SNAPSHOT_NETWORK)
|
|
1491
1573
|
*/
|
|
1492
1574
|
buildConfig() {
|
|
1493
1575
|
if (!this.netns || !this.vmOverlayPath) {
|
|
1494
1576
|
throw new Error("VM not properly initialized");
|
|
1495
1577
|
}
|
|
1496
|
-
const
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
drive_id: "rootfs",
|
|
1509
|
-
path_on_host: this.config.rootfsPath,
|
|
1510
|
-
is_root_device: true,
|
|
1511
|
-
is_read_only: true
|
|
1512
|
-
},
|
|
1513
|
-
// Overlay drive (ext4, read-write, per-VM)
|
|
1514
|
-
// Mounted as /dev/vdb inside the VM
|
|
1515
|
-
// The vm-init script combines these using overlayfs
|
|
1516
|
-
{
|
|
1517
|
-
drive_id: "overlay",
|
|
1518
|
-
path_on_host: this.vmOverlayPath,
|
|
1519
|
-
is_root_device: false,
|
|
1520
|
-
is_read_only: false
|
|
1521
|
-
}
|
|
1522
|
-
],
|
|
1523
|
-
"machine-config": {
|
|
1524
|
-
vcpu_count: this.config.vcpus,
|
|
1525
|
-
mem_size_mib: this.config.memoryMb
|
|
1526
|
-
},
|
|
1527
|
-
"network-interfaces": [
|
|
1528
|
-
{
|
|
1529
|
-
// Network interface uses fixed config from SNAPSHOT_NETWORK
|
|
1530
|
-
// TAP device is inside the namespace, created by netns-pool
|
|
1531
|
-
iface_id: "eth0",
|
|
1532
|
-
guest_mac: SNAPSHOT_NETWORK.guestMac,
|
|
1533
|
-
host_dev_name: SNAPSHOT_NETWORK.tapName
|
|
1534
|
-
}
|
|
1535
|
-
],
|
|
1536
|
-
// Guest CID 3 is the standard guest identifier (CID 0=hypervisor, 1=local, 2=host)
|
|
1537
|
-
vsock: {
|
|
1538
|
-
guest_cid: 3,
|
|
1539
|
-
uds_path: this.vsockPath
|
|
1540
|
-
}
|
|
1541
|
-
};
|
|
1578
|
+
const config = buildFirecrackerConfig({
|
|
1579
|
+
kernelPath: this.config.kernelPath,
|
|
1580
|
+
rootfsPath: this.config.rootfsPath,
|
|
1581
|
+
overlayPath: this.vmOverlayPath,
|
|
1582
|
+
vsockPath: this.vsockPath,
|
|
1583
|
+
vcpus: this.config.vcpus,
|
|
1584
|
+
memoryMb: this.config.memoryMb
|
|
1585
|
+
});
|
|
1586
|
+
logger4.log(
|
|
1587
|
+
`[VM ${this.config.vmId}] Boot args: ${config["boot-source"].boot_args}`
|
|
1588
|
+
);
|
|
1589
|
+
return config;
|
|
1542
1590
|
}
|
|
1543
1591
|
/**
|
|
1544
1592
|
* Stop the VM
|
|
@@ -7666,6 +7714,7 @@ var modelProviderTypeSchema = z18.enum([
|
|
|
7666
7714
|
"openrouter-api-key",
|
|
7667
7715
|
"moonshot-api-key",
|
|
7668
7716
|
"minimax-api-key",
|
|
7717
|
+
"deepseek-api-key",
|
|
7669
7718
|
"aws-bedrock"
|
|
7670
7719
|
]);
|
|
7671
7720
|
var modelProviderFrameworkSchema = z18.enum(["claude-code", "codex"]);
|
|
@@ -8931,7 +8980,7 @@ var FEATURE_SWITCHES = {
|
|
|
8931
8980
|
},
|
|
8932
8981
|
["platformAgents" /* PlatformAgents */]: {
|
|
8933
8982
|
maintainer: "ethan@vm0.ai",
|
|
8934
|
-
enabled:
|
|
8983
|
+
enabled: true
|
|
8935
8984
|
},
|
|
8936
8985
|
["platformSecrets" /* PlatformSecrets */]: {
|
|
8937
8986
|
maintainer: "ethan@vm0.ai",
|
|
@@ -10039,6 +10088,13 @@ async function executeJob(context, config, options = {}) {
|
|
|
10039
10088
|
firecrackerBinary: config.firecracker.binary,
|
|
10040
10089
|
workDir: runnerPaths.vmWorkDir(config.base_dir, vmId)
|
|
10041
10090
|
};
|
|
10091
|
+
fs9.mkdirSync(vmConfig.workDir, { recursive: true });
|
|
10092
|
+
fs9.mkdirSync(vmPaths.vsockDir(vmConfig.workDir), { recursive: true });
|
|
10093
|
+
const vsockPath = vmPaths.vsock(vmConfig.workDir);
|
|
10094
|
+
vsockClient = new VsockClient(vsockPath);
|
|
10095
|
+
const guest = vsockClient;
|
|
10096
|
+
logger9.log(`Starting vsock listener: ${vsockPath}`);
|
|
10097
|
+
const guestConnectionPromise = guest.waitForGuestConnection(3e4);
|
|
10042
10098
|
logger9.log(`Creating VM ${vmId}...`);
|
|
10043
10099
|
vm = new FirecrackerVM(vmConfig);
|
|
10044
10100
|
await withSandboxTiming("vm_create", () => vm.start());
|
|
@@ -10050,10 +10106,6 @@ async function executeJob(context, config, options = {}) {
|
|
|
10050
10106
|
logger9.log(
|
|
10051
10107
|
`VM ${vmId} started, guest IP: ${guestIp}, veth NS IP: ${vethNsIp}`
|
|
10052
10108
|
);
|
|
10053
|
-
const vsockPath = vm.getVsockPath();
|
|
10054
|
-
vsockClient = new VsockClient(vsockPath);
|
|
10055
|
-
const guest = vsockClient;
|
|
10056
|
-
logger9.log(`Using vsock for guest communication: ${vsockPath}`);
|
|
10057
10109
|
const envJson = JSON.stringify(
|
|
10058
10110
|
buildEnvironmentVariables(context, config.server.url)
|
|
10059
10111
|
);
|
|
@@ -10071,10 +10123,7 @@ async function executeJob(context, config, options = {}) {
|
|
|
10071
10123
|
});
|
|
10072
10124
|
}
|
|
10073
10125
|
logger9.log(`Waiting for guest connection...`);
|
|
10074
|
-
await withSandboxTiming(
|
|
10075
|
-
"guest_wait",
|
|
10076
|
-
() => guest.waitForGuestConnection(3e4)
|
|
10077
|
-
);
|
|
10126
|
+
await withSandboxTiming("guest_wait", () => guestConnectionPromise);
|
|
10078
10127
|
logger9.log(`Guest client ready`);
|
|
10079
10128
|
if (context.storageManifest) {
|
|
10080
10129
|
await withSandboxTiming(
|
|
@@ -10277,24 +10326,11 @@ async function isPortInUse(port) {
|
|
|
10277
10326
|
}
|
|
10278
10327
|
|
|
10279
10328
|
// src/lib/runner/runner-lock.ts
|
|
10280
|
-
import
|
|
10281
|
-
import fs9 from "fs";
|
|
10329
|
+
import fs10 from "fs";
|
|
10282
10330
|
import path5 from "path";
|
|
10283
|
-
import { promisify as promisify4 } from "util";
|
|
10284
|
-
var execAsync4 = promisify4(exec4);
|
|
10285
10331
|
var logger11 = createLogger("RunnerLock");
|
|
10286
10332
|
var DEFAULT_PID_FILE = runtimePaths.runnerPid;
|
|
10287
10333
|
var currentPidFile = null;
|
|
10288
|
-
async function ensureRunDir(dirPath, skipSudo) {
|
|
10289
|
-
if (!fs9.existsSync(dirPath)) {
|
|
10290
|
-
if (skipSudo) {
|
|
10291
|
-
fs9.mkdirSync(dirPath, { recursive: true });
|
|
10292
|
-
} else {
|
|
10293
|
-
await execAsync4(`sudo mkdir -p ${dirPath}`);
|
|
10294
|
-
await execAsync4(`sudo chmod 777 ${dirPath}`);
|
|
10295
|
-
}
|
|
10296
|
-
}
|
|
10297
|
-
}
|
|
10298
10334
|
function isProcessRunning(pid) {
|
|
10299
10335
|
try {
|
|
10300
10336
|
process.kill(pid, 0);
|
|
@@ -10306,13 +10342,12 @@ function isProcessRunning(pid) {
|
|
|
10306
10342
|
return false;
|
|
10307
10343
|
}
|
|
10308
10344
|
}
|
|
10309
|
-
|
|
10345
|
+
function acquireRunnerLock(options = {}) {
|
|
10310
10346
|
const pidFile = options.pidFile ?? DEFAULT_PID_FILE;
|
|
10311
|
-
const skipSudo = options.skipSudo ?? false;
|
|
10312
10347
|
const runDir = path5.dirname(pidFile);
|
|
10313
|
-
|
|
10314
|
-
if (
|
|
10315
|
-
const pidStr =
|
|
10348
|
+
fs10.mkdirSync(runDir, { recursive: true });
|
|
10349
|
+
if (fs10.existsSync(pidFile)) {
|
|
10350
|
+
const pidStr = fs10.readFileSync(pidFile, "utf-8").trim();
|
|
10316
10351
|
const pid = parseInt(pidStr, 10);
|
|
10317
10352
|
if (!isNaN(pid) && isProcessRunning(pid)) {
|
|
10318
10353
|
logger11.error(`Error: Another runner is already running (PID ${pid})`);
|
|
@@ -10324,16 +10359,16 @@ async function acquireRunnerLock(options = {}) {
|
|
|
10324
10359
|
} else {
|
|
10325
10360
|
logger11.log(`Cleaning up stale PID file (PID ${pid} not running)`);
|
|
10326
10361
|
}
|
|
10327
|
-
|
|
10362
|
+
fs10.unlinkSync(pidFile);
|
|
10328
10363
|
}
|
|
10329
|
-
|
|
10364
|
+
fs10.writeFileSync(pidFile, process.pid.toString());
|
|
10330
10365
|
currentPidFile = pidFile;
|
|
10331
10366
|
logger11.log(`Runner lock acquired (PID ${process.pid})`);
|
|
10332
10367
|
}
|
|
10333
10368
|
function releaseRunnerLock() {
|
|
10334
10369
|
const pidFile = currentPidFile ?? DEFAULT_PID_FILE;
|
|
10335
|
-
if (
|
|
10336
|
-
|
|
10370
|
+
if (fs10.existsSync(pidFile)) {
|
|
10371
|
+
fs10.unlinkSync(pidFile);
|
|
10337
10372
|
logger11.log("Runner lock released");
|
|
10338
10373
|
}
|
|
10339
10374
|
currentPidFile = null;
|
|
@@ -11205,7 +11240,7 @@ var benchmarkCommand = new Command4("benchmark").description(
|
|
|
11205
11240
|
});
|
|
11206
11241
|
|
|
11207
11242
|
// src/index.ts
|
|
11208
|
-
var version = true ? "3.11.
|
|
11243
|
+
var version = true ? "3.11.2" : "0.1.0";
|
|
11209
11244
|
program.name("vm0-runner").version(version).description("Self-hosted runner for VM0 agents");
|
|
11210
11245
|
program.addCommand(startCommand);
|
|
11211
11246
|
program.addCommand(doctorCommand);
|