@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.
- package/dist/index.js +77 -12
- 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.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: "
|
|
1262
|
-
stderr: "
|
|
1269
|
+
stdout: "pipe",
|
|
1270
|
+
stderr: "pipe",
|
|
1263
1271
|
...service.env && { env: service.env },
|
|
1264
1272
|
signal: servicesAbort.signal
|
|
1265
1273
|
});
|
|
1266
|
-
|
|
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 (
|
|
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
|
-
})
|
|
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
|
|
1292
|
-
|
|
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 (
|
|
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 (${
|
|
1358
|
+
message: `Projected guest service "${service.id}" did not become ready (${failure}); continuing without it`
|
|
1301
1359
|
});
|
|
1302
1360
|
}
|
|
1303
1361
|
}
|
|
1304
|
-
|
|
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.
|
|
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",
|