025 — half done: nothing unrunnable reaches a machine now

The refusal landed, and the examples pin images that exist. What is
still missing is the part that makes it unnecessary: nothing in the mesh
turns a tag into a digest, so it was done by hand — which is precisely
what the issue says a person should not be asked to do.

Recorded because the resolution mechanism turned out to be trivial:
asking a registry what a tag points at takes about a second and pulls
nothing. That removes the main argument for leaving this open.

Also records the two faults that fell out of pinning for real. The mail
system named seven repositories that do not exist, because it publishes
to a different registry than the manifest assumed, and one of the seven
had been renamed upstream. Nothing checking only the shape of a
reference could have found either.
This commit is contained in:
2026-09-01 15:13:52 +02:00
parent fc7717f6b1
commit 2bbdc52440
@@ -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.