@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.
Files changed (2) hide show
  1. package/dist/index.js +66 -11
  2. 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 = "1.37.0";
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: "ignore",
1268
+ stderr: "pipe",
1263
1269
  ...service.env && { env: service.env },
1264
1270
  signal: servicesAbort.signal
1265
1271
  });
1266
- serviceHandles.push(Promise.resolve(handle).catch(() => void 0));
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 ((await vm.exec([
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
- })).exitCode === 0) {
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 new Promise((resolve) => {
1292
- setTimeout(resolve, 200);
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 (service.readiness.required) throw new Error(`Projected guest service "${service.id}" did not become ready: ${service.readiness.path} absent or its process exited`);
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 (${service.readiness.path} absent or its process exited); continuing without it`
1348
+ message: `Projected guest service "${service.id}" did not become ready (${failure}); continuing without it`
1301
1349
  });
1302
1350
  }
1303
1351
  }
1304
- await Promise.all(projectedServices.map((service) => awaitServiceReady(service)));
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.1",
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",