Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
14 changes: 9 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,11 +87,14 @@ The demo shows:
- building a deterministic plan from a workflow definition (`.psd1`)
- executing the plan using a host-provided step registry

The execution result buffers all emitted events in `result.Events`. Hosts can optionally stream events live
by providing `-EventSink` as an object implementing `WriteEvent(event)`.

Next steps:

- Usage & examples: `docs/02-examples.md`
- Architecture: `docs/01-architecture.md`
- Documentation entry point: `docs/index.md`
- Workflow samples: `examples/workflows/`
- Repository demo: `examples/run-demo.ps1`
- Pester tests: `tests/`

---
Expand All @@ -100,9 +103,10 @@ Next steps:

Start here:

- `docs/00-index.md` – documentation map
- `docs/01-architecture.md` – architecture and principles
- `docs/02-examples.md` – runnable examples + workflow snippets
- `docs/index.md` – documentation map
- `docs/getting-started/quickstart.md` – plan → execute walkthrough
- `docs/advanced/architecture.md` – architecture and principles
- `docs/usage/workflows.md` – workflow schema and validation

Project docs:

Expand Down
1 change: 1 addition & 0 deletions docs/_config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ markdown: kramdown
kramdown:
input: GFM
hard_wrap: false
logo: "/assets/idle_logo_flat_white.png"
plugins: []
header_pages:
- overview/concept.md
Expand Down
14 changes: 14 additions & 0 deletions docs/advanced/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,20 @@ This enables previews, approvals, and repeatable audits.
Conditions are data-only objects.
They are validated early and evaluated deterministically.

## Eventing

IdLE emits **structured events** during execution.

- The engine always creates an `EventSink` and exposes it as `Context.EventSink`.
- Steps and the engine use a single contract: `Context.EventSink.WriteEvent(Type, Message, StepName, Data)`.
- All events are buffered in the execution result (`result.Events`).

Hosts may optionally provide an external sink to stream events live:

- `Invoke-IdlePlan -EventSink <object>`
- The sink must implement `WriteEvent(event)`
- ScriptBlock sinks are rejected (secure default)

## State ownership

Steps may only write to `State.*` and only to declared output paths.
Expand Down
6 changes: 6 additions & 0 deletions docs/advanced/extensibility.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,12 @@ A new step typically involves:
3. An execution function (invoke) that performs actions via providers
4. Unit tests (Pester)

Steps can emit structured events using the execution context contract:

- `Context.EventSink.WriteEvent(Type, Message, StepName, Data)`

Keep steps host-agnostic: do not call UI APIs directly.

## Add a new provider

A new provider typically involves:
Expand Down
Binary file added docs/assets/idle_logo_flat_white.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/assets/idle_logo_flat_white_text.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/assets/idle_logo_text.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/assets/idle_logo_tranparent.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
34 changes: 33 additions & 1 deletion docs/getting-started/quickstart.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ pwsh -File .\examples\run-demo.ps1
## Minimal plan and execute

```powershell
$workflowPath = Join-Path $PSScriptRoot 'workflows\joiner-with-when.psd1'
$workflowPath = Join-Path (Get-Location) 'examples\workflows\joiner-with-when.psd1'

$request = New-IdleLifecycleRequest -LifecycleEvent 'Joiner' -Actor 'example-user'
$plan = New-IdlePlan -WorkflowPath $workflowPath -Request $request
Expand All @@ -26,6 +26,38 @@ $providers = @{}
$result = Invoke-IdlePlan -Plan $plan -Providers $providers
```

## Inspect results and events

Execution returns a result object containing step results and buffered events:

```powershell
$result.Status
$result.Steps
$result.Events | Select-Object Type, StepName, Message
```

## Optional: stream events with -EventSink

If a host wants live progress, it can provide an **object** event sink.
The sink must implement `WriteEvent(event)`.

> Security note: ScriptBlock sinks are not supported.

Example:

