@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.
Files changed (2) hide show
  1. package/index.js +113 -52
  2. 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 === 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 this.config.setMac(resource.tapDevice, guestMac);
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 set failure: ${resource.tapDevice}`
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
- await clearArpEntry(resource.guestIp);
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 can only force kill the process.
1699
- * The VM doesn't have an API endpoint for graceful shutdown.
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
- "moonshot-api-key"
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 = getVmIdFromRunId(context.runId);
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
- const guest = new VsockClient(vsockPath);
10114
+ vsockClient = new VsockClient(vsockPath);
10115
+ const guest = vsockClient;
10057
10116
  logger10.log(`Using vsock for guest communication: ${vsockPath}`);
10058
- logger10.log(`Waiting for guest connection...`);
10059
- await withSandboxTiming(
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
- await withSandboxTiming("network_setup", async () => {
10072
- getVMRegistry().register(
10073
- guestIp,
10074
- context.runId,
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
- return processes.find((p) => p.vmId === vmId) || null;
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.split("-")[0];
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
- const vmId = input.split("-")[0];
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: input, runId: match };
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.9.4" : "0.1.0";
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);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vm0/runner",
3
- "version": "3.9.4",
3
+ "version": "3.10.1",
4
4
  "description": "Self-hosted runner for VM0 agents",
5
5
  "repository": {
6
6
  "type": "git",