@vm0/runner 2.12.0 → 2.13.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/index.js +1346 -406
  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() {
@@ -6267,43 +6642,137 @@ var scopeContract = c9.router({
6267
6642
  }
6268
6643
  });
6269
6644
 
6270
- // ../../packages/core/src/contracts/sessions.ts
6645
+ // ../../packages/core/src/contracts/credentials.ts
6271
6646
  import { z as z14 } from "zod";
6272
6647
  var c10 = initContract();
6273
- var sessionResponseSchema = z14.object({
6274
- id: z14.string(),
6275
- agentComposeId: z14.string(),
6276
- agentComposeVersionId: z14.string().nullable(),
6277
- conversationId: z14.string().nullable(),
6278
- artifactName: z14.string().nullable(),
6279
- vars: z14.record(z14.string(), z14.string()).nullable(),
6280
- secretNames: z14.array(z14.string()).nullable(),
6281
- 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(),
6282
6656
  createdAt: z14.string(),
6283
6657
  updatedAt: z14.string()
6284
6658
  });
6285
- var agentComposeSnapshotSchema = z14.object({
6286
- agentComposeVersionId: z14.string(),
6287
- vars: z14.record(z14.string(), z14.string()).optional(),
6288
- 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
+ }
6289
6737
  });
6290
- var artifactSnapshotSchema2 = z14.object({
6291
- artifactName: z14.string(),
6292
- 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()
6293
6753
  });
6294
- var volumeVersionsSnapshotSchema2 = z14.object({
6295
- 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()
6296
6762
  });
6297
- var checkpointResponseSchema = z14.object({
6298
- id: z14.string(),
6299
- runId: z14.string(),
6300
- 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(),
6301
6770
  agentComposeSnapshot: agentComposeSnapshotSchema,
6302
6771
  artifactSnapshot: artifactSnapshotSchema2.nullable(),
6303
6772
  volumeVersionsSnapshot: volumeVersionsSnapshotSchema2.nullable(),
6304
- createdAt: z14.string()
6773
+ createdAt: z15.string()
6305
6774
  });
