LOCKED interface spec - Unified Registry Access

All contracts land in namespace StellaOps.ReleaseOrchestrator.Deployment.Registry.


1. stella+oci:// masked reference grammar

The masked reference is stored, displayed, and audited by Stella. It is not a Docker-pullable reference and must be resolved server-side before any Docker, compose, Docker.DotNet, or deploy-plugin transport consumes it.

masked-ref     = stella-scheme "://" backend-selector "/" object-path [ ref-anchor ]
stella-scheme  = "stella+oci"                       ; case-insensitive parse, lower on format

backend-selector =
      "registry"                                    ; internal primary image SoR, reserved
    | "meta"                                        ; internal OCI metadata/referrer backend
    | "registries" "/" integration-ref              ; external integrated registry
    | "bundle"                                      ; air-gap or agent-served cache

integration-ref  = integration-slug / integration-guid
integration-slug = 1*( lc-alpha / DIGIT / "-" )
integration-guid = 8hex "-" 4hex "-" 4hex "-" 4hex "-" 12hex

object-path  = path-segment *( "/" path-segment )
path-segment = lc-component *( separator lc-component )
lc-component = 1*( lc-alpha / DIGIT )
separator    = "." / "_" / "__" / 1*"-"

ref-anchor   = ":" tag / "@" digest / ":" tag "@" digest
tag          = 1*128( ALPHA / DIGIT / "_" / "." / "-" )
digest       = "sha256:" 64hex

real-ref     = host [ ":" port ] "/" object-path "@" digest
             | host [ ":" port ] "/" object-path ":" tag

lc-alpha     = %x61-7A
hex          = DIGIT / %x61-66
8hex         = 8(hex)
4hex         = 4(hex)
12hex        = 12(hex)
64hex        = 64(hex)

Notes:

Worked examples

Masked (canonical Stella ref)Real (resolved)Meaning
stella+oci://registry/polaris/core@sha256:3f...<64hex>stellaops-registry:5000/polaris/core@sha256:3f...<64hex>Internal primary image SoR, digest-pinned. Reserved until Stella stores image layers itself; implementations must fail closed for layer pull while preserving the canonical display form.
stella+oci://registry/polaris/core:v1.0stellaops-registry:5000/polaris/core:v1.0Same backend, tag form for browse or pre-pin display. Release creation resolves the tag to a digest and stores the @sha256 form.
stella+oci://registries/polaris-gitlab/partnerco/polaris-backend/core@sha256:9c...<64hex>registry.example.com/partnerco/polaris-backend/core@sha256:9c...<64hex>External integrated registry by immutable slug. The host is resolved from the tenant-scoped integration row and is not repeated in object-path. Credentials resolve through authref:///ISecretProvider.
stella+oci://registries/4f8c1a2e-1b2c-4d3e-8a90-aabbccddeeff/partnerco/polaris-backend/core@sha256:9c...<64hex>registry.example.com/partnerco/polaris-backend/core@sha256:9c...<64hex>Same external integration by GUID fallback, used before a slug exists or when a slug cannot be emitted.
stella+oci://meta/polaris/core@sha256:9c...<64hex><gateway>/v2/polaris/core/referrers/sha256:9c...<64hex>Internal OCI metadata/referrer backend by subject digest. Metadata only; no image layers. Artifact-type filtering is supplied to ResolveReferrersAsync as a method argument.
stella+oci://bundle/polaris/core@sha256:3f...<64hex>127.0.0.1:8021/polaris/core@sha256:3f...<64hex>Air-gap or no-direct-registry target. The deployment agent mimics a loopback OCI registry and preserves the digest verbatim.
stella+oci://registries/polaris-gitlab/partnerco/polaris-backend/core@sha256:fa11...<64hex>127.0.0.1:8021/registries/polaris-gitlab/partnerco/polaris-backend/core@sha256:fa11...<64hex>Connected pull-through via agent. The loopback path may carry the integration token for origin routing; the content-store key and referrer subject remain partnerco/polaris-backend/core@sha256:fa11....
stella+oci://registries/harbor-prod/library/nginx:1.27@sha256:7e...<64hex>harbor.corp.example/library/nginx:1.27@sha256:7e...<64hex>Tag plus digest on an external Harbor integration. The tag is display-only; the digest is authoritative for machines.

