@vm0/runner 3.0.1 → 3.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/index.js +113 -122
  2. package/package.json +1 -1
package/index.js CHANGED
@@ -1226,7 +1226,6 @@ import * as net from "net";
1226
1226
  import * as fs4 from "fs";
1227
1227
  import * as crypto from "crypto";
1228
1228
  var VSOCK_PORT = 1e3;
1229
- var CONNECT_TIMEOUT_MS = 5e3;
1230
1229
  var HEADER_SIZE = 4;
1231
1230
  var MAX_MESSAGE_SIZE = 1024 * 1024;
1232
1231
  var DEFAULT_EXEC_TIMEOUT_MS = 3e5;
@@ -1261,90 +1260,6 @@ var VsockClient = class {
1261
1260
  constructor(vsockPath) {
1262
1261
  this.vsockPath = vsockPath;
1263
1262
  }
1264
- /**
1265
- * Connect to the guest agent via vsock
1266
- */
1267
- async connect() {
1268
- if (this.connected && this.socket) {
1269
- return;
1270
- }
1271
- return new Promise((resolve, reject) => {
1272
- if (!fs4.existsSync(this.vsockPath)) {
1273
- reject(new Error(`Vsock socket not found: ${this.vsockPath}`));
1274
- return;
1275
- }
1276
- const socket = net.createConnection(this.vsockPath);
1277
- const decoder = new Decoder();
1278
- let fcConnected = false;
1279
- let gotReady = false;
1280
- let pingId = null;
1281
- let connectionEstablished = false;
1282
- const timeout = setTimeout(() => {
1283
- socket.destroy();
1284
- reject(new Error("Vsock connection timeout"));
1285
- }, CONNECT_TIMEOUT_MS);
1286
- socket.on("connect", () => {
1287
- socket.write(`CONNECT ${VSOCK_PORT}
1288
- `);
1289
- });
1290
- socket.on("data", (data) => {
1291
- if (!fcConnected) {
1292
- const str = data.toString();
1293
- if (str.startsWith("OK ")) {
1294
- fcConnected = true;
1295
- } else {
1296
- clearTimeout(timeout);
1297
- socket.destroy();
1298
- reject(new Error(`Firecracker connect failed: ${str.trim()}`));
1299
- }
1300
- return;
1301
- }
1302
- try {
1303
- for (const msg of decoder.decode(data)) {
1304
- if (!connectionEstablished) {
1305
- if (!gotReady && msg.type === "ready") {
1306
- gotReady = true;
1307
- pingId = crypto.randomUUID();
1308
- const ping = { type: "ping", id: pingId, payload: {} };
1309
- socket.write(encode(ping));
1310
- } else if (msg.type === "pong" && msg.id === pingId) {
1311
- clearTimeout(timeout);
1312
- this.socket = socket;
1313
- this.connected = true;
1314
- connectionEstablished = true;
1315
- resolve();
1316
- }
1317
- } else {
1318
- this.handleMessage(msg);
1319
- }
1320
- }
1321
- } catch (e) {
1322
- clearTimeout(timeout);
1323
- socket.destroy();
1324
- reject(new Error(`Failed to parse message: ${e}`));
1325
- }
1326
- });
1327
- socket.on("error", (err) => {
1328
- clearTimeout(timeout);
1329
- this.connected = false;
1330
- this.socket = null;
1331
- reject(new Error(`Vsock error: ${err.message}`));
1332
- });
1333
- socket.on("close", () => {
1334
- clearTimeout(timeout);
1335
- this.connected = false;
1336
- this.socket = null;
1337
- if (!gotReady) {
1338
- reject(new Error("Vsock closed before ready"));
1339
- }
1340
- for (const [id, req] of this.pendingRequests) {
1341
- clearTimeout(req.timeout);
1342
- req.reject(new Error("Connection closed"));
1343
- this.pendingRequests.delete(id);
1344
- }
1345
- });
1346
- });
1347
- }
1348
1263
  /**
1349
1264
  * Handle incoming message and route to pending request
1350
1265
  */
@@ -1360,9 +1275,8 @@ var VsockClient = class {
1360
1275
  * Send a request and wait for response
1361
1276
  */
1362
1277
  async request(type, payload, timeoutMs) {
1363
- await this.connect();
1364
- if (!this.socket) {
1365
- throw new Error("Not connected");
1278
+ if (!this.connected || !this.socket) {
1279
+ throw new Error("Not connected - call waitForGuestConnection() first");
1366
1280
  }
1367
1281
  const id = crypto.randomUUID();
1368
1282
  const msg = { type, id, payload };
@@ -1486,24 +1400,113 @@ var VsockClient = class {
1486
1400
  }
1487
1401
  }
1488
1402
  /**
1489
- * Wait for vsock to become available
1403
+ * Wait for guest to connect (Guest-initiated mode)
1404
+ *
1405
+ * Instead of polling, this listens on "{vsockPath}_{port}" and waits
1406
+ * for the guest to actively connect. This provides zero-latency
1407
+ * notification when the guest is ready.
1408
+ *
1409
+ * Flow:
1410
+ * 1. Host creates UDS server at "{vsockPath}_{port}"
1411
+ * 2. Guest boots and vsock-agent connects to CID=2, port
1412
+ * 3. Firecracker forwards connection to Host's UDS
1413
+ * 4. Host accepts, receives "ready", sends ping/pong
1490
1414
  */
1491
- async waitUntilReachable(timeoutMs = 12e4, intervalMs = 2e3) {
1492
- const start = Date.now();
1493
- while (Date.now() - start < timeoutMs) {
1494
- if (await this.isReachable()) {
1495
- return;
1496
- }
1497
- await new Promise((resolve) => {
1498
- const remaining = timeoutMs - (Date.now() - start);
1499
- if (remaining > 0) {
1500
- setTimeout(resolve, Math.min(intervalMs, remaining));
1501
- } else {
1502
- resolve();
1415
+ async waitForGuestConnection(timeoutMs = 3e4) {
1416
+ if (this.connected && this.socket) {
1417
+ return;
1418
+ }
1419
+ const listenerPath = `${this.vsockPath}_${VSOCK_PORT}`;
1420
+ if (fs4.existsSync(listenerPath)) {
1421
+ fs4.unlinkSync(listenerPath);
1422
+ }
1423
+ return new Promise((resolve, reject) => {
1424
+ const server = net.createServer();
1425
+ const decoder = new Decoder();
1426
+ let settled = false;
1427
+ const timeout = setTimeout(() => {
1428
+ if (!settled) {
1429
+ settled = true;
1430
+ server.close();
1431
+ if (fs4.existsSync(listenerPath)) {
1432
+ fs4.unlinkSync(listenerPath);
1433
+ }
1434
+ reject(new Error(`Guest connection timeout after ${timeoutMs}ms`));
1503
1435
  }
1436
+ }, timeoutMs);
1437
+ const cleanup = (err) => {
1438
+ if (!settled) {
1439
+ settled = true;
1440
+ clearTimeout(timeout);
1441
+ server.close();
1442
+ if (fs4.existsSync(listenerPath)) {
1443
+ fs4.unlinkSync(listenerPath);
1444
+ }
1445
+ reject(err);
1446
+ }
1447
+ };
1448
+ server.on("error", (err) => {
1449
+ cleanup(new Error(`Server error: ${err.message}`));
1504
1450
  });
1505
- }
1506
- throw new Error(`Vsock not reachable after ${timeoutMs}ms`);
1451
+ server.on("connection", (socket) => {
1452
+ server.close();
1453
+ let State;
1454
+ ((State2) => {
1455
+ State2[State2["WaitingForReady"] = 0] = "WaitingForReady";
1456
+ State2[State2["WaitingForPong"] = 1] = "WaitingForPong";
1457
+ State2[State2["Connected"] = 2] = "Connected";
1458
+ })(State || (State = {}));
1459
+ let state = 0 /* WaitingForReady */;
1460
+ let pingId = null;
1461
+ socket.on("data", (data) => {
1462
+ try {
1463
+ for (const msg of decoder.decode(data)) {
1464
+ if (state === 0 /* WaitingForReady */ && msg.type === "ready") {
1465
+ state = 1 /* WaitingForPong */;
1466
+ pingId = crypto.randomUUID();
1467
+ socket.write(encode({ type: "ping", id: pingId, payload: {} }));
1468
+ } else if (state === 1 /* WaitingForPong */ && msg.type === "pong" && msg.id === pingId) {
1469
+ if (settled) {
1470
+ socket.destroy();
1471
+ return;
1472
+ }
1473
+ settled = true;
1474
+ clearTimeout(timeout);
1475
+ if (fs4.existsSync(listenerPath)) {
1476
+ fs4.unlinkSync(listenerPath);
1477
+ }
1478
+ state = 2 /* Connected */;
1479
+ this.socket = socket;
1480
+ this.connected = true;
1481
+ resolve();
1482
+ } else if (state === 2 /* Connected */) {
1483
+ this.handleMessage(msg);
1484
+ }
1485
+ }
1486
+ } catch (e) {
1487
+ cleanup(new Error(`Failed to parse message: ${e}`));
1488
+ }
1489
+ });
1490
+ socket.on("error", (err) => {
1491
+ cleanup(new Error(`Socket error: ${err.message}`));
1492
+ });
1493
+ socket.on("close", () => {
1494
+ if (!settled) {
1495
+ cleanup(new Error("Guest disconnected before ready"));
1496
+ }
1497
+ this.connected = false;
1498
+ this.socket = null;
1499
+ const pending = Array.from(this.pendingRequests.values());
1500
+ this.pendingRequests.clear();
1501
+ for (const req of pending) {
1502
+ clearTimeout(req.timeout);
1503
+ req.reject(new Error("Connection closed"));
1504
+ }
1505
+ });
1506
+ });
1507
+ server.listen(listenerPath, () => {
1508
+ });
1509
+ });
1507
1510
  }
1508
1511
  /**
1509
1512
  * Create a directory on the remote VM
@@ -1533,10 +1536,11 @@ var VsockClient = class {
1533
1536
  this.socket = null;
1534
1537
  }
1535
1538
  this.connected = false;
1536
- for (const [id, req] of this.pendingRequests) {
1539
+ const pending = Array.from(this.pendingRequests.values());
1540
+ this.pendingRequests.clear();
1541
+ for (const req of pending) {
1537
1542
  clearTimeout(req.timeout);
1538
1543
  req.reject(new Error("Connection closed"));
1539
- this.pendingRequests.delete(id);
1540
1544
  }
1541
1545
  }
1542
1546
  };
@@ -5542,9 +5546,6 @@ var appStringSchema = z5.string().superRefine((val, ctx) => {
5542
5546
  });
5543
5547
  }
5544
5548
  });
5545
- var nonEmptyStringArraySchema = z5.array(
5546
- z5.string().min(1, "Array entries cannot be empty strings")
5547
- );
5548
5549
  var agentDefinitionSchema = z5.object({
5549
5550
  description: z5.string().optional(),
5550
5551
  /**
@@ -5589,17 +5590,7 @@ var agentDefinitionSchema = z5.object({
5589
5590
  * Requires experimental_runner to be configured.
5590
5591
  * When enabled, filters outbound traffic by domain/IP rules.
5591
5592
  */
5592
- experimental_firewall: experimentalFirewallSchema.optional(),
5593
- /**
5594
- * Array of secret names to inject from the scope's secret store.
5595
- * Each entry must be a non-empty string.
5596
- */
5597
- experimental_secrets: nonEmptyStringArraySchema.optional(),
5598
- /**
5599
- * Array of variable names to inject from the scope's variable store.
5600
- * Each entry must be a non-empty string.
5601
- */
5602
- experimental_vars: nonEmptyStringArraySchema.optional()
5593
+ experimental_firewall: experimentalFirewallSchema.optional()
5603
5594
  });
5604
5595
  var agentComposeContentSchema = z5.object({
5605
5596
  version: z5.string().min(1, "Version is required"),
@@ -9213,10 +9204,10 @@ async function executeJob(context, config, options = {}) {
9213
9204
  const vsockPath = vm.getVsockPath();
9214
9205
  const guest = new VsockClient(vsockPath);
9215
9206
  log(`[Executor] Using vsock for guest communication: ${vsockPath}`);
9216
- log(`[Executor] Verifying vsock connectivity...`);
9207
+ log(`[Executor] Waiting for guest connection...`);
9217
9208
  await withSandboxTiming(
9218
9209
  "guest_wait",
9219
- () => guest.waitUntilReachable(3e4, 1e3)
9210
+ () => guest.waitForGuestConnection(3e4)
9220
9211
  );
9221
9212
  log(`[Executor] Guest client ready`);
9222
9213
  const firewallConfig = context.experimentalFirewall;
@@ -10240,7 +10231,7 @@ var benchmarkCommand = new Command4("benchmark").description(
10240
10231
  });
10241
10232
 
10242
10233
  // src/index.ts
10243
- var version = true ? "3.0.1" : "0.1.0";
10234
+ var version = true ? "3.0.2" : "0.1.0";
10244
10235
  program.name("vm0-runner").version(version).description("Self-hosted runner for VM0 agents");
10245
10236
  program.addCommand(startCommand);
10246
10237
  program.addCommand(doctorCommand);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vm0/runner",
3
- "version": "3.0.1",
3
+ "version": "3.0.2",
4
4
  "description": "Self-hosted runner for VM0 agents",
5
5
  "repository": {
6
6
  "type": "git",