Skip to content

OCI Install Strips Prefix on Package #1947

Open

Description

Prerequisites

  • Write a descriptive title.
  • Make sure you are able to repro it on the latest released version
  • Search the existing issues.

Steps to reproduce

When trying to install a package from an OCI registry that contains a prefix, the package prefix is removed when it goes to download the nupkg blob.

Due to #1946 I've had to add a hack that fixes up the authentication to GHCR. This uses PSNetDetour to patch the token logic. If/when that is ever fixed then this patch isn't needed to replicate this error.

$repoParams = @{
    Name = 'GHCR'
    Uri = 'https://ghcr.io/'
    ApiVersion = 'ContainerRegistry'
}
Register-PSResourceRepository @repoParams

$psGetCmd = Get-Command -Name Install-PSResource -Module Microsoft.PowerShell.PSResourceGet
$psGetType = $psGetCmd.ImplementingType.Assembly.GetType(
    'Microsoft.PowerShell.PSResourceGet.ContainerRegistryServerAPICalls')

$getRegistryTokenMeth = $psGetType.GetMethod(
    'GetContainerRegistryAccessToken',
    [Reflection.BindingFlags]'Instance, NonPublic',
    [type[]]@([bool], [bool], [System.Management.Automation.ErrorRecord].MakeByRefType()))

$sendRequestMeth = $psGetType.GetMethod(
    'GetHttpResponseJObjectUsingDefaultHeaders',
    [Reflection.BindingFlags]'Instance, NonPublic',
    [type[]]@(
        [string],
        [Net.Http.HttpMethod],
        [Collections.ObjectModel.Collection[System.Collections.Generic.KeyValuePair[string, string]]],
        [System.Management.Automation.ErrorRecord].MakeByRefType(),
        [bool]))

Use-NetDetourContext {
    # Fixes logic for getting anonymous token for GHCR
    New-NetDetourHook -Method $getRegistryTokenMeth -Hook {
        param ([bool]$NeedCatalogAccess, [bool]$IsPushOperation, [ref]$ErrorRecord)

        try {
            $registryUri = "https://ghcr.io/v2/"
            $resp = Invoke-WebRequest -Uri $registryUri -SkipHttpErrorCheck
            $bearer = $resp.Headers['WWW-Authenticate'] | Select-Object -First 1
            if (-not $bearer) {
                throw "No WWW-Authenticate found in response headers for '$registryUri'"
            }

            if ($bearer -match 'realm="([^"]+)"') {
                $realm = $matches[1]
            } else {
                throw "Could not extract realm from WWW-Authenticate header '$bearer'"
            }

            if ($bearer -match 'service="([^"]+)"') {
                $service = $matches[1]
            } else {
                throw "Could not extract service from WWW-Authenticate header '$bearer'"
            }

            $tokenUri = "${realm}?service=${service}&client_id=testclient&scope=repository:jborean93/publishtest:pull"
            $tokenResponse = Invoke-WebRequest -Uri $tokenUri -Method Get
            ($tokenResponse.Content | ConvertFrom-Json).token
        }
        catch {
            $_.ErrorDetails = "Failed to retrieve anonymous token: $_"
            $ErrorRecord.Value = $_
        }
    }

    # Prints the URIs requested to show what the problem is
    New-NetDetourHook -Method $sendRequestMeth -Hook {
        param($Url, $Method, $DefaultHeaders, $ErrRecord, $UsePagination)

         [Console]::WriteLine("$Method -> $Url")
        , $Detour.Invoke($Url, $Method, $DefaultHeaders, $ErrRecord, $UsePagination)
    }

    Install-PSResource -Name jborean93/publishtest -Repository GHCR -TrustRepository -Verbose -Debug
}

Expected behavior

Package is installed

Actual behavior

