@vm0/runner 3.12.0 → 3.12.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 +152 -63
  2. package/package.json +1 -1
package/index.js CHANGED
@@ -416,6 +416,37 @@ async function withFileLock(path9, fn, options) {
416
416
  }
417
417
  }
418
418
 
419
+ // src/lib/utils/process.ts
420
+ import { execSync } from "child_process";
421
+ function isProcessRunning(pid) {
422
+ try {
423
+ process.kill(pid, 0);
424
+ return true;
425
+ } catch (err) {
426
+ if (err instanceof Error && "code" in err && err.code === "EPERM") {
427
+ return true;
428
+ }
429
+ return false;
430
+ }
431
+ }
432
+ function killProcessTree(pid) {
433
+ try {
434
+ const childPidsStr = execSync(`pgrep -P ${pid} 2>/dev/null || true`, {
435
+ encoding: "utf-8"
436
+ }).trim();
437
+ if (childPidsStr) {
438
+ const childPids = childPidsStr.split("\n").map((p) => parseInt(p, 10));
439
+ for (const childPid of childPids) {
440
+ if (!isNaN(childPid)) {
441
+ killProcessTree(childPid);
442
+ }
443
+ }
444
+ }
445
+ process.kill(pid, "SIGKILL");
446
+ } catch {
447
+ }
448
+ }
449
+
419
450
  // src/lib/utils/exec.ts
420
451
  import { exec } from "child_process";
421
452
  import { promisify } from "util";
