Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
7bf4dfc
core: add provider capability discovery helper
blindzero Jan 3, 2026
d003f12
provider: advertise mock provider capabilities
blindzero Jan 3, 2026
b2dc3bc
provider: advertise mock provider capabilities
blindzero Jan 3, 2026
08ac5dc
Merge branch 'issues/42-Refactor-provider-contracts-to-capability-bas…
blindzero Jan 3, 2026
1647a88
tests: use provider contracts for mock provider
blindzero Jan 3, 2026
5f88e97
tests: add provider capabilities contract
blindzero Jan 3, 2026
dd62ce0
tests: add unit tests for provider capability discovery
blindzero Jan 3, 2026
b4f0274
core: validate required step capabilities during planning
blindzero Jan 3, 2026
9dfa48a
provider: advertise mock provider capabilities
blindzero Jan 3, 2026
aff99e1
tests: use provider contracts for mock provider
blindzero Jan 3, 2026
50abd3f
tests: add provider capabilities contract
blindzero Jan 3, 2026
6b978b4
tests: add unit tests for provider capability discovery
blindzero Jan 3, 2026
266d052
core: validate required step capabilities during planning
blindzero Jan 3, 2026
faeffb0
Merge branch 'issues/42-Refactor-provider-contracts-to-capability-bas…
blindzero Jan 3, 2026
92770ef
tests: validate required capabilities during plan build
blindzero Jan 3, 2026
cff0075
docs: document capability-based provider model and link from architec…
blindzero Jan 3, 2026
51fad65
core: fix invocation of provider GetCapabilities method
blindzero Jan 3, 2026
57a094e
tests: load provider contracts during discovery using stable paths
blindzero Jan 3, 2026
f821d8e
core: allow RequiresCapabilities in workflow step schema
blindzero Jan 3, 2026
f347610
core: normalize workflow steps from hashtable or object for planning
blindzero Jan 3, 2026
65f2ec1
tests: add capability contracts and plan validation tests
blindzero Jan 3, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions docs/_config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,5 +15,7 @@ header_pages:
- usage/steps.md
- usage/providers.md
- advanced/architecture.md
- advanced/provider-capabilities.md
- advanced/extensibility.md
- advanced/security.md
- advanced/testing.md
7 changes: 7 additions & 0 deletions docs/_sidebar.md
Original file line number Diff line number Diff line change
@@ -1,18 +1,22 @@
- [Home](index.md)

### Overview

- [Concept](overview/concept.md)

### Getting started

- [Installation](getting-started/installation.md)
- [Quickstart](getting-started/quickstart.md)

### Usage

- [Workflows](usage/workflows.md)
- [Steps](usage/steps.md)
- [Providers](usage/providers.md)

### Reference

- [Cmdlet Reference](reference/cmdlets.md)
- [Events and Observability](reference/events-and-observability.md)
- [Providers and Contracts](reference/providers-and-contracts.md)
Expand All @@ -21,10 +25,13 @@
- [Step Catalog](reference/steps.md)

### Specifications

- [Plan export (JSON)](specs/plan-export.md)

### Advanced

- [Architecture](advanced/architecture.md)
- [Provider Capabilities](advanced/provider-capabilities.md)
- [Security](advanced/security.md)
- [Extensibility](advanced/extensibility.md)
- [Testing](advanced/testing.md)
16 changes: 15 additions & 1 deletion docs/advanced/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,27 @@ IdLE splits orchestration into two phases.

### Plan

Planning creates a deterministic plan:
IdLE builds a deterministic execution plan before any step is executed.
During this planning phase, the engine validates structural correctness,
conditions, and execution prerequisites.

- evaluates declarative conditions
- validates inputs and references
- produces data-only actions
- captures a **data-only request intent snapshot** (e.g. IdentityKeys / DesiredState / Changes) for auditing and export

#### Provider Capabilities (Planning-time Validation)

IdLE uses a **capability-based provider model** to validate execution
prerequisites during plan build.

Steps may declare required capabilities, while providers explicitly
advertise which capabilities they support. The engine matches both sides
and fails fast if required functionality is missing.

For details on the capability-based provider model and the validation flow,
see [Provider Capabilities](provider-capabilities.md).

### Execute

Execution runs the plan exactly as built:
Expand Down
16 changes: 16 additions & 0 deletions docs/advanced/extensibility.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,29 @@ Keep steps host-agnostic: do not call UI APIs directly.

## Add a new provider

