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
8 changes: 8 additions & 0 deletions .gitleaks.toml
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,14 @@ regexTarget = "line"
paths = ['''^tests/NodePilot\.Ai\.Tests/WorkflowSecretContentTests\.cs$''']
regexes = ['''^\s*\[InlineData\(''']

[[allowlists]]
description = "Tailwind classes for the credential object kind in the dependency icons"
targetRules = ["generic-api-key"]
condition = "AND"
regexTarget = "line"
paths = ['''^src/nodepilot-ui/src/components/dependencies/DependencyObjectIcon\.tsx$''']
regexes = ['''^\s*credential:\s*'bg-fuchsia-''']

[[allowlists]]
description = "Synthetic webhook HMAC test secrets"
targetRules = ["generic-api-key"]
Expand Down
44 changes: 35 additions & 9 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,7 @@ exhaustive.

## [Unreleased]

Planned version: **1.5.2**.

### Changed

- Desktop runtime updated to Electron 44.9.0 (embedded Node 24.22.0), with matching
manifest, lockfile and runtime contract pins.
## [1.5.2] - 2026-10-24

### Added

Expand Down Expand Up @@ -52,8 +47,9 @@ Planned version: **1.5.2**.
context menu.
- The server setup wizard was reworked: readiness page, certificate guidance and host defaults. The
release acceptance checks for the setup are reproducible scripts under `scripts/release-lab/`.
- The desktop app moves to Electron 44.7.0; minor and patch updates of the UI, documentation site and
desktop dependencies.
- The desktop app moves to Electron 44.9.3 (embedded Node 24.22.0) with matching manifest, lockfile and
runtime contract pins. Minor and patch updates of the NuGet packages, the UI, the documentation site
and the desktop dependencies; the coordinated runtime pins stay as they were.
- German website pages are no longer redirected to English by the browser language, only by a language
the visitor saved with the switch.

