@vm0/runner 2.11.1 → 2.13.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 +1354 -401
  2. package/package.json +1 -1
package/index.js CHANGED
@@ -5,8 +5,8 @@ import { program } from "commander";
5
5
 
6
6
  // src/commands/start.ts
7
7
  import { Command } from "commander";
8
- import { writeFileSync } from "fs";
9
- import { dirname, join } from "path";
8
+ import { writeFileSync as writeFileSync2 } from "fs";
9
+ import { dirname, join as join2 } from "path";
10
10
 
11
11
  // src/lib/config.ts
12
12
  import { z } from "zod";
@@ -187,12 +187,12 @@ async function completeJob(apiUrl, context, exitCode, error) {
187
187
  }
188
188
 
189
189
  // src/lib/executor.ts
190
- import path4 from "path";
190
+ import path5 from "path";
191
191
 
192
192
  // src/lib/firecracker/vm.ts
193
193
  import { execSync as execSync2, spawn } from "child_process";
194
- import fs2 from "fs";
195
- import path from "path";
194
+ import fs3 from "fs";
195
+ import path2 from "path";
196
196
  import readline from "readline";
197
197
 
198
198
  // src/lib/firecracker/client.ts
@@ -205,7 +205,7 @@ var FirecrackerClient = class {
205
205
  /**
206
206
  * Make HTTP request to Firecracker API
207
207
  */
208
- async request(method, path5, body) {
208
+ async request(method, path7, body) {
209
209
  return new Promise((resolve, reject) => {
210
210
  const bodyStr = body !== void 0 ? JSON.stringify(body) : void 0;
211
211
  const headers = {
@@ -218,11 +218,11 @@ var FirecrackerClient = class {
218
218
  headers["Content-Length"] = Buffer.byteLength(bodyStr);
219
219
  }
220
220
  console.log(
221
- `[FC API] ${method} ${path5}${bodyStr ? ` (${Buffer.byteLength(bodyStr)} bytes)` : ""}`
221
+ `[FC API] ${method} ${path7}${bodyStr ? ` (${Buffer.byteLength(bodyStr)} bytes)` : ""}`
222
222
  );
223
223
  const options = {
224
224
  socketPath: this.socketPath,
225
- path: path5,
225
+ path: path7,
226
226
  method,
227
227
  headers,
228
228
  // Disable agent to ensure fresh connection for each request
@@ -356,10 +356,214 @@ var FirecrackerClient = class {
356
356
  };
357
357
 
358
358
  // src/lib/firecracker/network.ts
359
- import { execSync, exec } from "child_process";
359
+ import { execSync, exec as exec2 } from "child_process";
360
+ import { promisify as promisify2 } from "util";
361
+
362
+ // src/lib/firecracker/ip-pool.ts
363
+ import { exec } from "child_process";
360
364
  import { promisify } from "util";
365
+ import * as fs2 from "fs";
366
+ import * as path from "path";
361
367
  var execAsync = promisify(exec);
368
+ var VM0_RUN_DIR = "/var/run/vm0";
369
+ var REGISTRY_FILE_PATH = path.join(VM0_RUN_DIR, "ip-registry.json");
362
370
  var BRIDGE_NAME = "vm0br0";
371
+ var IP_PREFIX = "172.16.0.";
372
+ var IP_START = 2;
373
+ var IP_END = 254;
374
+ var LOCK_TIMEOUT_MS = 1e4;
375
+ var LOCK_RETRY_INTERVAL_MS = 100;
376
+ var ALLOCATION_GRACE_PERIOD_MS = 3e4;
377
+ async function ensureRunDir() {
378
+ if (!fs2.existsSync(VM0_RUN_DIR)) {
379
+ await execAsync(`sudo mkdir -p ${VM0_RUN_DIR}`);
380
+ await execAsync(`sudo chmod 777 ${VM0_RUN_DIR}`);
381
+ }
382
+ }
383
+ async function withLock(fn) {
384
+ await ensureRunDir();
385
+ const lockMarker = path.join(VM0_RUN_DIR, "ip-pool.lock.active");
386
+ const startTime = Date.now();
387
+ let lockAcquired = false;
388
+ while (Date.now() - startTime < LOCK_TIMEOUT_MS) {
389
+ try {
390
+ fs2.writeFileSync(lockMarker, process.pid.toString(), { flag: "wx" });
391
+ lockAcquired = true;
392
+ break;
393
+ } catch {
394
+ try {
395
+ const pidStr = fs2.readFileSync(lockMarker, "utf-8");
396
+ const pid = parseInt(pidStr, 10);
397
+ try {
398
+ process.kill(pid, 0);
399
+ } catch {
400
+ fs2.unlinkSync(lockMarker);
401
+ continue;
402
+ }
403
+ } catch {
404
+ }
405
+ await new Promise(
406
+ (resolve) => setTimeout(resolve, LOCK_RETRY_INTERVAL_MS)
407
+ );
408
+ }
409
+ }
410
+ if (!lockAcquired) {
411
+ throw new Error(
412
+ `Failed to acquire IP pool lock after ${LOCK_TIMEOUT_MS}ms`
413
+ );
414
+ }
415
+ try {
416
+ return await fn();
417
+ } finally {
418
+ try {
419
+ fs2.unlinkSync(lockMarker);
420
+ } catch {
421
+ }
422
+ }
423
+ }
424
+ function readRegistry() {
425
+ try {
426
+ if (fs2.existsSync(REGISTRY_FILE_PATH)) {
427
+ const content = fs2.readFileSync(REGISTRY_FILE_PATH, "utf-8");
428
+ return JSON.parse(content);
429
+ }
430
+ } catch {
431
+ }
432
+ return { allocations: {} };
433
+ }
434
+ function writeRegistry(registry) {
435
+ fs2.writeFileSync(REGISTRY_FILE_PATH, JSON.stringify(registry, null, 2));
436
+ }
437
+ function getAllocations() {
438
+ const registry = readRegistry();
439
+ return new Map(Object.entries(registry.allocations));
440
+ }
441
+ function getIPForVm(vmId) {
442
+ const registry = readRegistry();
443
+ for (const [ip, allocation] of Object.entries(registry.allocations)) {
444
+ if (allocation.vmId === vmId) {
445
+ return ip;
446
+ }
447
+ }
448
+ return void 0;
449
+ }
450
+ async function scanTapDevices() {
451
+ const tapDevices = /* @__PURE__ */ new Map();
452
+ try {
453
+ const { stdout } = await execAsync(
454
+ `ip link show master ${BRIDGE_NAME} 2>/dev/null || true`
455
+ );
456
+ const lines = stdout.split("\n");
457
+ for (const line of lines) {
458
+ const match = line.match(/^\d+:\s+(tap[a-f0-9]+):/);
459
+ if (match && match[1]) {
460
+ const tapName = match[1];
461
+ const vmIdPrefix = tapName.substring(3);
462
+ tapDevices.set(tapName, vmIdPrefix);
463
+ }
464
+ }
465
+ } catch {
466
+ }
467
+ return tapDevices;
468
+ }
469
+ function reconcileRegistry(registry, activeTaps) {
470
+ const reconciled = { allocations: {} };
471
+ const activeTapNames = new Set(activeTaps.keys());
472
+ const now = Date.now();
473
+ for (const [ip, allocation] of Object.entries(registry.allocations)) {
474
+ const allocatedTime = new Date(allocation.allocatedAt).getTime();
475
+ const isWithinGracePeriod = now - allocatedTime < ALLOCATION_GRACE_PERIOD_MS;
476
+ if (activeTapNames.has(allocation.tapDevice)) {
477
+ reconciled.allocations[ip] = allocation;
478
+ } else if (isWithinGracePeriod) {
479
+ reconciled.allocations[ip] = allocation;
480
+ } else {
481
+ console.log(
482
+ `[IP Pool] Removing stale allocation for ${ip} (TAP ${allocation.tapDevice} no longer exists)`
483
+ );
484
+ }
485
+ }
486
+ return reconciled;
487
+ }
488
+ function findFreeIP(registry) {
489
+ const allocatedIPs = new Set(Object.keys(registry.allocations));
490
+ for (let octet = IP_START; octet <= IP_END; octet++) {
491
+ const ip = `${IP_PREFIX}${octet}`;
492
+ if (!allocatedIPs.has(ip)) {
493
+ return ip;
494
+ }
495
+ }
496
+ return null;
497
+ }
498
+ async function allocateIP(vmId) {
499
+ const tapDevice = `tap${vmId.substring(0, 8)}`;
500
+ return withLock(async () => {
501
+ const registry = readRegistry();
502
+ const ip = findFreeIP(registry);
503
+ if (!ip) {
504
+ throw new Error(
505
+ "No free IP addresses available in pool (172.16.0.2-254)"
506
+ );
507
+ }
508
+ const allocatedCount = Object.keys(registry.allocations).length;
509
+ const allocatedIPs = Object.keys(registry.allocations).sort();
510
+ console.log(
511
+ `[IP Pool] Current state: ${allocatedCount} IPs allocated [${allocatedIPs.join(", ")}], assigning ${ip}`
512
+ );
513
+ registry.allocations[ip] = {
514
+ vmId,
515
+ tapDevice,
516
+ allocatedAt: (/* @__PURE__ */ new Date()).toISOString()
517
+ };
518
+ writeRegistry(registry);
519
+ console.log(`[IP Pool] Allocated ${ip} for VM ${vmId} (TAP ${tapDevice})`);
520
+ return ip;
521
+ });
522
+ }
523
+ async function releaseIP(ip) {
524
+ return withLock(async () => {
525
+ const registry = readRegistry();
526
+ if (registry.allocations[ip]) {
527
+ const allocation = registry.allocations[ip];
528
+ delete registry.allocations[ip];
529
+ writeRegistry(registry);
530
+ console.log(
531
+ `[IP Pool] Released ${ip} (was allocated to VM ${allocation.vmId})`
532
+ );
533
+ } else {
534
+ console.log(`[IP Pool] IP ${ip} was not in registry, nothing to release`);
535
+ }
536
+ });
537
+ }
538
+ async function cleanupOrphanedAllocations() {
539
+ return withLock(async () => {
540
+ console.log("[IP Pool] Cleaning up orphaned allocations...");
541
+ const registry = readRegistry();
542
+ const beforeCount = Object.keys(registry.allocations).length;
543
+ if (beforeCount === 0) {
544
+ console.log("[IP Pool] No allocations in registry, nothing to clean up");
545
+ return;
546
+ }
547
+ const activeTaps = await scanTapDevices();
548
+ console.log(
549
+ `[IP Pool] Found ${activeTaps.size} active TAP device(s) on bridge`
550
+ );
551
+ const reconciled = reconcileRegistry(registry, activeTaps);
552
+ const afterCount = Object.keys(reconciled.allocations).length;
553
+ if (afterCount !== beforeCount) {
554
+ writeRegistry(reconciled);
555
+ console.log(
556
+ `[IP Pool] Cleaned up ${beforeCount - afterCount} orphaned allocation(s)`
557
+ );
558
+ } else {
559
+ console.log("[IP Pool] No orphaned allocations found");
560
+ }
561
+ });
562
+ }
563
+
564
+ // src/lib/firecracker/network.ts
565
+ var execAsync2 = promisify2(exec2);
566
+ var BRIDGE_NAME2 = "vm0br0";
363
567
  var BRIDGE_IP = "172.16.0.1";
364
568
  var BRIDGE_NETMASK = "255.255.255.0";
365
569
  var BRIDGE_CIDR = "172.16.0.0/24";
@@ -379,11 +583,6 @@ function generateMacAddress(vmId) {
379
583
  const b3 = hash & 255;
380
584
  return `02:00:00:${b1.toString(16).padStart(2, "0")}:${b2.toString(16).padStart(2, "0")}:${b3.toString(16).padStart(2, "0")}`;
381
585
  }
382
- function generateGuestIp(vmId) {
383
- const hash = hashString(vmId);
384
- const lastOctet = hash % 253 + 2;
385
- return `172.16.0.${lastOctet}`;
386
- }
387
586
  function commandExists(cmd) {
388
587
  try {
389
588
  execSync(`which ${cmd}`, { stdio: "ignore" });
@@ -395,7 +594,7 @@ function commandExists(cmd) {
395
594
  async function execCommand(cmd, sudo = true) {
396
595
  const fullCmd = sudo ? `sudo ${cmd}` : cmd;
397
596
  try {
398
- const { stdout } = await execAsync(fullCmd);
597
+ const { stdout } = await execAsync2(fullCmd);
399
598
  return stdout.trim();
400
599
  } catch (error) {
401
600
  const execError = error;
@@ -415,33 +614,33 @@ async function getDefaultInterface() {
415
614
  }
416
615
  async function setupForwardRules() {
417
616
  const extIface = await getDefaultInterface();
418
- console.log(`Setting up FORWARD rules for ${BRIDGE_NAME} <-> ${extIface}`);
617
+ console.log(`Setting up FORWARD rules for ${BRIDGE_NAME2} <-> ${extIface}`);
419
618
  try {
420
619
  await execCommand(
421
- `iptables -C FORWARD -i ${BRIDGE_NAME} -o ${extIface} -j ACCEPT`
620
+ `iptables -C FORWARD -i ${BRIDGE_NAME2} -o ${extIface} -j ACCEPT`
422
621
  );
423
622
  console.log("FORWARD outbound rule already exists");
424
623
  } catch {
425
624
  await execCommand(
426
- `iptables -I FORWARD -i ${BRIDGE_NAME} -o ${extIface} -j ACCEPT`
625
+ `iptables -I FORWARD -i ${BRIDGE_NAME2} -o ${extIface} -j ACCEPT`
427
626
  );
428
627
  console.log("FORWARD outbound rule added");
429
628
  }
430
629
  try {
431
630
  await execCommand(
432
- `iptables -C FORWARD -i ${extIface} -o ${BRIDGE_NAME} -m state --state RELATED,ESTABLISHED -j ACCEPT`
631
+ `iptables -C FORWARD -i ${extIface} -o ${BRIDGE_NAME2} -m state --state RELATED,ESTABLISHED -j ACCEPT`
433
632
  );
434
633
  console.log("FORWARD inbound rule already exists");
435
634
  } catch {
436
635
  await execCommand(
437
- `iptables -I FORWARD -i ${extIface} -o ${BRIDGE_NAME} -m state --state RELATED,ESTABLISHED -j ACCEPT`
636
+ `iptables -I FORWARD -i ${extIface} -o ${BRIDGE_NAME2} -m state --state RELATED,ESTABLISHED -j ACCEPT`
438
637
  );
439
638
  console.log("FORWARD inbound rule added");
440
639
  }
441
640
  }
442
641
  async function bridgeExists() {
443
642
  try {
444
- await execCommand(`ip link show ${BRIDGE_NAME}`, true);
643
+ await execCommand(`ip link show ${BRIDGE_NAME2}`, true);
445
644
  return true;
446
645
  } catch {
447
646
  return false;
@@ -449,16 +648,16 @@ async function bridgeExists() {
449
648
  }
450
649
  async function setupBridge() {
451
650
  if (await bridgeExists()) {
452
- console.log(`Bridge ${BRIDGE_NAME} already exists`);
651
+ console.log(`Bridge ${BRIDGE_NAME2} already exists`);
453
652
  await setupForwardRules();
454
653
  return;
455
654
  }
456
- console.log(`Creating bridge ${BRIDGE_NAME}...`);
457
- await execCommand(`ip link add name ${BRIDGE_NAME} type bridge`);
655
+ console.log(`Creating bridge ${BRIDGE_NAME2}...`);
656
+ await execCommand(`ip link add name ${BRIDGE_NAME2} type bridge`);
458
657
  await execCommand(
459
- `ip addr add ${BRIDGE_IP}/${BRIDGE_NETMASK} dev ${BRIDGE_NAME}`
658
+ `ip addr add ${BRIDGE_IP}/${BRIDGE_NETMASK} dev ${BRIDGE_NAME2}`
460
659
  );
461
- await execCommand(`ip link set ${BRIDGE_NAME} up`);
660
+ await execCommand(`ip link set ${BRIDGE_NAME2} up`);
462
661
  await execCommand(`sysctl -w net.ipv4.ip_forward=1`);
463
662
  try {
464
663
  await execCommand(
@@ -472,7 +671,7 @@ async function setupBridge() {
472
671
  console.log("NAT rule added");
473
672
  }
474
673
  await setupForwardRules();
475
- console.log(`Bridge ${BRIDGE_NAME} configured with IP ${BRIDGE_IP}`);
674
+ console.log(`Bridge ${BRIDGE_NAME2} configured with IP ${BRIDGE_IP}`);
476
675
  }
477
676
  async function tapDeviceExists(tapDevice) {
478
677
  try {
@@ -482,10 +681,34 @@ async function tapDeviceExists(tapDevice) {
482
681
  return false;
483
682
  }
484
683
  }
684
+ async function clearStaleIptablesRulesForIP(ip) {
685
+ try {
686
+ const { stdout } = await execAsync2(
687
+ "sudo iptables -t nat -S PREROUTING 2>/dev/null || true"
688
+ );
689
+ const lines = stdout.split("\n");
690
+ const rulesForIP = lines.filter((line) => line.includes(`-s ${ip}`));
691
+ if (rulesForIP.length === 0) {
692
+ return;
693
+ }
694
+ console.log(
695
+ `Clearing ${rulesForIP.length} stale iptables rule(s) for IP ${ip}`
696
+ );
697
+ for (const rule of rulesForIP) {
698
+ const deleteRule = rule.replace("-A ", "-D ");
699
+ try {
700
+ await execCommand(`iptables -t nat ${deleteRule}`);
701
+ } catch {
702
+ }
703
+ }
704
+ } catch {
705
+ }
706
+ }
485
707
  async function createTapDevice(vmId) {
486
708
  const tapDevice = `tap${vmId.substring(0, 8)}`;
487
709
  const guestMac = generateMacAddress(vmId);
488
- const guestIp = generateGuestIp(vmId);
710
+ const guestIp = await allocateIP(vmId);
711
+ await clearStaleIptablesRulesForIP(guestIp);
489
712
  console.log(`Creating TAP device ${tapDevice} for VM ${vmId}...`);
490
713
  await setupBridge();
491
714
  if (await tapDeviceExists(tapDevice)) {
@@ -493,7 +716,7 @@ async function createTapDevice(vmId) {
493
716
  await deleteTapDevice(tapDevice);
494
717
  }
495
718
  await execCommand(`ip tuntap add ${tapDevice} mode tap`);
496
- await execCommand(`ip link set ${tapDevice} master ${BRIDGE_NAME}`);
719
+ await execCommand(`ip link set ${tapDevice} master ${BRIDGE_NAME2}`);
497
720
  await execCommand(`ip link set ${tapDevice} up`);
498
721
  console.log(
499
722
  `TAP ${tapDevice} created: guest MAC ${guestMac}, guest IP ${guestIp}`
@@ -506,13 +729,23 @@ async function createTapDevice(vmId) {
506
729
  netmask: BRIDGE_NETMASK
507
730
  };
508
731
  }
509
- async function deleteTapDevice(tapDevice) {
732
+ async function deleteTapDevice(tapDevice, guestIp) {
510
733
  if (!await tapDeviceExists(tapDevice)) {
511
734
  console.log(`TAP device ${tapDevice} does not exist, skipping delete`);
512
- return;
735
+ } else {
736
+ await execCommand(`ip link delete ${tapDevice}`);
737
+ console.log(`TAP device ${tapDevice} deleted`);
738
+ }
739
+ if (guestIp) {
740
+ try {
741
+ await execCommand(`ip neigh del ${guestIp} dev ${BRIDGE_NAME2}`, true);
742
+ console.log(`ARP entry cleared for ${guestIp}`);
743
+ } catch {
744
+ }
745
+ }
746
+ if (guestIp) {
747
+ await releaseIP(guestIp);
513
748
  }
514
- await execCommand(`ip link delete ${tapDevice}`);
515
- console.log(`TAP device ${tapDevice} deleted`);
516
749
  }
517
750
  function generateNetworkBootArgs(config) {
518
751
  return `ip=${config.guestIp}::${config.gatewayIp}:${config.netmask}:vm0-guest:eth0:off`;
@@ -537,52 +770,191 @@ function checkNetworkPrerequisites() {
537
770
  errors
538
771
  };
539
772
  }
540
- async function setupVMProxyRules(vmIp, proxyPort) {
773
+ async function setupVMProxyRules(vmIp, proxyPort, runnerName) {
774
+ const comment = `vm0:runner:${runnerName}`;
541
775
  console.log(
542
- `Setting up proxy rules for VM ${vmIp} -> localhost:${proxyPort}`
776
+ `Setting up proxy rules for VM ${vmIp} -> localhost:${proxyPort} (comment: ${comment})`
543
777
  );
544
778
  try {
545
779
  await execCommand(
546
- `iptables -t nat -C PREROUTING -s ${vmIp} -p tcp --dport 80 -j REDIRECT --to-port ${proxyPort}`
780
+ `iptables -t nat -C PREROUTING -s ${vmIp} -p tcp --dport 80 -j REDIRECT --to-port ${proxyPort} -m comment --comment "${comment}"`
547
781
  );
548
782
  console.log(`Proxy rule for ${vmIp}:80 already exists`);
549
783
  } catch {
550
784
  await execCommand(
551
- `iptables -t nat -A PREROUTING -s ${vmIp} -p tcp --dport 80 -j REDIRECT --to-port ${proxyPort}`
785
+ `iptables -t nat -A PREROUTING -s ${vmIp} -p tcp --dport 80 -j REDIRECT --to-port ${proxyPort} -m comment --comment "${comment}"`
552
786
  );
553
787
  console.log(`Proxy rule for ${vmIp}:80 added`);
554
788
  }
555
789
  try {
556
790
  await execCommand(
557
- `iptables -t nat -C PREROUTING -s ${vmIp} -p tcp --dport 443 -j REDIRECT --to-port ${proxyPort}`
791
+ `iptables -t nat -C PREROUTING -s ${vmIp} -p tcp --dport 443 -j REDIRECT --to-port ${proxyPort} -m comment --comment "${comment}"`
558
792
  );
559
793
  console.log(`Proxy rule for ${vmIp}:443 already exists`);
560
794
  } catch {
561
795
  await execCommand(
562
- `iptables -t nat -A PREROUTING -s ${vmIp} -p tcp --dport 443 -j REDIRECT --to-port ${proxyPort}`
796
+ `iptables -t nat -A PREROUTING -s ${vmIp} -p tcp --dport 443 -j REDIRECT --to-port ${proxyPort} -m comment --comment "${comment}"`
563
797
  );
564
798
  console.log(`Proxy rule for ${vmIp}:443 added`);
565
799
  }
566
800
  console.log(`Proxy rules configured for VM ${vmIp}`);
567
801
  }
568
- async function removeVMProxyRules(vmIp, proxyPort) {
802
+ async function removeVMProxyRules(vmIp, proxyPort, runnerName) {
803
+ const comment = `vm0:runner:${runnerName}`;
569
804
  console.log(`Removing proxy rules for VM ${vmIp}...`);
570
805
  try {
571
806
  await execCommand(
572
- `iptables -t nat -D PREROUTING -s ${vmIp} -p tcp --dport 80 -j REDIRECT --to-port ${proxyPort}`
807
+ `iptables -t nat -D PREROUTING -s ${vmIp} -p tcp --dport 80 -j REDIRECT --to-port ${proxyPort} -m comment --comment "${comment}"`
573
808
  );
574
809
  console.log(`Proxy rule for ${vmIp}:80 removed`);
575
810
  } catch {
576
811
  }
577
812
  try {
578
813
  await execCommand(
579
- `iptables -t nat -D PREROUTING -s ${vmIp} -p tcp --dport 443 -j REDIRECT --to-port ${proxyPort}`
814
+ `iptables -t nat -D PREROUTING -s ${vmIp} -p tcp --dport 443 -j REDIRECT --to-port ${proxyPort} -m comment --comment "${comment}"`
580
815
  );
581
816
  console.log(`Proxy rule for ${vmIp}:443 removed`);
582
817
  } catch {
583
818
  }
584
819
  console.log(`Proxy rules cleanup complete for VM ${vmIp}`);
585
820
  }
821
+ async function listTapDevices() {
822
+ try {
823
+ const result = await execCommand("ip -o link show type tuntap", false);
824
+ const devices = [];
825
+ const lines = result.split("\n");
826
+ for (const line of lines) {
827
+ const match = line.match(/^\d+:\s+(tap[a-f0-9]{8}):/);
828
+ if (match && match[1]) {
829
+ devices.push(match[1]);
830
+ }
831
+ }
832
+ return devices;
833
+ } catch {
834
+ return [];
835
+ }
836
+ }
837
+ async function checkBridgeStatus() {
838
+ try {
839
+ const result = await execCommand(`ip -o addr show ${BRIDGE_NAME2}`, false);
840
+ const ipMatch = result.match(/inet\s+(\d+\.\d+\.\d+\.\d+)/);
841
+ const upMatch = result.includes("UP") || result.includes("state UP");
842
+ return {
843
+ exists: true,
844
+ ip: ipMatch?.[1] ?? BRIDGE_IP,
845
+ up: upMatch
846
+ };
847
+ } catch {
848
+ return { exists: false };
849
+ }
850
+ }
851
+ async function isPortInUse(port) {
852
+ try {
853
+ await execCommand(`ss -tln | grep -q ":${port} "`, false);
854
+ return true;
855
+ } catch {
856
+ return false;
857
+ }
858
+ }
859
+ async function listIptablesNatRules() {
860
+ try {
861
+ const result = await execCommand("iptables -t nat -L PREROUTING -n", true);
862
+ const rules = [];
863
+ const lines = result.split("\n");
864
+ for (const line of lines) {
865
+ const match = line.match(
866
+ /REDIRECT\s+tcp\s+--\s+(172\.16\.0\.\d+)\s+\S+\s+tcp\s+dpt:(\d+)\s+redir\s+ports\s+(\d+)/
867
+ );
868
+ if (match && match[1] && match[2] && match[3]) {
869
+ rules.push({
870
+ sourceIp: match[1],
871
+ destPort: parseInt(match[2], 10),
872
+ redirectPort: parseInt(match[3], 10)
873
+ });
874
+ }
875
+ }
876
+ return rules;
877
+ } catch {
878
+ return [];
879
+ }
880
+ }
881
+ async function findOrphanedIptablesRules(rules, activeVmIps, expectedProxyPort) {
882
+ const orphaned = [];
883
+ for (const rule of rules) {
884
+ const isActiveVm = activeVmIps.has(rule.sourceIp);
885
+ const correctPort = rule.redirectPort === expectedProxyPort;
886
+ if (!isActiveVm || !correctPort) {
887
+ const portListening = await isPortInUse(rule.redirectPort);
888
+ if (!portListening) {
889
+ orphaned.push(rule);
890
+ }
891
+ }
892
+ }
893
+ return orphaned;
894
+ }
895
+ async function flushBridgeArpCache() {
896
+ console.log(`Flushing ARP cache on bridge ${BRIDGE_NAME2}...`);
897
+ try {
898
+ if (!await bridgeExists()) {
899
+ console.log("Bridge does not exist, skipping ARP flush");
900
+ return;
901
+ }
902
+ const { stdout } = await execAsync2(
903
+ `ip neigh show dev ${BRIDGE_NAME2} 2>/dev/null || true`
904
+ );
905
+ if (!stdout.trim()) {
906
+ console.log("No ARP entries on bridge");
907
+ return;
908
+ }
909
+ const lines = stdout.split("\n").filter((line) => line.trim());
910
+ let cleared = 0;
911
+ for (const line of lines) {
912
+ const match = line.match(/^(\d+\.\d+\.\d+\.\d+)\s/);
913
+ if (match && match[1]) {
914
+ const ip = match[1];
915
+ try {
916
+ await execCommand(`ip neigh del ${ip} dev ${BRIDGE_NAME2}`, true);
917
+ cleared++;
918
+ } catch {
919
+ }
920
+ }
921
+ }
922
+ console.log(`Cleared ${cleared} ARP entries from bridge`);
923
+ } catch (error) {
924
+ console.log(
925
+ `Warning: Could not flush ARP cache: ${error instanceof Error ? error.message : "Unknown error"}`
926
+ );
927
+ }
928
+ }
929
+ async function cleanupOrphanedProxyRules(runnerName) {
930
+ const comment = `vm0:runner:${runnerName}`;
931
+ console.log(`Cleaning up orphaned proxy rules for runner '${runnerName}'...`);
932
+ try {
933
+ const rules = await execCommand("iptables -t nat -S PREROUTING", false);
934
+ const ourRules = rules.split("\n").filter((rule) => rule.includes(comment));
935
+ if (ourRules.length === 0) {
936
+ console.log("No orphaned proxy rules found");
937
+ return;
938
+ }
939
+ console.log(`Found ${ourRules.length} orphaned rule(s) to clean up`);
940
+ for (const rule of ourRules) {
941
+ const deleteRule = rule.replace("-A ", "-D ");
942
+ try {
943
+ await execCommand(`iptables -t nat ${deleteRule}`);
944
+ console.log(`Deleted orphaned rule: ${rule.substring(0, 80)}...`);
945
+ } catch {
946
+ console.log(
947
+ `Failed to delete rule (may already be gone): ${rule.substring(0, 80)}...`
948
+ );
949
+ }
950
+ }
951
+ console.log("Orphaned proxy rules cleanup complete");
952
+ } catch (error) {
953
+ console.log(
954
+ `Warning: Could not clean up orphaned rules: ${error instanceof Error ? error.message : "Unknown error"}`
955
+ );
956
+ }
957
+ }
586
958
 
587
959
  // src/lib/firecracker/vm.ts
588
960
  var FirecrackerVM = class {
@@ -598,8 +970,8 @@ var FirecrackerVM = class {
598
970
  constructor(config) {
599
971
  this.config = config;
600
972
  this.workDir = config.workDir || `/tmp/vm0-vm-${config.vmId}`;
601
- this.socketPath = path.join(this.workDir, "firecracker.sock");
602
- this.vmOverlayPath = path.join(this.workDir, "overlay.ext4");
973
+ this.socketPath = path2.join(this.workDir, "firecracker.sock");
974
+ this.vmOverlayPath = path2.join(this.workDir, "overlay.ext4");
603
975
  }
604
976
  /**
605
977
  * Get current VM state
@@ -634,15 +1006,15 @@ var FirecrackerVM = class {
634
1006
  throw new Error(`Cannot start VM in state: ${this.state}`);
635
1007
  }
636
1008
  try {
637
- fs2.mkdirSync(this.workDir, { recursive: true });
638
- if (fs2.existsSync(this.socketPath)) {
639
- fs2.unlinkSync(this.socketPath);
1009
+ fs3.mkdirSync(this.workDir, { recursive: true });
1010
+ if (fs3.existsSync(this.socketPath)) {
1011
+ fs3.unlinkSync(this.socketPath);
640
1012
  }
641
1013
  console.log(`[VM ${this.config.vmId}] Creating sparse overlay file...`);
642
1014
  const overlaySize = 2 * 1024 * 1024 * 1024;
643
- const fd = fs2.openSync(this.vmOverlayPath, "w");
644
- fs2.ftruncateSync(fd, overlaySize);
645
- fs2.closeSync(fd);
1015
+ const fd = fs3.openSync(this.vmOverlayPath, "w");
1016
+ fs3.ftruncateSync(fd, overlaySize);
1017
+ fs3.closeSync(fd);
646
1018
  execSync2(`mkfs.ext4 -F -q "${this.vmOverlayPath}"`, { stdio: "ignore" });
647
1019
  console.log(`[VM ${this.config.vmId}] Setting up network...`);
648
1020
  this.networkConfig = await createTapDevice(this.config.vmId);
@@ -792,11 +1164,14 @@ var FirecrackerVM = class {
792
1164
  this.process = null;
793
1165
  }
794
1166
  if (this.networkConfig) {
795
- await deleteTapDevice(this.networkConfig.tapDevice);
1167
+ await deleteTapDevice(
1168
+ this.networkConfig.tapDevice,
1169
+ this.networkConfig.guestIp
1170
+ );
796
1171
  this.networkConfig = null;
797
1172
  }
798
- if (fs2.existsSync(this.workDir)) {
799
- fs2.rmSync(this.workDir, { recursive: true, force: true });
1173
+ if (fs3.existsSync(this.workDir)) {
1174
+ fs3.rmSync(this.workDir, { recursive: true, force: true });
800
1175
  }
801
1176
  this.client = null;
802
1177
  this.state = "stopped";
@@ -830,12 +1205,12 @@ var FirecrackerVM = class {
830
1205
  };
831
1206
 
832
1207
  // src/lib/firecracker/guest.ts
833
- import { exec as exec2, execSync as execSync3 } from "child_process";
834
- import { promisify as promisify2 } from "util";
835
- import fs3 from "fs";
836
- import path2 from "path";
1208
+ import { exec as exec3, execSync as execSync3 } from "child_process";
1209
+ import { promisify as promisify3 } from "util";
1210
+ import fs4 from "fs";
1211
+ import path3 from "path";
837
1212
  import os from "os";
838
- var execAsync2 = promisify2(exec2);
1213
+ var execAsync3 = promisify3(exec3);
839
1214
  var DEFAULT_SSH_OPTIONS = [
840
1215
  "-o",
841
1216
  "StrictHostKeyChecking=no",
@@ -886,7 +1261,7 @@ var SSHClient = class {
886
1261
  const escapedCommand = command.replace(/'/g, "'\\''");
887
1262
  const fullCmd = [...sshCmd, `'${escapedCommand}'`].join(" ");
888
1263
  try {
889
- const { stdout, stderr } = await execAsync2(fullCmd, {
1264
+ const { stdout, stderr } = await execAsync3(fullCmd, {
890
1265
  maxBuffer: 50 * 1024 * 1024,
891
1266
  // 50MB buffer
892
1267
  timeout: timeoutMs ?? 3e5
@@ -1033,11 +1408,11 @@ function createVMSSHClient(guestIp, user = "root", privateKeyPath) {
1033
1408
  }
1034
1409
  function getRunnerSSHKeyPath() {
1035
1410
  const runnerKeyPath = "/opt/vm0-runner/ssh/id_rsa";
1036
- if (fs3.existsSync(runnerKeyPath)) {
1411
+ if (fs4.existsSync(runnerKeyPath)) {
1037
1412
  return runnerKeyPath;
1038
1413
  }
1039
- const userKeyPath = path2.join(os.homedir(), ".ssh", "id_rsa");
1040
- if (fs3.existsSync(userKeyPath)) {
1414
+ const userKeyPath = path3.join(os.homedir(), ".ssh", "id_rsa");
1415
+ if (fs4.existsSync(userKeyPath)) {
1041
1416
  return userKeyPath;
1042
1417
  }
1043
1418
  return "";
@@ -1391,8 +1766,8 @@ function getErrorMap() {
1391
1766
  return overrideErrorMap;
1392
1767
  }
1393
1768
  var makeIssue = (params) => {
1394
- const { data, path: path5, errorMaps, issueData } = params;
1395
- const fullPath = [...path5, ...issueData.path || []];
1769
+ const { data, path: path7, errorMaps, issueData } = params;
1770
+ const fullPath = [...path7, ...issueData.path || []];
1396
1771
  const fullIssue = {
1397
1772
  ...issueData,
1398
1773
  path: fullPath
@@ -1491,11 +1866,11 @@ var errorUtil;
1491
1866
  errorUtil2.toString = (message) => typeof message === "string" ? message : message === null || message === void 0 ? void 0 : message.message;
1492
1867
  })(errorUtil || (errorUtil = {}));
1493
1868
  var ParseInputLazyPath = class {
1494
- constructor(parent, value, path5, key) {
1869
+ constructor(parent, value, path7, key) {
1495
1870
  this._cachedPath = [];
1496
1871
  this.parent = parent;
1497
1872
  this.data = value;
1498
- this._path = path5;
1873
+ this._path = path7;
1499
1874
  this._key = key;
1500
1875
  }
1501
1876
  get path() {
@@ -4954,7 +5329,9 @@ var storedExecutionContextSchema = z4.object({
4954
5329
  encryptedSecrets: z4.string().nullable(),
4955
5330
  // AES-256-GCM encrypted secrets
4956
5331
  cliAgentType: z4.string(),
4957
- experimentalFirewall: experimentalFirewallSchema.optional()
5332
+ experimentalFirewall: experimentalFirewallSchema.optional(),
5333
+ // Debug flag to force real Claude in mock environments (internal use only)
5334
+ debugNoMockClaude: z4.boolean().optional()
4958
5335
  });
4959
5336
  var executionContextSchema = z4.object({
4960
5337
  runId: z4.string().uuid(),
@@ -4972,7 +5349,9 @@ var executionContextSchema = z4.object({
4972
5349
  secretValues: z4.array(z4.string()).nullable(),
4973
5350
  cliAgentType: z4.string(),
4974
5351
  // Experimental firewall configuration
4975
- experimentalFirewall: experimentalFirewallSchema.optional()
5352
+ experimentalFirewall: experimentalFirewallSchema.optional(),
5353
+ // Debug flag to force real Claude in mock environments (internal use only)
5354
+ debugNoMockClaude: z4.boolean().optional()
4976
5355
  });
4977
5356
  var runnersJobClaimContract = c.router({
4978
5357
  claim: {
@@ -5255,6 +5634,8 @@ var unifiedRunRequestSchema = z6.object({
5255
5634
  vars: z6.record(z6.string(), z6.string()).optional(),
5256
5635
  secrets: z6.record(z6.string(), z6.string()).optional(),
5257
5636
  volumeVersions: z6.record(z6.string(), z6.string()).optional(),
5637
+ // Debug flag to force real Claude in mock environments (internal use only)
5638
+ debugNoMockClaude: z6.boolean().optional(),
5258
5639
  // Required
5259
5640
  prompt: z6.string().min(1, "Missing prompt")
5260
5641
  });
@@ -6261,43 +6642,137 @@ var scopeContract = c9.router({
6261
6642
  }
6262
6643
  });
6263
6644
 
6264
- // ../../packages/core/src/contracts/sessions.ts
6645
+ // ../../packages/core/src/contracts/credentials.ts
6265
6646
  import { z as z14 } from "zod";
6266
6647
  var c10 = initContract();
6267
- var sessionResponseSchema = z14.object({
6268
- id: z14.string(),
6269
- agentComposeId: z14.string(),
6270
- agentComposeVersionId: z14.string().nullable(),
6271
- conversationId: z14.string().nullable(),
6272
- artifactName: z14.string().nullable(),
6273
- vars: z14.record(z14.string(), z14.string()).nullable(),
6274
- secretNames: z14.array(z14.string()).nullable(),
6275
- volumeVersions: z14.record(z14.string(), z14.string()).nullable(),
6648
+ var credentialNameSchema = z14.string().min(1, "Credential name is required").max(255, "Credential name must be at most 255 characters").regex(
6649
+ /^[A-Z][A-Z0-9_]*$/,
6650
+ "Credential name must contain only uppercase letters, numbers, and underscores, and must start with a letter (e.g., MY_API_KEY)"
6651
+ );
6652
+ var credentialResponseSchema = z14.object({
6653
+ id: z14.string().uuid(),
6654
+ name: z14.string(),
6655
+ description: z14.string().nullable(),
6276
6656
  createdAt: z14.string(),
6277
6657
  updatedAt: z14.string()
6278
6658
  });
6279
- var agentComposeSnapshotSchema = z14.object({
6280
- agentComposeVersionId: z14.string(),
6281
- vars: z14.record(z14.string(), z14.string()).optional(),
6282
- secretNames: z14.array(z14.string()).optional()
6659
+ var credentialListResponseSchema = z14.object({
6660
+ credentials: z14.array(credentialResponseSchema)
6661
+ });
6662
+ var setCredentialRequestSchema = z14.object({
6663
+ name: credentialNameSchema,
6664
+ value: z14.string().min(1, "Credential value is required"),
6665
+ description: z14.string().max(1e3).optional()
6666
+ });
6667
+ var credentialsMainContract = c10.router({
6668
+ /**
6669
+ * GET /api/credentials
6670
+ * List all credentials for the current user's scope (metadata only)
6671
+ */
6672
+ list: {
6673
+ method: "GET",
6674
+ path: "/api/credentials",
6675
+ responses: {
6676
+ 200: credentialListResponseSchema,
6677
+ 401: apiErrorSchema,
6678
+ 500: apiErrorSchema
6679
+ },
6680
+ summary: "List all credentials (metadata only)"
6681
+ },
6682
+ /**
6683
+ * PUT /api/credentials
6684
+ * Create or update a credential
6685
+ */
6686
+ set: {
6687
+ method: "PUT",
6688
+ path: "/api/credentials",
6689
+ body: setCredentialRequestSchema,
6690
+ responses: {
6691
+ 200: credentialResponseSchema,
6692
+ 201: credentialResponseSchema,
6693
+ 400: apiErrorSchema,
6694
+ 401: apiErrorSchema,
6695
+ 500: apiErrorSchema
6696
+ },
6697
+ summary: "Create or update a credential"
6698
+ }
6699
+ });
6700
+ var credentialsByNameContract = c10.router({
6701
+ /**
6702
+ * GET /api/credentials/:name
6703
+ * Get a credential by name (metadata only)
6704
+ */
6705
+ get: {
6706
+ method: "GET",
6707
+ path: "/api/credentials/:name",
6708
+ pathParams: z14.object({
6709
+ name: credentialNameSchema
6710
+ }),
6711
+ responses: {
6712
+ 200: credentialResponseSchema,
6713
+ 401: apiErrorSchema,
6714
+ 404: apiErrorSchema,
6715
+ 500: apiErrorSchema
6716
+ },
6717
+ summary: "Get credential metadata by name"
6718
+ },
6719
+ /**
6720
+ * DELETE /api/credentials/:name
6721
+ * Delete a credential by name
6722
+ */
6723
+ delete: {
6724
+ method: "DELETE",
6725
+ path: "/api/credentials/:name",
6726
+ pathParams: z14.object({
6727
+ name: credentialNameSchema
6728
+ }),
6729
+ responses: {
6730
+ 204: z14.undefined(),
6731
+ 401: apiErrorSchema,
6732
+ 404: apiErrorSchema,
6733
+ 500: apiErrorSchema
6734
+ },
6735
+ summary: "Delete a credential"
6736
+ }
6283
6737
  });
6284
- var artifactSnapshotSchema2 = z14.object({
6285
- artifactName: z14.string(),
6286
- artifactVersion: z14.string()
6738
+
6739
+ // ../../packages/core/src/contracts/sessions.ts
6740
+ import { z as z15 } from "zod";
6741
+ var c11 = initContract();
6742
+ var sessionResponseSchema = z15.object({
6743
+ id: z15.string(),
6744
+ agentComposeId: z15.string(),
6745
+ agentComposeVersionId: z15.string().nullable(),
6746
+ conversationId: z15.string().nullable(),
6747
+ artifactName: z15.string().nullable(),
6748
+ vars: z15.record(z15.string(), z15.string()).nullable(),
6749
+ secretNames: z15.array(z15.string()).nullable(),
6750
+ volumeVersions: z15.record(z15.string(), z15.string()).nullable(),
6751
+ createdAt: z15.string(),
6752
+ updatedAt: z15.string()
6287
6753
  });
6288
- var volumeVersionsSnapshotSchema2 = z14.object({
6289
- versions: z14.record(z14.string(), z14.string())
6754
+ var agentComposeSnapshotSchema = z15.object({
6755
+ agentComposeVersionId: z15.string(),
6756
+ vars: z15.record(z15.string(), z15.string()).optional(),
6757
+ secretNames: z15.array(z15.string()).optional()
6758
+ });
6759
+ var artifactSnapshotSchema2 = z15.object({
6760
+ artifactName: z15.string(),
6761
+ artifactVersion: z15.string()
6290
6762
  });
6291
- var checkpointResponseSchema = z14.object({
6292
- id: z14.string(),
6293
- runId: z14.string(),
6294
- conversationId: z14.string(),
6763
+ var volumeVersionsSnapshotSchema2 = z15.object({
6764
+ versions: z15.record(z15.string(), z15.string())
6765
+ });
6766
+ var checkpointResponseSchema = z15.object({
6767
+ id: z15.string(),
6768
+ runId: z15.string(),
6769
+ conversationId: z15.string(),
6295
6770
  agentComposeSnapshot: agentComposeSnapshotSchema,
6296
6771
  artifactSnapshot: artifactSnapshotSchema2.nullable(),
6297
6772
  volumeVersionsSnapshot: volumeVersionsSnapshotSchema2.nullable(),
6298
- createdAt: z14.string()
6773
+ createdAt: z15.string()
6299
6774
  });
6300
- var sessionsByIdContract = c10.router({
6775
+ var sessionsByIdContract = c11.router({
6301
6776
  /**
6302
6777
  * GET /api/agent/sessions/:id
6303
6778
  * Get session by ID
@@ -6305,8 +6780,8 @@ var sessionsByIdContract = c10.router({
6305
6780
  getById: {
6306
6781
  method: "GET",
6307
6782
  path: "/api/agent/sessions/:id",
6308
- pathParams: z14.object({
6309
- id: z14.string().min(1, "Session ID is required")
6783
+ pathParams: z15.object({
6784
+ id: z15.string().min(1, "Session ID is required")
6310
6785
  }),
6311
6786
  responses: {
6312
6787
  200: sessionResponseSchema,
@@ -6317,7 +6792,7 @@ var sessionsByIdContract = c10.router({
6317
6792
  summary: "Get session by ID"
6318
6793
  }
6319
6794
  });
6320
- var checkpointsByIdContract = c10.router({
6795
+ var checkpointsByIdContract = c11.router({
6321
6796
  /**
6322
6797
  * GET /api/agent/checkpoints/:id
6323
6798
  * Get checkpoint by ID
@@ -6325,8 +6800,8 @@ var checkpointsByIdContract = c10.router({
6325
6800
  getById: {
6326
6801
  method: "GET",
6327
6802
  path: "/api/agent/checkpoints/:id",
6328
- pathParams: z14.object({
6329
- id: z14.string().min(1, "Checkpoint ID is required")
6803
+ pathParams: z15.object({
6804
+ id: z15.string().min(1, "Checkpoint ID is required")
6330
6805
  }),
6331
6806
  responses: {
6332
6807
  200: checkpointResponseSchema,
@@ -6339,91 +6814,91 @@ var checkpointsByIdContract = c10.router({
6339
6814
  });
6340
6815
 
6341
6816
  // ../../packages/core/src/contracts/schedules.ts
6342
- import { z as z15 } from "zod";
6343
- var c11 = initContract();
6344
- var scheduleTriggerSchema = z15.object({
6345
- cron: z15.string().optional(),
6346
- at: z15.string().optional(),
6347
- timezone: z15.string().default("UTC")
6817
+ import { z as z16 } from "zod";
6818
+ var c12 = initContract();
6819
+ var scheduleTriggerSchema = z16.object({
6820
+ cron: z16.string().optional(),
6821
+ at: z16.string().optional(),
6822
+ timezone: z16.string().default("UTC")
6348
6823
  }).refine((data) => data.cron && !data.at || !data.cron && data.at, {
6349
6824
  message: "Exactly one of 'cron' or 'at' must be specified"
6350
6825
  });
6351
- var scheduleRunConfigSchema = z15.object({
6352
- agent: z15.string().min(1, "Agent reference required"),
6353
- prompt: z15.string().min(1, "Prompt required"),
6354
- vars: z15.record(z15.string(), z15.string()).optional(),
6355
- secrets: z15.record(z15.string(), z15.string()).optional(),
6356
- artifactName: z15.string().optional(),
6357
- artifactVersion: z15.string().optional(),
6358
- volumeVersions: z15.record(z15.string(), z15.string()).optional()
6826
+ var scheduleRunConfigSchema = z16.object({
6827
+ agent: z16.string().min(1, "Agent reference required"),
6828
+ prompt: z16.string().min(1, "Prompt required"),
6829
+ vars: z16.record(z16.string(), z16.string()).optional(),
6830
+ secrets: z16.record(z16.string(), z16.string()).optional(),
6831
+ artifactName: z16.string().optional(),
6832
+ artifactVersion: z16.string().optional(),
6833
+ volumeVersions: z16.record(z16.string(), z16.string()).optional()
6359
6834
  });
6360
- var scheduleDefinitionSchema = z15.object({
6835
+ var scheduleDefinitionSchema = z16.object({
6361
6836
  on: scheduleTriggerSchema,
6362
6837
  run: scheduleRunConfigSchema
6363
6838
  });
6364
- var scheduleYamlSchema = z15.object({
6365
- version: z15.literal("1.0"),
6366
- schedules: z15.record(z15.string(), scheduleDefinitionSchema)
6367
- });
6368
- var deployScheduleRequestSchema = z15.object({
6369
- name: z15.string().min(1).max(64, "Schedule name max 64 chars"),
6370
- cronExpression: z15.string().optional(),
6371
- atTime: z15.string().optional(),
6372
- timezone: z15.string().default("UTC"),
6373
- prompt: z15.string().min(1, "Prompt required"),
6374
- vars: z15.record(z15.string(), z15.string()).optional(),
6375
- secrets: z15.record(z15.string(), z15.string()).optional(),
6376
- artifactName: z15.string().optional(),
6377
- artifactVersion: z15.string().optional(),
6378
- volumeVersions: z15.record(z15.string(), z15.string()).optional(),
6839
+ var scheduleYamlSchema = z16.object({
6840
+ version: z16.literal("1.0"),
6841
+ schedules: z16.record(z16.string(), scheduleDefinitionSchema)
6842
+ });
6843
+ var deployScheduleRequestSchema = z16.object({
6844
+ name: z16.string().min(1).max(64, "Schedule name max 64 chars"),
6845
+ cronExpression: z16.string().optional(),
6846
+ atTime: z16.string().optional(),
6847
+ timezone: z16.string().default("UTC"),
6848
+ prompt: z16.string().min(1, "Prompt required"),
6849
+ vars: z16.record(z16.string(), z16.string()).optional(),
6850
+ secrets: z16.record(z16.string(), z16.string()).optional(),
6851
+ artifactName: z16.string().optional(),
6852
+ artifactVersion: z16.string().optional(),
6853
+ volumeVersions: z16.record(z16.string(), z16.string()).optional(),
6379
6854
  // Resolved agent compose ID (CLI resolves scope/name:version → composeId)
6380
- composeId: z15.string().uuid("Invalid compose ID")
6855
+ composeId: z16.string().uuid("Invalid compose ID")
6381
6856
  }).refine(
6382
6857
  (data) => data.cronExpression && !data.atTime || !data.cronExpression && data.atTime,
6383
6858
  {
6384
6859
  message: "Exactly one of 'cronExpression' or 'atTime' must be specified"
6385
6860
  }
6386
6861
  );
6387
- var scheduleResponseSchema = z15.object({
6388
- id: z15.string().uuid(),
6389
- composeId: z15.string().uuid(),
6390
- composeName: z15.string(),
6391
- scopeSlug: z15.string(),
6392
- name: z15.string(),
6393
- cronExpression: z15.string().nullable(),
6394
- atTime: z15.string().nullable(),
6395
- timezone: z15.string(),
6396
- prompt: z15.string(),
6397
- vars: z15.record(z15.string(), z15.string()).nullable(),
6862
+ var scheduleResponseSchema = z16.object({
6863
+ id: z16.string().uuid(),
6864
+ composeId: z16.string().uuid(),
6865
+ composeName: z16.string(),
6866
+ scopeSlug: z16.string(),
6867
+ name: z16.string(),
6868
+ cronExpression: z16.string().nullable(),
6869
+ atTime: z16.string().nullable(),
6870
+ timezone: z16.string(),
6871
+ prompt: z16.string(),
6872
+ vars: z16.record(z16.string(), z16.string()).nullable(),
6398
6873
  // Secret names only (values are never returned)
6399
- secretNames: z15.array(z15.string()).nullable(),
6400
- artifactName: z15.string().nullable(),
6401
- artifactVersion: z15.string().nullable(),
6402
- volumeVersions: z15.record(z15.string(), z15.string()).nullable(),
6403
- enabled: z15.boolean(),
6404
- nextRunAt: z15.string().nullable(),
6405
- createdAt: z15.string(),
6406
- updatedAt: z15.string()
6407
- });
6408
- var runSummarySchema = z15.object({
6409
- id: z15.string().uuid(),
6410
- status: z15.enum(["pending", "running", "completed", "failed", "timeout"]),
6411
- createdAt: z15.string(),
6412
- completedAt: z15.string().nullable(),
6413
- error: z15.string().nullable()
6414
- });
6415
- var scheduleRunsResponseSchema = z15.object({
6416
- runs: z15.array(runSummarySchema)
6417
- });
6418
- var scheduleListResponseSchema = z15.object({
6419
- schedules: z15.array(scheduleResponseSchema)
6420
- });
6421
- var deployScheduleResponseSchema = z15.object({
6874
+ secretNames: z16.array(z16.string()).nullable(),
6875
+ artifactName: z16.string().nullable(),
6876
+ artifactVersion: z16.string().nullable(),
6877
+ volumeVersions: z16.record(z16.string(), z16.string()).nullable(),
6878
+ enabled: z16.boolean(),
6879
+ nextRunAt: z16.string().nullable(),
6880
+ createdAt: z16.string(),
6881
+ updatedAt: z16.string()
6882
+ });
6883
+ var runSummarySchema = z16.object({
6884
+ id: z16.string().uuid(),
6885
+ status: z16.enum(["pending", "running", "completed", "failed", "timeout"]),
6886
+ createdAt: z16.string(),
6887
+ completedAt: z16.string().nullable(),
6888
+ error: z16.string().nullable()
6889
+ });
6890
+ var scheduleRunsResponseSchema = z16.object({
6891
+ runs: z16.array(runSummarySchema)
6892
+ });
6893
+ var scheduleListResponseSchema = z16.object({
6894
+ schedules: z16.array(scheduleResponseSchema)
6895
+ });
6896
+ var deployScheduleResponseSchema = z16.object({
6422
6897
  schedule: scheduleResponseSchema,
6423
- created: z15.boolean()
6898
+ created: z16.boolean()
6424
6899
  // true if created, false if updated
6425
6900
  });
6426
- var schedulesMainContract = c11.router({
6901
+ var schedulesMainContract = c12.router({
6427
6902
  /**
6428
6903
  * POST /api/agent/schedules
6429
6904
  * Deploy (create or update) a schedule
@@ -6459,7 +6934,7 @@ var schedulesMainContract = c11.router({
6459
6934
  summary: "List all schedules"
6460
6935
  }
6461
6936
  });
6462
- var schedulesByNameContract = c11.router({
6937
+ var schedulesByNameContract = c12.router({
6463
6938
  /**
6464
6939
  * GET /api/agent/schedules/:name
6465
6940
  * Get schedule by name
@@ -6467,11 +6942,11 @@ var schedulesByNameContract = c11.router({
6467
6942
  getByName: {
6468
6943
  method: "GET",
6469
6944
  path: "/api/agent/schedules/:name",
6470
- pathParams: z15.object({
6471
- name: z15.string().min(1, "Schedule name required")
6945
+ pathParams: z16.object({
6946
+ name: z16.string().min(1, "Schedule name required")
6472
6947
  }),
6473
- query: z15.object({
6474
- composeId: z15.string().uuid("Compose ID required")
6948
+ query: z16.object({
6949
+ composeId: z16.string().uuid("Compose ID required")
6475
6950
  }),
6476
6951
  responses: {
6477
6952
  200: scheduleResponseSchema,
@@ -6487,21 +6962,21 @@ var schedulesByNameContract = c11.router({
6487
6962
  delete: {
6488
6963
  method: "DELETE",
6489
6964
  path: "/api/agent/schedules/:name",
6490
- pathParams: z15.object({
6491
- name: z15.string().min(1, "Schedule name required")
6965
+ pathParams: z16.object({
6966
+ name: z16.string().min(1, "Schedule name required")
6492
6967
  }),
6493
- query: z15.object({
6494
- composeId: z15.string().uuid("Compose ID required")
6968
+ query: z16.object({
6969
+ composeId: z16.string().uuid("Compose ID required")
6495
6970
  }),
6496
6971
  responses: {
6497
- 204: z15.undefined(),
6972
+ 204: z16.undefined(),
6498
6973
  401: apiErrorSchema,
6499
6974
  404: apiErrorSchema
6500
6975
  },
6501
6976
  summary: "Delete schedule"
6502
6977
  }
6503
6978
  });
6504
- var schedulesEnableContract = c11.router({
6979
+ var schedulesEnableContract = c12.router({
6505
6980
  /**
6506
6981
  * POST /api/agent/schedules/:name/enable
6507
6982
  * Enable a disabled schedule
@@ -6509,11 +6984,11 @@ var schedulesEnableContract = c11.router({
6509
6984
  enable: {
6510
6985
  method: "POST",
6511
6986
  path: "/api/agent/schedules/:name/enable",
6512
- pathParams: z15.object({
6513
- name: z15.string().min(1, "Schedule name required")
6987
+ pathParams: z16.object({
6988
+ name: z16.string().min(1, "Schedule name required")
6514
6989
  }),
6515
- body: z15.object({
6516
- composeId: z15.string().uuid("Compose ID required")
6990
+ body: z16.object({
6991
+ composeId: z16.string().uuid("Compose ID required")
6517
6992
  }),
6518
6993
  responses: {
6519
6994
  200: scheduleResponseSchema,
@@ -6529,11 +7004,11 @@ var schedulesEnableContract = c11.router({
6529
7004
  disable: {
6530
7005
  method: "POST",
6531
7006
  path: "/api/agent/schedules/:name/disable",
6532
- pathParams: z15.object({
6533
- name: z15.string().min(1, "Schedule name required")
7007
+ pathParams: z16.object({
7008
+ name: z16.string().min(1, "Schedule name required")
6534
7009
  }),
6535
- body: z15.object({
6536
- composeId: z15.string().uuid("Compose ID required")
7010
+ body: z16.object({
7011
+ composeId: z16.string().uuid("Compose ID required")
6537
7012
  }),
6538
7013
  responses: {
6539
7014
  200: scheduleResponseSchema,
@@ -6543,7 +7018,7 @@ var schedulesEnableContract = c11.router({
6543
7018
  summary: "Disable schedule"
6544
7019
  }
6545
7020
  });
6546
- var scheduleRunsContract = c11.router({
7021
+ var scheduleRunsContract = c12.router({
6547
7022
  /**
6548
7023
  * GET /api/agent/schedules/:name/runs
6549
7024
  * List recent runs for a schedule
@@ -6551,12 +7026,12 @@ var scheduleRunsContract = c11.router({
6551
7026
  listRuns: {
6552
7027
  method: "GET",
6553
7028
  path: "/api/agent/schedules/:name/runs",
6554
- pathParams: z15.object({
6555
- name: z15.string().min(1, "Schedule name required")
7029
+ pathParams: z16.object({
7030
+ name: z16.string().min(1, "Schedule name required")
6556
7031
  }),
6557
- query: z15.object({
6558
- composeId: z15.string().uuid("Compose ID required"),
6559
- limit: z15.coerce.number().min(0).max(100).default(5)
7032
+ query: z16.object({
7033
+ composeId: z16.string().uuid("Compose ID required"),
7034
+ limit: z16.coerce.number().min(0).max(100).default(5)
6560
7035
  }),
6561
7036
  responses: {
6562
7037
  200: scheduleRunsResponseSchema,
@@ -6568,8 +7043,8 @@ var scheduleRunsContract = c11.router({
6568
7043
  });
6569
7044
 
6570
7045
  // ../../packages/core/src/contracts/public/common.ts
6571
- import { z as z16 } from "zod";
6572
- var publicApiErrorTypeSchema = z16.enum([
7046
+ import { z as z17 } from "zod";
7047
+ var publicApiErrorTypeSchema = z17.enum([
6573
7048
  "api_error",
6574
7049
  // Internal server error (5xx)
6575
7050
  "invalid_request_error",
@@ -6581,55 +7056,55 @@ var publicApiErrorTypeSchema = z16.enum([
6581
7056
  "conflict_error"
6582
7057
  // Resource conflict (409)
6583
7058
  ]);
6584
- var publicApiErrorSchema = z16.object({
6585
- error: z16.object({
7059
+ var publicApiErrorSchema = z17.object({
7060
+ error: z17.object({
6586
7061
  type: publicApiErrorTypeSchema,
6587
- code: z16.string(),
6588
- message: z16.string(),
6589
- param: z16.string().optional(),
6590
- doc_url: z16.string().url().optional()
7062
+ code: z17.string(),
7063
+ message: z17.string(),
7064
+ param: z17.string().optional(),
7065
+ doc_url: z17.string().url().optional()
6591
7066
  })
6592
7067
  });
6593
- var paginationSchema = z16.object({
6594
- has_more: z16.boolean(),
6595
- next_cursor: z16.string().nullable()
7068
+ var paginationSchema = z17.object({
7069
+ has_more: z17.boolean(),
7070
+ next_cursor: z17.string().nullable()
6596
7071
  });
6597
7072
  function createPaginatedResponseSchema(dataSchema) {
6598
- return z16.object({
6599
- data: z16.array(dataSchema),
7073
+ return z17.object({
7074
+ data: z17.array(dataSchema),
6600
7075
  pagination: paginationSchema
6601
7076
  });
6602
7077
  }
6603
- var listQuerySchema = z16.object({
6604
- cursor: z16.string().optional(),
6605
- limit: z16.coerce.number().min(1).max(100).default(20)
7078
+ var listQuerySchema = z17.object({
7079
+ cursor: z17.string().optional(),
7080
+ limit: z17.coerce.number().min(1).max(100).default(20)
6606
7081
  });
6607
- var requestIdSchema = z16.string().uuid();
6608
- var timestampSchema = z16.string().datetime();
7082
+ var requestIdSchema = z17.string().uuid();
7083
+ var timestampSchema = z17.string().datetime();
6609
7084
 
6610
7085
  // ../../packages/core/src/contracts/public/agents.ts
6611
- import { z as z17 } from "zod";
6612
- var c12 = initContract();
6613
- var publicAgentSchema = z17.object({
6614
- id: z17.string(),
6615
- name: z17.string(),
6616
- current_version_id: z17.string().nullable(),
7086
+ import { z as z18 } from "zod";
7087
+ var c13 = initContract();
7088
+ var publicAgentSchema = z18.object({
7089
+ id: z18.string(),
7090
+ name: z18.string(),
7091
+ current_version_id: z18.string().nullable(),
6617
7092
  created_at: timestampSchema,
6618
7093
  updated_at: timestampSchema
6619
7094
  });
6620
- var agentVersionSchema = z17.object({
6621
- id: z17.string(),
6622
- agent_id: z17.string(),
6623
- version_number: z17.number(),
7095
+ var agentVersionSchema = z18.object({
7096
+ id: z18.string(),
7097
+ agent_id: z18.string(),
7098
+ version_number: z18.number(),
6624
7099
  created_at: timestampSchema
6625
7100
  });
6626
7101
  var publicAgentDetailSchema = publicAgentSchema;
6627
7102
  var paginatedAgentsSchema = createPaginatedResponseSchema(publicAgentSchema);
6628
7103
  var paginatedAgentVersionsSchema = createPaginatedResponseSchema(agentVersionSchema);
6629
7104
  var agentListQuerySchema = listQuerySchema.extend({
6630
- name: z17.string().optional()
7105
+ name: z18.string().optional()
6631
7106
  });
6632
- var publicAgentsListContract = c12.router({
7107
+ var publicAgentsListContract = c13.router({
6633
7108
  list: {
6634
7109
  method: "GET",
6635
7110
  path: "/v1/agents",
@@ -6643,12 +7118,12 @@ var publicAgentsListContract = c12.router({
6643
7118
  description: "List all agents in the current scope with pagination. Use the `name` query parameter to filter by agent name."
6644
7119
  }
6645
7120
  });
6646
- var publicAgentByIdContract = c12.router({
7121
+ var publicAgentByIdContract = c13.router({
6647
7122
  get: {
6648
7123
  method: "GET",
6649
7124
  path: "/v1/agents/:id",
6650
- pathParams: z17.object({
6651
- id: z17.string().min(1, "Agent ID is required")
7125
+ pathParams: z18.object({
7126
+ id: z18.string().min(1, "Agent ID is required")
6652
7127
  }),
6653
7128
  responses: {
6654
7129
  200: publicAgentDetailSchema,
@@ -6660,12 +7135,12 @@ var publicAgentByIdContract = c12.router({
6660
7135
  description: "Get agent details by ID"
6661
7136
  }
6662
7137
  });
6663
- var publicAgentVersionsContract = c12.router({
7138
+ var publicAgentVersionsContract = c13.router({
6664
7139
  list: {
6665
7140
  method: "GET",
6666
7141
  path: "/v1/agents/:id/versions",
6667
- pathParams: z17.object({
6668
- id: z17.string().min(1, "Agent ID is required")
7142
+ pathParams: z18.object({
7143
+ id: z18.string().min(1, "Agent ID is required")
6669
7144
  }),
6670
7145
  query: listQuerySchema,
6671
7146
  responses: {
@@ -6680,9 +7155,9 @@ var publicAgentVersionsContract = c12.router({
6680
7155
  });
6681
7156
 
6682
7157
  // ../../packages/core/src/contracts/public/runs.ts
6683
- import { z as z18 } from "zod";
6684
- var c13 = initContract();
6685
- var publicRunStatusSchema = z18.enum([
7158
+ import { z as z19 } from "zod";
7159
+ var c14 = initContract();
7160
+ var publicRunStatusSchema = z19.enum([
6686
7161
  "pending",
6687
7162
  "running",
6688
7163
  "completed",
@@ -6690,56 +7165,56 @@ var publicRunStatusSchema = z18.enum([
6690
7165
  "timeout",
6691
7166
  "cancelled"
6692
7167
  ]);
6693
- var publicRunSchema = z18.object({
6694
- id: z18.string(),
6695
- agent_id: z18.string(),
6696
- agent_name: z18.string(),
7168
+ var publicRunSchema = z19.object({
7169
+ id: z19.string(),
7170
+ agent_id: z19.string(),
7171
+ agent_name: z19.string(),
6697
7172
  status: publicRunStatusSchema,
6698
- prompt: z18.string(),
7173
+ prompt: z19.string(),
6699
7174
  created_at: timestampSchema,
6700
7175
  started_at: timestampSchema.nullable(),
6701
7176
  completed_at: timestampSchema.nullable()
6702
7177
  });
6703
7178
  var publicRunDetailSchema = publicRunSchema.extend({
6704
- error: z18.string().nullable(),
6705
- execution_time_ms: z18.number().nullable(),
6706
- checkpoint_id: z18.string().nullable(),
6707
- session_id: z18.string().nullable(),
6708
- artifact_name: z18.string().nullable(),
6709
- artifact_version: z18.string().nullable(),
6710
- volumes: z18.record(z18.string(), z18.string()).optional()
7179
+ error: z19.string().nullable(),
7180
+ execution_time_ms: z19.number().nullable(),
7181
+ checkpoint_id: z19.string().nullable(),
7182
+ session_id: z19.string().nullable(),
7183
+ artifact_name: z19.string().nullable(),
7184
+ artifact_version: z19.string().nullable(),
7185
+ volumes: z19.record(z19.string(), z19.string()).optional()
6711
7186
  });
6712
7187
  var paginatedRunsSchema = createPaginatedResponseSchema(publicRunSchema);
6713
- var createRunRequestSchema = z18.object({
7188
+ var createRunRequestSchema = z19.object({
6714
7189
  // Agent identification (one of: agent, agent_id, session_id, checkpoint_id)
6715
- agent: z18.string().optional(),
7190
+ agent: z19.string().optional(),
6716
7191
  // Agent name
6717
- agent_id: z18.string().optional(),
7192
+ agent_id: z19.string().optional(),
6718
7193
  // Agent ID
6719
- agent_version: z18.string().optional(),
7194
+ agent_version: z19.string().optional(),
6720
7195
  // Version specifier (e.g., "latest", "v1", specific ID)
6721
7196
  // Continue session
6722
- session_id: z18.string().optional(),
7197
+ session_id: z19.string().optional(),
6723
7198
  // Resume from checkpoint
6724
- checkpoint_id: z18.string().optional(),
7199
+ checkpoint_id: z19.string().optional(),
6725
7200
  // Required
6726
- prompt: z18.string().min(1, "Prompt is required"),
7201
+ prompt: z19.string().min(1, "Prompt is required"),
6727
7202
  // Optional configuration
6728
- variables: z18.record(z18.string(), z18.string()).optional(),
6729
- secrets: z18.record(z18.string(), z18.string()).optional(),
6730
- artifact_name: z18.string().optional(),
7203
+ variables: z19.record(z19.string(), z19.string()).optional(),
7204
+ secrets: z19.record(z19.string(), z19.string()).optional(),
7205
+ artifact_name: z19.string().optional(),
6731
7206
  // Artifact name to mount
6732
- artifact_version: z18.string().optional(),
7207
+ artifact_version: z19.string().optional(),
6733
7208
  // Artifact version (defaults to latest)
6734
- volumes: z18.record(z18.string(), z18.string()).optional()
7209
+ volumes: z19.record(z19.string(), z19.string()).optional()
6735
7210
  // volume_name -> version
6736
7211
  });
6737
7212
  var runListQuerySchema = listQuerySchema.extend({
6738
- agent_id: z18.string().optional(),
7213
+ agent_id: z19.string().optional(),
6739
7214
  status: publicRunStatusSchema.optional(),
6740
7215
  since: timestampSchema.optional()
6741
7216
  });
6742
- var publicRunsListContract = c13.router({
7217
+ var publicRunsListContract = c14.router({
6743
7218
  list: {
6744
7219
  method: "GET",
6745
7220
  path: "/v1/runs",
@@ -6768,12 +7243,12 @@ var publicRunsListContract = c13.router({
6768
7243
  description: "Create and execute a new agent run. Returns 202 Accepted as runs execute asynchronously."
6769
7244
  }
6770
7245
  });
6771
- var publicRunByIdContract = c13.router({
7246
+ var publicRunByIdContract = c14.router({
6772
7247
  get: {
6773
7248
  method: "GET",
6774
7249
  path: "/v1/runs/:id",
6775
- pathParams: z18.object({
6776
- id: z18.string().min(1, "Run ID is required")
7250
+ pathParams: z19.object({
7251
+ id: z19.string().min(1, "Run ID is required")
6777
7252
  }),
6778
7253
  responses: {
6779
7254
  200: publicRunDetailSchema,
@@ -6785,14 +7260,14 @@ var publicRunByIdContract = c13.router({
6785
7260
  description: "Get run details by ID"
6786
7261
  }
6787
7262
  });
6788
- var publicRunCancelContract = c13.router({
7263
+ var publicRunCancelContract = c14.router({
6789
7264
  cancel: {
6790
7265
  method: "POST",
6791
7266
  path: "/v1/runs/:id/cancel",
6792
- pathParams: z18.object({
6793
- id: z18.string().min(1, "Run ID is required")
7267
+ pathParams: z19.object({
7268
+ id: z19.string().min(1, "Run ID is required")
6794
7269
  }),
6795
- body: z18.undefined(),
7270
+ body: z19.undefined(),
6796
7271
  responses: {
6797
7272
  200: publicRunDetailSchema,
6798
7273
  400: publicApiErrorSchema,
@@ -6805,26 +7280,26 @@ var publicRunCancelContract = c13.router({
6805
7280
  description: "Cancel a pending or running execution"
6806
7281
  }
6807
7282
  });
6808
- var logEntrySchema = z18.object({
7283
+ var logEntrySchema = z19.object({
6809
7284
  timestamp: timestampSchema,
6810
- type: z18.enum(["agent", "system", "network"]),
6811
- level: z18.enum(["debug", "info", "warn", "error"]),
6812
- message: z18.string(),
6813
- metadata: z18.record(z18.string(), z18.unknown()).optional()
7285
+ type: z19.enum(["agent", "system", "network"]),
7286
+ level: z19.enum(["debug", "info", "warn", "error"]),
7287
+ message: z19.string(),
7288
+ metadata: z19.record(z19.string(), z19.unknown()).optional()
6814
7289
  });
6815
7290
  var paginatedLogsSchema = createPaginatedResponseSchema(logEntrySchema);
6816
7291
  var logsQuerySchema = listQuerySchema.extend({
6817
- type: z18.enum(["agent", "system", "network", "all"]).default("all"),
7292
+ type: z19.enum(["agent", "system", "network", "all"]).default("all"),
6818
7293
  since: timestampSchema.optional(),
6819
7294
  until: timestampSchema.optional(),
6820
- order: z18.enum(["asc", "desc"]).default("asc")
7295
+ order: z19.enum(["asc", "desc"]).default("asc")
6821
7296
  });
6822
- var publicRunLogsContract = c13.router({
7297
+ var publicRunLogsContract = c14.router({
6823
7298
  getLogs: {
6824
7299
  method: "GET",
6825
7300
  path: "/v1/runs/:id/logs",
6826
- pathParams: z18.object({
6827
- id: z18.string().min(1, "Run ID is required")
7301
+ pathParams: z19.object({
7302
+ id: z19.string().min(1, "Run ID is required")
6828
7303
  }),
6829
7304
  query: logsQuerySchema,
6830
7305
  responses: {
@@ -6837,29 +7312,29 @@ var publicRunLogsContract = c13.router({
6837
7312
  description: "Get unified logs for a run. Combines agent, system, and network logs."
6838
7313
  }
6839
7314
  });
6840
- var metricPointSchema = z18.object({
7315
+ var metricPointSchema = z19.object({
6841
7316
  timestamp: timestampSchema,
6842
- cpu_percent: z18.number(),
6843
- memory_used_mb: z18.number(),
6844
- memory_total_mb: z18.number(),
6845
- disk_used_mb: z18.number(),
6846
- disk_total_mb: z18.number()
6847
- });
6848
- var metricsSummarySchema = z18.object({
6849
- avg_cpu_percent: z18.number(),
6850
- max_memory_used_mb: z18.number(),
6851
- total_duration_ms: z18.number().nullable()
6852
- });
6853
- var metricsResponseSchema2 = z18.object({
6854
- data: z18.array(metricPointSchema),
7317
+ cpu_percent: z19.number(),
7318
+ memory_used_mb: z19.number(),
7319
+ memory_total_mb: z19.number(),
7320
+ disk_used_mb: z19.number(),
7321
+ disk_total_mb: z19.number()
7322
+ });
7323
+ var metricsSummarySchema = z19.object({
7324
+ avg_cpu_percent: z19.number(),
7325
+ max_memory_used_mb: z19.number(),
7326
+ total_duration_ms: z19.number().nullable()
7327
+ });
7328
+ var metricsResponseSchema2 = z19.object({
7329
+ data: z19.array(metricPointSchema),
6855
7330
  summary: metricsSummarySchema
6856
7331
  });
6857
- var publicRunMetricsContract = c13.router({
7332
+ var publicRunMetricsContract = c14.router({
6858
7333
  getMetrics: {
6859
7334
  method: "GET",
6860
7335
  path: "/v1/runs/:id/metrics",
6861
- pathParams: z18.object({
6862
- id: z18.string().min(1, "Run ID is required")
7336
+ pathParams: z19.object({
7337
+ id: z19.string().min(1, "Run ID is required")
6863
7338
  }),
6864
7339
  responses: {
6865
7340
  200: metricsResponseSchema2,
@@ -6871,7 +7346,7 @@ var publicRunMetricsContract = c13.router({
6871
7346
  description: "Get CPU, memory, and disk metrics for a run"
6872
7347
  }
6873
7348
  });
6874
- var sseEventTypeSchema = z18.enum([
7349
+ var sseEventTypeSchema = z19.enum([
6875
7350
  "status",
6876
7351
  // Run status change
6877
7352
  "output",
@@ -6883,25 +7358,25 @@ var sseEventTypeSchema = z18.enum([
6883
7358
  "heartbeat"
6884
7359
  // Keep-alive
6885
7360
  ]);
6886
- var sseEventSchema = z18.object({
7361
+ var sseEventSchema = z19.object({
6887
7362
  event: sseEventTypeSchema,
6888
- data: z18.unknown(),
6889
- id: z18.string().optional()
7363
+ data: z19.unknown(),
7364
+ id: z19.string().optional()
6890
7365
  // For Last-Event-ID reconnection
6891
7366
  });
6892
- var publicRunEventsContract = c13.router({
7367
+ var publicRunEventsContract = c14.router({
6893
7368
  streamEvents: {
6894
7369
  method: "GET",
6895
7370
  path: "/v1/runs/:id/events",
6896
- pathParams: z18.object({
6897
- id: z18.string().min(1, "Run ID is required")
7371
+ pathParams: z19.object({
7372
+ id: z19.string().min(1, "Run ID is required")
6898
7373
  }),
6899
- query: z18.object({
6900
- last_event_id: z18.string().optional()
7374
+ query: z19.object({
7375
+ last_event_id: z19.string().optional()
6901
7376
  // For reconnection
6902
7377
  }),
6903
7378
  responses: {
6904
- 200: z18.any(),
7379
+ 200: z19.any(),
6905
7380
  // SSE stream - actual content is text/event-stream
6906
7381
  401: publicApiErrorSchema,
6907
7382
  404: publicApiErrorSchema,
@@ -6913,28 +7388,28 @@ var publicRunEventsContract = c13.router({
6913
7388
  });
6914
7389
 
6915
7390
  // ../../packages/core/src/contracts/public/artifacts.ts
6916
- import { z as z19 } from "zod";
6917
- var c14 = initContract();
6918
- var publicArtifactSchema = z19.object({
6919
- id: z19.string(),
6920
- name: z19.string(),
6921
- current_version_id: z19.string().nullable(),
6922
- size: z19.number(),
7391
+ import { z as z20 } from "zod";
7392
+ var c15 = initContract();
7393
+ var publicArtifactSchema = z20.object({
7394
+ id: z20.string(),
7395
+ name: z20.string(),
7396
+ current_version_id: z20.string().nullable(),
7397
+ size: z20.number(),
6923
7398
  // Total size in bytes
6924
- file_count: z19.number(),
7399
+ file_count: z20.number(),
6925
7400
  created_at: timestampSchema,
6926
7401
  updated_at: timestampSchema
6927
7402
  });
6928
- var artifactVersionSchema = z19.object({
6929
- id: z19.string(),
7403
+ var artifactVersionSchema = z20.object({
7404
+ id: z20.string(),
6930
7405
  // SHA-256 content hash
6931
- artifact_id: z19.string(),
6932
- size: z19.number(),
7406
+ artifact_id: z20.string(),
7407
+ size: z20.number(),
6933
7408
  // Size in bytes
6934
- file_count: z19.number(),
6935
- message: z19.string().nullable(),
7409
+ file_count: z20.number(),
7410
+ message: z20.string().nullable(),
6936
7411
  // Optional commit message
6937
- created_by: z19.string(),
7412
+ created_by: z20.string(),
6938
7413
  created_at: timestampSchema
6939
7414
  });
6940
7415
  var publicArtifactDetailSchema = publicArtifactSchema.extend({
@@ -6944,7 +7419,7 @@ var paginatedArtifactsSchema = createPaginatedResponseSchema(publicArtifactSchem
6944
7419
  var paginatedArtifactVersionsSchema = createPaginatedResponseSchema(
6945
7420
  artifactVersionSchema
6946
7421
  );
6947
- var publicArtifactsListContract = c14.router({
7422
+ var publicArtifactsListContract = c15.router({
6948
7423
  list: {
6949
7424
  method: "GET",
6950
7425
  path: "/v1/artifacts",
@@ -6958,12 +7433,12 @@ var publicArtifactsListContract = c14.router({
6958
7433
  description: "List all artifacts in the current scope with pagination"
6959
7434
  }
6960
7435
  });
6961
- var publicArtifactByIdContract = c14.router({
7436
+ var publicArtifactByIdContract = c15.router({
6962
7437
  get: {
6963
7438
  method: "GET",
6964
7439
  path: "/v1/artifacts/:id",
6965
- pathParams: z19.object({
6966
- id: z19.string().min(1, "Artifact ID is required")
7440
+ pathParams: z20.object({
7441
+ id: z20.string().min(1, "Artifact ID is required")
6967
7442
  }),
6968
7443
  responses: {
6969
7444
  200: publicArtifactDetailSchema,
@@ -6975,12 +7450,12 @@ var publicArtifactByIdContract = c14.router({
6975
7450
  description: "Get artifact details by ID"
6976
7451
  }
6977
7452
  });
6978
- var publicArtifactVersionsContract = c14.router({
7453
+ var publicArtifactVersionsContract = c15.router({
6979
7454
  list: {
6980
7455
  method: "GET",
6981
7456
  path: "/v1/artifacts/:id/versions",
6982
- pathParams: z19.object({
6983
- id: z19.string().min(1, "Artifact ID is required")
7457
+ pathParams: z20.object({
7458
+ id: z20.string().min(1, "Artifact ID is required")
6984
7459
  }),
6985
7460
  query: listQuerySchema,
6986
7461
  responses: {
@@ -6993,19 +7468,19 @@ var publicArtifactVersionsContract = c14.router({
6993
7468
  description: "List all versions of an artifact with pagination"
6994
7469
  }
6995
7470
  });
6996
- var publicArtifactDownloadContract = c14.router({
7471
+ var publicArtifactDownloadContract = c15.router({
6997
7472
  download: {
6998
7473
  method: "GET",
6999
7474
  path: "/v1/artifacts/:id/download",
7000
- pathParams: z19.object({
7001
- id: z19.string().min(1, "Artifact ID is required")
7475
+ pathParams: z20.object({
7476
+ id: z20.string().min(1, "Artifact ID is required")
7002
7477
  }),
7003
- query: z19.object({
7004
- version_id: z19.string().optional()
7478
+ query: z20.object({
7479
+ version_id: z20.string().optional()
7005
7480
  // Defaults to current version
7006
7481
  }),
7007
7482
  responses: {
7008
- 302: z19.undefined(),
7483
+ 302: z20.undefined(),
7009
7484
  // Redirect to presigned URL
7010
7485
  401: publicApiErrorSchema,
7011
7486
  404: publicApiErrorSchema,
@@ -7017,28 +7492,28 @@ var publicArtifactDownloadContract = c14.router({
7017
7492
  });
7018
7493
 
7019
7494
  // ../../packages/core/src/contracts/public/volumes.ts
7020
- import { z as z20 } from "zod";
7021
- var c15 = initContract();
7022
- var publicVolumeSchema = z20.object({
7023
- id: z20.string(),
7024
- name: z20.string(),
7025
- current_version_id: z20.string().nullable(),
7026
- size: z20.number(),
7495
+ import { z as z21 } from "zod";
7496
+ var c16 = initContract();
7497
+ var publicVolumeSchema = z21.object({
7498
+ id: z21.string(),
7499
+ name: z21.string(),
7500
+ current_version_id: z21.string().nullable(),
7501
+ size: z21.number(),
7027
7502
  // Total size in bytes
7028
- file_count: z20.number(),
7503
+ file_count: z21.number(),
7029
7504
  created_at: timestampSchema,
7030
7505
  updated_at: timestampSchema
7031
7506
  });
7032
- var volumeVersionSchema = z20.object({
7033
- id: z20.string(),
7507
+ var volumeVersionSchema = z21.object({
7508
+ id: z21.string(),
7034
7509
  // SHA-256 content hash
7035
- volume_id: z20.string(),
7036
- size: z20.number(),
7510
+ volume_id: z21.string(),
7511
+ size: z21.number(),
7037
7512
  // Size in bytes
7038
- file_count: z20.number(),
7039
- message: z20.string().nullable(),
7513
+ file_count: z21.number(),
7514
+ message: z21.string().nullable(),
7040
7515
  // Optional commit message
7041
- created_by: z20.string(),
7516
+ created_by: z21.string(),
7042
7517
  created_at: timestampSchema
7043
7518
  });
7044
7519
  var publicVolumeDetailSchema = publicVolumeSchema.extend({
@@ -7046,7 +7521,7 @@ var publicVolumeDetailSchema = publicVolumeSchema.extend({
7046
7521
  });
7047
7522
  var paginatedVolumesSchema = createPaginatedResponseSchema(publicVolumeSchema);
7048
7523
  var paginatedVolumeVersionsSchema = createPaginatedResponseSchema(volumeVersionSchema);
7049
- var publicVolumesListContract = c15.router({
7524
+ var publicVolumesListContract = c16.router({
7050
7525
  list: {
7051
7526
  method: "GET",
7052
7527
  path: "/v1/volumes",
@@ -7060,12 +7535,12 @@ var publicVolumesListContract = c15.router({
7060
7535
  description: "List all volumes in the current scope with pagination"
7061
7536
  }
7062
7537
  });
7063
- var publicVolumeByIdContract = c15.router({
7538
+ var publicVolumeByIdContract = c16.router({
7064
7539
  get: {
7065
7540
  method: "GET",
7066
7541
  path: "/v1/volumes/:id",
7067
- pathParams: z20.object({
7068
- id: z20.string().min(1, "Volume ID is required")
7542
+ pathParams: z21.object({
7543
+ id: z21.string().min(1, "Volume ID is required")
7069
7544
  }),
7070
7545
  responses: {
7071
7546
  200: publicVolumeDetailSchema,
@@ -7077,12 +7552,12 @@ var publicVolumeByIdContract = c15.router({
7077
7552
  description: "Get volume details by ID"
7078
7553
  }
7079
7554
  });
7080
- var publicVolumeVersionsContract = c15.router({
7555
+ var publicVolumeVersionsContract = c16.router({
7081
7556
  list: {
7082
7557
  method: "GET",
7083
7558
  path: "/v1/volumes/:id/versions",
7084
- pathParams: z20.object({
7085
- id: z20.string().min(1, "Volume ID is required")
7559
+ pathParams: z21.object({
7560
+ id: z21.string().min(1, "Volume ID is required")
7086
7561
  }),
7087
7562
  query: listQuerySchema,
7088
7563
  responses: {
@@ -7095,19 +7570,19 @@ var publicVolumeVersionsContract = c15.router({
7095
7570
  description: "List all versions of a volume with pagination"
7096
7571
  }
7097
7572
  });
7098
- var publicVolumeDownloadContract = c15.router({
7573
+ var publicVolumeDownloadContract = c16.router({
7099
7574
  download: {
7100
7575
  method: "GET",
7101
7576
  path: "/v1/volumes/:id/download",
7102
- pathParams: z20.object({
7103
- id: z20.string().min(1, "Volume ID is required")
7577
+ pathParams: z21.object({
7578
+ id: z21.string().min(1, "Volume ID is required")
7104
7579
  }),
7105
- query: z20.object({
7106
- version_id: z20.string().optional()
7580
+ query: z21.object({
7581
+ version_id: z21.string().optional()
7107
7582
  // Defaults to current version
7108
7583
  }),
7109
7584
  responses: {
7110
- 302: z20.undefined(),
7585
+ 302: z21.undefined(),
7111
7586
  // Redirect to presigned URL
7112
7587
  401: publicApiErrorSchema,
7113
7588
  404: publicApiErrorSchema,
@@ -7138,11 +7613,21 @@ var SCRIPT_PATHS = {
7138
7613
  envLoader: "/usr/local/bin/vm0-agent/env-loader.mjs"
7139
7614
  };
7140
7615
 
7616
+ // ../../packages/core/src/feature-switch.ts
7617
+ var PricingSwitch = {
7618
+ key: "pricing" /* Pricing */,
7619
+ maintainer: "ethan@vm0.ai",
7620
+ enabled: false
7621
+ };
7622
+ var FEATURE_SWITCHES = {
7623
+ ["pricing" /* Pricing */]: PricingSwitch
7624
+ };
7625
+
7141
7626
  // src/lib/scripts/index.ts
7142
7627
  var ENV_LOADER_PATH = "/usr/local/bin/vm0-agent/env-loader.mjs";
7143
7628
 
7144
7629
  // src/lib/proxy/vm-registry.ts
7145
- import fs4 from "fs";
7630
+ import fs5 from "fs";
7146
7631
  var DEFAULT_REGISTRY_PATH = "/tmp/vm0-vm-registry.json";
7147
7632
  var VMRegistry = class {
7148
7633
  registryPath;
@@ -7156,8 +7641,8 @@ var VMRegistry = class {
7156
7641
  */
7157
7642
  load() {
7158
7643
  try {
7159
- if (fs4.existsSync(this.registryPath)) {
7160
- const content = fs4.readFileSync(this.registryPath, "utf-8");
7644
+ if (fs5.existsSync(this.registryPath)) {
7645
+ const content = fs5.readFileSync(this.registryPath, "utf-8");
7161
7646
  return JSON.parse(content);
7162
7647
  }
7163
7648
  } catch {
@@ -7171,8 +7656,8 @@ var VMRegistry = class {
7171
7656
  this.data.updatedAt = Date.now();
7172
7657
  const content = JSON.stringify(this.data, null, 2);
7173
7658
  const tempPath = `${this.registryPath}.tmp`;
7174
- fs4.writeFileSync(tempPath, content, { mode: 420 });
7175
- fs4.renameSync(tempPath, this.registryPath);
7659
+ fs5.writeFileSync(tempPath, content, { mode: 420 });
7660
+ fs5.renameSync(tempPath, this.registryPath);
7176
7661
  }
7177
7662
  /**
7178
7663
  * Register a VM with its IP address
@@ -7247,8 +7732,8 @@ function initVMRegistry(registryPath) {
7247
7732
 
7248
7733
  // src/lib/proxy/proxy-manager.ts
7249
7734
  import { spawn as spawn2 } from "child_process";
7250
- import fs5 from "fs";
7251
- import path3 from "path";
7735
+ import fs6 from "fs";
7736
+ import path4 from "path";
7252
7737
 
7253
7738
  // src/lib/proxy/mitm-addon-script.ts
7254
7739
  var RUNNER_MITM_ADDON_SCRIPT = `#!/usr/bin/env python3
@@ -7765,11 +8250,11 @@ var ProxyManager = class {
7765
8250
  * Ensure the addon script exists at the configured path
7766
8251
  */
7767
8252
  ensureAddonScript() {
7768
- const addonDir = path3.dirname(this.config.addonPath);
7769
- if (!fs5.existsSync(addonDir)) {
7770
- fs5.mkdirSync(addonDir, { recursive: true });
8253
+ const addonDir = path4.dirname(this.config.addonPath);
8254
+ if (!fs6.existsSync(addonDir)) {
8255
+ fs6.mkdirSync(addonDir, { recursive: true });
7771
8256
  }
7772
- fs5.writeFileSync(this.config.addonPath, RUNNER_MITM_ADDON_SCRIPT, {
8257
+ fs6.writeFileSync(this.config.addonPath, RUNNER_MITM_ADDON_SCRIPT, {
7773
8258
  mode: 493
7774
8259
  });
7775
8260
  console.log(
@@ -7780,11 +8265,11 @@ var ProxyManager = class {
7780
8265
  * Validate proxy configuration
7781
8266
  */
7782
8267
  validateConfig() {
7783
- if (!fs5.existsSync(this.config.caDir)) {
8268
+ if (!fs6.existsSync(this.config.caDir)) {
7784
8269
  throw new Error(`Proxy CA directory not found: ${this.config.caDir}`);
7785
8270
  }
7786
- const caCertPath = path3.join(this.config.caDir, "mitmproxy-ca.pem");
7787
- if (!fs5.existsSync(caCertPath)) {
8271
+ const caCertPath = path4.join(this.config.caDir, "mitmproxy-ca.pem");
8272
+ if (!fs6.existsSync(caCertPath)) {
7788
8273
  throw new Error(`Proxy CA certificate not found: ${caCertPath}`);
7789
8274
  }
7790
8275
  this.ensureAddonScript();
@@ -8142,7 +8627,7 @@ function buildEnvironmentVariables(context, apiUrl) {
8142
8627
  envVars.VERCEL_PROTECTION_BYPASS = vercelBypass;
8143
8628
  }
8144
8629
  const useMockClaude = process.env.USE_MOCK_CLAUDE;
8145
- if (useMockClaude) {
8630
+ if (useMockClaude && !context.debugNoMockClaude) {
8146
8631
  envVars.USE_MOCK_CLAUDE = useMockClaude;
8147
8632
  }
8148
8633
  if (context.storageManifest?.artifact) {
@@ -8173,17 +8658,17 @@ function buildEnvironmentVariables(context, apiUrl) {
8173
8658
  }
8174
8659
 
8175
8660
  // src/lib/network-logs/network-logs.ts
8176
- import fs6 from "fs";
8661
+ import fs7 from "fs";
8177
8662
  function getNetworkLogPath(runId) {
8178
8663
  return `/tmp/vm0-network-${runId}.jsonl`;
8179
8664
  }
8180
8665
  function readNetworkLogs(runId) {
8181
8666
  const logPath = getNetworkLogPath(runId);
8182
- if (!fs6.existsSync(logPath)) {
8667
+ if (!fs7.existsSync(logPath)) {
8183
8668
  return [];
8184
8669
  }
8185
8670
  try {
8186
- const content = fs6.readFileSync(logPath, "utf-8");
8671
+ const content = fs7.readFileSync(logPath, "utf-8");
8187
8672
  const lines = content.split("\n").filter((line) => line.trim());
8188
8673
  return lines.map((line) => JSON.parse(line));
8189
8674
  } catch (err) {
@@ -8196,8 +8681,8 @@ function readNetworkLogs(runId) {
8196
8681
  function cleanupNetworkLogs(runId) {
8197
8682
  const logPath = getNetworkLogPath(runId);
8198
8683
  try {
8199
- if (fs6.existsSync(logPath)) {
8200
- fs6.unlinkSync(logPath);
8684
+ if (fs7.existsSync(logPath)) {
8685
+ fs7.unlinkSync(logPath);
8201
8686
  }
8202
8687
  } catch (err) {
8203
8688
  console.error(
@@ -8240,7 +8725,7 @@ async function uploadNetworkLogs(apiUrl, sandboxToken, runId) {
8240
8725
  }
8241
8726
 
8242
8727
  // src/lib/vm-setup/vm-setup.ts
8243
- import fs7 from "fs";
8728
+ import fs8 from "fs";
8244
8729
 
8245
8730
  // src/lib/scripts/utils.ts
8246
8731
  function getAllScripts() {
@@ -8302,12 +8787,12 @@ async function restoreSessionHistory(ssh, resumeSession, workingDir, cliAgentTyp
8302
8787
  );
8303
8788
  }
8304
8789
  async function installProxyCA(ssh) {
8305
- if (!fs7.existsSync(PROXY_CA_CERT_PATH)) {
8790
+ if (!fs8.existsSync(PROXY_CA_CERT_PATH)) {
8306
8791
  throw new Error(
8307
8792
  `Proxy CA certificate not found at ${PROXY_CA_CERT_PATH}. Run generate-proxy-ca.sh first.`
8308
8793
  );
8309
8794
  }
8310
- const caCert = fs7.readFileSync(PROXY_CA_CERT_PATH, "utf-8");
8795
+ const caCert = fs8.readFileSync(PROXY_CA_CERT_PATH, "utf-8");
8311
8796
  console.log(
8312
8797
  `[Executor] Installing proxy CA certificate (${caCert.length} bytes)`
8313
8798
  );
@@ -8388,7 +8873,7 @@ async function executeJob(context, config, options = {}) {
8388
8873
  const log = options.logger ?? ((msg) => console.log(msg));
8389
8874
  log(`[Executor] Starting job ${context.runId} in VM ${vmId}`);
8390
8875
  try {
8391
- const workspacesDir = path4.join(process.cwd(), "workspaces");
8876
+ const workspacesDir = path5.join(process.cwd(), "workspaces");
8392
8877
  const vmConfig = {
8393
8878
  vmId,
8394
8879
  vcpus: config.sandbox.vcpu,
@@ -8396,7 +8881,7 @@ async function executeJob(context, config, options = {}) {
8396
8881
  kernelPath: config.firecracker.kernel,
8397
8882
  rootfsPath: config.firecracker.rootfs,
8398
8883
  firecrackerBinary: config.firecracker.binary,
8399
- workDir: path4.join(workspacesDir, `vm0-${vmId}`)
8884
+ workDir: path5.join(workspacesDir, `vm0-${vmId}`)
8400
8885
  };
8401
8886
  log(`[Executor] Creating VM ${vmId}...`);
8402
8887
  vm = new FirecrackerVM(vmConfig);
@@ -8421,7 +8906,7 @@ async function executeJob(context, config, options = {}) {
8421
8906
  log(
8422
8907
  `[Executor] Setting up network security for VM ${guestIp} (mitm=${mitmEnabled}, sealSecrets=${sealSecretsEnabled})`
8423
8908
  );
8424
- await setupVMProxyRules(guestIp, config.proxy.port);
8909
+ await setupVMProxyRules(guestIp, config.proxy.port, config.name);
8425
8910
  getVMRegistry().register(guestIp, context.runId, context.sandboxToken, {
8426
8911
  firewallRules: firewallConfig?.rules,
8427
8912
  mitmEnabled,
@@ -8592,7 +9077,7 @@ async function executeJob(context, config, options = {}) {
8592
9077
  if (context.experimentalFirewall?.enabled && guestIp) {
8593
9078
  log(`[Executor] Cleaning up network security for VM ${guestIp}`);
8594
9079
  try {
8595
- await removeVMProxyRules(guestIp, config.proxy.port);
9080
+ await removeVMProxyRules(guestIp, config.proxy.port, config.name);
8596
9081
  } catch (err) {
8597
9082
  console.error(
8598
9083
  `[Executor] Failed to remove VM proxy rules: ${err instanceof Error ? err.message : "Unknown error"}`
@@ -8631,7 +9116,7 @@ function writeStatusFile(statusFilePath, mode, startedAt) {
8631
9116
  updated_at: (/* @__PURE__ */ new Date()).toISOString()
8632
9117
  };
8633
9118
  try {
8634
- writeFileSync(statusFilePath, JSON.stringify(status, null, 2));
9119
+ writeFileSync2(statusFilePath, JSON.stringify(status, null, 2));
8635
9120
  } catch (err) {
8636
9121
  console.error(
8637
9122
  `Failed to write status file: ${err instanceof Error ? err.message : "Unknown error"}`
@@ -8684,6 +9169,12 @@ var startCommand = new Command("start").description("Start the runner").option("
8684
9169
  }
8685
9170
  console.log("Setting up network bridge...");
8686
9171
  await setupBridge();
9172
+ console.log("Flushing bridge ARP cache...");
9173
+ await flushBridgeArpCache();
9174
+ console.log("Cleaning up orphaned proxy rules...");
9175
+ await cleanupOrphanedProxyRules(config.name);
9176
+ console.log("Cleaning up orphaned IP allocations...");
9177
+ await cleanupOrphanedAllocations();
8687
9178
  console.log("Initializing network proxy...");
8688
9179
  initVMRegistry();
8689
9180
  const proxyManager = initProxyManager({
@@ -8703,7 +9194,7 @@ var startCommand = new Command("start").description("Start the runner").option("
8703
9194
  "Jobs with experimentalFirewall enabled will run without network interception"
8704
9195
  );
8705
9196
  }
8706
- const statusFilePath = join(dirname(options.config), "status.json");
9197
+ const statusFilePath = join2(dirname(options.config), "status.json");
8707
9198
  const startedAt = /* @__PURE__ */ new Date();
8708
9199
  const state = { mode: "running" };
8709
9200
  const updateStatus = () => {
@@ -8833,32 +9324,493 @@ var startCommand = new Command("start").description("Start the runner").option("
8833
9324
  }
8834
9325
  });
8835
9326
 
8836
- // src/commands/status.ts
9327
+ // src/commands/doctor.ts
8837
9328
  import { Command as Command2 } from "commander";
8838
- var statusCommand = new Command2("status").description("Check runner connectivity to API").option("--config <path>", "Config file path", "./runner.yaml").action(async (options) => {
9329
+ import { existsSync as existsSync3, readFileSync as readFileSync3, readdirSync as readdirSync2 } from "fs";
9330
+ import { dirname as dirname2, join as join3 } from "path";
9331
+
9332
+ // src/lib/firecracker/process.ts
9333
+ import { readdirSync, readFileSync as readFileSync2, existsSync as existsSync2 } from "fs";
9334
+ import path6 from "path";
9335
+ function parseFirecrackerCmdline(cmdline) {
9336
+ const args = cmdline.split("\0");
9337
+ if (!args[0]?.includes("firecracker")) return null;
9338
+ const sockIdx = args.indexOf("--api-sock");
9339
+ const socketPath = args[sockIdx + 1];
9340
+ if (sockIdx === -1 || !socketPath) return null;
9341
+ const match = socketPath.match(/vm0-([a-f0-9]+)\/firecracker\.sock$/);
9342
+ if (!match?.[1]) return null;
9343
+ return { vmId: match[1], socketPath };
9344
+ }
9345
+ function parseMitmproxyCmdline(cmdline) {
9346
+ if (!cmdline.includes("mitmproxy") && !cmdline.includes("mitmdump")) {
9347
+ return null;
9348
+ }
9349
+ const args = cmdline.split("\0");
9350
+ const portIdx = args.findIndex((a) => a === "-p" || a === "--listen-port");
9351
+ const portArg = args[portIdx + 1];
9352
+ const port = portIdx !== -1 && portArg ? parseInt(portArg, 10) : void 0;
9353
+ return { port };
9354
+ }
9355
+ function findFirecrackerProcesses() {
9356
+ const processes = [];
9357
+ const procDir = "/proc";
9358
+ let entries;
9359
+ try {
9360
+ entries = readdirSync(procDir);
9361
+ } catch {
9362
+ return [];
9363
+ }
9364
+ for (const entry of entries) {
9365
+ if (!/^\d+$/.test(entry)) continue;
9366
+ const pid = parseInt(entry, 10);
9367
+ const cmdlinePath = path6.join(procDir, entry, "cmdline");
9368
+ if (!existsSync2(cmdlinePath)) continue;
9369
+ try {
9370
+ const cmdline = readFileSync2(cmdlinePath, "utf-8");
9371
+ const parsed = parseFirecrackerCmdline(cmdline);
9372
+ if (parsed) {
9373
+ processes.push({ pid, ...parsed });
9374
+ }
9375
+ } catch {
9376
+ continue;
9377
+ }
9378
+ }
9379
+ return processes;
9380
+ }
9381
+ function findProcessByVmId(vmId) {
9382
+ const processes = findFirecrackerProcesses();
9383
+ return processes.find((p) => p.vmId === vmId) || null;
9384
+ }
9385
+ function isProcessRunning(pid) {
9386
+ try {
9387
+ process.kill(pid, 0);
9388
+ return true;
9389
+ } catch {
9390
+ return false;
9391
+ }
9392
+ }
9393
+ async function killProcess(pid, timeoutMs = 5e3) {
9394
+ if (!isProcessRunning(pid)) return true;
9395
+ try {
9396
+ process.kill(pid, "SIGTERM");
9397
+ } catch {
9398
+ return !isProcessRunning(pid);
9399
+ }
9400
+ const startTime = Date.now();
9401
+ while (Date.now() - startTime < timeoutMs) {
9402
+ if (!isProcessRunning(pid)) return true;
9403
+ await new Promise((resolve) => setTimeout(resolve, 100));
9404
+ }
9405
+ if (isProcessRunning(pid)) {
9406
+ try {
9407
+ process.kill(pid, "SIGKILL");
9408
+ } catch {
9409
+ }
9410
+ }
9411
+ return !isProcessRunning(pid);
9412
+ }
9413
+ function findMitmproxyProcess() {
9414
+ const procDir = "/proc";
9415
+ let entries;
9416
+ try {
9417
+ entries = readdirSync(procDir);
9418
+ } catch {
9419
+ return null;
9420
+ }
9421
+ for (const entry of entries) {
9422
+ if (!/^\d+$/.test(entry)) continue;
9423
+ const pid = parseInt(entry, 10);
9424
+ const cmdlinePath = path6.join(procDir, entry, "cmdline");
9425
+ if (!existsSync2(cmdlinePath)) continue;
9426
+ try {
9427
+ const cmdline = readFileSync2(cmdlinePath, "utf-8");
9428
+ const parsed = parseMitmproxyCmdline(cmdline);
9429
+ if (parsed) {
9430
+ return { pid, port: parsed.port };
9431
+ }
9432
+ } catch {
9433
+ continue;
9434
+ }
9435
+ }
9436
+ return null;
9437
+ }
9438
+
9439
+ // src/commands/doctor.ts
9440
+ 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) => {
8839
9441
  try {
8840
9442
  const config = loadConfig(options.config);
8841
- console.log(`Checking connectivity to ${config.server.url}...`);
8842
- console.log(`Runner group: ${config.group}`);
8843
- await pollForJob(config.server, config.group);
9443
+ const configDir = dirname2(options.config);
9444
+ const statusFilePath = join3(configDir, "status.json");
9445
+ const workspacesDir = join3(configDir, "workspaces");
9446
+ console.log(`Runner: ${config.name}`);
9447
+ let status = null;
9448
+ if (existsSync3(statusFilePath)) {
9449
+ try {
9450
+ status = JSON.parse(
9451
+ readFileSync3(statusFilePath, "utf-8")
9452
+ );
9453
+ console.log(`Mode: ${status.mode}`);
9454
+ if (status.started_at) {
9455
+ const started = new Date(status.started_at);
9456
+ const uptime = formatUptime(Date.now() - started.getTime());
9457
+ console.log(
9458
+ `Started: ${started.toLocaleString()} (uptime: ${uptime})`
9459
+ );
9460
+ }
9461
+ } catch {
9462
+ console.log("Mode: unknown (status.json unreadable)");
9463
+ }
9464
+ } else {
9465
+ console.log("Mode: unknown (no status.json)");
9466
+ }
8844
9467
  console.log("");
8845
- console.log("\u2713 Runner can connect to API");
8846
- console.log(` API: ${config.server.url}`);
8847
- console.log(` Group: ${config.group}`);
8848
- console.log(` Auth: OK`);
8849
- process.exit(0);
9468
+ console.log("API Connectivity:");
9469
+ try {
9470
+ await pollForJob(config.server, config.group);
9471
+ console.log(` \u2713 Connected to ${config.server.url}`);
9472
+ console.log(" \u2713 Authentication: OK");
9473
+ } catch (error) {
9474
+ console.log(` \u2717 Cannot connect to ${config.server.url}`);
9475
+ console.log(
9476
+ ` Error: ${error instanceof Error ? error.message : "Unknown error"}`
9477
+ );
9478
+ }
9479
+ console.log("");
9480
+ console.log("Network:");
9481
+ const warnings = [];
9482
+ const bridgeStatus = await checkBridgeStatus();
9483
+ if (bridgeStatus.exists) {
9484
+ console.log(` \u2713 Bridge ${BRIDGE_NAME2} (${bridgeStatus.ip})`);
9485
+ } else {
9486
+ console.log(` \u2717 Bridge ${BRIDGE_NAME2} not found`);
9487
+ warnings.push({
9488
+ message: `Network bridge ${BRIDGE_NAME2} does not exist`
9489
+ });
9490
+ }
9491
+ const proxyPort = config.proxy.port;
9492
+ const mitmProc = findMitmproxyProcess();
9493
+ const portInUse = await isPortInUse(proxyPort);
9494
+ if (mitmProc) {
9495
+ console.log(
9496
+ ` \u2713 Proxy mitmproxy (PID ${mitmProc.pid}) on :${proxyPort}`
9497
+ );
9498
+ } else if (portInUse) {
9499
+ console.log(
9500
+ ` \u26A0\uFE0F Proxy port :${proxyPort} in use but mitmproxy process not found`
9501
+ );
9502
+ warnings.push({
9503
+ message: `Port ${proxyPort} is in use but mitmproxy process not detected`
9504
+ });
9505
+ } else {
9506
+ console.log(` \u2717 Proxy mitmproxy not running`);
9507
+ warnings.push({ message: "Proxy mitmproxy is not running" });
9508
+ }
9509
+ console.log("");
9510
+ const processes = findFirecrackerProcesses();
9511
+ const tapDevices = await listTapDevices();
9512
+ const workspaces = existsSync3(workspacesDir) ? readdirSync2(workspacesDir).filter((d) => d.startsWith("vm0-")) : [];
9513
+ const jobs = [];
9514
+ const statusVmIds = /* @__PURE__ */ new Set();
9515
+ const allocations = getAllocations();
9516
+ if (status?.active_job_ids) {
9517
+ for (const runId of status.active_job_ids) {
9518
+ const vmId = runId.split("-")[0];
9519
+ if (!vmId) continue;
9520
+ statusVmIds.add(vmId);
9521
+ const proc = processes.find((p) => p.vmId === vmId);
9522
+ const ip = getIPForVm(vmId) ?? "not allocated";
9523
+ jobs.push({
9524
+ runId,
9525
+ vmId,
9526
+ ip,
9527
+ hasProcess: !!proc,
9528
+ pid: proc?.pid
9529
+ });
9530
+ }
9531
+ }
9532
+ const ipToVmIds = /* @__PURE__ */ new Map();
9533
+ for (const [ip, allocation] of allocations) {
9534
+ const existing = ipToVmIds.get(ip) ?? [];
9535
+ existing.push(allocation.vmId);
9536
+ ipToVmIds.set(ip, existing);
9537
+ }
9538
+ const maxConcurrent = config.sandbox.max_concurrent;
9539
+ console.log(`Jobs (${jobs.length} active, max ${maxConcurrent}):`);
9540
+ if (jobs.length === 0) {
9541
+ console.log(" No active jobs");
9542
+ } else {
9543
+ console.log(" ID VM ID IP Status");
9544
+ for (const job of jobs) {
9545
+ const shortId = job.runId.substring(0, 12) + "...";
9546
+ const ipConflict = (ipToVmIds.get(job.ip)?.length ?? 0) > 1;
9547
+ let statusText;
9548
+ if (ipConflict) {
9549
+ statusText = "\u26A0\uFE0F IP conflict!";
9550
+ } else if (job.hasProcess) {
9551
+ statusText = `\u2713 Running (PID ${job.pid})`;
9552
+ } else {
9553
+ statusText = "\u26A0\uFE0F No process";
9554
+ }
9555
+ console.log(
9556
+ ` ${shortId} ${job.vmId} ${job.ip.padEnd(15)} ${statusText}`
9557
+ );
9558
+ }
9559
+ }
9560
+ console.log("");
9561
+ for (const job of jobs) {
9562
+ if (!job.hasProcess) {
9563
+ warnings.push({
9564
+ message: `Job ${job.vmId} in status.json but no Firecracker process running`
9565
+ });
9566
+ }
9567
+ }
9568
+ for (const [ip, vmIds] of ipToVmIds) {
9569
+ if (vmIds.length > 1) {
9570
+ warnings.push({
9571
+ message: `IP conflict: ${ip} assigned to ${vmIds.join(", ")}`
9572
+ });
9573
+ }
9574
+ }
9575
+ const processVmIds = new Set(processes.map((p) => p.vmId));
9576
+ for (const proc of processes) {
9577
+ if (!statusVmIds.has(proc.vmId)) {
9578
+ warnings.push({
9579
+ message: `Orphan process: PID ${proc.pid} (vmId ${proc.vmId}) not in status.json`
9580
+ });
9581
+ }
9582
+ }
9583
+ for (const tap of tapDevices) {
9584
+ const vmId = tap.replace("tap", "");
9585
+ if (!processVmIds.has(vmId) && !statusVmIds.has(vmId)) {
9586
+ warnings.push({
9587
+ message: `Orphan TAP device: ${tap} (no matching job or process)`
9588
+ });
9589
+ }
9590
+ }
9591
+ for (const ws of workspaces) {
9592
+ const vmId = ws.replace("vm0-", "");
9593
+ if (!processVmIds.has(vmId) && !statusVmIds.has(vmId)) {
9594
+ warnings.push({
9595
+ message: `Orphan workspace: ${ws} (no matching job or process)`
9596
+ });
9597
+ }
9598
+ }
9599
+ const activeVmIps = new Set(jobs.map((j) => j.ip));
9600
+ const iptablesRules = await listIptablesNatRules();
9601
+ const orphanedIptables = await findOrphanedIptablesRules(
9602
+ iptablesRules,
9603
+ activeVmIps,
9604
+ proxyPort
9605
+ );
9606
+ for (const rule of orphanedIptables) {
9607
+ warnings.push({
9608
+ message: `Orphan iptables rule: redirect ${rule.sourceIp}:${rule.destPort} -> :${rule.redirectPort}`
9609
+ });
9610
+ }
9611
+ console.log("Warnings:");
9612
+ if (warnings.length === 0) {
9613
+ console.log(" None");
9614
+ } else {
9615
+ for (const w of warnings) {
9616
+ console.log(` - ${w.message}`);
9617
+ }
9618
+ }
9619
+ process.exit(warnings.length > 0 ? 1 : 0);
8850
9620
  } catch (error) {
8851
- console.error("");
8852
- console.error("\u2717 Runner cannot connect to API");
8853
9621
  console.error(
8854
- ` Error: ${error instanceof Error ? error.message : "Unknown error"}`
9622
+ `Error: ${error instanceof Error ? error.message : "Unknown error"}`
8855
9623
  );
8856
9624
  process.exit(1);
8857
9625
  }
8858
9626
  });
9627
+ function formatUptime(ms) {
9628
+ const seconds = Math.floor(ms / 1e3);
9629
+ const minutes = Math.floor(seconds / 60);
9630
+ const hours = Math.floor(minutes / 60);
9631
+ const days = Math.floor(hours / 24);
9632
+ if (days > 0) return `${days}d ${hours % 24}h`;
9633
+ if (hours > 0) return `${hours}h ${minutes % 60}m`;
9634
+ if (minutes > 0) return `${minutes}m`;
9635
+ return `${seconds}s`;
9636
+ }
8859
9637
 
8860
- // src/commands/benchmark.ts
9638
+ // src/commands/kill.ts
8861
9639
  import { Command as Command3 } from "commander";
9640
+ import { existsSync as existsSync4, readFileSync as readFileSync4, writeFileSync as writeFileSync3, rmSync } from "fs";
9641
+ import { dirname as dirname3, join as join4 } from "path";
9642
+ import * as readline2 from "readline";
9643
+ var killCommand = new Command3("kill").description("Force terminate a job and clean up all resources").argument("<job-id>", "Job ID (full runId UUID or short 8-char vmId)").option("--config <path>", "Config file path", "./runner.yaml").option("--force", "Skip confirmation prompt").action(
9644
+ async (jobId, options) => {
9645
+ try {
9646
+ loadConfig(options.config);
9647
+ const configDir = dirname3(options.config);
9648
+ const statusFilePath = join4(configDir, "status.json");
9649
+ const workspacesDir = join4(configDir, "workspaces");
9650
+ const { vmId, runId } = resolveJobId(jobId, statusFilePath);
9651
+ console.log(`Killing job ${vmId}...`);
9652
+ const proc = findProcessByVmId(vmId);
9653
+ const tapDevice = `tap${vmId}`;
9654
+ const workspaceDir = join4(workspacesDir, `vm0-${vmId}`);
9655
+ console.log("");
9656
+ console.log("Resources to clean up:");
9657
+ if (proc) {
9658
+ console.log(` - Firecracker process (PID ${proc.pid})`);
9659
+ } else {
9660
+ console.log(" - Firecracker process: not found");
9661
+ }
9662
+ console.log(` - TAP device: ${tapDevice}`);
9663
+ console.log(` - Workspace: ${workspaceDir}`);
9664
+ if (runId) {
9665
+ console.log(` - status.json entry: ${runId.substring(0, 12)}...`);
9666
+ }
9667
+ console.log("");
9668
+ if (!options.force) {
9669
+ const confirmed = await confirm("Proceed with cleanup?");
9670
+ if (!confirmed) {
9671
+ console.log("Aborted.");
9672
+ process.exit(0);
9673
+ }
9674
+ }
9675
+ const results = [];
9676
+ if (proc) {
9677
+ const killed = await killProcess(proc.pid);
9678
+ results.push({
9679
+ step: "Firecracker process",
9680
+ success: killed,
9681
+ message: killed ? `PID ${proc.pid} terminated` : `Failed to kill PID ${proc.pid}`
9682
+ });
9683
+ } else {
9684
+ results.push({
9685
+ step: "Firecracker process",
9686
+ success: true,
9687
+ message: "Not running"
9688
+ });
9689
+ }
9690
+ try {
9691
+ await deleteTapDevice(tapDevice);
9692
+ results.push({
9693
+ step: "TAP device",
9694
+ success: true,
9695
+ message: `${tapDevice} deleted`
9696
+ });
9697
+ } catch (error) {
9698
+ results.push({
9699
+ step: "TAP device",
9700
+ success: false,
9701
+ message: error instanceof Error ? error.message : "Unknown error"
9702
+ });
9703
+ }
9704
+ if (existsSync4(workspaceDir)) {
9705
+ try {
9706
+ rmSync(workspaceDir, { recursive: true, force: true });
9707
+ results.push({
9708
+ step: "Workspace",
9709
+ success: true,
9710
+ message: `${workspaceDir} removed`
9711
+ });
9712
+ } catch (error) {
9713
+ results.push({
9714
+ step: "Workspace",
9715
+ success: false,
9716
+ message: error instanceof Error ? error.message : "Unknown error"
9717
+ });
9718
+ }
9719
+ } else {
9720
+ results.push({
9721
+ step: "Workspace",
9722
+ success: true,
9723
+ message: "Not found (already cleaned)"
9724
+ });
9725
+ }
9726
+ if (runId && existsSync4(statusFilePath)) {
9727
+ try {
9728
+ const status = JSON.parse(
9729
+ readFileSync4(statusFilePath, "utf-8")
9730
+ );
9731
+ const oldCount = status.active_jobs;
9732
+ status.active_job_ids = status.active_job_ids.filter(
9733
+ (id) => id !== runId
9734
+ );
9735
+ status.active_jobs = status.active_job_ids.length;
9736
+ status.updated_at = (/* @__PURE__ */ new Date()).toISOString();
9737
+ writeFileSync3(statusFilePath, JSON.stringify(status, null, 2));
9738
+ results.push({
9739
+ step: "status.json",
9740
+ success: true,
9741
+ message: `Updated (active_jobs: ${oldCount} -> ${status.active_jobs})`
9742
+ });
9743
+ } catch (error) {
9744
+ results.push({
9745
+ step: "status.json",
9746
+ success: false,
9747
+ message: error instanceof Error ? error.message : "Unknown error"
9748
+ });
9749
+ }
9750
+ } else {
9751
+ results.push({
9752
+ step: "status.json",
9753
+ success: true,
9754
+ message: "No update needed"
9755
+ });
9756
+ }
9757
+ console.log("");
9758
+ let allSuccess = true;
9759
+ for (const r of results) {
9760
+ const icon = r.success ? "\u2713" : "\u2717";
9761
+ console.log(` ${icon} ${r.step}: ${r.message}`);
9762
+ if (!r.success) allSuccess = false;
9763
+ }
9764
+ console.log("");
9765
+ if (allSuccess) {
9766
+ console.log(`Job ${vmId} killed successfully.`);
9767
+ process.exit(0);
9768
+ } else {
9769
+ console.log(`Job ${vmId} cleanup completed with errors.`);
9770
+ process.exit(1);
9771
+ }
9772
+ } catch (error) {
9773
+ console.error(
9774
+ `Error: ${error instanceof Error ? error.message : "Unknown error"}`
9775
+ );
9776
+ process.exit(1);
9777
+ }
9778
+ }
9779
+ );
9780
+ function resolveJobId(input, statusFilePath) {
9781
+ if (input.includes("-")) {
9782
+ const vmId = input.split("-")[0];
9783
+ return { vmId: vmId ?? input, runId: input };
9784
+ }
9785
+ if (existsSync4(statusFilePath)) {
9786
+ try {
9787
+ const status = JSON.parse(
9788
+ readFileSync4(statusFilePath, "utf-8")
9789
+ );
9790
+ const match = status.active_job_ids.find((id) => id.startsWith(input));
9791
+ if (match) {
9792
+ return { vmId: input, runId: match };
9793
+ }
9794
+ } catch {
9795
+ }
9796
+ }
9797
+ return { vmId: input, runId: null };
9798
+ }
9799
+ async function confirm(message) {
9800
+ const rl = readline2.createInterface({
9801
+ input: process.stdin,
9802
+ output: process.stdout
9803
+ });
9804
+ return new Promise((resolve) => {
9805
+ rl.question(`${message} [y/N] `, (answer) => {
9806
+ rl.close();
9807
+ resolve(answer.toLowerCase() === "y" || answer.toLowerCase() === "yes");
9808
+ });
9809
+ });
9810
+ }
9811
+
9812
+ // src/commands/benchmark.ts
9813
+ import { Command as Command4 } from "commander";
8862
9814
  import crypto from "crypto";
8863
9815
 
8864
9816
  // src/lib/timing.ts
@@ -8909,7 +9861,7 @@ function createBenchmarkContext(prompt, options) {
8909
9861
  cliAgentType: options.agentType
8910
9862
  };
8911
9863
  }
8912
- var benchmarkCommand = new Command3("benchmark").description(
9864
+ var benchmarkCommand = new Command4("benchmark").description(
8913
9865
  "Run a VM performance benchmark (executes bash command directly)"
8914
9866
  ).argument("<prompt>", "The bash command to execute in the VM").option("--config <path>", "Config file path", "./runner.yaml").option("--working-dir <path>", "Working directory in VM", "/home/user").option("--agent-type <type>", "Agent type", "claude-code").action(async (prompt, options) => {
8915
9867
  const timer = new Timer();
@@ -8949,10 +9901,11 @@ var benchmarkCommand = new Command3("benchmark").description(
8949
9901
  });
8950
9902
 
8951
9903
  // src/index.ts
8952
- var version = true ? "2.11.1" : "0.1.0";
9904
+ var version = true ? "2.13.0" : "0.1.0";
8953
9905
  program.name("vm0-runner").version(version).description("Self-hosted runner for VM0 agents");
8954
9906
  program.addCommand(startCommand);
8955
- program.addCommand(statusCommand);
9907
+ program.addCommand(doctorCommand);
9908
+ program.addCommand(killCommand);
8956
9909
  program.addCommand(benchmarkCommand);
8957
9910
  program.parse();
8958
9911
  //# sourceMappingURL=index.js.map