```powershell
$streamed = [System.Collections.Generic.List[object]]::new()

$sink = [pscustomobject]@{}
$null = Add-Member -InputObject $sink -MemberType ScriptMethod -Name 'WriteEvent' -Value {
param($e)
[void]$streamed.Add($e)
Write-Host ("[{0}] {1}" -f $e.Type, $e.Message)
}

$result = Invoke-IdlePlan -Plan $plan -Providers $providers -EventSink $sink
```

## Next steps

- [Workflows](../usage/workflows.md)
Expand Down
33 changes: 33 additions & 0 deletions docs/usage/steps.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,39 @@ Avoid executing code from configuration. Keep inputs data-only.
Steps may write to `State.*` only, and only to declared output paths.
This prevents hidden coupling between steps.

## Eventing

Steps may emit **structured events** for progress and audit.

The engine provides a stable, object-based contract on the execution context:

- `Context.EventSink.WriteEvent(Type, Message, StepName, Data)`

Notes:

- `Type` is a short string (for example: `Custom`, `Debug`).
- `Message` is a human-readable message.
- `StepName` should be the current step name (if available).
- `Data` is an optional hashtable for structured details.

Example:

```powershell
$Context.EventSink.WriteEvent(
'Custom',
'Ensured Department attribute.',
$Step.Name,
@{ Provider = 'Identity'; Attribute = 'Department' }
)
```

Security and portability:

- Steps must never execute code from configuration.
- Steps must not assume a specific host UI.
- Hosts can optionally stream events via `Invoke-IdlePlan -EventSink <object>`,
but **ScriptBlock sinks are not supported**.

## Error behavior

IdLE uses a fail-fast execution model in V1:
Expand Down
20 changes: 16 additions & 4 deletions docs/usage/workflows.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,22 @@ Workflows are **data-only** configuration files (PSD1) describing which steps sh

## Format

A workflow is a PowerShell hashtable stored as `.psd1`:
A workflow is a PowerShell hashtable stored as `.psd1`.

Workflow definitions are **data-only**. Do not embed executable code.

Example:

