Skip to content

Latest commit

 

History

History
413 lines (311 loc) · 15.3 KB

File metadata and controls

413 lines (311 loc) · 15.3 KB

PSPublishModule Module Isolation

Import-IsolatedModule imports known dependency-sensitive PowerShell modules through curated PowerForge isolation profiles.

The feature is intended for PowerShell 7+ sessions where one service module's binary dependencies cannot safely share the default load context with another module or host. Instead of trying to control import order in the default context, PowerForge copies the profiled module to a temporary workspace, patches the configured script or manifest surface, loads selected assemblies through a module-scoped AssemblyLoadContext, and imports the generated wrapper into the current session.

This is a maintained-profile model, not a generic arbitrary-module loader. Each supported module has explicit rules for what to copy, patch, preload, import, and expose.

Quick Start

Import the module that owns Import-IsolatedModule, then import one or more supported profiles:

Import-Module PSPublishModule

$exo = Import-IsolatedModule -Profile ExchangeOnlineManagement -PassThru
$teams = Import-IsolatedModule -Profile MicrosoftTeams -PassThru
$graph = Import-IsolatedModule -Profile MicrosoftGraphAuthentication -PassThru

$exo, $teams, $graph |
    Format-Table ProfileName, ModuleName, ContextName, IsolatedImportPath

After the isolated import, use the profiled module's normal commands:

Connect-ExchangeOnline -ShowBanner:$false
Connect-MicrosoftTeams
Connect-MgGraph -NoWelcome

The isolated import only prepares and imports the command surface. It does not authenticate to Exchange, Teams, Graph, or any other service.

Built-In Profiles

ExchangeOnlineManagement

Import-IsolatedModule -Profile ExchangeOnlineManagement
Connect-ExchangeOnline -ShowBanner:$false

Get-ConnectionInformation |
    Format-List UserPrincipalName, ConnectionUri, ModuleName

Get-EXOMailbox -ResultSize 5 |
    Select-Object DisplayName, PrimarySmtpAddress

Disconnect-ExchangeOnline -Confirm:$false

The profile currently:

  • resolves ExchangeOnlineManagement from PSModulePath when -Path is omitted,
  • requires ExchangeOnlineManagement 3.9.0 or newer,
  • copies the module into %TEMP%\PowerForge\IsolatedModules\ExchangeOnlineManagement unless -WorkRoot is supplied,
  • patches netCore\ExchangeOnlineManagement.psm1,
  • patches the copied ExchangeOnlineManagement.psd1 manifest so EXO keeps its upstream module name, version, and export metadata,
  • loads Microsoft.Exchange.Management.RestApiClient.dll and Microsoft.Exchange.Management.ExoPowershellGalleryModule.dll through the same ExchangeOnlineManagement.ALC context,
  • exposes public types from Microsoft.Exchange.Management.* and Microsoft.Online.CSE.RestApiPowerShellModule.* as type accelerators so EXO script type literals keep working.

MicrosoftTeams

Import-IsolatedModule -Profile MicrosoftTeams
Connect-MicrosoftTeams

Get-Team |
    Select-Object -First 10 DisplayName, GroupId, Visibility

Disconnect-MicrosoftTeams

The profile currently:

  • resolves MicrosoftTeams from PSModulePath when -Path is omitted,
  • requires MicrosoftTeams 7.8.0 or newer,
  • copies the module into %TEMP%\PowerForge\IsolatedModules\MicrosoftTeams unless -WorkRoot is supplied,
  • generates MicrosoftTeams.ALC.psm1 and a patched MicrosoftTeams.ALC.psd1 manifest that preserves the upstream export contract,
  • loads the Teams connect, Teams cmdlet, policy administration, and ConfigAPI binary surfaces through the same MicrosoftTeams.ALC context,
  • rewrites selected copied submodule script imports so they load binary modules through the shared context,
  • imports the required Teams submodule manifests after the isolated binary load,
  • exposes public types from Microsoft.Teams.* as type accelerators.

MicrosoftGraphAuthentication

Import-IsolatedModule -Profile MicrosoftGraphAuthentication
Connect-MgGraph -NoWelcome

Get-MgContext |
    Format-List Account, TenantId, Scopes

Disconnect-MgGraph

The profile currently:

  • resolves Microsoft.Graph.Authentication from PSModulePath when -Path is omitted,
  • requires Microsoft.Graph.Authentication 2.36.0 or newer,
  • copies the module into %TEMP%\PowerForge\IsolatedModules\MicrosoftGraphAuthentication unless -WorkRoot is supplied,
  • generates Microsoft.Graph.Authentication.ALC.psm1 and a patched Microsoft.Graph.Authentication.ALC.psd1 manifest,
  • clears copied NestedModules so Graph binaries are not imported through the default loader before the isolated wrapper runs,
  • loads Graph, Kiota, MSAL, Azure.Identity, and supporting dependency assemblies into Microsoft.Graph.Authentication.ALC,
  • imports Microsoft.Graph.Authentication.Core.dll and Microsoft.Graph.Authentication.dll through the same isolated context,
  • removes copied Authenticode signature blocks from the patched script because the generated wrapper necessarily changes the original script,
  • replaces Graph's path-based binary command discovery with explicit cmdlet and alias exports for the supported command surface,
  • exposes public types from Microsoft.Graph.* as type accelerators.

