diff --git a/02-DECISIONS/0109-a-package-registry-seat-is-one-per-ecosystem.md b/02-DECISIONS/0109-a-package-registry-seat-is-one-per-ecosystem.md new file mode 100644 index 0000000..e89a743 --- /dev/null +++ b/02-DECISIONS/0109-a-package-registry-seat-is-one-per-ecosystem.md @@ -0,0 +1,106 @@ +--- +topic: the tiers +status: accepted +date: 2026-09-25 +deciders: jochen +reconstructed: false +extends: 0075-two-stores-and-which-provides-what.md +--- + +# 109. A package registry seat is one per ecosystem, not one for all of them + +## Context + +Fixing `builder`'s consumption of `package-registry` tonight surfaced the shape ADR 0075 actually +left implicit. 0075 split `artifact-store` from `package-registry` and said the second is "an +ecosystem's own registry — npm, cargo, PyPI, Go" — but it defined one provision for all four, +not one each. + +What that produces, read from the manifests as they stand: + +- `gitea`'s `module.json` declares `provides: package-registry` once, and its `serves` block + carries exactly one path: `npm-path`. Nothing names a cargo or PyPI endpoint, though gitea's own + package API serves both. +- `builder` had, until tonight, a hand-written JSON fragment standing in for a real grant — + `{"provision": "package-registry", "from": "gitea", "at": "127.0.0.1", ...}` — because nothing + in the interface gave it a real one to ask for. The fragment named `npm-path` specifically; there + was nowhere to put a second ecosystem's endpoint even if one had been wired. +- The fix applied tonight declares `requires: ["artifact-store", "package-registry"]` and lets the + mesh mint the grant properly — correct for what exists today, but it is one seat standing in for + what should be several, the same conflation 0075 itself named and did not resolve for this + provision specifically. + +**The version-skew problem 0075 wrote down for the artifact-store/package-registry split repeats +one level down, inside "package registry" itself:** npm resolves by name and range from one +namespace, cargo from another, and a single grant conflates them exactly the way one store for +both digests and ranges would have. + +## Considered Options + +**1. One `package-registry` provision, gitea answers every ecosystem it can.** What exists today. +Simplest to grant — one credential, one binding, done once per consumer. Rejected: a consumer +that only ever needs npm still receives a grant shaped to cover cargo and PyPI, and there is no way +to hand off *only* npm to a different provider (verdaccio, say) without renegotiating the whole +provision for every consumer of any ecosystem. + +**2. One provision, parameterised by ecosystem.** `requires: package-registry` plus a declared +`ecosystem: npm` alongside it, still one interface. Rejected: the `provides`/`requires` refusal +mechanism this mesh already uses (two providers of one provision is a naming conflict until +resolved) would need to become conditional on a parameter it does not otherwise carry anywhere in +the mesh's resolution — a special case for exactly one provision, rather than the mesh's existing +mechanism applied again. + +**3. One provision per ecosystem — `npm-package-registry`, `cargo-package-registry`, +`docker-package-registry`, and so on, each independently `provides`/`requires`.** Chosen. + +## Decision + +**A package registry seat is one per ecosystem.** `npm-package-registry`, `cargo-package-registry`, +`docker-package-registry` — each its own provision, resolved, granted, and refused exactly the way +`artifact-store` and today's single `package-registry` already are. Adding an ecosystem is adding a +provision, not widening one. + +**Gitea may hold several seats at once.** Nothing here says gitea answers only one; ADR 0075 +already established that a provider may answer more than one named thing on one machine ("a mesh +running gitea for git and packages alongside a registry serving artifacts is an ordinary +arrangement"). Gitea fulfilling `npm-package-registry` and `cargo-package-registry` both is the +expected shape, not an exception. + +**Each seat's grant is independent.** A consumer that only needs npm holds only the +`npm-package-registry` grant. Moving that one ecosystem to a different provider — verdaccio, +named directly as the motivating case — means assigning `npm-package-registry` to verdaccio and +leaving every other seat exactly where it was. No consumer of `cargo-package-registry` observes +the change; no manifest naming `package-registry` broadly needs to be found and re-read. + +**`builder`'s fix tonight is the interim shape, not the target.** It correctly consumes the one +seat that exists today (`package-registry`, npm in practice). Splitting it becomes, later, +replacing that one line with the ecosystems `builder` actually uses — a manifest change, not a +redesign of how `builder` asks for anything. + +## Consequences + +- `gitea`'s `module.json` gains a `provides` entry per ecosystem it actually serves, each with its + own `serves` block (`npm-path`, a cargo path, a PyPI path) in place of the one `package-registry` + entry with a single `npm-path` inside it. +- `gitea`'s provisioner (`modules/gitea/provisioner/index.ts`) currently runs one `runProvisioner` + registration for `package-registry`; each seat needs its own registration, or one provisioner + keyed by which seat's `create`/`remove` fired — the mesh's `Provision` type does not yet carry + which named provision a call is for when a module answers more than one, and that is worth + checking before assuming the harness already supports it. +- Every consumer's `requires` moves from the one name to however many ecosystems it actually uses. + `builder` is the only known consumer today; widening later is one manifest line per module, not + a migration. +- `verdaccio`'s role sharpens: not "package-registry, an alternative for npm alone" (0075's phrasing) + but a named `npm-package-registry` *provider*, a straight swap against gitea's answer to the same + seat. +- Not solved here: whether `cargo-package-registry` and `pypi-package-registry` are needed at all + before something actually consumes them. This record names the shape; building unused seats is + its own decision. + +## References + +- [ADR 0075](0075-two-stores-and-which-provides-what.md) — the record this extends; defined + `package-registry` as the second provision without splitting it per ecosystem. +- `mesh-catalog modules/builder/module.json` — tonight's fix, the interim single-seat shape. +- `mesh-catalog modules/gitea/module.json`, `modules/gitea/provisioner/index.ts` — today's + single-provision, npm-only implementation. diff --git a/02-DECISIONS/README.md b/02-DECISIONS/README.md index e0ae9bd..dd26cb8 100644 --- a/02-DECISIONS/README.md +++ b/02-DECISIONS/README.md @@ -123,6 +123,7 @@ python3 00-META/checks/index.py fail if stale - **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) - **0098** — [A fact a provider makes at first start is fetched from it, not carried in its manifest](0098-a-fact-a-provider-makes-at-first-start-is-fetched-from-it.md) +- **0109** — [A package registry seat is one per ecosystem, not one for all of them](0109-a-package-registry-seat-is-one-per-ecosystem.md) ### What runs on them, and how it gets there diff --git a/03-DESIGN/01-to-be/22-the-work-ahead.md b/03-DESIGN/01-to-be/22-the-work-ahead.md index a9ddda4..97c3519 100644 --- a/03-DESIGN/01-to-be/22-the-work-ahead.md +++ b/03-DESIGN/01-to-be/22-the-work-ahead.md @@ -12,6 +12,7 @@ decisions: - 02-DECISIONS/0074-the-wire-is-specified-not-the-types.md - 02-DECISIONS/0075-two-stores-and-which-provides-what.md - 02-DECISIONS/0014-no-npm-workspace.md + - 02-DECISIONS/0109-a-package-registry-seat-is-one-per-ecosystem.md --- # The work ahead