From 54aa2951e240df89156222cb21b64e2fef871a2d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 8 Feb 2026 11:14:21 +0000 Subject: [PATCH 1/9] Initial plan From 5146b0cb3732b1ca351c4b13a3e1b14dc15d0cba Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 8 Feb 2026 11:18:52 +0000 Subject: [PATCH 2/9] Implement provider fallback in Invoke-IdlePlanObject with tests Co-authored-by: blindzero <13959569+blindzero@users.noreply.github.com> --- .../Public/Invoke-IdlePlanObject.ps1 | 47 +++- src/IdLE/Public/Invoke-IdlePlan.ps1 | 18 +- ...Invoke-IdlePlan.ProviderFallback.Tests.ps1 | 238 ++++++++++++++++++ 3 files changed, 291 insertions(+), 12 deletions(-) create mode 100644 tests/Core/Invoke-IdlePlan.ProviderFallback.Tests.ps1 diff --git a/src/IdLE.Core/Public/Invoke-IdlePlanObject.ps1 b/src/IdLE.Core/Public/Invoke-IdlePlanObject.ps1 index c37a0045..d8ac2cf7 100644 --- a/src/IdLE.Core/Public/Invoke-IdlePlanObject.ps1 +++ b/src/IdLE.Core/Public/Invoke-IdlePlanObject.ps1 @@ -6,6 +6,11 @@ function Invoke-IdlePlanObject { .DESCRIPTION Executes steps in order, emits structured events, and returns a stable execution result. + Provider resolution: + - If -Providers is supplied, it is used for execution. + - If -Providers is not supplied (null), Plan.Providers is used if available. + - If neither is present, execution fails early with a clear error message. + Security: - ScriptBlocks are rejected in plan and providers. - The returned execution result is an output boundary: Providers are redacted. @@ -15,6 +20,8 @@ function Invoke-IdlePlanObject { .PARAMETER Providers Provider registry/collection passed through to execution. + If omitted and Plan.Providers exists, Plan.Providers will be used. + If supplied, overrides Plan.Providers. .PARAMETER EventSink Optional external event sink object. Must provide a WriteEvent(event) method. @@ -96,18 +103,38 @@ function Invoke-IdlePlanObject { } Assert-IdleNoScriptBlock -InputObject $Plan -Path 'Plan' - Assert-IdleNoScriptBlock -InputObject $Providers -Path 'Providers' + + # Resolve effective providers: explicit -Providers parameter takes precedence, otherwise use Plan.Providers. + # This allows the common workflow: build plan with providers once, execute without re-supplying them. + $effectiveProviders = $Providers + if ($null -eq $effectiveProviders) { + if ($planPropNames -contains 'Providers') { + $planProviders = $Plan.Providers + if ($null -ne $planProviders -and $planProviders -is [System.Collections.IDictionary]) { + $effectiveProviders = $planProviders + } + } + } + + # Early validation: fail with a clear message if no providers are available. + if ($null -eq $effectiveProviders) { + throw [System.InvalidOperationException]::new( + 'Providers are required. Provide -Providers to Invoke-IdlePlan or build the plan with Providers.' + ) + } + + Assert-IdleNoScriptBlock -InputObject $effectiveProviders -Path 'Providers' # Validate ExecutionOptions Assert-IdleExecutionOptions -ExecutionOptions $ExecutionOptions # StepRegistry is constructed via helper to ensure built-in steps and host-provided steps can co-exist. - $stepRegistry = Get-IdleStepRegistry -Providers $Providers + $stepRegistry = Get-IdleStepRegistry -Providers $effectiveProviders $context = [pscustomobject]@{ PSTypeName = 'IdLE.ExecutionContext' Plan = $Plan - Providers = $Providers + Providers = $effectiveProviders EventSink = $engineEventSink } @@ -202,14 +229,14 @@ function Invoke-IdlePlanObject { if ($requiresAuthBroker) { $broker = $null - if ($Providers -is [System.Collections.IDictionary]) { - if ($Providers.Contains('AuthSessionBroker')) { - $broker = $Providers['AuthSessionBroker'] + if ($effectiveProviders -is [System.Collections.IDictionary]) { + if ($effectiveProviders.Contains('AuthSessionBroker')) { + $broker = $effectiveProviders['AuthSessionBroker'] } } else { - if ($null -ne $Providers -and $Providers.PSObject.Properties.Name -contains 'AuthSessionBroker') { - $broker = $Providers.AuthSessionBroker + if ($null -ne $effectiveProviders -and $effectiveProviders.PSObject.Properties.Name -contains 'AuthSessionBroker') { + $broker = $effectiveProviders.AuthSessionBroker } } @@ -620,8 +647,8 @@ function Invoke-IdlePlanObject { # Issue #48: # Redact provider configuration/state at the output boundary (execution result). - $redactedProviders = if ($null -ne $Providers) { - Copy-IdleRedactedObject -Value $Providers + $redactedProviders = if ($null -ne $effectiveProviders) { + Copy-IdleRedactedObject -Value $effectiveProviders } else { $null diff --git a/src/IdLE/Public/Invoke-IdlePlan.ps1 b/src/IdLE/Public/Invoke-IdlePlan.ps1 index 4fde87f9..2fc5e5f8 100644 --- a/src/IdLE/Public/Invoke-IdlePlan.ps1 +++ b/src/IdLE/Public/Invoke-IdlePlan.ps1 @@ -7,11 +7,18 @@ function Invoke-IdlePlan { Executes a plan deterministically and emits structured events. Delegates execution to IdLE.Core. + Provider resolution: + - If -Providers is supplied, it is used for execution. + - If -Providers is not supplied, Plan.Providers is used if available. + - If neither is present, execution fails early with a clear error message. + .PARAMETER Plan The plan object created by New-IdlePlan. .PARAMETER Providers Provider registry/collection passed through to execution. + If omitted and Plan.Providers exists, Plan.Providers will be used. + If supplied, overrides Plan.Providers. .PARAMETER EventSink Optional external event sink for streaming. Must be an object with a WriteEvent(event) method. @@ -21,7 +28,14 @@ function Invoke-IdlePlan { Must be a hashtable with optional keys: RetryProfiles, DefaultRetryProfile. .EXAMPLE - Invoke-IdlePlan -Plan $plan -Providers $providers + # Default: plan built with providers, execution uses Plan.Providers + $providers = @{ Identity = $provider; AuthSessionBroker = $broker } + $plan = New-IdlePlan -WorkflowPath ./joiner.psd1 -Request $req -Providers $providers + Invoke-IdlePlan -Plan $plan + + .EXAMPLE + # Override: explicit -Providers at invoke time + Invoke-IdlePlan -Plan $plan -Providers $otherProviders .EXAMPLE $execOptions = @{ @@ -31,7 +45,7 @@ function Invoke-IdlePlan { } DefaultRetryProfile = 'Default' } - Invoke-IdlePlan -Plan $plan -Providers $providers -ExecutionOptions $execOptions + Invoke-IdlePlan -Plan $plan -ExecutionOptions $execOptions .OUTPUTS PSCustomObject (PSTypeName: IdLE.ExecutionResult) diff --git a/tests/Core/Invoke-IdlePlan.ProviderFallback.Tests.ps1 b/tests/Core/Invoke-IdlePlan.ProviderFallback.Tests.ps1 new file mode 100644 index 00000000..ae642bfd --- /dev/null +++ b/tests/Core/Invoke-IdlePlan.ProviderFallback.Tests.ps1 @@ -0,0 +1,238 @@ +BeforeAll { + . (Join-Path (Split-Path -Path $PSScriptRoot -Parent) '_testHelpers.ps1') + Import-IdleTestModule + + # Test step handler for provider fallback tests + function global:Invoke-IdleTestProviderFallbackStep { + [CmdletBinding()] + param( + [Parameter(Mandatory)] + [ValidateNotNull()] + [object] $Context, + + [Parameter(Mandatory)] + [ValidateNotNull()] + [object] $Step + ) + + # Verify Context.Providers is not null (reproduces the original failure scenario) + if ($null -eq $Context.Providers) { + throw "Context.Providers must be a hashtable." + } + + return [pscustomobject]@{ + PSTypeName = 'IdLE.StepResult' + Name = [string]$Step.Name + Type = [string]$Step.Type + Status = 'Completed' + Error = $null + } + } +} + +AfterAll { + Remove-Item -Path 'Function:\Invoke-IdleTestProviderFallbackStep' -ErrorAction SilentlyContinue +} + +Describe 'Invoke-IdlePlan Provider Fallback' { + It 'uses Plan.Providers when -Providers is not supplied' { + $wfPath = Join-Path -Path $TestDrive -ChildPath 'joiner.psd1' + Set-Content -Path $wfPath -Encoding UTF8 -Value @' +@{ + Name = 'Joiner - Standard' + LifecycleEvent = 'Joiner' + Steps = @( + @{ Name = 'TestStep'; Type = 'IdLE.Step.Test' } + ) +} +'@ + + $req = New-IdleLifecycleRequest -LifecycleEvent 'Joiner' + + $providers = @{ + StepRegistry = @{ + 'IdLE.Step.Test' = 'Invoke-IdleTestProviderFallbackStep' + } + StepMetadata = New-IdleTestStepMetadata -StepTypes @('IdLE.Step.Test') + } + + # Build plan with providers + $plan = New-IdlePlan -WorkflowPath $wfPath -Request $req -Providers $providers + + # Execute without passing -Providers (should use Plan.Providers) + $result = Invoke-IdlePlan -Plan $plan + + $result.PSTypeNames | Should -Contain 'IdLE.ExecutionResult' + $result.Status | Should -Be 'Completed' + $result.Steps[0].Status | Should -Be 'Completed' + } + + It 'explicit -Providers overrides Plan.Providers' { + $wfPath = Join-Path -Path $TestDrive -ChildPath 'joiner.psd1' + Set-Content -Path $wfPath -Encoding UTF8 -Value @' +@{ + Name = 'Joiner - Standard' + LifecycleEvent = 'Joiner' + Steps = @( + @{ Name = 'TestStep'; Type = 'IdLE.Step.Test' } + ) +} +'@ + + $req = New-IdleLifecycleRequest -LifecycleEvent 'Joiner' + + $planProviders = @{ + StepRegistry = @{ + 'IdLE.Step.Test' = 'Invoke-IdleTestProviderFallbackStep' + } + StepMetadata = New-IdleTestStepMetadata -StepTypes @('IdLE.Step.Test') + TestMarker = 'PlanProviders' + } + + $explicitProviders = @{ + StepRegistry = @{ + 'IdLE.Step.Test' = 'Invoke-IdleTestProviderFallbackStep' + } + StepMetadata = New-IdleTestStepMetadata -StepTypes @('IdLE.Step.Test') + TestMarker = 'ExplicitProviders' + } + + # Build plan with planProviders + $plan = New-IdlePlan -WorkflowPath $wfPath -Request $req -Providers $planProviders + + # Execute with explicit providers (should override Plan.Providers) + $result = Invoke-IdlePlan -Plan $plan -Providers $explicitProviders + + $result.PSTypeNames | Should -Contain 'IdLE.ExecutionResult' + $result.Status | Should -Be 'Completed' + # Verify that explicitProviders were used (check redacted providers) + $result.Providers.TestMarker | Should -Be 'ExplicitProviders' + } + + It 'fails with clear error when neither -Providers nor Plan.Providers exist' { + $wfPath = Join-Path -Path $TestDrive -ChildPath 'joiner.psd1' + Set-Content -Path $wfPath -Encoding UTF8 -Value @' +@{ + Name = 'Joiner - Standard' + LifecycleEvent = 'Joiner' + Steps = @( + @{ Name = 'TestStep'; Type = 'IdLE.Step.Test' } + ) +} +'@ + + $req = New-IdleLifecycleRequest -LifecycleEvent 'Joiner' + + # Build plan without providers + $providers = @{ + StepRegistry = @{ + 'IdLE.Step.Test' = 'Invoke-IdleTestProviderFallbackStep' + } + StepMetadata = New-IdleTestStepMetadata -StepTypes @('IdLE.Step.Test') + } + $plan = New-IdlePlan -WorkflowPath $wfPath -Request $req -Providers $providers + + # Manually remove Providers from plan to simulate exported plan scenario + $plan.PSObject.Properties.Remove('Providers') + + # Execute without -Providers and without Plan.Providers + { Invoke-IdlePlan -Plan $plan } | Should -Throw '*Providers are required*' + } + + It 'regression: does not fail with "Context.Providers must be a hashtable" when using Plan.Providers' { + $wfPath = Join-Path -Path $TestDrive -ChildPath 'joiner.psd1' + Set-Content -Path $wfPath -Encoding UTF8 -Value @' +@{ + Name = 'Joiner - Standard' + LifecycleEvent = 'Joiner' + Steps = @( + @{ Name = 'TestStep'; Type = 'IdLE.Step.Test' } + ) +} +'@ + + $req = New-IdleLifecycleRequest -LifecycleEvent 'Joiner' + + $providers = @{ + StepRegistry = @{ + 'IdLE.Step.Test' = 'Invoke-IdleTestProviderFallbackStep' + } + StepMetadata = New-IdleTestStepMetadata -StepTypes @('IdLE.Step.Test') + } + + # Build plan with providers + $plan = New-IdlePlan -WorkflowPath $wfPath -Request $req -Providers $providers + + # Execute without passing -Providers (should NOT throw "Context.Providers must be a hashtable") + { Invoke-IdlePlan -Plan $plan } | Should -Not -Throw + } + + It 'applies security validations to Plan.Providers' { + $wfPath = Join-Path -Path $TestDrive -ChildPath 'joiner.psd1' + Set-Content -Path $wfPath -Encoding UTF8 -Value @' +@{ + Name = 'Joiner - Standard' + LifecycleEvent = 'Joiner' + Steps = @( + @{ Name = 'TestStep'; Type = 'IdLE.Step.Test' } + ) +} +'@ + + $req = New-IdleLifecycleRequest -LifecycleEvent 'Joiner' + + $providers = @{ + StepRegistry = @{ + 'IdLE.Step.Test' = 'Invoke-IdleTestProviderFallbackStep' + } + StepMetadata = New-IdleTestStepMetadata -StepTypes @('IdLE.Step.Test') + MaliciousCode = { Write-Host "Malicious" } # ScriptBlock should be rejected + } + + # Build plan with providers containing ScriptBlock + $plan = New-IdlePlan -WorkflowPath $wfPath -Request $req -Providers $providers + + # Execute without passing -Providers (should still reject ScriptBlocks in Plan.Providers) + { Invoke-IdlePlan -Plan $plan } | Should -Throw + } + + It 'redacts Plan.Providers in execution result when used as fallback' { + $wfPath = Join-Path -Path $TestDrive -ChildPath 'joiner.psd1' + Set-Content -Path $wfPath -Encoding UTF8 -Value @' +@{ + Name = 'Joiner - Standard' + LifecycleEvent = 'Joiner' + Steps = @( + @{ Name = 'TestStep'; Type = 'IdLE.Step.Test' } + ) +} +'@ + + $req = New-IdleLifecycleRequest -LifecycleEvent 'Joiner' + + $providers = @{ + StepRegistry = @{ + 'IdLE.Step.Test' = 'Invoke-IdleTestProviderFallbackStep' + } + StepMetadata = New-IdleTestStepMetadata -StepTypes @('IdLE.Step.Test') + TestProvider = @{ + endpoint = 'https://example.test' + token = 'SecretToken123' + apiKey = 'ApiKey456' + } + } + + # Build plan with providers + $plan = New-IdlePlan -WorkflowPath $wfPath -Request $req -Providers $providers + + # Execute without passing -Providers + $result = Invoke-IdlePlan -Plan $plan + + $result.Status | Should -Be 'Completed' + # Providers should be redacted (sensitive keys should have [REDACTED]) + $result.Providers | Should -Not -BeNullOrEmpty + $result.Providers.TestProvider.token | Should -Be '[REDACTED]' + $result.Providers.TestProvider.apiKey | Should -Be '[REDACTED]' + $result.Providers.TestProvider.endpoint | Should -Be 'https://example.test' + } +} From 4ce184baf836b3e48d8cada5609ff670be036d3c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 8 Feb 2026 11:20:16 +0000 Subject: [PATCH 3/9] Update documentation and examples for provider fallback Co-authored-by: blindzero <13959569+blindzero@users.noreply.github.com> --- docs/use/plan-export.md | 15 ++++++++++++- docs/use/providers.md | 42 +++++++++++++++++++++++++++++++++++- docs/use/quickstart.md | 20 +++++++++-------- examples/Invoke-IdleDemo.ps1 | 3 ++- 4 files changed, 68 insertions(+), 12 deletions(-) diff --git a/docs/use/plan-export.md b/docs/use/plan-export.md index aa7199e5..5891dc07 100644 --- a/docs/use/plan-export.md +++ b/docs/use/plan-export.md @@ -47,10 +47,23 @@ For the exact format and normative rules, see [Plan Export Specification](../ref ```powershell # Example only. Adjust parameters to your environment. $request = New-IdleLifecycleRequest -LifecycleEvent 'Joiner' -IdentityKeys @{ EmployeeId = 'jdoe' } -$plan = New-IdlePlan -WorkflowPath './workflows/joiner.psd1' -Request $request +$providers = @{ Identity = New-IdleMockIdentityProvider } +$plan = New-IdlePlan -WorkflowPath './workflows/joiner.psd1' -Request $request -Providers $providers Export-IdlePlan -Plan $plan -Path './artifacts/joiner.plan.json' ``` +:::note + +Exported plans typically do not include provider objects. When importing and executing an exported plan, +you must supply providers at execution time: + +```powershell +$importedPlan = Import-IdlePlan -Path './artifacts/joiner.plan.json' +$result = Invoke-IdlePlan -Plan $importedPlan -Providers $providers +``` + +::: + ### Review tips - Verify provider selection matches your intent (especially in multi-provider environments). diff --git a/docs/use/providers.md b/docs/use/providers.md index 723d9be1..a0c84569 100644 --- a/docs/use/providers.md +++ b/docs/use/providers.md @@ -34,6 +34,12 @@ As providers may require additional tools and configuration, they are not import ::: +### Provider Resolution + +When executing a plan, providers can be supplied in two ways: + +1. **During planning** (recommended for most scenarios): + ```powershell Import-Module -Name IdLE.Provider.Mock @@ -41,7 +47,41 @@ $providers = @{ Identity = New-IdleMockIdentityProvider } -$result = Invoke-IdlePlan -Plan $plan -Providers $providers +# Build plan with providers +$plan = New-IdlePlan -WorkflowPath ./joiner.psd1 -Request $request -Providers $providers + +# Execute without re-supplying providers (uses Plan.Providers) +$result = Invoke-IdlePlan -Plan $plan +``` + +2. **At execution time** (for provider override or exported plans): + +```powershell +# Override providers at execution time +$otherProviders = @{ + Identity = New-IdleMockIdentityProvider -Config $differentConfig +} + +$result = Invoke-IdlePlan -Plan $plan -Providers $otherProviders +``` + +#### Resolution Rules + +- If `-Providers` is supplied to `Invoke-IdlePlan`, it **takes precedence** over `Plan.Providers`. +- If `-Providers` is **not** supplied, `Invoke-IdlePlan` uses `Plan.Providers` (if available). +- If neither is present, execution fails early with: `Providers are required. Provide -Providers to Invoke-IdlePlan or build the plan with Providers.` + +#### Exported Plans + +When a plan is exported without provider objects (for review or audit), providers must be supplied at execution time: + +```powershell +# Export plan (without providers) +Export-IdlePlan -Plan $plan -Path ./plan.json + +# Later: import and execute with providers +$importedPlan = Import-IdlePlan -Path ./plan.json +$result = Invoke-IdlePlan -Plan $importedPlan -Providers $providers ``` ### Provider Aliases diff --git a/docs/use/quickstart.md b/docs/use/quickstart.md index 96767f33..9e29882a 100644 --- a/docs/use/quickstart.md +++ b/docs/use/quickstart.md @@ -124,28 +124,29 @@ With the following command we create a simple 'Joiner' request. $request = New-IdleLifecycleRequest -LifecycleEvent 'Joiner' ``` -### 4. Build the plan (deterministic, data-only) +### 4. Select providers -The plan evaluates validity of the request in combination with the workflow definition. +For first run, we just use our internal mock provider. ```powershell -$plan = New-IdlePlan -WorkflowPath $workflow -Request $request +$providers = @{ + Identity = New-IdleMockIdentityProvider +} ``` -### 5. Select providers +### 5. Build the plan with providers -For first run, we just use our internal mock provider. +The plan evaluates validity of the request in combination with the workflow definition. ```powershell -$providers = @{ - Identity = New-IdleMockIdentityProvider -} +$plan = New-IdlePlan -WorkflowPath $workflow -Request $request -Providers $providers ``` ### 6. Execute the plan ```powershell -$result = Invoke-IdlePlan -Plan $plan -Providers $providers +# Execute without re-supplying providers (uses Plan.Providers automatically) +$result = Invoke-IdlePlan -Plan $plan ``` ### 7. Inspect result + events @@ -161,5 +162,6 @@ $result.Events | Select-Object Type, StepName, Message - If your workflow contains steps that require additional provider roles (e.g. `Messaging`, `Entitlement`), you must add them to `$providers`. - Many steps default to the provider alias `'Identity'` unless a step explicitly sets `With.Provider`. +- You can override providers at execution time by passing `-Providers` to `Invoke-IdlePlan`. ::: diff --git a/examples/Invoke-IdleDemo.ps1 b/examples/Invoke-IdleDemo.ps1 index 796cf4d3..f8d7f34d 100644 --- a/examples/Invoke-IdleDemo.ps1 +++ b/examples/Invoke-IdleDemo.ps1 @@ -269,7 +269,8 @@ foreach ($wf in $selected) { Write-Host "" Write-DemoHeader "Execute" - $result = Invoke-IdlePlan -Plan $plan -Providers $providers + # Execute plan using Plan.Providers (no need to re-supply -Providers) + $result = Invoke-IdlePlan -Plan $plan $allResults += $result Write-Host "" From b080bb3f05e8c8e9a1da2e2496bb909eb6d13276 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 8 Feb 2026 11:21:27 +0000 Subject: [PATCH 4/9] Fix ExecutionOptions tests to include providers Co-authored-by: blindzero <13959569+blindzero@users.noreply.github.com> --- .../Core/Invoke-IdlePlan.ExecutionOptions.Tests.ps1 | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/tests/Core/Invoke-IdlePlan.ExecutionOptions.Tests.ps1 b/tests/Core/Invoke-IdlePlan.ExecutionOptions.Tests.ps1 index 2a59f2d0..1780e3f0 100644 --- a/tests/Core/Invoke-IdlePlan.ExecutionOptions.Tests.ps1 +++ b/tests/Core/Invoke-IdlePlan.ExecutionOptions.Tests.ps1 @@ -139,7 +139,8 @@ Describe 'Invoke-IdlePlan - ExecutionOptions validation' { '@ $req = New-IdleLifecycleRequest -LifecycleEvent 'Joiner' - $plan = New-IdlePlan -WorkflowPath $wfPath -Request $req + $providers = @{ StepRegistry = @{} } + $plan = New-IdlePlan -WorkflowPath $wfPath -Request $req -Providers $providers $opts = @{ SomeKey = { Write-Host 'test' } @@ -159,7 +160,8 @@ Describe 'Invoke-IdlePlan - ExecutionOptions validation' { '@ $req = New-IdleLifecycleRequest -LifecycleEvent 'Joiner' - $plan = New-IdlePlan -WorkflowPath $wfPath -Request $req + $providers = @{ StepRegistry = @{} } + $plan = New-IdlePlan -WorkflowPath $wfPath -Request $req -Providers $providers $opts = @{ RetryProfiles = @{ @@ -181,7 +183,8 @@ Describe 'Invoke-IdlePlan - ExecutionOptions validation' { '@ $req = New-IdleLifecycleRequest -LifecycleEvent 'Joiner' - $plan = New-IdlePlan -WorkflowPath $wfPath -Request $req + $providers = @{ StepRegistry = @{} } + $plan = New-IdlePlan -WorkflowPath $wfPath -Request $req -Providers $providers $opts = @{ RetryProfiles = @{ @@ -204,7 +207,8 @@ Describe 'Invoke-IdlePlan - ExecutionOptions validation' { '@ $req = New-IdleLifecycleRequest -LifecycleEvent 'Joiner' - $plan = New-IdlePlan -WorkflowPath $wfPath -Request $req + $providers = @{ StepRegistry = @{} } + $plan = New-IdlePlan -WorkflowPath $wfPath -Request $req -Providers $providers $opts = @{ RetryProfiles = @{ From 383117c43b34f6036841fe1b49462818a9bb62bf Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 8 Feb 2026 11:40:36 +0000 Subject: [PATCH 5/9] Remove references to non-existent Import-IdlePlan function Co-authored-by: blindzero <13959569+blindzero@users.noreply.github.com> --- docs/use/plan-export.md | 9 ++------- docs/use/providers.md | 5 ++--- 2 files changed, 4 insertions(+), 10 deletions(-) diff --git a/docs/use/plan-export.md b/docs/use/plan-export.md index 5891dc07..6f8ace40 100644 --- a/docs/use/plan-export.md +++ b/docs/use/plan-export.md @@ -54,13 +54,8 @@ Export-IdlePlan -Plan $plan -Path './artifacts/joiner.plan.json' :::note -Exported plans typically do not include provider objects. When importing and executing an exported plan, -you must supply providers at execution time: - -```powershell -$importedPlan = Import-IdlePlan -Path './artifacts/joiner.plan.json' -$result = Invoke-IdlePlan -Plan $importedPlan -Providers $providers -``` +Exported plans typically do not include provider objects. When executing an exported plan, +you must supply providers at execution time. ::: diff --git a/docs/use/providers.md b/docs/use/providers.md index a0c84569..f9e446c0 100644 --- a/docs/use/providers.md +++ b/docs/use/providers.md @@ -79,9 +79,8 @@ When a plan is exported without provider objects (for review or audit), provider # Export plan (without providers) Export-IdlePlan -Plan $plan -Path ./plan.json -# Later: import and execute with providers -$importedPlan = Import-IdlePlan -Path ./plan.json -$result = Invoke-IdlePlan -Plan $importedPlan -Providers $providers +# Later: execute with providers (plan import functionality is planned for future release) +$result = Invoke-IdlePlan -Plan $plan -Providers $providers ``` ### Provider Aliases From c3a712351624d8ae2bdf9f07c4c0e5ec3a526916 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 8 Feb 2026 11:52:18 +0000 Subject: [PATCH 6/9] Improve test comment clarity for exported plan simulation Co-authored-by: blindzero <13959569+blindzero@users.noreply.github.com> --- tests/Core/Invoke-IdlePlan.ProviderFallback.Tests.ps1 | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/Core/Invoke-IdlePlan.ProviderFallback.Tests.ps1 b/tests/Core/Invoke-IdlePlan.ProviderFallback.Tests.ps1 index ae642bfd..7f3db9b3 100644 --- a/tests/Core/Invoke-IdlePlan.ProviderFallback.Tests.ps1 +++ b/tests/Core/Invoke-IdlePlan.ProviderFallback.Tests.ps1 @@ -123,7 +123,7 @@ Describe 'Invoke-IdlePlan Provider Fallback' { $req = New-IdleLifecycleRequest -LifecycleEvent 'Joiner' - # Build plan without providers + # Build plan with providers, then remove Providers property to simulate exported plan scenario $providers = @{ StepRegistry = @{ 'IdLE.Step.Test' = 'Invoke-IdleTestProviderFallbackStep' @@ -132,7 +132,7 @@ Describe 'Invoke-IdlePlan Provider Fallback' { } $plan = New-IdlePlan -WorkflowPath $wfPath -Request $req -Providers $providers - # Manually remove Providers from plan to simulate exported plan scenario + # Remove Providers property to simulate an exported plan without provider objects $plan.PSObject.Properties.Remove('Providers') # Execute without -Providers and without Plan.Providers From e88de415545866f3ebbbdcf72737c0a595672b3d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 8 Feb 2026 12:05:19 +0000 Subject: [PATCH 7/9] Support PSCustomObject provider registries in Plan.Providers fallback Co-authored-by: blindzero <13959569+blindzero@users.noreply.github.com> --- .../Public/Invoke-IdlePlanObject.ps1 | 11 +++++-- ...Invoke-IdlePlan.ProviderFallback.Tests.ps1 | 33 +++++++++++++++++++ 2 files changed, 42 insertions(+), 2 deletions(-) diff --git a/src/IdLE.Core/Public/Invoke-IdlePlanObject.ps1 b/src/IdLE.Core/Public/Invoke-IdlePlanObject.ps1 index d8ac2cf7..2c1fb0bf 100644 --- a/src/IdLE.Core/Public/Invoke-IdlePlanObject.ps1 +++ b/src/IdLE.Core/Public/Invoke-IdlePlanObject.ps1 @@ -110,8 +110,15 @@ function Invoke-IdlePlanObject { if ($null -eq $effectiveProviders) { if ($planPropNames -contains 'Providers') { $planProviders = $Plan.Providers - if ($null -ne $planProviders -and $planProviders -is [System.Collections.IDictionary]) { - $effectiveProviders = $planProviders + # Accept both IDictionary (hashtables) and PSCustomObject-shaped provider registries + if ($null -ne $planProviders) { + if ($planProviders -is [System.Collections.IDictionary]) { + $effectiveProviders = $planProviders + } + elseif ($planProviders.PSObject -and $planProviders.PSObject.Properties) { + # Accept PSCustomObject with properties (e.g., StepRegistry, AuthSessionBroker) + $effectiveProviders = $planProviders + } } } } diff --git a/tests/Core/Invoke-IdlePlan.ProviderFallback.Tests.ps1 b/tests/Core/Invoke-IdlePlan.ProviderFallback.Tests.ps1 index 7f3db9b3..24d69fdb 100644 --- a/tests/Core/Invoke-IdlePlan.ProviderFallback.Tests.ps1 +++ b/tests/Core/Invoke-IdlePlan.ProviderFallback.Tests.ps1 @@ -235,4 +235,37 @@ Describe 'Invoke-IdlePlan Provider Fallback' { $result.Providers.TestProvider.apiKey | Should -Be '[REDACTED]' $result.Providers.TestProvider.endpoint | Should -Be 'https://example.test' } + + It 'uses Plan.Providers when it is a PSCustomObject (not just IDictionary)' { + $wfPath = Join-Path -Path $TestDrive -ChildPath 'joiner.psd1' + Set-Content -Path $wfPath -Encoding UTF8 -Value @' +@{ + Name = 'Joiner - Standard' + LifecycleEvent = 'Joiner' + Steps = @( + @{ Name = 'TestStep'; Type = 'IdLE.Step.Test' } + ) +} +'@ + + $req = New-IdleLifecycleRequest -LifecycleEvent 'Joiner' + + # Create a PSCustomObject-shaped provider registry (not a hashtable) + $providersObject = [pscustomobject]@{ + StepRegistry = @{ + 'IdLE.Step.Test' = 'Invoke-IdleTestProviderFallbackStep' + } + StepMetadata = New-IdleTestStepMetadata -StepTypes @('IdLE.Step.Test') + } + + # Build plan with PSCustomObject providers + $plan = New-IdlePlan -WorkflowPath $wfPath -Request $req -Providers $providersObject + + # Execute without passing -Providers (should use Plan.Providers even though it's a PSCustomObject) + $result = Invoke-IdlePlan -Plan $plan + + $result.PSTypeNames | Should -Contain 'IdLE.ExecutionResult' + $result.Status | Should -Be 'Completed' + $result.Steps[0].Status | Should -Be 'Completed' + } } From d3b6db15777826aba65201cf2801045a57a1db55 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 8 Feb 2026 12:08:39 +0000 Subject: [PATCH 8/9] Simplify provider validation logic with combined condition Co-authored-by: blindzero <13959569+blindzero@users.noreply.github.com> --- src/IdLE.Core/Public/Invoke-IdlePlanObject.ps1 | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/IdLE.Core/Public/Invoke-IdlePlanObject.ps1 b/src/IdLE.Core/Public/Invoke-IdlePlanObject.ps1 index 2c1fb0bf..cba6d9b8 100644 --- a/src/IdLE.Core/Public/Invoke-IdlePlanObject.ps1 +++ b/src/IdLE.Core/Public/Invoke-IdlePlanObject.ps1 @@ -112,11 +112,9 @@ function Invoke-IdlePlanObject { $planProviders = $Plan.Providers # Accept both IDictionary (hashtables) and PSCustomObject-shaped provider registries if ($null -ne $planProviders) { - if ($planProviders -is [System.Collections.IDictionary]) { - $effectiveProviders = $planProviders - } - elseif ($planProviders.PSObject -and $planProviders.PSObject.Properties) { - # Accept PSCustomObject with properties (e.g., StepRegistry, AuthSessionBroker) + $isValidProvider = ($planProviders -is [System.Collections.IDictionary]) -or + ($planProviders.PSObject -and $planProviders.PSObject.Properties) + if ($isValidProvider) { $effectiveProviders = $planProviders } } From b6de909fc37e42d528cf6a9ec010b5f1713d4b2a Mon Sep 17 00:00:00 2001 From: Matthias Fleschuetz <55826276+ntt-matthias-fleschuetz@users.noreply.github.com> Date: Sun, 8 Feb 2026 13:14:59 +0100 Subject: [PATCH 9/9] update cmdlet reference --- docs/reference/cmdlets/Invoke-IdlePlan.md | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/docs/reference/cmdlets/Invoke-IdlePlan.md b/docs/reference/cmdlets/Invoke-IdlePlan.md index b47340e7..b152e300 100644 --- a/docs/reference/cmdlets/Invoke-IdlePlan.md +++ b/docs/reference/cmdlets/Invoke-IdlePlan.md @@ -22,15 +22,29 @@ Invoke-IdlePlan [-Plan] <Object> [[-Providers] <Hashtable>] [[-Event Executes a plan deterministically and emits structured events. Delegates execution to IdLE.Core. +Provider resolution: +- If -Providers is supplied, it is used for execution. +- If -Providers is not supplied, Plan.Providers is used if available. +- If neither is present, execution fails early with a clear error message. + ## EXAMPLES ### EXAMPLE 1 ``` -Invoke-IdlePlan -Plan $plan -Providers $providers +# Default: plan built with providers, execution uses Plan.Providers +$providers = @{ Identity = $provider; AuthSessionBroker = $broker } +$plan = New-IdlePlan -WorkflowPath ./joiner.psd1 -Request $req -Providers $providers +Invoke-IdlePlan -Plan $plan ``` ### EXAMPLE 2 ``` +# Override: explicit -Providers at invoke time +Invoke-IdlePlan -Plan $plan -Providers $otherProviders +``` + +### EXAMPLE 3 +``` $execOptions = @{ RetryProfiles = @{ Default = @{ MaxAttempts = 3; InitialDelayMilliseconds = 200 } @@ -38,7 +52,7 @@ $execOptions = @{ } DefaultRetryProfile = 'Default' } -Invoke-IdlePlan -Plan $plan -Providers $providers -ExecutionOptions $execOptions +Invoke-IdlePlan -Plan $plan -ExecutionOptions $execOptions ``` ## PARAMETERS @@ -60,6 +74,8 @@ Accept wildcard characters: False ### -Providers Provider registry/collection passed through to execution. +If omitted and Plan.Providers exists, Plan.Providers will be used. +If supplied, overrides Plan.Providers. ```yaml Type: Hashtable