From 5e45a115e11fa8f14c7ba9fb0696a822d4d72137 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 25 Jan 2026 16:37:12 +0000 Subject: [PATCH 01/14] Initial plan From b83e5d2fe137e678ade66e0ac854dcf0d7c4ab5c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 25 Jan 2026 16:44:49 +0000 Subject: [PATCH 02/14] Add stability tests and capability rename implementation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Added StabilityContract.Tests.ps1 to enforce v1.0 API surface - Added internal module import warnings with IDLE_ALLOW_INTERNAL_IMPORT bypass - Implemented capability rename: IdLE.Mailbox.Read → IdLE.Mailbox.Info.Read - Added deprecation mapping with warnings during planning - Updated provider, step metadata, and tests to use new capability ID - Added CapabilityDeprecation.Tests.ps1 to validate migration behavior - Created New-IdleAuthSessionBroker wrapper in IdLE module Co-authored-by: blindzero <13959569+blindzero@users.noreply.github.com> --- src/IdLE.Core/IdLE.Core.psm1 | 5 + src/IdLE.Core/Public/New-IdlePlanObject.ps1 | 55 ++++++++- .../Public/New-IdleExchangeOnlineProvider.ps1 | 2 +- src/IdLE.Steps.Common/IdLE.Steps.Common.psm1 | 5 + .../Public/Get-IdleStepMetadataCatalog.ps1 | 8 +- src/IdLE/IdLE.psm1 | 3 +- src/IdLE/Public/New-IdleAuthSessionBroker.ps1 | 70 +++++++++++ tests/CapabilityDeprecation.Tests.ps1 | 88 ++++++++++++++ .../ExchangeOnlineProvider.Tests.ps1 | 2 +- tests/StabilityContract.Tests.ps1 | 114 ++++++++++++++++++ 10 files changed, 343 insertions(+), 9 deletions(-) create mode 100644 src/IdLE/Public/New-IdleAuthSessionBroker.ps1 create mode 100644 tests/CapabilityDeprecation.Tests.ps1 create mode 100644 tests/StabilityContract.Tests.ps1 diff --git a/src/IdLE.Core/IdLE.Core.psm1 b/src/IdLE.Core/IdLE.Core.psm1 index 969fd60f..80dea9da 100644 --- a/src/IdLE.Core/IdLE.Core.psm1 +++ b/src/IdLE.Core/IdLE.Core.psm1 @@ -2,6 +2,11 @@ Set-StrictMode -Version Latest +# Internal module warning: discourage direct import unless explicitly allowed +if (-not $env:IDLE_ALLOW_INTERNAL_IMPORT) { + Write-Warning "IdLE.Core is an internal/unsupported module. Import 'IdLE' instead for the supported public API. To bypass this warning, set IDLE_ALLOW_INTERNAL_IMPORT=1." +} + $PublicPath = Join-Path -Path $PSScriptRoot -ChildPath 'Public' $PrivatePath = Join-Path -Path $PSScriptRoot -ChildPath 'Private' diff --git a/src/IdLE.Core/Public/New-IdlePlanObject.ps1 b/src/IdLE.Core/Public/New-IdlePlanObject.ps1 index 4956ae01..2cf2f6d5 100644 --- a/src/IdLE.Core/Public/New-IdlePlanObject.ps1 +++ b/src/IdLE.Core/Public/New-IdlePlanObject.ps1 @@ -169,7 +169,8 @@ function New-IdlePlanObject { ) } - $normalized += $s + # Normalize deprecated capabilities + $normalized += ConvertTo-IdleNormalizedCapability -Capability $s } return @($normalized | Sort-Object -Unique) @@ -217,6 +218,45 @@ function New-IdlePlanObject { return @($Providers) } + function ConvertTo-IdleNormalizedCapability { + <# + .SYNOPSIS + Normalizes capability identifiers and maps deprecated IDs to current ones. + + .DESCRIPTION + Handles capability ID migrations and deprecation warnings during planning. + Pre-1.0 deprecated capability IDs are mapped to their replacements and emit a warning. + + .PARAMETER Capability + The raw capability identifier to normalize. + + .OUTPUTS + Normalized capability identifier (string). + #> + [CmdletBinding()] + param( + [Parameter(Mandatory)] + [ValidateNotNullOrEmpty()] + [string] $Capability + ) + + # Deprecated capability ID mappings (pre-1.0) + # Format: @{ 'OldID' = 'NewID' } + $deprecatedMappings = @{ + 'IdLE.Mailbox.Read' = 'IdLE.Mailbox.Info.Read' + } + + $normalized = $Capability.Trim() + + if ($deprecatedMappings.ContainsKey($normalized)) { + $newId = $deprecatedMappings[$normalized] + Write-Warning "DEPRECATED: Capability '$normalized' is deprecated and will be removed in a future major version. Use '$newId' instead." + return $newId + } + + return $normalized + } + function Get-IdleProviderCapabilities { <# .SYNOPSIS @@ -242,7 +282,18 @@ function New-IdlePlanObject { if ($null -eq $caps) { return @() } - return @($caps | Where-Object { $null -ne $_ } | ForEach-Object { ([string]$_).Trim() } | Where-Object { -not [string]::IsNullOrWhiteSpace($_) } | Sort-Object -Unique) + return @( + $caps | + Where-Object { $null -ne $_ } | + ForEach-Object { + $rawCap = ([string]$_).Trim() + if (-not [string]::IsNullOrWhiteSpace($rawCap)) { + ConvertTo-IdleNormalizedCapability -Capability $rawCap + } + } | + Where-Object { -not [string]::IsNullOrWhiteSpace($_) } | + Sort-Object -Unique + ) } return @() diff --git a/src/IdLE.Provider.ExchangeOnline/Public/New-IdleExchangeOnlineProvider.ps1 b/src/IdLE.Provider.ExchangeOnline/Public/New-IdleExchangeOnlineProvider.ps1 index e949bb59..bf57fae4 100644 --- a/src/IdLE.Provider.ExchangeOnline/Public/New-IdleExchangeOnlineProvider.ps1 +++ b/src/IdLE.Provider.ExchangeOnline/Public/New-IdleExchangeOnlineProvider.ps1 @@ -150,7 +150,7 @@ function New-IdleExchangeOnlineProvider { $provider | Add-Member -MemberType ScriptMethod -Name GetCapabilities -Value { $caps = @( - 'IdLE.Mailbox.Read' + 'IdLE.Mailbox.Info.Read' 'IdLE.Mailbox.Type.Ensure' 'IdLE.Mailbox.OutOfOffice.Ensure' ) diff --git a/src/IdLE.Steps.Common/IdLE.Steps.Common.psm1 b/src/IdLE.Steps.Common/IdLE.Steps.Common.psm1 index 5211dfbd..1f203a46 100644 --- a/src/IdLE.Steps.Common/IdLE.Steps.Common.psm1 +++ b/src/IdLE.Steps.Common/IdLE.Steps.Common.psm1 @@ -1,6 +1,11 @@ #requires -Version 7.0 Set-StrictMode -Version Latest +# Internal module warning: discourage direct import unless explicitly allowed +if (-not $env:IDLE_ALLOW_INTERNAL_IMPORT) { + Write-Warning "IdLE.Steps.Common is an internal/unsupported module. Import 'IdLE' instead for the supported public API. To bypass this warning, set IDLE_ALLOW_INTERNAL_IMPORT=1." +} + $PrivatePath = Join-Path -Path $PSScriptRoot -ChildPath 'Private' if (Test-Path -Path $PrivatePath) { diff --git a/src/IdLE.Steps.Mailbox/Public/Get-IdleStepMetadataCatalog.ps1 b/src/IdLE.Steps.Mailbox/Public/Get-IdleStepMetadataCatalog.ps1 index 8d60b21d..efc17e47 100644 --- a/src/IdLE.Steps.Mailbox/Public/Get-IdleStepMetadataCatalog.ps1 +++ b/src/IdLE.Steps.Mailbox/Public/Get-IdleStepMetadataCatalog.ps1 @@ -19,7 +19,7 @@ function Get-IdleStepMetadataCatalog { .EXAMPLE $metadata = Get-IdleStepMetadataCatalog $metadata['IdLE.Step.Mailbox.GetInfo'].RequiredCapabilities - # Returns: @('IdLE.Mailbox.Read') + # Returns: @('IdLE.Mailbox.Info.Read') #> [CmdletBinding()] param() @@ -28,17 +28,17 @@ function Get-IdleStepMetadataCatalog { # IdLE.Step.Mailbox.GetInfo - read mailbox details $catalog['IdLE.Step.Mailbox.GetInfo'] = @{ - RequiredCapabilities = @('IdLE.Mailbox.Read') + RequiredCapabilities = @('IdLE.Mailbox.Info.Read') } # IdLE.Step.Mailbox.Type.Ensure - idempotent mailbox type conversion $catalog['IdLE.Step.Mailbox.Type.Ensure'] = @{ - RequiredCapabilities = @('IdLE.Mailbox.Read', 'IdLE.Mailbox.Type.Ensure') + RequiredCapabilities = @('IdLE.Mailbox.Info.Read', 'IdLE.Mailbox.Type.Ensure') } # IdLE.Step.Mailbox.OutOfOffice.Ensure - idempotent Out of Office configuration $catalog['IdLE.Step.Mailbox.OutOfOffice.Ensure'] = @{ - RequiredCapabilities = @('IdLE.Mailbox.Read', 'IdLE.Mailbox.OutOfOffice.Ensure') + RequiredCapabilities = @('IdLE.Mailbox.Info.Read', 'IdLE.Mailbox.OutOfOffice.Ensure') } return $catalog diff --git a/src/IdLE/IdLE.psm1 b/src/IdLE/IdLE.psm1 index b3c51e68..cc2342fb 100644 --- a/src/IdLE/IdLE.psm1 +++ b/src/IdLE/IdLE.psm1 @@ -93,5 +93,6 @@ Export-ModuleMember -Function @( 'New-IdleLifecycleRequest', 'New-IdlePlan', 'Invoke-IdlePlan', - 'Export-IdlePlan' + 'Export-IdlePlan', + 'New-IdleAuthSessionBroker' ) diff --git a/src/IdLE/Public/New-IdleAuthSessionBroker.ps1 b/src/IdLE/Public/New-IdleAuthSessionBroker.ps1 new file mode 100644 index 00000000..bde7c8c4 --- /dev/null +++ b/src/IdLE/Public/New-IdleAuthSessionBroker.ps1 @@ -0,0 +1,70 @@ +function New-IdleAuthSessionBroker { + <# + .SYNOPSIS + Creates a simple AuthSessionBroker for use with IdLE providers. + + .DESCRIPTION + Creates an AuthSessionBroker that routes authentication based on user-defined options. + The broker is used by steps to acquire credentials at runtime without embedding + secrets in workflows or provider construction. + + This is a convenience function for common scenarios. For advanced scenarios + (vault integration, MFA, etc.), implement a custom broker object with an + AcquireAuthSession method. + + .PARAMETER SessionMap + A hashtable that maps session configurations to credentials. Each key is a hashtable + representing the AuthSessionOptions pattern, and each value is the PSCredential to return. + + Common patterns: + - @{ Role = 'Tier0' } -> $tier0Credential + - @{ Role = 'Admin' } -> $adminCredential + - @{ Domain = 'SourceAD' } -> $sourceCred + - @{ Environment = 'Production' } -> $prodCred + + .PARAMETER DefaultCredential + Optional default credential to return when no session options are provided or + when the options don't match any entry in SessionMap. + + .EXAMPLE + # Simple role-based broker + $broker = New-IdleAuthSessionBroker -SessionMap @{ + @{ Role = 'Tier0' } = $tier0Credential + @{ Role = 'Admin' } = $adminCredential + } -DefaultCredential $adminCredential + + $plan = New-IdlePlan -WorkflowPath './workflow.psd1' -Request $request -Providers @{ + Identity = New-IdleADIdentityProvider + AuthSessionBroker = $broker + } + + .EXAMPLE + # Domain-based broker for multi-forest scenarios + $broker = New-IdleAuthSessionBroker -SessionMap @{ + @{ Domain = 'SourceAD' } = $sourceCred + @{ Domain = 'TargetAD' } = $targetCred + } + + .OUTPUTS + PSCustomObject with AcquireAuthSession method + #> + [CmdletBinding()] + param( + [Parameter(Mandatory)] + [ValidateNotNull()] + [hashtable] $SessionMap, + + [Parameter()] + [AllowNull()] + [PSCredential] $DefaultCredential + ) + + # Keep meta module thin: delegate to IdLE.Core. + # Since IdLE.Core is a nested module, its exported functions are available in the current scope. + if ($PSBoundParameters.ContainsKey('DefaultCredential')) { + return IdLE.Core\New-IdleAuthSessionBroker -SessionMap $SessionMap -DefaultCredential $DefaultCredential + } + else { + return IdLE.Core\New-IdleAuthSessionBroker -SessionMap $SessionMap + } +} diff --git a/tests/CapabilityDeprecation.Tests.ps1 b/tests/CapabilityDeprecation.Tests.ps1 new file mode 100644 index 00000000..263cc27d --- /dev/null +++ b/tests/CapabilityDeprecation.Tests.ps1 @@ -0,0 +1,88 @@ +Set-StrictMode -Version Latest + +BeforeAll { + . (Join-Path $PSScriptRoot '_testHelpers.ps1') + Import-IdleTestModule + + # Import mailbox steps module for capability metadata + $mailboxStepsPath = Join-Path $PSScriptRoot '..' 'src' 'IdLE.Steps.Mailbox' 'IdLE.Steps.Mailbox.psd1' + if (Test-Path $mailboxStepsPath) { + Import-Module $mailboxStepsPath -Force -ErrorAction SilentlyContinue + } +} + +Describe 'Capability Deprecation and Migration' { + Context 'IdLE.Mailbox.Read deprecation' { + It 'Maps deprecated IdLE.Mailbox.Read to IdLE.Mailbox.Info.Read with warning' { + # Create a mock provider that advertises the old capability + $mockProvider = [PSCustomObject]@{ + PSTypeName = 'IdLE.MockProvider' + } + $mockProvider | Add-Member -MemberType ScriptMethod -Name GetCapabilities -Value { + return @( + 'IdLE.Mailbox.Read' + 'IdLE.Mailbox.Type.Ensure' + 'IdLE.Mailbox.OutOfOffice.Ensure' + ) + } + + # Use a real workflow file that uses mailbox steps + $wfPath = Join-Path $PSScriptRoot '..' 'examples' 'workflows' 'live' 'exo-leaver-mailbox-offboarding.psd1' + + # Verify the workflow file exists + $wfPath | Should -Exist + + $req = New-IdleLifecycleRequest -LifecycleEvent 'Leaver' + $providers = @{ MockProvider = $mockProvider } + + # Planning should succeed and emit a deprecation warning + # Capture warnings by redirecting stream 3 to output + $output = New-IdlePlan -WorkflowPath $wfPath -Request $req -Providers $providers 3>&1 + + # Separate plan from warnings + $plan = $output | Where-Object { $_ -isnot [System.Management.Automation.WarningRecord] } + $warnings = $output | Where-Object { $_ -is [System.Management.Automation.WarningRecord] } + + # Assert plan was created successfully + $plan | Should -Not -BeNullOrEmpty + + # Assert deprecation warning was emitted + $warnings | Should -Not -BeNullOrEmpty + ($warnings | Out-String) | Should -Match "DEPRECATED.*IdLE\.Mailbox\.Read.*IdLE\.Mailbox\.Info\.Read" -Because "Should emit deprecation warning for IdLE.Mailbox.Read" + } + + It 'New capability IdLE.Mailbox.Info.Read does not emit warning' { + # Create a mock provider that advertises the new capability + $mockProvider = [PSCustomObject]@{ + PSTypeName = 'IdLE.MockProvider' + } + $mockProvider | Add-Member -MemberType ScriptMethod -Name GetCapabilities -Value { + return @( + 'IdLE.Mailbox.Info.Read' + 'IdLE.Mailbox.Type.Ensure' + 'IdLE.Mailbox.OutOfOffice.Ensure' + ) + } + + # Use a real workflow file + $wfPath = Join-Path $PSScriptRoot '..' 'examples' 'workflows' 'live' 'exo-leaver-mailbox-offboarding.psd1' + + $req = New-IdleLifecycleRequest -LifecycleEvent 'Leaver' + $providers = @{ MockProvider = $mockProvider } + + # Planning should succeed without deprecation warnings + $output = New-IdlePlan -WorkflowPath $wfPath -Request $req -Providers $providers 3>&1 + + # Separate plan from warnings + $plan = $output | Where-Object { $_ -isnot [System.Management.Automation.WarningRecord] } + $warnings = $output | Where-Object { $_ -is [System.Management.Automation.WarningRecord] } + + # Assert plan was created successfully + $plan | Should -Not -BeNullOrEmpty + + # Assert NO deprecation warning was emitted for the new capability + $matchedWarnings = $warnings | Where-Object { $_.Message -match "DEPRECATED.*IdLE\.Mailbox" } + $matchedWarnings | Should -BeNullOrEmpty -Because "New capability should not emit deprecation warning" + } + } +} diff --git a/tests/Providers/ExchangeOnlineProvider.Tests.ps1 b/tests/Providers/ExchangeOnlineProvider.Tests.ps1 index a3986e3a..2b2ffd1a 100644 --- a/tests/Providers/ExchangeOnlineProvider.Tests.ps1 +++ b/tests/Providers/ExchangeOnlineProvider.Tests.ps1 @@ -172,7 +172,7 @@ Describe 'ExchangeOnline provider - Unit tests' { Context 'GetCapabilities' { It 'returns mailbox-specific capabilities' { $caps = $provider.GetCapabilities() - $caps | Should -Contain 'IdLE.Mailbox.Read' + $caps | Should -Contain 'IdLE.Mailbox.Info.Read' $caps | Should -Contain 'IdLE.Mailbox.Type.Ensure' $caps | Should -Contain 'IdLE.Mailbox.OutOfOffice.Ensure' } diff --git a/tests/StabilityContract.Tests.ps1 b/tests/StabilityContract.Tests.ps1 new file mode 100644 index 00000000..42adf638 --- /dev/null +++ b/tests/StabilityContract.Tests.ps1 @@ -0,0 +1,114 @@ +Set-StrictMode -Version Latest + +BeforeAll { + . (Join-Path $PSScriptRoot '_testHelpers.ps1') + Import-IdleTestModule +} + +Describe 'IdLE v1.0 Stability Contract' { + Context 'Supported public API surface' { + It 'IdLE exports exactly the v1.0 supported command set' { + # Source of truth: src/IdLE/IdLE.psd1 FunctionsToExport + # This is the minimal supported surface for v1.0.0 + $expectedCommands = @( + 'Test-IdleWorkflow' + 'New-IdleLifecycleRequest' + 'New-IdlePlan' + 'Invoke-IdlePlan' + 'Export-IdlePlan' + 'New-IdleAuthSessionBroker' + ) | Sort-Object + + $actualCommands = (Get-Command -Module IdLE -CommandType Function).Name | Sort-Object + + # Exact match: no more, no less + $actualCommands | Should -Be $expectedCommands -Because "IdLE v1.0 must export exactly the documented supported commands" + } + + It 'IdLE does not export cmdlets or aliases' { + $cmdlets = Get-Command -Module IdLE -CommandType Cmdlet -ErrorAction SilentlyContinue + $cmdlets | Should -BeNullOrEmpty -Because "IdLE should only export functions, not cmdlets" + + $aliases = Get-Command -Module IdLE -CommandType Alias -ErrorAction SilentlyContinue + $aliases | Should -BeNullOrEmpty -Because "IdLE v1.0 does not export aliases" + } + } + + Context 'Internal modules emit warnings when imported directly' { + It 'IdLE.Core emits warning when imported directly without bypass' { + # Skip this test if running as part of the overall test suite where IdLE.Core is already loaded + $existingModule = Get-Module -Name IdLE.Core + if ($existingModule) { + Set-ItResult -Skipped -Because "IdLE.Core is already loaded; cannot test direct import warning" + return + } + + $corePsd1 = Join-Path $PSScriptRoot '..' 'src' 'IdLE.Core' 'IdLE.Core.psd1' + + # Clear the bypass env var + $originalValue = $env:IDLE_ALLOW_INTERNAL_IMPORT + try { + $env:IDLE_ALLOW_INTERNAL_IMPORT = $null + + # Import and capture warning output + $output = Import-Module $corePsd1 -Force 3>&1 | Out-String + + $output | Should -Not -BeNullOrEmpty -Because "Internal module should emit warning on direct import" + $output | Should -Match "internal.*unsupported.*IdLE.*instead" -Because "Warning should indicate module is internal and suggest importing IdLE" + } + finally { + $env:IDLE_ALLOW_INTERNAL_IMPORT = $originalValue + Remove-Module IdLE.Core -Force -ErrorAction SilentlyContinue + } + } + + It 'IdLE.Core does not emit warning when IDLE_ALLOW_INTERNAL_IMPORT is set' { + $existingModule = Get-Module -Name IdLE.Core + if ($existingModule) { + Set-ItResult -Skipped -Because "IdLE.Core is already loaded; cannot test bypass" + return + } + + $corePsd1 = Join-Path $PSScriptRoot '..' 'src' 'IdLE.Core' 'IdLE.Core.psd1' + + $originalValue = $env:IDLE_ALLOW_INTERNAL_IMPORT + try { + $env:IDLE_ALLOW_INTERNAL_IMPORT = '1' + + # Import and capture warning output + $output = Import-Module $corePsd1 -Force 3>&1 | Out-String + + $output | Should -BeNullOrEmpty -Because "Internal module should not emit warning when bypass is set" + } + finally { + $env:IDLE_ALLOW_INTERNAL_IMPORT = $originalValue + Remove-Module IdLE.Core -Force -ErrorAction SilentlyContinue + } + } + + It 'IdLE.Steps.Common emits warning when imported directly without bypass' { + $existingModule = Get-Module -Name IdLE.Steps.Common + if ($existingModule) { + Set-ItResult -Skipped -Because "IdLE.Steps.Common is already loaded; cannot test direct import warning" + return + } + + $stepsPsd1 = Join-Path $PSScriptRoot '..' 'src' 'IdLE.Steps.Common' 'IdLE.Steps.Common.psd1' + + $originalValue = $env:IDLE_ALLOW_INTERNAL_IMPORT + try { + $env:IDLE_ALLOW_INTERNAL_IMPORT = $null + + # Import and capture warning output + $output = Import-Module $stepsPsd1 -Force 3>&1 | Out-String + + $output | Should -Not -BeNullOrEmpty -Because "Internal module should emit warning on direct import" + $output | Should -Match "internal.*unsupported.*IdLE.*instead" -Because "Warning should indicate module is internal and suggest importing IdLE" + } + finally { + $env:IDLE_ALLOW_INTERNAL_IMPORT = $originalValue + Remove-Module IdLE.Steps.Common -Force -ErrorAction SilentlyContinue + } + } + } +} From 3a598250ca480b2c5ceafa2770d7017f90669607 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 25 Jan 2026 16:47:56 +0000 Subject: [PATCH 03/14] Add v1.0 compatibility policy documentation - Created docs/advanced/compatibility.md with comprehensive v1.0 contracts - Documented supported API surface and enforcement - Documented stability contracts (command, data, non-contracts) - Documented capability ID baseline and deprecation - Documented deprecation mechanism - Updated docs/index.md and docs/_sidebar.md to reference new doc - Fixed ModuleSurface.Tests.ps1 to include New-IdleAuthSessionBroker Co-authored-by: blindzero <13959569+blindzero@users.noreply.github.com> --- docs/_sidebar.md | 1 + docs/advanced/compatibility.md | 351 +++++++++++++++++++++++++++++++++ docs/index.md | 1 + tests/ModuleSurface.Tests.ps1 | 1 + 4 files changed, 354 insertions(+) create mode 100644 docs/advanced/compatibility.md diff --git a/docs/_sidebar.md b/docs/_sidebar.md index bc0afb12..0ec6904a 100644 --- a/docs/_sidebar.md +++ b/docs/_sidebar.md @@ -41,6 +41,7 @@ #### Advanced - [Architecture](advanced/architecture.md) +- [Compatibility](advanced/compatibility.md) - [Security](advanced/security.md) - [Extensibility](advanced/extensibility.md) - [Provider Capabilities](advanced/provider-capabilities.md) diff --git a/docs/advanced/compatibility.md b/docs/advanced/compatibility.md new file mode 100644 index 00000000..963479e1 --- /dev/null +++ b/docs/advanced/compatibility.md @@ -0,0 +1,351 @@ +# v1.0 Compatibility Policy and Stability Contracts + +This document defines the **supported public API surface**, **stable contracts**, and **compatibility guarantees** for IdLE v1.0.0 and beyond. + +--- + +## 1. Supported Public API Surface + +### 1.1 Definition + +**Supported** = exported + documented + stability-tested. + +Only the IdLE meta-module's exported commands are supported. Internal modules (IdLE.Core, IdLE.Steps.*, IdLE.Provider.*) are **unsupported** when imported directly. + +### 1.2 Source of Truth + +The **sole source of truth** for the supported command surface is: + +``` +src/IdLE/IdLE.psd1 → FunctionsToExport +``` + +### 1.3 v1.0 Supported Commands + +The minimal supported command set for v1.0: + +- `Test-IdleWorkflow` +- `New-IdleLifecycleRequest` +- `New-IdlePlan` +- `Invoke-IdlePlan` +- `Export-IdlePlan` +- `New-IdleAuthSessionBroker` + +### 1.4 Enforcement + +The supported surface is enforced by **stability tests**: + +```powershell +# tests/StabilityContract.Tests.ps1 +Describe 'IdLE v1.0 Stability Contract' { + It 'IdLE exports exactly the v1.0 supported command set' { + # Validates exact command list - no more, no less + } +} +``` + +--- + +## 2. Internal Modules (Defense-in-Depth) + +PowerShell cannot fully prevent direct import of nested modules. IdLE uses a defense-in-depth approach: + +### 2.1 Policy (Primary Control) + +Only IdLE meta-module exports are supported. Direct imports of internal modules are **unsupported** and may break in any version. + +### 2.2 Export Minimization + +Internal modules export only what is required for internal composition, not for external consumption. + +### 2.3 Import Warning (Recommended) + +Internal modules emit a warning when imported directly: + +``` +WARNING: IdLE.Core is an internal/unsupported module. Import 'IdLE' instead for the supported public API. +To bypass this warning, set IDLE_ALLOW_INTERNAL_IMPORT=1. +``` + +**Bypass mechanism** (for advanced scenarios only): + +```powershell +$env:IDLE_ALLOW_INTERNAL_IMPORT = '1' +Import-Module IdLE.Core +``` + +### 2.4 Security Boundary + +Output-boundary redaction rules (credential/secret redaction) must hold regardless of import path. + +--- + +## 3. Stability Contracts + +### 3.1 Command Contracts (Supported Cmdlets) + +The following are considered **breaking changes** and require a new major version: + +- Removing a supported command +- Renaming a supported command +- Removing a parameter +- Renaming a parameter +- Changing a parameter from optional to mandatory +- Changing a parameter's type in an incompatible way + +The following are **non-breaking** (allowed in minor/patch versions): + +- Adding a new command +- Adding a new parameter (must be optional with a sensible default) +- Changing exact error message strings +- Adding new output properties (output types are coarse-grained) +- Internal implementation changes + +### 3.2 Data Contracts (Public Artifacts) + +#### Workflow Authoring Contract + +- **Format**: PSD1 workflow definitions +- **Validation**: `Test-IdleWorkflow` +- **Stability**: The workflow schema is a stable contract + - Unknown keys: **FAIL** (strict validation) + - Required fields (Name, LifecycleEvent, Steps[].Name, Steps[].Type): **FAIL** if null/empty + - `With` payload values: allow `null` and empty strings (supports "clear attribute" scenarios) + +#### Lifecycle Request Contract + +- **Format**: Object created by `New-IdleLifecycleRequest` +- **Required fields**: `LifecycleEvent`, `CorrelationId` +- **Optional fields**: `Actor`, `IdentityKeys`, `DesiredState`, `Changes` + +#### Plan Export Contract + +- **Format**: JSON from `Export-IdlePlan` +- **Stability**: The JSON schema is a stable contract for plan interchange +- **Use case**: Plan review, auditing, CI/CD integration + +### 3.3 Explicit Non-Contracts + +The following are **not** considered stable contracts and may change without notice: + +- Exact error message strings (error types and parameters are stable) +- Undocumented internal object properties +- Internal module cmdlets (when accessed by path import) +- Internal helper functions + +--- + +## 4. Canonical Formats + +### 4.1 Workflow Authoring Format + +**Canonical format**: PSD1 workflow definitions + +Validated by `Test-IdleWorkflow`. + +### 4.2 Plan Interchange Format + +**Canonical format**: JSON from `Export-IdlePlan` + +Used for: +- Plan review and auditing +- CI/CD integration +- External tooling + +### 4.3 Non-Goals (v1.0) + +JSON as a **workflow authoring format** is not a v1.0 goal. + +--- + +## 5. Validation Strictness + +### 5.1 Unknown Keys + +Workflow definitions with **unknown keys** will **FAIL** validation. + +This enforces a strict authoring contract and prevents typos/configuration drift. + +### 5.2 Required vs. Optional Fields + +- **Required contract fields** (e.g., step `Name`, step `Type`, request `LifecycleEvent`): **FAIL** if null/empty +- **`With` payload values**: + - Required keys must exist + - Values may be `null` or empty string (supports "clear attribute" scenarios) + - Step contracts must document per-key null/empty allowance + +--- + +## 6. Capability ID Baseline (v1.0) + +### 6.1 Capability Namespace Convention + +All capability IDs use the **IdLE.** namespace: + +- ✅ `IdLE.Identity.Read` +- ✅ `IdLE.Mailbox.Info.Read` +- ❌ `Identity.Read` (un-namespaced, legacy) + +New work **MUST** use the `IdLE.` namespace. + +### 6.2 v1.0 Capability Baseline + +The following capability IDs are frozen as the v1.0 baseline: + +| Capability ID | Description | +|---------------------------------------|-------------| +| `IdLE.DirectorySync.Status` | Read directory sync status | +| `IdLE.DirectorySync.Trigger` | Trigger directory sync | +| `IdLE.Entitlement.Grant` | Grant group membership/entitlement | +| `IdLE.Entitlement.List` | List user entitlements | +| `IdLE.Entitlement.Revoke` | Revoke group membership/entitlement | +| `IdLE.Identity.Attribute.Ensure` | Ensure identity attribute value | +| `IdLE.Identity.Create` | Create identity | +| `IdLE.Identity.Delete` | Delete identity | +| `IdLE.Identity.Disable` | Disable identity | +| `IdLE.Identity.Enable` | Enable identity | +| `IdLE.Identity.Move` | Move identity (OU/container) | +| `IdLE.Mailbox.Info.Read` | Read mailbox metadata/configuration (renamed from `IdLE.Mailbox.Read`) | +| `IdLE.Mailbox.OutOfOffice.Ensure` | Ensure Out of Office configuration | +| `IdLE.Mailbox.Type.Ensure` | Ensure mailbox type (User/Shared/etc.) | + +### 6.3 Capability Rename (Pre-1.0) + +**Deprecated (pre-1.0):** `IdLE.Mailbox.Read` +**New:** `IdLE.Mailbox.Info.Read` + +**Meaning**: "Read mailbox metadata/configuration required by IdLE steps" (does not read mailbox contents) + +**Migration Policy**: +- The old capability ID is mapped to the new ID during planning +- A deprecation warning is emitted: + ``` + WARNING: DEPRECATED: Capability 'IdLE.Mailbox.Read' is deprecated and will be removed in a future major version. + Use 'IdLE.Mailbox.Info.Read' instead. + ``` +- The mapping will be removed in a future major version (post-1.0) + +--- + +## 7. Deprecation Mechanism + +### 7.1 Deprecation Warning Format + +Deprecated supported cmdlets/parameters **MUST** emit a `Write-Warning` on use: + +**Format**: +``` +DEPRECATED: is deprecated in and will be removed in . +Use instead. +``` + +**Example**: +```powershell +Write-Warning "DEPRECATED: Parameter 'OldName' is deprecated in v1.2 and will be removed in v2.0. Use 'NewName' instead." +``` + +### 7.2 Enforcement + +Deprecation warnings are enforced by **Pester tests** that assert the warning exists. + +### 7.3 Capability Deprecation + +Deprecated capability IDs are automatically mapped to their replacements during planning, with a warning emitted. + +See section 6.3 for the `IdLE.Mailbox.Read` → `IdLE.Mailbox.Info.Read` migration. + +--- + +## 8. Step Capability Ownership + +**Rule**: Step packs own required capabilities via step metadata catalogs. + +**Implementation**: +- Step metadata is declared in `Get-IdleStepMetadataCatalog` functions +- Core engine enforces capability requirements during planning +- Workflows **do not** declare capabilities directly + +**Example**: + +```powershell +# src/IdLE.Steps.Mailbox/Public/Get-IdleStepMetadataCatalog.ps1 +$catalog['IdLE.Step.Mailbox.GetInfo'] = @{ + RequiredCapabilities = @('IdLE.Mailbox.Info.Read') +} +``` + +--- + +## 9. Compatibility Guarantees + +### 9.1 Semantic Versioning + +IdLE follows [Semantic Versioning](https://semver.org/): + +- **MAJOR** (breaking): Incompatible API changes +- **MINOR** (feature): Backward-compatible functionality additions +- **PATCH** (fix): Backward-compatible bug fixes + +### 9.2 What Constitutes a Breaking Change + +See section 3.1 for details. + +### 9.3 Deprecation Timeline + +Deprecated features will be supported for **at least one minor version** before removal in the next major version. + +**Example**: +- Deprecated in v1.2 → Removed in v2.0 +- Deprecated in v1.8 → Removed in v2.0 + +--- + +## 10. Workflow and Request Schema Evolution + +### 10.1 Adding Optional Fields + +Adding new **optional** fields to workflow definitions or lifecycle requests is **non-breaking** (minor version). + +### 10.2 Removing Fields + +Removing fields is **breaking** (major version). + +### 10.3 Renaming Fields + +Renaming fields is **breaking** (major version). + +Migration path: Support both old and new names with deprecation warnings for at least one minor version. + +--- + +## 11. Testing and Verification + +### 11.1 Stability Tests + +Stability contract tests enforce the supported API surface: + +```powershell +# Run stability tests +pwsh -NoProfile -File ./tools/Invoke-IdlePesterTests.ps1 -TestPath tests/StabilityContract.Tests.ps1 +``` + +### 11.2 Deprecation Tests + +Deprecation behavior is validated by Pester tests: + +```powershell +# Run deprecation tests +pwsh -NoProfile -File ./tools/Invoke-IdlePesterTests.ps1 -TestPath tests/CapabilityDeprecation.Tests.ps1 +``` + +--- + +## 12. References + +- [AGENTS.md](../../AGENTS.md) - Agent operating manual +- [STYLEGUIDE.md](../../STYLEGUIDE.md) - Coding standards +- [CONTRIBUTING.md](../../CONTRIBUTING.md) - Contributor workflow +- [architecture.md](./architecture.md) - Architecture decisions +- [security.md](./security.md) - Security and trust boundaries +- [provider-capabilities.md](./provider-capabilities.md) - Provider capability rules +- [providers-and-contracts.md](../reference/providers-and-contracts.md) - Provider contracts diff --git a/docs/index.md b/docs/index.md index 9502a750..9fe558b3 100644 --- a/docs/index.md +++ b/docs/index.md @@ -56,6 +56,7 @@ For contributors, extenders, and maintainers: ### Advanced Topics - [Architecture](advanced/architecture.md) — Design principles and decisions +- [Compatibility](advanced/compatibility.md) — v1.0 API surface, contracts, and compatibility guarantees - [Security](advanced/security.md) — Trust boundaries and threat model - [Extensibility](advanced/extensibility.md) — Add steps and providers - [Provider Capabilities](advanced/provider-capabilities.md) — Capability system and validation diff --git a/tests/ModuleSurface.Tests.ps1 b/tests/ModuleSurface.Tests.ps1 index b74009dd..8f69de94 100644 --- a/tests/ModuleSurface.Tests.ps1 +++ b/tests/ModuleSurface.Tests.ps1 @@ -53,6 +53,7 @@ Describe 'Module manifests and public surface' { $expected = @( 'Invoke-IdlePlan' + 'New-IdleAuthSessionBroker' 'New-IdleLifecycleRequest' 'New-IdlePlan' 'Test-IdleWorkflow' From c834dfc6b6979cea13f619dbb1d60fa30edf7bde Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 25 Jan 2026 16:49:05 +0000 Subject: [PATCH 04/14] Improve deprecation warning with version information - Updated capability deprecation warning to include specific version info - Format: 'DEPRECATED: is deprecated in v1.0 and will be removed in v2.0' - Follows the documented deprecation mechanism from compatibility.md Co-authored-by: blindzero <13959569+blindzero@users.noreply.github.com> --- src/IdLE.Core/Public/New-IdlePlanObject.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/IdLE.Core/Public/New-IdlePlanObject.ps1 b/src/IdLE.Core/Public/New-IdlePlanObject.ps1 index 2cf2f6d5..318faa58 100644 --- a/src/IdLE.Core/Public/New-IdlePlanObject.ps1 +++ b/src/IdLE.Core/Public/New-IdlePlanObject.ps1 @@ -250,7 +250,7 @@ function New-IdlePlanObject { if ($deprecatedMappings.ContainsKey($normalized)) { $newId = $deprecatedMappings[$normalized] - Write-Warning "DEPRECATED: Capability '$normalized' is deprecated and will be removed in a future major version. Use '$newId' instead." + Write-Warning "DEPRECATED: Capability '$normalized' is deprecated in v1.0 and will be removed in v2.0. Use '$newId' instead." return $newId } From 96a5ec82e593f90e62f442ffa1fe77c0f0312c34 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 25 Jan 2026 17:16:05 +0000 Subject: [PATCH 05/14] Address PR feedback: consolidate tests and merge docs - Merged internal module warning tests into ModuleSurface.Tests.ps1 - Removed StabilityContract.Tests.ps1 (duplicate functionality) - Merged compatibility.md content into architecture.md and releases.md - Removed standalone compatibility.md file (clearer doc structure) - Updated navigation in docs/index.md and docs/_sidebar.md - All 322 tests passing Co-authored-by: blindzero <13959569+blindzero@users.noreply.github.com> --- docs/_sidebar.md | 1 - docs/advanced/architecture.md | 70 ++++++ docs/advanced/compatibility.md | 351 ------------------------------ docs/advanced/releases.md | 48 ++++ docs/index.md | 1 - tests/ModuleSurface.Tests.ps1 | 70 ++++++ tests/StabilityContract.Tests.ps1 | 114 ---------- 7 files changed, 188 insertions(+), 467 deletions(-) delete mode 100644 docs/advanced/compatibility.md delete mode 100644 tests/StabilityContract.Tests.ps1 diff --git a/docs/_sidebar.md b/docs/_sidebar.md index 0ec6904a..bc0afb12 100644 --- a/docs/_sidebar.md +++ b/docs/_sidebar.md @@ -41,7 +41,6 @@ #### Advanced - [Architecture](advanced/architecture.md) -- [Compatibility](advanced/compatibility.md) - [Security](advanced/security.md) - [Extensibility](advanced/extensibility.md) - [Provider Capabilities](advanced/provider-capabilities.md) diff --git a/docs/advanced/architecture.md b/docs/advanced/architecture.md index 68d9d651..5e695e62 100644 --- a/docs/advanced/architecture.md +++ b/docs/advanced/architecture.md @@ -104,3 +104,73 @@ See: [Extensibility](extensibility.md). IdLE treats workflow configuration and lifecycle requests as **untrusted data** and validates that they contain no ScriptBlocks. Host-provided extension points (step registry, providers, external event sinks) are **trusted inputs** and are validated for safe shapes (object contracts). For details, see `advanced/security.md`. + +## v1.0 Public API and Contracts + +### Supported Commands + +The **supported public API** for v1.0 consists of the following commands exported from the IdLE meta-module: + +- `Test-IdleWorkflow` +- `New-IdleLifecycleRequest` +- `New-IdlePlan` +- `Invoke-IdlePlan` +- `Export-IdlePlan` +- `New-IdleAuthSessionBroker` + +**Source of truth**: `src/IdLE/IdLE.psd1` → `FunctionsToExport` + +Only these commands are considered **stable contracts**. Internal modules (IdLE.Core, IdLE.Steps.*, IdLE.Provider.*) are unsupported when imported directly. + +### Command Contracts + +For supported commands, the following are **stable contracts** (breaking changes require a major version): + +- Command name +- Parameter names and parameter sets +- Observable semantics (mandatory/optional/default behavior) +- Output type identity at a coarse level (PSTypeName) + +The following are **not contracts** and may change in minor/patch versions: + +- Exact error message strings +- Undocumented internal object properties +- Internal module cmdlets + +### Data Contracts + +**Workflow authoring contract** (PSD1): +- Format: PSD1 workflow definitions validated by `Test-IdleWorkflow` +- Unknown keys: **FAIL** (strict validation) +- Required fields (Name, LifecycleEvent, Steps[].Name, Steps[].Type): **FAIL** if null/empty +- `With` payload values: allow `null` and empty strings (supports "clear attribute" scenarios) + +**Lifecycle request contract**: +- Required fields: `LifecycleEvent`, `CorrelationId` +- Optional fields: `Actor`, `IdentityKeys`, `DesiredState`, `Changes` + +**Plan export contract** (JSON): +- Format: JSON from `Export-IdlePlan` +- Schema and semantics are stable +- See [Plan export specification](../specs/plan-export.md) + +### Capability ID Baseline (v1.0) + +The following capability IDs are frozen as the v1.0 baseline: + +- `IdLE.DirectorySync.Status` - Read directory sync status +- `IdLE.DirectorySync.Trigger` - Trigger directory sync +- `IdLE.Entitlement.Grant` - Grant group membership/entitlement +- `IdLE.Entitlement.List` - List user entitlements +- `IdLE.Entitlement.Revoke` - Revoke group membership/entitlement +- `IdLE.Identity.Attribute.Ensure` - Ensure identity attribute value +- `IdLE.Identity.Create` - Create identity +- `IdLE.Identity.Delete` - Delete identity +- `IdLE.Identity.Disable` - Disable identity +- `IdLE.Identity.Enable` - Enable identity +- `IdLE.Identity.Move` - Move identity (OU/container) +- `IdLE.Mailbox.Info.Read` - Read mailbox metadata/configuration +- `IdLE.Mailbox.OutOfOffice.Ensure` - Ensure Out of Office configuration +- `IdLE.Mailbox.Type.Ensure` - Ensure mailbox type (User/Shared/etc.) + +**Deprecated (pre-1.0)**: `IdLE.Mailbox.Read` → automatically mapped to `IdLE.Mailbox.Info.Read` with deprecation warning during planning. diff --git a/docs/advanced/compatibility.md b/docs/advanced/compatibility.md deleted file mode 100644 index 963479e1..00000000 --- a/docs/advanced/compatibility.md +++ /dev/null @@ -1,351 +0,0 @@ -# v1.0 Compatibility Policy and Stability Contracts - -This document defines the **supported public API surface**, **stable contracts**, and **compatibility guarantees** for IdLE v1.0.0 and beyond. - ---- - -## 1. Supported Public API Surface - -### 1.1 Definition - -**Supported** = exported + documented + stability-tested. - -Only the IdLE meta-module's exported commands are supported. Internal modules (IdLE.Core, IdLE.Steps.*, IdLE.Provider.*) are **unsupported** when imported directly. - -### 1.2 Source of Truth - -The **sole source of truth** for the supported command surface is: - -``` -src/IdLE/IdLE.psd1 → FunctionsToExport -``` - -### 1.3 v1.0 Supported Commands - -The minimal supported command set for v1.0: - -- `Test-IdleWorkflow` -- `New-IdleLifecycleRequest` -- `New-IdlePlan` -- `Invoke-IdlePlan` -- `Export-IdlePlan` -- `New-IdleAuthSessionBroker` - -### 1.4 Enforcement - -The supported surface is enforced by **stability tests**: - -```powershell -# tests/StabilityContract.Tests.ps1 -Describe 'IdLE v1.0 Stability Contract' { - It 'IdLE exports exactly the v1.0 supported command set' { - # Validates exact command list - no more, no less - } -} -``` - ---- - -## 2. Internal Modules (Defense-in-Depth) - -PowerShell cannot fully prevent direct import of nested modules. IdLE uses a defense-in-depth approach: - -### 2.1 Policy (Primary Control) - -Only IdLE meta-module exports are supported. Direct imports of internal modules are **unsupported** and may break in any version. - -### 2.2 Export Minimization - -Internal modules export only what is required for internal composition, not for external consumption. - -### 2.3 Import Warning (Recommended) - -Internal modules emit a warning when imported directly: - -``` -WARNING: IdLE.Core is an internal/unsupported module. Import 'IdLE' instead for the supported public API. -To bypass this warning, set IDLE_ALLOW_INTERNAL_IMPORT=1. -``` - -**Bypass mechanism** (for advanced scenarios only): - -```powershell -$env:IDLE_ALLOW_INTERNAL_IMPORT = '1' -Import-Module IdLE.Core -``` - -### 2.4 Security Boundary - -Output-boundary redaction rules (credential/secret redaction) must hold regardless of import path. - ---- - -## 3. Stability Contracts - -### 3.1 Command Contracts (Supported Cmdlets) - -The following are considered **breaking changes** and require a new major version: - -- Removing a supported command -- Renaming a supported command -- Removing a parameter -- Renaming a parameter -- Changing a parameter from optional to mandatory -- Changing a parameter's type in an incompatible way - -The following are **non-breaking** (allowed in minor/patch versions): - -- Adding a new command -- Adding a new parameter (must be optional with a sensible default) -- Changing exact error message strings -- Adding new output properties (output types are coarse-grained) -- Internal implementation changes - -### 3.2 Data Contracts (Public Artifacts) - -#### Workflow Authoring Contract - -- **Format**: PSD1 workflow definitions -- **Validation**: `Test-IdleWorkflow` -- **Stability**: The workflow schema is a stable contract - - Unknown keys: **FAIL** (strict validation) - - Required fields (Name, LifecycleEvent, Steps[].Name, Steps[].Type): **FAIL** if null/empty - - `With` payload values: allow `null` and empty strings (supports "clear attribute" scenarios) - -#### Lifecycle Request Contract - -- **Format**: Object created by `New-IdleLifecycleRequest` -- **Required fields**: `LifecycleEvent`, `CorrelationId` -- **Optional fields**: `Actor`, `IdentityKeys`, `DesiredState`, `Changes` - -#### Plan Export Contract - -- **Format**: JSON from `Export-IdlePlan` -- **Stability**: The JSON schema is a stable contract for plan interchange -- **Use case**: Plan review, auditing, CI/CD integration - -### 3.3 Explicit Non-Contracts - -The following are **not** considered stable contracts and may change without notice: - -- Exact error message strings (error types and parameters are stable) -- Undocumented internal object properties -- Internal module cmdlets (when accessed by path import) -- Internal helper functions - ---- - -## 4. Canonical Formats - -### 4.1 Workflow Authoring Format - -**Canonical format**: PSD1 workflow definitions - -Validated by `Test-IdleWorkflow`. - -### 4.2 Plan Interchange Format - -**Canonical format**: JSON from `Export-IdlePlan` - -Used for: -- Plan review and auditing -- CI/CD integration -- External tooling - -### 4.3 Non-Goals (v1.0) - -JSON as a **workflow authoring format** is not a v1.0 goal. - ---- - -## 5. Validation Strictness - -### 5.1 Unknown Keys - -Workflow definitions with **unknown keys** will **FAIL** validation. - -This enforces a strict authoring contract and prevents typos/configuration drift. - -### 5.2 Required vs. Optional Fields - -- **Required contract fields** (e.g., step `Name`, step `Type`, request `LifecycleEvent`): **FAIL** if null/empty -- **`With` payload values**: - - Required keys must exist - - Values may be `null` or empty string (supports "clear attribute" scenarios) - - Step contracts must document per-key null/empty allowance - ---- - -## 6. Capability ID Baseline (v1.0) - -### 6.1 Capability Namespace Convention - -All capability IDs use the **IdLE.** namespace: - -- ✅ `IdLE.Identity.Read` -- ✅ `IdLE.Mailbox.Info.Read` -- ❌ `Identity.Read` (un-namespaced, legacy) - -New work **MUST** use the `IdLE.` namespace. - -### 6.2 v1.0 Capability Baseline - -The following capability IDs are frozen as the v1.0 baseline: - -| Capability ID | Description | -|---------------------------------------|-------------| -| `IdLE.DirectorySync.Status` | Read directory sync status | -| `IdLE.DirectorySync.Trigger` | Trigger directory sync | -| `IdLE.Entitlement.Grant` | Grant group membership/entitlement | -| `IdLE.Entitlement.List` | List user entitlements | -| `IdLE.Entitlement.Revoke` | Revoke group membership/entitlement | -| `IdLE.Identity.Attribute.Ensure` | Ensure identity attribute value | -| `IdLE.Identity.Create` | Create identity | -| `IdLE.Identity.Delete` | Delete identity | -| `IdLE.Identity.Disable` | Disable identity | -| `IdLE.Identity.Enable` | Enable identity | -| `IdLE.Identity.Move` | Move identity (OU/container) | -| `IdLE.Mailbox.Info.Read` | Read mailbox metadata/configuration (renamed from `IdLE.Mailbox.Read`) | -| `IdLE.Mailbox.OutOfOffice.Ensure` | Ensure Out of Office configuration | -| `IdLE.Mailbox.Type.Ensure` | Ensure mailbox type (User/Shared/etc.) | - -### 6.3 Capability Rename (Pre-1.0) - -**Deprecated (pre-1.0):** `IdLE.Mailbox.Read` -**New:** `IdLE.Mailbox.Info.Read` - -**Meaning**: "Read mailbox metadata/configuration required by IdLE steps" (does not read mailbox contents) - -**Migration Policy**: -- The old capability ID is mapped to the new ID during planning -- A deprecation warning is emitted: - ``` - WARNING: DEPRECATED: Capability 'IdLE.Mailbox.Read' is deprecated and will be removed in a future major version. - Use 'IdLE.Mailbox.Info.Read' instead. - ``` -- The mapping will be removed in a future major version (post-1.0) - ---- - -## 7. Deprecation Mechanism - -### 7.1 Deprecation Warning Format - -Deprecated supported cmdlets/parameters **MUST** emit a `Write-Warning` on use: - -**Format**: -``` -DEPRECATED: is deprecated in and will be removed in . -Use instead. -``` - -**Example**: -```powershell -Write-Warning "DEPRECATED: Parameter 'OldName' is deprecated in v1.2 and will be removed in v2.0. Use 'NewName' instead." -``` - -### 7.2 Enforcement - -Deprecation warnings are enforced by **Pester tests** that assert the warning exists. - -### 7.3 Capability Deprecation - -Deprecated capability IDs are automatically mapped to their replacements during planning, with a warning emitted. - -See section 6.3 for the `IdLE.Mailbox.Read` → `IdLE.Mailbox.Info.Read` migration. - ---- - -## 8. Step Capability Ownership - -**Rule**: Step packs own required capabilities via step metadata catalogs. - -**Implementation**: -- Step metadata is declared in `Get-IdleStepMetadataCatalog` functions -- Core engine enforces capability requirements during planning -- Workflows **do not** declare capabilities directly - -**Example**: - -```powershell -# src/IdLE.Steps.Mailbox/Public/Get-IdleStepMetadataCatalog.ps1 -$catalog['IdLE.Step.Mailbox.GetInfo'] = @{ - RequiredCapabilities = @('IdLE.Mailbox.Info.Read') -} -``` - ---- - -## 9. Compatibility Guarantees - -### 9.1 Semantic Versioning - -IdLE follows [Semantic Versioning](https://semver.org/): - -- **MAJOR** (breaking): Incompatible API changes -- **MINOR** (feature): Backward-compatible functionality additions -- **PATCH** (fix): Backward-compatible bug fixes - -### 9.2 What Constitutes a Breaking Change - -See section 3.1 for details. - -### 9.3 Deprecation Timeline - -Deprecated features will be supported for **at least one minor version** before removal in the next major version. - -**Example**: -- Deprecated in v1.2 → Removed in v2.0 -- Deprecated in v1.8 → Removed in v2.0 - ---- - -## 10. Workflow and Request Schema Evolution - -### 10.1 Adding Optional Fields - -Adding new **optional** fields to workflow definitions or lifecycle requests is **non-breaking** (minor version). - -### 10.2 Removing Fields - -Removing fields is **breaking** (major version). - -### 10.3 Renaming Fields - -Renaming fields is **breaking** (major version). - -Migration path: Support both old and new names with deprecation warnings for at least one minor version. - ---- - -## 11. Testing and Verification - -### 11.1 Stability Tests - -Stability contract tests enforce the supported API surface: - -```powershell -# Run stability tests -pwsh -NoProfile -File ./tools/Invoke-IdlePesterTests.ps1 -TestPath tests/StabilityContract.Tests.ps1 -``` - -### 11.2 Deprecation Tests - -Deprecation behavior is validated by Pester tests: - -```powershell -# Run deprecation tests -pwsh -NoProfile -File ./tools/Invoke-IdlePesterTests.ps1 -TestPath tests/CapabilityDeprecation.Tests.ps1 -``` - ---- - -## 12. References - -- [AGENTS.md](../../AGENTS.md) - Agent operating manual -- [STYLEGUIDE.md](../../STYLEGUIDE.md) - Coding standards -- [CONTRIBUTING.md](../../CONTRIBUTING.md) - Contributor workflow -- [architecture.md](./architecture.md) - Architecture decisions -- [security.md](./security.md) - Security and trust boundaries -- [provider-capabilities.md](./provider-capabilities.md) - Provider capability rules -- [providers-and-contracts.md](../reference/providers-and-contracts.md) - Provider contracts diff --git a/docs/advanced/releases.md b/docs/advanced/releases.md index d545ad04..90949fc2 100644 --- a/docs/advanced/releases.md +++ b/docs/advanced/releases.md @@ -29,6 +29,54 @@ These checks prevent "broken" releases (e.g., tagging the wrong commit or forget ## Versioning policy +IdLE follows [Semantic Versioning](https://semver.org/): + +- **MAJOR** (breaking): Incompatible API changes +- **MINOR** (feature): Backward-compatible functionality additions +- **PATCH** (fix): Backward-compatible bug fixes + +### What Constitutes a Breaking Change + +The following are **breaking changes** and require a new major version: + +- Removing a supported command +- Renaming a supported command +- Removing a parameter +- Renaming a parameter +- Changing a parameter from optional to mandatory +- Changing a parameter's type in an incompatible way +- Removing fields from workflow/request/plan contracts +- Renaming fields in workflow/request/plan contracts + +The following are **non-breaking** (allowed in minor/patch versions): + +- Adding a new command +- Adding a new parameter (must be optional with a sensible default) +- Changing exact error message strings +- Adding new output properties (output types are coarse-grained) +- Internal implementation changes + +### Deprecation Mechanism + +Deprecated supported cmdlets/parameters **MUST** emit a `Write-Warning` on use: + +**Format**: +``` +DEPRECATED: is deprecated in and will be removed in . +Use instead. +``` + +**Example**: +```powershell +Write-Warning "DEPRECATED: Parameter 'OldName' is deprecated in v1.2 and will be removed in v2.0. Use 'NewName' instead." +``` + +**Timeline**: Deprecated features will be supported for **at least one minor version** before removal in the next major version. + +**Example timeline**: +- Deprecated in v1.2 → Removed in v2.0 +- Deprecated in v1.8 → Removed in v2.0 + ### Stable tags Stable releases use tags in the form: diff --git a/docs/index.md b/docs/index.md index 9fe558b3..9502a750 100644 --- a/docs/index.md +++ b/docs/index.md @@ -56,7 +56,6 @@ For contributors, extenders, and maintainers: ### Advanced Topics - [Architecture](advanced/architecture.md) — Design principles and decisions -- [Compatibility](advanced/compatibility.md) — v1.0 API surface, contracts, and compatibility guarantees - [Security](advanced/security.md) — Trust boundaries and threat model - [Extensibility](advanced/extensibility.md) — Add steps and providers - [Provider Capabilities](advanced/provider-capabilities.md) — Capability system and validation diff --git a/tests/ModuleSurface.Tests.ps1 b/tests/ModuleSurface.Tests.ps1 index 8f69de94..b3b205ba 100644 --- a/tests/ModuleSurface.Tests.ps1 +++ b/tests/ModuleSurface.Tests.ps1 @@ -205,4 +205,74 @@ Describe 'Module manifests and public surface' { (Get-Command -Module IdLE.Provider.Mock).Name | Should -Contain 'New-IdleMockIdentityProvider' } + + Context 'Internal module import warnings' { + It 'IdLE.Core emits warning when imported directly without bypass' { + $existingModule = Get-Module -Name IdLE.Core + if ($existingModule) { + Set-ItResult -Skipped -Because "IdLE.Core is already loaded; cannot test direct import warning" + return + } + + $originalValue = $env:IDLE_ALLOW_INTERNAL_IMPORT + try { + $env:IDLE_ALLOW_INTERNAL_IMPORT = $null + + # Import and capture warning output + $output = Import-Module $corePsd1 -Force 3>&1 | Out-String + + $output | Should -Not -BeNullOrEmpty -Because "Internal module should emit warning on direct import" + $output | Should -Match "internal.*unsupported.*IdLE.*instead" -Because "Warning should indicate module is internal and suggest importing IdLE" + } + finally { + $env:IDLE_ALLOW_INTERNAL_IMPORT = $originalValue + Remove-Module IdLE.Core -Force -ErrorAction SilentlyContinue + } + } + + It 'IdLE.Core does not emit warning when IDLE_ALLOW_INTERNAL_IMPORT is set' { + $existingModule = Get-Module -Name IdLE.Core + if ($existingModule) { + Set-ItResult -Skipped -Because "IdLE.Core is already loaded; cannot test bypass" + return + } + + $originalValue = $env:IDLE_ALLOW_INTERNAL_IMPORT + try { + $env:IDLE_ALLOW_INTERNAL_IMPORT = '1' + + # Import and capture warning output + $output = Import-Module $corePsd1 -Force 3>&1 | Out-String + + $output | Should -BeNullOrEmpty -Because "Internal module should not emit warning when bypass is set" + } + finally { + $env:IDLE_ALLOW_INTERNAL_IMPORT = $originalValue + Remove-Module IdLE.Core -Force -ErrorAction SilentlyContinue + } + } + + It 'IdLE.Steps.Common emits warning when imported directly without bypass' { + $existingModule = Get-Module -Name IdLE.Steps.Common + if ($existingModule) { + Set-ItResult -Skipped -Because "IdLE.Steps.Common is already loaded; cannot test direct import warning" + return + } + + $originalValue = $env:IDLE_ALLOW_INTERNAL_IMPORT + try { + $env:IDLE_ALLOW_INTERNAL_IMPORT = $null + + # Import and capture warning output + $output = Import-Module $stepsPsd1 -Force 3>&1 | Out-String + + $output | Should -Not -BeNullOrEmpty -Because "Internal module should emit warning on direct import" + $output | Should -Match "internal.*unsupported.*IdLE.*instead" -Because "Warning should indicate module is internal and suggest importing IdLE" + } + finally { + $env:IDLE_ALLOW_INTERNAL_IMPORT = $originalValue + Remove-Module IdLE.Steps.Common -Force -ErrorAction SilentlyContinue + } + } + } } diff --git a/tests/StabilityContract.Tests.ps1 b/tests/StabilityContract.Tests.ps1 deleted file mode 100644 index 42adf638..00000000 --- a/tests/StabilityContract.Tests.ps1 +++ /dev/null @@ -1,114 +0,0 @@ -Set-StrictMode -Version Latest - -BeforeAll { - . (Join-Path $PSScriptRoot '_testHelpers.ps1') - Import-IdleTestModule -} - -Describe 'IdLE v1.0 Stability Contract' { - Context 'Supported public API surface' { - It 'IdLE exports exactly the v1.0 supported command set' { - # Source of truth: src/IdLE/IdLE.psd1 FunctionsToExport - # This is the minimal supported surface for v1.0.0 - $expectedCommands = @( - 'Test-IdleWorkflow' - 'New-IdleLifecycleRequest' - 'New-IdlePlan' - 'Invoke-IdlePlan' - 'Export-IdlePlan' - 'New-IdleAuthSessionBroker' - ) | Sort-Object - - $actualCommands = (Get-Command -Module IdLE -CommandType Function).Name | Sort-Object - - # Exact match: no more, no less - $actualCommands | Should -Be $expectedCommands -Because "IdLE v1.0 must export exactly the documented supported commands" - } - - It 'IdLE does not export cmdlets or aliases' { - $cmdlets = Get-Command -Module IdLE -CommandType Cmdlet -ErrorAction SilentlyContinue - $cmdlets | Should -BeNullOrEmpty -Because "IdLE should only export functions, not cmdlets" - - $aliases = Get-Command -Module IdLE -CommandType Alias -ErrorAction SilentlyContinue - $aliases | Should -BeNullOrEmpty -Because "IdLE v1.0 does not export aliases" - } - } - - Context 'Internal modules emit warnings when imported directly' { - It 'IdLE.Core emits warning when imported directly without bypass' { - # Skip this test if running as part of the overall test suite where IdLE.Core is already loaded - $existingModule = Get-Module -Name IdLE.Core - if ($existingModule) { - Set-ItResult -Skipped -Because "IdLE.Core is already loaded; cannot test direct import warning" - return - } - - $corePsd1 = Join-Path $PSScriptRoot '..' 'src' 'IdLE.Core' 'IdLE.Core.psd1' - - # Clear the bypass env var - $originalValue = $env:IDLE_ALLOW_INTERNAL_IMPORT - try { - $env:IDLE_ALLOW_INTERNAL_IMPORT = $null - - # Import and capture warning output - $output = Import-Module $corePsd1 -Force 3>&1 | Out-String - - $output | Should -Not -BeNullOrEmpty -Because "Internal module should emit warning on direct import" - $output | Should -Match "internal.*unsupported.*IdLE.*instead" -Because "Warning should indicate module is internal and suggest importing IdLE" - } - finally { - $env:IDLE_ALLOW_INTERNAL_IMPORT = $originalValue - Remove-Module IdLE.Core -Force -ErrorAction SilentlyContinue - } - } - - It 'IdLE.Core does not emit warning when IDLE_ALLOW_INTERNAL_IMPORT is set' { - $existingModule = Get-Module -Name IdLE.Core - if ($existingModule) { - Set-ItResult -Skipped -Because "IdLE.Core is already loaded; cannot test bypass" - return - } - - $corePsd1 = Join-Path $PSScriptRoot '..' 'src' 'IdLE.Core' 'IdLE.Core.psd1' - - $originalValue = $env:IDLE_ALLOW_INTERNAL_IMPORT - try { - $env:IDLE_ALLOW_INTERNAL_IMPORT = '1' - - # Import and capture warning output - $output = Import-Module $corePsd1 -Force 3>&1 | Out-String - - $output | Should -BeNullOrEmpty -Because "Internal module should not emit warning when bypass is set" - } - finally { - $env:IDLE_ALLOW_INTERNAL_IMPORT = $originalValue - Remove-Module IdLE.Core -Force -ErrorAction SilentlyContinue - } - } - - It 'IdLE.Steps.Common emits warning when imported directly without bypass' { - $existingModule = Get-Module -Name IdLE.Steps.Common - if ($existingModule) { - Set-ItResult -Skipped -Because "IdLE.Steps.Common is already loaded; cannot test direct import warning" - return - } - - $stepsPsd1 = Join-Path $PSScriptRoot '..' 'src' 'IdLE.Steps.Common' 'IdLE.Steps.Common.psd1' - - $originalValue = $env:IDLE_ALLOW_INTERNAL_IMPORT - try { - $env:IDLE_ALLOW_INTERNAL_IMPORT = $null - - # Import and capture warning output - $output = Import-Module $stepsPsd1 -Force 3>&1 | Out-String - - $output | Should -Not -BeNullOrEmpty -Because "Internal module should emit warning on direct import" - $output | Should -Match "internal.*unsupported.*IdLE.*instead" -Because "Warning should indicate module is internal and suggest importing IdLE" - } - finally { - $env:IDLE_ALLOW_INTERNAL_IMPORT = $originalValue - Remove-Module IdLE.Steps.Common -Force -ErrorAction SilentlyContinue - } - } - } -} From 0b47110e26b5cf1778e395e7649004af44fac026 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 25 Jan 2026 17:27:40 +0000 Subject: [PATCH 06/14] Rename wrapper to avoid filename collision with IdLE.Core - Renamed src/IdLE/Public/New-IdleAuthSessionBroker.ps1 to New-AuthSessionBroker.ps1 - Avoids confusion with IdLE.Core\New-IdleAuthSessionBroker.ps1 - Wrapper remains minimal with concise help pointing to full docs in Core - All 322 tests passing Co-authored-by: blindzero <13959569+blindzero@users.noreply.github.com> --- src/IdLE/Public/New-AuthSessionBroker.ps1 | 51 ++++++++++++++ src/IdLE/Public/New-IdleAuthSessionBroker.ps1 | 70 ------------------- 2 files changed, 51 insertions(+), 70 deletions(-) create mode 100644 src/IdLE/Public/New-AuthSessionBroker.ps1 delete mode 100644 src/IdLE/Public/New-IdleAuthSessionBroker.ps1 diff --git a/src/IdLE/Public/New-AuthSessionBroker.ps1 b/src/IdLE/Public/New-AuthSessionBroker.ps1 new file mode 100644 index 00000000..46df946d --- /dev/null +++ b/src/IdLE/Public/New-AuthSessionBroker.ps1 @@ -0,0 +1,51 @@ +# Re-export New-IdleAuthSessionBroker from IdLE.Core. +# This avoids filename collision while keeping the wrapper minimal. + +function New-IdleAuthSessionBroker { + <# + .SYNOPSIS + Creates a simple AuthSessionBroker for use with IdLE providers. + + .DESCRIPTION + Creates an AuthSessionBroker that routes authentication based on user-defined options. + The broker is used by steps to acquire credentials at runtime without embedding + secrets in workflows or provider construction. + + This is a thin wrapper that delegates to IdLE.Core\New-IdleAuthSessionBroker. + + .PARAMETER SessionMap + A hashtable that maps session configurations to credentials. + + .PARAMETER DefaultCredential + Optional default credential to return when no session options are provided. + + .EXAMPLE + $broker = New-IdleAuthSessionBroker -SessionMap @{ + @{ Role = 'Tier0' } = $tier0Credential + } + + .OUTPUTS + PSCustomObject with AcquireAuthSession method + + .NOTES + For detailed documentation, see: Get-Help IdLE.Core\New-IdleAuthSessionBroker -Full + #> + [CmdletBinding()] + param( + [Parameter(Mandatory)] + [ValidateNotNull()] + [hashtable] $SessionMap, + + [Parameter()] + [AllowNull()] + [PSCredential] $DefaultCredential + ) + + # Delegate to IdLE.Core implementation. + $params = @{ SessionMap = $SessionMap } + if ($PSBoundParameters.ContainsKey('DefaultCredential')) { + $params['DefaultCredential'] = $DefaultCredential + } + + return IdLE.Core\New-IdleAuthSessionBroker @params +} diff --git a/src/IdLE/Public/New-IdleAuthSessionBroker.ps1 b/src/IdLE/Public/New-IdleAuthSessionBroker.ps1 deleted file mode 100644 index bde7c8c4..00000000 --- a/src/IdLE/Public/New-IdleAuthSessionBroker.ps1 +++ /dev/null @@ -1,70 +0,0 @@ -function New-IdleAuthSessionBroker { - <# - .SYNOPSIS - Creates a simple AuthSessionBroker for use with IdLE providers. - - .DESCRIPTION - Creates an AuthSessionBroker that routes authentication based on user-defined options. - The broker is used by steps to acquire credentials at runtime without embedding - secrets in workflows or provider construction. - - This is a convenience function for common scenarios. For advanced scenarios - (vault integration, MFA, etc.), implement a custom broker object with an - AcquireAuthSession method. - - .PARAMETER SessionMap - A hashtable that maps session configurations to credentials. Each key is a hashtable - representing the AuthSessionOptions pattern, and each value is the PSCredential to return. - - Common patterns: - - @{ Role = 'Tier0' } -> $tier0Credential - - @{ Role = 'Admin' } -> $adminCredential - - @{ Domain = 'SourceAD' } -> $sourceCred - - @{ Environment = 'Production' } -> $prodCred - - .PARAMETER DefaultCredential - Optional default credential to return when no session options are provided or - when the options don't match any entry in SessionMap. - - .EXAMPLE - # Simple role-based broker - $broker = New-IdleAuthSessionBroker -SessionMap @{ - @{ Role = 'Tier0' } = $tier0Credential - @{ Role = 'Admin' } = $adminCredential - } -DefaultCredential $adminCredential - - $plan = New-IdlePlan -WorkflowPath './workflow.psd1' -Request $request -Providers @{ - Identity = New-IdleADIdentityProvider - AuthSessionBroker = $broker - } - - .EXAMPLE - # Domain-based broker for multi-forest scenarios - $broker = New-IdleAuthSessionBroker -SessionMap @{ - @{ Domain = 'SourceAD' } = $sourceCred - @{ Domain = 'TargetAD' } = $targetCred - } - - .OUTPUTS - PSCustomObject with AcquireAuthSession method - #> - [CmdletBinding()] - param( - [Parameter(Mandatory)] - [ValidateNotNull()] - [hashtable] $SessionMap, - - [Parameter()] - [AllowNull()] - [PSCredential] $DefaultCredential - ) - - # Keep meta module thin: delegate to IdLE.Core. - # Since IdLE.Core is a nested module, its exported functions are available in the current scope. - if ($PSBoundParameters.ContainsKey('DefaultCredential')) { - return IdLE.Core\New-IdleAuthSessionBroker -SessionMap $SessionMap -DefaultCredential $DefaultCredential - } - else { - return IdLE.Core\New-IdleAuthSessionBroker -SessionMap $SessionMap - } -} From 88a133ac6e61c55f006f1aa98b9dc10c5e62ea57 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 25 Jan 2026 18:09:49 +0000 Subject: [PATCH 07/14] Rename wrapper to New-IdleAuthSession.ps1 and clarify necessity - Renamed src/IdLE/Public/New-AuthSessionBroker.ps1 to New-IdleAuthSession.ps1 - Follows the *-Idle*.ps1 naming convention used by other public files - Updated comment to clarify why wrapper is necessary: PowerShell's Export-ModuleMember can only export functions in the current module's scope, not from nested modules - All 322 tests passing Co-authored-by: blindzero <13959569+blindzero@users.noreply.github.com> --- .../{New-AuthSessionBroker.ps1 => New-IdleAuthSession.ps1} | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) rename src/IdLE/Public/{New-AuthSessionBroker.ps1 => New-IdleAuthSession.ps1} (87%) diff --git a/src/IdLE/Public/New-AuthSessionBroker.ps1 b/src/IdLE/Public/New-IdleAuthSession.ps1 similarity index 87% rename from src/IdLE/Public/New-AuthSessionBroker.ps1 rename to src/IdLE/Public/New-IdleAuthSession.ps1 index 46df946d..2fbc6dac 100644 --- a/src/IdLE/Public/New-AuthSessionBroker.ps1 +++ b/src/IdLE/Public/New-IdleAuthSession.ps1 @@ -1,5 +1,7 @@ # Re-export New-IdleAuthSessionBroker from IdLE.Core. -# This avoids filename collision while keeping the wrapper minimal. +# This wrapper is necessary because PowerShell's Export-ModuleMember can only export +# functions defined in the current module's scope. The wrapper creates the function +# in IdLE's scope, allowing it to be exported. function New-IdleAuthSessionBroker { <# From 601f116d267db7b0f2c05235e7b496fcfd3bb393 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 25 Jan 2026 18:28:20 +0000 Subject: [PATCH 08/14] Keep function name as New-IdleAuthSessionBroker for v1.0 compatibility - Filename is New-IdleAuthSession.ps1 (avoids collision with Core implementation) - Function name remains New-IdleAuthSessionBroker (v1.0 public API requirement) - This matches the pattern used by other wrappers - All 322 tests passing Co-authored-by: blindzero <13959569+blindzero@users.noreply.github.com> --- src/IdLE/Public/New-IdleAuthSession.ps1 | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/IdLE/Public/New-IdleAuthSession.ps1 b/src/IdLE/Public/New-IdleAuthSession.ps1 index 2fbc6dac..d04c6f6f 100644 --- a/src/IdLE/Public/New-IdleAuthSession.ps1 +++ b/src/IdLE/Public/New-IdleAuthSession.ps1 @@ -2,6 +2,9 @@ # This wrapper is necessary because PowerShell's Export-ModuleMember can only export # functions defined in the current module's scope. The wrapper creates the function # in IdLE's scope, allowing it to be exported. +# +# The filename is New-IdleAuthSession.ps1 to avoid collision with the Core implementation, +# but the function name remains New-IdleAuthSessionBroker for v1.0 public API compatibility. function New-IdleAuthSessionBroker { <# @@ -51,3 +54,4 @@ function New-IdleAuthSessionBroker { return IdLE.Core\New-IdleAuthSessionBroker @params } + From 5e71c2148dc1c5a6f7e84a5fde8af071104fb978 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 25 Jan 2026 18:37:21 +0000 Subject: [PATCH 09/14] Rename function to New-IdleAuthSession per feedback - Changed function name from New-IdleAuthSessionBroker to New-IdleAuthSession - Updated IdLE.psd1 FunctionsToExport - Updated IdLE.psm1 Export-ModuleMember - Updated tests to expect New-IdleAuthSession - Updated documentation in architecture.md and provider docs - Core implementation remains as New-IdleAuthSessionBroker - All 322 tests passing Co-authored-by: blindzero <13959569+blindzero@users.noreply.github.com> --- docs/advanced/architecture.md | 2 +- docs/reference/providers/provider-ad.md | 8 ++++---- docs/reference/providers/provider-entraID.md | 6 +++--- src/IdLE/IdLE.psd1 | 2 +- src/IdLE/IdLE.psm1 | 2 +- src/IdLE/Public/New-IdleAuthSession.ps1 | 10 +++++----- tests/ModuleSurface.Tests.ps1 | 2 +- 7 files changed, 16 insertions(+), 16 deletions(-) diff --git a/docs/advanced/architecture.md b/docs/advanced/architecture.md index 5e695e62..10a44749 100644 --- a/docs/advanced/architecture.md +++ b/docs/advanced/architecture.md @@ -116,7 +116,7 @@ The **supported public API** for v1.0 consists of the following commands exporte - `New-IdlePlan` - `Invoke-IdlePlan` - `Export-IdlePlan` -- `New-IdleAuthSessionBroker` +- `New-IdleAuthSession` **Source of truth**: `src/IdLE/IdLE.psd1` → `FunctionsToExport` diff --git a/docs/reference/providers/provider-ad.md b/docs/reference/providers/provider-ad.md index b3ea84d1..c59bc5c2 100644 --- a/docs/reference/providers/provider-ad.md +++ b/docs/reference/providers/provider-ad.md @@ -101,7 +101,7 @@ $plan = New-IdlePlan -WorkflowPath './workflow.psd1' -Request $request -Provider Use an AuthSessionBroker to manage authentication centrally and enable multi-role scenarios. -**Simple approach with New-IdleAuthSessionBroker:** +**Simple approach with New-IdleAuthSession:** ```powershell # Assuming you have credentials available (e.g., from a secure vault or credential manager) @@ -112,7 +112,7 @@ $adminCredential = Get-Credential -Message "Enter regular admin credentials" $provider = New-IdleADIdentityProvider # Create broker with role-based credential mapping -$broker = New-IdleAuthSessionBroker -SessionMap @{ +$broker = New-IdleAuthSession -SessionMap @{ @{ Role = 'Tier0' } = $tier0Credential @{ Role = 'Admin' } = $adminCredential } -DefaultCredential $adminCredential @@ -195,8 +195,8 @@ $targetCred = Get-Credential -Message "Enter Target AD admin credentials" $sourceAD = New-IdleADIdentityProvider $targetAD = New-IdleADIdentityProvider -AllowDelete -# Use New-IdleAuthSessionBroker for domain-based credential routing -$broker = New-IdleAuthSessionBroker -SessionMap @{ +# Use New-IdleAuthSession for domain-based credential routing +$broker = New-IdleAuthSession -SessionMap @{ @{ Domain = 'Source' } = $sourceCred @{ Domain = 'Target' } = $targetCred } diff --git a/docs/reference/providers/provider-entraID.md b/docs/reference/providers/provider-entraID.md index 3095fca3..de561ee1 100644 --- a/docs/reference/providers/provider-entraID.md +++ b/docs/reference/providers/provider-entraID.md @@ -45,7 +45,7 @@ Connect-AzAccount $token = (Get-AzAccessToken -ResourceUrl "https://graph.microsoft.com").Token # Create broker -$broker = New-IdleAuthSessionBroker -SessionMap @{ +$broker = New-IdleAuthSession -SessionMap @{ @{} = $token } -DefaultCredential $token @@ -71,7 +71,7 @@ $tenantId = "your-tenant-id" $token = Get-GraphAppOnlyToken -ClientId $clientId -ClientSecret $clientSecret -TenantId $tenantId # Create broker -$broker = New-IdleAuthSessionBroker -SessionMap @{ +$broker = New-IdleAuthSession -SessionMap @{ @{} = $token } -DefaultCredential $token @@ -84,7 +84,7 @@ $broker = New-IdleAuthSessionBroker -SessionMap @{ $tier0Token = Get-GraphToken -Role 'Tier0' $adminToken = Get-GraphToken -Role 'Admin' -$broker = New-IdleAuthSessionBroker -SessionMap @{ +$broker = New-IdleAuthSession -SessionMap @{ @{ Role = 'Tier0' } = $tier0Token @{ Role = 'Admin' } = $adminToken } -DefaultCredential $adminToken diff --git a/src/IdLE/IdLE.psd1 b/src/IdLE/IdLE.psd1 index 77a3b171..7edc71b2 100644 --- a/src/IdLE/IdLE.psd1 +++ b/src/IdLE/IdLE.psd1 @@ -18,7 +18,7 @@ 'New-IdlePlan', 'Invoke-IdlePlan', 'Export-IdlePlan', - 'New-IdleAuthSessionBroker' + 'New-IdleAuthSession' ) CmdletsToExport = @() AliasesToExport = @() diff --git a/src/IdLE/IdLE.psm1 b/src/IdLE/IdLE.psm1 index cc2342fb..4d47ef1c 100644 --- a/src/IdLE/IdLE.psm1 +++ b/src/IdLE/IdLE.psm1 @@ -94,5 +94,5 @@ Export-ModuleMember -Function @( 'New-IdlePlan', 'Invoke-IdlePlan', 'Export-IdlePlan', - 'New-IdleAuthSessionBroker' + 'New-IdleAuthSession' ) diff --git a/src/IdLE/Public/New-IdleAuthSession.ps1 b/src/IdLE/Public/New-IdleAuthSession.ps1 index d04c6f6f..903e46ae 100644 --- a/src/IdLE/Public/New-IdleAuthSession.ps1 +++ b/src/IdLE/Public/New-IdleAuthSession.ps1 @@ -1,12 +1,12 @@ -# Re-export New-IdleAuthSessionBroker from IdLE.Core. +# Re-export authentication session broker functionality from IdLE.Core. # This wrapper is necessary because PowerShell's Export-ModuleMember can only export # functions defined in the current module's scope. The wrapper creates the function # in IdLE's scope, allowing it to be exported. # -# The filename is New-IdleAuthSession.ps1 to avoid collision with the Core implementation, -# but the function name remains New-IdleAuthSessionBroker for v1.0 public API compatibility. +# The function is named New-IdleAuthSession to provide a clean public API name, +# while the Core implementation remains as New-IdleAuthSessionBroker. -function New-IdleAuthSessionBroker { +function New-IdleAuthSession { <# .SYNOPSIS Creates a simple AuthSessionBroker for use with IdLE providers. @@ -25,7 +25,7 @@ function New-IdleAuthSessionBroker { Optional default credential to return when no session options are provided. .EXAMPLE - $broker = New-IdleAuthSessionBroker -SessionMap @{ + $broker = New-IdleAuthSession -SessionMap @{ @{ Role = 'Tier0' } = $tier0Credential } diff --git a/tests/ModuleSurface.Tests.ps1 b/tests/ModuleSurface.Tests.ps1 index b3b205ba..80149ecd 100644 --- a/tests/ModuleSurface.Tests.ps1 +++ b/tests/ModuleSurface.Tests.ps1 @@ -53,7 +53,7 @@ Describe 'Module manifests and public surface' { $expected = @( 'Invoke-IdlePlan' - 'New-IdleAuthSessionBroker' + 'New-IdleAuthSession' 'New-IdleLifecycleRequest' 'New-IdlePlan' 'Test-IdleWorkflow' From 8c9d42777df835315ee41ec7bfcce26e75501723 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 25 Jan 2026 18:50:51 +0000 Subject: [PATCH 10/14] Add cmdlet reference documentation for New-IdleAuthSession - Created docs/reference/cmdlets/New-IdleAuthSession.md - Updated docs/reference/cmdlets.md index to include New-IdleAuthSession - Ran Generate-IdleStepReference.ps1 (no changes needed) - All tests passing Co-authored-by: blindzero <13959569+blindzero@users.noreply.github.com> --- docs/reference/cmdlets.md | 1 + docs/reference/cmdlets/New-IdleAuthSession.md | 95 +++++++++++++++++++ 2 files changed, 96 insertions(+) create mode 100644 docs/reference/cmdlets/New-IdleAuthSession.md diff --git a/docs/reference/cmdlets.md b/docs/reference/cmdlets.md index 046e775c..96558f22 100644 --- a/docs/reference/cmdlets.md +++ b/docs/reference/cmdlets.md @@ -9,6 +9,7 @@ This page links the generated per-cmdlet reference pages and includes their syno | --- | --- | | [Export-IdlePlan](cmdlets/Export-IdlePlan.md) | Exports an IdLE LifecyclePlan as a canonical JSON artifact. | | [Invoke-IdlePlan](cmdlets/Invoke-IdlePlan.md) | Executes an IdLE plan. | +| [New-IdleAuthSession](cmdlets/New-IdleAuthSession.md) | Creates a simple AuthSessionBroker for use with IdLE providers. | | [New-IdleLifecycleRequest](cmdlets/New-IdleLifecycleRequest.md) | Creates a lifecycle request object. | | [New-IdlePlan](cmdlets/New-IdlePlan.md) | Creates a deterministic plan from a lifecycle request and a workflow definition. | | [Test-IdleWorkflow](cmdlets/Test-IdleWorkflow.md) | Validates an IdLE workflow definition file. | diff --git a/docs/reference/cmdlets/New-IdleAuthSession.md b/docs/reference/cmdlets/New-IdleAuthSession.md new file mode 100644 index 00000000..1fd052c9 --- /dev/null +++ b/docs/reference/cmdlets/New-IdleAuthSession.md @@ -0,0 +1,95 @@ +--- +external help file: IdLE-help.xml +Module Name: IdLE +online version: +schema: 2.0.0 +--- + +# New-IdleAuthSession + +## SYNOPSIS +Creates a simple AuthSessionBroker for use with IdLE providers. + +## SYNTAX + +``` +New-IdleAuthSession [-SessionMap] [[-DefaultCredential] ] + [-ProgressAction ] [] +``` + +## DESCRIPTION +Creates an AuthSessionBroker that routes authentication based on user-defined options. +The broker is used by steps to acquire credentials at runtime without embedding +secrets in workflows or provider construction. + +This is a thin wrapper that delegates to IdLE.Core\New-IdleAuthSessionBroker. + +## EXAMPLES + +### EXAMPLE 1 +``` +$broker = New-IdleAuthSession -SessionMap @{ + @{ Role = 'Tier0' } = $tier0Credential +} +``` + +## PARAMETERS + +### -SessionMap +A hashtable that maps session configurations to credentials. + +```yaml +Type: Hashtable +Parameter Sets: (All) +Aliases: + +Required: True +Position: 1 +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -DefaultCredential +Optional default credential to return when no session options are provided. + +```yaml +Type: PSCredential +Parameter Sets: (All) +Aliases: + +Required: False +Position: 2 +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -ProgressAction +{{ Fill ProgressAction Description }} + +```yaml +Type: ActionPreference +Parameter Sets: (All) +Aliases: proga + +Required: False +Position: Named +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + +### CommonParameters +This cmdlet supports the common parameters: -Debug, -ErrorAction, -ErrorVariable, -InformationAction, -InformationVariable, -OutVariable, -OutBuffer, -PipelineVariable, -Verbose, -WarningAction, and -WarningVariable. For more information, see [about_CommonParameters](http://go.microsoft.com/fwlink/?LinkID=113216). + +## INPUTS + +## OUTPUTS + +### PSCustomObject with AcquireAuthSession method + +## NOTES +For detailed documentation, see: Get-Help IdLE.Core\New-IdleAuthSessionBroker -Full + +## RELATED LINKS From 5c6a629334631e5bb0f4b48b8304974868a23efd Mon Sep 17 00:00:00 2001 From: Matthias Fleschuetz <13959569+blindzero@users.noreply.github.com> Date: Sun, 25 Jan 2026 19:59:35 +0100 Subject: [PATCH 11/14] updated cmdlet reference --- docs/reference/cmdlets/New-IdleAuthSession.md | 1 - 1 file changed, 1 deletion(-) diff --git a/docs/reference/cmdlets/New-IdleAuthSession.md b/docs/reference/cmdlets/New-IdleAuthSession.md index 1fd052c9..a04183bf 100644 --- a/docs/reference/cmdlets/New-IdleAuthSession.md +++ b/docs/reference/cmdlets/New-IdleAuthSession.md @@ -88,7 +88,6 @@ This cmdlet supports the common parameters: -Debug, -ErrorAction, -ErrorVariable ## OUTPUTS ### PSCustomObject with AcquireAuthSession method - ## NOTES For detailed documentation, see: Get-Help IdLE.Core\New-IdleAuthSessionBroker -Full From 9440d0c8e47c2707f7a1f9d69e623c4b52c6750b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 25 Jan 2026 19:06:25 +0000 Subject: [PATCH 12/14] Fix: Suppress internal module warnings during correct IdLE import - Changed warning behavior to opt-in via IDLE_WARN_INTERNAL_IMPORT - Avoids false positives when IdLE.Core/IdLE.Steps.Common load as nested modules - Prevents failures in warning-as-error environments ($WarningPreference = 'Stop') - Updated tests to reflect new behavior - All 322 tests passing Co-authored-by: blindzero <13959569+blindzero@users.noreply.github.com> --- src/IdLE.Core/IdLE.Core.psm1 | 7 +++- src/IdLE.Steps.Common/IdLE.Steps.Common.psm1 | 7 +++- tests/ModuleSurface.Tests.ps1 | 39 ++++++++++---------- 3 files changed, 29 insertions(+), 24 deletions(-) diff --git a/src/IdLE.Core/IdLE.Core.psm1 b/src/IdLE.Core/IdLE.Core.psm1 index 80dea9da..7aa8c367 100644 --- a/src/IdLE.Core/IdLE.Core.psm1 +++ b/src/IdLE.Core/IdLE.Core.psm1 @@ -3,8 +3,11 @@ Set-StrictMode -Version Latest # Internal module warning: discourage direct import unless explicitly allowed -if (-not $env:IDLE_ALLOW_INTERNAL_IMPORT) { - Write-Warning "IdLE.Core is an internal/unsupported module. Import 'IdLE' instead for the supported public API. To bypass this warning, set IDLE_ALLOW_INTERNAL_IMPORT=1." +# Note: Warning is suppressed when loaded as a nested module of IdLE to avoid +# false positives in correct usage scenarios. Direct imports outside the IdLE +# ecosystem will not trigger a warning, but are unsupported per documentation. +if ($env:IDLE_WARN_INTERNAL_IMPORT -eq '1') { + Write-Warning "IdLE.Core is an internal/unsupported module. Import 'IdLE' instead for the supported public API." } $PublicPath = Join-Path -Path $PSScriptRoot -ChildPath 'Public' diff --git a/src/IdLE.Steps.Common/IdLE.Steps.Common.psm1 b/src/IdLE.Steps.Common/IdLE.Steps.Common.psm1 index 1f203a46..de4073d6 100644 --- a/src/IdLE.Steps.Common/IdLE.Steps.Common.psm1 +++ b/src/IdLE.Steps.Common/IdLE.Steps.Common.psm1 @@ -2,8 +2,11 @@ Set-StrictMode -Version Latest # Internal module warning: discourage direct import unless explicitly allowed -if (-not $env:IDLE_ALLOW_INTERNAL_IMPORT) { - Write-Warning "IdLE.Steps.Common is an internal/unsupported module. Import 'IdLE' instead for the supported public API. To bypass this warning, set IDLE_ALLOW_INTERNAL_IMPORT=1." +# Note: Warning is suppressed when loaded as a nested module of IdLE to avoid +# false positives in correct usage scenarios. Direct imports outside the IdLE +# ecosystem will not trigger a warning, but are unsupported per documentation. +if ($env:IDLE_WARN_INTERNAL_IMPORT -eq '1') { + Write-Warning "IdLE.Steps.Common is an internal/unsupported module. Import 'IdLE' instead for the supported public API." } $PrivatePath = Join-Path -Path $PSScriptRoot -ChildPath 'Private' diff --git a/tests/ModuleSurface.Tests.ps1 b/tests/ModuleSurface.Tests.ps1 index 80149ecd..d060897a 100644 --- a/tests/ModuleSurface.Tests.ps1 +++ b/tests/ModuleSurface.Tests.ps1 @@ -207,70 +207,69 @@ Describe 'Module manifests and public surface' { } Context 'Internal module import warnings' { - It 'IdLE.Core emits warning when imported directly without bypass' { + It 'IdLE.Core does not emit warning by default (avoid false positives during nested load)' { $existingModule = Get-Module -Name IdLE.Core if ($existingModule) { - Set-ItResult -Skipped -Because "IdLE.Core is already loaded; cannot test direct import warning" + Set-ItResult -Skipped -Because "IdLE.Core is already loaded; cannot test direct import behavior" return } - $originalValue = $env:IDLE_ALLOW_INTERNAL_IMPORT + $originalValue = $env:IDLE_WARN_INTERNAL_IMPORT try { - $env:IDLE_ALLOW_INTERNAL_IMPORT = $null + $env:IDLE_WARN_INTERNAL_IMPORT = $null # Import and capture warning output $output = Import-Module $corePsd1 -Force 3>&1 | Out-String - $output | Should -Not -BeNullOrEmpty -Because "Internal module should emit warning on direct import" - $output | Should -Match "internal.*unsupported.*IdLE.*instead" -Because "Warning should indicate module is internal and suggest importing IdLE" + $output | Should -BeNullOrEmpty -Because "Internal modules should not warn by default to avoid false positives when correctly loaded via IdLE" } finally { - $env:IDLE_ALLOW_INTERNAL_IMPORT = $originalValue + $env:IDLE_WARN_INTERNAL_IMPORT = $originalValue Remove-Module IdLE.Core -Force -ErrorAction SilentlyContinue } } - It 'IdLE.Core does not emit warning when IDLE_ALLOW_INTERNAL_IMPORT is set' { + It 'IdLE.Core emits warning when IDLE_WARN_INTERNAL_IMPORT is set' { $existingModule = Get-Module -Name IdLE.Core if ($existingModule) { - Set-ItResult -Skipped -Because "IdLE.Core is already loaded; cannot test bypass" + Set-ItResult -Skipped -Because "IdLE.Core is already loaded; cannot test direct import warning" return } - $originalValue = $env:IDLE_ALLOW_INTERNAL_IMPORT + $originalValue = $env:IDLE_WARN_INTERNAL_IMPORT try { - $env:IDLE_ALLOW_INTERNAL_IMPORT = '1' + $env:IDLE_WARN_INTERNAL_IMPORT = '1' # Import and capture warning output $output = Import-Module $corePsd1 -Force 3>&1 | Out-String - $output | Should -BeNullOrEmpty -Because "Internal module should not emit warning when bypass is set" + $output | Should -Not -BeNullOrEmpty -Because "Internal module should emit warning when explicitly requested" + $output | Should -Match "internal.*unsupported.*IdLE.*instead" -Because "Warning should indicate module is internal and suggest importing IdLE" } finally { - $env:IDLE_ALLOW_INTERNAL_IMPORT = $originalValue + $env:IDLE_WARN_INTERNAL_IMPORT = $originalValue Remove-Module IdLE.Core -Force -ErrorAction SilentlyContinue } } - It 'IdLE.Steps.Common emits warning when imported directly without bypass' { + It 'IdLE.Steps.Common does not emit warning by default' { $existingModule = Get-Module -Name IdLE.Steps.Common if ($existingModule) { - Set-ItResult -Skipped -Because "IdLE.Steps.Common is already loaded; cannot test direct import warning" + Set-ItResult -Skipped -Because "IdLE.Steps.Common is already loaded; cannot test direct import behavior" return } - $originalValue = $env:IDLE_ALLOW_INTERNAL_IMPORT + $originalValue = $env:IDLE_WARN_INTERNAL_IMPORT try { - $env:IDLE_ALLOW_INTERNAL_IMPORT = $null + $env:IDLE_WARN_INTERNAL_IMPORT = $null # Import and capture warning output $output = Import-Module $stepsPsd1 -Force 3>&1 | Out-String - $output | Should -Not -BeNullOrEmpty -Because "Internal module should emit warning on direct import" - $output | Should -Match "internal.*unsupported.*IdLE.*instead" -Because "Warning should indicate module is internal and suggest importing IdLE" + $output | Should -BeNullOrEmpty -Because "Internal modules should not warn by default" } finally { - $env:IDLE_ALLOW_INTERNAL_IMPORT = $originalValue + $env:IDLE_WARN_INTERNAL_IMPORT = $originalValue Remove-Module IdLE.Steps.Common -Force -ErrorAction SilentlyContinue } } From 63bc5f608ddf04322dd2088c5d6189d5cdc93220 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 25 Jan 2026 19:31:21 +0000 Subject: [PATCH 13/14] Fix: Restore opt-out warnings with ScriptsToProcess bypass - Reverted to opt-out approach: warnings appear by default on direct import - Added IdLE.Init.ps1 script that runs via ScriptsToProcess before NestedModules load - IdLE.Init.ps1 sets IDLE_ALLOW_INTERNAL_IMPORT=1 to suppress warnings during correct import - Direct imports of IdLE.Core/IdLE.Steps.Common now warn as intended - Importing IdLE correctly produces no warnings - Updated tests to verify new behavior - All 323 tests passing Co-authored-by: blindzero <13959569+blindzero@users.noreply.github.com> --- src/IdLE.Core/IdLE.Core.psm1 | 9 ++-- src/IdLE.Steps.Common/IdLE.Steps.Common.psm1 | 9 ++-- src/IdLE/IdLE.Init.ps1 | 4 ++ src/IdLE/IdLE.psd1 | 4 ++ tests/ModuleSurface.Tests.ps1 | 55 +++++++++++++------- 5 files changed, 52 insertions(+), 29 deletions(-) create mode 100644 src/IdLE/IdLE.Init.ps1 diff --git a/src/IdLE.Core/IdLE.Core.psm1 b/src/IdLE.Core/IdLE.Core.psm1 index 7aa8c367..9e54b336 100644 --- a/src/IdLE.Core/IdLE.Core.psm1 +++ b/src/IdLE.Core/IdLE.Core.psm1 @@ -3,11 +3,10 @@ Set-StrictMode -Version Latest # Internal module warning: discourage direct import unless explicitly allowed -# Note: Warning is suppressed when loaded as a nested module of IdLE to avoid -# false positives in correct usage scenarios. Direct imports outside the IdLE -# ecosystem will not trigger a warning, but are unsupported per documentation. -if ($env:IDLE_WARN_INTERNAL_IMPORT -eq '1') { - Write-Warning "IdLE.Core is an internal/unsupported module. Import 'IdLE' instead for the supported public API." +# Suppress warning if IDLE_ALLOW_INTERNAL_IMPORT is set +# (IdLE meta-module sets this automatically; users can also set it for advanced scenarios) +if (-not $env:IDLE_ALLOW_INTERNAL_IMPORT) { + Write-Warning "IdLE.Core is an internal/unsupported module. Import 'IdLE' instead for the supported public API. To bypass: set IDLE_ALLOW_INTERNAL_IMPORT=1." } $PublicPath = Join-Path -Path $PSScriptRoot -ChildPath 'Public' diff --git a/src/IdLE.Steps.Common/IdLE.Steps.Common.psm1 b/src/IdLE.Steps.Common/IdLE.Steps.Common.psm1 index de4073d6..163f731d 100644 --- a/src/IdLE.Steps.Common/IdLE.Steps.Common.psm1 +++ b/src/IdLE.Steps.Common/IdLE.Steps.Common.psm1 @@ -2,11 +2,10 @@ Set-StrictMode -Version Latest # Internal module warning: discourage direct import unless explicitly allowed -# Note: Warning is suppressed when loaded as a nested module of IdLE to avoid -# false positives in correct usage scenarios. Direct imports outside the IdLE -# ecosystem will not trigger a warning, but are unsupported per documentation. -if ($env:IDLE_WARN_INTERNAL_IMPORT -eq '1') { - Write-Warning "IdLE.Steps.Common is an internal/unsupported module. Import 'IdLE' instead for the supported public API." +# Suppress warning if IDLE_ALLOW_INTERNAL_IMPORT is set +# (IdLE meta-module sets this automatically; users can also set it for advanced scenarios) +if (-not $env:IDLE_ALLOW_INTERNAL_IMPORT) { + Write-Warning "IdLE.Steps.Common is an internal/unsupported module. Import 'IdLE' instead for the supported public API. To bypass: set IDLE_ALLOW_INTERNAL_IMPORT=1." } $PrivatePath = Join-Path -Path $PSScriptRoot -ChildPath 'Private' diff --git a/src/IdLE/IdLE.Init.ps1 b/src/IdLE/IdLE.Init.ps1 new file mode 100644 index 00000000..9f97d419 --- /dev/null +++ b/src/IdLE/IdLE.Init.ps1 @@ -0,0 +1,4 @@ +# IdLE Module Initialization Script +# This script runs BEFORE nested modules are loaded (via ScriptsToProcess in manifest) +# Set environment variable to suppress internal module warnings during correct nested load +$env:IDLE_ALLOW_INTERNAL_IMPORT = '1' diff --git a/src/IdLE/IdLE.psd1 b/src/IdLE/IdLE.psd1 index 7edc71b2..14b107fc 100644 --- a/src/IdLE/IdLE.psd1 +++ b/src/IdLE/IdLE.psd1 @@ -7,6 +7,10 @@ Description = 'IdentityLifecycleEngine (IdLE) meta-module. Imports IdLE.Core and optional packs.' PowerShellVersion = '7.0' + # ScriptsToProcess runs BEFORE NestedModules are loaded + # This allows us to set environment variables to suppress internal module warnings + ScriptsToProcess = @('IdLE.Init.ps1') + NestedModules = @( '..\IdLE.Core\IdLE.Core.psd1', '..\IdLE.Steps.Common\IdLE.Steps.Common.psd1' diff --git a/tests/ModuleSurface.Tests.ps1 b/tests/ModuleSurface.Tests.ps1 index d060897a..a5a43167 100644 --- a/tests/ModuleSurface.Tests.ps1 +++ b/tests/ModuleSurface.Tests.ps1 @@ -207,71 +207,88 @@ Describe 'Module manifests and public surface' { } Context 'Internal module import warnings' { - It 'IdLE.Core does not emit warning by default (avoid false positives during nested load)' { + It 'IdLE.Core emits warning when imported directly' { $existingModule = Get-Module -Name IdLE.Core if ($existingModule) { - Set-ItResult -Skipped -Because "IdLE.Core is already loaded; cannot test direct import behavior" + Set-ItResult -Skipped -Because "IdLE.Core is already loaded; cannot test direct import warning" return } - $originalValue = $env:IDLE_WARN_INTERNAL_IMPORT + $originalValue = $env:IDLE_ALLOW_INTERNAL_IMPORT try { - $env:IDLE_WARN_INTERNAL_IMPORT = $null + $env:IDLE_ALLOW_INTERNAL_IMPORT = $null # Import and capture warning output $output = Import-Module $corePsd1 -Force 3>&1 | Out-String - $output | Should -BeNullOrEmpty -Because "Internal modules should not warn by default to avoid false positives when correctly loaded via IdLE" + $output | Should -Not -BeNullOrEmpty -Because "Internal module should emit warning on direct import" + $output | Should -Match "internal.*unsupported.*IdLE.*instead" -Because "Warning should indicate module is internal and suggest importing IdLE" } finally { - $env:IDLE_WARN_INTERNAL_IMPORT = $originalValue + $env:IDLE_ALLOW_INTERNAL_IMPORT = $originalValue Remove-Module IdLE.Core -Force -ErrorAction SilentlyContinue } } - It 'IdLE.Core emits warning when IDLE_WARN_INTERNAL_IMPORT is set' { + It 'IdLE.Core does not emit warning when IDLE_ALLOW_INTERNAL_IMPORT is set' { $existingModule = Get-Module -Name IdLE.Core if ($existingModule) { - Set-ItResult -Skipped -Because "IdLE.Core is already loaded; cannot test direct import warning" + Set-ItResult -Skipped -Because "IdLE.Core is already loaded; cannot test bypass" return } - $originalValue = $env:IDLE_WARN_INTERNAL_IMPORT + $originalValue = $env:IDLE_ALLOW_INTERNAL_IMPORT try { - $env:IDLE_WARN_INTERNAL_IMPORT = '1' + $env:IDLE_ALLOW_INTERNAL_IMPORT = '1' # Import and capture warning output $output = Import-Module $corePsd1 -Force 3>&1 | Out-String - $output | Should -Not -BeNullOrEmpty -Because "Internal module should emit warning when explicitly requested" - $output | Should -Match "internal.*unsupported.*IdLE.*instead" -Because "Warning should indicate module is internal and suggest importing IdLE" + $output | Should -BeNullOrEmpty -Because "Internal module should not emit warning when bypass is set" } finally { - $env:IDLE_WARN_INTERNAL_IMPORT = $originalValue + $env:IDLE_ALLOW_INTERNAL_IMPORT = $originalValue Remove-Module IdLE.Core -Force -ErrorAction SilentlyContinue } } - It 'IdLE.Steps.Common does not emit warning by default' { + It 'IdLE.Steps.Common emits warning when imported directly' { $existingModule = Get-Module -Name IdLE.Steps.Common if ($existingModule) { - Set-ItResult -Skipped -Because "IdLE.Steps.Common is already loaded; cannot test direct import behavior" + Set-ItResult -Skipped -Because "IdLE.Steps.Common is already loaded; cannot test direct import warning" return } - $originalValue = $env:IDLE_WARN_INTERNAL_IMPORT + $originalValue = $env:IDLE_ALLOW_INTERNAL_IMPORT try { - $env:IDLE_WARN_INTERNAL_IMPORT = $null + $env:IDLE_ALLOW_INTERNAL_IMPORT = $null # Import and capture warning output $output = Import-Module $stepsPsd1 -Force 3>&1 | Out-String - $output | Should -BeNullOrEmpty -Because "Internal modules should not warn by default" + $output | Should -Not -BeNullOrEmpty -Because "Internal module should emit warning on direct import" + $output | Should -Match "internal.*unsupported.*IdLE.*instead" -Because "Warning should indicate module is internal and suggest importing IdLE" } finally { - $env:IDLE_WARN_INTERNAL_IMPORT = $originalValue + $env:IDLE_ALLOW_INTERNAL_IMPORT = $originalValue Remove-Module IdLE.Steps.Common -Force -ErrorAction SilentlyContinue } } + + It 'IdLE meta-module does not emit internal module warnings' { + $originalValue = $env:IDLE_ALLOW_INTERNAL_IMPORT + try { + $env:IDLE_ALLOW_INTERNAL_IMPORT = $null + + # Import and capture warning output + $output = Import-Module $idlePsd1 -Force 3>&1 | Out-String + + $output | Should -BeNullOrEmpty -Because "IdLE meta-module should suppress internal module warnings via ScriptsToProcess" + } + finally { + $env:IDLE_ALLOW_INTERNAL_IMPORT = $originalValue + Remove-Module IdLE -Force -ErrorAction SilentlyContinue + } + } } } From 9d906c253d3ce13dd602b09fe6cb36f7a73069b9 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 25 Jan 2026 19:42:33 +0000 Subject: [PATCH 14/14] Fix: Clarify bypass syntax in internal module warnings - Updated warning messages to show correct PowerShell syntax: `$env:IDLE_ALLOW_INTERNAL_IMPORT = '1'` - Previous message was ambiguous ("set IDLE_ALLOW_INTERNAL_IMPORT=1") - Users were incorrectly using Set-Variable instead of $env: - Updated tests to verify correct syntax appears in warnings - All 323 tests passing Co-authored-by: blindzero <13959569+blindzero@users.noreply.github.com> --- src/IdLE.Core/IdLE.Core.psm1 | 2 +- src/IdLE.Steps.Common/IdLE.Steps.Common.psm1 | 2 +- tests/ModuleSurface.Tests.ps1 | 2 ++ 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/IdLE.Core/IdLE.Core.psm1 b/src/IdLE.Core/IdLE.Core.psm1 index 9e54b336..5631c354 100644 --- a/src/IdLE.Core/IdLE.Core.psm1 +++ b/src/IdLE.Core/IdLE.Core.psm1 @@ -6,7 +6,7 @@ Set-StrictMode -Version Latest # Suppress warning if IDLE_ALLOW_INTERNAL_IMPORT is set # (IdLE meta-module sets this automatically; users can also set it for advanced scenarios) if (-not $env:IDLE_ALLOW_INTERNAL_IMPORT) { - Write-Warning "IdLE.Core is an internal/unsupported module. Import 'IdLE' instead for the supported public API. To bypass: set IDLE_ALLOW_INTERNAL_IMPORT=1." + Write-Warning "IdLE.Core is an internal/unsupported module. Import 'IdLE' instead for the supported public API. To bypass: `$env:IDLE_ALLOW_INTERNAL_IMPORT = '1'" } $PublicPath = Join-Path -Path $PSScriptRoot -ChildPath 'Public' diff --git a/src/IdLE.Steps.Common/IdLE.Steps.Common.psm1 b/src/IdLE.Steps.Common/IdLE.Steps.Common.psm1 index 163f731d..3485dbd2 100644 --- a/src/IdLE.Steps.Common/IdLE.Steps.Common.psm1 +++ b/src/IdLE.Steps.Common/IdLE.Steps.Common.psm1 @@ -5,7 +5,7 @@ Set-StrictMode -Version Latest # Suppress warning if IDLE_ALLOW_INTERNAL_IMPORT is set # (IdLE meta-module sets this automatically; users can also set it for advanced scenarios) if (-not $env:IDLE_ALLOW_INTERNAL_IMPORT) { - Write-Warning "IdLE.Steps.Common is an internal/unsupported module. Import 'IdLE' instead for the supported public API. To bypass: set IDLE_ALLOW_INTERNAL_IMPORT=1." + Write-Warning "IdLE.Steps.Common is an internal/unsupported module. Import 'IdLE' instead for the supported public API. To bypass: `$env:IDLE_ALLOW_INTERNAL_IMPORT = '1'" } $PrivatePath = Join-Path -Path $PSScriptRoot -ChildPath 'Private' diff --git a/tests/ModuleSurface.Tests.ps1 b/tests/ModuleSurface.Tests.ps1 index a5a43167..94473ea1 100644 --- a/tests/ModuleSurface.Tests.ps1 +++ b/tests/ModuleSurface.Tests.ps1 @@ -223,6 +223,7 @@ Describe 'Module manifests and public surface' { $output | Should -Not -BeNullOrEmpty -Because "Internal module should emit warning on direct import" $output | Should -Match "internal.*unsupported.*IdLE.*instead" -Because "Warning should indicate module is internal and suggest importing IdLE" + $output | Should -Match '\$env:IDLE_ALLOW_INTERNAL_IMPORT' -Because "Warning should show correct PowerShell syntax for bypass" } finally { $env:IDLE_ALLOW_INTERNAL_IMPORT = $originalValue @@ -268,6 +269,7 @@ Describe 'Module manifests and public surface' { $output | Should -Not -BeNullOrEmpty -Because "Internal module should emit warning on direct import" $output | Should -Match "internal.*unsupported.*IdLE.*instead" -Because "Warning should indicate module is internal and suggest importing IdLE" + $output | Should -Match '\$env:IDLE_ALLOW_INTERNAL_IMPORT' -Because "Warning should show correct PowerShell syntax for bypass" } finally { $env:IDLE_ALLOW_INTERNAL_IMPORT = $originalValue