File 009: a digest-pinned image cannot be placed in the lab

Two accepted decisions collide, and testing found it rather than review.

0046 pins images by digest and has the host refuse anything unpinned. The lab
places images by exporting them from the workstation, because a sealed scenario
cannot reach a registry -- and that loses the digest, since a repo digest only
exists for an image a registry served. Measured: the load says 'Loaded image
ID:' rather than 'Loaded image:', and the image lands dangling.

So a tag is refused by the host and a digest is unusable in the lab. There is
currently no declaration the lab can raise that exercises the container shape,
which matters because the container shape IS the substrate -- every bootstrap
step past the runtime is one.

The resolution is a registry inside the scenario, and that is not a workaround:
0048 already names an OCI registry as substrate and every node after the first
pulls from the mesh's own. It also removes the lab's export-and-push mechanism
rather than repairing it.

0046 now carries a pointer, since its own consequence is where the collision
was predicted -- half of it is closed and the other half turned out to be
harder than 'not solved here' suggested.
This commit is contained in:
2026-08-28 01:56:49 +02:00
parent 9dc57b4712
commit f728c3fd98
2 changed files with 94 additions and 0 deletions
@@ -60,6 +60,12 @@ would be the tail wagging the dog.
- **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. Both are lab-installation concerns and
neither is solved here.
**The runtime half is now done** — the lab builds a base image on a machine with a network and
raises sealed machines from it. **The image half turned out to collide with this record**: an
image placed from an archive cannot keep its digest, and this record has the host refuse
anything unpinned, so the lab can satisfy neither form. See
[04-ISSUES/009](../04-ISSUES/009-a-digest-pinned-image-cannot-be-placed-in-the-lab/00-report.md);
the resolution is a registry inside the scenario, which is what a real node pulls from anyway.
- **The build-time-versus-apply-time reframing in
[research 012](../01-RESEARCH/012-the-minimum-viable-node/00-overview.md) narrows.** It still
holds for what a *tailored installer* contains — the missing pieces for a given machine — but
@@ -0,0 +1,88 @@
---
status: open
opened: 2026-08-28
located-in: [mesh-lab, mesh-host]
fixed-by:
amended-design:
---
# 009 — A digest-pinned image cannot be placed in the lab, so `container` cannot be tested there
## Symptom
Two accepted decisions collide, and the collision makes one resource shape untestable.
- **[ADR 0046](../../02-DECISIONS/0046-the-installer-fetches-what-it-pins.md)** pins images by
digest, and the host **refuses** an image reference that is not pinned:
```
resource "store": image "alpine:3.20" is not pinned. Write it as name@sha256:...
```
- **The lab cannot place a digest-pinned image.** A sealed scenario cannot reach a registry, so
the lab exports an image from the workstation and loads it in the machine — and that loses the
digest.
So a `container` resource is refused by the host if it names a tag, and unusable if it names a
digest. **There is no declaration the lab can currently raise that exercises the shape.**
## What was measured
Not inferred. `docker save alpine@sha256:d9e8…` produces an archive with **no repo tag**, because
a repo digest exists only for an image a registry served. Loading it says:
```
Loaded image ID: sha256:63f227… (not "Loaded image: alpine:3.20")
```
and `docker images` then lists nothing — the image is there but dangling. A container declaring
that digest therefore falls through to the registry:
```
Unable to find image 'alpine@sha256:d9e8…' locally
dial tcp: lookup registry-1.docker.io: no such host
```
which is correct behaviour on a machine with no route out.
## What is not affected
Everything else placed in the same sealed machine works, and was verified there:
| shape | |
|---|---|
| `package` | applied, idempotent |
| `service` incl. `boot: enabled` | applied, read back as `enabled` |
| `action` | ran, verified |
| `container` | **blocked by this issue** |
## Why it matters more than one shape
The container shape is the substrate. Every step of raising a mesh past the container runtime is
a container ([`07-the-substrate.md`](../../03-DESIGN/01-to-be/07-the-substrate.md)), so the
bootstrap cannot be tested end-to-end until this is resolved — which is the thing the lab exists
for.
## The shape of a resolution
**A registry inside the scenario**, on its public segment, that machines pull from. That is not a
workaround: it is what the real mesh does — [ADR 0048](../../02-DECISIONS/0048-the-substrate-is-named.md)
names an OCI registry as substrate, and every node after the first pulls from the mesh's own.
Testing against a registry is testing the real path rather than a stand-in for it.
It also removes the lab's export-and-push mechanism rather than fixing it, which is the better
outcome: pushing image tarballs over the hypervisor was always a lab-only invention.
**Not decided here**, because it is design rather than repair: where the registry runs, whether
it is scenery like the router ([ADR 0033](../../02-DECISIONS/0033-a-router-is-scenery-not-a-node.md))
or a placed artifact, and how images get into it.
## Incidental, and already fixed
The lab's own check on the load was too weak: it matched `"Loaded image"`, which is a prefix of
both `Loaded image:` and `Loaded image ID:`. So a load that produced an unusable dangling image
**reported success**, and the failure surfaced later as a container that would not start. It now
matches `Loaded image:` exactly and says what the runtime actually said.
That is this repository's own subject arriving in its own tooling: a check that passes on the
wrong thing is worse than no check, because it moves the failure away from its cause.