LOCKED interface spec - Unified Registry Access
- Status: LOCKED / binding for all implementers (ADR-033).
- Date: 2026-06-22
- Authority: This file defines the registry masked-reference grammar and the exact C# contracts currently implemented under
src/ReleaseOrchestrator/__Libraries/StellaOps.ReleaseOrchestrator.Deployment/Registry/. Implementers MUST NOT alter signatures without amending this spec + ADR-033.src/is source of truth; if a type name here drifts from code, the code wins and this spec is amended.
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:
- The parser accepts the
stella+oci://scheme case-insensitively and formats it back as lowercase. object-pathis the canonical repository key. Hostnames, integration tokens, metadata gateway bases, and loopback agent prefixes are folded out of this key.- The implemented
StellaRegistryRefcurrently parses the path and tag/digest anchor only. Query-string referrer selectors such as?referrers=<media-type>are ADR-level address forms, not parsed fields on the currentStellaRegistryReftype. - Deploy flows require
@sha256:<64 lowercase hex>. Tag-only refs are browse or pre-pin input and must be resolved to a digest before release deployment.
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.0 | stellaops-registry:5000/polaris/core:v1.0 | Same 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:
ParsethrowsFormatExceptionfor null/whitespace input, unsupported schemes, unknown backends, missing object paths, malformedregistriesintegration refs, malformed tags, malformed digests, duplicate digest anchors, or invalid object-path segments.TryParsecatchesFormatException, returnsfalse, and setsresulttonull.IntegrationRefis populated only forBackendKind.Registries.Digestis the literalsha256:plus 64 lowercase hex characters. Uppercase hex is rejected by the current parser.Format()emits the canonical lowercase scheme and reassemblesstella+oci://registry/...,stella+oci://meta/...,stella+oci://registries/<integrationRef>/..., orstella+oci://bundle/....
3. Live resolution contracts (WS-A, 2026-06-23)
Note:
IImageRegistryGateway,RoutingImageRegistryGateway, andRegistryGatewayStartupProbewere deleted as part of WS-A. The routing gateway is deferred until ≥ 2 live backends + ≥ 2 consumers exist. The live contracts areIMaskedImageRefResolver(deploy path) andIMetadataReferrerClient(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)
RegistryGatewayStartupProbewas deleted alongsideIImageRegistryGateway. No startup probe exists for the resolver; theIMetadataReferrerClientis 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:
- registry integration rows carry non-secret endpoint metadata plus an auth reference such as
authref://...orbuiltin://...; - the gateway resolves that reference through
ISecretProviderin the caller’sRegistryAccessContext.TenantId; - missing credentials, cross-tenant lookup failure, transport failure, or a malformed reference fails closed;
- plaintext credentials are used only for the OCI token dance and must never be logged or returned in descriptors.
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
- Masked refs are presentation/routing strings. The sha256 digest is identity.
- Deploy refs must resolve to Docker-valid real refs with an explicit digest.
- A tag-only input must be resolved to a digest and frozen before deployment.
- Across direct, metadata, external, pull-through, and air-gap paths, the digest is carried verbatim.
- Agent pull-through rewrites host routing to
127.0.0.1:<agentPort>while preservingobject-path@digest. If the loopback path contains/registries/<token>/, that prefix is for origin routing and is stripped before content-store/referrer-subject lookup. - Fetchers must fail closed by verifying
sha256(bytes)against the descriptor digest before using pulled content. - Docker registry mirrors are not a substitute for the loopback rewrite because they do not redirect arbitrary non-Docker-Hub registries.
