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..e57e3dcb 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,7 +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 @@ -140,21 +139,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..ac14a2df 100644 --- a/src/IdLE.Core/Public/New-IdlePlanObject.ps1 +++ b/src/IdLE.Core/Public/New-IdlePlanObject.ps1 @@ -71,7 +71,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..ad42751c 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 @@ -122,19 +102,6 @@ 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*' - } } } 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": {