Providers are responsible for interacting with external systems (directories,
cloud services, APIs, etc.).

A new provider typically involves:

1. A contract interface (if not already present)
2. A provider implementation module
3. Session acquisition via host execution context
4. Contract tests and unit tests

### Capability Advertisement

Providers must explicitly advertise their supported capabilities via a
`GetCapabilities()` method. These capabilities are used by the engine
during plan build to validate whether all required functionality is
available.

The full contract, naming rules, and validation behavior are described in
[Provider Capabilities](provider-capabilities.md).

Providers should include the corresponding provider capability contract tests
to ensure compliance.

## Versioning strategy

Keep workflows stable by treating step identifiers as contracts.
Expand Down
173 changes: 173 additions & 0 deletions docs/advanced/provider-capabilities.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
# Provider capabilities

This document describes IdLE's capability-based provider model and how capability validation fits into the planning and execution flow.

## Motivation

IdLE is designed to run in different environments with different provider implementations.
To keep the core engine generic and portable, IdLE uses **capabilities** as the contract boundary between:

- **Steps** (what is required)
- **Providers** (what is available)

Capabilities enable:

- deterministic, fail-fast validation during planning
- clear error messages when prerequisites are missing
- provider depth without hard-coding provider-specific assumptions into the engine
- re-usable contract tests for any provider module

## Terminology

### Capability

A capability is a **stable string identifier** describing a feature a provider can perform.

Naming convention:

- dot-separated segments
- no whitespace
- starts with a letter
- examples: `Identity.Read`, `Identity.Disable`, `Entitlement.Write`

## High-level flow

The following describes the end-to-end flow with capability validation included.

```text
Workflow Definition (PSD1)
|
v
Plan Builder (New-IdlePlan / New-IdlePlanObject)
|
|-- normalizes steps (Name/Type/With/Condition/RequiresCapabilities)
|
|-- NEW: capability validation (fail fast)
| - collect required capabilities from steps
| - discover available capabilities from providers
| - compare and throw on missing capabilities
|
v
Plan artifact (IdLE.Plan) is created
|
v
Plan execution (Invoke-IdlePlan / Invoke-IdlePlanObject)
|
v
Steps execute (optional runtime defensive checks may be added later)
```

Key point: **planning is the primary enforcement point**.
If a plan cannot be executed due to missing provider functionality, the plan build fails early and deterministically.

## Provider advertisement

Providers advertise capabilities explicitly via a method named:

- `GetCapabilities()`

The method returns a string list, e.g.:

```powershell
$provider | Add-Member -MemberType ScriptMethod -Name GetCapabilities -Value {
return @(
'Identity.Read'
'Identity.Attribute.Ensure'
'Identity.Disable'
)
} -Force
```

### Contract requirement

Every provider intended for use with IdLE should expose `GetCapabilities()` and return:

- only valid capability identifiers
- no duplicates
- a deterministic set (order-insensitive)

IdLE includes a reusable Pester contract to enforce this.

## Step requirements

Steps can declare required capabilities in workflow definitions using the optional key:

- `RequiresCapabilities`

Supported shapes:

- missing / `$null` -> no requirements
- string -> single capability
- string array -> multiple capabilities

Example:

```powershell
@{
Name = 'Disable identity'
Type = 'DisableIdentity'
RequiresCapabilities = @('Identity.Read', 'Identity.Disable')
}
```

During planning, IdLE normalizes this into a stable, sorted, unique string array on each plan step.

## Capability validation

Capability validation is performed during plan build:

1. Collect required capabilities from all steps (`RequiresCapabilities`)
2. Discover available capabilities from all provider instances passed via `-Providers`
3. Compare required vs. available
4. Throw a deterministic error if any required capabilities are missing

The thrown error message includes:

- `MissingCapabilities: ...`
- `AffectedSteps: ...`
- `AvailableCapabilities: ...`

This is designed for good UX and for automated diagnostics in CI logs.

## Provider discovery from `-Providers`

The engine treats the `-Providers` argument as a host-controlled "bag of objects".
For capability discovery, IdLE currently extracts candidate providers from:

- hashtable values (excluding known non-provider keys like `StepRegistry`)
- public properties on PSCustomObject provider bags (also excluding `StepRegistry`)

This keeps the engine host-agnostic while still allowing deterministic capability validation.

## Migration and inference

During migration, IdLE may infer a minimal capability set for legacy providers that do not yet implement `GetCapabilities()`.

This inference is intentionally conservative to avoid overstating what a provider can do.

