@vm0/runner 2.13.4 → 2.14.0

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 +340 -329
  2. package/package.json +1 -1
package/index.js CHANGED
@@ -5638,6 +5638,8 @@ var unifiedRunRequestSchema = z6.object({
5638
5638
  volumeVersions: z6.record(z6.string(), z6.string()).optional(),
5639
5639
  // Debug flag to force real Claude in mock environments (internal use only)
5640
5640
  debugNoMockClaude: z6.boolean().optional(),
5641
+ // Model provider for automatic LLM credential injection
5642
+ modelProvider: z6.string().optional(),
5641
5643
  // Required
5642
5644
  prompt: z6.string().min(1, "Missing prompt")
5643
5645
  });
@@ -9316,188 +9318,193 @@ async function executeJob2(context, config) {
9316
9318
  console.log(` Job ${context.runId} reported as ${result.status}`);
9317
9319
  }
9318
9320
  }
9319
- var startCommand = new Command("start").description("Start the runner").option("--config <path>", "Config file path", "./runner.yaml").action(async (options) => {
9320
- try {
9321
- const config = loadConfig(options.config);
9322
- validateFirecrackerPaths(config.firecracker);
9323
- console.log("Config valid");
9324
- const datasetSuffix = process.env.AXIOM_DATASET_SUFFIX;
9325
- if (!datasetSuffix) {
9326
- throw new Error(
9327
- "AXIOM_DATASET_SUFFIX is required. Set to 'dev' or 'prod'."
9328
- );
9329
- }
9330
- initMetrics({
9331
- serviceName: "vm0-runner",
9332
- runnerLabel: config.name,
9333
- axiomToken: process.env.AXIOM_TOKEN,
9334
- environment: datasetSuffix
9335
- });
9336
- const networkCheck = checkNetworkPrerequisites();
9337
- if (!networkCheck.ok) {
9338
- console.error("Network prerequisites not met:");
9339
- for (const error of networkCheck.errors) {
9340
- console.error(` - ${error}`);
9341
- }
9342
- process.exit(1);
9343
- }
9344
- console.log("Setting up network bridge...");
9345
- await setupBridge();
9346
- console.log("Flushing bridge ARP cache...");
9347
- await flushBridgeArpCache();
9348
- console.log("Cleaning up orphaned proxy rules...");
9349
- await cleanupOrphanedProxyRules(config.name);
9350
- console.log("Cleaning up orphaned IP allocations...");
9351
- await cleanupOrphanedAllocations();
9352
- console.log("Initializing network proxy...");
9353
- initVMRegistry();
9354
- const proxyManager = initProxyManager({
9355
- apiUrl: config.server.url,
9356
- port: config.proxy.port,
9357
- caDir: config.proxy.ca_dir
9358
- });
9359
- let proxyEnabled = false;
9321
+ var startCommand = new Command("start").description("Start the runner").option("--config <path>", "Config file path", "./runner.yaml").action(
9322
+ // eslint-disable-next-line complexity -- TODO: refactor complex function
9323
+ async (options) => {
9360
9324
  try {
9361
- await proxyManager.start();
9362
- proxyEnabled = true;
9363
- console.log("Network proxy initialized successfully");
9364
- } catch (err) {
9365
- console.warn(
9366
- `Network proxy not available: ${err instanceof Error ? err.message : "Unknown error"}`
9367
- );
9368
- console.warn(
9369
- "Jobs with experimentalFirewall enabled will run without network interception"
9370
- );
9371
- }
9372
- const statusFilePath = join2(dirname(options.config), "status.json");
9373
- const startedAt = /* @__PURE__ */ new Date();
9374
- const state = { mode: "running" };
9375
- const updateStatus = () => {
9376
- writeStatusFile(statusFilePath, state.mode, startedAt);
9377
- };
9378
- console.log(
9379
- `Starting runner '${config.name}' for group '${config.group}'...`
9380
- );
9381
- console.log(`Max concurrent jobs: ${config.sandbox.max_concurrent}`);
9382
- console.log(`Status file: ${statusFilePath}`);
9383
- console.log("Press Ctrl+C to stop");
9384
- console.log("");
9385
- updateStatus();
9386
- let running = true;
9387
- process.on("SIGINT", () => {
9388
- console.log("\nShutting down...");
9389
- running = false;
9390
- state.mode = "stopped";
9391
- updateStatus();
9392
- });
9393
- process.on("SIGTERM", () => {
9394
- console.log("\nShutting down...");
9395
- running = false;
9396
- state.mode = "stopped";
9397
- updateStatus();
9398
- });
9399
- process.on("SIGUSR1", () => {
9400
- if (state.mode === "running") {
9401
- console.log("\n[Maintenance] Entering drain mode...");
9402
- console.log(
9403
- `[Maintenance] Active jobs: ${activeRuns.size} (will wait for completion)`
9325
+ const config = loadConfig(options.config);
9326
+ validateFirecrackerPaths(config.firecracker);
9327
+ console.log("Config valid");
9328
+ const datasetSuffix = process.env.AXIOM_DATASET_SUFFIX;
9329
+ if (!datasetSuffix) {
9330
+ throw new Error(
9331
+ "AXIOM_DATASET_SUFFIX is required. Set to 'dev' or 'prod'."
9404
9332
  );
9405
- state.mode = "draining";
9406
- updateStatus();
9407
9333
  }
9408
- });
9409
- const jobPromises = /* @__PURE__ */ new Set();
9410
- while (running) {
9411
- if (state.mode === "draining") {
9412
- if (activeRuns.size === 0) {
9413
- console.log("[Maintenance] All jobs completed, exiting drain mode");
9414
- running = false;
9415
- break;
9416
- }
9417
- if (jobPromises.size > 0) {
9418
- await Promise.race(jobPromises);
9419
- updateStatus();
9420
- }
9421
- continue;
9422
- }
9423
- if (activeRuns.size >= config.sandbox.max_concurrent) {
9424
- if (jobPromises.size > 0) {
9425
- await Promise.race(jobPromises);
9426
- updateStatus();
9334
+ initMetrics({
9335
+ serviceName: "vm0-runner",
9336
+ runnerLabel: config.name,
9337
+ axiomToken: process.env.AXIOM_TOKEN,
9338
+ environment: datasetSuffix
9339
+ });
9340
+ const networkCheck = checkNetworkPrerequisites();
9341
+ if (!networkCheck.ok) {
9342
+ console.error("Network prerequisites not met:");
9343
+ for (const error of networkCheck.errors) {
9344
+ console.error(` - ${error}`);
9427
9345
  }
9428
- continue;
9346
+ process.exit(1);
9429
9347
  }
9348
+ console.log("Setting up network bridge...");
9349
+ await setupBridge();
9350
+ console.log("Flushing bridge ARP cache...");
9351
+ await flushBridgeArpCache();
9352
+ console.log("Cleaning up orphaned proxy rules...");
9353
+ await cleanupOrphanedProxyRules(config.name);
9354
+ console.log("Cleaning up orphaned IP allocations...");
9355
+ await cleanupOrphanedAllocations();
9356
+ console.log("Initializing network proxy...");
9357
+ initVMRegistry();
9358
+ const proxyManager = initProxyManager({
9359
+ apiUrl: config.server.url,
9360
+ port: config.proxy.port,
9361
+ caDir: config.proxy.ca_dir
9362
+ });
9363
+ let proxyEnabled = false;
9430
9364
  try {
9431
- const job = await withRunnerTiming(
9432
- "poll",
9433
- () => pollForJob(config.server, config.group)
9365
+ await proxyManager.start();
9366
+ proxyEnabled = true;
9367
+ console.log("Network proxy initialized successfully");
9368
+ } catch (err) {
9369
+ console.warn(
9370
+ `Network proxy not available: ${err instanceof Error ? err.message : "Unknown error"}`
9434
9371
  );
9435
- if (!job) {
9436
- await new Promise(
9437
- (resolve) => setTimeout(resolve, config.sandbox.poll_interval_ms)
9372
+ console.warn(
9373
+ "Jobs with experimentalFirewall enabled will run without network interception"
9374
+ );
9375
+ }
9376
+ const statusFilePath = join2(dirname(options.config), "status.json");
9377
+ const startedAt = /* @__PURE__ */ new Date();
9378
+ const state = { mode: "running" };
9379
+ const updateStatus = () => {
9380
+ writeStatusFile(statusFilePath, state.mode, startedAt);
9381
+ };
9382
+ console.log(
9383
+ `Starting runner '${config.name}' for group '${config.group}'...`
9384
+ );
9385
+ console.log(`Max concurrent jobs: ${config.sandbox.max_concurrent}`);
9386
+ console.log(`Status file: ${statusFilePath}`);
9387
+ console.log("Press Ctrl+C to stop");
9388
+ console.log("");
9389
+ updateStatus();
9390
+ let running = true;
9391
+ process.on("SIGINT", () => {
9392
+ console.log("\nShutting down...");
9393
+ running = false;
9394
+ state.mode = "stopped";
9395
+ updateStatus();
9396
+ });
9397
+ process.on("SIGTERM", () => {
9398
+ console.log("\nShutting down...");
9399
+ running = false;
9400
+ state.mode = "stopped";
9401
+ updateStatus();
9402
+ });
9403
+ process.on("SIGUSR1", () => {
9404
+ if (state.mode === "running") {
9405
+ console.log("\n[Maintenance] Entering drain mode...");
9406
+ console.log(
9407
+ `[Maintenance] Active jobs: ${activeRuns.size} (will wait for completion)`
9438
9408
  );
9409
+ state.mode = "draining";
9410
+ updateStatus();
9411
+ }
9412
+ });
9413
+ const jobPromises = /* @__PURE__ */ new Set();
9414
+ while (running) {
9415
+ if (state.mode === "draining") {
9416
+ if (activeRuns.size === 0) {
9417
+ console.log(
9418
+ "[Maintenance] All jobs completed, exiting drain mode"
9419
+ );
9420
+ running = false;
9421
+ break;
9422
+ }
9423
+ if (jobPromises.size > 0) {
9424
+ await Promise.race(jobPromises);
9425
+ updateStatus();
9426
+ }
9427
+ continue;
9428
+ }
9429
+ if (activeRuns.size >= config.sandbox.max_concurrent) {
9430
+ if (jobPromises.size > 0) {
9431
+ await Promise.race(jobPromises);
9432
+ updateStatus();
9433
+ }
9439
9434
  continue;
9440
9435
  }
9441
- console.log(`Found job: ${job.runId}`);
9442
9436
  try {
9443
- const context = await withRunnerTiming(
9444
- "claim",
9445
- () => claimJob(config.server, job.runId)
9437
+ const job = await withRunnerTiming(
9438
+ "poll",
9439
+ () => pollForJob(config.server, config.group)
9446
9440
  );
9447
- console.log(`Claimed job: ${context.runId}`);
9448
- activeRuns.add(context.runId);
9449
- updateStatus();
9450
- const jobPromise = executeJob2(context, config).catch((error) => {
9451
- console.error(
9452
- `Job ${context.runId} failed:`,
9453
- error instanceof Error ? error.message : "Unknown error"
9441
+ if (!job) {
9442
+ await new Promise(
9443
+ (resolve) => setTimeout(resolve, config.sandbox.poll_interval_ms)
9454
9444
  );
9455
- }).finally(() => {
9456
- activeRuns.delete(context.runId);
9457
- jobPromises.delete(jobPromise);
9445
+ continue;
9446
+ }
9447
+ console.log(`Found job: ${job.runId}`);
9448
+ try {
9449
+ const context = await withRunnerTiming(
9450
+ "claim",
9451
+ () => claimJob(config.server, job.runId)
9452
+ );
9453
+ console.log(`Claimed job: ${context.runId}`);
9454
+ activeRuns.add(context.runId);
9458
9455
  updateStatus();
9459
- });
9460
- jobPromises.add(jobPromise);
9456
+ const jobPromise = executeJob2(context, config).catch((error) => {
9457
+ console.error(
9458
+ `Job ${context.runId} failed:`,
9459
+ error instanceof Error ? error.message : "Unknown error"
9460
+ );
9461
+ }).finally(() => {
9462
+ activeRuns.delete(context.runId);
9463
+ jobPromises.delete(jobPromise);
9464
+ updateStatus();
9465
+ });
9466
+ jobPromises.add(jobPromise);
9467
+ } catch (error) {
9468
+ console.log(
9469
+ `Could not claim job ${job.runId}:`,
9470
+ error instanceof Error ? error.message : "Unknown error"
9471
+ );
9472
+ }
9461
9473
  } catch (error) {
9462
- console.log(
9463
- `Could not claim job ${job.runId}:`,
9474
+ console.error(
9475
+ "Polling error:",
9464
9476
  error instanceof Error ? error.message : "Unknown error"
9465
9477
  );
9478
+ await new Promise((resolve) => setTimeout(resolve, 2e3));
9466
9479
  }
9467
- } catch (error) {
9468
- console.error(
9469
- "Polling error:",
9470
- error instanceof Error ? error.message : "Unknown error"
9480
+ }
9481
+ if (jobPromises.size > 0) {
9482
+ console.log(
9483
+ `Waiting for ${jobPromises.size} active job(s) to complete...`
9471
9484
  );
9472
- await new Promise((resolve) => setTimeout(resolve, 2e3));
9485
+ await Promise.all(jobPromises);
9473
9486
  }
9487
+ if (proxyEnabled) {
9488
+ console.log("Stopping network proxy...");
9489
+ await getProxyManager().stop();
9490
+ }
9491
+ console.log("Flushing metrics...");
9492
+ await flushMetrics();
9493
+ await shutdownMetrics();
9494
+ state.mode = "stopped";
9495
+ updateStatus();
9496
+ console.log("Runner stopped");
9497
+ process.exit(0);
9498
+ } catch (error) {
9499
+ if (error instanceof Error) {
9500
+ console.error(`Error: ${error.message}`);
9501
+ } else {
9502
+ console.error("An unknown error occurred");
9503
+ }
9504
+ process.exit(1);
9474
9505
  }
9475
- if (jobPromises.size > 0) {
9476
- console.log(
9477
- `Waiting for ${jobPromises.size} active job(s) to complete...`
9478
- );
9479
- await Promise.all(jobPromises);
9480
- }
9481
- if (proxyEnabled) {
9482
- console.log("Stopping network proxy...");
9483
- await getProxyManager().stop();
9484
- }
9485
- console.log("Flushing metrics...");
9486
- await flushMetrics();
9487
- await shutdownMetrics();
9488
- state.mode = "stopped";
9489
- updateStatus();
9490
- console.log("Runner stopped");
9491
- process.exit(0);
9492
- } catch (error) {
9493
- if (error instanceof Error) {
9494
- console.error(`Error: ${error.message}`);
9495
- } else {
9496
- console.error("An unknown error occurred");
9497
- }
9498
- process.exit(1);
9499
9506
  }
9500
- });
9507
+ );
9501
9508
 
9502
9509
  // src/commands/doctor.ts
9503
9510
  import { Command as Command2 } from "commander";
@@ -9612,194 +9619,197 @@ function findMitmproxyProcess() {
9612
9619
  }
9613
9620
 
9614
9621
  // src/commands/doctor.ts
9615
- var doctorCommand = new Command2("doctor").description("Diagnose runner health, check network, and detect issues").option("--config <path>", "Config file path", "./runner.yaml").action(async (options) => {
9616
- try {
9617
- const config = loadConfig(options.config);
9618
- const configDir = dirname2(options.config);
9619
- const statusFilePath = join3(configDir, "status.json");
9620
- const workspacesDir = join3(configDir, "workspaces");
9621
- console.log(`Runner: ${config.name}`);
9622
- let status = null;
9623
- if (existsSync3(statusFilePath)) {
9624
- try {
9625
- status = JSON.parse(
9626
- readFileSync3(statusFilePath, "utf-8")
9627
- );
9628
- console.log(`Mode: ${status.mode}`);
9629
- if (status.started_at) {
9630
- const started = new Date(status.started_at);
9631
- const uptime = formatUptime(Date.now() - started.getTime());
9632
- console.log(
9633
- `Started: ${started.toLocaleString()} (uptime: ${uptime})`
9622
+ var doctorCommand = new Command2("doctor").description("Diagnose runner health, check network, and detect issues").option("--config <path>", "Config file path", "./runner.yaml").action(
9623
+ // eslint-disable-next-line complexity -- TODO: refactor complex function
9624
+ async (options) => {
9625
+ try {
9626
+ const config = loadConfig(options.config);
9627
+ const configDir = dirname2(options.config);
9628
+ const statusFilePath = join3(configDir, "status.json");
9629
+ const workspacesDir = join3(configDir, "workspaces");
9630
+ console.log(`Runner: ${config.name}`);
9631
+ let status = null;
9632
+ if (existsSync3(statusFilePath)) {
9633
+ try {
9634
+ status = JSON.parse(
9635
+ readFileSync3(statusFilePath, "utf-8")
9634
9636
  );
9637
+ console.log(`Mode: ${status.mode}`);
9638
+ if (status.started_at) {
9639
+ const started = new Date(status.started_at);
9640
+ const uptime = formatUptime(Date.now() - started.getTime());
9641
+ console.log(
9642
+ `Started: ${started.toLocaleString()} (uptime: ${uptime})`
9643
+ );
9644
+ }
9645
+ } catch {
9646
+ console.log("Mode: unknown (status.json unreadable)");
9635
9647
  }
9636
- } catch {
9637
- console.log("Mode: unknown (status.json unreadable)");
9638
- }
9639
- } else {
9640
- console.log("Mode: unknown (no status.json)");
9641
- }
9642
- console.log("");
9643
- console.log("API Connectivity:");
9644
- try {
9645
- await pollForJob(config.server, config.group);
9646
- console.log(` \u2713 Connected to ${config.server.url}`);
9647
- console.log(" \u2713 Authentication: OK");
9648
- } catch (error) {
9649
- console.log(` \u2717 Cannot connect to ${config.server.url}`);
9650
- console.log(
9651
- ` Error: ${error instanceof Error ? error.message : "Unknown error"}`
9652
- );
9653
- }
9654
- console.log("");
9655
- console.log("Network:");
9656
- const warnings = [];
9657
- const bridgeStatus = await checkBridgeStatus();
9658
- if (bridgeStatus.exists) {
9659
- console.log(` \u2713 Bridge ${BRIDGE_NAME2} (${bridgeStatus.ip})`);
9660
- } else {
9661
- console.log(` \u2717 Bridge ${BRIDGE_NAME2} not found`);
9662
- warnings.push({
9663
- message: `Network bridge ${BRIDGE_NAME2} does not exist`
9664
- });
9665
- }
9666
- const proxyPort = config.proxy.port;
9667
- const mitmProc = findMitmproxyProcess();
9668
- const portInUse = await isPortInUse(proxyPort);
9669
- if (mitmProc) {
9670
- console.log(
9671
- ` \u2713 Proxy mitmproxy (PID ${mitmProc.pid}) on :${proxyPort}`
9672
- );
9673
- } else if (portInUse) {
9674
- console.log(
9675
- ` \u26A0\uFE0F Proxy port :${proxyPort} in use but mitmproxy process not found`
9676
- );
9677
- warnings.push({
9678
- message: `Port ${proxyPort} is in use but mitmproxy process not detected`
9679
- });
9680
- } else {
9681
- console.log(` \u2717 Proxy mitmproxy not running`);
9682
- warnings.push({ message: "Proxy mitmproxy is not running" });
9683
- }
9684
- console.log("");
9685
- const processes = findFirecrackerProcesses();
9686
- const tapDevices = await listTapDevices();
9687
- const workspaces = existsSync3(workspacesDir) ? readdirSync2(workspacesDir).filter((d) => d.startsWith("vm0-")) : [];
9688
- const jobs = [];
9689
- const statusVmIds = /* @__PURE__ */ new Set();
9690
- const allocations = getAllocations();
9691
- if (status?.active_run_ids) {
9692
- for (const runId of status.active_run_ids) {
9693
- const vmId = runId.split("-")[0];
9694
- if (!vmId) continue;
9695
- statusVmIds.add(vmId);
9696
- const proc = processes.find((p) => p.vmId === vmId);
9697
- const ip = getIPForVm(vmId) ?? "not allocated";
9698
- jobs.push({
9699
- runId,
9700
- vmId,
9701
- ip,
9702
- hasProcess: !!proc,
9703
- pid: proc?.pid
9704
- });
9648
+ } else {
9649
+ console.log("Mode: unknown (no status.json)");
9705
9650
  }
9706
- }
9707
- const ipToVmIds = /* @__PURE__ */ new Map();
9708
- for (const [ip, allocation] of allocations) {
9709
- const existing = ipToVmIds.get(ip) ?? [];
9710
- existing.push(allocation.vmId);
9711
- ipToVmIds.set(ip, existing);
9712
- }
9713
- const maxConcurrent = config.sandbox.max_concurrent;
9714
- console.log(`Runs (${jobs.length} active, max ${maxConcurrent}):`);
9715
- if (jobs.length === 0) {
9716
- console.log(" No active runs");
9717
- } else {
9718
- console.log(
9719
- " Run ID VM ID IP Status"
9720
- );
9721
- for (const job of jobs) {
9722
- const ipConflict = (ipToVmIds.get(job.ip)?.length ?? 0) > 1;
9723
- let statusText;
9724
- if (ipConflict) {
9725
- statusText = "\u26A0\uFE0F IP conflict!";
9726
- } else if (job.hasProcess) {
9727
- statusText = `\u2713 Running (PID ${job.pid})`;
9728
- } else {
9729
- statusText = "\u26A0\uFE0F No process";
9730
- }
9651
+ console.log("");
9652
+ console.log("API Connectivity:");
9653
+ try {
9654
+ await pollForJob(config.server, config.group);
9655
+ console.log(` \u2713 Connected to ${config.server.url}`);
9656
+ console.log(" \u2713 Authentication: OK");
9657
+ } catch (error) {
9658
+ console.log(` \u2717 Cannot connect to ${config.server.url}`);
9731
9659
  console.log(
9732
- ` ${job.runId} ${job.vmId} ${job.ip.padEnd(15)} ${statusText}`
9660
+ ` Error: ${error instanceof Error ? error.message : "Unknown error"}`
9733
9661
  );
9734
9662
  }
9735
- }
9736
- console.log("");
9737
- for (const job of jobs) {
9738
- if (!job.hasProcess) {
9663
+ console.log("");
9664
+ console.log("Network:");
9665
+ const warnings = [];
9666
+ const bridgeStatus = await checkBridgeStatus();
9667
+ if (bridgeStatus.exists) {
9668
+ console.log(` \u2713 Bridge ${BRIDGE_NAME2} (${bridgeStatus.ip})`);
9669
+ } else {
9670
+ console.log(` \u2717 Bridge ${BRIDGE_NAME2} not found`);
9739
9671
  warnings.push({
9740
- message: `Run ${job.vmId} in status.json but no Firecracker process running`
9672
+ message: `Network bridge ${BRIDGE_NAME2} does not exist`
9741
9673
  });
9742
9674
  }
9743
- }
9744
- for (const [ip, vmIds] of ipToVmIds) {
9745
- if (vmIds.length > 1) {
9675
+ const proxyPort = config.proxy.port;
9676
+ const mitmProc = findMitmproxyProcess();
9677
+ const portInUse = await isPortInUse(proxyPort);
9678
+ if (mitmProc) {
9679
+ console.log(
9680
+ ` \u2713 Proxy mitmproxy (PID ${mitmProc.pid}) on :${proxyPort}`
9681
+ );
9682
+ } else if (portInUse) {
9683
+ console.log(
9684
+ ` \u26A0\uFE0F Proxy port :${proxyPort} in use but mitmproxy process not found`
9685
+ );
9746
9686
  warnings.push({
9747
- message: `IP conflict: ${ip} assigned to ${vmIds.join(", ")}`
9687
+ message: `Port ${proxyPort} is in use but mitmproxy process not detected`
9748
9688
  });
9689
+ } else {
9690
+ console.log(` \u2717 Proxy mitmproxy not running`);
9691
+ warnings.push({ message: "Proxy mitmproxy is not running" });
9749
9692
  }
9750
- }
9751
- const processVmIds = new Set(processes.map((p) => p.vmId));
9752
- for (const proc of processes) {
9753
- if (!statusVmIds.has(proc.vmId)) {
9754
- warnings.push({
9755
- message: `Orphan process: PID ${proc.pid} (vmId ${proc.vmId}) not in status.json`
9756
- });
9693
+ console.log("");
9694
+ const processes = findFirecrackerProcesses();
9695
+ const tapDevices = await listTapDevices();
9696
+ const workspaces = existsSync3(workspacesDir) ? readdirSync2(workspacesDir).filter((d) => d.startsWith("vm0-")) : [];
9697
+ const jobs = [];
9698
+ const statusVmIds = /* @__PURE__ */ new Set();
9699
+ const allocations = getAllocations();
9700
+ if (status?.active_run_ids) {
9701
+ for (const runId of status.active_run_ids) {
9702
+ const vmId = runId.split("-")[0];
9703
+ if (!vmId) continue;
9704
+ statusVmIds.add(vmId);
9705
+ const proc = processes.find((p) => p.vmId === vmId);
9706
+ const ip = getIPForVm(vmId) ?? "not allocated";
9707
+ jobs.push({
9708
+ runId,
9709
+ vmId,
9710
+ ip,
9711
+ hasProcess: !!proc,
9712
+ pid: proc?.pid
9713
+ });
9714
+ }
9757
9715
  }
9758
- }
9759
- for (const tap of tapDevices) {
9760
- const vmId = tap.replace("tap", "");
9761
- if (!processVmIds.has(vmId) && !statusVmIds.has(vmId)) {
9762
- warnings.push({
9763
- message: `Orphan TAP device: ${tap} (no matching job or process)`
9764
- });
9716
+ const ipToVmIds = /* @__PURE__ */ new Map();
9717
+ for (const [ip, allocation] of allocations) {
9718
+ const existing = ipToVmIds.get(ip) ?? [];
9719
+ existing.push(allocation.vmId);
9720
+ ipToVmIds.set(ip, existing);
9765
9721
  }
9766
- }
9767
- for (const ws of workspaces) {
9768
- const vmId = ws.replace("vm0-", "");
9769
- if (!processVmIds.has(vmId) && !statusVmIds.has(vmId)) {
9722
+ const maxConcurrent = config.sandbox.max_concurrent;
9723
+ console.log(`Runs (${jobs.length} active, max ${maxConcurrent}):`);
9724
+ if (jobs.length === 0) {
9725
+ console.log(" No active runs");
9726
+ } else {
9727
+ console.log(
9728
+ " Run ID VM ID IP Status"
9729
+ );
9730
+ for (const job of jobs) {
9731
+ const ipConflict = (ipToVmIds.get(job.ip)?.length ?? 0) > 1;
9732
+ let statusText;
9733
+ if (ipConflict) {
9734
+ statusText = "\u26A0\uFE0F IP conflict!";
9735
+ } else if (job.hasProcess) {
9736
+ statusText = `\u2713 Running (PID ${job.pid})`;
9737
+ } else {
9738
+ statusText = "\u26A0\uFE0F No process";
9739
+ }
9740
+ console.log(
9741
+ ` ${job.runId} ${job.vmId} ${job.ip.padEnd(15)} ${statusText}`
9742
+ );
9743
+ }
9744
+ }
9745
+ console.log("");
9746
+ for (const job of jobs) {
9747
+ if (!job.hasProcess) {
9748
+ warnings.push({
9749
+ message: `Run ${job.vmId} in status.json but no Firecracker process running`
9750
+ });
9751
+ }
9752
+ }
9753
+ for (const [ip, vmIds] of ipToVmIds) {
9754
+ if (vmIds.length > 1) {
9755
+ warnings.push({
9756
+ message: `IP conflict: ${ip} assigned to ${vmIds.join(", ")}`
9757
+ });
9758
+ }
9759
+ }
9760
+ const processVmIds = new Set(processes.map((p) => p.vmId));
9761
+ for (const proc of processes) {
9762
+ if (!statusVmIds.has(proc.vmId)) {
9763
+ warnings.push({
9764
+ message: `Orphan process: PID ${proc.pid} (vmId ${proc.vmId}) not in status.json`
9765
+ });
9766
+ }
9767
+ }
9768
+ for (const tap of tapDevices) {
9769
+ const vmId = tap.replace("tap", "");
9770
+ if (!processVmIds.has(vmId) && !statusVmIds.has(vmId)) {
9771
+ warnings.push({
9772
+ message: `Orphan TAP device: ${tap} (no matching job or process)`
9773
+ });
9774
+ }
9775
+ }
9776
+ for (const ws of workspaces) {
9777
+ const vmId = ws.replace("vm0-", "");
9778
+ if (!processVmIds.has(vmId) && !statusVmIds.has(vmId)) {
9779
+ warnings.push({
9780
+ message: `Orphan workspace: ${ws} (no matching job or process)`
9781
+ });
9782
+ }
9783
+ }
9784
+ const activeVmIps = new Set(jobs.map((j) => j.ip));
9785
+ const iptablesRules = await listIptablesNatRules();
9786
+ const orphanedIptables = await findOrphanedIptablesRules(
9787
+ iptablesRules,
9788
+ activeVmIps,
9789
+ proxyPort
9790
+ );
9791
+ for (const rule of orphanedIptables) {
9770
9792
  warnings.push({
9771
- message: `Orphan workspace: ${ws} (no matching job or process)`
9793
+ message: `Orphan iptables rule: redirect ${rule.sourceIp}:${rule.destPort} -> :${rule.redirectPort}`
9772
9794
  });
9773
9795
  }
9774
- }
9775
- const activeVmIps = new Set(jobs.map((j) => j.ip));
9776
- const iptablesRules = await listIptablesNatRules();
9777
- const orphanedIptables = await findOrphanedIptablesRules(
9778
- iptablesRules,
9779
- activeVmIps,
9780
- proxyPort
9781
- );
9782
- for (const rule of orphanedIptables) {
9783
- warnings.push({
9784
- message: `Orphan iptables rule: redirect ${rule.sourceIp}:${rule.destPort} -> :${rule.redirectPort}`
9785
- });
9786
- }
9787
- console.log("Warnings:");
9788
- if (warnings.length === 0) {
9789
- console.log(" None");
9790
- } else {
9791
- for (const w of warnings) {
9792
- console.log(` - ${w.message}`);
9796
+ console.log("Warnings:");
9797
+ if (warnings.length === 0) {
9798
+ console.log(" None");
9799
+ } else {
9800
+ for (const w of warnings) {
9801
+ console.log(` - ${w.message}`);
9802
+ }
9793
9803
  }
9804
+ process.exit(warnings.length > 0 ? 1 : 0);
9805
+ } catch (error) {
9806
+ console.error(
9807
+ `Error: ${error instanceof Error ? error.message : "Unknown error"}`
9808
+ );
9809
+ process.exit(1);
9794
9810
  }
9795
- process.exit(warnings.length > 0 ? 1 : 0);
9796
- } catch (error) {
9797
- console.error(
9798
- `Error: ${error instanceof Error ? error.message : "Unknown error"}`
9799
- );
9800
- process.exit(1);
9801
9811
  }
9802
- });
9812
+ );
9803
9813
  function formatUptime(ms) {
9804
9814
  const seconds = Math.floor(ms / 1e3);
9805
9815
  const minutes = Math.floor(seconds / 60);
@@ -9817,6 +9827,7 @@ import { existsSync as existsSync4, readFileSync as readFileSync4, writeFileSync
9817
9827
  import { dirname as dirname3, join as join4 } from "path";
9818
9828
  import * as readline2 from "readline";
9819
9829
  var killCommand = new Command3("kill").description("Force terminate a run and clean up all resources").argument("<run-id>", "Run ID (full UUID or short 8-char vmId)").option("--config <path>", "Config file path", "./runner.yaml").option("--force", "Skip confirmation prompt").action(
9830
+ // eslint-disable-next-line complexity -- TODO: refactor complex function
9820
9831
  async (runIdArg, options) => {
9821
9832
  try {
9822
9833
  loadConfig(options.config);
@@ -10079,7 +10090,7 @@ var benchmarkCommand = new Command4("benchmark").description(
10079
10090
  });
10080
10091
 
10081
10092
  // src/index.ts
10082
- var version = true ? "2.13.4" : "0.1.0";
10093
+ var version = true ? "2.14.0" : "0.1.0";
10083
10094
  program.name("vm0-runner").version(version).description("Self-hosted runner for VM0 agents");
10084
10095
  program.addCommand(startCommand);
10085
10096
  program.addCommand(doctorCommand);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vm0/runner",
3
- "version": "2.13.4",
3
+ "version": "2.14.0",
4
4
  "description": "Self-hosted runner for VM0 agents",
5
5
  "repository": {
6
6
  "type": "git",