ADR-033 - Unified Registry Access (stella+oci:// masked refs + registry gateway)

Context

Stella Ops currently addresses container images through several overlapping paths: deployment helpers assemble real registry references, registry integrations are catalogued by connector/integration state, the internal OCI metadata registry is reached through Scanner-owned endpoints, and air-gap hosts do not have a single pull mechanism when the target host has no direct registry reachability.

The result is a contract gap:

  1. No single Stella-owned image reference. UI, audit, release creation, and deployment code can talk about the same image using different strings.
  2. Real registry hosts leak into identity. External hosts, internal metadata endpoints, and future agent-local transport are routing coordinates, not image identity.
  3. Digest policy is not represented by the naming scheme. Release deployment must remain digest-pinned even when operators enter or browse tag-shaped refs.
  4. Credential routing must remain uniform. Registry credentials must flow through the in-house secret-provider/authref model, never inline plaintext.
  5. No direct-registry hosts need a real pull path. Docker cannot be taught an arbitrary stella-ops/... namespace; Stella must resolve a masked ref to a Docker-valid reference before compose, Docker.DotNet, or deploy plugins see it.

The unified secret provider work already established the pattern for routing opaque URLs to configured backends. Registry access follows that pattern: a single injected gateway parses a Stella-owned URL and dispatches to the concrete registry backend for the selected scheme segment.

Decision

D1 - Digest is identity; masking is routing and presentation

Stella Ops adopts the opaque masked image reference:

stella+oci://<backend-selector>/<object-path>[:tag][@sha256:<digest>]

The masked reference is the stored, displayed, and audited Stella identity string. It is not Docker-pullable. Server-side deployment code must resolve it to a real Docker-valid reference before compose, Docker.DotNet, agent tasks, or native deploy plugins consume it.

The sha256 digest remains the machine identity. Tags may be accepted for browse or human-entry flows, and a tag may be retained for display, but release deployment is pinned to @sha256:<64 lowercase hex>. If both tag and digest are present, the digest wins for every machine path.

D2 - Four backend selectors are reserved by the URL scheme

The stella+oci:// backend selector is one of:

object-path is the canonical repository key. Real hosts, metadata gateway prefixes, integration tokens, and loopback agent prefixes are routing coordinates and must not become the referrer subject key.

D3 - IImageRegistryGateway is the registry router (SUPERSEDED — see note below)

WS-A Superseding note (2026-06-23): The IImageRegistryGateway router (RoutingImageRegistryGateway) and its startup probe (RegistryGatewayStartupProbe) have been deleted. The unified router is deferred until there are ≥ 2 live backends with ≥ 2 active consumers — at that point the router re-introduction trigger is: a second backend kind (beyond InternalMetadata) must be fully implemented AND wired into two distinct production call-sites. Until then the single-backend complexity is unjustified.

Current live resolution path:

  • Masked-ref resolution is performed by IMaskedImageRefResolver (implemented by MaskedImageRefResolver), registered via AddMaskedImageRefResolver.
  • The metadata backend is a standalone IMetadataReferrerClient (implemented by MetadataRegistryGateway), registered as a singleton in the same extension.
  • DI registration: services.AddMaskedImageRefResolver(configuration) — replaces the former AddRoutingImageRegistryGateway.

The historical design (router + adapters + startup probe) is preserved below for reference; it is not compiled and not deployed.

ReleaseOrchestrator owns the registry access abstraction in StellaOps.ReleaseOrchestrator.Deployment.Registry.

IImageRegistryGateway is implemented as RoutingImageRegistryGateway, a router holding one inner adapter per configured backend kind. It dispatches by the parsed StellaRegistryRef.BackendKind:

- registry -> RegistryBackendKind.InternalPrimary- meta -> RegistryBackendKind.InternalMetadata- registries/<token> -> RegistryBackendKind.ExternalIntegrated- bundle -> RegistryBackendKind.AirgapAgent

The router mirrors the current URL-routed secret-provider architecture: many configured backends can exist at once, and the reference chooses the backend. An unknown selector, malformed reference, unconfigured backend, unreachable backend, or unsupported reserved backend fails closed.

D4 - Credentials compose through the secret provider

External registry credentials are addressed by secret references such as authref://... or builtin://... and resolved through the routing ISecretProvider. Registry credentials must not be stored in masked image refs, deployment bundles, logs, descriptors, or audit payloads as plaintext.

Internal metadata, internal primary, and agent-local flows use service identity or agent trust appropriate to the deployment path rather than per-registry inline secrets.

D5 - Resolve before Docker; never hand Docker a masked ref

Docker determines whether the first path segment is a registry host by syntax such as . or : or localhost. A user-facing string like stella-ops/registry/image:v1.0 would be treated as a Docker Hub namespace, not a Stella registry. The stella+oci:// scheme prevents that ambiguity.

Every transport path must resolve masked refs server-side to real refs before Docker or compose sees them. The real ref must contain a Docker-valid host and a digest pin for deploy flows.

D6 - No-direct-registry hosts pull through a loopback agent registry

For targets flagged for agent registry pull-through or air-gap deployment, the resolved real reference rewrites the host portion to the deployment agent’s local OCI endpoint while preserving the original digest:

127.0.0.1:<agentPort>/<canonical-repo>@sha256:<digest>

For connected pull-through, the agent fetches by digest from the upstream registry using credentials resolved through ISecretProvider, verifies the digest fail-closed, caches the content-addressed object, and streams it to Docker. For air-gap, the agent serves the same /v2/ shape from an imported bundle-backed content store. If the origin integration token is needed for pull-through routing, the loopback path may carry it as a routing prefix, but the content-store key and decision-referrer subject remain the bare canonical object-path@digest.

Loopback is preferred because Docker permits plain HTTP pulls from 127.0.0.0/8 without daemon-wide insecure-registries configuration. Remote SSH deployment uses a reverse tunnel so the target still pulls from its own loopback address. Registry mirrors are rejected for this purpose because Docker mirrors do not rewrite arbitrary non-Docker-Hub hosts.

Consequences

Rejected alternatives