DEBUG: Parameters passed in >>> Name: 'jborean93/publishtest'; VersionRange: ''; NuGetVersion: ''; VersionType: 'NoVersion'; Version: ''; Prerelease: 'False'; Repository: 'GHCR'; AcceptLicense: 'False'; Quiet: 'False'; Reinstall: 'False'; TrustRepository: 'True'; NoClobber: 'False'; AsNupkg: 'False'; IncludeXml 'True'; SavePackage 'False'; TemporaryPath ''; SkipDependencyCheck: 'False'; AuthenticodeCheck: 'False'; PathsToInstallPkg: '/home/jborean/.local/share/powershell/Modules,/home/jborean/.local/share/powershell/Scripts'; Scope 'CurrentUser'
DEBUG: In InstallHelper::ProcessRepositories()
VERBOSE: Setting Secret Management network credentials
VERBOSE: Attempting to search for packages in 'GHCR'
DEBUG: In InstallHelper::InstallPackages()
DEBUG: In InstallHelper::InstallPackage()
DEBUG: In ContainerRegistryServerAPICalls::FindName()
DEBUG: In ContainerRegistryServerAPICalls::FindPackagesWithVersionHelper()
DEBUG: In ContainerRegistryServerAPICalls::FindContainerRegistryImageTags()                                             
GET -> https://ghcr.io/v2/jborean93/publishtest/tags/list
DEBUG: In ContainerRegistryServerAPICalls::GetHttpResponseJObjectUsingDefaultHeaders()
DEBUG: In ContainerRegistryServerAPICalls::GetPackagesWithRequiredVersion()
DEBUG: 'jborean93/publishtest' version parsed as '0.1.0'
DEBUG: 'jborean93/publishtest' version parsed as '0.1.1'
DEBUG: 'jborean93/publishtest' version parsed as '0.1.2'
DEBUG: In ContainerRegistryServerAPICalls::GetContainerRegistryMetadata()
DEBUG: In ContainerRegistryServerAPICalls::FindContainerRegistryManifest()
DEBUG: GET manifest url:  https://ghcr.io/v2/jborean93/publishtest/manifests/0.1.2
GET -> https://ghcr.io/v2/jborean93/publishtest/manifests/0.1.2
DEBUG: In ContainerRegistryServerAPICalls::GetHttpResponseJObjectUsingDefaultHeaders()
DEBUG: In ContainerRegistryServerAPICalls::GetMetadataProperty()
DEBUG: 'jborean93/publishtest' version parsed as '0.1.2'

Confirm
Are you sure you want to perform this action?
Performing the operation "Install-PSResource" on target "Package to install: 'PublishTest', version: '0.1.2'".
[Y] Yes  [A] Yes to All  [N] No  [L] No to All  [S] Suspend  [?] Help (default is "Y"): y
DEBUG: In ContainerRegistryServerAPICalls::InstallPackage()
DEBUG: In ContainerRegistryServerAPICalls::InstallVersion()
VERBOSE: Getting manifest for publishtest - 0.1.2                                                                       
DEBUG: In ContainerRegistryServerAPICalls::GetContainerRegistryRepositoryManifest()
GET -> https://ghcr.io/v2/publishtest/manifests/0.1.2
DEBUG: In ContainerRegistryServerAPICalls::GetHttpResponseJObjectUsingDefaultHeaders()
Use-NetDetourContext: Response returned error with status code BadRequest: Bad Request.
VERBOSE: Attempting to delete '/tmp/43de5d14-17ef-4a1a-9137-b0b3fa84dd3d'
VERBOSE: Successfully deleted '/tmp/43de5d14-17ef-4a1a-9137-b0b3fa84dd3d'
Use-NetDetourContext: Package(s) 'jborean93/publishtest' could not be installed from repository 'GHCR'.

We can see that

  • It is able to find the tags/versions for this package
  • It selects the latest version 0.1.2
    • GET -> https://ghcr.io/v2/jborean93/publishtest/tags/list
  • It is able to retrieve the OCI manifest through
    • GET -> https://ghcr.io/v2/jborean93/publishtest/manifests/0.1.2
  • It derives the module name through that manifest as PublishTest based on the confirmation message
  • The step to gettings the manifest after the confirmation fails with a HTTP 400
    • GET -> https://ghcr.io/v2/publishtest/manifests/0.1.2

This step is failing because the URL used no longer contains the package prefix (jborean93) and thus isn't a valid request. The code when calling InstallPackage after it has found the version now uses the module name contained in the manifest which is just PublishTest rather than the original package name provided with the prefix.

The solution to this is to not update the package name from the manifest details and just use the module name on the downloaded artifact locally.