2. StellaRegistryRef and parser enum

namespace StellaOps.ReleaseOrchestrator.Deployment.Registry;

public enum BackendKind
{
    Registry,
    Meta,
    Registries,
    Bundle,
}

public sealed class StellaRegistryRef : IEquatable<StellaRegistryRef>
{
    public BackendKind BackendKind { get; }
    public string? IntegrationRef { get; }
    public string ObjectPath { get; }
    public string? Tag { get; }
    public string? Digest { get; }

    public static StellaRegistryRef Parse(string input);

    public static bool TryParse(
        string input,
        [NotNullWhen(true)] out StellaRegistryRef? result);

    public string Format();
    public override string ToString();
    public bool Equals(StellaRegistryRef? other);
    public override bool Equals(object? obj);
    public override int GetHashCode();
}

Parser behavior:


3. Live resolution contracts (WS-A, 2026-06-23)

Note: IImageRegistryGateway, RoutingImageRegistryGateway, and RegistryGatewayStartupProbe were deleted as part of WS-A. The routing gateway is deferred until ≥ 2 live backends + ≥ 2 consumers exist. The live contracts are IMaskedImageRefResolver (deploy path) and IMetadataReferrerClient (metadata reads). Historical type signatures are preserved in §3-legacy below for reference only.

3.1 IMaskedImageRefResolver — deploy-path resolution

namespace StellaOps.ReleaseOrchestrator.Deployment.Registry;

public interface IMaskedImageRefResolver
{
    /// <summary>
    /// Resolve a masked <c>stella+oci://</c> reference (or a legacy real ref)
    /// to a Docker-valid, digest-pinned <see cref="ResolvedImage"/>.
    /// Legacy back-compat: null/whitespace or non-<c>stella+oci://</c> input
    /// is returned unchanged (pass-through for releases pinned before migration 005).
    /// Fail-closed: unknown slug, missing integration, cross-tenant lookup, or
    /// missing/invalid digest all throw <see cref="RegistryUnreachableException"/>.
    /// </summary>
    Task<ResolvedImage> ResolveAsync(
        string? maskedOrLegacyRef,
        RegistryAccessContext context,
        CancellationToken cancellationToken);
}

Implementation: MaskedImageRefResolver (scoped).

3.2 IMetadataReferrerClient — standalone metadata backend

namespace StellaOps.ReleaseOrchestrator.Deployment.Registry;

public interface IMetadataReferrerClient
{
    /// <summary>Resolves the OCI referrers index for the given subject digest.</summary>
    Task<IReadOnlyList<ReferrerDescriptor>> ResolveReferrersAsync(
        StellaRegistryRef reference,
        string? artifactType,
        RegistryAccessContext context,
        CancellationToken cancellationToken);

    /// <summary>
    /// Resolves a deployment-decision referrer from the internal metadata store.
    /// Returns <c>null</c> when none exists.
    /// </summary>
    Task<DeploymentDecision?> ResolveDecisionAsync(
        StellaRegistryRef reference,
        RegistryAccessContext context,
        CancellationToken cancellationToken);
}

Implementation: MetadataRegistryGateway (singleton, backed by a named HttpClient pointed at Deployment:Registry:MetaGatewayBaseUri). This type implements IMetadataReferrerClient directly — it does not implement IImageRegistryGateway (which is deleted).

3.3 Shared models

namespace StellaOps.ReleaseOrchestrator.Deployment.Registry;

public enum RegistryBackendKind
{
    InternalPrimary,
    InternalMetadata,
    ExternalIntegrated,
    AirgapAgent,
    Router,          // reserved — not instantiated while the router is deferred
}

