@vm0/runner 3.7.0 → 3.7.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 +173 -188
  2. package/package.json +1 -1
package/index.js CHANGED
@@ -1072,133 +1072,192 @@ import { promisify as promisify3 } from "util";
1072
1072
  var execAsync3 = promisify3(exec3);
1073
1073
  var logger3 = createLogger("OverlayPool");
1074
1074
  var VM0_RUN_DIR2 = "/var/run/vm0";
1075
- var POOL_DIR = path2.join(VM0_RUN_DIR2, "overlay-pool");
1075
+ var DEFAULT_POOL_DIR = path2.join(VM0_RUN_DIR2, "overlay-pool");
1076
1076
  var OVERLAY_SIZE = 2 * 1024 * 1024 * 1024;
1077
- var poolState = {
1078
- initialized: false,
1079
- config: null,
1080
- queue: [],
1081
- replenishing: false
1082
- };
1083
- async function ensurePoolDir() {
1084
- if (!fs3.existsSync(VM0_RUN_DIR2)) {
1085
- await execAsync3(`sudo mkdir -p ${VM0_RUN_DIR2}`);
1086
- await execAsync3(`sudo chmod 777 ${VM0_RUN_DIR2}`);
1087
- }
1088
- if (!fs3.existsSync(POOL_DIR)) {
1089
- fs3.mkdirSync(POOL_DIR, { recursive: true });
1090
- }
1091
- }
1092
- async function createOverlayFile(filePath) {
1077
+ async function defaultCreateFile(filePath) {
1093
1078
  const fd = fs3.openSync(filePath, "w");
1094
1079
  fs3.ftruncateSync(fd, OVERLAY_SIZE);
1095
1080
  fs3.closeSync(fd);
1096
1081
  await execAsync3(`mkfs.ext4 -F -q "${filePath}"`);
1097
1082
  }
1098
- function generateFileName() {
1099
- return `overlay-${randomUUID()}.ext4`;
1100
- }
1101
- function scanPoolDir() {
1102
- if (!fs3.existsSync(POOL_DIR)) {
1103
- return [];
1083
+ var OverlayPool = class {
1084
+ initialized = false;
1085
+ queue = [];
1086
+ replenishing = false;
1087
+ config;
1088
+ constructor(config) {
1089
+ this.config = {
1090
+ size: config.size,
1091
+ replenishThreshold: config.replenishThreshold,
1092
+ poolDir: config.poolDir ?? DEFAULT_POOL_DIR,
1093
+ createFile: config.createFile ?? defaultCreateFile
1094
+ };
1104
1095
  }
1105
- return fs3.readdirSync(POOL_DIR).filter((f) => f.startsWith("overlay-") && f.endsWith(".ext4")).map((f) => path2.join(POOL_DIR, f));
1106
- }
1107
- async function replenishPool() {
1108
- if (poolState.replenishing || !poolState.initialized || !poolState.config) {
1109
- return;
1096
+ /**
1097
+ * Generate unique file name using UUID
1098
+ */
1099
+ generateFileName() {
1100
+ return `overlay-${randomUUID()}.ext4`;
1110
1101
  }
1111
- const needed = poolState.config.size - poolState.queue.length;
1112
- if (needed <= 0) {
1113
- return;
1102
+ /**
1103
+ * Ensure the pool directory exists
1104
+ */
1105
+ async ensurePoolDir() {
1106
+ const parentDir = path2.dirname(this.config.poolDir);
1107
+ if (!fs3.existsSync(parentDir)) {
1108
+ await execAsync3(`sudo mkdir -p ${parentDir}`);
1109
+ await execAsync3(`sudo chmod 777 ${parentDir}`);
1110
+ }
1111
+ if (!fs3.existsSync(this.config.poolDir)) {
1112
+ fs3.mkdirSync(this.config.poolDir, { recursive: true });
1113
+ }
1114
1114
  }
1115
- poolState.replenishing = true;
1116
- logger3.log(`Replenishing pool: creating ${needed} overlay(s)...`);
1117
- try {
1118
- const promises = [];
1119
- for (let i = 0; i < needed; i++) {
1120
- const filePath = path2.join(POOL_DIR, generateFileName());
1121
- promises.push(
1122
- createOverlayFile(filePath).then(() => {
1123
- poolState.queue.push(filePath);
1124
- })
1125
- );
1115
+ /**
1116
+ * Scan pool directory for overlay files
1117
+ */
1118
+ scanPoolDir() {
1119
+ if (!fs3.existsSync(this.config.poolDir)) {
1120
+ return [];
1126
1121
  }
1127
- await Promise.all(promises);
1128
- logger3.log(`Pool replenished: ${poolState.queue.length} available`);
1129
- } catch (err) {
1130
- logger3.error(
1131
- `Replenish failed: ${err instanceof Error ? err.message : "Unknown"}`
1132
- );
1133
- } finally {
1134
- poolState.replenishing = false;
1122
+ return fs3.readdirSync(this.config.poolDir).filter((f) => f.startsWith("overlay-") && f.endsWith(".ext4")).map((f) => path2.join(this.config.poolDir, f));
1135
1123
  }
1136
- }
1137
- async function initOverlayPool(config) {
1138
- poolState.config = config;
1139
- poolState.queue = [];
1140
- logger3.log(
1141
- `Initializing overlay pool (size=${config.size}, threshold=${config.replenishThreshold})...`
1142
- );
1143
- await ensurePoolDir();
1144
- const existing = scanPoolDir();
1145
- if (existing.length > 0) {
1146
- logger3.log(`Cleaning up ${existing.length} stale overlay(s)`);
1147
- for (const file of existing) {
1148
- fs3.unlinkSync(file);
1124
+ /**
1125
+ * Replenish the pool in background
1126
+ */
1127
+ async replenish() {
1128
+ if (this.replenishing || !this.initialized) {
1129
+ return;
1130
+ }
1131
+ const needed = this.config.size - this.queue.length;
1132
+ if (needed <= 0) {
1133
+ return;
1134
+ }
1135
+ this.replenishing = true;
1136
+ logger3.log(`Replenishing pool: creating ${needed} overlay(s)...`);
1137
+ try {
1138
+ const promises = [];
1139
+ for (let i = 0; i < needed; i++) {
1140
+ const filePath = path2.join(
1141
+ this.config.poolDir,
1142
+ this.generateFileName()
1143
+ );
1144
+ promises.push(
1145
+ this.config.createFile(filePath).then(() => {
1146
+ this.queue.push(filePath);
1147
+ })
1148
+ );
1149
+ }
1150
+ await Promise.all(promises);
1151
+ logger3.log(`Pool replenished: ${this.queue.length} available`);
1152
+ } catch (err) {
1153
+ logger3.error(
1154
+ `Replenish failed: ${err instanceof Error ? err.message : "Unknown"}`
1155
+ );
1156
+ } finally {
1157
+ this.replenishing = false;
1149
1158
  }
1150
1159
  }
1151
- poolState.initialized = true;
1152
- await replenishPool();
1153
- logger3.log("Overlay pool initialized");
1154
- }
1155
- async function acquireOverlay() {
1156
- const filePath = poolState.queue.shift();
1157
- if (filePath) {
1160
+ /**
1161
+ * Initialize the overlay pool
1162
+ */
1163
+ async init() {
1164
+ this.queue = [];
1158
1165
  logger3.log(
1159
- `Acquired overlay from pool (${poolState.queue.length} remaining)`
1166
+ `Initializing overlay pool (size=${this.config.size}, threshold=${this.config.replenishThreshold})...`
1160
1167
  );
1161
- if (poolState.config && poolState.queue.length < poolState.config.replenishThreshold) {
1162
- replenishPool().catch((err) => {
1163
- logger3.error(
1164
- `Background replenish failed: ${err instanceof Error ? err.message : "Unknown"}`
1165
- );
1166
- });
1168
+ await this.ensurePoolDir();
1169
+ const existing = this.scanPoolDir();
1170
+ if (existing.length > 0) {
1171
+ logger3.log(`Cleaning up ${existing.length} stale overlay(s)`);
1172
+ for (const file of existing) {
1173
+ fs3.unlinkSync(file);
1174
+ }
1167
1175
  }
1168
- return filePath;
1169
- }
1170
- logger3.log("Pool exhausted, creating overlay on-demand");
1171
- const newPath = path2.join(POOL_DIR, generateFileName());
1172
- await createOverlayFile(newPath);
1173
- return newPath;
1174
- }
1175
- function cleanupOverlayPool() {
1176
- if (!poolState.initialized) {
1177
- return;
1176
+ this.initialized = true;
1177
+ await this.replenish();
1178
+ logger3.log("Overlay pool initialized");
1178
1179
  }
1179
- logger3.log("Cleaning up overlay pool...");
1180
- for (const file of poolState.queue) {
1181
- try {
1182
- fs3.unlinkSync(file);
1183
- } catch (err) {
1184
- logger3.log(
1185
- `Failed to delete ${file}: ${err instanceof Error ? err.message : "Unknown"}`
1186
- );
1180
+ /**
1181
+ * Acquire an overlay file from the pool
1182
+ *
1183
+ * Returns the file path. Caller owns the file and must delete it when done.
1184
+ * Falls back to on-demand creation if pool is exhausted.
1185
+ */
1186
+ async acquire() {
1187
+ if (!this.initialized) {
1188
+ throw new Error("Overlay pool not initialized");
1189
+ }
1190
+ const filePath = this.queue.shift();
1191
+ if (filePath) {
1192
+ logger3.log(`Acquired overlay from pool (${this.queue.length} remaining)`);
1193
+ if (this.queue.length < this.config.replenishThreshold) {
1194
+ this.replenish().catch((err) => {
1195
+ logger3.error(
1196
+ `Background replenish failed: ${err instanceof Error ? err.message : "Unknown"}`
1197
+ );
1198
+ });
1199
+ }
1200
+ return filePath;
1187
1201
  }
1202
+ logger3.log("Pool exhausted, creating overlay on-demand");
1203
+ const newPath = path2.join(this.config.poolDir, this.generateFileName());
1204
+ await this.config.createFile(newPath);
1205
+ return newPath;
1188
1206
  }
1189
- poolState.queue = [];
1190
- for (const file of scanPoolDir()) {
1191
- try {
1192
- fs3.unlinkSync(file);
1193
- } catch (err) {
1194
- logger3.log(
1195
- `Failed to delete ${file}: ${err instanceof Error ? err.message : "Unknown"}`
1196
- );
1207
+ /**
1208
+ * Clean up the overlay pool
1209
+ */
1210
+ cleanup() {
1211
+ if (!this.initialized) {
1212
+ return;
1213
+ }
1214
+ logger3.log("Cleaning up overlay pool...");
1215
+ for (const file of this.queue) {
1216
+ try {
1217
+ fs3.unlinkSync(file);
1218
+ } catch (err) {
1219
+ logger3.log(
1220
+ `Failed to delete ${file}: ${err instanceof Error ? err.message : "Unknown"}`
1221
+ );
1222
+ }
1223
+ }
1224
+ this.queue = [];
1225
+ for (const file of this.scanPoolDir()) {
1226
+ try {
1227
+ fs3.unlinkSync(file);
1228
+ } catch (err) {
1229
+ logger3.log(
1230
+ `Failed to delete ${file}: ${err instanceof Error ? err.message : "Unknown"}`
1231
+ );
1232
+ }
1197
1233
  }
1234
+ this.initialized = false;
1235
+ this.replenishing = false;
1236
+ logger3.log("Overlay pool cleaned up");
1237
+ }
1238
+ };
1239
+ var overlayPool = null;
1240
+ async function initOverlayPool(config) {
1241
+ if (overlayPool) {
1242
+ overlayPool.cleanup();
1243
+ }
1244
+ overlayPool = new OverlayPool(config);
1245
+ await overlayPool.init();
1246
+ return overlayPool;
1247
+ }
1248
+ function acquireOverlay() {
1249
+ if (!overlayPool) {
1250
+ throw new Error(
1251
+ "Overlay pool not initialized. Call initOverlayPool() first."
1252
+ );
1253
+ }
1254
+ return overlayPool.acquire();
1255
+ }
1256
+ function cleanupOverlayPool() {
1257
+ if (overlayPool) {
1258
+ overlayPool.cleanup();
1259
+ overlayPool = null;
1198
1260
  }
1199
- poolState.initialized = false;
1200
- poolState.replenishing = false;
1201
- logger3.log("Overlay pool cleaned up");
1202
1261
  }
1203
1262
 
1204
1263
  // src/lib/firecracker/vm.ts
@@ -1212,7 +1271,7 @@ var FirecrackerVM = class {
1212
1271
  workDir;
1213
1272
  socketPath;
1214
1273
  vmOverlayPath = null;
1215
- // Set by acquireOverlay() during start
1274
+ // Set during start()
1216
1275
  vsockPath;
1217
1276
  // Vsock UDS path for host-guest communication
1218
1277
  constructor(config) {
@@ -9709,56 +9768,6 @@ var logger9 = createLogger("Executor");
9709
9768
  function getVmIdFromRunId(runId) {
9710
9769
  return runId.split("-")[0] || runId.substring(0, 8);
9711
9770
  }
9712
- var CURL_ERROR_MESSAGES = {
9713
- 6: "DNS resolution failed",
9714
- 7: "Connection refused",
9715
- 28: "Connection timeout",
9716
- 60: "TLS certificate error (proxy CA not trusted)",
9717
- 22: "HTTP error from server"
9718
- };
9719
- async function runPreflightCheck(guest, apiUrl, runId, sandboxToken, bypassSecret) {
9720
- const heartbeatUrl = `${apiUrl}/api/webhooks/agent/heartbeat`;
9721
- const bypassHeader = bypassSecret ? ` -H "x-vercel-protection-bypass: ${bypassSecret}"` : "";
9722
- const curlCmd = `curl -sf --connect-timeout 5 --max-time 10 "${heartbeatUrl}" -X POST -H "Content-Type: application/json" -H "Authorization: Bearer ${sandboxToken}"${bypassHeader} -d '{"runId":"${runId}"}'`;
9723
- const result = await guest.exec(curlCmd, 2e4);
9724
- if (result.exitCode === 0) {
9725
- return { success: true };
9726
- }
9727
- const errorDetail = CURL_ERROR_MESSAGES[result.exitCode] ?? `curl exit code ${result.exitCode}`;
9728
- const stderrInfo = result.stderr?.trim() ? ` (${result.stderr.trim()})` : "";
9729
- return {
9730
- success: false,
9731
- error: `Preflight check failed: ${errorDetail}${stderrInfo} - VM cannot reach VM0 API at ${apiUrl}`
9732
- };
9733
- }
9734
- async function reportPreflightFailure(apiUrl, runId, sandboxToken, error, bypassSecret) {
9735
- const completeUrl = `${apiUrl}/api/webhooks/agent/complete`;
9736
- const headers = {
9737
- "Content-Type": "application/json",
9738
- Authorization: `Bearer ${sandboxToken}`
9739
- };
9740
- if (bypassSecret) {
9741
- headers["x-vercel-protection-bypass"] = bypassSecret;
9742
- }
9743
- try {
9744
- const response = await fetch(completeUrl, {
9745
- method: "POST",
9746
- headers,
9747
- body: JSON.stringify({
9748
- runId,
9749
- exitCode: 1,
9750
- error
9751
- })
9752
- });
9753
- if (!response.ok) {
9754
- logger9.error(
9755
- `Failed to report preflight failure: HTTP ${response.status}`
9756
- );
9757
- }
9758
- } catch (err) {
9759
- logger9.error(`Failed to report preflight failure: ${err}`);
9760
- }
9761
- }
9762
9771
  async function executeJob(context, config, options = {}) {
9763
9772
  setSandboxContext({
9764
9773
  apiUrl: config.server.url,
@@ -9847,35 +9856,6 @@ async function executeJob(context, config, options = {}) {
9847
9856
  `Writing env JSON (${envJson.length} bytes) to ${ENV_JSON_PATH}`
9848
9857
  );
9849
9858
  await guest.writeFile(ENV_JSON_PATH, envJson);
9850
- if (!options.benchmarkMode) {
9851
- logger9.log(`Running preflight connectivity check...`);
9852
- const bypassSecret = process.env.VERCEL_AUTOMATION_BYPASS_SECRET;
9853
- const preflight = await withSandboxTiming(
9854
- "preflight_check",
9855
- () => runPreflightCheck(
9856
- guest,
9857
- config.server.url,
9858
- context.runId,
9859
- context.sandboxToken,
9860
- bypassSecret
9861
- )
9862
- );
9863
- if (!preflight.success) {
9864
- logger9.log(`Preflight check failed: ${preflight.error}`);
9865
- await reportPreflightFailure(
9866
- config.server.url,
9867
- context.runId,
9868
- context.sandboxToken,
9869
- preflight.error,
9870
- bypassSecret
9871
- );
9872
- return {
9873
- exitCode: 1,
9874
- error: preflight.error
9875
- };
9876
- }
9877
- logger9.log(`Preflight check passed`);
9878
- }
9879
9859
  const systemLogFile = `/tmp/vm0-main-${context.runId}.log`;
9880
9860
  const startTime = Date.now();
9881
9861
  const maxWaitMs = 2 * 60 * 60 * 1e3;
@@ -10094,7 +10074,6 @@ async function setupEnvironment(options) {
10094
10074
  await initOverlayPool({
10095
10075
  size: config.sandbox.max_concurrent + 2,
10096
10076
  replenishThreshold: config.sandbox.max_concurrent
10097
- // Start replenishing early to handle bursts
10098
10077
  });
10099
10078
  logger12.log("Initializing network proxy...");
10100
10079
  initVMRegistry();
@@ -10983,6 +10962,12 @@ var benchmarkCommand = new Command4("benchmark").description(
10983
10962
  }
10984
10963
  timer.log("Setting up network bridge...");
10985
10964
  await setupBridge();
10965
+ timer.log("Initializing overlay pool...");
10966
+ await initOverlayPool({
10967
+ size: 2,
10968
+ replenishThreshold: 1,
10969
+ poolDir: "/var/run/vm0/overlay-pool-benchmark"
10970
+ });
10986
10971
  timer.log(`Executing command: ${prompt}`);
10987
10972
  const context = createBenchmarkContext(prompt, options);
10988
10973
  const result = await executeJob(context, config, {
@@ -11003,7 +10988,7 @@ var benchmarkCommand = new Command4("benchmark").description(
11003
10988
  });
11004
10989
 
11005
10990
  // src/index.ts
11006
- var version = true ? "3.7.0" : "0.1.0";
10991
+ var version = true ? "3.7.2" : "0.1.0";
11007
10992
  program.name("vm0-runner").version(version).description("Self-hosted runner for VM0 agents");
11008
10993
  program.addCommand(startCommand);
11009
10994
  program.addCommand(doctorCommand);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vm0/runner",
3
- "version": "3.7.0",
3
+ "version": "3.7.2",
4
4
  "description": "Self-hosted runner for VM0 agents",
5
5
  "repository": {
6
6
  "type": "git",