ValidateOCI validates that an OCI image contains the correct MCP server name annotation. Supports canonical OCI references including: - registry/namespace/image:tag - registry/namespace/image@sha256:digest - registry/namespace/image:tag@sha256:digest - namespace/image:tag (defaults to docker.io) Su
(ctx context.Context, pkg model.Package, serverName string)
| 54 | // - Google Artifact Registry (*.pkg.dev) |
| 55 | // - Microsoft Container Registry (mcr.microsoft.com) |
| 56 | func ValidateOCI(ctx context.Context, pkg model.Package, serverName string) error { |
| 57 | if pkg.Identifier == "" { |
| 58 | return ErrMissingIdentifierForOCI |
| 59 | } |
| 60 | |
| 61 | // Validate that old format fields are not present |
| 62 | if pkg.RegistryBaseURL != "" { |
| 63 | return fmt.Errorf("OCI packages must not have 'registryBaseUrl' field - use canonical reference in 'identifier' instead (e.g., 'docker.io/owner/image:1.0.0')") |
| 64 | } |
| 65 | if pkg.Version != "" { |
| 66 | return fmt.Errorf("OCI packages must not have 'version' field - include version in 'identifier' instead (e.g., 'docker.io/owner/image:1.0.0')") |
| 67 | } |
| 68 | if pkg.FileSHA256 != "" { |
| 69 | return fmt.Errorf("OCI packages must not have 'fileSha256' field") |
| 70 | } |
| 71 | |
| 72 | // Parse the OCI reference using go-containerregistry's name package |
| 73 | // This handles all the complexity of reference parsing including defaults |
| 74 | ref, err := name.ParseReference(pkg.Identifier) |
| 75 | if err != nil { |
| 76 | return fmt.Errorf("invalid OCI reference: %w", err) |
| 77 | } |
| 78 | |
| 79 | // Validate that the registry is in the allowlist |
| 80 | registry := ref.Context().RegistryStr() |
| 81 | if !isAllowedRegistry(registry) { |
| 82 | return fmt.Errorf("%w: %s", ErrUnsupportedRegistry, registry) |
| 83 | } |
| 84 | |
| 85 | // Add explicit timeout to prevent hanging on slow registries |
| 86 | // Use a new context with timeout to avoid modifying the caller's context |
| 87 | timeoutCtx, cancel := context.WithTimeout(ctx, 30*time.Second) |
| 88 | defer cancel() |
| 89 | |
| 90 | // Fetch the image using anonymous authentication (public images only) |
| 91 | // The go-containerregistry library handles: |
| 92 | // - OCI auth discovery via WWW-Authenticate headers |
| 93 | // - Token negotiation for different registries |
| 94 | // - Rate limiting and retries |
| 95 | // - Multi-arch manifest resolution |
| 96 | img, err := remote.Image(ref, remote.WithAuth(authn.Anonymous), remote.WithContext(timeoutCtx)) |
| 97 | if err != nil { |
| 98 | // Check if this is a timeout error |
| 99 | if errors.Is(err, context.DeadlineExceeded) { |
| 100 | return fmt.Errorf("OCI image validation timed out after 30 seconds for '%s'. The registry may be slow or unreachable", pkg.Identifier) |
| 101 | } |
| 102 | |
| 103 | // Check for specific HTTP status codes |
| 104 | var transportErr *transport.Error |
| 105 | if errors.As(err, &transportErr) { |
| 106 | switch transportErr.StatusCode { |
| 107 | case http.StatusTooManyRequests: |
| 108 | // Fail closed: a 429 means we could not verify the |
| 109 | // `io.modelcontextprotocol.server.name` label on the image, which is |
| 110 | // the only ownership proof we apply to OCI packages. Returning nil |
| 111 | // here would let a publisher bind their server record to an arbitrary |
| 112 | // public image they do not control, which is the bug this branch |
| 113 | // used to have. Surface the rate-limit as a retryable error instead. |
searching dependent graphs…