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).
Amended 2026-09-27 (SPRINT_20260927_002 PCD-65): the registration is manual. The installer flag (install.sh --with-demo-registry, install.ps1 -WithDemoRegistry) starts the demo zot and prints the registration step at the end of the run; it does not register the integration. Registering from the installer means calling the Integrations API as some client with some scope, and this ADR never recorded which. Until the owner records that client and scope, the evaluator registers http://demo-registry.stella-ops.local:5000 once as an ordinary registries/<slug> integration. Automating it later takes three things: the owner-recorded client and scope, the installer calling the Integrations API once the stack is healthy, and a proof on an isolated install. A registration seeded by a database migration stays excluded: it would ship a demo integration into every real operator’s estate.
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.