public sealed record RegistryAccessContext(
    Guid TenantId,
    DeployTargetCapability Capability,
    string? ActorId = null);

public enum DeployTargetCapability
{
    Direct,
    AgentPullThrough,
    Airgap,
}

public sealed record ResolvedImage(
    StellaRegistryRef? Masked,
    string RealRef,
    string Digest,
    string? Tag);

public sealed record ReferrerDescriptor(
    string Digest,
    string ArtifactType,
    string MediaType,
    long Size,
    string? AttachmentState = null);

public sealed record DeploymentDecision(
    string SubjectDigest,
    string Verdict,
    string SignatureRef,
    ReferrerDescriptor Descriptor);

public sealed record RegistryGatewayDescriptor(
    RegistryBackendKind BackendKind,
    bool Reachable,
    string RealHost,
    string? IntegrationToken = null,
    string? CredentialBackendKind = null);

public sealed class RegistryUnreachableException : Exception
{
    public RegistryUnreachableException(string message, Exception? inner = null);
}

4. DI registration (live)

namespace StellaOps.ReleaseOrchestrator.Deployment.Registry;

public static class RegistryGatewayServiceCollectionExtensions
{
    public const string MetadataHttpClientName =
        "StellaOps.ReleaseOrchestrator.Deployment.Registry.Metadata";

    /// <summary>
    /// Registers <see cref="IMaskedImageRefResolver"/> (scoped) and
    /// <see cref="IMetadataReferrerClient"/> (singleton).
    /// The routing gateway and startup probe were removed (WS-A).
    /// </summary>
    public static IServiceCollection AddMaskedImageRefResolver(
        this IServiceCollection services,
        IConfiguration configuration);
}

Configuration keys:

Deployment:Registry:MetaGatewayBaseUri   — base URL for the Scanner /v2/ surface

AgentLocalPort, InternalPrimaryHost, and the Backends[] / Backend array keys remain in RegistryGatewayOptions for forward-compatibility but are not read by the live resolver path.


5. Fail-closed probe (DELETED — WS-A)

RegistryGatewayStartupProbe was deleted alongside IImageRegistryGateway. No startup probe exists for the resolver; the IMetadataReferrerClient is validated lazily (first call site that needs it). A probe will be re-introduced when the router is reinstated (re-introduction trigger: ≥ 2 live backends + ≥ 2 consumers).


3-legacy. Historical gateway contract (reference only — not compiled)

The following types were part of the original D3 design and are preserved here for historical context. They are not in the compiled codebase.

// HISTORICAL — deleted WS-A. Not compiled.
public interface IImageRegistryGateway
{
    RegistryBackendKind BackendKind { get; }
    Task<ResolvedImage?> ResolveManifestAsync(StellaRegistryRef reference, RegistryAccessContext context, CancellationToken cancellationToken);
    Task<Stream> OpenBlobAsync(StellaRegistryRef reference, RegistryAccessContext context, CancellationToken cancellationToken);
    Task<IReadOnlyList<ReferrerDescriptor>> ResolveReferrersAsync(StellaRegistryRef reference, string? artifactType, RegistryAccessContext context, CancellationToken cancellationToken);
    Task<DeploymentDecision?> ResolveDecisionAsync(StellaRegistryRef reference, RegistryAccessContext context, CancellationToken cancellationToken);
    Task<RegistryGatewayDescriptor> DescribeAsync(CancellationToken cancellationToken);
}

public sealed class RoutingImageRegistryGateway : IImageRegistryGateway { /* deleted */ }

public sealed class RegistryGatewayStartupProbe : IHostedService { /* deleted */ }

7. Credential composition

External registry adapters must resolve credentials through the accepted secret-provider routing model:

Internal primary, metadata, and agent-served paths use service identity or agent trust for the registry path itself. They may still report the composed credential backend kind for audit when such information exists, but descriptors remain non-secret.


8. Digest-pinning and loopback transport invariants