Error details

Exception             : 
    Type    : Microsoft.PowerShell.PSResourceGet.UtilClasses.ResourceNotFoundException
    Message : Package(s) 'jborean93/publishtest' could not be installed from repository 'GHCR'.
    HResult : -2146233088
TargetObject          : Microsoft.PowerShell.PSResourceGet.Cmdlets.InstallPSResource
CategoryInfo          : InvalidData: (Microsoft.PowerShel…s.InstallPSResource:InstallPSResource) [Use-NetDetourContext], 
ResourceNotFoundException
FullyQualifiedErrorId : InstallPackageFailure,PSNetDetour.Commands.UseNetDetourContext
InvocationInfo        : 
    MyCommand        : Use-NetDetourContext
    ScriptLineNumber : 1
    OffsetInLine     : 1
    HistoryId        : 7
    Line             : Use-NetDetourContext {
                       
    Statement        : Use-NetDetourContext {
                       # Fixes logic for getting anonymous token for GHCR
                       New-NetDetourHook -Method $getRegistryTokenMeth -Hook {
                       param ([bool]$NeedCatalogAccess, [bool]$IsPushOperation, [ref]$ErrorRecord)
                       
                       try {
                       $registryUri = "https://ghcr.io/v2/"
                       $resp = Invoke-WebRequest -Uri $registryUri -SkipHttpErrorCheck
                       $bearer = $resp.Headers['WWW-Authenticate'] | Select-Object -First 1
                       if (-not $bearer) {
                       throw "No WWW-Authenticate found in response headers for '$registryUri'"
                       }
                       
                       if ($bearer -match 'realm="([^"]+)"') {
                       $realm = $matches[1]
                       } else {
                       throw "Could not extract realm from WWW-Authenticate header '$bearer'"
                       }
                       
                       if ($bearer -match 'service="([^"]+)"') {
                       $service = $matches[1]
                       } else {
                       throw "Could not extract service from WWW-Authenticate header '$bearer'"
                       }
                       
                       $tokenUri = "${realm}?service=${service}&client_id=testclient&scope=repository:jborean93/publishtest:pull"
                       $tokenResponse = Invoke-WebRequest -Uri $tokenUri -Method Get
                       ($tokenResponse.Content | ConvertFrom-Json).token
                       }
                       catch {
                       $_.ErrorDetails = "Failed to retrieve anonymous token: $_"
                       $ErrorRecord.Value = $_
                       }
                       } -State @{ Name = $Name }
                       
                       Install-PSResource -Name jborean93/publishtest -Repository GHCR -TrustRepository -Verbose -Debug
                       }
    PositionMessage  : At line:1 char:1
                       + Use-NetDetourContext {
                       + ~~~~~~~~~~~~~~~~~~~~~~
    InvocationName   : Use-NetDetourContext
    CommandOrigin    : Internal
ScriptStackTrace      : at <ScriptBlock>, <No file>: line 36
                        at <ScriptBlock>, <No file>: line 1
                        at <ScriptBlock>, <No file>: line 1
PipelineIterationInfo : 
      0
      1

Environment data

ModuleType Version    PreRelease Name                                ExportedCommands
---------- -------    ---------- ----                                ----------------
Binary     1.2.0      rc3        Microsoft.PowerShell.PSResourceGet  {Compress-PSResource, Find-PSResource, Get-InstalledPSResource, G…


Name                           Value
----                           -----
PSVersion                      7.5.1
PSEdition                      Core
GitCommitId                    7.5.1
OS                             Fedora Linux 43 (Server Edition)
Platform                       Unix
PSCompatibleVersions           {1.0, 2.0, 3.0, 4.0…}
PSRemotingProtocolVersion      2.3
SerializationVersion           1.1.0.1
WSManStackVersion              3.0

Visuals

No response

Activity

  1. jborean93 commented on Feb 20, 2026

    @jborean93
    Author

    This and the authentication issues are the only 2 blockers that stop PSResourceGet from installing packages from other OCI registries like GHCR. I've been tracking the progress and other general information around this at https://github.com/jborean93/PowerShell-PublishTest?tab=readme-ov-file#github-container-registry-oci.

  2. jborean93 commented on Feb 22, 2026

    @jborean93
    Author

    I've tried to write up more around the problem and the potential solutions for this issue. I'm happy to do the work it would just need a consensus on the way forward before I do so.

    OCI Registry Package Prefixes: Current Issues and Proposed Solutions

    Summary

    PSResourceGet cannot properly install packages with a prefix before the module name. This prefix may be required by the OCI registry (like GHCR) or just published with that prefix. This document outlines:

    • The Problem: Can find packages at ghcr.io/user/packagename but then tries to download from ghcr.io/packagename
    • Root Cause: OCI manifests store the module name without the namespace prefix, but PSResourceGet needs the full path for API operations
    • Current Workaround: MAR has a hardcoded psresource/ prefix, but this doesn't scale to other registries
    • Potential Solutions:
      • Parse prefix from requested package name Install-PSResource -Name ghuser/package (simplest UX wise)
      • Add -Prefix parameter to Register-PSResourceRepository (requires multiple repos per namespace)
      • Add -Prefix to other cmdlets like Install-PSResource rather than a per repository configuration

    Glossary

    OCI Registry Namespacing Concepts

    • Root Namespace - Packages directly under the registry (e.g., myacr.azurecr.io/packagename)

      • Some registries allow this (ACR)
      • Some registries prohibit this (GHCR)
    • Namespace/Prefix - Organizational grouping before the package name

      • Examples: ghcr.io/username/packagename, docker.io/organization/imagename
      • Acts as a scope or owner identifier
      • Required by GHCR and possibly others
    • Registry Path - The full path used in OCI API calls

      • Format: {namespace}/{packagename} or just {packagename} for root namespace
      • Example: In ghcr.io/ghuser/mymodule, the repository path is ghuser/mymodule
    • Module Name - The PowerShell module name from the manifest (.psd1)

      • Stored in OCI manifest under the nupkg layer annotation as org.opencontainers.image.title
      • Does NOT include namespace prefix
      • Example: MyModule (not ghuser/MyModule)
      • Dependencies will also not include any prefix

    Key Registries and Their Namespace Requirements

    Registry Root Namespace Allowed? Example Path
    MAR (Microsoft Artifact Registry) ✅ Yes (pwsh res under psresource/ by design) mcr.microsoft.com/psresource/packagename
    ACR (Azure Container Registry) ✅ Yes (optional namespaces) myregistry.azurecr.io/packagename or
    myregistry.azurecr.io/namespace/packagename
    GHCR (GitHub Container Registry) ❌ No - Requires user/org namespace ghcr.io/username/packagename

    Current Behavior

    What Works Today

    Microsoft Artifact Registry (MAR):

    # PSResourceGet has hardcoded support for MAR's psresource/ prefix
    Install-PSResource -Name MyModule -Repository MAR
    # Internally translates to: mcr.microsoft.com/psresource/mymodule

    This works because:

    1. MAR always uses the psresource/ prefix for PowerShell resources
    2. Code specifically checks for MAR URI and prepends the prefix
    3. See PrependMARPrefix() in ContainerRegistryServerAPICalls.cs

    Azure Container Registry (ACR) with no prefix:

    Install-PSResource -Name MyModule -Repository MyACR
    # Interally translates to: myacr.azurecr.io/mymodule

    What Breaks

    GitHub Container Registry (GHCR):

    # Try to install (FAILS)
    Install-PSResource -Name 'ghuser/MyModule' -Repository GHCR
    
    # What happens internally:
    # 1. Find operation: GET ghcr.io/v2/ghuser/mymodule/tags/list ✅ Works
    # 2. Get manifest: GET ghcr.io/v2/ghuser/mymodule/manifests/1.0.0 ✅ Works
    # 3. Read manifest, extracts module name: "MyModule" (prefix stripped)
    # 4. Download blob: GET ghcr.io/v2/mymodule/blobs/sha256:... ❌ FAILS
    #    Should be: GET ghcr.io/v2/ghuser/mymodule/blobs/sha256:...

    Azure Container Registry (ACR) published under explicit prefix:

    Install-PSResource -Name testprefix/publishtest -Repository MyACR

    Same problem occurs where it can find the package tags and manifest but then strips off the package during the install stage.

    Root Cause Analysis

    The OCI Manifest Problem

    When PSResourceGet retrieves the OCI manifest, it extracts the module name from the org.opencontainers.image.title annotation:

    {
      "schemaVersion": 2,
      "layers": [{
        "digest": "sha256:abc123...",
        "annotations": {
          "org.opencontainers.image.title": "MyModule",
          "resourceType": "Nupkg"
        }
      }]
    }

    The Download Problem

    For subsequent operations (downloading blobs, pulling dependencies), PSResourceGet uses the module name from the manifest. But OCI API calls need the full repository path:

    # What PSResourceGet does:
    GET /v2/mymodule/blobs/sha256:abc123
    
    # What it should do:
    GET /v2/ghuser/mymodule/blobs/sha256:abc123
    

    The Dependency Problem

    Dependencies listed in the module manifest don't include namespace prefixes:

    # MyModule.psd1
    @{
        ModuleName = 'MyModule'
        RequiredModules = @('DependencyModule')  # No prefix!
    }

    When installing ghuser/MyModule, PSResourceGet needs to find ghuser/DependencyModule, but the dependency is just listed as DependencyModule.

    Proposed Solutions

    Solution 1: Parse Prefix from Package Name

    Concept: When a user provides a package name with a /, treat the prefix portion as a namespace to use for all operations.

    Implementation:

    Install-PSResource -Name 'ghuser/MyModule' -Repository GHCR
    
    # Internally:
    # 1. Parse: prefix = 'ghuser', moduleName = 'MyModule'
    # 2. Find: ghcr.io/ghuser/mymodule ✅
    # 3. Download: ghcr.io/ghuser/mymodule ✅
    # 4. Dependencies: ghcr.io/ghuser/{dependencyname} ✅

    Code changes:

    • Parse package name on entry to install/find operations
    • Store prefix in context passed through the operation
    • Prepend prefix to all OCI API calls (tags, manifests, blobs)
    • Prepend prefix when resolving dependencies

    Pros:

    • ✅ Minimal breaking changes
    • ✅ No new parameters required
    • ✅ Intuitive user experience (name matches registry path)
    • ✅ Works immediately without repository registration per namespace/prefix

    Cons:

    • ❌ Only works when user explicitly includes prefix in name
    • ❌ User must know the namespace structure
    • ❌ MAR still has the internal code to automatically add the psresource/ prefix

    Example usage:

    # Works - explicit prefix
    Install-PSResource -Name 'ghuser/MyModule' -Repository GHCR
    
    # Doesn't work - find without knowing prefix
    Find-PSResource -Name 'MyModule' -Repository GHCR
    # How does it know to look in ghuser/ vs other users?

    Solution 2: Repository-Level Prefix Configuration

    Concept: Add a -Prefix/-ResourcePrefix/-ModulePrefix parameter to Register-PSResourceRepository that gets prepended to all package operations.

    Implementation:

    # Register repository with prefix
    Register-PSResourceRepository -Name MyGHCR -Uri 'https://ghcr.io' -Prefix 'ghuser'
    
    # Use without specifying prefix in name
    Install-PSResource -Name MyModule -Repository MyGHCR
    # Internally uses: ghcr.io/ghuser/mymodule

    Code changes:

    • Add a Prefix property to PSRepositoryInfo
    • Update Register-PSResourceRepository to accept -Prefix parameter
    • Modify all OCI API calls to prepend prefix from repository info
    • Remove the MAR specific checks and just have the psresource prefix added as part of the repository registration
    • Obsoletes -ModulePrefix in Publish-PSResource as the prefix can be derived from the repo registration instead

    Pros:

    • ✅ Works for all operations (install, publish)
    • ✅ User doesn't need to specify prefix in package names during install
    • ✅ Explicit configuration - clear what namespace is being used
    • ✅ Supports both prefixed and non-prefixed registries
    • ✅ Removes hardcoding of MAR in ContainerRegistryServerAPICalls.cs and instead makes it a repository configuration

    Cons:

    • ❌ Requires separate repository registration for each namespace even if for the same registry
    • ❌ A disconnect between what people see the package name as online vs what they use with PSResourceGet
    • ❌ Find-PSResource uses the /v2/_catalog API which does not support server side filtering, it would have to filter it in the cmdlet
    • ❌ Still have an overlapping functionality of Publish-PSResource -ModulePrefix and repository prefix configuration

    Example usage:

    # Register for your namespace
    Register-PSResourceRepository -Name MyGHCR -Uri 'https://ghcr.io' -Prefix 'ghuser'
    
    # Register for organization namespace
    Register-PSResourceRepository -Name OrgGHCR -Uri 'https://ghcr.io' -Prefix 'myorg'
    
    # Clean usage - no prefix in names
    Install-PSResource -Name MyModule -Repository MyGHCR
    Publish-PSResource -Path ./MyModule -Repository MyGHCR

    Solution 3: Add -Prefix Parameter for Install-PSResource

    Concept: Keep -Name as the module name but add -Prefix parameter to specify a prefix on Install-PSResource

    Implementation:

    Install-PSResource -Name MyModule -Prefix ghuser -Repository GHCR
    # All operations will use the package `ghuser/mymodule`
    # Prefix will apply to any dependencies found for that module as well

    Code changes:

    • Add -Prefix parameter to Install-PSResource
    • Prepend prefix to the package name with prefix/packagename for any API requests

    Pros:

    • ✅ Automatic - works without extra configuration
    • ✅ Mirrors the existing -ModulePrefix exposed in Publish-PSResource
    • ✅ No extra parsing logic on the name
    • ✅ No repository re-registration needed

    Cons:

    • ❌ Messier to have the user split the prefix themselves
    • ❌ Still keeps the hardcoded internal logic for MAR to be auto prefixed

    Dependency Resolution Strategy

    None of these solutions support finding dependencies in other repositories. I don't think this should be solved as dependencies for a package should all be contained in the same registry/namespace.

    Migration Path for MAR

    Current hardcoded MAR behavior should be preserved for backwards compatibility but instead of the prefix work being done in ContainerRegistryServerAPICalls.cs the prefix could be applied using the same mechanisms proposed in the various solutions just in an automatic way. There would be no changes for the end user here just how it is dealt with internally.

    Comparison Matrix

    Feature Solution 1:
    Parse Name
    Solution 2:
    Repo Prefix
    Solution 3:
    Prefix Parameter
    Backwards compatible ✅ Yes ✅ Yes ✅ Yes
    User complexity 🟢 Low 🟡 Medium 🟡 Medium
    Implementation complexity 🟢 Low 🟡 Medium 🟢 Low

    Recommended Solution

    I would recommend either option 1 so that the cmdlets favour less UX complexity with a slight tradeoff in code complexity. Option 3 as a second option if that trade off isn't desired.

    From an end user experience if they see a package in a repository as namespace/module then they would expect to be able to use that as the -Name rather than having to know that the prefix is some OCI specific configuration that needs to be handled separately.

    Example User Experience

    After implementation, users could work with GHCR or other OCI registry packages with a prefix like this:

    # One-time setup: Register GHCR
    Register-PSResourceRepository -Name GHCR -Uri 'https://ghcr.io' -ApiVersion ContainerRegistry
    
    # Publishing (prefix as parameter)
    Publish-PSResource -Path ./MyModule -Repository GHCR -ModulePrefix ghuser
    # Publishes to: ghcr.io/ghuser/mymodule
    
    # Installing (prefix in name)
    Install-PSResource -Name ghuser/MyModule -Repository GHCR
    # Installs from: ghcr.io/ghuser/mymodule
    
    # Finding packages in registry
    # /v2/_catalog doesn't allow filtering in the API so would have to be done
    # either in the cmdlet based on the results or with Where-Object
    Find-PSResource -Repository GHCR
    # Searches: ghcr.io/*
    # Would eventually return something with ghuser/mymodule
    
    # Dependencies resolved automatically
    # MyModule depends on DependencyModule
    Install-PSResource -Name ghuser/MyModule -Repository GHCR
    # Also installs: ghcr.io/ghuser/dependencymodule
  3. SydneyhSmith commented on Aug 13, 2026

    @SydneyhSmith
    Collaborator

    Thanks Jordan Borean (@jborean93) let's revisit this after the ORAS PR goes in

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions