Registry topology — the one page of truth

Verified against commit 942a18727c (2026-09-26, DOC-2 of SPRINT_20260926_001) for the Stella Registry, token service, demo-integration and release-bundle rows (compose and bundle files named in architecture.md → Evidence). Vendor-infrastructure rows (registry.stella-ops.org, mirrors.stella-ops.org) are not verifiable from this repository. Dated sections below are measurements recorded on their date.

Binding records: ADR-041 (owner ruling 2026-08-03), which supersedes the retention/boundary framing of ADR-040 while carrying its inventory and D2/D5/D6 rulings forward. Every registry-shaped thing in the estate is listed here with its single role, auth posture, and who talks to it. If reality drifts from this page, one of them is a defect — fix the drift or fix this page (AGENTS.md §2.10), never let them disagree silently. Program state: docs/implplan/SPRINT_20260803_004_Registry_stella_registry_service_program.md.

The rule of roles

Every registry-shaped thing

SurfaceRole (exactly one)Auth postureWho talks to it
registry.stella-ops.orgVendor distribution — release/CLI images (separate vendor infrastructure: nginx + registry:3.0.0)anonymous pull; htpasswd pushcustomer estates pulling release images by digest; vendor CI publishing
mirrors.stella-ops.orgVendor mirror — compact offline vuln-dbanonymous HTTPSCLI / offline-kit downloads
Stella Registry (src/Registry/, deployables registry-web + registry-token; LIVE AND HOLDING BOTH NAMES since the SR-6 cutover, 2026-08-21: stella-registry.stella-ops.local (its own identity, and the bearer service it challenges with) AND registry.stella-ops.local (inherited from the retired zot), published on 127.1.0.49 and 127.1.1.5 in the dev compose estate)The estate’s product registry: Stella artifact store (stella-ops/plugins/*, stella-ops/enterprise/*, stella-ops/public/**) + meta-masking facade over per-installation integrated registries (registries/<slug>/… pull-through with referrers overlaid from the meta SoR) + backing of stella+oci://registrybearer tokens from its own token service (plans); anonymous pull only on stella-ops/public/** and only where the operator opts in (anonymousPlan); all pushes authenticateddeploy agents and estates pulling masked/plugin content; stella plugin sign-release publish; CLI; the release bundle
Registry token service (src/Registry/StellaOps.Registry.TokenService, container stellaops-registry-token)Auth plane for the Stella Registry. Mints 5-min plan-scoped registry JWTs; plan admin API; plan_rules is the live grant storecallers authenticate via Authority (registry.token.issue); admin via registry.admindocker/oras clients following the 401 realm; operators/CI via the gateway route ^/api/registry/token
Meta referrer plane (Scanner WebService read-only /v2/{repo}/referrers|manifests|blobs/{digest} over scanner.oci_referrers + object store)System of Record for SBOM/reachability/decision referrers (stella+oci://meta). Not a layer registry, not a registry daemonservice identity via gateway/owner APIscan pipeline (ingest), ReleaseOrchestrator decision ingest, the Stella Registry facade (read), UI/evidence surfaces
StellaOps.Agent.Registry (in-tree C#, loopback on deploy targets, default OFF)Agent-side pull-through / air-gap masker (stella+oci://bundle, ADR-033 D6); seed of the shared OCI core library (ADR-041 R1)agent trust; loopback onlyDocker daemons on no-direct-registry deploy targets
Demo-integration zot customer-registry.stella-ops.local = 127.1.2.9 (integrations family, container stellaops-customer-registry)THE demo integration — the simulated customer registry, peer of jenkins/nexus/gitlab; scan-target and fixture host; referrers-capable (proven 7/7). The lab’s only zot after R6none, deliberately — it plays “a customer’s registry”; open push/pull is the fixture pointfixture/e2e/CI lanes; Scanner scan-target plane; registries/<slug> integration registrations; EF5/QA provisioning
docker-registry.stella-ops.local / oci-registry.stella-ops.local = 127.1.2.5 (registry:2.8)Connector-surface test fixture (ADR-040 D6). No Referrers API — measured, which is why it is not the simulatornone / per-fixture htpasswd variantsIntegrations connector tests; auto-SBOM CI lane
CI pull-through proxy cache (registry:2.8.3 proxy)Upstream image cache for CI — nothing Stella-owned lives herenone.gitea workflows resolving upstream images
Release bundle registryThe Stella Registry (registry-web + registry-token in devops/release/bundle/docker-compose.yml), plus the optional demo-overlay zot demo-registry (docker-compose.demo-registry.yml, owner decision OD-1) registered as a demo integration through registries/<slug>; the bundle ships no zot by defaultStella Registry: bearer (as above). Demo overlay: open, bound to 127.0.0.1, off by defaultevaluators (“push a test image, scan it” — through the demo integration, exercising the product plane)

History

The interim lab zot (stellaops-registry, 127.1.1.5) retired at the SR-6 cutover on 2026-08-21; its compose key is gone and its volume compose_registry-data is retained on disk as the rollback. The measured build and deployment state from before and during the SR-6 cutover (the interim zot, the alias split, the plan-catalogue loading defect and its fix) is archived in registry-topology-history.md.

Plan binding RATIFIED and proven end-to-end — 2026-08-24

The owner ratified option (a) — Authority-issued plan claims — after a live end-to-end measurement (register item 10 was ruled to (a) on 2026-08-19; SR-10 implemented it; INT-5 closed on today’s proof). The measured chain, reproducible with stellaops-plugin-publisher credentials:

  1. Mint: client_credentials at Authority → token carries stellaops:plan: plugin-publisher, aud [registry, stellaops] (both audiences are load-bearing — see the standard.yaml note).
  2. Exchange: front-door /api/registry/token?service=stella-registry.stella-ops.local&scope=… → registry bearer granting pull,push on stella-ops/plugins/*. The service= identity MUST be stella-registry.stella-ops.local; the retired zot identity 401s in a way that reads as a scope failure (the KEY-2R gotcha, third sighting).
  3. Pull: manifest GET → 200, body re-hashes byte-exact to the requested digest; anonymous → 401 with the correct realm.

plan_rules.allowlist disposition (SR-10’s no-third-state mandate): reserved-unused, recorded in migration 002_reserve_plan_allowlist.sql as a COMMENT ON COLUMN and in src/Registry/AGENTS.md; no admin endpoint, validator, or importer writes it. The unknown-claim refusal is pinned by PlanRegistryTests with a positive control (community WOULD grant the probe request; an unknown explicit claim must refuse plan_unknown, never fall through to defaultPlan).

The public name is served again (fixed 2026-08-24). registry.stella-ops.local (hosts → 127.1.1.5) had NO listener from the zot retirement until today — SR-6 moved the name but the IP publish never followed. registry-web now publishes 127.1.1.5:80 beside 127.1.0.49:80 (base compose), verified healthy with the 401 challenge and a byte-exact pull through the name. A probe of the name during the outage hit the Console SPA fallback, whose 200-with-HTML reads as “anonymous pull allowed” — check content-type before believing a 200 here.

SR-6 CUTOVER EXECUTED — 2026-08-21

The Stella Registry now holds registry.stella-ops.local. The interim zot is gone. Both halves were proven, not assumed.

The forcing function, on the real client path. Authority mint → exchange at the ADVERTISED realm (https://stella-ops.local/api/registry/token) → signed publish → digest-pinned pull:

access = [{"type":"repository","name":"stella-ops/plugins/sr6-forcing-function",
           "actions":["pull","push"]}]
published @sha256:591d56ac1d06647d8d0bffd6d727fe0322d37c3c2b6fe42c7056586cedb2a384
digest-pinned GET  with a plan-scoped token -> 200 (body re-hashes to the requested digest)
digest-pinned GET  with no token            -> 401

The push grant is what makes this proof of BINDING rather than of a 200: the live community default plan grants pull-only on stella-ops/public/* and registries/* and nothing on the plugin namespace, so a defaultPlan fallback cannot produce that claim.

The alias flip, re-proven through the INHERITED name after the flip — in-network against registry.stella-ops.local:8080, the name that served zot an hour earlier: 200 with a plan-scoped token, 401 without, body sha256 = 591d56ac1d06647d8d0bffd6d727fe0322d37c3c2b6fe42c7056586cedb2a384, exactly the requested digest.

Two traps worth keeping for anyone operating this:

Rollback, unused and still intact: compose_registry-data retained on disk; stellaops/registry-web:rollback-d212aa07 and registry-token:rollback-aa329e4b resolve; Authority’s previous tag is named in docker-compose.authority-image-pin.override.yml (note its 43 → 44 migration makes that one non-symmetric).

A guard class this page keeps meeting: named for a category, bound to one path

Recorded here as a CLASS rather than as three anecdotes, because it has now caused three separate false greens in this program and the next one will not look like the last three.

The shape: a guard is named after a category, asserts in the language of that category, and is bound to a single path or a single string. It then reports on its stated scope while inspecting a fraction of it — and because it is green, nobody re-derives the scope. The name is the claim; the binding is the truth; nothing reconciles them.

InstanceNamed forActually bound toWhat slipped through
Every_plan_can_pull_through_the_facade_and_none_can_push_to_itevery shipped planShippedConfigPath = "devops/etc/registry-token.yaml"the release bundle’s sibling catalogue, which had zero facade grants for two days (2026-08-21)
NoReleaseSurface_MayReferenceAVendoredConformanceSuiteevery release surfacea string match on tools/conformancea wholesale directory copy of third-party-licenses/, which never names that path, so the text shipped in the offline kit and a runtime image (2026-08-20)
guard-commit.sh “is this commit -a?”the -a flaggrep -qE 'commit[[:space:]].*-[a-zA-Z]*a'any hyphenated path containing an a, which silently unioned the whole shared worktree into the checked set (2026-08-21)

What to do about it, in order of strength:

  1. Enumerate the category at run time, not at authoring time. Iterate the catalogue, the file set, the plan list — never a constant naming one member. The facade pin already iterated options.Plans; it just iterated the plans of one file.
  2. Bind to the property, not to a path. The absolute-path guard added in the same window asserts on RAW TEXT for exactly this reason: the next leak arrives through whichever key nobody thought to map, so a key-enumerating assertion would be green by construction.
  3. When a guard names a plural, ask what the plural actually resolves to — git grep the binding constant and count the members. If the count is 1 and the name says “every”, that is the defect, whether or not anything has slipped through yet.
  4. Sibling files never propagate. Where two artefacts are deliberate siblings rather than copies (here: the flagship and bundle plan catalogues, stage-config.py:81), a guard over one says nothing about the other, and a record claiming “both were updated” must be measured.

Countermeasure 3 was then APPLIED to this program’s other plural-named guards, and found nothing — recorded so nobody repeats the sweep. The two strongest candidates both hold, for two different structural reasons that are worth copying:

The generalisable test: a constant is safe when something independently reconciles it to reality, and an enumeration is unnecessary when the property is structural. The three failures in the table above had neither — no reconciliation, and no structural reason the single binding covered the named category.

What is deliberately NOT a registry role