From b9f2855b1b5eda2b4ff7b9fd1692da2f55b3da57 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 22 Feb 2026 15:59:06 +0000 Subject: [PATCH 1/5] Initial plan From 72a7962ca235d28cb9f72fc920e0e8c11fcd1c15 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 22 Feb 2026 16:08:34 +0000 Subject: [PATCH 2/5] Remove Request.Changes and -Changes request parameter (breaking change) Co-authored-by: blindzero <13959569+blindzero@users.noreply.github.com> --- docs/extend/extensibility.md | 2 +- docs/reference/cmdlets/New-IdleRequest.md | 19 +---- docs/reference/specs/plan-export.md | 4 +- docs/use/workflows.md | 1 - .../ConvertTo-IdlePlanExportObject.ps1 | 6 +- .../Private/IdleLifecycleRequest.ps1 | 9 --- .../Private/Resolve-IdleTemplateString.ps1 | 5 +- src/IdLE.Core/Public/New-IdlePlanObject.ps1 | 10 ++- .../Public/New-IdleRequestObject.ps1 | 20 ++--- src/IdLE/Public/New-IdleRequest.ps1 | 9 +-- tests/Core/New-IdleRequest.Tests.ps1 | 80 ++++++++++++------- .../Resolve-IdleWorkflowTemplates.Tests.ps1 | 13 +++ tests/_testHelpers.ps1 | 4 - .../plan-export/expected/plan-export.json | 3 +- .../template-tests/template-changes-root.psd1 | 13 +++ 15 files changed, 98 insertions(+), 100 deletions(-) create mode 100644 tests/fixtures/workflows/template-tests/template-changes-root.psd1 diff --git a/docs/extend/extensibility.md b/docs/extend/extensibility.md index 1a95cec6..1a29f552 100644 --- a/docs/extend/extensibility.md +++ b/docs/extend/extensibility.md @@ -141,4 +141,4 @@ The following are **not contracts** and may change in minor/patch versions: **Lifecycle request contract**: - Required fields: `LifecycleEvent`, `CorrelationId` -- Optional fields: `Actor`, `IdentityKeys`, `Intent`, `Context`, `Changes` +- Optional fields: `Actor`, `IdentityKeys`, `Intent`, `Context` diff --git a/docs/reference/cmdlets/New-IdleRequest.md b/docs/reference/cmdlets/New-IdleRequest.md index 69e111af..c7f92a8c 100644 --- a/docs/reference/cmdlets/New-IdleRequest.md +++ b/docs/reference/cmdlets/New-IdleRequest.md @@ -14,7 +14,7 @@ Creates a lifecycle request object. ``` New-IdleRequest [-LifecycleEvent] <String> [[-CorrelationId] <String>] [[-Actor] <String>] - [[-IdentityKeys] <Hashtable>] [[-Intent] <Hashtable>] [[-Context] <Hashtable>] [[-Changes] <Hashtable>] + [[-IdentityKeys] <Hashtable>] [[-Intent] <Hashtable>] [[-Context] <Hashtable>] [-ProgressAction <ActionPreference>] [<CommonParameters>] ``` @@ -24,8 +24,6 @@ Creates and normalizes an IdLE LifecycleRequest representing business intent Joiner/Mover/Leaver). CorrelationId is generated if missing. Actor is optional. -Changes is optional and stays $null when omitted. - ## EXAMPLES ### EXAMPLE 1 @@ -140,21 +138,6 @@ Accept pipeline input: False Accept wildcard characters: False ``` -### -Changes -Optional hashtable describing changes (typically used for Mover lifecycle events). - -```yaml -Type: Hashtable -Parameter Sets: (All) -Aliases: - -Required: False -Position: 7 -Default value: None -Accept pipeline input: False -Accept wildcard characters: False -``` - ### -ProgressAction TODO: ProgressAction Description diff --git a/docs/reference/specs/plan-export.md b/docs/reference/specs/plan-export.md index 43b68cdb..d7f11107 100644 --- a/docs/reference/specs/plan-export.md +++ b/docs/reference/specs/plan-export.md @@ -94,8 +94,7 @@ The request object captures *why* a plan was created, independent of *how* it wi }, "desiredState": { "department": "IT" - }, - "changes": null + } } } ``` @@ -122,7 +121,6 @@ The request object captures *why* a plan was created, independent of *how* it wi - For **IdLE-native lifecycle requests**, `input` SHOULD contain: - `identityKeys` – identifiers of the target identity - `desiredState` – intended target state - - `changes` – explicit deltas, if applicable - Hosts MAY include additional fields in `input`. - The request payload is exported for **audit, approval, and traceability purposes** and MUST remain stable once the plan is created. diff --git a/docs/use/workflows.md b/docs/use/workflows.md index 72a44e8b..cd14e79b 100644 --- a/docs/use/workflows.md +++ b/docs/use/workflows.md @@ -87,7 +87,6 @@ For security, only these path roots are permitted: | `Request.Intent.*` | Caller-provided action inputs | | `Request.Context.*` | Read-only associated context (host/resolver-provided) | | `Request.IdentityKeys.*` | Identifiers of the target identity | -| `Request.Changes.*` | Explicit deltas (Mover events) | | `Request.LifecycleEvent` | Lifecycle event type (e.g. `Joiner`) | | `Request.CorrelationId` | Stable correlation identifier | | `Request.Actor` | Originator of the request | diff --git a/src/IdLE.Core/Private/ConvertTo-IdlePlanExportObject.ps1 b/src/IdLE.Core/Private/ConvertTo-IdlePlanExportObject.ps1 index 7226a225..f5b3639b 100644 --- a/src/IdLE.Core/Private/ConvertTo-IdlePlanExportObject.ps1 +++ b/src/IdLE.Core/Private/ConvertTo-IdlePlanExportObject.ps1 @@ -80,19 +80,17 @@ function ConvertTo-IdlePlanExportObject { $requestInput = Get-FirstPropertyValue -Object $request -Names @('Input', 'Data', 'Payload', 'Attributes') if ($null -eq $requestInput) { - # IdLE lifecycle requests store business intent as IdentityKeys/Intent/Context/Changes. + # IdLE lifecycle requests store business intent as IdentityKeys/Intent/Context. # When present, export these as the canonical request.input payload. $identityKeys = Get-FirstPropertyValue -Object $request -Names @('IdentityKeys', 'IdentityKey', 'Keys') $intent = Get-FirstPropertyValue -Object $request -Names @('Intent', 'TargetState') $context = Get-FirstPropertyValue -Object $request -Names @('Context') - $changes = Get-FirstPropertyValue -Object $request -Names @('Changes', 'Delta') - if ($null -ne $identityKeys -or $null -ne $intent -or $null -ne $context -or $null -ne $changes) { + if ($null -ne $identityKeys -or $null -ne $intent -or $null -ne $context) { $requestInput = New-OrderedMap $requestInput.identityKeys = $identityKeys $requestInput.intent = $intent $requestInput.context = $context - $requestInput.changes = $changes } } } diff --git a/src/IdLE.Core/Private/IdleLifecycleRequest.ps1 b/src/IdLE.Core/Private/IdleLifecycleRequest.ps1 index 514f670e..a5408b15 100644 --- a/src/IdLE.Core/Private/IdleLifecycleRequest.ps1 +++ b/src/IdLE.Core/Private/IdleLifecycleRequest.ps1 @@ -1,6 +1,5 @@ # Domain model: LifecycleRequest # Actor is intentionally optional in V1 (see architecture). -# Changes is optional and stays $null if not provided. # # Intent - canonical caller-provided input block. # Context - read-only associated context provided by the host or resolvers. @@ -10,7 +9,6 @@ class IdleLifecycleRequest { [hashtable] $IdentityKeys [hashtable] $Intent [hashtable] $Context - [hashtable] $Changes [string] $CorrelationId [string] $Actor @@ -19,7 +17,6 @@ class IdleLifecycleRequest { [hashtable] $identityKeys, [hashtable] $intent, [hashtable] $context, - [hashtable] $changes, [string] $correlationId, [string] $actor ) { @@ -27,7 +24,6 @@ class IdleLifecycleRequest { $this.IdentityKeys = $identityKeys $this.Intent = $intent $this.Context = $context - $this.Changes = $changes $this.CorrelationId = $correlationId $this.Actor = $actor @@ -51,11 +47,6 @@ class IdleLifecycleRequest { $this.Context = @{} } - # Changes stays $null if not provided. If provided, it must be a hashtable. - if ($null -ne $this.Changes -and $this.Changes -isnot [hashtable]) { - throw [System.ArgumentException]::new('Changes must be a hashtable when provided.', 'Changes') - } - if ([string]::IsNullOrWhiteSpace($this.CorrelationId)) { $this.CorrelationId = [guid]::NewGuid().Guid } diff --git a/src/IdLE.Core/Private/Resolve-IdleTemplateString.ps1 b/src/IdLE.Core/Private/Resolve-IdleTemplateString.ps1 index a5e8c1aa..0090276e 100644 --- a/src/IdLE.Core/Private/Resolve-IdleTemplateString.ps1 +++ b/src/IdLE.Core/Private/Resolve-IdleTemplateString.ps1 @@ -16,7 +16,6 @@ function Resolve-IdleTemplateString { - Request.Intent.* (canonical caller-provided action inputs) - Request.Context.* (read-only associated context) - Request.IdentityKeys.* - - Request.Changes.* - Request.LifecycleEvent - Request.CorrelationId - Request.Actor @@ -72,7 +71,7 @@ function Resolve-IdleTemplateString { # Define validation constants used in multiple paths $pathValidationPattern = '^[A-Za-z][A-Za-z0-9_]*(\.[A-Za-z0-9_]+)*$' - $allowedRoots = @('Request.Intent', 'Request.Context', 'Request.IdentityKeys', 'Request.Changes', 'Request.LifecycleEvent', 'Request.CorrelationId', 'Request.Actor') + $allowedRoots = @('Request.Intent', 'Request.Context', 'Request.IdentityKeys', 'Request.LifecycleEvent', 'Request.CorrelationId', 'Request.Actor') # Helper function to validate path pattern $validatePath = { @@ -188,7 +187,7 @@ function Resolve-IdleTemplateString { # like \{{Request.Foo}} (invalid root) or \{{Request..Name}} (double dot) are still escaped to # literal {{, rather than flowing into template parsing and failing with path/root errors. $litOpenPlaceholder = [string][char]0xE001 - $backslashEscapePattern = '\\{{(?!Request\.(?:(?:Intent|Context|IdentityKeys|Changes)(?:\.[A-Za-z0-9_]+)*|LifecycleEvent|CorrelationId|Actor)}})' + $backslashEscapePattern = '\\{{(?!Request\.(?:(?:Intent|Context|IdentityKeys)(?:\.[A-Za-z0-9_]+)*|LifecycleEvent|CorrelationId|Actor)}})' $normalizedValue = if ($stringValue -match '\\{{') { [regex]::Replace($stringValue, $backslashEscapePattern, $litOpenPlaceholder) } else { diff --git a/src/IdLE.Core/Public/New-IdlePlanObject.ps1 b/src/IdLE.Core/Public/New-IdlePlanObject.ps1 index 69be1fa5..d9ab7719 100644 --- a/src/IdLE.Core/Public/New-IdlePlanObject.ps1 +++ b/src/IdLE.Core/Public/New-IdlePlanObject.ps1 @@ -62,6 +62,15 @@ function New-IdlePlanObject { ) } + # Reject requests that contain Request.Changes — this property has been removed. + # Model delta-like instructions explicitly under Request.Intent instead. + if ($reqProps -contains 'Changes') { + throw [System.ArgumentException]::new( + "Request object must not contain property 'Changes'. 'Changes' has been removed from the request model. Model delta-like instructions explicitly under 'Intent' instead.", + 'Request' + ) + } + # Create a data-only snapshot of the incoming request for deterministic exports. $requestSnapshot = [pscustomobject]@{ PSTypeName = 'IdLE.LifecycleRequestSnapshot' @@ -71,7 +80,6 @@ function New-IdlePlanObject { IdentityKeys = if ($reqProps -contains 'IdentityKeys') { Copy-IdleDataObject -Value $Request.IdentityKeys } else { $null } Intent = if ($reqProps -contains 'Intent') { Copy-IdleDataObject -Value $Request.Intent } else { $null } Context = if ($reqProps -contains 'Context') { Copy-IdleDataObject -Value $Request.Context } else { $null } - Changes = if ($reqProps -contains 'Changes') { Copy-IdleDataObject -Value $Request.Changes } else { $null } } # Validate workflow and ensure it matches the request's LifecycleEvent. diff --git a/src/IdLE.Core/Public/New-IdleRequestObject.ps1 b/src/IdLE.Core/Public/New-IdleRequestObject.ps1 index aa948d07..77d8d096 100644 --- a/src/IdLE.Core/Public/New-IdleRequestObject.ps1 +++ b/src/IdLE.Core/Public/New-IdleRequestObject.ps1 @@ -8,7 +8,7 @@ function New-IdleRequestObject { (e.g. Joiner/Mover/Leaver). This is the core factory function used by the IdLE module wrapper. The function validates that no ScriptBlocks are present in the input data (IdentityKeys, - Intent, Context, Changes) to enforce the data-only configuration principle. Input hashtables + Intent, Context) to enforce the data-only configuration principle. Input hashtables are cloned to prevent external mutation after object creation. CorrelationId is preserved if provided; otherwise, the IdleLifecycleRequest class generates @@ -40,10 +40,6 @@ function New-IdleRequestObject { (e.g. identity snapshots, device hints). Defaults to an empty hashtable if not provided. Must not contain ScriptBlocks. Must not be treated as mutable state within IdLE. - .PARAMETER Changes - Optional hashtable describing changes (typically used for Mover lifecycle events to indicate - what changed from the previous state). Remains $null when omitted. Must not contain ScriptBlocks. - .EXAMPLE $request = New-IdleRequestObject -LifecycleEvent 'Joiner' @@ -55,9 +51,9 @@ function New-IdleRequestObject { Creates a Joiner request with specific identity keys and intent attributes for a typical onboarding workflow. .EXAMPLE - $request = New-IdleRequestObject -LifecycleEvent 'Mover' -IdentityKeys @{ UPN = 'user@contoso.com' } -Changes @{ Department = 'Sales' } -Actor 'admin@contoso.com' + $request = New-IdleRequestObject -LifecycleEvent 'Mover' -IdentityKeys @{ UPN = 'user@contoso.com' } -Intent @{ NewDepartment = 'Sales' } -Actor 'admin@contoso.com' - Creates a Mover request with identity keys, changes, and actor information for a department transfer workflow. + Creates a Mover request with identity keys, intent, and actor information for a department transfer workflow. .OUTPUTS IdleLifecycleRequest @@ -66,7 +62,7 @@ function New-IdleRequestObject { Security Considerations: - Input data must be data-only (no ScriptBlocks or executable objects). The function validates this constraint and throws if violated. - - Do not embed secrets in IdentityKeys, Intent, Context, or Changes. Use the AuthSessionBroker + - Do not embed secrets in IdentityKeys, Intent, or Context. Use the AuthSessionBroker pattern for credential/token management. - Sensitive data in request objects may be logged or emitted in events. Rely on redaction boundaries defined in the engine's event sink and logging layers. @@ -93,24 +89,19 @@ function New-IdleRequestObject { [hashtable] $Intent = @{}, [Parameter()] - [hashtable] $Context = @{}, - - [Parameter()] - [hashtable] $Changes + [hashtable] $Context = @{} ) # Validate that no ScriptBlocks are present in the input data Assert-IdleNoScriptBlock -InputObject $IdentityKeys -Path 'IdentityKeys' Assert-IdleNoScriptBlock -InputObject $Intent -Path 'Intent' Assert-IdleNoScriptBlock -InputObject $Context -Path 'Context' - Assert-IdleNoScriptBlock -InputObject $Changes -Path 'Changes' # Clone hashtables to avoid external mutation after object creation # shallow clone is sufficient as we have already validated no ScriptBlocks are present $IdentityKeys = if ($null -eq $IdentityKeys) { @{} } else { $IdentityKeys.Clone() } $Intent = if ($null -eq $Intent) { @{} } else { $Intent.Clone() } $Context = if ($null -eq $Context) { @{} } else { $Context.Clone() } - $Changes = if ($null -eq $Changes) { $null } else { $Changes.Clone() } # Construct and return the core domain object defined in Private/IdleLifecycleRequest.ps1 return [IdleLifecycleRequest]::new( @@ -118,7 +109,6 @@ function New-IdleRequestObject { $IdentityKeys, $Intent, $Context, - $Changes, $CorrelationId, $Actor ) diff --git a/src/IdLE/Public/New-IdleRequest.ps1 b/src/IdLE/Public/New-IdleRequest.ps1 index 693393e9..c434cb20 100644 --- a/src/IdLE/Public/New-IdleRequest.ps1 +++ b/src/IdLE/Public/New-IdleRequest.ps1 @@ -6,7 +6,6 @@ function New-IdleRequest { .DESCRIPTION Creates and normalizes an IdLE LifecycleRequest representing business intent (e.g. Joiner/Mover/Leaver). CorrelationId is generated if missing. Actor is optional. - Changes is optional and stays $null when omitted. .PARAMETER LifecycleEvent The lifecycle event name (e.g. Joiner, Mover, Leaver). @@ -28,9 +27,6 @@ function New-IdleRequest { A hashtable containing read-only associated context provided by the host or resolvers (e.g. identity snapshots, device hints). Must not be treated as mutable state within IdLE. - .PARAMETER Changes - Optional hashtable describing changes (typically used for Mover lifecycle events). - .EXAMPLE # Minimal Joiner request — CorrelationId is auto-generated, Intent/Context default to empty New-IdleRequest -LifecycleEvent Joiner -CorrelationId (New-Guid) -IdentityKeys @{ EmployeeId = '12345' } @@ -61,10 +57,7 @@ function New-IdleRequest { [hashtable] $Intent = @{}, [Parameter()] - [hashtable] $Context = @{}, - - [Parameter()] - [hashtable] $Changes + [hashtable] $Context = @{} ) # Use core-exported factory to construct the domain object. Keeps domain model inside IdLE.Core. diff --git a/tests/Core/New-IdleRequest.Tests.ps1 b/tests/Core/New-IdleRequest.Tests.ps1 index 647c7acd..16665de1 100644 --- a/tests/Core/New-IdleRequest.Tests.ps1 +++ b/tests/Core/New-IdleRequest.Tests.ps1 @@ -46,26 +46,6 @@ Describe 'New-IdleRequest' { } Context 'Optional properties' { - It 'leaves Changes as null when omitted' { - $req = New-IdleRequest -LifecycleEvent 'Mover' - $req.Changes | Should -BeNullOrEmpty - } - - It 'accepts Changes when provided' { - $req = New-IdleRequest -LifecycleEvent 'Mover' -Changes @{ - Attributes = @{ - Department = @{ - From = 'Sales' - To = 'IT' - } - } - } - - $req.Changes | Should -BeOfType 'hashtable' - $req.Changes.Attributes.Department.From | Should -Be 'Sales' - $req.Changes.Attributes.Department.To | Should -Be 'IT' - } - It 'treats Actor as optional (null when omitted)' { $req = New-IdleRequest -LifecycleEvent 'Joiner' $req.Actor | Should -BeNullOrEmpty @@ -75,6 +55,16 @@ Describe 'New-IdleRequest' { $req = New-IdleRequest -LifecycleEvent 'Joiner' -Actor 'alice@contoso.com' $req.Actor | Should -Be 'alice@contoso.com' } + + It 'does not expose a Changes property' { + $req = New-IdleRequest -LifecycleEvent 'Joiner' + $req.PSObject.Properties.Name | Should -Not -Contain 'Changes' + } + + It 'does not accept a -Changes parameter' { + { New-IdleRequest -LifecycleEvent 'Joiner' -Changes @{ Foo = 'Bar' } } | + Should -Throw + } } Context 'Intent parameter' { @@ -122,19 +112,47 @@ Describe 'New-IdleRequest - data-only validation' { } } | Should -Throw -ExpectedMessage '*ScriptBlocks are not allowed*' } + } +} - It 'rejects ScriptBlock in Changes when provided' { - { - New-IdleRequest -LifecycleEvent 'Joiner' -Changes @{ - Attributes = @{ - Department = @{ - From = 'Sales' - To = { 'IT' } - } - } - } - } | Should -Throw -ExpectedMessage '*ScriptBlocks are not allowed*Changes*' +Describe 'New-IdlePlan - Request.Changes rejection' { + BeforeAll { + function global:Invoke-IdleTestNoopStep2 { + [CmdletBinding()] + param( + [Parameter(Mandatory)][ValidateNotNull()][object] $Context, + [Parameter(Mandatory)][ValidateNotNull()][object] $Step + ) + return [pscustomobject]@{ + PSTypeName = 'IdLE.StepResult' + Name = [string]$Step.Name + Type = [string]$Step.Type + Status = 'Completed' + Error = $null + } + } + } + + AfterAll { + Remove-Item -Path 'Function:\Invoke-IdleTestNoopStep2' -ErrorAction SilentlyContinue + } + + It 'rejects a request object that contains a Changes property' { + $badRequest = [pscustomobject]@{ + PSTypeName = 'IdLE.LifecycleRequest' + LifecycleEvent = 'Joiner' + CorrelationId = [guid]::NewGuid().ToString() + Changes = @{ Department = @{ From = 'Sales'; To = 'IT' } } } + + $wfPath = Join-Path $PSScriptRoot '..' 'fixtures/workflows/template-tests/template-simple.psd1' + $providers = @{ + StepRegistry = @{ 'IdLE.Step.Test' = 'Invoke-IdleTestNoopStep2' } + StepMetadata = New-IdleTestStepMetadata -StepTypes @('IdLE.Step.Test') + } + + { New-IdlePlan -WorkflowPath $wfPath -Request $badRequest -Providers $providers } | + Should -Throw -ExpectedMessage "*must not contain property 'Changes'*" } } diff --git a/tests/Core/Resolve-IdleWorkflowTemplates.Tests.ps1 b/tests/Core/Resolve-IdleWorkflowTemplates.Tests.ps1 index 160ba5a5..77deb6e2 100644 --- a/tests/Core/Resolve-IdleWorkflowTemplates.Tests.ps1 +++ b/tests/Core/Resolve-IdleWorkflowTemplates.Tests.ps1 @@ -338,6 +338,19 @@ Describe 'Template Substitution' { { New-IdlePlan -WorkflowPath $wfPath -Request $req -Providers $providers } | Should -Throw -ExpectedMessage '*is not allowed*' } + + It 'throws when accessing Request.Changes root' { + $wfPath = Get-TemplateTestFixture 'template-changes-root' + + $req = New-IdleTestRequest -LifecycleEvent 'Joiner' -Intent @{ Name = 'Test' } + $providers = @{ + StepRegistry = @{ 'IdLE.Step.Test' = 'Invoke-IdleTestNoopStep' } + StepMetadata = New-IdleTestStepMetadata -StepTypes @('IdLE.Step.Test') + } + + { New-IdlePlan -WorkflowPath $wfPath -Request $req -Providers $providers } | + Should -Throw -ExpectedMessage '*is not allowed*' + } } Context 'Escaping' { diff --git a/tests/_testHelpers.ps1 b/tests/_testHelpers.ps1 index 195f3342..8e00fc99 100644 --- a/tests/_testHelpers.ps1 +++ b/tests/_testHelpers.ps1 @@ -85,9 +85,6 @@ function New-IdleTestRequest { [Parameter()] [hashtable] $Context, - [Parameter()] - [hashtable] $Changes, - [Parameter()] [string] $CorrelationId, @@ -100,7 +97,6 @@ function New-IdleTestRequest { if ($PSBoundParameters.ContainsKey('IdentityKeys')) { $params.IdentityKeys = $IdentityKeys } if ($PSBoundParameters.ContainsKey('Intent')) { $params.Intent = $Intent } if ($PSBoundParameters.ContainsKey('Context')) { $params.Context = $Context } - if ($PSBoundParameters.ContainsKey('Changes')) { $params.Changes = $Changes } if ($PSBoundParameters.ContainsKey('CorrelationId')) { $params.CorrelationId = $CorrelationId } if ($PSBoundParameters.ContainsKey('Actor')) { $params.Actor = $Actor } diff --git a/tests/fixtures/plan-export/expected/plan-export.json b/tests/fixtures/plan-export/expected/plan-export.json index 1a4bea62..a21c7d09 100644 --- a/tests/fixtures/plan-export/expected/plan-export.json +++ b/tests/fixtures/plan-export/expected/plan-export.json @@ -14,8 +14,7 @@ "intent": { "department": "IT" }, - "context": {}, - "changes": null + "context": {} } }, "plan": { diff --git a/tests/fixtures/workflows/template-tests/template-changes-root.psd1 b/tests/fixtures/workflows/template-tests/template-changes-root.psd1 new file mode 100644 index 00000000..bc52d59f --- /dev/null +++ b/tests/fixtures/workflows/template-tests/template-changes-root.psd1 @@ -0,0 +1,13 @@ +@{ + Name = 'Template Test - Changes Root' + LifecycleEvent = 'Joiner' + Steps = @( + @{ + Name = 'TestStep' + Type = 'IdLE.Step.Test' + With = @{ + Value = '{{Request.Changes.Department}}' + } + } + ) +} From eb18148425aee8e0c47ac245718aed85469b064b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 22 Feb 2026 16:26:44 +0000 Subject: [PATCH 3/5] Remove blacklist-style Changes checks from tests and guard from New-IdlePlanObject Co-authored-by: blindzero <13959569+blindzero@users.noreply.github.com> --- src/IdLE.Core/Public/New-IdlePlanObject.ps1 | 9 ---- tests/Core/New-IdleRequest.Tests.ps1 | 51 --------------------- 2 files changed, 60 deletions(-) diff --git a/src/IdLE.Core/Public/New-IdlePlanObject.ps1 b/src/IdLE.Core/Public/New-IdlePlanObject.ps1 index d9ab7719..ac14a2df 100644 --- a/src/IdLE.Core/Public/New-IdlePlanObject.ps1 +++ b/src/IdLE.Core/Public/New-IdlePlanObject.ps1 @@ -62,15 +62,6 @@ function New-IdlePlanObject { ) } - # Reject requests that contain Request.Changes — this property has been removed. - # Model delta-like instructions explicitly under Request.Intent instead. - if ($reqProps -contains 'Changes') { - throw [System.ArgumentException]::new( - "Request object must not contain property 'Changes'. 'Changes' has been removed from the request model. Model delta-like instructions explicitly under 'Intent' instead.", - 'Request' - ) - } - # Create a data-only snapshot of the incoming request for deterministic exports. $requestSnapshot = [pscustomobject]@{ PSTypeName = 'IdLE.LifecycleRequestSnapshot' diff --git a/tests/Core/New-IdleRequest.Tests.ps1 b/tests/Core/New-IdleRequest.Tests.ps1 index 16665de1..ad42751c 100644 --- a/tests/Core/New-IdleRequest.Tests.ps1 +++ b/tests/Core/New-IdleRequest.Tests.ps1 @@ -55,16 +55,6 @@ Describe 'New-IdleRequest' { $req = New-IdleRequest -LifecycleEvent 'Joiner' -Actor 'alice@contoso.com' $req.Actor | Should -Be 'alice@contoso.com' } - - It 'does not expose a Changes property' { - $req = New-IdleRequest -LifecycleEvent 'Joiner' - $req.PSObject.Properties.Name | Should -Not -Contain 'Changes' - } - - It 'does not accept a -Changes parameter' { - { New-IdleRequest -LifecycleEvent 'Joiner' -Changes @{ Foo = 'Bar' } } | - Should -Throw - } } Context 'Intent parameter' { @@ -115,47 +105,6 @@ Describe 'New-IdleRequest - data-only validation' { } } -Describe 'New-IdlePlan - Request.Changes rejection' { - BeforeAll { - function global:Invoke-IdleTestNoopStep2 { - [CmdletBinding()] - param( - [Parameter(Mandatory)][ValidateNotNull()][object] $Context, - [Parameter(Mandatory)][ValidateNotNull()][object] $Step - ) - return [pscustomobject]@{ - PSTypeName = 'IdLE.StepResult' - Name = [string]$Step.Name - Type = [string]$Step.Type - Status = 'Completed' - Error = $null - } - } - } - - AfterAll { - Remove-Item -Path 'Function:\Invoke-IdleTestNoopStep2' -ErrorAction SilentlyContinue - } - - It 'rejects a request object that contains a Changes property' { - $badRequest = [pscustomobject]@{ - PSTypeName = 'IdLE.LifecycleRequest' - LifecycleEvent = 'Joiner' - CorrelationId = [guid]::NewGuid().ToString() - Changes = @{ Department = @{ From = 'Sales'; To = 'IT' } } - } - - $wfPath = Join-Path $PSScriptRoot '..' 'fixtures/workflows/template-tests/template-simple.psd1' - $providers = @{ - StepRegistry = @{ 'IdLE.Step.Test' = 'Invoke-IdleTestNoopStep2' } - StepMetadata = New-IdleTestStepMetadata -StepTypes @('IdLE.Step.Test') - } - - { New-IdlePlan -WorkflowPath $wfPath -Request $badRequest -Providers $providers } | - Should -Throw -ExpectedMessage "*must not contain property 'Changes'*" - } -} - Describe 'New-IdlePlan - Request.Identity rejection' { BeforeAll { function global:Invoke-IdleTestNoopStep { From 57adc96a9444e8c66439aa4a44e28011df301e61 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 22 Feb 2026 16:32:33 +0000 Subject: [PATCH 4/5] Remove Request.Changes disallowed-root test and fixture Co-authored-by: blindzero <13959569+blindzero@users.noreply.github.com> --- tests/Core/Resolve-IdleWorkflowTemplates.Tests.ps1 | 13 ------------- .../template-tests/template-changes-root.psd1 | 13 ------------- 2 files changed, 26 deletions(-) delete mode 100644 tests/fixtures/workflows/template-tests/template-changes-root.psd1 diff --git a/tests/Core/Resolve-IdleWorkflowTemplates.Tests.ps1 b/tests/Core/Resolve-IdleWorkflowTemplates.Tests.ps1 index 77deb6e2..160ba5a5 100644 --- a/tests/Core/Resolve-IdleWorkflowTemplates.Tests.ps1 +++ b/tests/Core/Resolve-IdleWorkflowTemplates.Tests.ps1 @@ -338,19 +338,6 @@ Describe 'Template Substitution' { { New-IdlePlan -WorkflowPath $wfPath -Request $req -Providers $providers } | Should -Throw -ExpectedMessage '*is not allowed*' } - - It 'throws when accessing Request.Changes root' { - $wfPath = Get-TemplateTestFixture 'template-changes-root' - - $req = New-IdleTestRequest -LifecycleEvent 'Joiner' -Intent @{ Name = 'Test' } - $providers = @{ - StepRegistry = @{ 'IdLE.Step.Test' = 'Invoke-IdleTestNoopStep' } - StepMetadata = New-IdleTestStepMetadata -StepTypes @('IdLE.Step.Test') - } - - { New-IdlePlan -WorkflowPath $wfPath -Request $req -Providers $providers } | - Should -Throw -ExpectedMessage '*is not allowed*' - } } Context 'Escaping' { diff --git a/tests/fixtures/workflows/template-tests/template-changes-root.psd1 b/tests/fixtures/workflows/template-tests/template-changes-root.psd1 deleted file mode 100644 index bc52d59f..00000000 --- a/tests/fixtures/workflows/template-tests/template-changes-root.psd1 +++ /dev/null @@ -1,13 +0,0 @@ -@{ - Name = 'Template Test - Changes Root' - LifecycleEvent = 'Joiner' - Steps = @( - @{ - Name = 'TestStep' - Type = 'IdLE.Step.Test' - With = @{ - Value = '{{Request.Changes.Department}}' - } - } - ) -} From 430ecdd9ce8d8028de400b5eedbadacf780371ab Mon Sep 17 00:00:00 2001 From: Matthias Fleschuetz <13959569+blindzero@users.noreply.github.com> Date: Sun, 22 Feb 2026 17:51:03 +0100 Subject: [PATCH 5/5] docs: update cmdlet reference --- docs/reference/cmdlets/New-IdleRequest.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/reference/cmdlets/New-IdleRequest.md b/docs/reference/cmdlets/New-IdleRequest.md index c7f92a8c..e57e3dcb 100644 --- a/docs/reference/cmdlets/New-IdleRequest.md +++ b/docs/reference/cmdlets/New-IdleRequest.md @@ -24,6 +24,7 @@ Creates and normalizes an IdLE LifecycleRequest representing business intent Joiner/Mover/Leaver). CorrelationId is generated if missing. Actor is optional. + ## EXAMPLES ### EXAMPLE 1