109: a package registry seat is one per ecosystem, not one for all of them

Extends ADR 0075. Surfaced fixing builder's hand-faked package-registry
binding tonight: gitea's manifest declares the provision once with a single
npm-path, conflating what should be independently assignable per ecosystem
(npm/cargo/docker/...) the same way artifact-store and package-registry
were themselves split. Cited in 22-the-work-ahead.md's Phase 2, where the
target state this decision points at was already described a week ago.

Numbered 109, not 108: route-proxy's policy feature (mesh-controller PR
still-unwritten decision record — reserved but never committed. Renumbered
around it rather than colliding.
This commit is contained in:
2026-09-25 20:29:22 +02:00
committed by jochen
parent 56669ee23b
commit 59c93dcfe4
3 changed files with 108 additions and 0 deletions
@@ -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.
+1
View File
@@ -124,6 +124,7 @@ python3 00-META/checks/index.py fail if stale
- **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)
- **0108** — [A route carries the policy applied to a request, and names a secret rather than holding one](0108-a-route-carries-the-policy-applied-to-a-request.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