@themoltnet/sandbox-gondolin 0.5.1 → 0.6.0

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 +77 -12
  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.11.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
  }
@@ -723,6 +727,7 @@ function isReservedGuestEnvironmentName(name) {
723
727
  return name.startsWith("MOLTNET_") || name.startsWith("GIT_CONFIG_") || RESERVED_GUEST_ENVIRONMENT_NAMES.has(name);
724
728
  }
725
729
  var GuestEnvironmentBoundaryError = class extends Error {
730
+ refusedNames;
726
731
  constructor(refusedNames) {
727
732
  super(`Guest credential boundary refuses runtime-controlled environment variables: ${refusedNames.join(", ")}. Remove them from the runtime profile; MoltNet operations use the trusted host-side Agent.`);
728
733
  this.refusedNames = refusedNames;
@@ -730,6 +735,7 @@ var GuestEnvironmentBoundaryError = class extends Error {
730
735
  }
731
736
  };
732
737
  var BrokeredHttpSecretBoundaryError = class extends Error {
738
+ issues;
733
739
  constructor(issues) {
734
740
  super("Brokered HTTP secret boundary refused the resolved bindings: " + issues.join("; "));
735
741
  this.issues = issues;
@@ -1184,6 +1190,8 @@ async function resumeVm(config) {
1184
1190
  const servicesAbort = new AbortController();
1185
1191
  const serviceHandles = [];
1186
1192
  const serviceIds = [];
1193
+ const serviceExits = /* @__PURE__ */ new Map();
1194
+ const serviceExitResults = /* @__PURE__ */ new Map();
1187
1195
  const services = { async stop() {
1188
1196
  if (serviceIds.length > 0) {
1189
1197
  const pidFiles = serviceIds.map((id) => `${GUEST_SERVICE_PID_DIR}/${id}.pid`).join(" ");
@@ -1258,22 +1266,57 @@ async function resumeVm(config) {
1258
1266
  "moltnet-service",
1259
1267
  ...service.command
1260
1268
  ], {
1261
- stdout: "ignore",
1262
- stderr: "ignore",
1269
+ stdout: "pipe",
1270
+ stderr: "pipe",
1263
1271
  ...service.env && { env: service.env },
1264
1272
  signal: servicesAbort.signal
1265
1273
  });
1266
- serviceHandles.push(Promise.resolve(handle).catch(() => void 0));
1274
+ let stderr = "";
1275
+ const completion = Promise.resolve(handle).then((result) => ({
1276
+ kind: "completed",
1277
+ result
1278
+ }), (error) => ({
1279
+ kind: "failed",
1280
+ error
1281
+ }));
1282
+ const exited = (async () => {
1283
+ try {
1284
+ if ("output" in handle && typeof handle.output === "function") {
1285
+ for await (const chunk of handle.output()) if (chunk.stream === "stderr") stderr = stderrTail(stderr, chunk.data);
1286
+ }
1287
+ } catch (error) {
1288
+ stderr = stderrTail(stderr, String(error));
1289
+ }
1290
+ const outcome = await completion;
1291
+ if (outcome.kind === "failed") return {
1292
+ exitCode: void 0,
1293
+ stderr: stderrTail(stderr, String(outcome.error))
1294
+ };
1295
+ stderr = stderrTail(stderr, String(outcome.result.stderr ?? ""));
1296
+ return {
1297
+ exitCode: outcome.result.exitCode,
1298
+ stderr
1299
+ };
1300
+ })();
1301
+ serviceExits.set(service.id, exited.then((result) => {
1302
+ serviceExitResults.set(service.id, result);
1303
+ return result;
1304
+ }));
1305
+ serviceHandles.push(exited);
1267
1306
  serviceIds.push(service.id);
1268
1307
  }
1269
1308
  async function awaitServiceReady(service) {
1270
1309
  if (!service.readiness) return;
1271
1310
  const deadline = Date.now() + (service.readiness.timeoutMs ?? 1e4);
1272
1311
  const pidFile = `${GUEST_SERVICE_PID_DIR}/${service.id}.pid`;
1312
+ const exited = serviceExits.get(service.id);
1313
+ if (!exited) throw new Error(`Service "${service.id}" was not started`);
1273
1314
  let ready = false;
1315
+ let failure;
1274
1316
  while (Date.now() < deadline) {
1275
1317
  throwIfAborted(config.signal, `service "${service.id}" readiness`);
1276
- if ((await vm.exec([
1318
+ if (servicesAbort.signal.aborted) return;
1319
+ const probe = await vm.exec([
1277
1320
  "sh",
1278
1321
  "-c",
1279
1322
  "[ -e \"$1\" ] && [ -r \"$2\" ] && kill -0 \"$(cat \"$2\" 2>/dev/null)\" 2>/dev/null",
@@ -1284,24 +1327,46 @@ async function resumeVm(config) {
1284
1327
  stdout: "ignore",
1285
1328
  stderr: "ignore",
1286
1329
  signal: config.signal
1287
- })).exitCode === 0) {
1330
+ });
1331
+ const alreadyExited = serviceExitResults.get(service.id);
1332
+ if (alreadyExited) {
1333
+ failure = `exited (code ${alreadyExited.exitCode ?? "unknown"})`;
1334
+ if (alreadyExited.stderr) failure += `; stderr tail: ${alreadyExited.stderr}`;
1335
+ break;
1336
+ }
1337
+ if (probe.exitCode === 0) {
1288
1338
  ready = true;
1289
1339
  break;
1290
1340
  }
1291
- await new Promise((resolve) => {
1292
- setTimeout(resolve, 200);
1293
- });
1341
+ const outcome = await Promise.race([exited.then((result) => ({
1342
+ kind: "exit",
1343
+ result
1344
+ })), delay(Math.min(200, Math.max(0, deadline - Date.now())), servicesAbort.signal, `service "${service.id}" readiness`).then(() => ({ kind: "retry" }))]);
1345
+ if (outcome.kind === "exit") {
1346
+ failure = `exited (code ${outcome.result.exitCode ?? "unknown"})`;
1347
+ if (outcome.result.stderr) failure += `; stderr tail: ${outcome.result.stderr}`;
1348
+ break;
1349
+ }
1294
1350
  }
1295
1351
  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`);
1352
+ if (servicesAbort.signal.aborted) return;
1353
+ failure ??= `${service.readiness.path} absent after ${service.readiness.timeoutMs ?? 1e4} ms`;
1354
+ if (service.readiness.required) throw new Error(`Projected guest service "${service.id}" did not become ready: ${failure}`);
1297
1355
  config.onDiagnostic?.({
1298
1356
  event: "vm.guest_service.not_ready",
1299
1357
  level: "warning",
1300
- message: `Projected guest service "${service.id}" did not become ready (${service.readiness.path} absent or its process exited); continuing without it`
1358
+ message: `Projected guest service "${service.id}" did not become ready (${failure}); continuing without it`
1301
1359
  });
1302
1360
  }
1303
1361
  }
1304
- await Promise.all(projectedServices.map((service) => awaitServiceReady(service)));
1362
+ for (const service of projectedServices.filter((item) => item.readiness && !item.readiness.required)) awaitServiceReady(service).catch((error) => {
1363
+ if (!servicesAbort.signal.aborted) config.onDiagnostic?.({
1364
+ event: "vm.guest_service.not_ready",
1365
+ level: "warning",
1366
+ message: `Projected guest service "${service.id}" readiness failed: ${String(error)}`
1367
+ });
1368
+ });
1369
+ await Promise.all(projectedServices.filter((service) => service.readiness?.required).map((service) => awaitServiceReady(service)));
1305
1370
  if (projectedFiles.length > 0 || projectedServices.length > 0) config.onDiagnostic?.({
1306
1371
  event: "vm.guest_projection.applied",
1307
1372
  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.6.0",
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",