From acc9824949b2d78d99ff0b65cd29f22c7607fbdc Mon Sep 17 00:00:00 2001 From: jochen Date: Mon, 21 Sep 2026 20:29:29 +0200 Subject: [PATCH 1/5] ADR 0094: a module may hold several secrets from one provider; issue 069 resolved; design 24 amended --- ...-hold-several-secrets-from-one-provider.md | 63 +++++++++++++++++++ 02-DECISIONS/README.md | 1 + 03-DESIGN/01-to-be/24-the-secrets-vault.md | 9 ++- .../00-report.md | 6 +- .../01-diagnosis.md | 7 ++- 5 files changed, 78 insertions(+), 8 deletions(-) create mode 100644 02-DECISIONS/0094-a-module-may-hold-several-secrets-from-one-provider.md diff --git a/02-DECISIONS/0094-a-module-may-hold-several-secrets-from-one-provider.md b/02-DECISIONS/0094-a-module-may-hold-several-secrets-from-one-provider.md new file mode 100644 index 0000000..13b515e --- /dev/null +++ b/02-DECISIONS/0094-a-module-may-hold-several-secrets-from-one-provider.md @@ -0,0 +1,63 @@ +--- +topic: the tiers +status: accepted +date: 2026-09-21 +deciders: jochen +reconstructed: false +extends: 02-DECISIONS/0085-a-secret-is-a-provision.md +--- + +# 94. A module may hold several secrets from one provider, each a pair of its own + +## Context + +[ADR 0085](0085-a-secret-is-a-provision.md) makes a module's own secret a provision: the module +requires `secret` from the vault and reads the pair credential minted for that consumer↔vault +pair. A pair has one credential, a module requires a provision once, and so a module received +one value. Read against the catalogue, nine modules hold two or more secrets besides their +broker account; seven of them hold genuinely independent values with independent lifetimes — a +root certificate, its key and that key's password; an admin password beside an API token. None +derives from another, so "one value, derivation the module's business" answers nothing +([issue 069](../04-ISSUES/069-one-secret-provision-yields-one-value/00-report.md)). + +## Considered Options + +1. **One value per module; the module derives the rest.** Rejected: the values are independent. +2. **Require the provision several times.** Rejected: `requires` is a list of names, and a + requirement is matched by name everywhere. +3. **The `secrets` map names several files under local names, and each local name is a pair + credential of its own.** Adopted. + +## Decision + +A module's `secrets` entry for a requirement may be a path, as before, or an object of local +names to paths. Each local name is its own pair credential, keyed on it beside the provision, +the consumer node, the consumer module and the provider; its own file on the consumer, referred +to as `${secret:}`; its own holder at the provider, named the consumer's identity +with the local name after it; and rotated apart from the others. A local name may not be one of +the module's own secrets or something it requires, so what a placeholder means is never +ambiguous. The plain shape is unchanged, and every credential that exists is the one it was. + +The holder's suffix is not a login any backend checks — a secret is not a login — so the +identity limit that binds a database role or an access key does not apply to it. + +## Consequences + +The ten modules that could not move onto the vault can. What got harder: `rotate secret` for a +consumer rotates every local name it holds from that provider; rotating one of several is a +finer command than the mesh has, and waits for a case that needs it. + +## How it is checked + +Manifest tests read both shapes, write them back, and refuse a colliding or unusable local +name. A resolver test asserts two local names are two needs, two files with two credentials, +and two holders at the provider. An inventory test asserts two local names are two rows, that +rotating one leaves the other, and that the provider is told both. The vault bed installs a +consumer that keeps two secrets and asserts two values delivered, two holders in the vault's +ledger, and both rotated by one command. + +## References + +- [issue 069](../04-ISSUES/069-one-secret-provision-yields-one-value/00-report.md) +- [ADR 0085](0085-a-secret-is-a-provision.md), [ADR 0049](0049-a-consumers-identity-fits-the-tightest-backend.md) +- [`03-DESIGN/01-to-be/24-the-secrets-vault.md`](../03-DESIGN/01-to-be/24-the-secrets-vault.md) diff --git a/02-DECISIONS/README.md b/02-DECISIONS/README.md index 89583b4..0cbf5d5 100644 --- a/02-DECISIONS/README.md +++ b/02-DECISIONS/README.md @@ -113,6 +113,7 @@ python3 00-META/checks/index.py fail if stale - **0078** — [The store and the broker are ordinary modules](0078-the-store-and-broker-are-modules.md) - **0079** — [The foundation seats are named after their servers](0079-the-foundation-seats-are-named-after-their-servers.md) - **0092** — [An operator delivers a pair credential, and the mesh never replaces it](0092-an-operator-delivers-a-pair-credential.md) +- **0094** — [A module may hold several secrets from one provider, each a pair of its own](0094-a-module-may-hold-several-secrets-from-one-provider.md) ### What runs on them, and how it gets there diff --git a/03-DESIGN/01-to-be/24-the-secrets-vault.md b/03-DESIGN/01-to-be/24-the-secrets-vault.md index 98ec487..898abe5 100644 --- a/03-DESIGN/01-to-be/24-the-secrets-vault.md +++ b/03-DESIGN/01-to-be/24-the-secrets-vault.md @@ -4,6 +4,7 @@ status: implemented code: [mesh-catalog, mesh-controller, mesh-host] updated: 2026-09-21 decisions: + - 02-DECISIONS/0094-a-module-may-hold-several-secrets-from-one-provider.md - 02-DECISIONS/0092-an-operator-delivers-a-pair-credential.md - 02-DECISIONS/0085-a-secret-is-a-provision.md - 02-DECISIONS/0031-the-control-plane-authenticates-nobody.md @@ -48,8 +49,12 @@ A module that needs a secret for its own use requires a `secret` provision, exac a database from the store. The vault generates the value — or takes custody of one an operator delivered, through `secret accept … --provider`, which seals it to both ends and records the pair as accepted ([ADR 0092](../../02-DECISIONS/0092-an-operator-delivers-a-pair-credential.md)) — and -the credential belongs to the consumer↔vault pair. An accepted pair is the one exception to what -follows: the mesh cannot make its replacement, so it is neither remade when a key changes nor +the credential belongs to the consumer↔vault pair. A module that needs several values names them +as local names under its `secrets` entry, and each is a pair of its own — keyed on the local +name, delivered as its own file, held at the vault as its own holder +([ADR 0094](../../02-DECISIONS/0094-a-module-may-hold-several-secrets-from-one-provider.md)). +*How it is checked:* manifest, resolver and inventory tests on two local names; the vault bed's +two-secret consumer. An accepted pair is the one exception to what follows: the mesh cannot make its replacement, so it is neither remade when a key changes nor rotated; both are refused aloud, and accepting a new value is the rotation. *How it is checked:* an inventory test accepts, reads back unchanged, and asserts the two refusals name the remedy. Because it is an ordinary pair credential, **everything already built for pair credentials applies to it unchanged**: it rotates diff --git a/04-ISSUES/069-one-secret-provision-yields-one-value/00-report.md b/04-ISSUES/069-one-secret-provision-yields-one-value/00-report.md index 4ab4d54..18a06d6 100644 --- a/04-ISSUES/069-one-secret-provision-yields-one-value/00-report.md +++ b/04-ISSUES/069-one-secret-provision-yields-one-value/00-report.md @@ -1,9 +1,9 @@ --- -status: located +status: resolved opened: 2026-09-20 located-in: [mesh-controller internal/catalogue (requires/secrets), mesh-controller internal/inventory (secret key)] -fixed-by: -amended-design: +fixed-by: ADR 0094; mesh-controller feat/several-secrets (secrets: under local names, the pair keyed on the local name, migration 0027); proven by the vault bed +amended-design: 03-DESIGN/01-to-be/24-the-secrets-vault.md --- # One `secret` provision yields one value, and a module may need several diff --git a/04-ISSUES/069-one-secret-provision-yields-one-value/01-diagnosis.md b/04-ISSUES/069-one-secret-provision-yields-one-value/01-diagnosis.md index b82ce93..9688ff0 100644 --- a/04-ISSUES/069-one-secret-provision-yields-one-value/01-diagnosis.md +++ b/04-ISSUES/069-one-secret-provision-yields-one-value/01-diagnosis.md @@ -18,6 +18,7 @@ is a manifest-vocabulary decision, and it moves the pair key: the credential must be keyed on the local name, not the provision name, or the second pair overwrites the first. -**Located in:** the manifest's `requires`/`secrets` vocabulary (the catalogue parser) and the pair -credential's key (the controller's secret store). Not fixed here: the key change touches every -existing pair and belongs in a feature of its own, with a lab run against the vault bed. +**Located in:** the manifest's `secrets` vocabulary (the catalogue parser) and the pair +credential's key (the controller's secret store). Fixed as +[ADR 0094](../../02-DECISIONS/0094-a-module-may-hold-several-secrets-from-one-provider.md): the +key gains the local name, empty for every existing pair, so nothing that exists changed. -- 2.54.0 From 39a01b7f7e2334baacc36b5d622f2aa73b8f331a Mon Sep 17 00:00:00 2001 From: jochen Date: Mon, 21 Sep 2026 20:38:23 +0200 Subject: [PATCH 2/5] ADR 0095: the control plane is the way to ask a module; issue 049 resolved; design 19 amended --- ...ontrol-plane-is-the-way-to-ask-a-module.md | 60 +++++++++++++++++++ 02-DECISIONS/README.md | 1 + 03-DESIGN/01-to-be/19-the-module-protocol.md | 19 +++--- .../00-report.md | 8 +-- .../01-diagnosis.md | 5 +- 5 files changed, 79 insertions(+), 14 deletions(-) create mode 100644 02-DECISIONS/0095-the-control-plane-is-the-way-to-ask-a-module.md diff --git a/02-DECISIONS/0095-the-control-plane-is-the-way-to-ask-a-module.md b/02-DECISIONS/0095-the-control-plane-is-the-way-to-ask-a-module.md new file mode 100644 index 0000000..df3d292 --- /dev/null +++ b/02-DECISIONS/0095-the-control-plane-is-the-way-to-ask-a-module.md @@ -0,0 +1,60 @@ +--- +topic: the tiers +status: accepted +date: 2026-09-21 +deciders: jochen +reconstructed: false +extends: 02-DECISIONS/0047-a-module-runs-its-code-as-its-own-process-with-its-own-account.md +--- + +# 95. The control plane is the way to ask a module + +## Context + +A module serves tools over the broker under an account scoped to what it emits, consumes and +serves ([ADR 0047](0047-a-module-runs-its-code-as-its-own-process-with-its-own-account.md)). A +tool call is a request and a reply: the caller creates a reply queue and publishes to the serving +module's request key, and no module's scope grants either — nor should it, since a module that +only publishes events has no business declaring queues. So a module could serve tools and nothing +in the mesh could call them +([issue 049](../04-ISSUES/049-a-module-can-serve-tools-and-nothing-can-call-them/00-report.md)): +not an operator at a terminal, not an agent acting for one. + +## Considered Options + +1. **Calling is a grant**: a module declares it may be asked, and a consumer is issued an + account that may create a reply queue and publish to that module's request key. Deferred: a + module-to-module call is the only caller that resembles what the mesh mints today, and none + asks for one yet. +2. **The control plane is the way in.** Adopted. It holds a connection that may already, so a + person or an agent asks through it, and every question passes one process — which is where + an audit of who asked what belongs. + +## Decision + +`mesh-controller ask [json]` publishes the request on the tool exchange under +`.`, with a private reply queue bound under its own name, waits for the answer +whose correlation matches, and prints it as the module gave it. A tool that answered with an +error has answered: the answer is printed and the exit status says so. A module that never +answers is said to have not answered, with where to look. + +A module declares nothing about being asked: serving a tool is being askable through the control +plane. A module-to-module call, if one is wanted, is a grant like any other and a later decision. + +## Consequences + +Anything with the control plane in reach can ask any module anything it serves. What got +harder: nothing outside the control plane can, and the control plane's connection is one more +thing on the path of every question — a cost accepted for the audit it buys. + +## How it is checked + +A tools-only bed asks a served tool through the control plane and asserts an answer arrived — +an error, since the lab has no upstream and no token, which is an answer where a timeout would +not be. + +## References + +- [issue 049](../04-ISSUES/049-a-module-can-serve-tools-and-nothing-can-call-them/00-report.md) +- [ADR 0047](0047-a-module-runs-its-code-as-its-own-process-with-its-own-account.md) +- [`03-DESIGN/01-to-be/19-the-module-protocol.md`](../03-DESIGN/01-to-be/19-the-module-protocol.md) diff --git a/02-DECISIONS/README.md b/02-DECISIONS/README.md index 0cbf5d5..71b8182 100644 --- a/02-DECISIONS/README.md +++ b/02-DECISIONS/README.md @@ -114,6 +114,7 @@ python3 00-META/checks/index.py fail if stale - **0079** — [The foundation seats are named after their servers](0079-the-foundation-seats-are-named-after-their-servers.md) - **0092** — [An operator delivers a pair credential, and the mesh never replaces it](0092-an-operator-delivers-a-pair-credential.md) - **0094** — [A module may hold several secrets from one provider, each a pair of its own](0094-a-module-may-hold-several-secrets-from-one-provider.md) +- **0095** — [The control plane is the way to ask a module](0095-the-control-plane-is-the-way-to-ask-a-module.md) ### What runs on them, and how it gets there diff --git a/03-DESIGN/01-to-be/19-the-module-protocol.md b/03-DESIGN/01-to-be/19-the-module-protocol.md index 633d451..8f0775b 100644 --- a/03-DESIGN/01-to-be/19-the-module-protocol.md +++ b/03-DESIGN/01-to-be/19-the-module-protocol.md @@ -5,8 +5,9 @@ code: - mesh-sdk src - mesh-tools src/broker-amqp.ts - mesh-controller internal/link -updated: 2026-09-15 +updated: 2026-09-21 decisions: + - 02-DECISIONS/0095-the-control-plane-is-the-way-to-ask-a-module.md - 02-DECISIONS/0074-the-wire-is-specified-not-the-types.md - 02-DECISIONS/0039-what-the-sdk-holds-and-refuses.md - 02-DECISIONS/0043-a-module-broker-account-is-scoped-by-emits-and-consumes.md @@ -137,13 +138,15 @@ A module's tools are its operator-facing surface. into any queue on the broker. - A caller needs a **reply queue**, and that is what a module's scoped account may not declare ([issue 049](../../04-ISSUES/049-a-module-can-serve-tools-and-nothing-can-call-them/00-report.md)). - So a module may serve tools and may not call them, and nothing today issues an account to anything - that wants to ask. - -### Not yet true - -The caller's half has no account. Until that is settled, the only thing that can ask a module a -question is the foundation's bootstrap admin, which is not a protocol so much as a way in. + So a module may serve tools and may not call them. +- **The control plane is the way to ask** + ([ADR 0095](../../02-DECISIONS/0095-the-control-plane-is-the-way-to-ask-a-module.md)): + `ask [json]` publishes on `mesh.rpc` under `.` with a private reply + queue bound under its own name, and prints the answer as the module gave it. A module declares + nothing about being asked — serving a tool is being askable through the control plane. A + module-to-module call, if one is wanted, is a grant like any other and a later decision. + *How it is checked:* a tools-only bed asks a served tool through the control plane and asserts + an answer arrived, where a timeout would read differently. --- diff --git a/04-ISSUES/049-a-module-can-serve-tools-and-nothing-can-call-them/00-report.md b/04-ISSUES/049-a-module-can-serve-tools-and-nothing-can-call-them/00-report.md index aa0a36d..a7fc02f 100644 --- a/04-ISSUES/049-a-module-can-serve-tools-and-nothing-can-call-them/00-report.md +++ b/04-ISSUES/049-a-module-can-serve-tools-and-nothing-can-call-them/00-report.md @@ -1,9 +1,9 @@ --- -status: located +status: resolved opened: 2026-09-14 -located-in: [mesh-controller cmd/mesh-controller, mesh-tools (the request contract)] -fixed-by: -amended-design: +located-in: [mesh-controller cmd/mesh-controller (ask), mesh-controller internal/link] +fixed-by: ADR 0095; mesh-controller multiple-fixes (ask: the control plane publishes the request with a private reply queue and prints the answer); proven by the confluence bed +amended-design: 03-DESIGN/01-to-be/19-the-module-protocol.md --- # 049 — A module can serve tools, and nothing is allowed to call them diff --git a/04-ISSUES/049-a-module-can-serve-tools-and-nothing-can-call-them/01-diagnosis.md b/04-ISSUES/049-a-module-can-serve-tools-and-nothing-can-call-them/01-diagnosis.md index ab791e9..6e06eb4 100644 --- a/04-ISSUES/049-a-module-can-serve-tools-and-nothing-can-call-them/01-diagnosis.md +++ b/04-ISSUES/049-a-module-can-serve-tools-and-nothing-can-call-them/01-diagnosis.md @@ -14,5 +14,6 @@ like any other, and a later decision. **Located in:** the controller (a command speaking the tool request/reply over its own connection) -and the tool runtime's request contract. Not fixed here: it is a new command against a protocol -the runtime owns, and needs a lab run against a tools-only bed to be proven. +and the tool runtime's request contract. Fixed as +[ADR 0095](../../02-DECISIONS/0095-the-control-plane-is-the-way-to-ask-a-module.md): `ask`, +proven by the confluence bed asking a served tool and getting its answer. -- 2.54.0 From 8d981e21b11256dab77abbf3b53ff4a96aa31b37 Mon Sep 17 00:00:00 2001 From: jochen Date: Mon, 21 Sep 2026 20:43:07 +0200 Subject: [PATCH 3/5] ADR 0096: an upstream image is copied between registries; issue 046 resolved; design 18 amended --- ...ream-image-is-copied-between-registries.md | 63 +++++++++++++++++++ 02-DECISIONS/README.md | 1 + 03-DESIGN/01-to-be/18-building-a-module.md | 13 ++++ .../00-report.md | 6 +- .../01-diagnosis.md | 8 ++- 5 files changed, 85 insertions(+), 6 deletions(-) create mode 100644 02-DECISIONS/0096-an-upstream-image-is-copied-between-registries.md diff --git a/02-DECISIONS/0096-an-upstream-image-is-copied-between-registries.md b/02-DECISIONS/0096-an-upstream-image-is-copied-between-registries.md new file mode 100644 index 0000000..ac6a913 --- /dev/null +++ b/02-DECISIONS/0096-an-upstream-image-is-copied-between-registries.md @@ -0,0 +1,63 @@ +--- +topic: building it +status: accepted +date: 2026-09-21 +deciders: jochen +reconstructed: false +extends: 02-DECISIONS/0006-the-substrate-and-the-control-plane.md +--- + +# 96. An upstream image is copied between registries, never through a machine's image store + +## Context + +A module may declare that an artifact is an image published elsewhere, to be copied into the +mesh's own registry so machines fetch it by a digest this mesh assigned rather than by a name +somebody else controls. The builder pulled it into the build machine's image store and pushed it +under the mesh's name, and the push was refused: a published image is an index over several +architectures, the runtime's store keeps the index, and pushing one platform out of it fails +however the platform is asked for +([issue 046](../04-ISSUES/046-an-upstream-image-cannot-be-mirrored-into-the-mesh/00-report.md)). +Every variant of pull-then-push was tried and failed the same way. + +## Considered Options + +1. **Resolve the index to one platform and push that.** Tried, reverted: it did not make the + push work, and a workaround for a store's behaviour is a thing nobody removes later. +2. **Tooling that copies between registries**, installed on the build machine. Rejected: one + more thing the builder's image carries, for a protocol the builder already speaks for blobs. +3. **Copy over the registry API**, in the builder. Adopted. + +## Decision + +The builder copies an upstream image between registries and never through a machine's image +store: it reads the index and every manifest it names, moves each blob by digest into the mesh's +registry — skipping what is already there, since blobs are content-named — puts the manifests +and then the index under the module's repository, and pins the index's digest. Public images are +read with the anonymous bearer token the registry hands out on challenge, which is how the +public hub and the others the catalogue names serve them. The mesh mirrors the whole index, so +what a machine fetches is the image for its own architecture; that every machine on one mesh is +the same architecture is an assumption this mesh makes and had not written down until now. + +Genesis has no registry to copy into and keeps the pull: the image stays in the first machine's +store, named by its own id, as every artifact does before there is anywhere to publish. + +## Consequences + +An upstream artifact builds. What got harder: the builder now holds a registry client of its +own, some two hundred lines, where a runtime command used to do; and a private upstream that +demands a credential is refused, since the copy is anonymous by design. + +## How it is checked + +A test raises a fake upstream registry serving an index over two platforms behind a bearer +challenge, and a fake mesh registry that records what arrives: every blob of both platforms +arrives once, two manifests and the index are put under their digests, the reference returned +pins the index under the module's repository, and a second copy uploads nothing. A reference +test reads names the way a runtime does. + +## References + +- [issue 046](../04-ISSUES/046-an-upstream-image-cannot-be-mirrored-into-the-mesh/00-report.md) +- [ADR 0006](0006-the-substrate-and-the-control-plane.md) +- [`03-DESIGN/01-to-be/18-building-a-module.md`](../03-DESIGN/01-to-be/18-building-a-module.md) diff --git a/02-DECISIONS/README.md b/02-DECISIONS/README.md index 71b8182..c508e29 100644 --- a/02-DECISIONS/README.md +++ b/02-DECISIONS/README.md @@ -161,6 +161,7 @@ python3 00-META/checks/index.py fail if stale - **0076** — [The SDK is a published package, and the toolchain resolves it by version](0076-the-sdk-is-a-published-package.md) - **0082** — [The registry is reached by name, and the overlay is its security](0082-the-registry-is-reached-by-name-and-trusted-by-the-overlay.md) - **0086** — [A secret reaches a process as a file, and an exception is declared](0086-a-secret-reaches-a-process-as-a-file.md) +- **0096** — [An upstream image is copied between registries, never through a machine's image store](0096-an-upstream-image-is-copied-between-registries.md) ### How it is checked diff --git a/03-DESIGN/01-to-be/18-building-a-module.md b/03-DESIGN/01-to-be/18-building-a-module.md index 96e218a..37ec58e 100644 --- a/03-DESIGN/01-to-be/18-building-a-module.md +++ b/03-DESIGN/01-to-be/18-building-a-module.md @@ -7,6 +7,7 @@ code: - mesh-catalog modules/builder updated: 2026-09-21 decisions: + - 02-DECISIONS/0096-an-upstream-image-is-copied-between-registries.md - 02-DECISIONS/0091-a-mount-is-declared-three-ways.md - 02-DECISIONS/0087-a-seeded-file-is-created-once.md - 02-DECISIONS/0040-what-a-module-is.md @@ -202,6 +203,18 @@ remedies, and a test parses every manifest in the catalogue beside the checkout. | `image` | built from a Dockerfile — for software that needs a particular base | | `upstream` | somebody else's image, mirrored and pinned by a digest this mesh assigned | +**An upstream image is copied between registries, never through a machine's image store** +([ADR 0096](../../02-DECISIONS/0096-an-upstream-image-is-copied-between-registries.md)). A +published image is an index over several architectures, and a runtime's store refuses to push +one platform out of an index it pulled. The builder reads the index and every manifest it names +over the registry API, moves each blob by digest into the mesh's registry, puts the manifests and +then the index under the module's repository, and pins the index — the whole image, so what a +machine fetches is the one for its own architecture. Public images are read with the anonymous +token a registry hands out on challenge; a private upstream is refused. *How it is checked:* a +test copies an index over two platforms from a fake registry behind a bearer challenge into a fake +mesh registry and asserts every blob arrived once, the manifests and index under their digests, +and nothing uploaded on a second copy. + ### What it puts on a machine | resource | is | a module may | diff --git a/04-ISSUES/046-an-upstream-image-cannot-be-mirrored-into-the-mesh/00-report.md b/04-ISSUES/046-an-upstream-image-cannot-be-mirrored-into-the-mesh/00-report.md index b1ed533..bf02f6a 100644 --- a/04-ISSUES/046-an-upstream-image-cannot-be-mirrored-into-the-mesh/00-report.md +++ b/04-ISSUES/046-an-upstream-image-cannot-be-mirrored-into-the-mesh/00-report.md @@ -1,9 +1,9 @@ --- -status: located +status: resolved opened: 2026-09-14 located-in: [mesh-controller internal/builder (upstream artifacts)] -fixed-by: -amended-design: +fixed-by: ADR 0096; mesh-controller multiple-fixes (the builder copies the index and its manifests between registries over the registry API); proven by a test against a fake upstream serving an index over two platforms +amended-design: 03-DESIGN/01-to-be/18-building-a-module.md --- # 046 — An upstream image cannot be mirrored into the mesh's own registry diff --git a/04-ISSUES/046-an-upstream-image-cannot-be-mirrored-into-the-mesh/01-diagnosis.md b/04-ISSUES/046-an-upstream-image-cannot-be-mirrored-into-the-mesh/01-diagnosis.md index 0d4fdfd..5417d19 100644 --- a/04-ISSUES/046-an-upstream-image-cannot-be-mirrored-into-the-mesh/01-diagnosis.md +++ b/04-ISSUES/046-an-upstream-image-cannot-be-mirrored-into-the-mesh/01-diagnosis.md @@ -12,6 +12,8 @@ than a limitation; today every machine on one mesh is the same architecture, and that assumption is now written down here rather than nowhere. -**Located in:** the builder's upstream-artifact step. Not fixed here: a registry-to-registry copy -is a few hundred lines against the registry API and is proven only against a real registry -serving a real index, which is a lab run of its own. +**Located in:** the builder's upstream-artifact step. Fixed as +[ADR 0096](../../02-DECISIONS/0096-an-upstream-image-is-copied-between-registries.md): a copy over +the registry API, proven against a fake upstream serving an index over two platforms behind a +bearer challenge. A run against the public hub from a mesh's builder is the remaining proof, and +the first build of a module with an upstream artifact will be it. -- 2.54.0 From 71072e240dbd46cfc04790a51cae4000a18b09d9 Mon Sep 17 00:00:00 2001 From: jochen Date: Mon, 21 Sep 2026 20:45:36 +0200 Subject: [PATCH 4/5] ADR 0097: a vendor image is a declared build input; issue 064's image half decided; design 18 amended --- ...-vendor-image-is-a-declared-build-input.md | 59 +++++++++++++++++++ 02-DECISIONS/README.md | 1 + 03-DESIGN/01-to-be/18-building-a-module.md | 9 +++ .../01-diagnosis.md | 7 ++- 4 files changed, 74 insertions(+), 2 deletions(-) create mode 100644 02-DECISIONS/0097-a-vendor-image-is-a-declared-build-input.md diff --git a/02-DECISIONS/0097-a-vendor-image-is-a-declared-build-input.md b/02-DECISIONS/0097-a-vendor-image-is-a-declared-build-input.md new file mode 100644 index 0000000..3763486 --- /dev/null +++ b/02-DECISIONS/0097-a-vendor-image-is-a-declared-build-input.md @@ -0,0 +1,59 @@ +--- +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 `FROM` or `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. + +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) diff --git a/02-DECISIONS/README.md b/02-DECISIONS/README.md index c508e29..3b098db 100644 --- a/02-DECISIONS/README.md +++ b/02-DECISIONS/README.md @@ -162,6 +162,7 @@ python3 00-META/checks/index.py fail if stale - **0082** — [The registry is reached by name, and the overlay is its security](0082-the-registry-is-reached-by-name-and-trusted-by-the-overlay.md) - **0086** — [A secret reaches a process as a file, and an exception is declared](0086-a-secret-reaches-a-process-as-a-file.md) - **0096** — [An upstream image is copied between registries, never through a machine's image store](0096-an-upstream-image-is-copied-between-registries.md) +- **0097** — [A vendor image is a declared build input, and a recipe fetches nothing undeclared](0097-a-vendor-image-is-a-declared-build-input.md) ### How it is checked diff --git a/03-DESIGN/01-to-be/18-building-a-module.md b/03-DESIGN/01-to-be/18-building-a-module.md index 37ec58e..d28567d 100644 --- a/03-DESIGN/01-to-be/18-building-a-module.md +++ b/03-DESIGN/01-to-be/18-building-a-module.md @@ -7,6 +7,7 @@ code: - mesh-catalog modules/builder updated: 2026-09-21 decisions: + - 02-DECISIONS/0097-a-vendor-image-is-a-declared-build-input.md - 02-DECISIONS/0096-an-upstream-image-is-copied-between-registries.md - 02-DECISIONS/0091-a-mount-is-declared-three-ways.md - 02-DECISIONS/0087-a-seeded-file-is-created-once.md @@ -215,6 +216,14 @@ test copies an index over two platforms from a fake registry behind a bearer cha mesh registry and asserts every blob arrived once, the manifests and index under their digests, and nothing uploaded on a second copy. +**A vendor image is a declared build input** +([ADR 0097](../../02-DECISIONS/0097-a-vendor-image-is-a-declared-build-input.md)). A build's `on` +entry is a module's artifact or an image published elsewhere, pinned by digest, read from one +build argument; the image is copied into the mesh's registry before the build and the recipe is +handed the copy. A recipe whose `FROM` or `COPY --from` names a registry image the manifest did not +declare is refused before the build, naming it and the remedy. *How it is checked:* builder tests +on a declared and an unpinned vendor image, and a recipe test on what counts as a fetch. + ### What it puts on a machine | resource | is | a module may | diff --git a/04-ISSUES/064-a-mesh-build-cannot-fetch-a-modules-external-dependencies/01-diagnosis.md b/04-ISSUES/064-a-mesh-build-cannot-fetch-a-modules-external-dependencies/01-diagnosis.md index 9f97344..8acce39 100644 --- a/04-ISSUES/064-a-mesh-build-cannot-fetch-a-modules-external-dependencies/01-diagnosis.md +++ b/04-ISSUES/064-a-mesh-build-cannot-fetch-a-modules-external-dependencies/01-diagnosis.md @@ -16,5 +16,8 @@ repositories succeed in the same Dockerfiles. **Located in:** the builder (what it tells npm and the runtime) and the catalogue (three modules -whose Dockerfiles fetch what no manifest names). Still open: a build must be re-run to place the -404, and a decision is needed on whether a vendor image is declared as a build input. +whose Dockerfiles fetch what no manifest names). The image half is decided and built: +[ADR 0097](../../02-DECISIONS/0097-a-vendor-image-is-a-declared-build-input.md) — a vendor image +is declared under `build.on`, copied in, and a recipe fetching what is undeclared is refused. Still +open: the package half — a build must be re-run against the mesh's proxying registry to place the +404 — and the three recipes themselves, which now declare their images or are refused. -- 2.54.0 From 43ca9ce0ae259d180101af1a42ef671190ff5024 Mon Sep 17 00:00:00 2001 From: jochen Date: Mon, 21 Sep 2026 20:48:19 +0200 Subject: [PATCH 5/5] ADR 0097: an undeclared base is said, not yet refused --- ...0097-a-vendor-image-is-a-declared-build-input.md | 13 +++++++++---- 03-DESIGN/01-to-be/18-building-a-module.md | 8 +++++--- 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/02-DECISIONS/0097-a-vendor-image-is-a-declared-build-input.md b/02-DECISIONS/0097-a-vendor-image-is-a-declared-build-input.md index 3763486..7b0131c 100644 --- a/02-DECISIONS/0097-a-vendor-image-is-a-declared-build-input.md +++ b/02-DECISIONS/0097-a-vendor-image-is-a-declared-build-input.md @@ -31,10 +31,15 @@ named it directly and the build worked when the public registry answered, which 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 `FROM` or `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. +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 is **said, not yet refused**: the mesh's own images +— the control plane's, the builder's, the tool runtime's — start from a public base and declare +none, and refusing those refuses genesis. They declare their bases next; until then every build +names the undeclared base and the remedy. 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. diff --git a/03-DESIGN/01-to-be/18-building-a-module.md b/03-DESIGN/01-to-be/18-building-a-module.md index d28567d..752e14c 100644 --- a/03-DESIGN/01-to-be/18-building-a-module.md +++ b/03-DESIGN/01-to-be/18-building-a-module.md @@ -220,9 +220,11 @@ and nothing uploaded on a second copy. ([ADR 0097](../../02-DECISIONS/0097-a-vendor-image-is-a-declared-build-input.md)). A build's `on` entry is a module's artifact or an image published elsewhere, pinned by digest, read from one build argument; the image is copied into the mesh's registry before the build and the recipe is -handed the copy. A recipe whose `FROM` or `COPY --from` names a registry image the manifest did not -declare is refused before the build, naming it and the remedy. *How it is checked:* builder tests -on a declared and an unpinned vendor image, and a recipe test on what counts as a fetch. +handed the copy. A recipe whose `COPY --from` names a registry image the manifest did not declare +is refused before the build, naming it and the remedy; an undeclared `FROM` is said, not yet +refused, because the mesh's own images start from a public base and declare none. *How it is +checked:* builder tests on a declared and an unpinned vendor image, and a recipe test on what +counts as a copy and what as a base. ### What it puts on a machine -- 2.54.0