Path Resolution

When -Path is omitted, the command resolves the profile's module name with:

Get-Module -ListAvailable -Name <profile module name>

If several versions are visible on PSModulePath, the highest version is selected.

Use -Path when you want a specific installed copy, a saved module payload, or an alternate manifest name in the module base:

$module = Get-Module -ListAvailable ExchangeOnlineManagement |
    Sort-Object Version -Descending |
    Select-Object -First 1

Import-IsolatedModule -Profile ExchangeOnlineManagement -Path $module.ModuleBase

Directory paths are treated as module bases:

Import-IsolatedModule `
    -Profile MicrosoftGraphAuthentication `
    -Path 'C:\Modules\Microsoft.Graph.Authentication\2.37.0'

Manifest paths are accepted. The manifest file is used for version validation and manifest patching, while its parent directory is treated as the module base:

Import-IsolatedModule `
    -Profile MicrosoftGraphAuthentication `
    -Path 'C:\Modules\Microsoft.Graph.Authentication\2.37.0\Microsoft.Graph.Authentication.psd1'

Alternate manifest names are also supported when the manifest lives beside the module payload that the profile expects:

Import-IsolatedModule `
    -Profile MicrosoftTeams `
    -Path 'C:\Lab\ContosoTeams\7.9.0\ContosoTeams.psd1'

In that example, C:\Lab\ContosoTeams\7.9.0 is the module base. The profile still expects the configured Teams script and binary layout below that directory. A manifest stored away from the module payload is not enough by itself; the parent directory of the manifest must contain the module files that the selected profile knows how to patch.

Non-manifest file paths are resolved to their parent directory. The service then tries to find a manifest for validation and patching from the profile's configured manifest path, then from a single .psd1 in the module base, then from <moduleBase>\<moduleBaseName>.psd1.

Preferred Module Resolution

By default, Import-IsolatedModule imports the generated wrapper but does not change PSModulePath. That keeps the isolated import narrow: commands are available in the current runspace, but later module-name resolution can still find the originally installed module first.

Use -PreferIsolatedModulePath when a downstream module imports the profiled module by name and should bind to the generated isolated copy:

Import-Module Az.Storage

