From 199c2f20bbba320dd3efbeeadb171188472bacd1 Mon Sep 17 00:00:00 2001 From: jochen Date: Tue, 15 Sep 2026 18:06:03 +0200 Subject: [PATCH] =?UTF-8?q?ADR=200075=20=E2=80=94=20an=20artifact=20store?= =?UTF-8?q?=20is=20a=20provision,=20and=20a=20package=20registry=20is=20an?= =?UTF-8?q?other?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two questions circling turned out to be one asked twice: should gitea be the mesh's registry, and where does the SDK come from. The framing that dissolves both is that artifact-store is already a provision and the registry already provides it — so this was never about replacing a component. It is a second provider of an existing provision, which this mesh has a mechanism for and uses for certificate authorities already. So: two provisions, because they are two jobs. artifact-store is content addressed, pinned by digest, no versions and no ranges — what the mesh delivers to machines. package-registry is an ecosystem's own, addressed by name and version — what code resolves when compiled. Conflating them is how a mesh that pins everything ends up rebuilding one commit into two different things. The small registry stays the provider genesis installs, not because it is better but because of what it is: a directory and one container, installable where there is no database and no control plane. Gitea needs both, and the pivot needs somewhere to publish before either exists. Gitea also provides artifact-store, so a mesh may choose it — and choosing it answers issues 042 and 048 by adopting something that already has accounts and TLS, rather than reimplementing them in a registry that has neither. Moving between providers is a designed act with a verification step that is easy to skip and is the only thing between it and a mesh that cannot restart its own control plane. And the bootstrap still has no package registry when the first build needs one. Named rather than solved, so the next person does not discover it. Claude-Session: https://claude.ai/code/session_01D6qtiYU3P9jk3pnAXyAFyx --- ...0075-two-stores-and-which-provides-what.md | 116 ++++++++++++++++++ 02-DECISIONS/README.md | 1 + 2 files changed, 117 insertions(+) create mode 100644 02-DECISIONS/0075-two-stores-and-which-provides-what.md diff --git a/02-DECISIONS/0075-two-stores-and-which-provides-what.md b/02-DECISIONS/0075-two-stores-and-which-provides-what.md new file mode 100644 index 0000000..af6a607 --- /dev/null +++ b/02-DECISIONS/0075-two-stores-and-which-provides-what.md @@ -0,0 +1,116 @@ +--- +topic: the tiers +status: proposed +date: 2026-09-15 +deciders: jochen +reconstructed: false +extends: 0014-no-npm-workspace.md +--- + +# 75. An artifact store is a provision; a package registry is a different one + +## Context + +Two questions have been circling, and they turn out to be one question asked twice. + +**"Should gitea be the mesh's registry?"** It serves OCI images and a dozen package ecosystems, it +is already needed — genesis clones from one — and the mesh's own registry has neither +authentication ([issue 042](../04-ISSUES/042-nothing-gives-a-node-an-account-for-a-registry/00-report.md)) +nor a transport a runtime will accept over a network +([issue 048](../04-ISSUES/048-nothing-makes-a-machine-trust-the-mesh-registry/00-report.md)). Gitea +has both. + +**"Where does the SDK come from?"** [ADR 0014](0014-no-npm-workspace.md) already answers it — each +module consumes its dependencies, the mesh's own shared library included, *from the private +registry* — and nothing installs one, so today it comes from a git URL, which is +[issue 053](../04-ISSUES/053-the-sdk-is-pinned-twice-and-the-two-disagree/00-report.md). + +**The framing that dissolves both:** `artifact-store` is already a provision, and `registry` +already provides it. So "should gitea be the registry" is not a question about replacing a +component. It is a question about **a second provider of an existing provision** — which this mesh +has a mechanism for, and uses for certificate authorities and VPNs already. + +## Decision + +**Two provisions, because they are two jobs.** + +| provision | is | for | +|---|---|---| +| `artifact-store` | content-addressed blobs, pinned by digest, no versions, no ranges | what the **mesh** delivers to **machines** | +| `package-registry` | an ecosystem's own registry — npm, cargo, PyPI, Go | what **code** resolves when it is compiled | + +They are not the same store with different clients. One is addressed by digest and immutable by +construction; the other is addressed by name and version, and resolves ranges. Conflating them is +how a mesh that pins everything ends up rebuilding one commit into two different things. + +**`registry` remains the provider genesis installs.** Not because it is better, but because of what +it is: a directory and one container, no database, no control plane, installable at step 8 of an +install where neither exists yet. Gitea needs a store and provisioning, which means a control plane, +which means the pivot has already happened — and the pivot needs somewhere to publish to. + +**Gitea also provides `artifact-store`, and a mesh may choose it.** Two providers of one provision +is a thing the mesh understands: it refuses, names both, and choosing is assigning the one you want. +A mesh that assigns gitea gets authentication and TLS for its artifacts — which is to say, **issues +042 and 048 are answered by choosing a provider that already solved them**, rather than by +reimplementing accounts and certificates in a registry that has none. + +**Gitea provides `package-registry`.** That is ADR 0014's private registry, and it is one service +rather than one per ecosystem. `verdaccio` may provide it too, for npm alone, and is then a choice +somebody makes rather than the answer. + +**The registry is not removed at the end of installing.** A mesh that never runs gitea still has an +artifact store. Retiring it is a migration a mesh performs, not a step an installation ends with. + +## Why not simply gitea, from the start + +Because genesis would need a control plane before the thing that stores the control plane's image, +and that is circular rather than merely awkward. It would also make one of the three things the +build loop cannot produce for itself into a stateful application with a database — the pivot is the +hardest part of this design already. + +And it puts every artifact in the service that is also the trust anchor for everything the mesh will +ever run ([ADR 0071](0071-where-genesis-gets-its-source.md)), which records that forge serving a +cryptominer with tampered git operations. Two blast radii are better than one. + +## Moving from one provider to the other is a designed act + +**Not a removal.** Every image a machine runs is pinned to a digest at a named store, the control +plane's own included. Changing the provider means: + +1. gitea installed, reachable, and holding an account the builder may publish with +2. every artifact mirrored +3. every declaration re-pinned, the control plane's **last**, because it is what performs the others +4. **every machine verified to have converged and to be able to pull from the new store** +5. only then the old provider unassigned, and its volume kept ([ADR 0030](0030-data-outlives-the-mesh-that-declared-it.md)) + +Step 4 is the one that is easy to skip and the only thing between this and a mesh that cannot +restart its own control plane. A machine that reboots mid-migration pulls from a store that no +longer exists, and a local image cache hides that until exactly the moment it matters. + +## Consequences + +**The bootstrap is unchanged**, which is the point of keeping the small provider. + +**042 and 048 gain a second answer.** They can be fixed in the registry, or dissolved by choosing a +provider that already has accounts and TLS. The second is less work and more service. + +**ADR 0014 becomes satisfiable.** There is a provision for the private registry, something that +provides it, and a module may depend on it — so the SDK can be published and consumed rather than +cloned, and issue 053 has somewhere to go. + +**A mesh can be minimal or complete, and both are legitimate.** One with the small registry and no +gitea builds and runs modules and cannot serve packages. That is a real configuration, not a broken +one — the same way a partial host is real. + +**And the bootstrap still has no package registry.** The first build of the shared base happens +before anything has installed one. That is the same pivot as everything else and it is **not solved +here**: it is named, so the next person does not discover it. + +## How this is checked + +| Rule | Checked by | +|---|---| +| Genesis needs no database | The installer raises a mesh of one on a machine with nothing, and the artifact store it installs has no store of its own. | +| Two providers are a choice, not a conflict | A mesh holding both is asked to resolve `artifact-store` and refuses, naming both, until one is assigned. | +| The two stores are not interchangeable | A module depending on `package-registry` is not satisfied by `artifact-store`, and the refusal says why. | +| A migration is verified before it is finished | The old provider cannot be unassigned while any machine's declaration still names it. | diff --git a/02-DECISIONS/README.md b/02-DECISIONS/README.md index 362477f..12367d3 100644 --- a/02-DECISIONS/README.md +++ b/02-DECISIONS/README.md @@ -107,6 +107,7 @@ python3 00-META/checks/index.py fail if stale - **0072** — [Two graphs, and a build chain that orders itself](0072-two-graphs-and-the-build-chain.md) - **0073** — [The installer carries a builder, and the registry stays where it is](0073-the-installer-carries-a-builder.md) - **0074** — [The mesh defines a module protocol; an SDK is an implementation of it](0074-the-wire-is-specified-not-the-types.md) +- **0075** — [An artifact store is a provision; a package registry is a different one](0075-two-stores-and-which-provides-what.md) *(proposed)* ### What runs on them, and how it gets there