ADR-041 - Stella Registry (the internal primary registry is provisioned) and zot’s demotion to demo integration
- Status: Accepted (owner ruling 2026-08-03; supersedes ADR-040 D1/D3 and re-scopes §R1 — see the superseding note there)
- Date: 2026-08-03
- Owners: Lead architect / Registry + Integrations + ReleaseOrchestrator + Scanner + DevOps
- Supersedes: ADR-040 D1 (zot as retained infrastructure), D3 (
stella+oci://registrystays reserved), D4-host (the anonymous path’s subject), and the §R1.3 role statement. ADR-040’s D2 boundary invariants, D5 namespace/plan work, D6, and the §R1.2 destination classification survive and are carried forward here. - Amends: ADR-033 D2 (
registryselector: reserved → provisioning committed) and D3 (the router re-introduction trigger is met — as a server-side registry facade, not the deleted client-side router). - Relates to: ADR-030, ADR-034, ADR-035, ADR-039 (P16/P18/P19);
SPRINT_20260722_024,SPRINT_20260803_002,SPRINT_20260803_003,SPRINT_20260803_004; ledger rounds 91–101; registry topology truth doc
Context
The ruling
The owner ruled (2026-08-03, verbatim, typos preserved): “Sit goes just for demo/development lab. Not part of the release installation … plan how to outline and cleanse our odisy . Present the plan . Do not miss a thing. Rethink when you present a plan. I need good arch where Stella registry is what it take care of all and Zoe is just demo integration” — read with the two preceding owner statements it refines: “zot theoretically is just stella ops and cli, nothing more” (round 96) and the round-100 premise challenge (the bundle’s zot is the evaluator’s scan target, while Stella’s distribution channel is a separately named thing, STELLA_REGISTRY=registry.stella-ops.org). The owner’s standing product want, recorded earlier: “to be able to protect/mask the meta information on top of existing integrated registries that are per installation.”
Interpretation, checked against the estate and adopted as binding:
- zot is demo/development-lab material only. It is not part of the release installation. In demos and the lab it appears as a registered integration — a simulated customer registry, a peer of the jenkins/nexus/gitlab/registry:2.8 fixtures in the 127.1.2.x family.
- The Stella Registry — Stella’s own product registry service — “takes care of all”: the product-side registry roles zot has been squatting on (plugin-bundle distribution, the estate’s registry surface, the bundle’s registry, the anonymous public namespace) and the meta-masking facade over per-installation integrated registries.
- Vendor distribution is unchanged and out of scope:
registry.stella-ops.org(CLI/release images, anonymous pull + htpasswd push) andmirrors.stella-ops.org(compact vuln-db).
What the evidence says (verified 2026-08-03 against src/ and devops/)
- The reserved selector’s consumer finally exists — and always did, unprovisioned. Round 93 recorded that the registry token service’s product-side subject does not exist yet: its only live subject is the lab zot (
AllowedServices__0 = registry.stella-ops.local). The consolidation design §4a states the product use case the plan model was built for: “client estates pull RELEASE images through the masking registry with per-release, plan-tuned access (ADR-033/034/035).” That masking registry is the Stella Registry. The auth plane was built first; the ruling orders the registry under it. StellaOps.Agent.Registryis a proven read-plane seed. ~3,150 lines (re-measured 2026-08-03, excluding build output; earlier docs said ~2,170),/v2/manifests + blobs over a digest-verified content store, PullThrough/BundleOnly modes, upstream resolver. Zero PUT/POST/PATCH/DELETE routes (re-verified by grep). The push path, GC, referrers index, and conformance surface do not exist and must be built — ADR-040’s cost record stands.- The referrer System of Record is already ours.
scanner.oci_referrers(schema comment: “system of record for SBOM/reachability referrers”) served read-only atOciReferrersReadEndpoints(/v2/{repo}/referrers|manifests|blobs/{digest}), ingest via the scan pipeline +OciReferrerIngestEndpoints;DualWriteOciReferrerCoordinatorreplays externally only per-integration opt-in, default off at all three gates. - The bundle has zot today as the evaluator on-ramp (
.env.example: “Push a test image here to scan it without an external registry”), hardened by RZB-2 (named plans, operator signing key, bearer on). INSTALL_GUIDE lists zot under-InfraOnlybeside PostgreSQL/Valkey/ RustFS/Rekor — the framing round 100 identified as what disguised a stand-in as infrastructure. - Two zots run in the lab: the stella-infra zot (
registry.stella-ops.local, 127.1.1.5, bearer-gated by the token service) and the customer-registry simulator from RZB-5 (customer-registry.stella-ops.local, 127.1.2.9, open, in the 127.1.2.x fixture family, referrers proven 7/7 bycustomer-registry-referrers-smoke.sh). - RZB-5 halted with four commits landed (
3e2278d22f,e8d1a4f2e2,873609c548,d3e83784e9), classified keep/premise-dependent in ledger round 100. The ruling resolves the premise: customer-application stand-ins belong ON the demo zot (the simulator), and it is the plugin publishing — Stella’s own artifacts — that must eventually move off zot to the Stella Registry. Every premise-dependent relocation therefore lands in the KEEP column (disposition table below).
Decision
D1 - The Stella Registry is built: stella+oci://registry is provisioned as a product component
Stella Ops builds its own registry service — the Stella Registry — as the backing of ADR-033’s reserved registry selector. It is the estate’s registry surface and “takes care of all” product-side registry roles:
- Stella artifact distribution inside an estate: plugin bundles (
stella-ops/plugins/<id>, per ADR-040 D5 — the namespace and plan work transfer verbatim), licensed content (stella-ops/enterprise/*), and the anonymous public namespace (stella-ops/public/**, per ADR-040 D4 — same mechanics, new subject). - The meta-masking facade (D3 below) over per-installation integrated registries.
- The release bundle’s registry (D5 below) — zot leaves the bundle.
The service family is Registry (src/Registry/), deployables registry-web (OCI data plane) + registry-token (the existing token service as its issuer role), own database stellaops_registry (ADR-039 §2.7, fail-closed STELLAOPS_POSTGRES_REGISTRY_CONNECTION), own object-store bucket (P16), isolated build graph (P19). Family placement of the token service supersedes EVL-5’s fold into Integrations pending the owner decision in OD-2 below.
Seed code: the OCI read plane (manifest/blob GET/HEAD, digest-verified content store, upstream pull-through) is extracted from StellaOps.Agent.Registry into a domain-neutral shared OCI core library (P19-classified foundation); the agent and the Stella Registry both consume it. What must be built new, sized honestly per ADR-040’s own record: the distribution-spec push path (monolithic + chunked resumable blob-upload sessions, cross-repo mount, manifest PUT with validation), tag listing, the OCI 1.1 Referrers API (native index, no fallback-tag emulation), deletion + GC (policy-driven, not the lab-zot 24h tag-loss trap recorded in sprint 003), bearer-JWT enforcement (validating exactly the token-service claim shape zot consumes today), and standing conformance (the official OCI distribution-spec conformance suite, vendored offline, green in CI as a release gate).
P18 reconciliation — why the service-birth calculus changed. ADR-040 rejected this build because (a) no consumer needed central layer custody and (b) an Apache-2.0 binary already in the bundle served every role. Both premises are gone: the owner mandated the component; the consumer set is real and enumerable (plugin publish + agent pull, the masking facade — the owner’s explicit product want and the token service’s missing subject, the bundle’s registry after zot leaves, the public namespace in estates); and the ruling removes zot from the release installation, so “the third-party binary already ships” is no longer an available alternative for product roles. The costs ADR-040 quantified did not shrink — they are now scheduled, phased, and gated (SPRINT_20260803_004), with zot as the interim host so there is no flag-day (D6).
D2 - Storage: object-store blobs + own Postgres metadata (recommended, owner-confirmable)
Blobs live content-addressed (blobs/sha256/<hex>) in a Registry-owned bucket on the in-stack S3 object store (SeaweedFS, compose key rustfs — present in the flagship stack and the bundle already). Metadata — repositories, tags, manifest index, upload sessions, referrer index, plan/audit tables absorbed from registry_token — lives in stellaops_registry.
Rejected: reusing the Scanner CAS / ADR-035 content broker (another service’s owned storage; P16 forbids shared non-Postgres ownership exactly as §2.7 forbids shared tables) and local filesystem volumes à la zot (couples the data plane to one node, breaks role-replica sharing, adds a second backup surface when the estate already operates an object store).
D3 - The meta-masking facade: ADR-033 D3’s router, re-introduced server-side
One /v2/ surface, three backend classes, dispatched by repository namespace:
| Namespace | Backend | Content |
|---|---|---|
stella-ops/** | own store (D2) | Stella’s artifacts: plugins, public, enterprise |
registries/<slug>/<repo> | the tenant’s integrated registry, resolved from the integration record; credentials via ISecretProvider (ADR-033 D4); layers pulled through digest-verified, never copied into identity | the customer’s images, masked |
| referrers for any scanned subject | the meta SoR via Scanner’s owner API (the existing read-only /v2/ endpoints) — never a direct database read (§2.7) | SBOM / reachability / decision referrers |
This is the owner’s masking want made concrete: a consumer that pulls a customer image through the facade sees Stella’s metadata overlaid — referrers answered from our System of Record — without Stella metadata ever having to be pushed into the customer’s registry. The per-integration external replay (DualWriteOciReferrerCoordinator) remains an opt-in interop projection, unchanged.
ADR-033 bookkeeping: this satisfies D3’s re-introduction trigger (≥ 2 live backends — own store, meta, external integrated — × ≥ 2 consumers — deploy agents/estates, plugin pull, CLI, CI). The re-introduction is a server-side registry facade, not a resurrection of the deleted client-side IImageRegistryGateway; MaskedImageRefResolver stays the client-side resolver and gains the facade as a resolution target for registry and (optionally, per deployment) registries/<slug> refs. ADR-033 D1 (digest is identity), D5 (resolve before Docker), and D6 (loopback agent pull-through for no-direct-registry hosts) are unchanged; the facade complements D6, it does not replace it.
D4 - Auth: the token service is the Stella Registry’s auth plane, unchanged in contract
Everything B1/B1c built transfers as-is: the realm dance, GET /token, the plan model, plan narrowing, registry.token.issue, the admin API, the anonymous pull-only mechanics, 5-minute JWTs, key custody (operator-generated, fail-closed — KEY-1 posture). The Stella Registry validates the same access claim shape zot validates today, so cutover changes the enforcing party, not the contract. Round 92’s control-model statement carries verbatim (total control at issuance; TTL, refusal, and key rotation as the levers).
Two auth-plane decisions ride this ADR:
plan_rulesprovisioning becomes a program step, with first-boot import (recommended, owner-confirmable — OD-5). The RZB-2 finding stands: a Postgres-backed token service resolves plans fromplan_rulesonly, and the table is born empty. Recommended resolution: on startup, when the durable store holds zero plan rows, import the reviewed YAML catalogue once (audited as an import event); thereafter durable rows are authoritative and a deleted plan is never resurrected. This makes the shipped file mean what operators read without the resurrection defect the RZB-2 implementer refused to introduce.anonymousPlanflips on only on the Stella Registry. ADR-040 D4’s semantics are unchanged (pull-only,stella-ops/public/**only, operator opt-in, fail-closed default); its subject becomes the Stella Registry when that serves the public namespace (phase R5). The interim zot stays token-only throughout.
D5 - zot leaves the release installation; the demo on-ramp is an optional overlay (recommended, owner-confirmable)
The default release bundle ships no zot. Its registry is the Stella Registry (with the token service, the hardened key custody, and named plans RZB-2 established — that work transfers to the bundle’s Stella Registry deployment).
The evaluator on-ramp zot served (“push a test image here to scan it without an external registry”) is preserved as an optional demo overlay (OD-1): an installer flag adds a zot container registered as a registries/<slug> demo integration — the same shape as the lab simulator — so the evaluator’s push-and-scan exercises the real product plane against a stand-in customer registry. Rejected: a sandbox push namespace on the Stella Registry (contradicts the standing 4b ruling that customer/tenant artifact hosting on the product registry is DENIED, and re-conflates the roles this ADR separates); docs-only (guts the evaluation story).
INSTALL_GUIDE and setup tooling drop zot from the “infrastructure” list; RZB-3 absorbs the doc moves and links the registry topology truth doc.
D6 - Staged migration; zot is the interim host; no flag-day
Until the Stella Registry push path is conformant and the cutover is proven live, the stella-infra zot (127.1.1.5) remains the interim plugin-publish target, bearer-gated. The 024 S7 forcing function still flips bearer auth on it — the anonymous-PUSH-open defect (measured 202 on unauthenticated blob upload) is a live integrity hole on the host serving signed plugin bundles, and the flip live-proves the exact token dance the Stella Registry inherits. After relocations, S7’s credential matrix is ~3 rows (RZB-4 re-derivation).
Lab end state: one zot — the customer-registry simulator (127.1.2.9), open, a registered demo integration. The stella-infra zot retires at plugin-distribution cutover (phase R6), its surviving content moved by the digest-preserving copy discipline sprint 003 established. The zot bearer-auth work is not wasted: it hardened the interim period, and its registry-side validation semantics are the executable specification for the Stella Registry’s enforcement.
Phases (program SPRINT_20260803_004; sizes are honest ballparks, not commitments):
| Phase | Content | Size |
|---|---|---|
| R1 | OCI core library extracted from StellaOps.Agent.Registry (P19-classified; agent unchanged) | S |
| R2 | Service birth: read plane over own store, own DB + bucket, bearer validation, compose key, isolated publish | M |
| R3 | Push path + tag listing + conformance suite (pull/push/discovery) green in CI | L |
| R4 | Referrers API (native) + deletion/GC policy | M |
| R5 | Plan enforcement end-to-end; plan_rules first-boot import; anonymousPlan mechanics for stella-ops/public/** | S–M |
| R6 | Plugin-distribution cutover (live forcing function: sign-release → publish → deploy plugin pull, by digest); stella-infra zot retired; lab consolidates to one zot | M |
| R7 | Bundle swap: Stella Registry replaces zot; demo overlay per OD-1; INSTALL_GUIDE/RZB-3 alignment | M |
| R8 | Masking facade: registries/<slug> pull-through + meta overlay via Scanner owner API; registry selector resolution wired into MaskedImageRefResolver | L |
| R9 | Docs closeout: module dossier, truth doc end-state, knowledge-base verify | S |
R8 can proceed in parallel with R6/R7 once R3 lands. Nothing repoints before its forcing function passes; every cutover is a config move (ADR-040 D2’s invariants made this true and they carry forward: no production code binds to a registry host; product OCI features stay registry-agnostic; no registry host enters identity).
D7 - What survives from ADR-040 unchanged
- D2 boundary invariants — strengthened: they now also guarantee zot’s demotion is a compose + docs change, which is what makes D6’s staging safe.
- D5 namespace + plan model (
stella-ops/plugins/*,plugin-publisher/plugin-consumer, public plugins understella-ops/public/plugins/**). - D6 the
registry:2.8fixture family (docker-registry.stella-ops.local, 127.1.2.5) — connector-surface test scaffolding, no auth plane, stays. - §R1.2’s destination classification — it was the roadmap for RZB-5 and remains correct under the ruling; only §R1.3’s “zot’s reduced role” statement is superseded (zot’s end-state role is demo integration, not Stella-artifact host).
Disposition of all prior registry work
Nothing is silently dropped. KEEP = stands as landed; TRANSFERS = stands, subject/host changes at cutover; RE-SCOPED = task text changes; SUPERSEDED = replaced by this ADR.
| # | Work item | Disposition |
|---|---|---|
| 1 | B1 zot bearer auth (1efb4c7c27…, zot config, X.509 cert mount, realm alignment) | KEEP — interim posture + the executable spec for Stella Registry enforcement; retires with the stella-infra zot at R6 |
| 2 | B1c anonymous pull-only tokens (shipped disabled, posture-pinned) | TRANSFERS — flips on on the Stella Registry (D4), never on zot |
| 3 | registry.token.issue scope + S049 seed; gateway realm route + tests | KEEP — contract unchanged through cutover |
| 4 | Config-binder grant-widening fix (f801c33f71) | KEEP |
| 5 | Token-service plan model + admin API + audit | KEEP — becomes the Stella Registry’s auth plane (D4) |
| 6 | KEY-1 signing-key custody (fail-closed operator key) | KEEP — same posture binds the Stella Registry |
| 7 | RZB-1 plugin namespace + plans (stella-ops/plugins/*) | TRANSFERS verbatim — same namespaces, same plans, new enforcing party at R6 |
| 8 | RZB-2 bundle hardening (named plans, operator key, realm generation, BundleRegistryTokenConfigTests) | RE-SCOPED — the bundle’s zot leaves at R7, but every pattern (fail-closed key, named plans, realm-from-env, posture tests) transfers to the bundle’s Stella Registry deployment; the registry-token.bundle.yaml catalogue carries over minus the zot-specific evaluation plan (demo overlay is open, needs no plan) |
| 9 | RZB-3 doc drift | RE-SCOPED — absorbs zot-leaves-the-infrastructure-list (INSTALL_GUIDE, setup scripts) + linking the truth doc; the 127.1.2.5 conflation fix and testcontainer-skew note stand as written |
| 10 | RZB-4 credential matrix | RE-DERIVED — ~3 rows under the end-state: plugin publisher (plan plugin-publisher), plugin-pulling deploy agents (plugin-consumer), operator/CI admin; fixture lanes are on the open simulator, the bundle’s default install has no zot |
| 11 | RZB-5 halt + four landed commits | UNBLOCKED, all four KEEP: 3e2278d22f (simulator = the end-state demo zot), e8d1a4f2e2 (scan-target plane → simulator: customer stand-ins belong on the demo integration), 873609c548 (fixture/e2e repoints: same), d3e83784e9 (renames, fail-closed attach-referrers defaults, mirror example, CLI bootstrap endpoint → simulator: the demo integration IS the simulator) |
| 12 | RZB-5 frozen-fixture blockers (compliance-golden-path, verify-os-analyzer-scan) | RIDE SPRINT 003 — FPR-5 re-targets the republish to the simulator and repoints the lane; the uncommitted copy-lab-fixture-to-simulator.sh gets committed as part of that task |
| 13 | RZB-6 fail-closed Scanner defaults | KEEP — the same fail-closed discipline binds the Stella Registry’s own client configs |
| 14 | Sprint 003 FPR-1…4 (provenance, preservation, recipes, per-lane analysis) | KEEP — unaffected by the ruling, as that sprint predicted |
| 15 | Sprint 003 FPR-6 (EF5 reference vs bytes) | RESOLVED DIRECTION — bytes follow the reference: copy sha256:a9898acd… to customer-registry.stella-ops.local:5000/atlas/pas-policy; the reference already names the end-state registry |
| 16 | Smoke scripts | KEEP — registry-auth-smoke.sh re-targets the Stella Registry at R6 (same dance); customer-registry-referrers-smoke.sh unchanged; both become regression harnesses |
| 17 | 024 INT-1 S0 design + EVL-5 fold | RE-SCOPED pending OD-2 — the availability-isolation guardrail (token issuance must never be taken down by webhook storms) carries regardless of family placement; the DC-15 pin-collapse work is Integrations’ own and proceeds independently |
| 18 | 024 S7 bearer flip | RE-TARGETED — guards the interim stella-infra zot until R6; preconditions unchanged (plan rows provisioned, RZB-4 matrix, key material staged); its posture then transfers to the Stella Registry |
| 19 | northwind/ renames, stellaops/tetragon verdict, dockerhub-mirror example | KEEP |
| 20 | StellaOps.Agent.Registry | KEEP + PROMOTED — read plane extracted to the shared OCI core (R1); the agent’s behavior is unchanged and its specs must stay green |
| 21 | ADR-040 D2 invariants, D5, D6, §R1.2 | CARRIED FORWARD (D7) |
| 22 | ADR-040 D1, D3, D4-host, §R1.3 | SUPERSEDED by this ADR |
| 23 | Vendor infra (registry.stella-ops.org, mirrors.stella-ops.org) | UNCHANGED, out of scope |
Owner decisions (each with recommendation; the program’s OD gate)
RULED 2026-08-03 (same day): OD-1/2/3/4/6 approved as recommended; OD-5 approved with a clarification (the import is config→empty-table seeding, never data preservation or overwrite — see SPRINT_20260803_004 SR-0 for the recorded wording); the fixture durable home approved (Registry-owned S3
fixtures/bucket + committed digest manifest). OD-2 explicitly supersedes EVL-5’s token-service fold into Integrations. The OD gate is CLOSED; SR-1 may proceed.
- OD-1 — Bundle evaluation on-ramp shape. Recommend: optional demo overlay (a zot registered as a demo
registries/<slug>integration, installer flag, off by default). Alternatives: sandbox namespace on the Stella Registry (rejected above — contradicts the 4b DENY on customer artifact hosting); docs-only. - OD-2 — Token-service family placement. Recommend: Registry family (registry-web + registry-token role containers, one DB
stellaops_registry), superseding EVL-5’s fold into Integrations — a registry and its token issuer are one inseparable domain lifecycle (ADR-039 P19’s consolidation test), and 024’s guardrail transfers intact. This changes an owner-ruled decision (EVL-5, 2026-07-22), so it is explicitly the owner’s to confirm, not mine. - OD-3 — Storage backend. Recommend: in-stack SeaweedFS S3, Registry-owned bucket, metadata in own Postgres (D2). Alternative: local FS volume (rejected above).
- OD-4 — zot consolidation. Recommend: the simulator identity survives (
customer-registry.stella-ops.local, 127.1.2.9, open, integrations family); the stella-infra zot (127.1.1.5) retires at R6. Alternative — keeping 127.1.1.5 and re-roling it — rejected: it carries the bearer plane a fixture must not have, and the simulator is already proven (referrers 7/7) and already sits in the demo-integration family. - OD-5 —
plan_rulesfirst-boot import (D4). Recommend: import-once-when-empty, audited; durable rows authoritative thereafter. This closes the RZB-2 open call. - OD-6 — Cutover timing. Recommend: R6 gates on the conformance suite green + the live plugin publish→pull forcing function; R7 rides the next bundle regeneration after R6; no calendar commitment.
- (Related, owned by sprint 003:) the durable home for the preserved unreproducible fixture bytes still needs an owner call — raised again here so it does not silt up.
Consequences
- The product finally owns its registry surface: distribution-spec conformance, push, GC, and referrers become Stella-owned obligations, priced in D6’s phases, with the conformance suite as a standing release gate.
- The token service stops being an auth plane without a product subject; the plan model’s original purpose (per-release, plan-tuned access to masked content) becomes deliverable.
- The bundle stops shipping a stand-in as infrastructure; INSTALL_GUIDE’s registry story becomes truthful; the “one zot, demo integration” lab matches what demos claim.
- ADR-040’s honest answer to “can we delete zot” flips from “yes mechanically, nothing gained” to “yes, staged, and here is what is gained” — because the ruling changed what the product must own, not because the earlier cost accounting was wrong.
- Every registry-shaped thing in the estate now has exactly one role, recorded in the registry topology truth doc; drift against it is a defect.
Rejected alternatives
- Keep ADR-040’s world (zot as retained infrastructure, selector reserved). Rejected by the owner ruling. The boundary invariants that made that world safe are exactly what make this migration cheap; they are kept.
- Back the
registryselector with zot. Still rejected (ADR-040 D3’s reasoning holds): it promotes a third-party binary into the product identity contract and would make the demo demotion a product break. - Sandbox push namespace on the Stella Registry as the bundle on-ramp. Rejected (D5): the 4b ruling denies customer artifact hosting on the product registry; evaluation is a demo flow and belongs on the demo integration.
- Reuse the Scanner CAS or ADR-035 broker as the blob store. Rejected (D2): P16 ownership.
- Gateway-fronting the registry. Re-rejected (rounds 92, ADR-040): layer-blob throughput through the front door for zero authorization gain.
- Flag-day cutover (build, then switch everything at once). Rejected: the interim zot is bearer-hardened and boring; every phase has its own forcing function; the frozen-fixture lesson (sprint 003) is that unverified moves lose bytes.
