--- 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. ## The mechanism already existed, and this issue was wrong about that **Corrected 2026-09-01, the same day.** This was filed saying nothing turns a tag into a digest. That is false, and the answer had been designed and built before any of it was written. A module does not name an image at all. It names an **artifact**, and declares where that artifact comes from: ``` build.artifacts: [{ name: "gitea", kind: "upstream", from: "gitea/gitea:1.22" }] resources: [{ id: "server", type: "container", artifact: "gitea", … }] ``` `kind: upstream` means *an image somebody else built, mirrored into the mesh's own registry and pinned by the digest it lands with*. The builder produces it; the manifest the mesh holds is derived, with `artifact` replaced by the real reference and the key removed, because the host has never heard of that word. A resource naming an artifact nothing produced is refused. So the two-document split this issue described as the shape of a fix **is the design**, and it covers both cases it said were unsolved: an image the mesh builds, and an image somebody else built. Mirroring also removes something worse than a stale pin — every machine needing a route to a public registry, and a tag a stranger can move. **What was actually wrong was the examples.** They hard-coded image references instead of naming artifacts, so they inherited a problem the design does not have. Pinning twelve of them by hand was treating the symptom, and left the reference pointing at a public registry rather than the mesh's own. ## What is still open **The examples should name upstream artifacts** rather than carry hand-pinned digests. That is the remaining work, and it is a rewrite of five manifests rather than a mechanism to build. The refusal added here stays: a placeholder must not reach a machine whatever the reason it is there.