--- topic: building it status: accepted date: 2026-09-21 deciders: jochen reconstructed: false extends: 02-DECISIONS/0096-an-upstream-image-is-copied-between-registries.md --- # 97. A vendor image is a declared build input, and a recipe fetches nothing undeclared ## Context Three modules could not be built by the mesh's builder because their recipes reached for what no manifest named: a public package, or a binary copied out of a public image ([issue 064](../04-ISSUES/064-a-mesh-build-cannot-fetch-a-modules-external-dependencies/00-report.md)). A module already names the bases it stands on — another module's artifact, by name — and the builder answers with what the mesh holds; a vendor's image had no such declaration, so a recipe named it directly and the build worked when the public registry answered, which is sometimes. ## Considered Options 1. **Let the build environment reach public registries.** Rejected: a build that fetches from somebody else's registry on its own is one the mesh cannot rebuild the same way twice. 2. **A vendor image is a base like any other**, declared under `build.on` with the argument the recipe reads it from, pinned by digest, copied into the mesh's registry before the build ([ADR 0096](0096-an-upstream-image-is-copied-between-registries.md)). Adopted. ## Decision A build's `on` entry is either a module's artifact or an image published elsewhere, pinned by digest, read from one build argument. Before the build the image is copied into the mesh's registry under the module's repository and the recipe is handed the copy; genesis, with no registry, pulls it into the first machine's store. A recipe whose `COPY --from` names a registry image the manifest did not declare is refused before the build, naming the image and the remedy; its own stages, declared arguments and `scratch` are not fetches. An unpinned vendor image is refused: a tag is what somebody else can move. A recipe whose `FROM` names an undeclared base was at first said, not refused: the mesh's own images — the control plane's, the tool runtime's, the route proxy's — started from a public base and declared none, and refusing those refuses genesis. *Amended the same day:* those three declare their bases now, and an undeclared `FROM` is refused like an undeclared copy. The builder's own image and the examples are built by `make`, not by the mesh, and take arguments with defaults. The package half of the issue is not decided here: the mesh's package registry already proxies the public one, and the failure the report saw has to be run again to be placed. ## Consequences A module's build inputs are all in its manifest, and every one of them is something the mesh holds a copy of. What got harder: a recipe that used to name a base image on its first line now names an argument, and the manifest names the image. ## How it is checked A builder test declares a pinned vendor image, asserts it is copied under the module's repository and handed to the recipe as the argument, and asserts an unpinned one is refused. A recipe test asserts an undeclared `FROM`, an undeclared `COPY --from` and an undeclared argument are named, and that stages, declared arguments and `scratch` are not. ## References - [issue 064](../04-ISSUES/064-a-mesh-build-cannot-fetch-a-modules-external-dependencies/00-report.md) - [ADR 0096](0096-an-upstream-image-is-copied-between-registries.md) - [`03-DESIGN/01-to-be/18-building-a-module.md`](../03-DESIGN/01-to-be/18-building-a-module.md)