@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.
- package/index.js +173 -188
- 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
|
|
1075
|
+
var DEFAULT_POOL_DIR = path2.join(VM0_RUN_DIR2, "overlay-pool");
|
|
1076
1076
|
var OVERLAY_SIZE = 2 * 1024 * 1024 * 1024;
|
|
1077
|
-
|
|
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
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
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
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
return
|
|
1096
|
+
/**
|
|
1097
|
+
* Generate unique file name using UUID
|
|
1098
|
+
*/
|
|
1099
|
+
generateFileName() {
|
|
1100
|
+
return `overlay-${randomUUID()}.ext4`;
|
|
1110
1101
|
}
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
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
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
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
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
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
|
-
`
|
|
1166
|
+
`Initializing overlay pool (size=${this.config.size}, threshold=${this.config.replenishThreshold})...`
|
|
1160
1167
|
);
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
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
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
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
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
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
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
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
|
|
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.
|
|
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);
|