ADR-041 - Stella Registry (the internal primary registry is provisioned) and zot’s demotion to demo integration

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:

  1. 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.
  2. 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.
  3. Vendor distribution is unchanged and out of scope: registry.stella-ops.org (CLI/release images, anonymous pull + htpasswd push) and mirrors.stella-ops.org (compact vuln-db).

What the evidence says (verified 2026-08-03 against src/ and devops/)

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:

  1. 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).
  2. The meta-masking facade (D3 below) over per-installation integrated registries.
  3. 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:

NamespaceBackendContent
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 identitythe customer’s images, masked
referrers for any scanned subjectthe 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:

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):

PhaseContentSize
R1OCI core library extracted from StellaOps.Agent.Registry (P19-classified; agent unchanged)S
R2Service birth: read plane over own store, own DB + bucket, bearer validation, compose key, isolated publishM
R3Push path + tag listing + conformance suite (pull/push/discovery) green in CIL
R4Referrers API (native) + deletion/GC policyM
R5Plan enforcement end-to-end; plan_rules first-boot import; anonymousPlan mechanics for stella-ops/public/**S–M
R6Plugin-distribution cutover (live forcing function: sign-release → publish → deploy plugin pull, by digest); stella-infra zot retired; lab consolidates to one zotM
R7Bundle swap: Stella Registry replaces zot; demo overlay per OD-1; INSTALL_GUIDE/RZB-3 alignmentM
R8Masking facade: registries/<slug> pull-through + meta overlay via Scanner owner API; registry selector resolution wired into MaskedImageRefResolverL
R9Docs closeout: module dossier, truth doc end-state, knowledge-base verifyS

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

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 itemDisposition
1B1 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
2B1c anonymous pull-only tokens (shipped disabled, posture-pinned)TRANSFERS — flips on on the Stella Registry (D4), never on zot
3registry.token.issue scope + S049 seed; gateway realm route + testsKEEP — contract unchanged through cutover
4Config-binder grant-widening fix (f801c33f71)KEEP
5Token-service plan model + admin API + auditKEEP — becomes the Stella Registry’s auth plane (D4)
6KEY-1 signing-key custody (fail-closed operator key)KEEP — same posture binds the Stella Registry
7RZB-1 plugin namespace + plans (stella-ops/plugins/*)TRANSFERS verbatim — same namespaces, same plans, new enforcing party at R6
8RZB-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)
9RZB-3 doc driftRE-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
10RZB-4 credential matrixRE-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
11RZB-5 halt + four landed commitsUNBLOCKED, 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)
12RZB-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
13RZB-6 fail-closed Scanner defaultsKEEP — the same fail-closed discipline binds the Stella Registry’s own client configs
14Sprint 003 FPR-1…4 (provenance, preservation, recipes, per-lane analysis)KEEP — unaffected by the ruling, as that sprint predicted
15Sprint 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
16Smoke scriptsKEEP — registry-auth-smoke.sh re-targets the Stella Registry at R6 (same dance); customer-registry-referrers-smoke.sh unchanged; both become regression harnesses
17024 INT-1 S0 design + EVL-5 foldRE-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
18024 S7 bearer flipRE-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
19northwind/ renames, stellaops/tetragon verdict, dockerhub-mirror exampleKEEP
20StellaOps.Agent.RegistryKEEP + PROMOTED — read plane extracted to the shared OCI core (R1); the agent’s behavior is unchanged and its specs must stay green
21ADR-040 D2 invariants, D5, D6, §R1.2CARRIED FORWARD (D7)
22ADR-040 D1, D3, D4-host, §R1.3SUPERSEDED by this ADR
23Vendor 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.

Consequences

Rejected alternatives