The lab can give a sealed machine a container runtime

ADR 0046's open consequence: "the lab needs a way to place images, and the
machine it places them into needs a container runtime, which a sealed scenario
cannot install either."

The runtime half is done, and it is research 012's reframing applied literally
-- fetch at build time on a machine with a network, apply on a target that
needs nothing. `mesh-lab base build` launches a machine WITH a network,
installs a runtime, verifies it by asking the runtime rather than the package
manager, and publishes the result. Measured: ~30s to install, ~60s to publish,
~700MiB, paid once per lab rather than per scenario.

A scenario that places `runtime` or an image is then raised from that base
image, chosen rather than declared -- a scenario says what it needs, not which
image provides it. If the base does not exist it says so and how to build it.

Verified in a genuinely sealed machine (no route out, confirmed by ping):
package, service including the new `boot: enabled`, and action all applied,
were idempotent on a second run, and read back correctly. Those three had never
run anywhere but a workstation.

The image half is NOT done, and testing found why: a digest-pinned image cannot
be placed from an archive. `docker save alpine@sha256:...` produces an archive
with no repo tag, because a repo digest only exists for an image a registry
served -- so it loads dangling and a container declaring that digest reaches
for a registry the machine cannot see.

That collides with ADR 0046, which has the host REFUSE an unpinned image. Tag
refused by the host, digest unusable in the lab: there is currently no
declaration the lab can raise that exercises the container shape at all. Filed
as 04-ISSUES/009, whose resolution is a registry inside the scenario -- which is
what the real mesh does rather than a workaround for the lab.

Also fixed a weak check of my own, which is the same fault in miniature: the
load was tested with `includes("Loaded image")`, a prefix of both `Loaded
image:` and `Loaded image ID:`. So an unusable dangling load reported success
and the failure surfaced later as a container that would not start.
This commit is contained in:
2026-08-28 01:56:32 +02:00
parent 78b6f058ff
commit d6eef25590
6 changed files with 379 additions and 15 deletions
+10
View File
@@ -91,6 +91,16 @@ async function main(): Promise<void> {
case "check":
return check();
case "base": {
// `base build` exists because a sealed scenario cannot install a container runtime, and
// the runtime has to come from somewhere with a network (novox/hq ADR 0046).
if (rest[0] !== "build") fail("base needs a subcommand: build");
const { buildBaseImage } = await import("./lifecycle/base.ts");
const built = await buildBaseImage((line) => console.log(line));
console.log(`${built.alias}: built, with docker ${built.runtime}`);
return;
}
case "validate": {
const path = rest[0] ?? fail("validate needs a scenario file");
const scenario = loadScenario(path);