```powershell
@{
Name = 'Joiner - Standard'
LifecycleEvent = 'Joiner'

Steps = @(
@{
Name = 'Emit:Start'
Step = 'EmitEvent'
Inputs = @{ Message = 'Starting Joiner' }
Name = 'Emit start'
Type = 'IdLE.Step.EmitEvent'
With = @{ Message = 'Starting Joiner' }
}
)
}
Expand All @@ -31,6 +36,13 @@ Typical validation rules:
- condition schemas must be valid
- `*From` paths must reference allowed roots

## Step identifiers

Step types are treated as **contracts**. Prefer fully-qualified ids (module + step name),
for example: `IdLE.Step.EmitEvent`.

The host maps step types to step implementations via a step registry.

## Conditional steps

Steps can be skipped using declarative `When` conditions.
Expand Down
51 changes: 51 additions & 0 deletions examples/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
# Examples

This folder contains runnable examples for IdLE.

## Run the demo

From the repository root:

```powershell
pwsh -File .\examples\run-demo.ps1
```

The demo:

- builds a plan from a workflow (`.psd1`)
- executes the plan using mock providers
- prints step results and buffered events

## Workflow samples

Workflow samples are located in:

- `examples/workflows/`

Workflows are **data-only** PSD1 files. A minimal workflow looks like:

```powershell
@{
Name = 'Joiner - Minimal Demo'
LifecycleEvent = 'Joiner'
Steps = @(
@{
Name = 'EmitHello'
Type = 'IdLE.Step.EmitEvent'
With = @{ Message = 'Hello from workflow.' }
}
)
}
```

For details, see `docs/usage/workflows.md`.

## Events

IdLE buffers all emitted events in the execution result:

```powershell
$result.Events | Select-Object Type, StepName, Message
```

Hosts can optionally stream events live by providing `-EventSink` as an object implementing `WriteEvent(event)`.
78 changes: 78 additions & 0 deletions src/IdLE.Core/Private/New-IdleEventSink.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
function New-IdleEventSink {
[CmdletBinding()]
param(
[Parameter(Mandatory)]
[ValidateNotNullOrEmpty()]
[string] $CorrelationId,

[Parameter()]
[AllowNull()]
[string] $Actor,

[Parameter()]
[AllowNull()]
[object] $ExternalEventSink,

[Parameter()]
[AllowNull()]
[System.Collections.Generic.List[object]] $EventBuffer
)

# External sinks are host-provided extension points.
# We validate strictly to keep the engine deterministic and to avoid code execution.
if ($null -ne $ExternalEventSink) {
if ($ExternalEventSink -is [scriptblock]) {
throw [System.ArgumentException]::new(
'ExternalEventSink must not be a ScriptBlock. Provide an object with a WriteEvent(event) method.',
'ExternalEventSink'
)
}

if (-not ($ExternalEventSink.PSObject.Methods.Name -contains 'WriteEvent')) {
throw [System.ArgumentException]::new(
'ExternalEventSink must provide a WriteEvent(event) method.',
'ExternalEventSink'
)
}
}

# Capture command references once to avoid scope/name resolution issues inside script methods.
$newIdleEventCmd = Get-Command -Name 'New-IdleEvent' -CommandType Function -ErrorAction Stop
$writeIdleEventCmd = Get-Command -Name 'Write-IdleEvent' -CommandType Function -ErrorAction Stop

$sink = [pscustomobject]@{
PSTypeName = 'IdLE.EventSink'
CorrelationId = $CorrelationId
Actor = $Actor
}

# Provide a stable, object-based contract to steps.
# Steps call: $Context.EventSink.WriteEvent(Type, Message, StepName, Data)
# The engine stays in control of event shape and buffering.
$writeMethod = {
param(
[Parameter(Mandatory)]
[ValidateNotNullOrEmpty()]
[string] $Type,

[Parameter(Mandatory)]
[ValidateNotNullOrEmpty()]
[string] $Message,

[Parameter()]
[AllowNull()]
[string] $StepName,

[Parameter()]
[AllowNull()]
[hashtable] $Data
)

$evt = & $newIdleEventCmd -Type $Type -Message $Message -CorrelationId $CorrelationId -Actor $Actor -StepName $StepName -Data $Data
& $writeIdleEventCmd -Event $evt -EventSink $ExternalEventSink -EventBuffer $EventBuffer
}.GetNewClosure()

$null = Add-Member -InputObject $sink -MemberType ScriptMethod -Name 'WriteEvent' -Value $writeMethod -Force

return $sink
}
16 changes: 9 additions & 7 deletions src/IdLE.Core/Private/Write-IdleEvent.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -14,18 +14,20 @@ function Write-IdleEvent {
[System.Collections.Generic.List[object]] $EventBuffer
)

# If an event sink is provided, try to emit events immediately.
# Supported shapes:
# - ScriptBlock: invoked with the event as the only argument
# If an external event sink is provided, emit events immediately.
# Security note: we do NOT support ScriptBlock sinks to avoid arbitrary code execution.
# Supported shape:
# - Object with method "WriteEvent": called as $EventSink.WriteEvent($Event)
# - If nothing is provided: do nothing (events can still be buffered)
if ($null -ne $EventSink) {
if ($EventSink -is [scriptblock]) {
& $EventSink $Event
throw [System.ArgumentException]::new('EventSink must not be a ScriptBlock. Provide an object with a WriteEvent(event) method.', 'EventSink')
}
elseif ($EventSink.PSObject.Methods.Name -contains 'WriteEvent') {
$EventSink.WriteEvent($Event)

if (-not ($EventSink.PSObject.Methods.Name -contains 'WriteEvent')) {
throw [System.ArgumentException]::new('EventSink must provide a WriteEvent(event) method.', 'EventSink')
}

$EventSink.WriteEvent($Event)
}

# Buffer events for return value / tests if requested.
Expand Down
Loading