Once all providers in the ecosystem advertise capabilities explicitly, inference can be disabled to make the contract stricter.

## Testing

### Provider contract tests

Providers should include contract tests that validate capability advertisement:

- `tests/ProviderContracts/ProviderCapabilities.Contract.ps1`

A provider test binds the contract to a provider instance via a factory function.

### Planning tests

Planning tests should cover:

- fail-fast behavior when capabilities are missing
- successful plan build when capabilities are available

## Future extensions

Potential follow-ups (not required for the initial capability model):

- runtime defensive checks (optional) during step execution
- richer capability metadata (versioning, parameters) if ever needed
- mapping capabilities to provider identities (which provider satisfied what), if multi-provider routing becomes necessary
1 change: 1 addition & 0 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ used between IdLE and its hosts.
## Advanced

- [Architecture](advanced/architecture.md)
- [Provider Capabilities](advanced/provider-capabilities.md)
- [Security](advanced/security.md)
- [Extensibility](advanced/extensibility.md)
- [Testing](advanced/testing.md)
Expand Down
88 changes: 88 additions & 0 deletions src/IdLE.Core/Private/Get-IdleProviderCapabilities.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
Set-StrictMode -Version Latest

function Get-IdleProviderCapabilities {
<#
.SYNOPSIS
Returns the advertised capabilities of a provider instance.

.DESCRIPTION
Capabilities are stable string identifiers that describe what a provider can do.
Steps will declare required capabilities, and the core will validate that the
required capabilities are available before executing a plan.

Providers can advertise capabilities explicitly by implementing a ScriptMethod
named 'GetCapabilities' that returns a list of capability strings.

For backward compatibility (during the migration), this function can infer a
minimal set of capabilities from well-known provider methods when no explicit
advertisement exists.

.PARAMETER Provider
The provider instance to read capabilities from.

.PARAMETER AllowInference
When set, capabilities may be inferred from provider methods if the provider
does not explicitly advertise capabilities via GetCapabilities().

.OUTPUTS
System.String[]
#>
[CmdletBinding()]
param(
[Parameter(Mandatory)]
[ValidateNotNull()]
[object] $Provider,

[Parameter()]
[switch] $AllowInference
)

$capabilities = @()

# Prefer explicit advertisement (provider-controlled, deterministic).
$hasGetCapabilitiesMethod = $Provider.PSObject.Methods.Name -contains 'GetCapabilities'
if ($hasGetCapabilitiesMethod) {
$capabilities = @($Provider.GetCapabilities())
}
elseif ($AllowInference) {
# Migration helper: infer a minimal set from known method names.
# We keep this conservative to avoid accidentally overstating capabilities.
$methodNames = @($Provider.PSObject.Methods.Name)

if ($methodNames -contains 'GetIdentity') {
$capabilities += 'Identity.Read'
}
if ($methodNames -contains 'EnsureAttribute') {
$capabilities += 'Identity.Attribute.Ensure'
}
if ($methodNames -contains 'DisableIdentity') {
$capabilities += 'Identity.Disable'
}
}

# Normalize, validate, and return a stable list.
$normalized = @()
foreach ($c in @($capabilities)) {
if ($null -eq $c) {
continue
}

$s = ($c -as [string]).Trim()
if ([string]::IsNullOrWhiteSpace($s)) {
continue
}

# Capability naming convention:
# - dot-separated segments
# - no whitespace
# - starts with a letter
# Example: 'Entitlement.Write', 'Identity.Attribute.Ensure'
if ($s -notmatch '^[A-Za-z][A-Za-z0-9]*(\.[A-Za-z0-9]+)+$') {
throw "Provider capability '$s' is invalid. Expected dot-separated segments like 'Identity.Read' or 'Entitlement.Write'."
}

$normalized += $s
}

return @($normalized | Sort-Object -Unique)
}
2 changes: 1 addition & 1 deletion src/IdLE.Core/Private/Test-IdleWorkflowSchema.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ function Test-IdleWorkflowSchema {
continue
}

$allowedStepKeys = @('Name', 'Type', 'Condition', 'With', 'Description')
$allowedStepKeys = @('Name', 'Type', 'Condition', 'With', 'Description', 'RequiresCapabilities')
foreach ($k in $step.Keys) {
if ($allowedStepKeys -notcontains $k) {
$errors.Add("Unknown key '$k' in $stepPath. Allowed keys: $($allowedStepKeys -join ', ').")
Expand Down
Loading