Three corrections, two of them to things I wrote today. 026 is the serious one. Four modules mounted fourteen host paths nothing declared — the mail spool, the databases, the object store's data. The runtime creates those as root, so owner and mode go unapplied, and the rule that keeps a directory holding data the mesh did not put there is written in terms of declared directories. It reached the configuration and missed the data. The cause was carrying compose files across: a container shape that can express one gets filled in like one. 025 claimed nothing turns a tag into a digest. That is false, and the answer was designed and built before I wrote it. A module names an artifact, not an image, and `kind: upstream` mirrors somebody else's image into the mesh's own registry, pinned by the digest it lands with. The two-document split the issue described as the shape of a fix is the design. Pinning twelve images by hand was treating the symptom, and left them pointing at a public registry rather than the mesh's. And the image store was written up as something the mesh does. It is an ordinary module — considered for the substrate and removed, because the test is whether the control plane needs it before its first instruction, not whether it can grant itself one. So somebody's own registry is the same module as the mesh's.
122 lines
5.9 KiB
Markdown
122 lines
5.9 KiB
Markdown
---
|
|
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.
|