6306
- var sessionsByIdContract = c10.router({
6775
+ var sessionsByIdContract = c11.router({
6307
6776
  /**
6308
6777
  * GET /api/agent/sessions/:id
6309
6778
  * Get session by ID
@@ -6311,8 +6780,8 @@ var sessionsByIdContract = c10.router({
6311
6780
  getById: {
6312
6781
  method: "GET",
6313
6782
  path: "/api/agent/sessions/:id",
6314
- pathParams: z14.object({
6315
- id: z14.string().min(1, "Session ID is required")
6783
+ pathParams: z15.object({
6784
+ id: z15.string().min(1, "Session ID is required")
6316
6785
  }),
6317
6786
  responses: {
6318
6787
  200: sessionResponseSchema,
@@ -6323,7 +6792,7 @@ var sessionsByIdContract = c10.router({
6323
6792
  summary: "Get session by ID"
6324
6793
  }
6325
6794
  });
6326
- var checkpointsByIdContract = c10.router({
6795
+ var checkpointsByIdContract = c11.router({
6327
6796
  /**
6328
6797
  * GET /api/agent/checkpoints/:id
6329
6798
  * Get checkpoint by ID
@@ -6331,8 +6800,8 @@ var checkpointsByIdContract = c10.router({
6331
6800
  getById: {
6332
6801
  method: "GET",
6333
6802
  path: "/api/agent/checkpoints/:id",
6334
- pathParams: z14.object({
6335
- id: z14.string().min(1, "Checkpoint ID is required")
6803
+ pathParams: z15.object({
6804
+ id: z15.string().min(1, "Checkpoint ID is required")
6336
6805
  }),
6337
6806
  responses: {
6338
6807
  200: checkpointResponseSchema,
@@ -6345,91 +6814,91 @@ var checkpointsByIdContract = c10.router({
6345
6814
  });
6346
6815
 
6347
6816
  // ../../packages/core/src/contracts/schedules.ts
6348
- import { z as z15 } from "zod";
6349
- var c11 = initContract();
6350
- var scheduleTriggerSchema = z15.object({
6351
- cron: z15.string().optional(),
6352
- at: z15.string().optional(),
6353
- 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")
6354
6823
  }).refine((data) => data.cron && !data.at || !data.cron && data.at, {
6355
6824
  message: "Exactly one of 'cron' or 'at' must be specified"
6356
6825
  });
6357
- var scheduleRunConfigSchema = z15.object({
6358
- agent: z15.string().min(1, "Agent reference required"),
6359
- prompt: z15.string().min(1, "Prompt required"),
6360
- vars: z15.record(z15.string(), z15.string()).optional(),
6361
- secrets: z15.record(z15.string(), z15.string()).optional(),
6362
- artifactName: z15.string().optional(),
6363
- artifactVersion: z15.string().optional(),
6364
- 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()
6365
6834
  });
6366
- var scheduleDefinitionSchema = z15.object({
6835
+ var scheduleDefinitionSchema = z16.object({
6367
6836
  on: scheduleTriggerSchema,
6368
6837
  run: scheduleRunConfigSchema
6369
6838
  });
6370
- var scheduleYamlSchema = z15.object({
6371
- version: z15.literal("1.0"),
6372
- schedules: z15.record(z15.string(), scheduleDefinitionSchema)
6373
- });
6374
- var deployScheduleRequestSchema = z15.object({
6375
- name: z15.string().min(1).max(64, "Schedule name max 64 chars"),
6376
- cronExpression: z15.string().optional(),
6377
- atTime: z15.string().optional(),
6378
- timezone: z15.string().default("UTC"),
6379
- prompt: z15.string().min(1, "Prompt required"),
6380
- vars: z15.record(z15.string(), z15.string()).optional(),
6381
- secrets: z15.record(z15.string(), z15.string()).optional(),
6382
- artifactName: z15.string().optional(),
6383
- artifactVersion: z15.string().optional(),
6384
- 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(),
6385
6854
  // Resolved agent compose ID (CLI resolves scope/name:version → composeId)
6386
- composeId: z15.string().uuid("Invalid compose ID")
6855
+ composeId: z16.string().uuid("Invalid compose ID")
6387
6856
  }).refine(
6388
6857
  (data) => data.cronExpression && !data.atTime || !data.cronExpression && data.atTime,
6389
6858
  {
6390
6859
  message: "Exactly one of 'cronExpression' or 'atTime' must be specified"
6391
6860
  }
6392
6861
  );
6393
- var scheduleResponseSchema = z15.object({
6394
- id: z15.string().uuid(),
6395
- composeId: z15.string().uuid(),
6396
- composeName: z15.string(),
6397
- scopeSlug: z15.string(),
6398
- name: z15.string(),
6399
- cronExpression: z15.string().nullable(),
6400
- atTime: z15.string().nullable(),
6401
- timezone: z15.string(),
6402
- prompt: z15.string(),
6403
- 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(),
6404
6873
  // Secret names only (values are never returned)
6405
- secretNames: z15.array(z15.string()).nullable(),
6406
- artifactName: z15.string().nullable(),
6407
- artifactVersion: z15.string().nullable(),
6408
- volumeVersions: z15.record(z15.string(), z15.string()).nullable(),
6409
- enabled: z15.boolean(),
6410
- nextRunAt: z15.string().nullable(),
6411
- createdAt: z15.string(),
6412
- updatedAt: z15.string()
6413
- });
6414
- var runSummarySchema = z15.object({
6415
- id: z15.string().uuid(),
6416
- status: z15.enum(["pending", "running", "completed", "failed", "timeout"]),
6417
- createdAt: z15.string(),
6418
- completedAt: z15.string().nullable(),
6419
- error: z15.string().nullable()
6420
- });
6421
- var scheduleRunsResponseSchema = z15.object({
6422
- runs: z15.array(runSummarySchema)
6423
- });
6424
- var scheduleListResponseSchema = z15.object({
6425
- schedules: z15.array(scheduleResponseSchema)
6426
- });
6427
- 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({
6428
6897
  schedule: scheduleResponseSchema,
6429
- created: z15.boolean()
6898
+ created: z16.boolean()
6430
6899
  // true if created, false if updated
6431
6900
  });
6432
- var schedulesMainContract = c11.router({
6901
+ var schedulesMainContract = c12.router({
6433
6902
  /**
6434
6903
  * POST /api/agent/schedules
6435
6904
  * Deploy (create or update) a schedule
@@ -6465,7 +6934,7 @@ var schedulesMainContract = c11.router({
6465
6934
  summary: "List all schedules"
6466
6935
  }
6467
6936
  });
6468
- var schedulesByNameContract = c11.router({
6937
+ var schedulesByNameContract = c12.router({
6469
6938
  /**
6470
6939
  * GET /api/agent/schedules/:name
6471
6940
  * Get schedule by name
@@ -6473,11 +6942,11 @@ var schedulesByNameContract = c11.router({
6473
6942
  getByName: {
6474
6943
  method: "GET",
6475
6944
  path: "/api/agent/schedules/:name",
6476
- pathParams: z15.object({
6477
- name: z15.string().min(1, "Schedule name required")
6945
+ pathParams: z16.object({
6946
+ name: z16.string().min(1, "Schedule name required")
6478
6947
  }),
6479
- query: z15.object({
6480
- composeId: z15.string().uuid("Compose ID required")
6948
+ query: z16.object({
6949
+ composeId: z16.string().uuid("Compose ID required")
6481
6950
  }),
6482
6951
  responses: {
6483
6952
  200: scheduleResponseSchema,
@@ -6493,21 +6962,21 @@ var schedulesByNameContract = c11.router({
6493
6962
  delete: {
6494
6963
  method: "DELETE",
6495
6964
  path: "/api/agent/schedules/:name",
6496
- pathParams: z15.object({
6497
- name: z15.string().min(1, "Schedule name required")
6965
+ pathParams: z16.object({
6966
+ name: z16.string().min(1, "Schedule name required")
6498
6967
  }),
6499
- query: z15.object({
6500
- composeId: z15.string().uuid("Compose ID required")
6968
+ query: z16.object({
6969
+ composeId: z16.string().uuid("Compose ID required")
6501
6970
  }),
6502
6971
  responses: {
6503
- 204: z15.undefined(),
6972
+ 204: z16.undefined(),
6504
6973
  401: apiErrorSchema,
6505
6974
  404: apiErrorSchema
6506
6975
  },
6507
6976
  summary: "Delete schedule"
6508
6977
  }
6509
6978
  });
6510
- var schedulesEnableContract = c11.router({
6979
+ var schedulesEnableContract = c12.router({
6511
6980
  /**
6512
6981
  * POST /api/agent/schedules/:name/enable
6513
6982
  * Enable a disabled schedule
@@ -6515,11 +6984,11 @@ var schedulesEnableContract = c11.router({
6515
6984
  enable: {
6516
6985
  method: "POST",
6517
6986
  path: "/api/agent/schedules/:name/enable",
6518
- pathParams: z15.object({
6519
- name: z15.string().min(1, "Schedule name required")
6987
+ pathParams: z16.object({
6988
+ name: z16.string().min(1, "Schedule name required")
6520
6989
  }),
6521
- body: z15.object({
6522
- composeId: z15.string().uuid("Compose ID required")
6990
+ body: z16.object({
6991
+ composeId: z16.string().uuid("Compose ID required")
6523
6992
  }),
6524
6993
  responses: {
6525
6994
  200: scheduleResponseSchema,
@@ -6535,11 +7004,11 @@ var schedulesEnableContract = c11.router({
6535
7004
  disable: {
6536
7005
  method: "POST",
6537
7006
  path: "/api/agent/schedules/:name/disable",
6538
- pathParams: z15.object({
6539
- name: z15.string().min(1, "Schedule name required")
7007
+ pathParams: z16.object({
7008
+ name: z16.string().min(1, "Schedule name required")
6540
7009
  }),
6541
- body: z15.object({
6542
- composeId: z15.string().uuid("Compose ID required")
7010
+ body: z16.object({
7011
+ composeId: z16.string().uuid("Compose ID required")
6543
7012
  }),
6544
7013
  responses: {
6545
7014
  200: scheduleResponseSchema,
@@ -6549,7 +7018,7 @@ var schedulesEnableContract = c11.router({
6549
7018
  summary: "Disable schedule"
6550
7019
  }
6551
7020
  });
6552
- var scheduleRunsContract = c11.router({
7021
+ var scheduleRunsContract = c12.router({
6553
7022
  /**
6554
7023
  * GET /api/agent/schedules/:name/runs
6555
7024
  * List recent runs for a schedule
@@ -6557,12 +7026,12 @@ var scheduleRunsContract = c11.router({
6557
7026
  listRuns: {
6558
7027
  method: "GET",
6559
7028
  path: "/api/agent/schedules/:name/runs",
6560
- pathParams: z15.object({
6561
- name: z15.string().min(1, "Schedule name required")
7029
+ pathParams: z16.object({
7030
+ name: z16.string().min(1, "Schedule name required")
6562
7031
  }),
6563
- query: z15.object({
6564
- composeId: z15.string().uuid("Compose ID required"),
6565
- 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)
6566
7035
  }),
6567
7036
  responses: {
6568
7037
  200: scheduleRunsResponseSchema,
@@ -6574,8 +7043,8 @@ var scheduleRunsContract = c11.router({
6574
7043
  });
6575
7044
 
6576
7045
  // ../../packages/core/src/contracts/public/common.ts
6577
- import { z as z16 } from "zod";
6578
- var publicApiErrorTypeSchema = z16.enum([
7046
+ import { z as z17 } from "zod";
7047
+ var publicApiErrorTypeSchema = z17.enum([
6579
7048
  "api_error",
6580
7049
  // Internal server error (5xx)
6581
7050
  "invalid_request_error",
@@ -6587,55 +7056,55 @@ var publicApiErrorTypeSchema = z16.enum([
6587
7056
  "conflict_error"
6588
7057
  // Resource conflict (409)
6589
7058
  ]);
6590
- var publicApiErrorSchema = z16.object({
6591
- error: z16.object({
7059
+ var publicApiErrorSchema = z17.object({
7060
+ error: z17.object({
6592
7061
  type: publicApiErrorTypeSchema,
6593
- code: z16.string(),
6594
- message: z16.string(),
6595
- param: z16.string().optional(),
6596
- 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()
6597
7066
  })
6598
7067
  });
6599
- var paginationSchema = z16.object({
6600
- has_more: z16.boolean(),
6601
- next_cursor: z16.string().nullable()
7068
+ var paginationSchema = z17.object({
7069
+ has_more: z17.boolean(),
7070
+ next_cursor: z17.string().nullable()
6602
7071
  });
6603
7072
  function createPaginatedResponseSchema(dataSchema) {
6604
- return z16.object({
6605
- data: z16.array(dataSchema),
7073
+ return z17.object({
7074
+ data: z17.array(dataSchema),
6606
7075
  pagination: paginationSchema
6607
7076
  });
6608
7077
  }
6609
- var listQuerySchema = z16.object({
6610
- cursor: z16.string().optional(),
6611
- 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)
6612
7081
  });
6613
- var requestIdSchema = z16.string().uuid();
6614
- var timestampSchema = z16.string().datetime();
7082
+ var requestIdSchema = z17.string().uuid();
7083
+ var timestampSchema = z17.string().datetime();
6615
7084
 
6616
7085
  // ../../packages/core/src/contracts/public/agents.ts
6617
- import { z as z17 } from "zod";
6618
- var c12 = initContract();
6619
- var publicAgentSchema = z17.object({
6620
- id: z17.string(),
6621
- name: z17.string(),
6622
- 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(),
6623
7092
  created_at: timestampSchema,
6624
7093
  updated_at: timestampSchema
6625
7094
  });
6626
- var agentVersionSchema = z17.object({
6627
- id: z17.string(),
6628
- agent_id: z17.string(),
6629
- version_number: z17.number(),
7095
+ var agentVersionSchema = z18.object({
7096
+ id: z18.string(),
7097
+ agent_id: z18.string(),
7098
+ version_number: z18.number(),
6630
7099
  created_at: timestampSchema
6631
7100
  });
6632
7101
  var publicAgentDetailSchema = publicAgentSchema;
6633
7102
  var paginatedAgentsSchema = createPaginatedResponseSchema(publicAgentSchema);
6634
7103
  var paginatedAgentVersionsSchema = createPaginatedResponseSchema(agentVersionSchema);
6635
7104
  var agentListQuerySchema = listQuerySchema.extend({
6636
- name: z17.string().optional()
7105
+ name: z18.string().optional()
6637
7106
  });
6638
- var publicAgentsListContract = c12.router({
7107
+ var publicAgentsListContract = c13.router({
6639
7108
  list: {
6640
7109
  method: "GET",
6641
7110
  path: "/v1/agents",
@@ -6649,12 +7118,12 @@ var publicAgentsListContract = c12.router({
6649
7118
  description: "List all agents in the current scope with pagination. Use the `name` query parameter to filter by agent name."
6650
7119
  }
6651
7120
  });
6652
- var publicAgentByIdContract = c12.router({
7121
+ var publicAgentByIdContract = c13.router({
6653
7122
  get: {
6654
7123
  method: "GET",
6655
7124
  path: "/v1/agents/:id",
6656
- pathParams: z17.object({
6657
- id: z17.string().min(1, "Agent ID is required")
7125
+ pathParams: z18.object({
7126
+ id: z18.string().min(1, "Agent ID is required")
6658
7127
  }),
6659
7128
  responses: {
6660
7129
  200: publicAgentDetailSchema,
@@ -6666,12 +7135,12 @@ var publicAgentByIdContract = c12.router({
6666
7135
  description: "Get agent details by ID"
6667
7136
  }
6668
7137
  });
6669
- var publicAgentVersionsContract = c12.router({
7138
+ var publicAgentVersionsContract = c13.router({
6670
7139
  list: {
6671
7140
  method: "GET",
6672
7141
  path: "/v1/agents/:id/versions",
6673
- pathParams: z17.object({
6674
- id: z17.string().min(1, "Agent ID is required")
7142
+ pathParams: z18.object({
7143
+ id: z18.string().min(1, "Agent ID is required")
6675
7144
  }),
6676
7145
  query: listQuerySchema,
6677
7146
  responses: {
@@ -6686,9 +7155,9 @@ var publicAgentVersionsContract = c12.router({
6686
7155
  });
6687
7156
 
6688
7157
  // ../../packages/core/src/contracts/public/runs.ts
6689
- import { z as z18 } from "zod";
6690
- var c13 = initContract();
6691
- var publicRunStatusSchema = z18.enum([
7158
+ import { z as z19 } from "zod";
7159
+ var c14 = initContract();
7160
+ var publicRunStatusSchema = z19.enum([
6692
7161
  "pending",
6693
7162
  "running",
6694
7163
  "completed",
@@ -6696,56 +7165,56 @@ var publicRunStatusSchema = z18.enum([
6696
7165
  "timeout",
6697
7166
  "cancelled"
6698
7167
  ]);
6699
- var publicRunSchema = z18.object({
6700
- id: z18.string(),
6701
- agent_id: z18.string(),
6702
- agent_name: z18.string(),
7168
+ var publicRunSchema = z19.object({
7169
+ id: z19.string(),
7170
+ agent_id: z19.string(),
7171
+ agent_name: z19.string(),
6703
7172
  status: publicRunStatusSchema,
6704
- prompt: z18.string(),
7173
+ prompt: z19.string(),
6705
7174
  created_at: timestampSchema,
6706
7175
  started_at: timestampSchema.nullable(),
6707
7176
  completed_at: timestampSchema.nullable()
6708
7177
  });
6709
7178
  var publicRunDetailSchema = publicRunSchema.extend({
6710
- error: z18.string().nullable(),
6711
- execution_time_ms: z18.number().nullable(),
6712
- checkpoint_id: z18.string().nullable(),
6713
- session_id: z18.string().nullable(),
6714
- artifact_name: z18.string().nullable(),
6715
- artifact_version: z18.string().nullable(),
6716
- 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()
6717
7186
  });
6718
7187
  var paginatedRunsSchema = createPaginatedResponseSchema(publicRunSchema);
6719
- var createRunRequestSchema = z18.object({
7188
+ var createRunRequestSchema = z19.object({
6720
7189
  // Agent identification (one of: agent, agent_id, session_id, checkpoint_id)
6721
- agent: z18.string().optional(),
7190
+ agent: z19.string().optional(),
6722
7191
  // Agent name
6723
- agent_id: z18.string().optional(),
7192
+ agent_id: z19.string().optional(),
6724
7193
  // Agent ID
6725
- agent_version: z18.string().optional(),
7194
+ agent_version: z19.string().optional(),
6726
7195
  // Version specifier (e.g., "latest", "v1", specific ID)
6727
7196
  // Continue session
6728
- session_id: z18.string().optional(),
7197
+ session_id: z19.string().optional(),
6729
7198
  // Resume from checkpoint
6730
- checkpoint_id: z18.string().optional(),
7199
+ checkpoint_id: z19.string().optional(),
6731
7200
  // Required
6732
- prompt: z18.string().min(1, "Prompt is required"),
7201
+ prompt: z19.string().min(1, "Prompt is required"),
6733
7202
  // Optional configuration
6734
- variables: z18.record(z18.string(), z18.string()).optional(),
6735
- secrets: z18.record(z18.string(), z18.string()).optional(),
6736
- 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(),
6737
7206
  // Artifact name to mount
6738
- artifact_version: z18.string().optional(),
7207
+ artifact_version: z19.string().optional(),
6739
7208
  // Artifact version (defaults to latest)
6740
- volumes: z18.record(z18.string(), z18.string()).optional()
7209
+ volumes: z19.record(z19.string(), z19.string()).optional()
6741
7210
  // volume_name -> version
6742
7211
  });
6743
7212
  var runListQuerySchema = listQuerySchema.extend({
6744
- agent_id: z18.string().optional(),
7213
+ agent_id: z19.string().optional(),
6745
7214
  status: publicRunStatusSchema.optional(),
6746
7215
  since: timestampSchema.optional()
6747
7216
  });
6748
- var publicRunsListContract = c13.router({
7217
+ var publicRunsListContract = c14.router({
6749
7218
  list: {
6750
7219
  method: "GET",
6751
7220
  path: "/v1/runs",
@@ -6774,12 +7243,12 @@ var publicRunsListContract = c13.router({
6774
7243
  description: "Create and execute a new agent run. Returns 202 Accepted as runs execute asynchronously."
6775
7244
  }
6776
7245
  });
6777
- var publicRunByIdContract = c13.router({
7246
+ var publicRunByIdContract = c14.router({
6778
7247
  get: {
6779
7248
  method: "GET",
6780
7249
  path: "/v1/runs/:id",
6781
- pathParams: z18.object({
6782
- id: z18.string().min(1, "Run ID is required")
7250
+ pathParams: z19.object({
7251
+ id: z19.string().min(1, "Run ID is required")
6783
7252
  }),
6784
7253
  responses: {
6785
7254
  200: publicRunDetailSchema,
@@ -6791,14 +7260,14 @@ var publicRunByIdContract = c13.router({
6791
7260
  description: "Get run details by ID"
6792
7261
  }
6793
7262
  });
6794
- var publicRunCancelContract = c13.router({
7263
+ var publicRunCancelContract = c14.router({
6795
7264
  cancel: {
6796
7265
  method: "POST",
6797
7266
  path: "/v1/runs/:id/cancel",
6798
- pathParams: z18.object({
6799
- id: z18.string().min(1, "Run ID is required")
7267
+ pathParams: z19.object({
7268
+ id: z19.string().min(1, "Run ID is required")
6800
7269
  }),
6801
- body: z18.undefined(),
7270
+ body: z19.undefined(),
6802
7271
  responses: {
6803
7272
  200: publicRunDetailSchema,
6804
7273
  400: publicApiErrorSchema,
@@ -6811,26 +7280,26 @@ var publicRunCancelContract = c13.router({
6811
7280
  description: "Cancel a pending or running execution"
6812
7281
  }
6813
7282
  });
6814
- var logEntrySchema = z18.object({
7283
+ var logEntrySchema = z19.object({
6815
7284
  timestamp: timestampSchema,
6816
- type: z18.enum(["agent", "system", "network"]),
6817
- level: z18.enum(["debug", "info", "warn", "error"]),
6818
- message: z18.string(),
6819
- 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()
6820
7289
  });
6821
7290
  var paginatedLogsSchema = createPaginatedResponseSchema(logEntrySchema);
6822
7291
  var logsQuerySchema = listQuerySchema.extend({
6823
- type: z18.enum(["agent", "system", "network", "all"]).default("all"),
7292
+ type: z19.enum(["agent", "system", "network", "all"]).default("all"),
6824
7293
  since: timestampSchema.optional(),
6825
7294
  until: timestampSchema.optional(),
6826
- order: z18.enum(["asc", "desc"]).default("asc")
7295
+ order: z19.enum(["asc", "desc"]).default("asc")
6827
7296
  });
6828
- var publicRunLogsContract = c13.router({
7297
+ var publicRunLogsContract = c14.router({
6829
7298
  getLogs: {
6830
7299
  method: "GET",
6831
7300
  path: "/v1/runs/:id/logs",
6832
- pathParams: z18.object({
6833
- id: z18.string().min(1, "Run ID is required")
7301
+ pathParams: z19.object({
7302
+ id: z19.string().min(1, "Run ID is required")
6834
7303
  }),
6835
7304
  query: logsQuerySchema,
6836
7305
  responses: {
@@ -6843,29 +7312,29 @@ var publicRunLogsContract = c13.router({
6843
7312
  description: "Get unified logs for a run. Combines agent, system, and network logs."
6844
7313
  }
6845
7314
  });
6846
- var metricPointSchema = z18.object({
7315
+ var metricPointSchema = z19.object({
6847
7316
  timestamp: timestampSchema,
6848
- cpu_percent: z18.number(),
6849
- memory_used_mb: z18.number(),
6850
- memory_total_mb: z18.number(),
6851
- disk_used_mb: z18.number(),
6852
- disk_total_mb: z18.number()
6853
- });
6854
- var metricsSummarySchema = z18.object({
6855
- avg_cpu_percent: z18.number(),
6856
- max_memory_used_mb: z18.number(),
6857
- total_duration_ms: z18.number().nullable()
6858
- });
6859
- var metricsResponseSchema2 = z18.object({
6860
- 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),
6861
7330
  summary: metricsSummarySchema
6862
7331
  });
6863
- var publicRunMetricsContract = c13.router({
7332
+ var publicRunMetricsContract = c14.router({
6864
7333
  getMetrics: {
6865
7334
  method: "GET",
6866
7335
  path: "/v1/runs/:id/metrics",
6867
- pathParams: z18.object({
6868
- id: z18.string().min(1, "Run ID is required")
7336
+ pathParams: z19.object({
7337
+ id: z19.string().min(1, "Run ID is required")
6869
7338
  }),
6870
7339
  responses: {
6871
7340
  200: metricsResponseSchema2,
@@ -6877,7 +7346,7 @@ var publicRunMetricsContract = c13.router({
6877
7346
  description: "Get CPU, memory, and disk metrics for a run"
6878
7347
  }
6879
7348
  });
6880
- var sseEventTypeSchema = z18.enum([
7349
+ var sseEventTypeSchema = z19.enum([
6881
7350
  "status",
6882
7351
  // Run status change
6883
7352
  "output",
@@ -6889,25 +7358,25 @@ var sseEventTypeSchema = z18.enum([
6889
7358
  "heartbeat"
6890
7359
  // Keep-alive
6891
7360
  ]);
6892
- var sseEventSchema = z18.object({
7361
+ var sseEventSchema = z19.object({
6893
7362
  event: sseEventTypeSchema,
6894
- data: z18.unknown(),
6895
- id: z18.string().optional()
7363
+ data: z19.unknown(),
7364
+ id: z19.string().optional()
6896
7365
  // For Last-Event-ID reconnection
6897
7366
  });
6898
- var publicRunEventsContract = c13.router({
7367
+ var publicRunEventsContract = c14.router({
6899
7368
  streamEvents: {
6900
7369
  method: "GET",
6901
7370
  path: "/v1/runs/:id/events",
6902
- pathParams: z18.object({
6903
- id: z18.string().min(1, "Run ID is required")
7371
+ pathParams: z19.object({
7372
+ id: z19.string().min(1, "Run ID is required")
6904
7373
  }),
6905
- query: z18.object({
6906
- last_event_id: z18.string().optional()
7374
+ query: z19.object({
7375
+ last_event_id: z19.string().optional()
6907
7376
  // For reconnection
6908
7377
  }),
6909
7378
  responses: {
6910
- 200: z18.any(),
7379
+ 200: z19.any(),
6911
7380
  // SSE stream - actual content is text/event-stream
6912
7381
  401: publicApiErrorSchema,
6913
7382
  404: publicApiErrorSchema,
@@ -6919,28 +7388,28 @@ var publicRunEventsContract = c13.router({
6919
7388
  });
6920
7389
 
6921
7390
  // ../../packages/core/src/contracts/public/artifacts.ts
6922
- import { z as z19 } from "zod";
6923
- var c14 = initContract();
6924
- var publicArtifactSchema = z19.object({
6925
- id: z19.string(),
6926
- name: z19.string(),
6927
- current_version_id: z19.string().nullable(),
6928
- 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(),
6929
7398
  // Total size in bytes
6930
- file_count: z19.number(),
7399
+ file_count: z20.number(),
6931
7400
  created_at: timestampSchema,
6932
7401
  updated_at: timestampSchema
6933
7402
  });
6934
- var artifactVersionSchema = z19.object({
6935
- id: z19.string(),
7403
+ var artifactVersionSchema = z20.object({
7404
+ id: z20.string(),
6936
7405
  // SHA-256 content hash
6937
- artifact_id: z19.string(),
6938
- size: z19.number(),
7406
+ artifact_id: z20.string(),
7407
+ size: z20.number(),
6939
7408
  // Size in bytes
6940
- file_count: z19.number(),
6941
- message: z19.string().nullable(),
7409
+ file_count: z20.number(),
7410
+ message: z20.string().nullable(),
6942
7411
  // Optional commit message
6943
- created_by: z19.string(),
7412
+ created_by: z20.string(),
6944
7413
  created_at: timestampSchema
6945
7414
  });
6946
7415
  var publicArtifactDetailSchema = publicArtifactSchema.extend({
@@ -6950,7 +7419,7 @@ var paginatedArtifactsSchema = createPaginatedResponseSchema(publicArtifactSchem
6950
7419
  var paginatedArtifactVersionsSchema = createPaginatedResponseSchema(
6951
7420
  artifactVersionSchema
6952
7421
  );
6953
- var publicArtifactsListContract = c14.router({
7422
+ var publicArtifactsListContract = c15.router({
6954
7423
  list: {
6955
7424
  method: "GET",
6956
7425
  path: "/v1/artifacts",
@@ -6964,12 +7433,12 @@ var publicArtifactsListContract = c14.router({
6964
7433
  description: "List all artifacts in the current scope with pagination"
6965
7434
  }
6966
7435
  });
6967
- var publicArtifactByIdContract = c14.router({
7436
+ var publicArtifactByIdContract = c15.router({
6968
7437
  get: {
6969
7438
  method: "GET",
6970
7439
  path: "/v1/artifacts/:id",
6971
- pathParams: z19.object({
6972
- id: z19.string().min(1, "Artifact ID is required")
7440
+ pathParams: z20.object({
7441
+ id: z20.string().min(1, "Artifact ID is required")
6973
7442
  }),
6974
7443
  responses: {
6975
7444
  200: publicArtifactDetailSchema,
@@ -6981,12 +7450,12 @@ var publicArtifactByIdContract = c14.router({
6981
7450
  description: "Get artifact details by ID"
6982
7451
  }
6983
7452
  });
6984
- var publicArtifactVersionsContract = c14.router({
7453
+ var publicArtifactVersionsContract = c15.router({
6985
7454
  list: {
6986
7455
  method: "GET",
6987
7456
  path: "/v1/artifacts/:id/versions",
6988
- pathParams: z19.object({
6989
- id: z19.string().min(1, "Artifact ID is required")
7457
+ pathParams: z20.object({
7458
+ id: z20.string().min(1, "Artifact ID is required")
6990
7459
  }),
6991
7460
  query: listQuerySchema,
6992
7461
  responses: {
@@ -6999,19 +7468,19 @@ var publicArtifactVersionsContract = c14.router({
6999
7468
  description: "List all versions of an artifact with pagination"
7000
7469
  }
7001
7470
  });
7002
- var publicArtifactDownloadContract = c14.router({
7471
+ var publicArtifactDownloadContract = c15.router({
7003
7472
  download: {
7004
7473
  method: "GET",
7005
7474
  path: "/v1/artifacts/:id/download",
7006
- pathParams: z19.object({
7007
- id: z19.string().min(1, "Artifact ID is required")
7475
+ pathParams: z20.object({
7476
+ id: z20.string().min(1, "Artifact ID is required")
7008
7477
  }),
7009
- query: z19.object({
7010
- version_id: z19.string().optional()
7478
+ query: z20.object({
7479
+ version_id: z20.string().optional()
7011
7480
  // Defaults to current version
7012
7481
  }),
7013
7482
  responses: {
7014
- 302: z19.undefined(),
7483
+ 302: z20.undefined(),
7015
7484
  // Redirect to presigned URL
7016
7485
  401: publicApiErrorSchema,
7017
7486
  404: publicApiErrorSchema,
@@ -7023,28 +7492,28 @@ var publicArtifactDownloadContract = c14.router({
7023
7492
  });
7024
7493
 
7025
7494
  // ../../packages/core/src/contracts/public/volumes.ts
7026
- import { z as z20 } from "zod";
7027
- var c15 = initContract();
7028
- var publicVolumeSchema = z20.object({
7029
- id: z20.string(),
7030
- name: z20.string(),
7031
- current_version_id: z20.string().nullable(),
7032
- 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(),
7033
7502
  // Total size in bytes
7034
- file_count: z20.number(),
7503
+ file_count: z21.number(),
7035
7504
  created_at: timestampSchema,
7036
7505
  updated_at: timestampSchema
7037
7506
  });
7038
- var volumeVersionSchema = z20.object({
7039
- id: z20.string(),
7507
+ var volumeVersionSchema = z21.object({
7508
+ id: z21.string(),
7040
7509
  // SHA-256 content hash
7041
- volume_id: z20.string(),
7042
- size: z20.number(),
7510
+ volume_id: z21.string(),
7511
+ size: z21.number(),
7043
7512
  // Size in bytes
7044
- file_count: z20.number(),
7045
- message: z20.string().nullable(),
7513
+ file_count: z21.number(),
7514
+ message: z21.string().nullable(),
7046
7515
  // Optional commit message
7047
- created_by: z20.string(),
7516
+ created_by: z21.string(),
7048
7517
  created_at: timestampSchema
7049
7518
  });
7050
7519
  var publicVolumeDetailSchema = publicVolumeSchema.extend({
@@ -7052,7 +7521,7 @@ var publicVolumeDetailSchema = publicVolumeSchema.extend({
7052
7521
  });
7053
7522
  var paginatedVolumesSchema = createPaginatedResponseSchema(publicVolumeSchema);
7054
7523
  var paginatedVolumeVersionsSchema = createPaginatedResponseSchema(volumeVersionSchema);
7055
- var publicVolumesListContract = c15.router({
7524
+ var publicVolumesListContract = c16.router({
7056
7525
  list: {
7057
7526
  method: "GET",
7058
7527
  path: "/v1/volumes",
@@ -7066,12 +7535,12 @@ var publicVolumesListContract = c15.router({
7066
7535
  description: "List all volumes in the current scope with pagination"
7067
7536
  }
7068
7537
  });
7069
- var publicVolumeByIdContract = c15.router({
7538
+ var publicVolumeByIdContract = c16.router({
7070
7539
  get: {
7071
7540
  method: "GET",
7072
7541
  path: "/v1/volumes/:id",
7073
- pathParams: z20.object({
7074
- id: z20.string().min(1, "Volume ID is required")
7542
+ pathParams: z21.object({
7543
+ id: z21.string().min(1, "Volume ID is required")
7075
7544
  }),
7076
7545
  responses: {
7077
7546
  200: publicVolumeDetailSchema,
@@ -7083,12 +7552,12 @@ var publicVolumeByIdContract = c15.router({
7083
7552
  description: "Get volume details by ID"
7084
7553
  }
7085
7554
  });
7086
- var publicVolumeVersionsContract = c15.router({
7555
+ var publicVolumeVersionsContract = c16.router({
7087
7556
  list: {
7088
7557
  method: "GET",
7089
7558
  path: "/v1/volumes/:id/versions",
7090
- pathParams: z20.object({
7091
- id: z20.string().min(1, "Volume ID is required")
7559
+ pathParams: z21.object({
7560
+ id: z21.string().min(1, "Volume ID is required")
7092
7561
  }),
7093
7562
  query: listQuerySchema,
7094
7563
  responses: {
@@ -7101,19 +7570,19 @@ var publicVolumeVersionsContract = c15.router({
7101
7570
  description: "List all versions of a volume with pagination"
7102
7571
  }
7103
7572
  });
7104
- var publicVolumeDownloadContract = c15.router({
7573
+ var publicVolumeDownloadContract = c16.router({
7105
7574
  download: {
7106
7575
  method: "GET",
7107
7576
  path: "/v1/volumes/:id/download",
7108
- pathParams: z20.object({
7109
- id: z20.string().min(1, "Volume ID is required")
7577
+ pathParams: z21.object({
7578
+ id: z21.string().min(1, "Volume ID is required")
7110
7579
  }),
7111
- query: z20.object({
7112
- version_id: z20.string().optional()
7580
+ query: z21.object({
7581
+ version_id: z21.string().optional()
7113
7582
  // Defaults to current version
7114
7583
  }),
7115
7584
  responses: {
7116
- 302: z20.undefined(),
7585
+ 302: z21.undefined(),
7117
7586
  // Redirect to presigned URL
7118
7587
  401: publicApiErrorSchema,
7119
7588
  404: publicApiErrorSchema,
@@ -7158,7 +7627,7 @@ var FEATURE_SWITCHES = {
7158
7627
  var ENV_LOADER_PATH = "/usr/local/bin/vm0-agent/env-loader.mjs";
7159
7628
 
7160
7629
  // src/lib/proxy/vm-registry.ts
7161
- import fs4 from "fs";
7630
+ import fs5 from "fs";
7162
7631
  var DEFAULT_REGISTRY_PATH = "/tmp/vm0-vm-registry.json";
7163
7632
  var VMRegistry = class {
7164
7633
  registryPath;
@@ -7172,8 +7641,8 @@ var VMRegistry = class {
7172
7641
  */
7173
7642
  load() {
7174
7643
  try {
7175
- if (fs4.existsSync(this.registryPath)) {
7176
- const content = fs4.readFileSync(this.registryPath, "utf-8");
7644
+ if (fs5.existsSync(this.registryPath)) {
7645
+ const content = fs5.readFileSync(this.registryPath, "utf-8");
7177
7646
  return JSON.parse(content);
7178
7647
  }
7179
7648
  } catch {
@@ -7187,8 +7656,8 @@ var VMRegistry = class {
7187
7656
  this.data.updatedAt = Date.now();
7188
7657
  const content = JSON.stringify(this.data, null, 2);
7189
7658
  const tempPath = `${this.registryPath}.tmp`;
7190
- fs4.writeFileSync(tempPath, content, { mode: 420 });
7191
- fs4.renameSync(tempPath, this.registryPath);
7659
+ fs5.writeFileSync(tempPath, content, { mode: 420 });
7660
+ fs5.renameSync(tempPath, this.registryPath);
7192
7661
  }
7193
7662
  /**
7194
7663
  * Register a VM with its IP address
@@ -7263,8 +7732,8 @@ function initVMRegistry(registryPath) {
7263
7732
 
7264
7733
  // src/lib/proxy/proxy-manager.ts
7265
7734
  import { spawn as spawn2 } from "child_process";
7266
- import fs5 from "fs";
7267
- import path3 from "path";
7735
+ import fs6 from "fs";
7736
+ import path4 from "path";
7268
7737
 
7269
7738
  // src/lib/proxy/mitm-addon-script.ts
7270
7739
  var RUNNER_MITM_ADDON_SCRIPT = `#!/usr/bin/env python3
@@ -7781,11 +8250,11 @@ var ProxyManager = class {
7781
8250
  * Ensure the addon script exists at the configured path
7782
8251
  */
7783
8252
  ensureAddonScript() {
7784
- const addonDir = path3.dirname(this.config.addonPath);
7785
- if (!fs5.existsSync(addonDir)) {
7786
- fs5.mkdirSync(addonDir, { recursive: true });
8253
+ const addonDir = path4.dirname(this.config.addonPath);
8254
+ if (!fs6.existsSync(addonDir)) {
8255
+ fs6.mkdirSync(addonDir, { recursive: true });
7787
8256
  }
7788
- fs5.writeFileSync(this.config.addonPath, RUNNER_MITM_ADDON_SCRIPT, {
8257
+ fs6.writeFileSync(this.config.addonPath, RUNNER_MITM_ADDON_SCRIPT, {
7789
8258
  mode: 493
7790
8259
  });
7791
8260
  console.log(
@@ -7796,11 +8265,11 @@ var ProxyManager = class {
7796
8265
  * Validate proxy configuration
7797
8266
  */
7798
8267
  validateConfig() {
7799
- if (!fs5.existsSync(this.config.caDir)) {
8268
+ if (!fs6.existsSync(this.config.caDir)) {
7800
8269
  throw new Error(`Proxy CA directory not found: ${this.config.caDir}`);
7801
8270
  }
7802
- const caCertPath = path3.join(this.config.caDir, "mitmproxy-ca.pem");
7803
- if (!fs5.existsSync(caCertPath)) {
8271
+ const caCertPath = path4.join(this.config.caDir, "mitmproxy-ca.pem");
8272
+ if (!fs6.existsSync(caCertPath)) {
7804
8273
  throw new Error(`Proxy CA certificate not found: ${caCertPath}`);
7805
8274
  }
7806
8275
  this.ensureAddonScript();
@@ -8189,17 +8658,17 @@ function buildEnvironmentVariables(context, apiUrl) {
8189
8658
  }
8190
8659
 
8191
8660
  // src/lib/network-logs/network-logs.ts
8192
- import fs6 from "fs";
8661
+ import fs7 from "fs";
8193
8662
  function getNetworkLogPath(runId) {
8194
8663
  return `/tmp/vm0-network-${runId}.jsonl`;
8195
8664
  }
8196
8665
  function readNetworkLogs(runId) {
8197
8666
  const logPath = getNetworkLogPath(runId);
8198
- if (!fs6.existsSync(logPath)) {
8667
+ if (!fs7.existsSync(logPath)) {
8199
8668
  return [];
8200
8669
  }
8201
8670
  try {
8202
- const content = fs6.readFileSync(logPath, "utf-8");
8671
+ const content = fs7.readFileSync(logPath, "utf-8");
8203
8672
  const lines = content.split("\n").filter((line) => line.trim());
8204
8673
  return lines.map((line) => JSON.parse(line));
8205
8674
  } catch (err) {
@@ -8212,8 +8681,8 @@ function readNetworkLogs(runId) {
8212
8681
  function cleanupNetworkLogs(runId) {
8213
8682
  const logPath = getNetworkLogPath(runId);
8214
8683
  try {
8215
- if (fs6.existsSync(logPath)) {
8216
- fs6.unlinkSync(logPath);
8684
+ if (fs7.existsSync(logPath)) {
8685
+ fs7.unlinkSync(logPath);
8217
8686
  }
8218
8687
  } catch (err) {
8219
8688
  console.error(
@@ -8256,7 +8725,7 @@ async function uploadNetworkLogs(apiUrl, sandboxToken, runId) {
8256
8725
  }
8257
8726
 
8258
8727
  // src/lib/vm-setup/vm-setup.ts
8259
- import fs7 from "fs";
8728
+ import fs8 from "fs";
8260
8729
 
8261
8730
  // src/lib/scripts/utils.ts
8262
8731
  function getAllScripts() {
@@ -8318,12 +8787,12 @@ async function restoreSessionHistory(ssh, resumeSession, workingDir, cliAgentTyp
8318
8787
  );
8319
8788
  }
8320
8789
  async function installProxyCA(ssh) {
8321
- if (!fs7.existsSync(PROXY_CA_CERT_PATH)) {
8790
+ if (!fs8.existsSync(PROXY_CA_CERT_PATH)) {
8322
8791
  throw new Error(
8323
8792
  `Proxy CA certificate not found at ${PROXY_CA_CERT_PATH}. Run generate-proxy-ca.sh first.`
8324
8793
  );
8325
8794
  }
8326
- const caCert = fs7.readFileSync(PROXY_CA_CERT_PATH, "utf-8");
8795
+ const caCert = fs8.readFileSync(PROXY_CA_CERT_PATH, "utf-8");
8327
8796
  console.log(
8328
8797
  `[Executor] Installing proxy CA certificate (${caCert.length} bytes)`
8329
8798
  );
@@ -8404,7 +8873,7 @@ async function executeJob(context, config, options = {}) {
8404
8873
  const log = options.logger ?? ((msg) => console.log(msg));
8405
8874
  log(`[Executor] Starting job ${context.runId} in VM ${vmId}`);
8406
8875
  try {
8407
- const workspacesDir = path4.join(process.cwd(), "workspaces");
8876
+ const workspacesDir = path5.join(process.cwd(), "workspaces");
8408
8877
  const vmConfig = {
8409
8878
  vmId,
8410
8879
  vcpus: config.sandbox.vcpu,
@@ -8412,7 +8881,7 @@ async function executeJob(context, config, options = {}) {
8412
8881
  kernelPath: config.firecracker.kernel,
8413
8882
  rootfsPath: config.firecracker.rootfs,
8414
8883
  firecrackerBinary: config.firecracker.binary,
8415
- workDir: path4.join(workspacesDir, `vm0-${vmId}`)
8884
+ workDir: path5.join(workspacesDir, `vm0-${vmId}`)
8416
8885
  };
8417
8886
  log(`[Executor] Creating VM ${vmId}...`);
8418
8887
  vm = new FirecrackerVM(vmConfig);
@@ -8437,7 +8906,7 @@ async function executeJob(context, config, options = {}) {
8437
8906
  log(
8438
8907
  `[Executor] Setting up network security for VM ${guestIp} (mitm=${mitmEnabled}, sealSecrets=${sealSecretsEnabled})`
8439
8908
  );
8440
- await setupVMProxyRules(guestIp, config.proxy.port);
8909
+ await setupVMProxyRules(guestIp, config.proxy.port, config.name);
8441
8910
  getVMRegistry().register(guestIp, context.runId, context.sandboxToken, {
8442
8911
  firewallRules: firewallConfig?.rules,
8443
8912
  mitmEnabled,
@@ -8608,7 +9077,7 @@ async function executeJob(context, config, options = {}) {
8608
9077
  if (context.experimentalFirewall?.enabled && guestIp) {
8609
9078
  log(`[Executor] Cleaning up network security for VM ${guestIp}`);
8610
9079
  try {
8611
- await removeVMProxyRules(guestIp, config.proxy.port);
9080
+ await removeVMProxyRules(guestIp, config.proxy.port, config.name);
8612
9081
  } catch (err) {
8613
9082
  console.error(
8614
9083
  `[Executor] Failed to remove VM proxy rules: ${err instanceof Error ? err.message : "Unknown error"}`
@@ -8637,17 +9106,17 @@ async function executeJob(context, config, options = {}) {
8637
9106
  }
8638
9107
 
8639
9108
  // src/commands/start.ts
8640
- var activeJobs = /* @__PURE__ */ new Set();
9109
+ var activeRuns = /* @__PURE__ */ new Set();
8641
9110
  function writeStatusFile(statusFilePath, mode, startedAt) {
8642
9111
  const status = {
8643
9112
  mode,
8644
- active_jobs: activeJobs.size,
8645
- active_job_ids: Array.from(activeJobs),
9113
+ active_runs: activeRuns.size,
9114
+ active_run_ids: Array.from(activeRuns),
8646
9115
  started_at: startedAt.toISOString(),
8647
9116
  updated_at: (/* @__PURE__ */ new Date()).toISOString()
8648
9117
  };
8649
9118
  try {
8650
- writeFileSync(statusFilePath, JSON.stringify(status, null, 2));
9119
+ writeFileSync2(statusFilePath, JSON.stringify(status, null, 2));
8651
9120
  } catch (err) {
8652
9121
  console.error(
8653
9122
  `Failed to write status file: ${err instanceof Error ? err.message : "Unknown error"}`
@@ -8700,6 +9169,12 @@ var startCommand = new Command("start").description("Start the runner").option("
8700
9169
  }
8701
9170
  console.log("Setting up network bridge...");
8702
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();
8703
9178
  console.log("Initializing network proxy...");
8704
9179
  initVMRegistry();
8705
9180
  const proxyManager = initProxyManager({
@@ -8719,7 +9194,7 @@ var startCommand = new Command("start").description("Start the runner").option("
8719
9194
  "Jobs with experimentalFirewall enabled will run without network interception"
8720
9195
  );
8721
9196
  }
8722
- const statusFilePath = join(dirname(options.config), "status.json");
9197
+ const statusFilePath = join2(dirname(options.config), "status.json");
8723
9198
  const startedAt = /* @__PURE__ */ new Date();
8724
9199
  const state = { mode: "running" };
8725
9200
  const updateStatus = () => {
@@ -8750,7 +9225,7 @@ var startCommand = new Command("start").description("Start the runner").option("
8750
9225
  if (state.mode === "running") {
8751
9226
  console.log("\n[Maintenance] Entering drain mode...");
8752
9227
  console.log(
8753
- `[Maintenance] Active jobs: ${activeJobs.size} (will wait for completion)`
9228
+ `[Maintenance] Active jobs: ${activeRuns.size} (will wait for completion)`
8754
9229
  );
8755
9230
  state.mode = "draining";
8756
9231
  updateStatus();
@@ -8759,7 +9234,7 @@ var startCommand = new Command("start").description("Start the runner").option("
8759
9234
  const jobPromises = /* @__PURE__ */ new Set();
8760
9235
  while (running) {
8761
9236
  if (state.mode === "draining") {
8762
- if (activeJobs.size === 0) {
9237
+ if (activeRuns.size === 0) {
8763
9238
  console.log("[Maintenance] All jobs completed, exiting drain mode");
8764
9239
  running = false;
8765
9240
  break;
@@ -8770,7 +9245,7 @@ var startCommand = new Command("start").description("Start the runner").option("
8770
9245
  }
8771
9246
  continue;
8772
9247
  }
8773
- if (activeJobs.size >= config.sandbox.max_concurrent) {
9248
+ if (activeRuns.size >= config.sandbox.max_concurrent) {
8774
9249
  if (jobPromises.size > 0) {
8775
9250
  await Promise.race(jobPromises);
8776
9251
  updateStatus();
@@ -8795,7 +9270,7 @@ var startCommand = new Command("start").description("Start the runner").option("
8795
9270
  () => claimJob(config.server, job.runId)
8796
9271
  );
8797
9272
  console.log(`Claimed job: ${context.runId}`);
8798
- activeJobs.add(context.runId);
9273
+ activeRuns.add(context.runId);
8799
9274
  updateStatus();
8800
9275
  const jobPromise = executeJob2(context, config).catch((error) => {
8801
9276
  console.error(
@@ -8803,7 +9278,7 @@ var startCommand = new Command("start").description("Start the runner").option("
8803
9278
  error instanceof Error ? error.message : "Unknown error"
8804
9279
  );
8805
9280
  }).finally(() => {
8806
- activeJobs.delete(context.runId);
9281
+ activeRuns.delete(context.runId);
8807
9282
  jobPromises.delete(jobPromise);
8808
9283
  updateStatus();
8809
9284
  });
@@ -8849,32 +9324,496 @@ var startCommand = new Command("start").description("Start the runner").option("
8849
9324
  }
8850
9325
  });
8851
9326
 
8852
- // src/commands/status.ts
9327
+ // src/commands/doctor.ts
8853
9328
  import { Command as Command2 } from "commander";
8854
- 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) => {
8855
9441
  try {
8856
9442
  const config = loadConfig(options.config);
8857
- console.log(`Checking connectivity to ${config.server.url}...`);
8858
- console.log(`Runner group: ${config.group}`);
8859
- 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
+ }
8860
9467
  console.log("");
8861
- console.log("\u2713 Runner can connect to API");
8862
- console.log(` API: ${config.server.url}`);
8863
- console.log(` Group: ${config.group}`);
8864
- console.log(` Auth: OK`);
8865
- 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_run_ids) {
9517
+ for (const runId of status.active_run_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(`Runs (${jobs.length} active, max ${maxConcurrent}):`);
9540
+ if (jobs.length === 0) {
9541
+ console.log(" No active runs");
9542
+ } else {
9543
+ console.log(
9544
+ " Run ID VM ID IP Status"
9545
+ );
9546
+ for (const job of jobs) {
9547
+ const ipConflict = (ipToVmIds.get(job.ip)?.length ?? 0) > 1;
9548
+ let statusText;
9549
+ if (ipConflict) {
9550
+ statusText = "\u26A0\uFE0F IP conflict!";
9551
+ } else if (job.hasProcess) {
9552
+ statusText = `\u2713 Running (PID ${job.pid})`;
9553
+ } else {
9554
+ statusText = "\u26A0\uFE0F No process";
9555
+ }
9556
+ console.log(
9557
+ ` ${job.runId} ${job.vmId} ${job.ip.padEnd(15)} ${statusText}`
9558
+ );
9559
+ }
9560
+ }
9561
+ console.log("");
9562
+ for (const job of jobs) {
9563
+ if (!job.hasProcess) {
9564
+ warnings.push({
9565
+ message: `Run ${job.vmId} in status.json but no Firecracker process running`
9566
+ });
9567
+ }
9568
+ }
9569
+ for (const [ip, vmIds] of ipToVmIds) {
9570
+ if (vmIds.length > 1) {
9571
+ warnings.push({
9572
+ message: `IP conflict: ${ip} assigned to ${vmIds.join(", ")}`
9573
+ });
9574
+ }
9575
+ }
9576
+ const processVmIds = new Set(processes.map((p) => p.vmId));
9577
+ for (const proc of processes) {
9578
+ if (!statusVmIds.has(proc.vmId)) {
9579
+ warnings.push({
9580
+ message: `Orphan process: PID ${proc.pid} (vmId ${proc.vmId}) not in status.json`
9581
+ });
9582
+ }
9583
+ }
9584
+ for (const tap of tapDevices) {
9585
+ const vmId = tap.replace("tap", "");
9586
+ if (!processVmIds.has(vmId) && !statusVmIds.has(vmId)) {
9587
+ warnings.push({
9588
+ message: `Orphan TAP device: ${tap} (no matching job or process)`
9589
+ });
9590
+ }
9591
+ }
9592
+ for (const ws of workspaces) {
9593
+ const vmId = ws.replace("vm0-", "");
9594
+ if (!processVmIds.has(vmId) && !statusVmIds.has(vmId)) {
9595
+ warnings.push({
9596
+ message: `Orphan workspace: ${ws} (no matching job or process)`
9597
+ });
9598
+ }
9599
+ }
9600
+ const activeVmIps = new Set(jobs.map((j) => j.ip));
9601
+ const iptablesRules = await listIptablesNatRules();
9602
+ const orphanedIptables = await findOrphanedIptablesRules(
9603
+ iptablesRules,
9604
+ activeVmIps,
9605
+ proxyPort
9606
+ );
9607
+ for (const rule of orphanedIptables) {
9608
+ warnings.push({
9609
+ message: `Orphan iptables rule: redirect ${rule.sourceIp}:${rule.destPort} -> :${rule.redirectPort}`
9610
+ });
9611
+ }
9612
+ console.log("Warnings:");
9613
+ if (warnings.length === 0) {
9614
+ console.log(" None");
9615
+ } else {
9616
+ for (const w of warnings) {
9617
+ console.log(` - ${w.message}`);
9618
+ }
9619
+ }
9620
+ process.exit(warnings.length > 0 ? 1 : 0);
8866
9621
  } catch (error) {
8867
- console.error("");
8868
- console.error("\u2717 Runner cannot connect to API");
8869
9622
  console.error(
8870
- ` Error: ${error instanceof Error ? error.message : "Unknown error"}`
9623
+ `Error: ${error instanceof Error ? error.message : "Unknown error"}`
8871
9624
  );
8872
9625
  process.exit(1);
8873
9626
  }
8874
9627
  });
9628
+ function formatUptime(ms) {
9629
+ const seconds = Math.floor(ms / 1e3);
9630
+ const minutes = Math.floor(seconds / 60);
9631
+ const hours = Math.floor(minutes / 60);
9632
+ const days = Math.floor(hours / 24);
9633
+ if (days > 0) return `${days}d ${hours % 24}h`;
9634
+ if (hours > 0) return `${hours}h ${minutes % 60}m`;
9635
+ if (minutes > 0) return `${minutes}m`;
9636
+ return `${seconds}s`;
9637
+ }
8875
9638
 
8876
- // src/commands/benchmark.ts
9639
+ // src/commands/kill.ts
8877
9640
  import { Command as Command3 } from "commander";
9641
+ import { existsSync as existsSync4, readFileSync as readFileSync4, writeFileSync as writeFileSync3, rmSync } from "fs";
9642
+ import { dirname as dirname3, join as join4 } from "path";
9643
+ import * as readline2 from "readline";
9644
+ var killCommand = new Command3("kill").description("Force terminate a run and clean up all resources").argument("<run-id>", "Run ID (full UUID or short 8-char vmId)").option("--config <path>", "Config file path", "./runner.yaml").option("--force", "Skip confirmation prompt").action(
9645
+ async (runIdArg, options) => {
9646
+ try {
9647
+ loadConfig(options.config);
9648
+ const configDir = dirname3(options.config);
9649
+ const statusFilePath = join4(configDir, "status.json");
9650
+ const workspacesDir = join4(configDir, "workspaces");
9651
+ const { vmId, runId } = resolveRunId(runIdArg, statusFilePath);
9652
+ console.log(`Killing run ${vmId}...`);
9653
+ const proc = findProcessByVmId(vmId);
9654
+ const tapDevice = `tap${vmId}`;
9655
+ const workspaceDir = join4(workspacesDir, `vm0-${vmId}`);
9656
+ console.log("");
9657
+ console.log("Resources to clean up:");
9658
+ if (proc) {
9659
+ console.log(` - Firecracker process (PID ${proc.pid})`);
9660
+ } else {
9661
+ console.log(" - Firecracker process: not found");
9662
+ }
9663
+ console.log(` - TAP device: ${tapDevice}`);
9664
+ console.log(` - Workspace: ${workspaceDir}`);
9665
+ if (runId) {
9666
+ console.log(` - status.json entry: ${runId.substring(0, 12)}...`);
9667
+ }
9668
+ console.log("");
9669
+ if (!options.force) {
9670
+ const confirmed = await confirm("Proceed with cleanup?");
9671
+ if (!confirmed) {
9672
+ console.log("Aborted.");
9673
+ process.exit(0);
9674
+ }
9675
+ }
9676
+ const results = [];
9677
+ if (proc) {
9678
+ const killed = await killProcess(proc.pid);
9679
+ results.push({
9680
+ step: "Firecracker process",
9681
+ success: killed,
9682
+ message: killed ? `PID ${proc.pid} terminated` : `Failed to kill PID ${proc.pid}`
9683
+ });
9684
+ } else {
9685
+ results.push({
9686
+ step: "Firecracker process",
9687
+ success: true,
9688
+ message: "Not running"
9689
+ });
9690
+ }
9691
+ try {
9692
+ await deleteTapDevice(tapDevice);
9693
+ results.push({
9694
+ step: "TAP device",
9695
+ success: true,
9696
+ message: `${tapDevice} deleted`
9697
+ });
9698
+ } catch (error) {
9699
+ results.push({
9700
+ step: "TAP device",
9701
+ success: false,
9702
+ message: error instanceof Error ? error.message : "Unknown error"
9703
+ });
9704
+ }
9705
+ if (existsSync4(workspaceDir)) {
9706
+ try {
9707
+ rmSync(workspaceDir, { recursive: true, force: true });
9708
+ results.push({
9709
+ step: "Workspace",
9710
+ success: true,
9711
+ message: `${workspaceDir} removed`
9712
+ });
9713
+ } catch (error) {
9714
+ results.push({
9715
+ step: "Workspace",
9716
+ success: false,
9717
+ message: error instanceof Error ? error.message : "Unknown error"
9718
+ });
9719
+ }
9720
+ } else {
9721
+ results.push({
9722
+ step: "Workspace",
9723
+ success: true,
9724
+ message: "Not found (already cleaned)"
9725
+ });
9726
+ }
9727
+ if (runId && existsSync4(statusFilePath)) {
9728
+ try {
9729
+ const status = JSON.parse(
9730
+ readFileSync4(statusFilePath, "utf-8")
9731
+ );
9732
+ const oldCount = status.active_runs;
9733
+ status.active_run_ids = status.active_run_ids.filter(
9734
+ (id) => id !== runId
9735
+ );
9736
+ status.active_runs = status.active_run_ids.length;
9737
+ status.updated_at = (/* @__PURE__ */ new Date()).toISOString();
9738
+ writeFileSync3(statusFilePath, JSON.stringify(status, null, 2));
9739
+ results.push({
9740
+ step: "status.json",
9741
+ success: true,
9742
+ message: `Updated (active_runs: ${oldCount} -> ${status.active_runs})`
9743
+ });
9744
+ } catch (error) {
9745
+ results.push({
9746
+ step: "status.json",
9747
+ success: false,
9748
+ message: error instanceof Error ? error.message : "Unknown error"
9749
+ });
9750
+ }
9751
+ } else {
9752
+ results.push({
9753
+ step: "status.json",
9754
+ success: true,
9755
+ message: "No update needed"
9756
+ });
9757
+ }
9758
+ console.log("");
9759
+ let allSuccess = true;
9760
+ for (const r of results) {
9761
+ const icon = r.success ? "\u2713" : "\u2717";
9762
+ console.log(` ${icon} ${r.step}: ${r.message}`);
9763
+ if (!r.success) allSuccess = false;
9764
+ }
9765
+ console.log("");
9766
+ if (allSuccess) {
9767
+ console.log(`Run ${vmId} killed successfully.`);
9768
+ process.exit(0);
9769
+ } else {
9770
+ console.log(`Run ${vmId} cleanup completed with errors.`);
9771
+ process.exit(1);
9772
+ }
9773
+ } catch (error) {
9774
+ console.error(
9775
+ `Error: ${error instanceof Error ? error.message : "Unknown error"}`
9776
+ );
9777
+ process.exit(1);
9778
+ }
9779
+ }
9780
+ );
9781
+ function resolveRunId(input, statusFilePath) {
9782
+ if (input.includes("-")) {
9783
+ const vmId = input.split("-")[0];
9784
+ return { vmId: vmId ?? input, runId: input };
9785
+ }
9786
+ if (existsSync4(statusFilePath)) {
9787
+ try {
9788
+ const status = JSON.parse(
9789
+ readFileSync4(statusFilePath, "utf-8")
9790
+ );
9791
+ const match = status.active_run_ids.find(
9792
+ (id) => id.startsWith(input)
9793
+ );
9794
+ if (match) {
9795
+ return { vmId: input, runId: match };
9796
+ }
9797
+ } catch {
9798
+ }
9799
+ }
9800
+ return { vmId: input, runId: null };
9801
+ }
9802
+ async function confirm(message) {
9803
+ const rl = readline2.createInterface({
9804
+ input: process.stdin,
9805
+ output: process.stdout
9806
+ });
9807
+ return new Promise((resolve) => {
9808
+ rl.question(`${message} [y/N] `, (answer) => {
9809
+ rl.close();
9810
+ resolve(answer.toLowerCase() === "y" || answer.toLowerCase() === "yes");
9811
+ });
9812
+ });
9813
+ }
9814
+
9815
+ // src/commands/benchmark.ts
9816
+ import { Command as Command4 } from "commander";
8878
9817
  import crypto from "crypto";
8879
9818
 
8880
9819
  // src/lib/timing.ts
@@ -8925,7 +9864,7 @@ function createBenchmarkContext(prompt, options) {
8925
9864
  cliAgentType: options.agentType
8926
9865
  };
8927
9866
  }
8928
- var benchmarkCommand = new Command3("benchmark").description(
9867
+ var benchmarkCommand = new Command4("benchmark").description(
8929
9868
  "Run a VM performance benchmark (executes bash command directly)"
8930
9869
  ).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) => {
8931
9870
  const timer = new Timer();
@@ -8965,10 +9904,11 @@ var benchmarkCommand = new Command3("benchmark").description(
8965
9904
  });
8966
9905
 
8967
9906
  // src/index.ts
8968
- var version = true ? "2.12.0" : "0.1.0";
9907
+ var version = true ? "2.13.1" : "0.1.0";
8969
9908
  program.name("vm0-runner").version(version).description("Self-hosted runner for VM0 agents");
8970
9909
  program.addCommand(startCommand);
8971
- program.addCommand(statusCommand);
9910
+ program.addCommand(doctorCommand);
9911
+ program.addCommand(killCommand);
8972
9912
  program.addCommand(benchmarkCommand);
8973
9913
  program.parse();
8974
9914
  //# sourceMappingURL=index.js.map