From 785e04603267d2fd570f2ea4d7a1a434dfd7d175 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 27 Feb 2026 19:02:08 +0000 Subject: [PATCH 1/3] Initial plan From 075efa1fdcb5e9bbf83e12cf10a2057a1491a81e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 27 Feb 2026 20:12:00 +0000 Subject: [PATCH 2/3] feat: align ContextResolver parameters with Step.With, remove root Provider key Co-authored-by: blindzero <13959569+blindzero@users.noreply.github.com> --- docs/reference/capabilities.md | 6 +- docs/use/workflows/context-resolver.md | 19 ++-- .../mock/joiner-with-context-resolvers.psd1 | 7 +- .../Private/Invoke-IdleContextResolvers.ps1 | 8 +- .../Private/Test-IdleWorkflowSchema.ps1 | 26 ++++-- tests/Core/Test-IdleWorkflowSchema.Tests.ps1 | 86 +++++++++++++++++++ .../workflows/resolver-condition.psd1 | 6 +- .../resolver-context-type-conflict.psd1 | 6 +- .../resolver-empty-entitlements.psd1 | 6 +- .../workflows/resolver-identity-read.psd1 | 6 +- .../fixtures/workflows/resolver-snapshot.psd1 | 6 +- .../fixtures/workflows/resolver-template.psd1 | 6 +- .../workflows/resolver-with-auth-session.psd1 | 6 +- 13 files changed, 154 insertions(+), 40 deletions(-) diff --git a/docs/reference/capabilities.md b/docs/reference/capabilities.md index ce79aee0..fe6194ea 100644 --- a/docs/reference/capabilities.md +++ b/docs/reference/capabilities.md @@ -146,8 +146,10 @@ Each capability writes to a **predefined, fixed path** under `Request.Context`. ContextResolvers = @( @{ Capability = 'IdLE.Entitlement.List' - Provider = 'Identity' # optional; auto-selected if omitted - With = @{ IdentityKey = '{{Request.IdentityKeys.EmployeeId}}' } + With = @{ + IdentityKey = '{{Request.IdentityKeys.EmployeeId}}' + Provider = 'Identity' # optional; auto-selected if omitted + } # Writes to Request.Context.Identity.Entitlements (predefined, not configurable) } @{ diff --git a/docs/use/workflows/context-resolver.md b/docs/use/workflows/context-resolver.md index d02281a8..d3409e18 100644 --- a/docs/use/workflows/context-resolver.md +++ b/docs/use/workflows/context-resolver.md @@ -41,9 +41,10 @@ A resolver entry is defined at workflow root level: ContextResolvers = @( @{ Capability = 'IdLE.Identity.Read' - Provider = 'Identity' # optional With = @{ - IdentityKey = '{{Request.IdentityKeys.EmployeeId}}' + IdentityKey = '{{Request.IdentityKeys.EmployeeId}}' + Provider = 'Identity' # optional; auto-selected if omitted + AuthSessionName = 'Tier0' # optional; requires AuthSessionBroker in Providers } } @@ -83,12 +84,16 @@ A resolver entry is defined at workflow root level: - `Capability` (required) A permitted read-only capability. -- `Provider` (optional) - Provider alias. If omitted, IdLE selects a provider advertising the capability. - -- `With` (required) +- `With` (hashtable, optional — required in practice, as capabilities need at least `IdentityKey`) Inputs required by the capability. Template substitution is supported. + | `With` key | Type | Required | Description | + |---|---|---|---| + | `IdentityKey` | `string` | Per capability | Required by `IdLE.Identity.Read` and `IdLE.Entitlement.List`. | + | `Provider` | `string` | No | Provider alias. If omitted, IdLE auto-selects a provider advertising the capability. Ambiguity (multiple providers matching) is a fail-fast error. | + | `AuthSessionName` | `string` | No | Named auth session to acquire via `AuthSessionBroker`. Requires an `AuthSessionBroker` entry in `Providers`. | + | `AuthSessionOptions` | `hashtable` | No | Options passed to `AuthSessionBroker.AcquireAuthSession`. Must be a hashtable. ScriptBlocks are rejected. | + Output paths are predefined and cannot be changed. --- @@ -156,7 +161,7 @@ Condition = @{ Exists = 'Request.Context.Identity.Entitlements' } ### Ambiguous provider -- If multiple providers advertise a capability, specify `Provider` explicitly. +- If multiple providers advertise a capability, specify `With.Provider` explicitly. ### Context value missing diff --git a/examples/workflows/mock/joiner-with-context-resolvers.psd1 b/examples/workflows/mock/joiner-with-context-resolvers.psd1 index 059c7423..7fd25a47 100644 --- a/examples/workflows/mock/joiner-with-context-resolvers.psd1 +++ b/examples/workflows/mock/joiner-with-context-resolvers.psd1 @@ -19,13 +19,12 @@ # Writes to Request.Context.Identity.Entitlements (predefined). Capability = 'IdLE.Entitlement.List' - # The provider alias that supports IdLE.Entitlement.List. - # If omitted, the first provider advertising the capability is used. - Provider = 'Identity' - # Resolver inputs. With = @{ IdentityKey = 'user1' + # Provider alias that supports IdLE.Entitlement.List. + # If omitted, the provider is auto-selected when exactly one match exists. + Provider = 'Identity' } } ) diff --git a/src/IdLE.Core/Private/Invoke-IdleContextResolvers.ps1 b/src/IdLE.Core/Private/Invoke-IdleContextResolvers.ps1 index 45f866a7..8396d781 100644 --- a/src/IdLE.Core/Private/Invoke-IdleContextResolvers.ps1 +++ b/src/IdLE.Core/Private/Invoke-IdleContextResolvers.ps1 @@ -14,7 +14,7 @@ function Invoke-IdleContextResolvers { - Only capabilities in the read-only allow-list (Get-IdleReadOnlyCapabilities) may be used. - Each capability writes to a fixed, predefined path under Request.Context. The output path is not user-configurable. - - Provider is selected by alias when 'Provider' is specified; otherwise the first + - Provider is selected by alias when 'With.Provider' is specified; otherwise the first provider (sorted by alias) that advertises the capability is used; ambiguity when multiple providers match causes a fail-fast error. - Auth sessions are supported via With.AuthSessionName / With.AuthSessionOptions, @@ -93,8 +93,8 @@ function Invoke-IdleContextResolvers { $with = Resolve-IdleWorkflowTemplates -Value $with -Request $Request -StepName $resolverPath # --- Provider selection --- - $providerAlias = if ($resolver.Contains('Provider') -and -not [string]::IsNullOrWhiteSpace([string]$resolver.Provider)) { - [string]$resolver.Provider + $providerAlias = if ($with -is [System.Collections.IDictionary] -and $with.Contains('Provider') -and -not [string]::IsNullOrWhiteSpace([string]$with.Provider)) { + [string]$with.Provider } else { $null @@ -244,7 +244,7 @@ function Select-IdleResolverProviderAlias { if ($matchingAliases.Count -gt 1) { $aliasList = $matchingAliases -join ', ' throw [System.ArgumentException]::new( - "${ResolverPath}: Multiple providers advertise capability '$Capability': $aliasList. Specify 'Provider' in the resolver to disambiguate.", + "${ResolverPath}: Multiple providers advertise capability '$Capability': $aliasList. Specify 'With.Provider' in the resolver to disambiguate.", 'Providers' ) } diff --git a/src/IdLE.Core/Private/Test-IdleWorkflowSchema.ps1 b/src/IdLE.Core/Private/Test-IdleWorkflowSchema.ps1 index 75671529..4f9f5423 100644 --- a/src/IdLE.Core/Private/Test-IdleWorkflowSchema.ps1 +++ b/src/IdLE.Core/Private/Test-IdleWorkflowSchema.ps1 @@ -238,7 +238,7 @@ function Test-IdleWorkflowSchema { } else { # 'To' is not user-configurable; each capability has a predefined output path. - $allowedResolverKeys = @('Capability', 'Provider', 'With') + $allowedResolverKeys = @('Capability', 'With') $i = 0 foreach ($resolver in $Workflow.ContextResolvers) { @@ -261,13 +261,23 @@ function Test-IdleWorkflowSchema { } # 'With' is optional but must be a hashtable if present. - if ($resolver.ContainsKey('With') -and $null -ne $resolver.With -and $resolver.With -isnot [hashtable]) { - $errors.Add("'$resolverPath.With' must be a hashtable (resolver input parameters).") - } - - # 'Provider' is optional but must be a non-empty string if present. - if ($resolver.ContainsKey('Provider') -and $null -ne $resolver.Provider -and [string]::IsNullOrWhiteSpace([string]$resolver.Provider)) { - $errors.Add("'$resolverPath.Provider' must not be an empty string.") + if ($resolver.ContainsKey('With') -and $null -ne $resolver.With) { + if ($resolver.With -isnot [hashtable]) { + $errors.Add("'$resolverPath.With' must be a hashtable (resolver input parameters).") + } + else { + $with = $resolver.With + + # 'With.Provider' is optional but must be a non-empty string if present. + if ($with.ContainsKey('Provider') -and $null -ne $with.Provider -and [string]::IsNullOrWhiteSpace([string]$with.Provider)) { + $errors.Add("'$resolverPath.With.Provider' must not be an empty string.") + } + + # 'With.AuthSessionOptions' must be a hashtable if present. + if ($with.ContainsKey('AuthSessionOptions') -and $null -ne $with.AuthSessionOptions -and $with.AuthSessionOptions -isnot [hashtable]) { + $errors.Add("'$resolverPath.With.AuthSessionOptions' must be a hashtable.") + } + } } $i++ diff --git a/tests/Core/Test-IdleWorkflowSchema.Tests.ps1 b/tests/Core/Test-IdleWorkflowSchema.Tests.ps1 index 37396e44..7cc7ea82 100644 --- a/tests/Core/Test-IdleWorkflowSchema.Tests.ps1 +++ b/tests/Core/Test-IdleWorkflowSchema.Tests.ps1 @@ -85,3 +85,89 @@ Describe 'Workflow schema validation - Condition/Precondition DSL parity' { } } } + +Describe 'Workflow schema validation - ContextResolvers' { + InModuleScope 'IdLE.Core' { + It 'rejects root-level Provider key in a resolver entry (must use With.Provider)' { + $workflow = @{ + Name = 'Root Provider Rejected' + LifecycleEvent = 'Joiner' + ContextResolvers = @( + @{ + Capability = 'IdLE.Entitlement.List' + Provider = 'Identity' + With = @{ IdentityKey = 'user1' } + } + ) + Steps = @( + @{ Name = 'Step1'; Type = 'IdLE.Step.Noop' } + ) + } + + $errors = Test-IdleWorkflowSchema -Workflow $workflow + @($errors | Where-Object { $_ -like "*Unknown key*Provider*" }).Count | Should -BeGreaterThan 0 + } + + It 'rejects With.Provider as empty string' { + $workflow = @{ + Name = 'With.Provider Empty' + LifecycleEvent = 'Joiner' + ContextResolvers = @( + @{ + Capability = 'IdLE.Entitlement.List' + With = @{ IdentityKey = 'user1'; Provider = '' } + } + ) + Steps = @( + @{ Name = 'Step1'; Type = 'IdLE.Step.Noop' } + ) + } + + $errors = Test-IdleWorkflowSchema -Workflow $workflow + @($errors | Where-Object { $_ -like "*With.Provider*must not be an empty string*" }).Count | Should -BeGreaterThan 0 + } + + It 'rejects With.AuthSessionOptions as a non-hashtable' { + $workflow = @{ + Name = 'With.AuthSessionOptions Invalid' + LifecycleEvent = 'Joiner' + ContextResolvers = @( + @{ + Capability = 'IdLE.Entitlement.List' + With = @{ IdentityKey = 'user1'; AuthSessionName = 'Tier0'; AuthSessionOptions = 'not-a-hashtable' } + } + ) + Steps = @( + @{ Name = 'Step1'; Type = 'IdLE.Step.Noop' } + ) + } + + $errors = Test-IdleWorkflowSchema -Workflow $workflow + @($errors | Where-Object { $_ -like "*With.AuthSessionOptions*must be a hashtable*" }).Count | Should -BeGreaterThan 0 + } + + It 'accepts a valid resolver with With.Provider and With.AuthSessionOptions' { + $workflow = @{ + Name = 'Valid Resolver With.Provider' + LifecycleEvent = 'Joiner' + ContextResolvers = @( + @{ + Capability = 'IdLE.Entitlement.List' + With = @{ + IdentityKey = 'user1' + Provider = 'Identity' + AuthSessionName = 'Tier0' + AuthSessionOptions = @{ Role = 'Tier0' } + } + } + ) + Steps = @( + @{ Name = 'Step1'; Type = 'IdLE.Step.Noop' } + ) + } + + $errors = Test-IdleWorkflowSchema -Workflow $workflow + @($errors).Count | Should -Be 0 + } + } +} diff --git a/tests/fixtures/workflows/resolver-condition.psd1 b/tests/fixtures/workflows/resolver-condition.psd1 index c38ad0fe..38c4a352 100644 --- a/tests/fixtures/workflows/resolver-condition.psd1 +++ b/tests/fixtures/workflows/resolver-condition.psd1 @@ -4,8 +4,10 @@ ContextResolvers = @( @{ Capability = 'IdLE.Entitlement.List' - Provider = 'Identity' - With = @{ IdentityKey = 'user1' } + With = @{ + IdentityKey = 'user1' + Provider = 'Identity' + } } ) Steps = @( diff --git a/tests/fixtures/workflows/resolver-context-type-conflict.psd1 b/tests/fixtures/workflows/resolver-context-type-conflict.psd1 index a558c737..883e8f8c 100644 --- a/tests/fixtures/workflows/resolver-context-type-conflict.psd1 +++ b/tests/fixtures/workflows/resolver-context-type-conflict.psd1 @@ -4,8 +4,10 @@ ContextResolvers = @( @{ Capability = 'IdLE.Entitlement.List' - Provider = 'Identity' - With = @{ IdentityKey = 'user1' } + With = @{ + IdentityKey = 'user1' + Provider = 'Identity' + } } ) Steps = @( diff --git a/tests/fixtures/workflows/resolver-empty-entitlements.psd1 b/tests/fixtures/workflows/resolver-empty-entitlements.psd1 index 2202d279..2bf94940 100644 --- a/tests/fixtures/workflows/resolver-empty-entitlements.psd1 +++ b/tests/fixtures/workflows/resolver-empty-entitlements.psd1 @@ -4,8 +4,10 @@ ContextResolvers = @( @{ Capability = 'IdLE.Entitlement.List' - Provider = 'Identity' - With = @{ IdentityKey = 'user2' } + With = @{ + IdentityKey = 'user2' + Provider = 'Identity' + } } ) Steps = @( diff --git a/tests/fixtures/workflows/resolver-identity-read.psd1 b/tests/fixtures/workflows/resolver-identity-read.psd1 index ab299681..f55cecc2 100644 --- a/tests/fixtures/workflows/resolver-identity-read.psd1 +++ b/tests/fixtures/workflows/resolver-identity-read.psd1 @@ -4,8 +4,10 @@ ContextResolvers = @( @{ Capability = 'IdLE.Identity.Read' - Provider = 'Identity' - With = @{ IdentityKey = 'user1' } + With = @{ + IdentityKey = 'user1' + Provider = 'Identity' + } } ) Steps = @( diff --git a/tests/fixtures/workflows/resolver-snapshot.psd1 b/tests/fixtures/workflows/resolver-snapshot.psd1 index 53a4fb60..2ef92522 100644 --- a/tests/fixtures/workflows/resolver-snapshot.psd1 +++ b/tests/fixtures/workflows/resolver-snapshot.psd1 @@ -4,8 +4,10 @@ ContextResolvers = @( @{ Capability = 'IdLE.Entitlement.List' - Provider = 'Identity' - With = @{ IdentityKey = 'snap-user' } + With = @{ + IdentityKey = 'snap-user' + Provider = 'Identity' + } } ) Steps = @( diff --git a/tests/fixtures/workflows/resolver-template.psd1 b/tests/fixtures/workflows/resolver-template.psd1 index 4eb3d5a2..79cfae5a 100644 --- a/tests/fixtures/workflows/resolver-template.psd1 +++ b/tests/fixtures/workflows/resolver-template.psd1 @@ -4,8 +4,10 @@ ContextResolvers = @( @{ Capability = 'IdLE.Entitlement.List' - Provider = 'Identity' - With = @{ IdentityKey = '{{Request.IdentityKeys.Id}}' } + With = @{ + IdentityKey = '{{Request.IdentityKeys.Id}}' + Provider = 'Identity' + } } ) Steps = @( diff --git a/tests/fixtures/workflows/resolver-with-auth-session.psd1 b/tests/fixtures/workflows/resolver-with-auth-session.psd1 index c03fc97c..4b5a68cb 100644 --- a/tests/fixtures/workflows/resolver-with-auth-session.psd1 +++ b/tests/fixtures/workflows/resolver-with-auth-session.psd1 @@ -4,10 +4,10 @@ ContextResolvers = @( @{ Capability = 'IdLE.Entitlement.List' - Provider = 'Identity' With = @{ - IdentityKey = 'user1' - AuthSessionName = 'TestSession' + IdentityKey = 'user1' + Provider = 'Identity' + AuthSessionName = 'TestSession' } } ) From 3b272fb1683e297d80bbddaef7d825437bcca5b4 Mon Sep 17 00:00:00 2001 From: Matthias <13959569+blindzero@users.noreply.github.com> Date: Fri, 27 Feb 2026 21:35:31 +0100 Subject: [PATCH 3/3] Update src/IdLE.Core/Private/Invoke-IdleContextResolvers.ps1 Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/IdLE.Core/Private/Invoke-IdleContextResolvers.ps1 | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/IdLE.Core/Private/Invoke-IdleContextResolvers.ps1 b/src/IdLE.Core/Private/Invoke-IdleContextResolvers.ps1 index 8396d781..00cadea8 100644 --- a/src/IdLE.Core/Private/Invoke-IdleContextResolvers.ps1 +++ b/src/IdLE.Core/Private/Invoke-IdleContextResolvers.ps1 @@ -14,9 +14,9 @@ function Invoke-IdleContextResolvers { - Only capabilities in the read-only allow-list (Get-IdleReadOnlyCapabilities) may be used. - Each capability writes to a fixed, predefined path under Request.Context. The output path is not user-configurable. - - Provider is selected by alias when 'With.Provider' is specified; otherwise the first - provider (sorted by alias) that advertises the capability is used; ambiguity when - multiple providers match causes a fail-fast error. + - Provider is selected by alias when 'With.Provider' is specified. When 'With.Provider' + is omitted, auto-selection only succeeds if exactly one provider advertises the + capability; zero matches or multiple matches both cause a fail-fast error. - Auth sessions are supported via With.AuthSessionName / With.AuthSessionOptions, using the AuthSessionBroker in Providers (same pattern as step execution).