@@ -549,14 +580,6 @@ function makeNsName(runnerIdx, nsIdx) {
549
580
  function makeVethName(runnerIdx, nsIdx) {
550
581
  return `${VETH_PREFIX}${runnerIdx}-${nsIdx}`;
551
582
  }
552
- function isPidAlive(pid) {
553
- try {
554
- process.kill(pid, 0);
555
- return true;
556
- } catch {
557
- return false;
558
- }
559
- }
560
583
  async function deleteIptablesRulesByComment(comment) {
561
584
  const deleteFromTable = async (table) => {
562
585
  try {
@@ -625,7 +648,7 @@ var NetnsPool = class _NetnsPool {
625
648
  const data = read();
626
649
  const orphaned = [];
627
650
  for (const [runnerIdx, runner] of Object.entries(data.runners)) {
628
- if (!isPidAlive(runner.pid)) {
651
+ if (!isProcessRunning(runner.pid)) {
629
652
  orphaned.push({
630
653
  runnerIdx,
631
654
  namespaces: Object.entries(runner.namespaces).map(
@@ -662,7 +685,7 @@ var NetnsPool = class _NetnsPool {
662
685
  const data = read();
663
686
  for (const { runnerIdx } of orphanedData) {
664
687
  const runner = data.runners[runnerIdx];
665
- if (runner && !isPidAlive(runner.pid)) {
688
+ if (runner && !isProcessRunning(runner.pid)) {
666
689
  delete data.runners[runnerIdx];
667
690
  }
668
691
  }
@@ -1671,8 +1694,8 @@ var FirecrackerVM = class {
1671
1694
  * since we want to clean up as much as possible even if some parts fail.
1672
1695
  */
1673
1696
  async cleanup() {
1674
- if (this.process && !this.process.killed) {
1675
- this.process.kill("SIGKILL");
1697
+ if (this.process && !this.process.killed && this.process.pid) {
1698
+ killProcessTree(this.process.pid);
1676
1699
  this.process = null;
1677
1700
  }
1678
1701
  if (this.netns) {
@@ -7884,6 +7907,7 @@ var modelProviderTypeSchema = z19.enum([
7884
7907
  "minimax-api-key",
7885
7908
  "deepseek-api-key",
7886
7909
  "zai-api-key",
7910
+ "azure-foundry",
7887
7911
  "aws-bedrock"
7888
7912
  ]);
7889
7913
  var modelProviderFrameworkSchema = z19.enum(["claude-code", "codex"]);
@@ -10472,12 +10496,12 @@ function createStatusUpdater(statusFilePath, state) {
10472
10496
  }
10473
10497
 
10474
10498
  // src/lib/firecracker/network.ts
10475
- import { execSync, exec as exec3 } from "child_process";
10499
+ import { execSync as execSync2, exec as exec3 } from "child_process";
10476
10500
  import { promisify as promisify3 } from "util";
10477
10501
  var execAsync3 = promisify3(exec3);
10478
10502
  function commandExists(cmd) {
10479
10503
  try {
10480
- execSync(`which ${cmd}`, { stdio: "ignore" });
10504
+ execSync2(`which ${cmd}`, { stdio: "ignore" });
10481
10505
  return true;
10482
10506
  } catch {
10483
10507
  return false;
@@ -10492,7 +10516,7 @@ function checkNetworkPrerequisites() {
10492
10516
  }
10493
10517
  }
10494
10518
  try {
10495
- execSync("sudo -n true 2>/dev/null", { stdio: "ignore" });
10519
+ execSync2("sudo -n true 2>/dev/null", { stdio: "ignore" });
10496
10520
  } catch {
10497
10521
  errors.push(
10498
10522
  "Root/sudo access required for network configuration. Please run with sudo or configure sudoers."
@@ -10518,17 +10542,6 @@ import path7 from "path";
10518
10542
  var logger11 = createLogger("RunnerLock");
10519
10543
  var DEFAULT_PID_FILE = runtimePaths.runnerPid;
10520
10544
  var currentPidFile = null;
10521
- function isProcessRunning(pid) {
10522
- try {
10523
- process.kill(pid, 0);
10524
- return true;
10525
- } catch (err) {
10526
- if (err instanceof Error && "code" in err && err.code === "EPERM") {
10527
- return true;
10528
- }
10529
- return false;
10530
- }
10531
- }
10532
10545
  function acquireRunnerLock(options = {}) {
10533
10546
  const pidFile = options.pidFile ?? DEFAULT_PID_FILE;
10534
10547
  const runDir = path7.dirname(pidFile);
@@ -10909,6 +10922,7 @@ var startCommand = new Command("start").description("Start the runner").option("
10909
10922
  // src/commands/doctor.ts
10910
10923
  import { Command as Command2 } from "commander";
10911
10924
  import { existsSync as existsSync5, readFileSync as readFileSync3, readdirSync as readdirSync2 } from "fs";
10925
+ import { execSync as execSync3 } from "child_process";
10912
10926
 
10913
10927
  // src/lib/firecracker/process.ts
10914
10928
  import { readdirSync, readFileSync as readFileSync2, existsSync as existsSync4 } from "fs";
@@ -10916,12 +10930,21 @@ import path8 from "path";
10916
10930
  function parseFirecrackerCmdline(cmdline) {
10917
10931
  const args = cmdline.split("\0");
10918
10932
  if (!args[0]?.includes("firecracker")) return null;
10933
+ let filePath;
10919
10934
  const sockIdx = args.indexOf("--api-sock");
10920
- const socketPath = args[sockIdx + 1];
10921
- if (sockIdx === -1 || !socketPath) return null;
10922
- const match = socketPath.match(/vm0-([a-f0-9]+)\/firecracker\.sock$/);
10935
+ if (sockIdx !== -1) {
10936
+ filePath = args[sockIdx + 1];
10937
+ }
10938
+ if (!filePath) {
10939
+ const configIdx = args.indexOf("--config-file");
10940
+ if (configIdx !== -1) {
10941
+ filePath = args[configIdx + 1];
10942
+ }
10943
+ }
10944
+ if (!filePath) return null;
10945
+ const match = filePath.match(/vm0-([a-f0-9]+)\//);
10923
10946
  if (!match?.[1]) return null;
10924
- return { vmId: createVmId(match[1]), socketPath };
10947
+ return createVmId(match[1]);
10925
10948
  }
10926
10949
  function parseMitmproxyCmdline(cmdline) {
10927
10950
  if (!cmdline.includes("mitmproxy") && !cmdline.includes("mitmdump")) {
@@ -10949,9 +10972,9 @@ function findFirecrackerProcesses() {
10949
10972
  if (!existsSync4(cmdlinePath)) continue;
10950
10973
  try {
10951
10974
  const cmdline = readFileSync2(cmdlinePath, "utf-8");
10952
- const parsed = parseFirecrackerCmdline(cmdline);
10953
- if (parsed) {
10954
- processes.push({ pid, ...parsed });
10975
+ const vmId = parseFirecrackerCmdline(cmdline);
10976
+ if (vmId) {
10977
+ processes.push({ pid, vmId });
10955
10978
  }
10956
10979
  } catch {
10957
10980
  continue;
@@ -10964,33 +10987,25 @@ function findProcessByVmId(vmId) {
10964
10987
  const vmIdStr = vmIdValue(vmId);
10965
10988
  return processes.find((p) => vmIdValue(p.vmId) === vmIdStr) || null;
10966
10989
  }
10967
- function isProcessRunning2(pid) {
10968
- try {
10969
- process.kill(pid, 0);
10970
- return true;
10971
- } catch {
10972
- return false;
10973
- }
10974
- }
10975
10990
  async function killProcess(pid, timeoutMs = 5e3) {
10976
- if (!isProcessRunning2(pid)) return true;
10991
+ if (!isProcessRunning(pid)) return true;
10977
10992
  try {
10978
10993
  process.kill(pid, "SIGTERM");
10979
10994
  } catch {
10980
- return !isProcessRunning2(pid);
10995
+ return !isProcessRunning(pid);
10981
10996
  }
10982
10997
  const startTime = Date.now();
10983
10998
  while (Date.now() - startTime < timeoutMs) {
10984
- if (!isProcessRunning2(pid)) return true;
10999
+ if (!isProcessRunning(pid)) return true;
10985
11000
  await new Promise((resolve) => setTimeout(resolve, 100));
10986
11001
  }
10987
- if (isProcessRunning2(pid)) {
11002
+ if (isProcessRunning(pid)) {
10988
11003
  try {
10989
11004
  process.kill(pid, "SIGKILL");
10990
11005
  } catch {
10991
11006
  }
10992
11007
  }
10993
- return !isProcessRunning2(pid);
11008
+ return !isProcessRunning(pid);
10994
11009
  }
10995
11010
  function findMitmproxyProcess() {
10996
11011
  const procDir = "/proc";
@@ -11018,15 +11033,26 @@ function findMitmproxyProcess() {
11018
11033
  return null;
11019
11034
  }
11020
11035
 
11036
+ // src/lib/runner/types.ts
11037
+ import { z as z30 } from "zod";
11038
+ var RunnerModeSchema = z30.enum(["running", "draining", "stopping", "stopped"]);
11039
+ var RunnerStatusSchema = z30.object({
11040
+ mode: RunnerModeSchema,
11041
+ active_runs: z30.number(),
11042
+ active_run_ids: z30.array(z30.string()),
11043
+ started_at: z30.string(),
11044
+ updated_at: z30.string()
11045
+ });
11046
+
11021
11047
  // src/commands/doctor.ts
11022
- function displayRunnerStatus(statusFilePath) {
11048
+ function displayRunnerStatus(statusFilePath, warnings) {
11023
11049
  if (!existsSync5(statusFilePath)) {
11024
11050
  console.log("Mode: unknown (no status.json)");
11025
11051
  return null;
11026
11052
  }
11027
11053
  try {
11028
- const status = JSON.parse(
11029
- readFileSync3(statusFilePath, "utf-8")
11054
+ const status = RunnerStatusSchema.parse(
11055
+ JSON.parse(readFileSync3(statusFilePath, "utf-8"))
11030
11056
  );
11031
11057
  console.log(`Mode: ${status.mode}`);
11032
11058
  if (status.started_at) {
@@ -11037,10 +11063,11 @@ function displayRunnerStatus(statusFilePath) {
11037
11063
  return status;
11038
11064
  } catch {
11039
11065
  console.log("Mode: unknown (status.json unreadable)");
11066
+ warnings.push({ message: "status.json exists but cannot be parsed" });
11040
11067
  return null;
11041
11068
  }
11042
11069
  }
11043
- async function checkApiConnectivity(config) {
11070
+ async function checkApiConnectivity(config, warnings) {
11044
11071
  console.log("API Connectivity:");
11045
11072
  try {
11046
11073
  await pollForJob(config.server, config.group);
@@ -11051,6 +11078,9 @@ async function checkApiConnectivity(config) {
11051
11078
  console.log(
11052
11079
  ` Error: ${error instanceof Error ? error.message : "Unknown error"}`
11053
11080
  );
11081
+ warnings.push({
11082
+ message: `Cannot connect to API: ${error instanceof Error ? error.message : "Unknown error"}`
11083
+ });
11054
11084
  }
11055
11085
  }
11056
11086
  async function checkNetwork(config, warnings) {
@@ -11086,8 +11116,7 @@ function buildJobInfo(status, processes) {
11086
11116
  jobs.push({
11087
11117
  runId,
11088
11118
  vmId,
11089
- hasProcess: !!proc,
11090
- pid: proc?.pid
11119
+ firecrackerPid: proc?.pid
11091
11120
  });
11092
11121
  }
11093
11122
  }
@@ -11101,13 +11130,61 @@ function displayRuns(jobs, maxConcurrent) {
11101
11130
  }
11102
11131
  console.log(" Run ID VM ID Status");
11103
11132
  for (const job of jobs) {
11104
- const statusText = job.hasProcess ? `\u2713 Running (PID ${job.pid})` : "\u26A0\uFE0F No process";
11133
+ const statusText = job.firecrackerPid ? `\u2713 Running (PID ${job.firecrackerPid})` : "\u26A0\uFE0F No process";
11105
11134
  console.log(` ${job.runId} ${job.vmId} ${statusText}`);
11106
11135
  }
11107
11136
  }
11108
- function detectOrphanResources(jobs, processes, workspaces, statusVmIds, warnings) {
11137
+ async function findOrphanNetworkNamespaces(warnings) {
11138
+ let allNamespaces = [];
11139
+ try {
11140
+ const output = execSync3("ip netns list 2>/dev/null || true", {
11141
+ encoding: "utf-8"
11142
+ });
11143
+ allNamespaces = output.split("\n").map((line) => line.split(" ")[0] ?? "").filter((ns) => ns.startsWith(NS_PREFIX));
11144
+ } catch (err) {
11145
+ warnings.push({
11146
+ message: `Failed to list network namespaces: ${err instanceof Error ? err.message : "Unknown error"}`
11147
+ });
11148
+ return [];
11149
+ }
11150
+ if (allNamespaces.length === 0) {
11151
+ return [];
11152
+ }
11153
+ const registryPath = runtimePaths.netnsRegistry;
11154
+ if (!existsSync5(registryPath)) {
11155
+ return allNamespaces;
11156
+ }
11157
+ try {
11158
+ return await withFileLock(registryPath, async () => {
11159
+ const registry = RegistrySchema.parse(
11160
+ JSON.parse(readFileSync3(registryPath, "utf-8"))
11161
+ );
11162
+ const aliveNamespaces = /* @__PURE__ */ new Set();
11163
+ for (const [runnerIdx, runner] of Object.entries(registry.runners)) {
11164
+ if (isProcessRunning(runner.pid)) {
11165
+ for (const nsIdx of Object.keys(runner.namespaces)) {
11166
+ aliveNamespaces.add(`${NS_PREFIX}${runnerIdx}-${nsIdx}`);
11167
+ }
11168
+ }
11169
+ }
11170
+ const orphans = [];
11171
+ for (const ns of allNamespaces) {
11172
+ if (!aliveNamespaces.has(ns)) {
11173
+ orphans.push(ns);
11174
+ }
11175
+ }
11176
+ return orphans;
11177
+ });
11178
+ } catch (err) {
11179
+ warnings.push({
11180
+ message: `Failed to read netns registry: ${err instanceof Error ? err.message : "Unknown error"}`
11181
+ });
11182
+ return [];
11183
+ }
11184
+ }
11185
+ async function detectOrphanResources(jobs, processes, workspaces, statusVmIds, warnings) {
11109
11186
  for (const job of jobs) {
11110
- if (!job.hasProcess) {
11187
+ if (!job.firecrackerPid) {
11111
11188
  warnings.push({
11112
11189
  message: `Run ${job.vmId} in status.json but no Firecracker process running`
11113
11190
  });
@@ -11121,6 +11198,12 @@ function detectOrphanResources(jobs, processes, workspaces, statusVmIds, warning
11121
11198
  });
11122
11199
  }
11123
11200
  }
11201
+ const orphanNetns = await findOrphanNetworkNamespaces(warnings);
11202
+ for (const ns of orphanNetns) {
11203
+ warnings.push({
11204
+ message: `Orphan network namespace: ${ns} (runner process not running)`
11205
+ });
11206
+ }
11124
11207
  for (const ws of workspaces) {
11125
11208
  const vmId = runnerPaths.extractVmId(ws);
11126
11209
  if (!processVmIds.has(vmId) && !statusVmIds.has(vmId)) {
@@ -11157,9 +11240,9 @@ var doctorCommand = new Command2("doctor").description("Diagnose runner health,
11157
11240
  const workspacesDir = runnerPaths.workspacesDir(config.base_dir);
11158
11241
  const warnings = [];
11159
11242
  console.log(`Runner: ${config.name}`);
11160
- const status = displayRunnerStatus(statusFilePath);
11243
+ const status = displayRunnerStatus(statusFilePath, warnings);
11161
11244
  console.log("");
11162
- await checkApiConnectivity(config);
11245
+ await checkApiConnectivity(config, warnings);
11163
11246
  console.log("");
11164
11247
  await checkNetwork(config, warnings);
11165
11248
  console.log("");
@@ -11168,7 +11251,13 @@ var doctorCommand = new Command2("doctor").description("Diagnose runner health,
11168
11251
  const { jobs, statusVmIds } = buildJobInfo(status, processes);
11169
11252
  displayRuns(jobs, config.sandbox.max_concurrent);
11170
11253
  console.log("");
11171
- detectOrphanResources(jobs, processes, workspaces, statusVmIds, warnings);
11254
+ await detectOrphanResources(
11255
+ jobs,
11256
+ processes,
11257
+ workspaces,
11258
+ statusVmIds,
11259
+ warnings
11260
+ );
11172
11261
  displayWarnings(warnings);
11173
11262
  process.exit(warnings.length > 0 ? 1 : 0);
11174
11263
  } catch (error) {
@@ -11251,8 +11340,8 @@ var killCommand = new Command3("kill").description("Force terminate a run and cl
11251
11340
  }
11252
11341
  if (runId && existsSync6(statusFilePath)) {
11253
11342
  try {
11254
- const status = JSON.parse(
11255
- readFileSync4(statusFilePath, "utf-8")
11343
+ const status = RunnerStatusSchema.parse(
11344
+ JSON.parse(readFileSync4(statusFilePath, "utf-8"))
11256
11345
  );
11257
11346
  const oldCount = status.active_runs;
11258
11347
  status.active_run_ids = status.active_run_ids.filter(
@@ -11309,8 +11398,8 @@ function resolveRunId(input, statusFilePath) {
11309
11398
  }
11310
11399
  if (existsSync6(statusFilePath)) {
11311
11400
  try {
11312
- const status = JSON.parse(
11313
- readFileSync4(statusFilePath, "utf-8")
11401
+ const status = RunnerStatusSchema.parse(
11402
+ JSON.parse(readFileSync4(statusFilePath, "utf-8"))
11314
11403
  );
11315
11404
  const match = status.active_run_ids.find(
11316
11405
  (id) => id.startsWith(input)
@@ -11658,7 +11747,7 @@ var snapshotCommand = new Command5("snapshot").description("Generate a Firecrack
11658
11747
  );
11659
11748
 
11660
11749
  // src/index.ts
11661
- var version = true ? "3.12.0" : "0.1.0";
11750
+ var version = true ? "3.12.1" : "0.1.0";
11662
11751
  program.name("vm0-runner").version(version).description("Self-hosted runner for VM0 agents");
11663
11752
  program.addCommand(startCommand);
11664
11753
  program.addCommand(doctorCommand);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vm0/runner",
3
- "version": "3.12.0",
3
+ "version": "3.12.1",
4
4
  "description": "Self-hosted runner for VM0 agents",
5
5
  "repository": {
6
6
  "type": "git",