Expand All @@ -67,6 +63,35 @@ Planned version: **1.5.2**.
and a warning replaces the abort; the server URL is only filled in when the file parses.
- The server setup's "Next" on the readiness page refuses to continue while a fix is still ticked
instead of dropping it silently (#536).
- Step test: the test runs the step as it is in the editor, including a changed target machine, instead of
the machine it had before (#587). A step whose target machine no longer exists is refused instead of
running on the NodePilot server (#588).
- The temporary-file cleanup example keeps the originals when archiving fails (#589).
- Audit log retention no longer deletes both the database rows and their archive when the outcome of the
delete is unknown. The archive is only removed after a read confirms that every archived row is still in
the database; otherwise it is kept, and a retry may write duplicate entries, which share their audit
entry id (#590).
- Designer: opening a second tab no longer discards unsaved work in the first one (#591), "Save & Close"
in the script editor persists the script (#592), and Enter on "Export PNG" in the command palette no
longer publishes the workflow (#593).
- A `waitAny` cancellation no longer skips another join that is ready, and its downstream work (#594).
Valid graphs no longer trigger exponential preprocessing that ignores cancellation (#575).
- The copied-secret masking also covers credential headers with Windows line endings.

### Security

- **Electron 44.9.3.** The desktop runtime moves from 44.9.0 to 44.9.3, which carries the mitigation for
the compromised eMudhra intermediate certificates (GHSA-jr5w-w6x2-84p9).
- `zod` moves to 4.6.7 in the UI and the documentation site (GHSA-mrhx-c7xc-m3w5, memory exhaustion on
oversized collections). It is only a development dependency through the ESLint React hooks plugin and
is not part of the shipped bundles; the CI audit step rejects it all the same.
- `undici` moves to 8.12.3 in the UI test environment (through jsdom) and to 7.30.1 in the desktop packaging
tree (through `@electron/get`) after three advisories for versions up to 8.12.1 and 7.30.0 (unbounded
memory in the EventSource parser, an SRI bypass, an unhandled error on excessive content encodings). Neither
copy is part of the shipped bundles.
- `dompurify` in the UI moves to 3.4.17, which closes three critical advisories in 3.4.16 and earlier
(`SAFE_FOR_TEMPLATES` bypass in shadow roots, `IN_PLACE` hook leaving event handlers armed, SMIL `href`
guard skipped for XHTML).

## [1.5.1] - 2026-10-16

Expand Down Expand Up @@ -2049,7 +2074,8 @@ multi-step automation in the browser, with no agents on the targets.
- PostgreSQL or SQL Server; optional HA, LDAP / Windows SSO, ECS/SIEM logging
- Licensed under Apache-2.0

[Unreleased]: https://github.com/Sev7eNup/NodePilot/compare/v1.5.1...main
[Unreleased]: https://github.com/Sev7eNup/NodePilot/compare/v1.5.2...main
[1.5.2]: https://github.com/Sev7eNup/NodePilot/releases/tag/v1.5.2
[1.5.1]: https://github.com/Sev7eNup/NodePilot/releases/tag/v1.5.1
[1.5.0]: https://github.com/Sev7eNup/NodePilot/releases/tag/v1.5.0
[1.4.4]: https://github.com/Sev7eNup/NodePilot/releases/tag/v1.4.4
Expand Down
8 changes: 6 additions & 2 deletions docs/claude-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -111,8 +111,12 @@ Bei mehreren Fehlern entscheidet die Reihenfolge aus `StartedAt` und `Id`.

`POST /api/workflows/{id}/steps/{stepId}/test` führt eine einzelne Activity aus,
ohne eine WorkflowExecution anzulegen. `mockVariables` liefert künstliche
Step-Ergebnisse. `configOverride` ersetzt nur `data.config`, während Maschine,
Credential und Output-Variable aus dem gespeicherten Node stammen.
Step-Ergebnisse. `stepOverride` übergibt den vollständigen aktuellen `node.data`-Stand
einschließlich Activity-Typ, Konfiguration, Maschine, Credential und Output-Variable.
Die drei Testaktionen im Designer verwenden diesen konsistenten Snapshot.
Alternativ ersetzt `configOverride` ausschließlich `data.config` des gespeicherten
Nodes. Beide Overrides sind gegenseitig exklusiv und verlangen eine eigene aktive
Edit-Sperre. Ein nicht mehr vorhandenes ausgewähltes Maschinenziel wird abgelehnt.

`test-context` liefert Vorgängerwerte aus einem gewählten oder dem letzten terminalen
Lauf. Bei nicht ausgeführten Vorgängern bleibt nur das Schema. Globals kommen aus
Expand Down
11 changes: 6 additions & 5 deletions docs/testing/e2e/21-30.md
Original file line number Diff line number Diff line change
Expand Up @@ -614,15 +614,16 @@ Pflicht-Lese: CLAUDE.md "Step-Test mit Kontext".

---

### Test 28.3 — configOverride mit Live-Editor-Stand
### Test 28.3 — Vollständiger Step-Snapshot mit Live-Editor-Stand

**Setup:** runScript hat `script: "echo OLD"` in DB. Im Editor (ohne Save) auf `echo NEW` ändern.
**Setup:** runScript hat `script: "echo OLD"`, Maschine A und Credential A in DB. Im Editor auf `echo NEW`, Maschine B und Credential B ändern.

**Schritte:** Step-Test ohne Save.
**Schritte:** Vor dem Autosave den Step-Test auslösen. Für Property-Panel, Skripteditor über „Open Editor“ und Skripteditor per Doppelklick wiederholen.

**Prüfpunkte:**
- [ ] Test verwendet `echo NEW` (configOverride).
- [ ] DB-Stand bleibt `echo OLD` unverändert.
- [ ] Test verwendet `echo NEW`, Maschine B und Credential B aus demselben `stepOverride`.
- [ ] Beim Wechsel zu lokaler Ausführung werden gespeichertes Ziel und Credential nicht übernommen.
- [ ] Der Test-Request selbst ändert die gespeicherte Definition nicht; Autosave ist davon unabhängig.

---

Expand Down
2 changes: 1 addition & 1 deletion scripts/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ to make the producing trigger and its declared output contract explicit.
- `example-windows-update-health-workflow.json` — a Windows Update health check of a host (CBS/WU log tails, service/registry/WMI/file-system probes, `decision` classification).
- `example-disk-space-watch-workflow.json` — daily free-space check on C: (`scheduleTrigger` → `runScript` → `decision` on three severities → critical branch collects the ten largest folders and mails ops, `waitAny` → `returnData`). 9 activities.
- `example-service-recovery-workflow.json` — Print Spooler watchdog every 15 minutes (`serviceManagement` status → `decision` → start + `delay` + verify → recovery or escalation mail, `waitAny` → `returnData`). The two edges out of the verify step compare the service state rather than step success, because reading a stopped service is a successful step. 11 activities.
- `example-temp-file-cleanup-workflow.json` — `manualTrigger` with `path`/`olderThanDays` parameters → `folderOperation` list → `zipOperation` → `fileHash` and a `runScript` purge in parallel → `waitAll` → summary mail → `returnData`. 8 activities.
- `example-temp-file-cleanup-workflow.json` — `manualTrigger` with `path`/`olderThanDays` parameters → successful `zipOperation` → `folderOperation` list and `fileHash` in parallel → `waitAll` → `runScript` purge only when both checks succeed → summary mail after successful cleanup → `returnData`. 8 activities.
- `example-guided-file-workflow.json` — guided browser-demo task: manual inputs → registry destination → service status → PowerShell configuration file → copy → log → return data. 7 activities. The demo simulates registry, service and files; `Test` and `Production` succeed, while `Protected` fails at the copy with access denied. Machine IDs are placeholders remapped by the demo seed.

These examples are also the seed of the browser demo (`src/nodepilot-ui/demo/seed/graphs.ts`), which
Expand Down
44 changes: 24 additions & 20 deletions scripts/example-temp-file-cleanup-workflow.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,18 @@
"workflows": [
{
"name": "Temp File Cleanup",
"description": "Archives a temp folder into a ZIP, then verifies the archive with a checksum while the aged-out originals are removed. Both branches converge on a waitAll junction before the summary mail goes out.",
"description": "Archives a temp folder into a ZIP, then inventories the folder and computes the archive checksum in parallel. After both checks succeed, aged-out originals are removed and a summary mail is sent.",
"definition": {
"nodes": [
{ "id": "note_input", "type": "stickyNote", "position": { "x": 0, "y": 60 },
"style": { "width": 940, "height": 110 },
"style": { "width": 640, "height": 110 },
"data": { "label": "Note", "activityType": "note",
"text": "Phase A - Input\nThe two trigger parameters are the only thing to fill in. Both are declared as strings: the run dialog sends strings, and a number would be rejected on execute.",
"fontSize": 13, "disabled": true } },
{ "id": "note_finish", "type": "stickyNote", "position": { "x": 1040, "y": 60 },
"style": { "width": 940, "height": 110 },
{ "id": "note_finish", "type": "stickyNote", "position": { "x": 740, "y": 60 },
"style": { "width": 1480, "height": 110 },
"data": { "label": "Note", "activityType": "note",
"text": "Phase B - Verify and clean\nChecksum and delete run in parallel and converge on waitAll, so the summary mail only goes out once both are done.",
"text": "Phase B - Verify and clean\nInventory and checksum run in parallel after successful archiving. waitAll joins both checks; only successful checks allow deletion. The summary mail follows successful cleanup.",
"fontSize": 13, "disabled": true } },

{ "id": "trg", "type": "activity", "position": { "x": 0, "y": 400 },
Expand All @@ -32,7 +32,7 @@
}
} },

{ "id": "inventory", "type": "activity", "position": { "x": 400, "y": 400 },
{ "id": "inventory", "type": "activity", "position": { "x": 740, "y": 560 },
"data": {
"label": "folderOperation: list the temp folder",
"activityType": "folderOperation",
Expand All @@ -41,7 +41,7 @@
"config": { "operation": "list", "path": "{{trg.param.path}}" }
} },

{ "id": "archive", "type": "activity", "position": { "x": 700, "y": 400 },
{ "id": "archive", "type": "activity", "position": { "x": 400, "y": 400 },
"data": {
"label": "zipOperation: archive the folder",
"activityType": "zipOperation",
Expand All @@ -56,7 +56,7 @@
}
} },

{ "id": "checksum", "type": "activity", "position": { "x": 1040, "y": 240 },
{ "id": "checksum", "type": "activity", "position": { "x": 740, "y": 240 },
"data": {
"label": "fileHash: checksum the archive",
"activityType": "fileHash",
Expand All @@ -65,7 +65,7 @@
"config": { "path": "C:\\Archive\\temp-cleanup.zip", "algorithm": "SHA256" }
} },

{ "id": "purge", "type": "activity", "position": { "x": 1040, "y": 560 },
{ "id": "purge", "type": "activity", "position": { "x": 1380, "y": 400 },
"data": {
"label": "runScript: remove the archived files",
"activityType": "runScript",
Expand All @@ -77,7 +77,7 @@
}
} },

{ "id": "gather", "type": "activity", "position": { "x": 1380, "y": 400 },
{ "id": "gather", "type": "activity", "position": { "x": 1080, "y": 400 },
"data": {
"label": "Junction: waitAll (2)",
"activityType": "junction",
Expand Down Expand Up @@ -109,23 +109,27 @@
} }
],
"edges": [
{ "id": "e_trg_inventory", "source": "trg", "target": "inventory", "type": "labeled",
{ "id": "e_trg_archive", "source": "trg", "target": "archive", "type": "labeled",
"data": { "label": "Always" } },
{ "id": "e_inventory_archive", "source": "inventory", "target": "archive", "type": "labeled",
"data": { "label": "On Success", "condition": "inventory.success" } },

{ "id": "e_archive_checksum", "source": "archive", "target": "checksum", "type": "labeled",
"data": { "label": "Always" } },
{ "id": "e_archive_purge", "source": "archive", "target": "purge", "type": "labeled",
"data": { "label": "Always" } },
"data": { "label": "On Success", "condition": "archive.success" } },
{ "id": "e_archive_inventory", "source": "archive", "target": "inventory", "type": "labeled",
"data": { "label": "On Success", "condition": "archive.success" } },

{ "id": "e_checksum_gather", "source": "checksum", "target": "gather", "type": "labeled",
"data": { "label": "Always" } },
{ "id": "e_purge_gather", "source": "purge", "target": "gather", "type": "labeled",
{ "id": "e_inventory_gather", "source": "inventory", "target": "gather", "type": "labeled",
"data": { "label": "Always" } },

{ "id": "e_gather_mail", "source": "gather", "target": "mail", "type": "labeled",
"data": { "label": "Always" } },
{ "id": "e_gather_purge", "source": "gather", "target": "purge", "type": "labeled",
"data": { "label": "Checks succeeded", "conditionExpression": {
"type": "group", "op": "AND", "children": [
{ "type": "comparison", "left": { "kind": "variable", "stepId": "checksum", "field": "success" }, "op": "isTrue" },
{ "type": "comparison", "left": { "kind": "variable", "stepId": "inventory", "field": "success" }, "op": "isTrue" }
]
} } },
{ "id": "e_purge_mail", "source": "purge", "target": "mail", "type": "labeled",
"data": { "label": "On Success", "condition": "purge.success" } },
{ "id": "e_mail_ret", "source": "mail", "target": "ret", "type": "labeled",
"data": { "label": "Always" } }
]
Expand Down
10 changes: 7 additions & 3 deletions src/NodePilot.Api/Controllers/WorkflowEditingController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -610,7 +610,7 @@
/// Useful for testing individual steps during development. Admin/Operator only.
///
/// <para>Body fields: <c>MockVariables</c> for upstream variable injection,
/// <c>ConfigOverride</c> for testing unsaved editor state without going through PUT first.
/// <c>ConfigOverride</c> for config-only changes or <c>StepOverride</c> for a complete node-data snapshot.
/// Testing the persisted config requires Run; an override additionally requires Edit and
/// the caller's active edit-lock because it can replace executable activity config.</para>
/// </summary>
Expand All @@ -628,7 +628,10 @@
if (await RequireWorkflowAccessAsync(workflow, NodePilot.Core.Interfaces.ResourceOp.Run, ct) is { } d8) return d8;

var configOverride = request?.ConfigOverride;
if (configOverride.HasValue)
var stepOverride = request?.StepOverride;
if (configOverride.HasValue && stepOverride.HasValue)
return BadRequest(new { message = "Config override and step override cannot be combined" });
if (configOverride.HasValue || stepOverride.HasValue)
{
// An override is unsaved executable editor state. Without the Edit + lock gates a
// FolderOperator could replace a persisted runScript/HTTP/SQL config and execute it
Expand All @@ -644,7 +647,8 @@
StepTestAuthorizationSnapshot.Capture(workflow),
request?.MockVariables,
configOverride,
ct);
ct,
stepOverride);
await _audit.LogAsync(AuditActions.WorkflowStepTested, "Workflow", workflow.Id,
AuditDetails.Json(("stepId", stepId), ("success", result.Success), ("durationMs", result.DurationMs)),
ct);
Expand Down
17 changes: 7 additions & 10 deletions src/NodePilot.Api/Dtos/StepTestDtos.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,19 +9,16 @@ namespace NodePilot.Api.Dtos;
/// <c>"checkDisk.output": "7"</c>, <c>"checkDisk.param.freeGb": "7"</c>). Populates
/// upstream <c>{{step.output}}</c> / <c>{{step.param.x}}</c> references.</para>
///
/// <para><b>ConfigOverride</b>: optional unsaved config JSON for the step under test. When
/// set, this replaces the persisted node config — the test then reflects what the user is
/// editing right now, not the last-saved DB state. Pass null to fall back to the persisted
/// definition. Only the <c>data.config</c> sub-tree is overridden; targetMachine, credential,
/// and outputVariable still come from the persisted node so the user doesn't have to re-enter
/// them on every test click. Because an override can replace executable script/HTTP/SQL
/// config while retaining stored targets and credentials, it requires Folder Edit permission
/// and the caller's active workflow edit-lock. A request without an override only requires
/// Run permission.</para>
/// <para><b>ConfigOverride</b> replaces only <c>data.config</c>, retaining persisted node metadata.
/// <b>StepOverride</b> replaces the complete <c>node.data</c> object, including activity type,
/// config, target and credential. Omitted target/credential fields clear their saved values.
/// The overrides are mutually exclusive. Either requires Folder Edit permission and the
/// caller's active edit-lock; testing the persisted step requires Run permission.</para>
/// </summary>
public record StepTestRequest(
Dictionary<string, string>? MockVariables = null,
JsonElement? ConfigOverride = null);
JsonElement? ConfigOverride = null,
JsonElement? StepOverride = null);

/// <summary>
/// Variable name + sample value the UI shows in the mock-editor when the user picks
Expand Down
3 changes: 2 additions & 1 deletion src/NodePilot.Cli/Api/Dtos/NewSurfaceDtos.cs
Original file line number Diff line number Diff line change
Expand Up @@ -184,7 +184,8 @@ public sealed record LlmTestProbeRequest(string? ProfileId, JsonElement Settings

public sealed record StepTestRequest(
Dictionary<string, string>? MockVariables = null,
JsonElement? ConfigOverride = null);
JsonElement? ConfigOverride = null,
JsonElement? StepOverride = null);

public sealed record StepTestResponse(
bool Success,
Expand Down
Loading
Loading