@vm0/runner 3.9.4 → 3.10.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/index.js +113 -52
- package/package.json +1 -1
package/index.js
CHANGED
|
@@ -13,6 +13,16 @@ import yaml from "yaml";
|
|
|
13
13
|
|
|
14
14
|
// src/lib/paths.ts
|
|
15
15
|
import path from "path";
|
|
16
|
+
|
|
17
|
+
// src/lib/firecracker/vm-id.ts
|
|
18
|
+
function createVmId(runId) {
|
|
19
|
+
return runId.substring(0, 8).padStart(8, "0");
|
|
20
|
+
}
|
|
21
|
+
function vmIdValue(vmId) {
|
|
22
|
+
return vmId;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// src/lib/paths.ts
|
|
16
26
|
var VM0_RUN_DIR = "/var/run/vm0";
|
|
17
27
|
var VM0_TMP_PREFIX = "/tmp/vm0";
|
|
18
28
|
var runtimePaths = {
|
|
@@ -34,7 +44,7 @@ var runnerPaths = {
|
|
|
34
44
|
/** Check if a directory name is a VM workspace */
|
|
35
45
|
isVmWorkspace: (dirname) => dirname.startsWith(VM_WORKSPACE_PREFIX),
|
|
36
46
|
/** Extract vmId from workspace directory name */
|
|
37
|
-
extractVmId: (dirname) => dirname.replace(VM_WORKSPACE_PREFIX, "")
|
|
47
|
+
extractVmId: (dirname) => createVmId(dirname.replace(VM_WORKSPACE_PREFIX, ""))
|
|
38
48
|
};
|
|
39
49
|
var vmPaths = {
|
|
40
50
|
/** Firecracker config file (used with --config-file --no-api) */
|
|
@@ -360,7 +370,7 @@ function hashString(str) {
|
|
|
360
370
|
return Math.abs(hash);
|
|
361
371
|
}
|
|
362
372
|
function generateMacAddress(vmId) {
|
|
363
|
-
const hash = hashString(vmId);
|
|
373
|
+
const hash = hashString(vmIdValue(vmId));
|
|
364
374
|
const b1 = hash >> 16 & 255;
|
|
365
375
|
const b2 = hash >> 8 & 255;
|
|
366
376
|
const b3 = hash & 255;
|
|
@@ -1089,7 +1099,7 @@ var IPRegistry = class {
|
|
|
1089
1099
|
return this.withIPLock(async () => {
|
|
1090
1100
|
const registry = this.readRegistry();
|
|
1091
1101
|
if (registry.allocations[ip]) {
|
|
1092
|
-
registry.allocations[ip].vmId = vmId;
|
|
1102
|
+
registry.allocations[ip].vmId = vmIdValue(vmId);
|
|
1093
1103
|
this.writeRegistry(registry);
|
|
1094
1104
|
}
|
|
1095
1105
|
});
|
|
@@ -1102,7 +1112,7 @@ var IPRegistry = class {
|
|
|
1102
1112
|
async clearVmIdFromIP(ip, expectedVmId) {
|
|
1103
1113
|
return this.withIPLock(async () => {
|
|
1104
1114
|
const registry = this.readRegistry();
|
|
1105
|
-
if (registry.allocations[ip] && registry.allocations[ip].vmId === expectedVmId) {
|
|
1115
|
+
if (registry.allocations[ip] && registry.allocations[ip].vmId === vmIdValue(expectedVmId)) {
|
|
1106
1116
|
registry.allocations[ip].vmId = null;
|
|
1107
1117
|
this.writeRegistry(registry);
|
|
1108
1118
|
}
|
|
@@ -1122,8 +1132,9 @@ var IPRegistry = class {
|
|
|
1122
1132
|
*/
|
|
1123
1133
|
getIPForVm(vmId) {
|
|
1124
1134
|
const registry = this.readRegistry();
|
|
1135
|
+
const vmIdStr = vmIdValue(vmId);
|
|
1125
1136
|
for (const [ip, allocation] of Object.entries(registry.allocations)) {
|
|
1126
|
-
if (allocation.vmId ===
|
|
1137
|
+
if (allocation.vmId === vmIdStr) {
|
|
1127
1138
|
return ip;
|
|
1128
1139
|
}
|
|
1129
1140
|
}
|
|
@@ -1352,12 +1363,15 @@ var TapPool = class {
|
|
|
1352
1363
|
}
|
|
1353
1364
|
const guestMac = generateMacAddress(vmId);
|
|
1354
1365
|
try {
|
|
1355
|
-
await
|
|
1366
|
+
await Promise.all([
|
|
1367
|
+
this.config.setMac(resource.tapDevice, guestMac),
|
|
1368
|
+
clearArpEntry(resource.guestIp)
|
|
1369
|
+
]);
|
|
1356
1370
|
} catch (err) {
|
|
1357
1371
|
if (fromPool) {
|
|
1358
1372
|
this.queue.push(resource);
|
|
1359
1373
|
logger4.log(
|
|
1360
|
-
`Returned pair to pool after MAC
|
|
1374
|
+
`Returned pair to pool after MAC/ARP failure: ${resource.tapDevice}`
|
|
1361
1375
|
);
|
|
1362
1376
|
} else {
|
|
1363
1377
|
await releaseIP(resource.guestIp).catch(() => {
|
|
@@ -1367,14 +1381,11 @@ var TapPool = class {
|
|
|
1367
1381
|
}
|
|
1368
1382
|
throw err;
|
|
1369
1383
|
}
|
|
1370
|
-
|
|
1371
|
-
try {
|
|
1372
|
-
await assignVmIdToIP(resource.guestIp, vmId);
|
|
1373
|
-
} catch (err) {
|
|
1384
|
+
assignVmIdToIP(resource.guestIp, vmId).catch((err) => {
|
|
1374
1385
|
logger4.error(
|
|
1375
1386
|
`Failed to assign vmId to IP registry: ${err instanceof Error ? err.message : "Unknown"}`
|
|
1376
1387
|
);
|
|
1377
|
-
}
|
|
1388
|
+
});
|
|
1378
1389
|
logger4.log(
|
|
1379
1390
|
`Acquired: TAP ${resource.tapDevice}, MAC ${guestMac}, IP ${resource.guestIp}`
|
|
1380
1391
|
);
|
|
@@ -1695,12 +1706,9 @@ var FirecrackerVM = class {
|
|
|
1695
1706
|
/**
|
|
1696
1707
|
* Stop the VM
|
|
1697
1708
|
*
|
|
1698
|
-
* Note: With --no-api mode, we
|
|
1699
|
-
*
|
|
1700
|
-
*
|
|
1701
|
-
* TODO(#2118): Implement graceful shutdown via vsock command to guest agent.
|
|
1702
|
-
* This would allow the guest to clean up before termination without
|
|
1703
|
-
* adding the startup latency of API mode.
|
|
1709
|
+
* Note: With --no-api mode, we force kill the Firecracker process.
|
|
1710
|
+
* Graceful shutdown (filesystem sync) should be done via vsock
|
|
1711
|
+
* before calling this method.
|
|
1704
1712
|
*/
|
|
1705
1713
|
async stop() {
|
|
1706
1714
|
if (this.state !== "running") {
|
|
@@ -1794,6 +1802,8 @@ var MSG_WRITE_FILE = 5;
|
|
|
1794
1802
|
var MSG_SPAWN_WATCH = 7;
|
|
1795
1803
|
var MSG_SPAWN_WATCH_RESULT = 8;
|
|
1796
1804
|
var MSG_PROCESS_EXIT = 9;
|
|
1805
|
+
var MSG_SHUTDOWN = 10;
|
|
1806
|
+
var MSG_SHUTDOWN_ACK = 11;
|
|
1797
1807
|
var MSG_ERROR = 255;
|
|
1798
1808
|
var FLAG_SUDO = 1;
|
|
1799
1809
|
function encode(type, seq, payload = Buffer.alloc(0)) {
|
|
@@ -2295,6 +2305,30 @@ var VsockClient = class {
|
|
|
2295
2305
|
getVsockPath() {
|
|
2296
2306
|
return this.vsockPath;
|
|
2297
2307
|
}
|
|
2308
|
+
/**
|
|
2309
|
+
* Request graceful shutdown from guest
|
|
2310
|
+
*
|
|
2311
|
+
* Sends shutdown command to guest agent, which syncs filesystems
|
|
2312
|
+
* and returns acknowledgment. Falls back gracefully on timeout.
|
|
2313
|
+
*
|
|
2314
|
+
* @param timeoutMs Maximum time to wait for acknowledgment (default: 2000ms)
|
|
2315
|
+
* @returns true if guest acknowledged, false on timeout/error
|
|
2316
|
+
*/
|
|
2317
|
+
async shutdown(timeoutMs = 2e3) {
|
|
2318
|
+
if (!this.connected || !this.socket) {
|
|
2319
|
+
return false;
|
|
2320
|
+
}
|
|
2321
|
+
try {
|
|
2322
|
+
const response = await this.request(
|
|
2323
|
+
MSG_SHUTDOWN,
|
|
2324
|
+
Buffer.alloc(0),
|
|
2325
|
+
timeoutMs
|
|
2326
|
+
);
|
|
2327
|
+
return response.type === MSG_SHUTDOWN_ACK;
|
|
2328
|
+
} catch {
|
|
2329
|
+
return false;
|
|
2330
|
+
}
|
|
2331
|
+
}
|
|
2298
2332
|
/**
|
|
2299
2333
|
* Close the connection
|
|
2300
2334
|
*/
|
|
@@ -7744,7 +7778,9 @@ var c11 = initContract();
|
|
|
7744
7778
|
var modelProviderTypeSchema = z17.enum([
|
|
7745
7779
|
"claude-code-oauth-token",
|
|
7746
7780
|
"anthropic-api-key",
|
|
7747
|
-
"
|
|
7781
|
+
"openrouter-api-key",
|
|
7782
|
+
"moonshot-api-key",
|
|
7783
|
+
"minimax-api-key"
|
|
7748
7784
|
]);
|
|
7749
7785
|
var modelProviderFrameworkSchema = z17.enum(["claude-code", "codex"]);
|
|
7750
7786
|
var modelProviderResponseSchema = z17.object({
|
|
@@ -7873,6 +7909,27 @@ var modelProvidersSetDefaultContract = c11.router({
|
|
|
7873
7909
|
summary: "Set a model provider as default for its framework"
|
|
7874
7910
|
}
|
|
7875
7911
|
});
|
|
7912
|
+
var updateModelRequestSchema = z17.object({
|
|
7913
|
+
selectedModel: z17.string().optional()
|
|
7914
|
+
});
|
|
7915
|
+
var modelProvidersUpdateModelContract = c11.router({
|
|
7916
|
+
updateModel: {
|
|
7917
|
+
method: "PATCH",
|
|
7918
|
+
path: "/api/model-providers/:type/model",
|
|
7919
|
+
headers: authHeadersSchema,
|
|
7920
|
+
pathParams: z17.object({
|
|
7921
|
+
type: modelProviderTypeSchema
|
|
7922
|
+
}),
|
|
7923
|
+
body: updateModelRequestSchema,
|
|
7924
|
+
responses: {
|
|
7925
|
+
200: modelProviderResponseSchema,
|
|
7926
|
+
401: apiErrorSchema,
|
|
7927
|
+
404: apiErrorSchema,
|
|
7928
|
+
500: apiErrorSchema
|
|
7929
|
+
},
|
|
7930
|
+
summary: "Update model selection for an existing provider"
|
|
7931
|
+
}
|
|
7932
|
+
});
|
|
7876
7933
|
|
|
7877
7934
|
// ../../packages/core/src/contracts/sessions.ts
|
|
7878
7935
|
import { z as z18 } from "zod";
|
|
@@ -8308,7 +8365,10 @@ var platformLogStatusSchema = z22.enum([
|
|
|
8308
8365
|
"cancelled"
|
|
8309
8366
|
]);
|
|
8310
8367
|
var platformLogEntrySchema = z22.object({
|
|
8311
|
-
id: z22.string().uuid()
|
|
8368
|
+
id: z22.string().uuid(),
|
|
8369
|
+
agentName: z22.string(),
|
|
8370
|
+
status: platformLogStatusSchema,
|
|
8371
|
+
createdAt: z22.string()
|
|
8312
8372
|
});
|
|
8313
8373
|
var platformLogsListResponseSchema = z22.object({
|
|
8314
8374
|
data: z22.array(platformLogEntrySchema),
|
|
@@ -10014,9 +10074,6 @@ async function uploadNetworkLogs(apiUrl, sandboxToken, runId) {
|
|
|
10014
10074
|
|
|
10015
10075
|
// src/lib/executor.ts
|
|
10016
10076
|
var logger10 = createLogger("Executor");
|
|
10017
|
-
function getVmIdFromRunId(runId) {
|
|
10018
|
-
return runId.split("-")[0] || runId.substring(0, 8);
|
|
10019
|
-
}
|
|
10020
10077
|
async function executeJob(context, config, options = {}) {
|
|
10021
10078
|
setSandboxContext({
|
|
10022
10079
|
apiUrl: config.server.url,
|
|
@@ -10030,9 +10087,10 @@ async function executeJob(context, config, options = {}) {
|
|
|
10030
10087
|
success: true
|
|
10031
10088
|
});
|
|
10032
10089
|
}
|
|
10033
|
-
const vmId =
|
|
10090
|
+
const vmId = createVmId(context.runId);
|
|
10034
10091
|
let vm = null;
|
|
10035
10092
|
let guestIp = null;
|
|
10093
|
+
let vsockClient = null;
|
|
10036
10094
|
logger10.log(`Starting job ${context.runId} in VM ${vmId}`);
|
|
10037
10095
|
try {
|
|
10038
10096
|
const vmConfig = {
|
|
@@ -10053,14 +10111,12 @@ async function executeJob(context, config, options = {}) {
|
|
|
10053
10111
|
}
|
|
10054
10112
|
logger10.log(`VM ${vmId} started, guest IP: ${guestIp}`);
|
|
10055
10113
|
const vsockPath = vm.getVsockPath();
|
|
10056
|
-
|
|
10114
|
+
vsockClient = new VsockClient(vsockPath);
|
|
10115
|
+
const guest = vsockClient;
|
|
10057
10116
|
logger10.log(`Using vsock for guest communication: ${vsockPath}`);
|
|
10058
|
-
|
|
10059
|
-
|
|
10060
|
-
"guest_wait",
|
|
10061
|
-
() => guest.waitForGuestConnection(3e4)
|
|
10117
|
+
const envJson = JSON.stringify(
|
|
10118
|
+
buildEnvironmentVariables(context, config.server.url)
|
|
10062
10119
|
);
|
|
10063
|
-
logger10.log(`Guest client ready`);
|
|
10064
10120
|
const firewallConfig = context.experimentalFirewall;
|
|
10065
10121
|
if (firewallConfig?.enabled) {
|
|
10066
10122
|
const mitmEnabled = firewallConfig.experimental_mitm ?? false;
|
|
@@ -10068,19 +10124,18 @@ async function executeJob(context, config, options = {}) {
|
|
|
10068
10124
|
logger10.log(
|
|
10069
10125
|
`Setting up network security for VM ${guestIp} (mitm=${mitmEnabled}, sealSecrets=${sealSecretsEnabled})`
|
|
10070
10126
|
);
|
|
10071
|
-
|
|
10072
|
-
|
|
10073
|
-
|
|
10074
|
-
|
|
10075
|
-
context.sandboxToken,
|
|
10076
|
-
{
|
|
10077
|
-
firewallRules: firewallConfig?.rules,
|
|
10078
|
-
mitmEnabled,
|
|
10079
|
-
sealSecretsEnabled
|
|
10080
|
-
}
|
|
10081
|
-
);
|
|
10127
|
+
getVMRegistry().register(guestIp, context.runId, context.sandboxToken, {
|
|
10128
|
+
firewallRules: firewallConfig?.rules,
|
|
10129
|
+
mitmEnabled,
|
|
10130
|
+
sealSecretsEnabled
|
|
10082
10131
|
});
|
|
10083
10132
|
}
|
|
10133
|
+
logger10.log(`Waiting for guest connection...`);
|
|
10134
|
+
await withSandboxTiming(
|
|
10135
|
+
"guest_wait",
|
|
10136
|
+
() => guest.waitForGuestConnection(3e4)
|
|
10137
|
+
);
|
|
10138
|
+
logger10.log(`Guest client ready`);
|
|
10084
10139
|
if (context.storageManifest) {
|
|
10085
10140
|
await withSandboxTiming(
|
|
10086
10141
|
"storage_download",
|
|
@@ -10098,8 +10153,6 @@ async function executeJob(context, config, options = {}) {
|
|
|
10098
10153
|
)
|
|
10099
10154
|
);
|
|
10100
10155
|
}
|
|
10101
|
-
const envVars = buildEnvironmentVariables(context, config.server.url);
|
|
10102
|
-
const envJson = JSON.stringify(envVars);
|
|
10103
10156
|
logger10.log(
|
|
10104
10157
|
`Writing env JSON (${envJson.length} bytes) to ${ENV_JSON_PATH}`
|
|
10105
10158
|
);
|
|
@@ -10196,6 +10249,15 @@ async function executeJob(context, config, options = {}) {
|
|
|
10196
10249
|
}
|
|
10197
10250
|
}
|
|
10198
10251
|
if (vm) {
|
|
10252
|
+
if (vsockClient) {
|
|
10253
|
+
const acked = await vsockClient.shutdown(2e3);
|
|
10254
|
+
if (acked) {
|
|
10255
|
+
logger10.log(`Guest acknowledged shutdown`);
|
|
10256
|
+
} else {
|
|
10257
|
+
logger10.log(`Guest shutdown timeout, proceeding with SIGKILL`);
|
|
10258
|
+
}
|
|
10259
|
+
vsockClient.close();
|
|
10260
|
+
}
|
|
10199
10261
|
logger10.log(`Cleaning up VM ${vmId}...`);
|
|
10200
10262
|
await withSandboxTiming("cleanup", () => vm.kill());
|
|
10201
10263
|
}
|
|
@@ -10667,7 +10729,7 @@ function parseFirecrackerCmdline(cmdline) {
|
|
|
10667
10729
|
if (sockIdx === -1 || !socketPath) return null;
|
|
10668
10730
|
const match = socketPath.match(/vm0-([a-f0-9]+)\/firecracker\.sock$/);
|
|
10669
10731
|
if (!match?.[1]) return null;
|
|
10670
|
-
return { vmId: match[1], socketPath };
|
|
10732
|
+
return { vmId: createVmId(match[1]), socketPath };
|
|
10671
10733
|
}
|
|
10672
10734
|
function parseMitmproxyCmdline(cmdline) {
|
|
10673
10735
|
if (!cmdline.includes("mitmproxy") && !cmdline.includes("mitmdump")) {
|
|
@@ -10707,7 +10769,8 @@ function findFirecrackerProcesses() {
|
|
|
10707
10769
|
}
|
|
10708
10770
|
function findProcessByVmId(vmId) {
|
|
10709
10771
|
const processes = findFirecrackerProcesses();
|
|
10710
|
-
|
|
10772
|
+
const vmIdStr = vmIdValue(vmId);
|
|
10773
|
+
return processes.find((p) => vmIdValue(p.vmId) === vmIdStr) || null;
|
|
10711
10774
|
}
|
|
10712
10775
|
function isProcessRunning3(pid) {
|
|
10713
10776
|
try {
|
|
@@ -10842,8 +10905,7 @@ var doctorCommand = new Command2("doctor").description("Diagnose runner health,
|
|
|
10842
10905
|
const allocations = getAllocations();
|
|
10843
10906
|
if (status?.active_run_ids) {
|
|
10844
10907
|
for (const runId of status.active_run_ids) {
|
|
10845
|
-
const vmId = runId
|
|
10846
|
-
if (!vmId) continue;
|
|
10908
|
+
const vmId = createVmId(runId);
|
|
10847
10909
|
statusVmIds.add(vmId);
|
|
10848
10910
|
const proc = processes.find((p) => p.vmId === vmId);
|
|
10849
10911
|
const ip = getIPForVm(vmId) ?? "not allocated";
|
|
@@ -11088,8 +11150,7 @@ var killCommand = new Command3("kill").description("Force terminate a run and cl
|
|
|
11088
11150
|
);
|
|
11089
11151
|
function resolveRunId(input, statusFilePath) {
|
|
11090
11152
|
if (input.includes("-")) {
|
|
11091
|
-
|
|
11092
|
-
return { vmId: vmId ?? input, runId: input };
|
|
11153
|
+
return { vmId: createVmId(input), runId: input };
|
|
11093
11154
|
}
|
|
11094
11155
|
if (existsSync5(statusFilePath)) {
|
|
11095
11156
|
try {
|
|
@@ -11100,12 +11161,12 @@ function resolveRunId(input, statusFilePath) {
|
|
|
11100
11161
|
(id) => id.startsWith(input)
|
|
11101
11162
|
);
|
|
11102
11163
|
if (match) {
|
|
11103
|
-
return { vmId:
|
|
11164
|
+
return { vmId: createVmId(match), runId: match };
|
|
11104
11165
|
}
|
|
11105
11166
|
} catch {
|
|
11106
11167
|
}
|
|
11107
11168
|
}
|
|
11108
|
-
return { vmId: input, runId: null };
|
|
11169
|
+
return { vmId: createVmId(input), runId: null };
|
|
11109
11170
|
}
|
|
11110
11171
|
async function confirm(message) {
|
|
11111
11172
|
const rl = readline2.createInterface({
|
|
@@ -11232,7 +11293,7 @@ var benchmarkCommand = new Command4("benchmark").description(
|
|
|
11232
11293
|
});
|
|
11233
11294
|
|
|
11234
11295
|
// src/index.ts
|
|
11235
|
-
var version = true ? "3.
|
|
11296
|
+
var version = true ? "3.10.1" : "0.1.0";
|
|
11236
11297
|
program.name("vm0-runner").version(version).description("Self-hosted runner for VM0 agents");
|
|
11237
11298
|
program.addCommand(startCommand);
|
|
11238
11299
|
program.addCommand(doctorCommand);
|