diff --git a/04-ISSUES/025-a-module-must-pin-a-digest-and-nothing-produces-one/00-report.md b/04-ISSUES/025-a-module-must-pin-a-digest-and-nothing-produces-one/00-report.md new file mode 100644 index 0000000..ecff3f1 --- /dev/null +++ b/04-ISSUES/025-a-module-must-pin-a-digest-and-nothing-produces-one/00-report.md @@ -0,0 +1,97 @@ +--- +status: located +opened: 2026-09-01 +located-in: [mesh-control, mesh-host] +fixed-by: partly — mesh-control ee3cc1b +amended-design: +--- + +# 025 — A module must pin a digest, and nothing produces one + +## Symptom + +Every image reference in every example module is **sixty-four zeros**: + +``` +gitea@sha256:0000000000000000000000000000000000000000000000000000000000000000 +``` + +Eighteen of them, across five modules. Each one parses, resolves, and composes into a declaration +a host accepts. None of them could ever start: the machine would reach `docker pull` and stop. + +This is why those modules are *written* and not *running*, and it was not visible from any check +because every check passes. + +## Why nothing caught it + +The host validates the **shape** of a reference and nothing else — that it is `name@sha256:` plus +sixty-four hexadecimal characters. Sixty-four zeros satisfies that exactly. + +That check is not wrong. A host cannot verify a digest exists without reaching a registry, and +reaching a registry is precisely what the design refuses to make it do +([ADR 0006](../../02-DECISIONS/0006-the-substrate-and-the-control-plane.md)). The host is the last +place that could catch this and the wrong place to try. + +## The actual gap + +**A manifest must carry a digest, and nothing in the system produces one.** + +- Images the mesh builds are fine: the bundle writes the digest down *after* building, which is + the whole reason the bundle exists in that shape. +- Images from anywhere else — a forge, a mail system, a database — have no path at all. Somebody + has to look up what `gitea:1.22` points at today and paste it in, and nothing re-checks it. + +So the design is coherent about *pinning* and silent about *where a pin comes from*. A person +writing a module is asked for something they cannot reasonably produce by hand, and given a +placeholder shape that passes every gate. + +## What a fix has to keep + +- **The host still refuses a tag.** A digest is what makes a declaration exact, and that must not + soften. The fix belongs where a module is added or built, not on the machine. +- **A person writes a tag; the mesh holds a digest.** A manifest in a repository naming + `gitea:1.22` is readable and reviewable; the mesh resolving that against a registry once, and + recording the answer, is what makes it exact. The module table already records this shape for + source repositories — where it came from, the branch followed, the commit read — and an image is + the same question asked of a registry. +- **Re-resolving is a decision, not a side effect.** A tag that moves must not silently change what + a machine runs. Whatever resolves it records both, so *this pin is behind its tag* is a question + the mesh can answer rather than something discovered on a restart. + +## Cheaply, now + +An all-zero digest is a placeholder and never a real image. Refusing it costs three lines and +would have caught all eighteen the day they were written. It does not fix the gap; it stops the +gap being invisible. + +## What this blocks + +Every module that names a third-party image, which is every module that is not the mesh itself. +The forge and the mail system are otherwise ready to run. + +## Half of it is done + +**A placeholder can no longer reach a machine.** The refusal sits where a declaration is composed, +not where a manifest is parsed — a file awaiting a pin is legitimate, and the design already says +so for artifacts the mesh builds. Composing is the last moment before a machine sees it. + +**The examples now pin images that exist.** Twelve third-party digests were resolved against their +registries without pulling anything, which is also the mechanism the rest of this issue needs: +`docker manifest inspect --verbose` answers *what does this tag point at* in about a second. + +Two faults came free, and both had been invisible for the same reason as the digests: the mail +system's seven images named repositories that **do not exist** — it publishes to a different +registry entirely — and one of the seven had been renamed upstream. Nothing that only checks the +shape of a reference could ever have found either. + +## What is still open + +**Nothing turns a tag into a digest as part of the mesh's own work.** It was done by hand here, +which is exactly what this issue says a person should not be asked to do. The shape of the answer +is unchanged: a person writes a tag, the mesh resolves it once and records both, and *this pin is +behind its tag* becomes a question the mesh can answer. + +**The mesh's own provisioner images still cannot be pinned in a repository**, because their digest +does not exist until they are built and pushed. That is the bundle's problem, and the bundle solves +it by writing the digest down after building. A module naming an image the mesh builds needs the +same treatment, and does not have it.