$exo = Import-IsolatedModule `
    -Profile ExchangeOnlineManagement `
    -PreferIsolatedModulePath `
    -PassThru

Connect-ExchangeOnline -ShowBanner:$false

Import-Module Contoso.ExchangeWorker
Invoke-ContosoExchangeWorker

$exo |
    Format-List ProfileName, IsolatedModuleResolutionPath, PreferIsolatedModulePath

The switch prepends the generated profile work path to process-scoped PSModulePath after the isolated import succeeds. In the Exchange example, a later Import-Module ExchangeOnlineManagement by name can resolve the generated copy under %TEMP%\PowerForge\IsolatedModules\ExchangeOnlineManagement\<guid> before the original installed module. Profiles that use an ALC-specific manifest name, such as MicrosoftTeams.ALC.psd1, also write a module-name manifest in the generated copy so PowerShell name-based resolution still points at the patched ALC root module.

Use this only when you need that resolution behavior. It is intentionally opt-in because it changes module-name resolution for the whole current PowerShell process:

  • Get-Module -ListAvailable <profile module> may show the generated copy first,
  • the most recently prepended isolated import wins when several generated copies exist,
  • explicit file-path imports still use the path provided by the caller,
  • the change is process-scoped and ends when the PowerShell process exits.

Validation Behavior

Import-IsolatedModule fails before importing when a required contract is missing. Use Test-IsolatedModuleProfile to run the same profile resolution and preflight path checks without copying or importing the generated wrapper.

Runtime validation:

  • Windows PowerShell 5.1 is rejected because AssemblyLoadContext requires PowerShell 7+ on CoreCLR.

Profile validation:

  • unknown profile names fail and list the available profiles,
  • profile names are resolved case-insensitively.

Module discovery validation:

  • without -Path, the module must be visible through PSModulePath,
  • with -Path, the supplied file or directory must exist,
  • if the profile declares a minimum version, a parseable module manifest with ModuleVersion must be available,
  • the resolved version must be greater than or equal to the profile minimum.

Profile-layout validation:

  • the configured source script path must exist under the module base,
  • profiles that preserve a manifest must have a source manifest available,
  • the source manifest must contain a RootModule entry that can be patched,
  • profile-declared dependency assemblies and binary module imports must exist relative to the profiled script module directory,
  • copied script binary imports and additional required files must exist relative to the module base.

Work-root behavior:

  • if -WorkRoot is omitted, the generated copy is created under %TEMP%\PowerForge\IsolatedModules\<profile>\<guid>,
  • if -WorkRoot is supplied, the directory is created when missing,
  • each import uses a new GUID child folder,
  • generated copies can remain locked until the PowerShell process exits.

Inspecting Results

Validate a profile source before importing:

$validation = Test-IsolatedModuleProfile -Profile MicrosoftGraphAuthentication

$validation | Format-List ProfileName, ModuleName, SourceModuleBase, IsValid
$validation.Paths | Format-Table Category, RelativePath, Exists
$validation.Issues | Format-Table Severity, Category, Message

Return only a Boolean result:

Test-IsolatedModuleProfile -Profile MicrosoftTeams -Quiet

Use -PassThru to get the generated import details:

$result = Import-IsolatedModule -Profile MicrosoftGraphAuthentication -PassThru

$result |
    Format-List ProfileName, ModuleName, SourceModuleBase, ContextName,
        IsolatedImportPath, IsolatedScriptPath, IsolatedManifestPath, WorkPath

Inspect the exported commands:

Get-Command -Module Microsoft.Graph.Authentication.ALC |
    Sort-Object CommandType, Name |
    Format-Table CommandType, Name, ModuleName

Inspect assemblies loaded into the profile context:

[System.Runtime.Loader.AssemblyLoadContext]::All |
    Where-Object Name -eq $result.ContextName |
    ForEach-Object {
        $_.Assemblies |
            Sort-Object { $_.GetName().Name } |
            Select-Object @{Name = 'Name'; Expression = { $_.GetName().Name } },
                @{Name = 'Version'; Expression = { $_.GetName().Version } },
                Location
    }

Confirm that a profile did not place Graph assemblies in the default context:

[System.Runtime.Loader.AssemblyLoadContext]::Default.Assemblies |
    Where-Object { $_.GetName().Name -like 'Microsoft.Graph*' } |
    Select-Object @{Name = 'Name'; Expression = { $_.GetName().Name } }, Location

Diagnostics With WorkRoot

Use -WorkRoot when you want deterministic generated files for inspection:

$workRoot = 'C:\Temp\PowerForge-Isolated'
$result = Import-IsolatedModule `
    -Profile MicrosoftTeams `
    -WorkRoot $workRoot `
    -PassThru

Get-ChildItem -LiteralPath $result.WorkPath -Recurse |
    Select-Object -First 40 FullName

Get-Content -LiteralPath $result.IsolatedScriptPath -TotalCount 80

Use -WhatIf to preview without creating or importing the generated wrapper:

Import-IsolatedModule -Profile ExchangeOnlineManagement -WhatIf

Multiple Profiles In One Session

Supported profiles can be imported in the same PowerShell 7+ process:

$profiles = foreach ($profile in @(
    'ExchangeOnlineManagement',
    'MicrosoftTeams',
    'MicrosoftGraphAuthentication'
)) {
    Import-IsolatedModule -Profile $profile -PassThru
}

$profiles |
    Format-Table ProfileName, ModuleName, ContextName

Each profile uses its own context name. PowerShell command names are still global in the runspace, so normal command-name conflicts can still occur if two modules export the same command. The isolation layer is about assembly loading, not command renaming.

Profile Design

Profiles are maintained in PowerForge so support is explicit and testable. A profile declares:

  • module name and minimum supported version,
  • source script path and generated script name,
  • optional manifest path and generated manifest name when the upstream export contract should be preserved,
  • whether copied manifests should clear NestedModules,
  • whether the generated wrapper appends the source script body,
  • number of original bootstrap lines to replace when source script content is used,
  • source line fragments to skip when appending the original script body,
  • whether copied Authenticode signature blocks should be removed from patched scripts,
  • additional profile-maintained script lines to append after isolated binary loading,
  • dependency assemblies to load into the context without importing as PowerShell modules,
  • binary assemblies to import as PowerShell modules through the isolated context,
  • copied script-module binary imports that should be rewritten to use the shared context,
  • namespace prefixes to bridge into PowerShell type resolution,
  • stable load-context name.

This keeps the cmdlet surface small while allowing future profiles to add module-specific patching rules without duplicating loader code.

Current Limitations

  • PowerShell 7+ only. Windows PowerShell cannot use AssemblyLoadContext.
  • Profiles are curated; arbitrary third-party modules are not automatically isolated.
  • The generated wrapper imports commands into the current runspace; command names are not namespaced or renamed.
  • Generated module copies are process-local working artifacts and can remain locked until the PowerShell process exits.
  • -PreferIsolatedModulePath changes process-scoped PSModulePath only after a successful import and does not prevent explicit imports of another module path.
  • Assembly isolation does not isolate service authentication state or remote service state.