@themoltnet/sandbox-gondolin 0.5.1 → 0.5.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/dist/index.js +66 -11
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -249,7 +249,7 @@ var GONDOLIN_BASE_EXECUTABLES = Object.freeze([
|
|
|
249
249
|
/** gh CLI version installed in every snapshot. */
|
|
250
250
|
var GH_VERSION = "2.74.0";
|
|
251
251
|
/** MoltNet CLI version — downloaded as a binary, no Node needed. */
|
|
252
|
-
var MOLTNET_CLI_VERSION = "
|
|
252
|
+
var MOLTNET_CLI_VERSION = "3.5.0";
|
|
253
253
|
/**
|
|
254
254
|
* Resolve guest architecture from host (Gondolin VMs match host arch).
|
|
255
255
|
*
|
|
@@ -410,6 +410,7 @@ async function buildSnapshot(vm, config, log) {
|
|
|
410
410
|
mv /tmp/package/bin/moltnet /usr/local/bin/moltnet
|
|
411
411
|
chmod +x /usr/local/bin/moltnet
|
|
412
412
|
rm -rf /tmp/moltnet.tgz /tmp/package
|
|
413
|
+
moltnet capability serve --help >/dev/null
|
|
413
414
|
'`);
|
|
414
415
|
await run(vm, log, "creating agent user...", `sh -eu -c '
|
|
415
416
|
addgroup -g 501 agent 2>/dev/null || true
|
|
@@ -562,6 +563,9 @@ var GUEST_TASK_CONTEXT_MOUNT = "/moltnet-task-context";
|
|
|
562
563
|
/** Guest directory holding one PID file per projected service. */
|
|
563
564
|
var GUEST_SERVICE_PID_DIR = "/run/moltnet/services";
|
|
564
565
|
var GUEST_SERVICE_ID_RE = /^[a-z][a-z0-9-]{0,62}$/;
|
|
566
|
+
function stderrTail(previous, next) {
|
|
567
|
+
return Buffer.concat([Buffer.from(previous), Buffer.from(next)]).subarray(-4096).toString("utf8");
|
|
568
|
+
}
|
|
565
569
|
function assertGuestServiceId(id) {
|
|
566
570
|
if (!GUEST_SERVICE_ID_RE.test(id)) throw new Error(`Invalid guest service id "${id}": expected ${GUEST_SERVICE_ID_RE}`);
|
|
567
571
|
}
|
|
@@ -1184,6 +1188,8 @@ async function resumeVm(config) {
|
|
|
1184
1188
|
const servicesAbort = new AbortController();
|
|
1185
1189
|
const serviceHandles = [];
|
|
1186
1190
|
const serviceIds = [];
|
|
1191
|
+
const serviceExits = /* @__PURE__ */ new Map();
|
|
1192
|
+
const serviceExitResults = /* @__PURE__ */ new Map();
|
|
1187
1193
|
const services = { async stop() {
|
|
1188
1194
|
if (serviceIds.length > 0) {
|
|
1189
1195
|
const pidFiles = serviceIds.map((id) => `${GUEST_SERVICE_PID_DIR}/${id}.pid`).join(" ");
|
|
@@ -1259,21 +1265,48 @@ async function resumeVm(config) {
|
|
|
1259
1265
|
...service.command
|
|
1260
1266
|
], {
|
|
1261
1267
|
stdout: "ignore",
|
|
1262
|
-
stderr: "
|
|
1268
|
+
stderr: "pipe",
|
|
1263
1269
|
...service.env && { env: service.env },
|
|
1264
1270
|
signal: servicesAbort.signal
|
|
1265
1271
|
});
|
|
1266
|
-
|
|
1272
|
+
let stderr = "";
|
|
1273
|
+
const exited = (async () => {
|
|
1274
|
+
try {
|
|
1275
|
+
if ("output" in handle && typeof handle.output === "function") {
|
|
1276
|
+
for await (const chunk of handle.output()) if (chunk.stream === "stderr") stderr = stderrTail(stderr, chunk.data);
|
|
1277
|
+
}
|
|
1278
|
+
const result = await handle;
|
|
1279
|
+
stderr = stderrTail(stderr, String(result.stderr ?? ""));
|
|
1280
|
+
return {
|
|
1281
|
+
exitCode: result.exitCode,
|
|
1282
|
+
stderr
|
|
1283
|
+
};
|
|
1284
|
+
} catch (error) {
|
|
1285
|
+
return {
|
|
1286
|
+
exitCode: void 0,
|
|
1287
|
+
stderr: stderrTail(stderr, String(error))
|
|
1288
|
+
};
|
|
1289
|
+
}
|
|
1290
|
+
})();
|
|
1291
|
+
serviceExits.set(service.id, exited.then((result) => {
|
|
1292
|
+
serviceExitResults.set(service.id, result);
|
|
1293
|
+
return result;
|
|
1294
|
+
}));
|
|
1295
|
+
serviceHandles.push(exited);
|
|
1267
1296
|
serviceIds.push(service.id);
|
|
1268
1297
|
}
|
|
1269
1298
|
async function awaitServiceReady(service) {
|
|
1270
1299
|
if (!service.readiness) return;
|
|
1271
1300
|
const deadline = Date.now() + (service.readiness.timeoutMs ?? 1e4);
|
|
1272
1301
|
const pidFile = `${GUEST_SERVICE_PID_DIR}/${service.id}.pid`;
|
|
1302
|
+
const exited = serviceExits.get(service.id);
|
|
1303
|
+
if (!exited) throw new Error(`Service "${service.id}" was not started`);
|
|
1273
1304
|
let ready = false;
|
|
1305
|
+
let failure;
|
|
1274
1306
|
while (Date.now() < deadline) {
|
|
1275
1307
|
throwIfAborted(config.signal, `service "${service.id}" readiness`);
|
|
1276
|
-
if (
|
|
1308
|
+
if (servicesAbort.signal.aborted) return;
|
|
1309
|
+
const probe = await vm.exec([
|
|
1277
1310
|
"sh",
|
|
1278
1311
|
"-c",
|
|
1279
1312
|
"[ -e \"$1\" ] && [ -r \"$2\" ] && kill -0 \"$(cat \"$2\" 2>/dev/null)\" 2>/dev/null",
|
|
@@ -1284,24 +1317,46 @@ async function resumeVm(config) {
|
|
|
1284
1317
|
stdout: "ignore",
|
|
1285
1318
|
stderr: "ignore",
|
|
1286
1319
|
signal: config.signal
|
|
1287
|
-
})
|
|
1320
|
+
});
|
|
1321
|
+
const alreadyExited = serviceExitResults.get(service.id);
|
|
1322
|
+
if (alreadyExited) {
|
|
1323
|
+
failure = `exited (code ${alreadyExited.exitCode ?? "unknown"})`;
|
|
1324
|
+
if (alreadyExited.stderr) failure += `; stderr tail: ${alreadyExited.stderr}`;
|
|
1325
|
+
break;
|
|
1326
|
+
}
|
|
1327
|
+
if (probe.exitCode === 0) {
|
|
1288
1328
|
ready = true;
|
|
1289
1329
|
break;
|
|
1290
1330
|
}
|
|
1291
|
-
await
|
|
1292
|
-
|
|
1293
|
-
|
|
1331
|
+
const outcome = await Promise.race([exited.then((result) => ({
|
|
1332
|
+
kind: "exit",
|
|
1333
|
+
result
|
|
1334
|
+
})), delay(Math.min(200, Math.max(0, deadline - Date.now())), servicesAbort.signal, `service "${service.id}" readiness`).then(() => ({ kind: "retry" }))]);
|
|
1335
|
+
if (outcome.kind === "exit") {
|
|
1336
|
+
failure = `exited (code ${outcome.result.exitCode ?? "unknown"})`;
|
|
1337
|
+
if (outcome.result.stderr) failure += `; stderr tail: ${outcome.result.stderr}`;
|
|
1338
|
+
break;
|
|
1339
|
+
}
|
|
1294
1340
|
}
|
|
1295
1341
|
if (!ready) {
|
|
1296
|
-
if (
|
|
1342
|
+
if (servicesAbort.signal.aborted) return;
|
|
1343
|
+
failure ??= `${service.readiness.path} absent after ${service.readiness.timeoutMs ?? 1e4} ms`;
|
|
1344
|
+
if (service.readiness.required) throw new Error(`Projected guest service "${service.id}" did not become ready: ${failure}`);
|
|
1297
1345
|
config.onDiagnostic?.({
|
|
1298
1346
|
event: "vm.guest_service.not_ready",
|
|
1299
1347
|
level: "warning",
|
|
1300
|
-
message: `Projected guest service "${service.id}" did not become ready (${
|
|
1348
|
+
message: `Projected guest service "${service.id}" did not become ready (${failure}); continuing without it`
|
|
1301
1349
|
});
|
|
1302
1350
|
}
|
|
1303
1351
|
}
|
|
1304
|
-
|
|
1352
|
+
for (const service of projectedServices.filter((item) => item.readiness && !item.readiness.required)) awaitServiceReady(service).catch((error) => {
|
|
1353
|
+
if (!servicesAbort.signal.aborted) config.onDiagnostic?.({
|
|
1354
|
+
event: "vm.guest_service.not_ready",
|
|
1355
|
+
level: "warning",
|
|
1356
|
+
message: `Projected guest service "${service.id}" readiness failed: ${String(error)}`
|
|
1357
|
+
});
|
|
1358
|
+
});
|
|
1359
|
+
await Promise.all(projectedServices.filter((service) => service.readiness?.required).map((service) => awaitServiceReady(service)));
|
|
1305
1360
|
if (projectedFiles.length > 0 || projectedServices.length > 0) config.onDiagnostic?.({
|
|
1306
1361
|
event: "vm.guest_projection.applied",
|
|
1307
1362
|
level: "info",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@themoltnet/sandbox-gondolin",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.2",
|
|
4
4
|
"description": "Gondolin microVM sandbox lifecycle for MoltNet runtimes: checkpoint resume, egress policy, VFS shadowing, guest credential boundary, host-brokered secrets",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"moltnet",
|