From 03f9b561844a7592911dbf358e0c5e8efe32c012 Mon Sep 17 00:00:00 2001 From: Robert Backhaus Date: Sun, 13 Sep 2026 12:18:40 +0200 Subject: [PATCH 1/3] feat: integrate desktop control into MCP runtime for 2.19 --- CHANGELOG.md | 12 + README.de.md | 2 +- README.md | 2 +- .../KI-Stack-Betriebs-und-Benutzerhandbuch.md | 4 +- docs/de/KI-Stack-Installationsanleitung.md | 8 +- .../KI-Stack-Manuelle-Modellbereitstellung.md | 2 +- docs/de/KI-Stack-Modell-Downloadanleitung.md | 2 +- docs/de/KI-Stack-Technische-Dokumentation.md | 14 +- docs/en/KI-Stack-Installation-Guide.md | 8 +- docs/en/KI-Stack-Manual-Model-Provisioning.md | 2 +- docs/en/KI-Stack-Model-Download-Guide.md | 2 +- docs/en/KI-Stack-Operations-and-User-Guide.md | 4 +- docs/en/KI-Stack-Technical-Documentation.md | 14 +- scripts/Test-Repository.ps1 | 4 +- .../current/CompleteInstaller.psm1 | 147 +++++-- .../Config/complete-installer.config.json | 2 +- .../current/Contracts/COMPONENTS.json | 6 +- .../current/Documentation/INSTALLATION.de.md | 2 +- .../current/Documentation/INSTALLATION.md | 2 +- .../complete-installer/current/MANIFEST.json | 6 +- tools/complete-installer/current/README.de.md | 8 +- tools/complete-installer/current/README.md | 8 +- .../complete-installer/current/SHA256SUMS.txt | 225 +++++------ .../current/Test-KIStackCompleteInstaller.ps1 | 4 +- .../Test-KIStackMcpRuntimePayloadParity.ps1 | 148 +++++++ ...est-KIStackMcpRuntimeSeededPrimaryFlow.ps1 | 15 +- tools/complete-installer/current/VERSION | 2 +- .../Validation/REGRESSION-COVERAGE.json | 7 +- .../Validation/VALIDATION-CONTRACT.json | 2 +- .../Config/desktop-control.config.json | 2 +- .../current/DesktopControl.psm1 | 4 +- tools/desktop-control/current/MANIFEST.json | 4 +- .../current/MCP-INTEGRATION.md | 171 ++++++-- tools/desktop-control/current/README.md | 12 +- tools/desktop-control/current/SHA256SUMS.txt | 22 +- .../current/Test-KIStackDesktopControl.ps1 | 16 +- tools/desktop-control/current/VERSION | 2 +- .../current/Config/mcp-runtime.config.json | 2 +- .../current/Invoke-KIStackMcpRuntime.ps1 | 13 +- tools/mcp-runtime/current/MANIFEST.json | 2 +- tools/mcp-runtime/current/McpRuntime.psm1 | 280 ++++++++++++- tools/mcp-runtime/current/README.md | 25 +- tools/mcp-runtime/current/SHA256SUMS.txt | 19 +- .../Scripts/ki_desktop_control_tools.py | 370 ++++++++++++++++++ .../current/Scripts/mcp_launcher.py | 30 +- .../Scripts/test_ki_desktop_control_tools.py | 283 ++++++++++++++ .../current/Test-KIStackMcpRuntime.ps1 | 5 + .../current/Test-KIStackMcpRuntimeInstall.ps1 | 212 ++++++++++ tools/mcp-runtime/current/VERSION | 2 +- 49 files changed, 1829 insertions(+), 311 deletions(-) create mode 100644 tools/complete-installer/current/Test-KIStackMcpRuntimePayloadParity.ps1 create mode 100644 tools/mcp-runtime/current/Scripts/ki_desktop_control_tools.py create mode 100644 tools/mcp-runtime/current/Scripts/test_ki_desktop_control_tools.py create mode 100644 tools/mcp-runtime/current/Test-KIStackMcpRuntimeInstall.ps1 diff --git a/CHANGELOG.md b/CHANGELOG.md index f8f3a86..89e0cb9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,17 @@ # Changelog +## 2.19.0 + +- Exposes Desktop Control's UIA operations as ten flat, natively-typed MCP tools (`ui_list_windows`, `ui_inspect_window`, `ui_find_element`, `ui_get_properties`, `ui_get_value`, `ui_screenshot`, `ui_wait_for`, `ui_set_value`, `ui_invoke`, `ui_focus`), registered as native FastMCP tools on the SAME MCP Runtime instance and port (`server:mcp:ki-stack-mcp-runtime`, `127.0.0.1:8021`) Open Terminal's own tools already run on -- no new MCP server, port, or credential. `scroll`/`scroll_into_view`/`send_input`/`send_keys`/raw-`winapp` remain structurally absent, not merely policy-blocked. +- Each `ui_*` tool is a thin transport straight to Desktop Control's own `Invoke-KIStackDesktopControl.ps1`, resolved only at `\tools\desktop-control\current\...` with no PATH fallback; every policy decision (Resolve -> Validate -> Act -> Re-observe -> Verify, the secret-context guard, the Target Contract) stays inside Desktop Control unchanged. A business-level result (`SecretContextBlocked`, `PostconditionNotProven`, `ResolverError`, ...) passes through unmodified; only a transport failure (dispatcher missing, invalid JSON) raises a structured MCP tool error. +- Fixes a real, previously undetected delivery gap: mcp-runtime never materialized a persistent, source-parity-checked package tree on a target the way winapp/desktop-control already do. Its generated `Start`/`Stop` scripts hard-coded a path into a transaction-scoped payload staging directory (`state\complete-installer\transactions\\payload\McpRuntime\...`), and neither `Test-KIMcpRuntime` nor `Test-KICompleteMcpRuntimeCompliant` ever compared deployed content against source -- so a changed payload at an unchanged component VERSION was silently reported `SkippedAlreadyCompliant` instead of being reconciled. +- mcp-runtime now deploys a persistent package tree at `\tools\mcp-runtime\current\` (`Get-KIMcpRuntimeInstallPaths`/`Test-KIMcpRuntimeDeployed`/`Test-KIMcpRuntimeSourceParity` in `McpRuntime.psm1`, mirroring `DesktopControl.psm1`'s own functions); `Install-KIMcpRuntime` accepts an externally-owned `-BackupRoot` (the same 2.18.1 Desktop-Control BackupRoot-respect fix applied here); generated starter/stopper scripts reference the persistent package root, never the transaction staging path. +- `Test-KICompleteMcpRuntimeCompliant` gains an `-InstallerPackageRoot` parameter and a new `Test-KICompleteMcpRuntimePayloadParity` function (mirroring `Test-KICompleteDesktopControlPayloadParity`), wired into both `New-KICompletePlan` and the resume-recheck block; the Complete Installer's own `mcp-runtime` step now passes its own `TransactionBackupRoot` slice and runs Install/Upgrade/Repair+Validate via a fresh, isolated `pwsh` process, matching desktop-control's own hardening. +- Corrects `Contracts/COMPONENTS.json`'s dependency order to `python-git -> winapp -> desktop-control -> mcp-runtime` (mcp-runtime now `requires` desktop-control, transitively winapp): its own `ui_*` tools resolve Desktop Control's dispatcher path unconditionally, so a target that provisioned mcp-runtime before Desktop Control would have had that path missing at every `ui_*` call. +- Fixes a real, reproduced `FastMCPDeprecationWarning`: `mcp_launcher.py`'s `OpenAPIProvider` client construction used a plain `httpx.AsyncClient` (aliased as `httpx2`), which fastmcp `4.0.3` (the version actually resolved by the pinned `open-terminal[mcp]==0.11.34`, reproducibly verified) flags as deprecated in favor of a real `httpx2.AsyncClient` -- `httpx2` is a genuine, separate package already resolved transitively by the same pin; no new dependency, no architecture change. +- Bumps mcp-runtime from 0.1.0 to 0.2.0 and Desktop Control from 0.1.0 to 0.1.1 so an existing target actually receives these fixes through a normal Upgrade/Repair run instead of being planned as Skip on unchanged component versions. Complete Installer advances from 2.18.2 to 2.19.0. WinApp stays at 0.6.1. +- Adds dedicated regression coverage: `Test-KIStackMcpRuntimeInstall.ps1` (fresh install deploys the persistent tree; same-version/same-payload Skip; same-version/changed-payload reconciles -- the actual bug fix; missing/extra deployed file reconciles; state never treated as payload; external `-BackupRoot` respected; rollback restores the persistent tree; a failed fresh install leaves no orphaned tree; source/config VERSION mismatch fails closed; Uninstall removes the persistent tree) and `Test-KIStackMcpRuntimePayloadParity.ps1` (the real `Test-KICompleteMcpRuntimePayloadParity`, never a copy, against real `Payload/McpRuntime/*.zip` archives: identical payload/deploy compliant, a changed productive file / a missing deployed file / an extra deployed file / a VERSION mismatch each non-compliant, including the full version-gate contract). `test_ki_desktop_control_tools.py` covers the `ui_*` tool layer itself (26 tests) against the real resolved `open-terminal[mcp]==0.11.34` environment. + ## 2.18.2 - Fixes a real, reproduced defect where the central `Start-KIStack.cmd`/`Stop-KIStack.cmd` deployed onto every target called the old cutover core (`modules\cutover\*-KIStack.cmd`) directly instead of `Invoke-KIStackCompleteInstaller.ps1 -Mode Start`/`-Mode Stop`; as a result MCP Runtime and Open Terminal were never started or stopped by the central starters, and the MCP health gate ahead of Open WebUI never applied there. diff --git a/README.de.md b/README.de.md index ec02a7b..6a62d6e 100644 --- a/README.de.md +++ b/README.de.md @@ -29,7 +29,7 @@ Projektseite und begleitende Artikel: [okami.de – Lokaler KI-Stack](https://ww | Open Terminal | 0.1.1 | Stabile Komponente; lokaler Tool-/Terminal-Backend-Dienst für OpenWebUI (Filesystem, PowerShell, WSL, Git, Prozess-/Command-Ausführung) unter `http://127.0.0.1:8000`, kein Docker; gestartet über den bestehenden, bereits verwalteten KI-Stack-Python/uv-Vertrag (deterministische Auflösung des verwalteten Pfads, nie ein blindes PATH-Lookup); authentifiziert über einen einzigen persistenten, DPAPI-geschützten lokalen API-Key (nie im Repository, nie geloggt, über Neustarts hinweg unverändert wiederverwendet); Install/Upgrade/Repair/Skip über den Complete Installer, Start/Stop/Status über dieselben zentralen KI-Stack-Lifecycle-Kommandos wie jede andere Komponente; real zielsystemvalidiert, einschließlich eines echten Complete-Installer-Laufs, der es beim zweiten Durchlauf korrekt als `SkippedAlreadyCompliant` meldete. Die Anbindung an OpenWebUI selbst erfordert weiterhin eine einmalige manuelle Tool-Server-Registrierung (siehe „Open Terminal" unten) | | WinApp | 0.6.1 | Zentral verwaltete Windows-UI-Automation-Basis für Desktop Control; lokaler Komponentenvertrag ohne eigenen Dienst, Port oder Credential | | Desktop Control | 0.1.0 | Kontrollierte Windows-UIA-Schicht auf WinApp mit Resolve -> Validate -> Act -> Re-observe -> Verify, Policy-/Secret-/Evidence-Prüfung und unabhängig verifizierten Postconditions; MCP-Anbindung in 2.18 noch nicht aktiviert | -| Complete Installer | 2.18.2 | Aktuell veröffentlichtes GitHub-Release `v2.18.2`. Enthält die MCP Foundation aus 2.15, autonomes Local Control aus 2.16, natives persistentes OpenWebUI-Memory aus 2.17 und die Desktop-Control-/WinApp-Basis aus 2.18; außerdem Component Isolation, interne Komponentenversions-Registry, automatische Release Attestation, sicheren OpenWebUI-Credential-Bootstrap, Codex Local `0.2.1`, RAG `0.4.0`, unterstützten Open-Terminal-Fallback, deterministische Builds, PackageSelfTest und die bis zu diesem Release validierten Installer-/Reconciliation-Härtungen. | +| Complete Installer | 2.19.0 | Aktuell veröffentlichtes GitHub-Release `v2.19.0`. Enthält die MCP Foundation aus 2.15, autonomes Local Control aus 2.16, natives persistentes OpenWebUI-Memory aus 2.17 und die Desktop-Control-/WinApp-Basis aus 2.18; außerdem Component Isolation, interne Komponentenversions-Registry, automatische Release Attestation, sicheren OpenWebUI-Credential-Bootstrap, Codex Local `0.2.1`, RAG `0.4.0`, unterstützten Open-Terminal-Fallback, deterministische Builds, PackageSelfTest und die bis zu diesem Release validierten Installer-/Reconciliation-Härtungen. | | System Cleanup Audit | 1.0.0 | Audit abgeschlossen; Bereinigungsplan wartet auf ausdrückliche Freigabe | Vollständige Paketquellen liegen im Verzeichnis `package`. Fertige ZIP-Pakete werden als GitHub-Release-Artefakte veröffentlicht und nicht dauerhaft in die normale Git-Historie aufgenommen. diff --git a/README.md b/README.md index f954bef..3607437 100644 --- a/README.md +++ b/README.md @@ -29,7 +29,7 @@ Project page and accompanying articles: [okami.de – Lokaler KI-Stack](https:// | Open Terminal | 0.1.1 | Stable component; local tool/terminal backend service for OpenWebUI (filesystem, PowerShell, WSL, Git, process/command execution) at `http://127.0.0.1:8000`, no Docker; started through the existing, already-managed KI-Stack Python/uv contract (deterministic managed-path resolution, never a bare PATH lookup); authenticated with a single persistent, DPAPI-protected local API key (never in the repository, never logged, reused unchanged across restarts); Install/Upgrade/Repair/Skip via the Complete Installer, Start/Stop/Status via the same central KI-Stack lifecycle commands as every other component; real-target validated, including a real Complete Installer run that left it `SkippedAlreadyCompliant` on a second pass. Connecting it to OpenWebUI itself still requires one manual, one-time tool-server registration (see "Open Terminal" below) | | WinApp | 0.6.1 | Centrally managed Windows UI Automation base for Desktop Control; local component contract with no runtime service, port, or credential of its own | | Desktop Control | 0.1.0 | Controlled Windows UIA layer on top of WinApp with Resolve -> Validate -> Act -> Re-observe -> Verify, policy/secret/evidence checks, and independently verified postconditions; MCP wiring is not yet activated in 2.18 | -| Complete Installer | 2.18.2 | Current published GitHub Release `v2.18.2`. Includes the 2.15 MCP Foundation, 2.16 autonomous Local Control, 2.17 native persistent Open WebUI Memory, and the 2.18 Desktop Control / WinApp foundation; also carries Component Isolation, the internal component version registry, automatic Release Attestation, secure OpenWebUI credential bootstrap, Codex Local `0.2.1`, RAG `0.4.0`, Open Terminal fallback support, deterministic builds, PackageSelfTest, and the validated installer/reconciliation hardening accumulated through this release. | +| Complete Installer | 2.19.0 | Current published GitHub Release `v2.19.0`. Includes the 2.15 MCP Foundation, 2.16 autonomous Local Control, 2.17 native persistent Open WebUI Memory, and the 2.18 Desktop Control / WinApp foundation; also carries Component Isolation, the internal component version registry, automatic Release Attestation, secure OpenWebUI credential bootstrap, Codex Local `0.2.1`, RAG `0.4.0`, Open Terminal fallback support, deterministic builds, PackageSelfTest, and the validated installer/reconciliation hardening accumulated through this release. | | System Cleanup Audit | 1.0.0 | Audit completed; cleanup plan pending explicit approval | The repository tracks complete package sources. Built ZIP files are published as GitHub Release assets rather than committed to normal Git history. diff --git a/docs/de/KI-Stack-Betriebs-und-Benutzerhandbuch.md b/docs/de/KI-Stack-Betriebs-und-Benutzerhandbuch.md index fc69269..d7f4d51 100644 --- a/docs/de/KI-Stack-Betriebs-und-Benutzerhandbuch.md +++ b/docs/de/KI-Stack-Betriebs-und-Benutzerhandbuch.md @@ -1,4 +1,4 @@ -# KI-Stack 2.18.2 – Betriebs- und Benutzerhandbuch +# KI-Stack 2.19.0 – Betriebs- und Benutzerhandbuch ## Normalbetrieb @@ -228,7 +228,7 @@ Eine erstmalige WSL2-Aktivierung auf einer wirklich leeren Maschine kann einen W - **SearXNG scheint nicht erreichbar**: in der WSL-Debian-Instanz `systemctl status ki-stack-searxng uwsgi nginx valkey-server` prüfen; dass entweder `ki-stack-searxng` oder `uwsgi` aktiv und auf Port 8888 gesund ist, ist ein gültiger, erwarteter Zustand. - **Ein OpenWebUI-API-abhängiger Schritt meldet einen Credential-bezogenen Pending-/Blocked-Zustand**: `Test-KIStackOpenWebUICredential.ps1` ausführen. Existiert kein gültiges Credential, dieses mit `Initialize-KIStackOpenWebUICredential.ps1` bootstrappen; nicht auf einen separat gepflegten temporären API-Key zurückfallen. -Die letzte vollständige, erfolgreiche, reale Greenfield-Installation auf einem leeren Zielsystem wurde mit Complete Installer 2.4.0 verifiziert. Die späteren Releases bis 2.18.2 ergänzen Regression-, Paket-, Komponenten-, Upgrade-/Reconcile- und Real-Target-Nachweise, behaupten jedoch keinen neueren vollständigen Windows-Greenfield-Lauf auf einem leeren Zielsystem. +Die letzte vollständige, erfolgreiche, reale Greenfield-Installation auf einem leeren Zielsystem wurde mit Complete Installer 2.4.0 verifiziert. Die späteren Releases bis 2.19.0 ergänzen Regression-, Paket-, Komponenten-, Upgrade-/Reconcile- und Real-Target-Nachweise, behaupten jedoch keinen neueren vollständigen Windows-Greenfield-Lauf auf einem leeren Zielsystem. ## Bekannte offene Punkte diff --git a/docs/de/KI-Stack-Installationsanleitung.md b/docs/de/KI-Stack-Installationsanleitung.md index c11f7c0..283dbf3 100644 --- a/docs/de/KI-Stack-Installationsanleitung.md +++ b/docs/de/KI-Stack-Installationsanleitung.md @@ -1,13 +1,13 @@ -# KI-Stack Complete Installer 2.18.2 – Installation und Upgrade +# KI-Stack Complete Installer 2.19.0 – Installation und Upgrade -Diese Anleitung gilt für das aktuell veröffentlichte Paket `KI-Stack-Complete-Installer-v2.18.2.zip`. Die letzte vollständige, erfolgreiche, physische Greenfield-Installation auf einem leeren Zielsystem wurde mit Version 2.4.0 durchgeführt und verifiziert. Die späteren Releases bis einschließlich 2.18.2 wurden durch Repository-Regressionstests, deterministische Paket-Builds, komponentenspezifische Acceptance-Tests und reale Upgrade-/Reconciliation-Läufe validiert, wie für die jeweiligen Releases dokumentiert; ein neuerer vollständiger Windows-Greenfield-Lauf auf einem leeren Zielsystem wird hier ausdrücklich nicht behauptet. +Diese Anleitung gilt für das aktuell veröffentlichte Paket `KI-Stack-Complete-Installer-v2.19.0.zip`. Die letzte vollständige, erfolgreiche, physische Greenfield-Installation auf einem leeren Zielsystem wurde mit Version 2.4.0 durchgeführt und verifiziert. Die späteren Releases bis einschließlich 2.19.0 wurden durch Repository-Regressionstests, deterministische Paket-Builds, komponentenspezifische Acceptance-Tests und reale Upgrade-/Reconciliation-Läufe validiert, wie für die jeweiligen Releases dokumentiert; ein neuerer vollständiger Windows-Greenfield-Lauf auf einem leeren Zielsystem wird hier ausdrücklich nicht behauptet. ## Download und SHA-256 -Lade ZIP und `KI-Stack-Complete-Installer-v2.18.2.zip.sha256` aus demselben GitHub-Release. Der verbindliche Hash steht ausschließlich im Sidecar und in der GitHub-Releasebeschreibung. +Lade ZIP und `KI-Stack-Complete-Installer-v2.19.0.zip.sha256` aus demselben GitHub-Release. Der verbindliche Hash steht ausschließlich im Sidecar und in der GitHub-Releasebeschreibung. ```powershell -$zip = '.\KI-Stack-Complete-Installer-v2.18.2.zip' +$zip = '.\KI-Stack-Complete-Installer-v2.19.0.zip' $expected = ((Get-Content "$zip.sha256" -Raw) -split '\s+')[0].ToLowerInvariant() $actual = (Get-FileHash -LiteralPath $zip -Algorithm SHA256).Hash.ToLowerInvariant() if ($actual -ne $expected) { throw 'SHA-256 stimmt nicht überein.' } diff --git a/docs/de/KI-Stack-Manuelle-Modellbereitstellung.md b/docs/de/KI-Stack-Manuelle-Modellbereitstellung.md index c9b8732..a04856c 100644 --- a/docs/de/KI-Stack-Manuelle-Modellbereitstellung.md +++ b/docs/de/KI-Stack-Manuelle-Modellbereitstellung.md @@ -1,6 +1,6 @@ # Optionale Modellbereitstellung und Preload -Complete Installer 2.18.2 lädt fehlende Modelle einschließlich des ausschließlich für Embeddings verwendeten Nomic Q4_K_M automatisch aus den revisionsgebundenen Quellen des zentralen Modellmanifests. Eine manuelle Bereitstellung ist keine Installationsvoraussetzung. +Complete Installer 2.19.0 lädt fehlende Modelle einschließlich des ausschließlich für Embeddings verwendeten Nomic Q4_K_M automatisch aus den revisionsgebundenen Quellen des zentralen Modellmanifests. Eine manuelle Bereitstellung ist keine Installationsvoraussetzung. Ein optionaler Cache oder `ExternalModels`-Preload kann Bandbreite sparen. Der Installer akzeptiert eine Datei ausschließlich nach Prüfung von Dateiname, exakter Größe und vollständigem SHA-256. Eine gültige Zieldatei wird wiederverwendet und nicht erneut geladen. Unterbrochene Downloads bleiben fortsetzbar; eine falsche Größe oder Prüfsumme führt zu `Failed`. diff --git a/docs/de/KI-Stack-Modell-Downloadanleitung.md b/docs/de/KI-Stack-Modell-Downloadanleitung.md index 085f1b1..345b630 100644 --- a/docs/de/KI-Stack-Modell-Downloadanleitung.md +++ b/docs/de/KI-Stack-Modell-Downloadanleitung.md @@ -1,6 +1,6 @@ # Automatischer Modell-Downloadvertrag -Dieser Vertrag gilt für das aktuell veröffentlichte Complete-Installer-Release 2.18.2. +Dieser Vertrag gilt für das aktuell veröffentlichte Complete-Installer-Release 2.19.0. Der Complete Installer benötigt auf einem leeren Zielsystem keine manuell bereitgestellten Modell- oder Payloaddateien. Die neun Visualartefakte für Z-Image Turbo und WAN2.2 T2V 14B mit beiden LightX2V-4-Step-LoRAs, die beiden Heretic-Dateien und das ausschließlich für Embeddings verwendete `nomic-embed-text-v1.5.Q4_K_M.gguf` besitzen revisionsgebundene Downloadquellen, exakte Bytegrößen und SHA-256-Werte in `tools/models-workflows/current/Manifests/models.manifest.json`. diff --git a/docs/de/KI-Stack-Technische-Dokumentation.md b/docs/de/KI-Stack-Technische-Dokumentation.md index 11b6bbc..3cd7ea8 100644 --- a/docs/de/KI-Stack-Technische-Dokumentation.md +++ b/docs/de/KI-Stack-Technische-Dokumentation.md @@ -1,6 +1,6 @@ -# KI-Stack 2.18.2 – Technische Dokumentation +# KI-Stack 2.19.0 – Technische Dokumentation -KI-Stack ist ein transaktionsgesicherter lokaler Windows-KI-Stack. Complete Installer `2.18.2` ist das aktuell veröffentlichte GitHub-Release. +KI-Stack ist ein transaktionsgesicherter lokaler Windows-KI-Stack. Complete Installer `2.19.0` ist das aktuell veröffentlichte GitHub-Release. Der Validierungsstand muss nach Umfang getrennt betrachtet werden: Die letzte vollständige physische Greenfield-Installation auf einem leeren Windows-Zielsystem wurde mit 2.4.0 durchgeführt und verifiziert; Complete Installer 2.10.0 bleibt der dokumentierte Referenzlauf für Gesamt-Regression plus reales Zielsystem. Spätere Releases ergänzten weitere reale Zielsystem-, Komponenten-, Upgrade-/Reconcile-, Security- und Paketvalidierungen, ohne damit einen neueren vollständigen Windows-Greenfield-Lauf auf einem leeren Zielsystem zu behaupten. @@ -19,16 +19,16 @@ Die aktuelle 2.18-Architektur umfasst den mit 2.15 eingeführten MCP Runtime als | Cutover Runtime | 1.6.16 | | Codex Local | 0.2.1 | | RAG | 0.4.0 | -| MCP Runtime | 0.1.0 | +| MCP Runtime | 0.2.0 | | Open Terminal | 0.1.1 | | WinApp | 0.6.1 | -| Desktop Control | 0.1.0 | +| Desktop Control | 0.1.1 | | Production Recovery | 1.7.0-r7 | | Validation Gate | 1.0.3 | | Target Acceptance | 1.0.10 | | OpenWebUI Visual Pack | 2.0.5 | | OpenWebUI Agent Pack | 1.9.0 | -| Complete Installer | 2.18.2 | +| Complete Installer | 2.19.0 | Referenz- und Mindestversion von ComfyUI für reproduzierbare Neuinstallationen und Reconcile ist `v0.34.0`; eine bestehende, unterstützte neuere Installation bleibt erhalten und wird nie automatisch zurückgestuft. `ReferenceVersion` und `MinimumSupportedVersion` von Open WebUI sind beide `0.11.3` -- jede installierte Version ab `0.11.3` wird unterstützt, und eine bestehende, unterstützte neuere Installation bleibt ebenso erhalten, nie automatisch auf exakt die Referenz zurückgestuft. @@ -127,6 +127,8 @@ Die Validierungsnachweise werden bewusst danach getrennt, was tatsächlich ausge - **2.18.1**: Hotfix. Der zentrale Desktop-Control-Reconcile-Schritt des Complete Installers lief bislang im selben, langlebigen Orchestrator-Prozess wie jede andere Komponente; auf dem realen Zielsystem scheiterte dabei die unmittelbar auf Install folgende Validate-Phase, obwohl beide Aktionen einzeln, jeweils in einem frischen Prozess, real erfolgreich waren. Install/Upgrade/Repair und die anschließende Validate laufen jetzt jeweils in einem frischen `pwsh`-Prozess; Desktop Control erhält zusätzlich einen optionalen, transaktionsgebundenen Backup-Root, und ein bereits vollständig zurückgerollter Failed-Step blockiert einen späteren Lauf nicht mehr über seinen eigenen, dann irrelevanten Backup-Pfad. Live gegen das reale betroffene Zielsystem verifiziert (echtes `winapp.exe`); kein neuer Funktionsumfang, keine neue Greenfield-Behauptung. Behebt außerdem einen real reproduzierten Defekt, bei dem OpenWebUIs eigener `Path.cwd()`-Fallback für `WEBUI_SECRET_KEY` das jeweils aktuelle Arbeitsverzeichnis der äußersten Starter-Kette übernahm (z. B. `C:\Windows\System32` bei einer wie üblich geöffneten erhöhten Shell) und dort ohne Admin-Rechte am Persistieren des Schlüssels scheiterte; der generierte OpenWebUI-Starter setzt `WEBUI_SECRET_KEY` jetzt selbst aus einem einmalig erzeugten, kryptografisch zufälligen, persistenten Schlüssel unter `\state\openwebui\.webui_secret_key` und migriert einen bereits bestehenden, legitimen Schlüssel unter `\.webui_secret_key`, damit bestehende Sessions nicht ungültig werden. Applications steigt von 1.4.11 auf 1.4.12, damit ein bestehendes Zielsystem diesen Fix im generierten Starter-Inhalt über einen regulären Upgrade-/Repair-Lauf tatsächlich erhält, statt bei unveränderter Komponentenversion als Skip geplant zu werden. - **2.18.2**: Hotfix. Der zentrale, auf jedem Zielsystem deployte `Start-KIStack.cmd`/`Stop-KIStack.cmd` rief bislang direkt den alten Cutover-Kern (`modules\cutover\*-KIStack.cmd`) auf und erreichte damit nie `-Mode Start`/`-Mode Stop` (`Invoke-KICompleteLifecycle`) -- MCP Runtime und Open Terminal wurden beim zentralen Start/Stop dadurch nie gestartet bzw. gestoppt, und das MCP-Health-Gate vor Open WebUI griff nicht. Die deployten Lifecycle-Vorlagen rufen jetzt `installer\complete\Invoke-KIStackCompleteInstaller.ps1 -Mode Start`/`-Mode Stop` auf, die intern weiterhin denselben Cutover-Kern nutzen, jetzt aber korrekt um MCP Runtime und Open Terminal ergänzt; die bestehende Stop-Bereinigung verwaister Prozesse/WSL/Registry-Einträge bleibt unverändert erhalten. Behebt außerdem einen real reproduzierten Open-Terminal-Defekt, bei dem die getrackte PID die des `uv`/`uvx`-Launcher-Prozesses statt des echten, langlebigen Listener-Prozesses sein konnte, an den übergeben wird (der Launcher beendet sich selbst, oft mit Exitcode 0, sobald das Tool übernimmt); Status/Stop verifizieren jetzt die Prozessidentität (Name, `CommandLine`, Port) gegen einen `Get-NetTCPConnection`-gestützten Fallback auf den echten Listener, sobald die getrackte PID fehlt, veraltet oder nicht identitätsgeprüft passt; der bestehende PID-Wiederverwendungsschutz bleibt erhalten. Ein verwandter Defekt in `Wait-KIOpenTerminalHealthy`, der den sauberen Exitcode-0-Handoff des Launchers fälschlich wie einen Absturz behandelte, ist ebenfalls behoben. Behebt außerdem eine real reproduzierte ComfyUI-Stop-Race-Bedingung: das generierte Stop-Skript prüft jetzt unmittelbar vor jedem `Stop-Process`-Aufruf erneut, ob der Prozess noch existiert, und wertet einen zwischenzeitlich von selbst beendeten Prozess als erfolgreich gestoppt statt als Fehler; ein tatsächlich noch laufender, nicht stoppbarer Prozess wird weiterhin als echter Fehler gemeldet. Open Terminal steigt von 0.1.0 auf 0.1.1 und Cutover Runtime von 1.6.14 auf 1.6.15, damit ein bestehendes Zielsystem diese beiden Fixes über einen regulären Upgrade-/Repair-Lauf tatsächlich erhält, statt bei unveränderten Komponentenversionen als Skip geplant zu werden -- dieselbe Lücke, die für Applications bereits in 2.18.1 geschlossen wurde. Behebt außerdem einen real reproduzierten WSL-Keeper-Defekt, live gegen eine echte Debian-WSL-Instanz verifiziert: der Start des Keepers über eine Login-Shell (`-u root -- bash -lc "exec sleep infinity"`) ließ dessen Windows-seitigen wsl.exe-Launcher innerhalb von rund einer Sekunde sterben, wodurch Debian kurz nach dem gemeldeten Running-Zustand wieder auf Stopped zurückfiel. Der Keeper startet jetzt über `wsl.exe -d Debian --exec /bin/sleep infinity` (keine Shell, keine Login-Session); ob er lebt, entscheidet ausschließlich ein echter `pgrep -f 'sleep infinity'`-Check innerhalb von Debian, sodass eine veraltete oder fehlende Windows-Launcher-PID (nur noch Best-Effort-Zusatzinfo) einen real laufenden Keeper nie mehr als gestoppt meldet; `Get-KIStackStatus.ps1`s WSL-Keeper-Erkennung wurde entsprechend korrigiert. Integration steigt von 1.5.11 auf 1.5.12 und Cutover Runtime weiter von 1.6.15 auf 1.6.16, damit ein bestehendes Zielsystem diesen Fix tatsächlich erhält -- dieselbe Lücke wie oben, da der Fix in generiertem Inhalt liegt, der von der Reconcile-/Plan-Logik nur bei geänderter Komponentenversion neu ausgeliefert wird. Kein neuer Funktionsumfang, keine neue Greenfield-Behauptung. +- **2.19.0**: Desktop Controls UIA-Operationen sind jetzt als zehn flache, konkret typisierte MCP-Tools erreichbar (`ui_list_windows`, `ui_inspect_window`, `ui_find_element`, `ui_get_properties`, `ui_get_value`, `ui_screenshot`, `ui_wait_for`, `ui_set_value`, `ui_invoke`, `ui_focus`), registriert auf derselben FastMCP-Instanz und demselben Port (`server:mcp:ki-stack-mcp-runtime`, `127.0.0.1:8021`), auf dem bereits die Open-Terminal-Tools laufen -- kein neuer MCP-Server, Port oder Credential; `scroll`/`send_input`/roher winapp-Zugriff bleiben strukturell nicht vorhanden, nicht nur policy-geblockt. Jedes Tool ist ein reiner Transport auf Desktop Controls eigenen Dispatcher, ausschließlich aufgelöst unter `\tools\desktop-control\current\Invoke-KIStackDesktopControl.ps1`, kein PATH-Fallback; ein von Desktop Control selbst gemeldetes Business-Ergebnis (`SecretContextBlocked`, `PostconditionNotProven`, ...) wird unverändert durchgereicht, nur ein Transportfehler löst einen MCP-Tool-Fehler aus. Damit ist der oben genannte offene Punkt "Desktop-Control-MCP-Anbindung" geschlossen. Behebt außerdem eine real vorhandene, bisher unentdeckte Delivery-Lücke: mcp-runtime materialisierte anders als winapp/desktop-control nie einen persistenten Paketbaum auf dem Zielsystem -- die eigenen Start-/Stop-Skripte verwiesen fest auf einen transaktionsgebundenen Payload-Staging-Ordner, und weder `Test-KIMcpRuntime` noch `Test-KICompleteMcpRuntimeCompliant` verglichen den deployten Inhalt jemals mit der Quelle, sodass ein geänderter Payload bei unveränderter Komponentenversion stillschweigend als `SkippedAlreadyCompliant` gemeldet statt reconciled wurde. mcp-runtime deployt jetzt einen persistenten, source-parity-geprüften Paketbaum unter `\tools\mcp-runtime\current\` (nach dem Vorbild von winapp/desktop-control), akzeptiert ein extern vorgegebenes `-BackupRoot` (derselbe 2.18.1-BackupRoot-Fix von Desktop Control, hier übernommen), und der eigene `mcp-runtime`-Schritt des Complete Installer führt Install/Upgrade/Repair+Validate jetzt über einen frischen, isolierten `pwsh`-Prozess aus, analog zu Desktop Controls eigener Härtung. Die Abhängigkeitsreihenfolge in `Contracts/COMPONENTS.json` ist korrigiert auf `python-git -> winapp -> desktop-control -> mcp-runtime` (mcp-runtime `requires` jetzt desktop-control, transitiv winapp), da die eigenen `ui_*`-Tools den Dispatcher-Pfad von Desktop Control bedingungslos auflösen. mcp-runtime steigt von 0.1.0 auf 0.2.0 und Desktop Control von 0.1.0 auf 0.1.1, damit ein bestehendes Zielsystem diese Fixes über einen regulären Upgrade-/Repair-Lauf tatsächlich erhält, statt bei unveränderten Komponentenversionen als Skip geplant zu werden. Behebt außerdem eine real reproduzierte `FastMCPDeprecationWarning`: Die Client-Konstruktion des `OpenAPIProvider` in `mcp_launcher.py` verwendete einen einfachen `httpx.AsyncClient` (als `httpx2` aliasiert), was fastmcp `4.0.3` (die tatsächlich vom gepinnten `open-terminal[mcp]==0.11.34` aufgelöste Version, reproduzierbar verifiziert, nicht nur angenommen) als veraltet meldet zugunsten eines echten `httpx2.AsyncClient` -- `httpx2` ist ein eigenständiges, separates Paket, das bereits transitiv über denselben Pin aufgelöst wird. Keine neue Greenfield-Behauptung. + Diese Umfänge sind kumulative Nachweise und keine austauschbaren Gesamtfreigaben. Insbesondere wurde nach 2.4.0 kein neuer vollständiger Windows-Greenfield-Lauf auf einem leeren Zielsystem behauptet oder durchgeführt; ebenso wurde in 2.17 kein Restore der produktiven `webui.db` durchgeführt. ## Bekannte offene Punkte @@ -134,5 +136,5 @@ Diese Umfänge sind kumulative Nachweise und keine austauschbaren Gesamtfreigabe - **Latenz-Tracing**: Es gibt weiterhin keine dedizierte Ende-zu-Ende-Zeitaufschlüsselung für OpenWebUI-Eingabe -> Prompt-/Tool-Aufbereitung -> LM-Studio-Request -> erstes Token. Der in 2.15 ergänzte LM-Studio-Runtime-Baseline-Check ersetzt kein vollständiges Tracing. - **Memory-Request-Default**: OpenWebUI 0.11.3 besitzt keinen persistenten serverseitigen Standard für `features.memory=true`. - **Produktionsdatenbank-Restore**: Das Online-Backup von `webui.db` ist real zielsystemvalidiert und das kontrollierte Restore acceptance-getestet; ein Restore der Produktionsdatenbank wurde nicht durchgeführt. -- **Desktop-Control-MCP-Anbindung**: Die MCP-Anbindung von Desktop Control ist noch nicht aktiviert; nicht freigegebene oder noch nicht produktionsvalidierte UIA-Fähigkeiten bleiben außerhalb des Vertrags. +- **Desktop-Control-MCP-Anbindung**: mit 2.19.0 aktiviert (siehe Release-Notiz oben) als flache `ui_*`-Tool-Oberfläche auf dem bestehenden MCP Runtime; eine formale Entscheidung, ob dies hinter einer eigenen Capability/einem eigenen Profil gegated werden soll, statt dass jedes bereits an `server:mcp:ki-stack-mcp-runtime` gebundene Profil sie strukturell mit erhält, bleibt offen. - **Bootstrap-Phase ohne PowerShell 7**: `Bootstrap-KIStackPowerShell7.ps1`, nur verwendet wenn PowerShell 7 selbst fehlt, besitzt weiterhin keine eigene Live-Heartbeat-Anzeige und schreibt stattdessen sein strukturiertes `.bootstrap.jsonl`-Diagnoselog. diff --git a/docs/en/KI-Stack-Installation-Guide.md b/docs/en/KI-Stack-Installation-Guide.md index bea6d08..8da1ca8 100644 --- a/docs/en/KI-Stack-Installation-Guide.md +++ b/docs/en/KI-Stack-Installation-Guide.md @@ -1,13 +1,13 @@ -# KI-Stack Complete Installer 2.18.2 – Installation and upgrade +# KI-Stack Complete Installer 2.19.0 – Installation and upgrade -This guide applies to the current published package `KI-Stack-Complete-Installer-v2.18.2.zip`. The last complete, successful, physical Greenfield installation on an empty target was performed and verified with version 2.4.0. Later releases through 2.18.2 were validated through repository regression, deterministic package builds, component-specific acceptance, and real-target upgrade/reconciliation tests as documented for each release; no newer complete empty-target Windows Greenfield run is claimed here. +This guide applies to the current published package `KI-Stack-Complete-Installer-v2.19.0.zip`. The last complete, successful, physical Greenfield installation on an empty target was performed and verified with version 2.4.0. Later releases through 2.19.0 were validated through repository regression, deterministic package builds, component-specific acceptance, and real-target upgrade/reconciliation tests as documented for each release; no newer complete empty-target Windows Greenfield run is claimed here. ## Download and SHA-256 -Download the ZIP and `KI-Stack-Complete-Installer-v2.18.2.zip.sha256` from the same GitHub Release. The authoritative hash is provided only by the sidecar and the GitHub Release description. +Download the ZIP and `KI-Stack-Complete-Installer-v2.19.0.zip.sha256` from the same GitHub Release. The authoritative hash is provided only by the sidecar and the GitHub Release description. ```powershell -$zip = '.\KI-Stack-Complete-Installer-v2.18.2.zip' +$zip = '.\KI-Stack-Complete-Installer-v2.19.0.zip' $expected = ((Get-Content "$zip.sha256" -Raw) -split '\s+')[0].ToLowerInvariant() $actual = (Get-FileHash -LiteralPath $zip -Algorithm SHA256).Hash.ToLowerInvariant() if ($actual -ne $expected) { throw 'SHA-256 mismatch.' } diff --git a/docs/en/KI-Stack-Manual-Model-Provisioning.md b/docs/en/KI-Stack-Manual-Model-Provisioning.md index 20bc6cd..0ebbfd8 100644 --- a/docs/en/KI-Stack-Manual-Model-Provisioning.md +++ b/docs/en/KI-Stack-Manual-Model-Provisioning.md @@ -1,6 +1,6 @@ # Optional model provisioning and preload -Complete Installer 2.18.2 automatically downloads missing models, including embedding-only Nomic Q4_K_M, from revision-pinned sources in the central model manifest. Manual provisioning is not an installation prerequisite. +Complete Installer 2.19.0 automatically downloads missing models, including embedding-only Nomic Q4_K_M, from revision-pinned sources in the central model manifest. Manual provisioning is not an installation prerequisite. An optional cache or `ExternalModels` preload can save bandwidth. A file is accepted only after filename, exact size, and full SHA-256 verification. A valid target is reused and is not downloaded again. Interrupted downloads remain resumable; an incorrect size or hash produces `Failed`. diff --git a/docs/en/KI-Stack-Model-Download-Guide.md b/docs/en/KI-Stack-Model-Download-Guide.md index 0ea3d8d..90cbcda 100644 --- a/docs/en/KI-Stack-Model-Download-Guide.md +++ b/docs/en/KI-Stack-Model-Download-Guide.md @@ -1,6 +1,6 @@ # Automatic model download contract -This contract applies to the current published Complete Installer 2.18.2 release. +This contract applies to the current published Complete Installer 2.19.0 release. The Complete Installer requires no manually supplied model or payload files on an empty target. The nine visual artifacts for Z-Image Turbo and WAN2.2 T2V 14B with both LightX2V four-step LoRAs, both Heretic files, and the embedding-only `nomic-embed-text-v1.5.Q4_K_M.gguf` have revision-bound sources, exact byte sizes and SHA-256 values in `tools/models-workflows/current/Manifests/models.manifest.json`. diff --git a/docs/en/KI-Stack-Operations-and-User-Guide.md b/docs/en/KI-Stack-Operations-and-User-Guide.md index 6266282..5bdd841 100644 --- a/docs/en/KI-Stack-Operations-and-User-Guide.md +++ b/docs/en/KI-Stack-Operations-and-User-Guide.md @@ -1,4 +1,4 @@ -# KI-Stack 2.18.2 operations and user guide +# KI-Stack 2.19.0 operations and user guide ## Normal operation @@ -228,7 +228,7 @@ A first-time WSL2 activation on a genuinely empty machine can require a Windows - **SearXNG appears unreachable**: check `systemctl status ki-stack-searxng uwsgi nginx valkey-server` inside the WSL Debian distribution; either `ki-stack-searxng` or `uwsgi` being active and healthy on port 8888 is a valid, expected state. - **An Open WebUI API-dependent step reports a credential-related Pending/Blocked state**: run `Test-KIStackOpenWebUICredential.ps1`. If no valid credential exists, bootstrap it with `Initialize-KIStackOpenWebUICredential.ps1`; do not fall back to a separately maintained temporary API key. -The last complete, successful, physical Greenfield installation on an empty target was verified with Complete Installer 2.4.0. Later releases through 2.18.2 add regression, package, component, upgrade/reconcile, and real-target evidence but do not claim a newer complete empty-target Windows Greenfield run. +The last complete, successful, physical Greenfield installation on an empty target was verified with Complete Installer 2.4.0. Later releases through 2.19.0 add regression, package, component, upgrade/reconcile, and real-target evidence but do not claim a newer complete empty-target Windows Greenfield run. ## Known open items diff --git a/docs/en/KI-Stack-Technical-Documentation.md b/docs/en/KI-Stack-Technical-Documentation.md index 23a8dc3..5f22f9c 100644 --- a/docs/en/KI-Stack-Technical-Documentation.md +++ b/docs/en/KI-Stack-Technical-Documentation.md @@ -1,6 +1,6 @@ -# KI-Stack 2.18.2 technical documentation +# KI-Stack 2.19.0 technical documentation -KI-Stack is a transactional Windows local-AI stack. Complete Installer `2.18.2` is the current published GitHub Release. +KI-Stack is a transactional Windows local-AI stack. Complete Installer `2.19.0` is the current published GitHub Release. The validation record must be read by scope rather than as one interchangeable claim: the last complete physical Greenfield installation on an empty Windows target was performed and verified with 2.4.0; Complete Installer 2.10.0 remains the documented whole-stack regression plus real-target reference run; later releases added additional real-target, component, upgrade/reconcile, security, and package-validation evidence without claiming a newer full empty-target Windows Greenfield run. @@ -19,16 +19,16 @@ The current 2.18 architecture includes the MCP Runtime introduced in 2.15 as the | Cutover Runtime | 1.6.16 | | Codex Local | 0.2.1 | | RAG | 0.4.0 | -| MCP Runtime | 0.1.0 | +| MCP Runtime | 0.2.0 | | Open Terminal | 0.1.1 | | WinApp | 0.6.1 | -| Desktop Control | 0.1.0 | +| Desktop Control | 0.1.1 | | Production Recovery | 1.7.0-r7 | | Validation Gate | 1.0.3 | | Target Acceptance | 1.0.10 | | OpenWebUI Visual Pack | 2.0.5 | | OpenWebUI Agent Pack | 1.9.0 | -| Complete Installer | 2.18.2 | +| Complete Installer | 2.19.0 | ComfyUI's reference and minimum supported version for reproducible Greenfield installs and reconciliation is `v0.34.0`; an existing, supported newer installation is preserved and never auto-downgraded. Open WebUI's `ReferenceVersion` and `MinimumSupportedVersion` are both `0.11.3` -- any installed version from `0.11.3` up is supported, and an existing, supported newer installation is preserved the same way, never auto-downgraded to the exact reference. @@ -129,6 +129,8 @@ Validation evidence is intentionally scoped by what was actually exercised: - **2.18.1**: hotfix. The Complete Installer own central desktop-control reconcile step previously ran in the same long-lived orchestrator process as every other component; on the real target, the Validate phase immediately following Install failed there even though both actions each passed for real when run individually, each in its own fresh process. Install/Upgrade/Repair and the following Validate now each run in a fresh `pwsh` process; Desktop Control additionally accepts an optional, transaction-scoped backup root, and a Failed step whose own rollback already completed no longer blocks a later run over its own, by then irrelevant, backup path. Verified live against the real affected target (a real `winapp.exe`); no new feature scope, no new Greenfield claim. Also fixes a real, reproduced defect where Open WebUI's own `Path.cwd()`-based fallback for `WEBUI_SECRET_KEY` picked up whatever working directory the outermost starter chain happened to run from (e.g. `C:\Windows\System32` for an elevated shell opened the usual Windows way) and then failed, without admin rights, to persist its key there; the generated Open WebUI starter now sets `WEBUI_SECRET_KEY` itself from a persistent, cryptographically random key stored once under `\state\openwebui\.webui_secret_key`, migrating a pre-fix installation's legitimate key at `\.webui_secret_key` if present so existing sessions are not invalidated. Applications advances from 1.4.11 to 1.4.12 so an existing target actually receives this generated-starter fix through a normal Upgrade/Repair run instead of being planned as Skip on an unchanged component version. - **2.18.2**: hotfix. The central `Start-KIStack.cmd`/`Stop-KIStack.cmd` deployed onto every target previously called the old cutover core (`modules\cutover\*-KIStack.cmd`) directly, never reaching `-Mode Start`/`-Mode Stop` (`Invoke-KICompleteLifecycle`) -- MCP Runtime and Open Terminal were therefore never started or stopped by the central starters, and the MCP health gate ahead of Open WebUI never applied there. The deployed lifecycle templates now call `installer\complete\Invoke-KIStackCompleteInstaller.ps1 -Mode Start`/`-Mode Stop`, which still run the same cutover core internally but now correctly wrapped with MCP Runtime and Open Terminal; the existing stale-process/WSL/registry stop cleanup is preserved unchanged. Also fixes a real, reproduced Open Terminal defect where the tracked PID could be the `uv`/`uvx` launcher process rather than the real listener it hands off to (the launcher exits itself, often with exit code 0, once the tool takes over); Status/Stop now verify process identity (name, `CommandLine`, port) against a `Get-NetTCPConnection`-based real-listener fallback whenever the tracked PID is missing, stale, or mismatched, PID-reuse protection is preserved, and a related `Wait-KIOpenTerminalHealthy` defect that mistook the launcher's clean exit-code-0 handoff for a crash is fixed too. Also fixes a real, reproduced ComfyUI stop-race by re-checking process existence immediately before each `Stop-Process` call, treating a process that already exited on its own as successfully stopped rather than an error, while still reporting genuine stop failures. Open Terminal advances from 0.1.0 to 0.1.1 and Cutover Runtime from 1.6.14 to 1.6.15 so an existing target actually receives these two fixes through a normal Upgrade/Repair run instead of being planned as Skip on unchanged component versions -- the same delivery gap previously closed for Applications in 2.18.1. Also fixes a real, reproduced WSL-Keeper defect, verified live against a real Debian WSL instance: launching the keeper via a login shell (`-u root -- bash -lc "exec sleep infinity"`) let its Windows-side wsl.exe launcher die within about a second, tearing Debian back down to Stopped shortly after it had been reported Running. The keeper now launches via `wsl.exe -d Debian --exec /bin/sleep infinity` (no shell, no login session); its liveness is decided solely by a real in-Debian `pgrep -f 'sleep infinity'` check, so a stale or missing Windows launcher PID (kept only as best-effort information) can never report a real, still-running keeper as stopped, and `Get-KIStackStatus.ps1`'s WSL-Keeper detection was fixed the same way. Integration advances from 1.5.11 to 1.5.12 and Cutover Runtime further from 1.6.15 to 1.6.16 so an existing target actually receives this fix -- the identical delivery gap as above, since the fix lives in generated content the reconcile/plan logic only redelivers when the pinned component version itself changes. No new feature scope, no new Greenfield claim. +- **2.19.0**: Desktop Control's UIA operations are now reachable as ten flat, natively-typed MCP tools (`ui_list_windows`, `ui_inspect_window`, `ui_find_element`, `ui_get_properties`, `ui_get_value`, `ui_screenshot`, `ui_wait_for`, `ui_set_value`, `ui_invoke`, `ui_focus`) registered on the SAME MCP Runtime FastMCP instance and port (`server:mcp:ki-stack-mcp-runtime`, `127.0.0.1:8021`) Open Terminal's own tools already run on -- no new MCP server, port, or credential; `scroll`/`send_input`/raw-winapp remain structurally absent, not merely policy-blocked. Each tool is a thin transport straight to Desktop Control's own dispatcher, resolved only at `\tools\desktop-control\current\Invoke-KIStackDesktopControl.ps1`, no PATH fallback; a business-level Desktop Control result (`SecretContextBlocked`, `PostconditionNotProven`, ...) passes through unchanged, only a transport failure raises an MCP tool error. This closed the "Desktop Control MCP wiring" open item above. Also fixes a real, previously-undetected delivery gap: mcp-runtime never materialized a persistent package tree on a target the way winapp/desktop-control already do -- its own `Start`/`Stop` scripts hard-coded a path into a transaction-scoped payload staging directory, and neither `Test-KIMcpRuntime` nor `Test-KICompleteMcpRuntimeCompliant` ever compared deployed content against source, so a changed payload at an unchanged component VERSION was silently reported `SkippedAlreadyCompliant` instead of being reconciled. mcp-runtime now deploys a persistent, source-parity-checked package tree at `\tools\mcp-runtime\current\` (mirroring winapp's/desktop-control's own shape), accepts an externally-owned `-BackupRoot` (the same 2.18.1 Desktop-Control BackupRoot-respect fix applied here), and the Complete Installer's own `mcp-runtime` step now runs Install/Upgrade/Repair+Validate via a fresh, isolated `pwsh` process, matching desktop-control's own hardening. `Contracts/COMPONENTS.json`'s dependency order is corrected to `python-git -> winapp -> desktop-control -> mcp-runtime` (mcp-runtime now `requires` desktop-control, transitively winapp), since its own `ui_*` tools resolve Desktop Control's dispatcher path unconditionally. mcp-runtime advances from 0.1.0 to 0.2.0 and Desktop Control from 0.1.0 to 0.1.1 so an existing target actually receives these fixes through a normal Upgrade/Repair run instead of being planned as Skip on unchanged component versions. Also fixes a real, reproduced `FastMCPDeprecationWarning`: `mcp_launcher.py`'s `OpenAPIProvider` client construction used a plain `httpx.AsyncClient` (aliased as `httpx2`), which fastmcp `4.0.3` (the version actually resolved by the pinned `open-terminal[mcp]==0.11.34`, reproducibly verified, not merely assumed) flags as deprecated in favor of a real `httpx2.AsyncClient` -- `httpx2` is a genuine, separate package already resolved transitively by the same pin. No new Greenfield claim. + These scopes are cumulative evidence, not interchangeable claims. In particular, no release after 2.4.0 has claimed or performed a new complete empty-target Windows Greenfield acceptance, and no production `webui.db` restore was performed in 2.17. ## Known open items @@ -136,5 +138,5 @@ These scopes are cumulative evidence, not interchangeable claims. In particular, - **Latency tracing**: there is still no dedicated end-to-end timing breakdown for Open WebUI input -> prompt/tool assembly -> LM Studio request -> first token. The LM Studio runtime-baseline check added in 2.15 is not a replacement for full tracing. - **Memory request default**: Open WebUI 0.11.3 has no persisted server-side default for `features.memory=true`. - **Production database restore**: online `webui.db` backup is real-target validated and controlled restore is acceptance-tested, but no production database restore has been performed. -- **Desktop Control MCP wiring**: Desktop Control MCP wiring is not yet activated; UIA capabilities that are not released, or not yet production-validated, remain outside the contract. +- **Desktop Control MCP wiring**: activated in 2.19.0 (see the release note above) as a flat `ui_*` tool surface on the existing MCP Runtime; a formal decision on whether to gate it behind a dedicated capability/profile, versus every existing `server:mcp:ki-stack-mcp-runtime`-bound profile gaining it structurally, remains open. - **Bootstrap phase without PowerShell 7**: `Bootstrap-KIStackPowerShell7.ps1`, used only when PowerShell 7 itself is absent, still has no live heartbeat display of its own and writes its structured `.bootstrap.jsonl` diagnostic log instead. diff --git a/scripts/Test-Repository.ps1 b/scripts/Test-Repository.ps1 index 6ba9c57..b19dc68 100644 --- a/scripts/Test-Repository.ps1 +++ b/scripts/Test-Repository.ps1 @@ -496,9 +496,9 @@ try { @{name='ComfyUI';root='tools/comfyui/current';version='1.2.4'}, @{name='Integration';root='tools/integration/current';version='1.5.12'}, @{name='Cutover Runtime';root='tools/cutover-runtime/current';version='1.6.16'}, - @{name='Complete Installer';root='tools/complete-installer/current';version='2.18.2'}, + @{name='Complete Installer';root='tools/complete-installer/current';version='2.19.0'}, @{name='WinApp';root='tools/winapp/current';version='0.6.1'}, - @{name='Desktop Control';root='tools/desktop-control/current';version='0.1.0'} + @{name='Desktop Control';root='tools/desktop-control/current';version='0.1.1'} ) foreach($packageContract in $gitFreePackages){ $packageRoot=Join-Path $RootPath $packageContract.root diff --git a/tools/complete-installer/current/CompleteInstaller.psm1 b/tools/complete-installer/current/CompleteInstaller.psm1 index f6716f2..3e5a379 100644 --- a/tools/complete-installer/current/CompleteInstaller.psm1 +++ b/tools/complete-installer/current/CompleteInstaller.psm1 @@ -527,18 +527,61 @@ function Test-KICompleteOpenTerminalCompliant { }catch{return $false} } +function Test-KICompleteMcpRuntimePayloadParity { + # Verbatim pattern of Test-KICompleteDesktopControlPayloadParity (s/DesktopControl/McpRuntime/, + # Payload key 'McpRuntime', deployed target 'tools/mcp-runtime/current'): desired-state parity + # for the Complete Installer's planning/skip-recheck decision -- the deployed + # \tools\mcp-runtime\current\ tree must still be byte-identical to THIS installer + # run's mcp-runtime payload (Payload/McpRuntime/*.zip), not merely internally consistent at + # the same component VERSION. + param([Parameter(Mandatory)][string]$PackageRoot,[Parameter(Mandatory)][string]$DeployedPackageRoot) + $payloadDir=Join-Path $PackageRoot 'Payload/McpRuntime' + if(-not(Test-Path -LiteralPath $payloadDir -PathType Container)){return $false} + if(@(Get-ChildItem -LiteralPath $payloadDir -File -Filter '*.zip' -ErrorAction SilentlyContinue).Count-ne1){return $false} + if(-not(Test-Path -LiteralPath $DeployedPackageRoot -PathType Container)){return $false} + $extract=Join-Path ([IO.Path]::GetTempPath()) ('KICompleteMRParity-'+[guid]::NewGuid().ToString('N').Substring(0,12)) + try{ + $sourceRoot=Expand-KICompletePayload -PackageRoot $PackageRoot -PayloadName 'McpRuntime' -Destination $extract + $rel={param($base,$path) [IO.Path]::GetRelativePath($base,$path).Replace('\','/')} + $keep={param($r) $r-ne'Payload'-and$r-notlike'Payload/*'} + $sourceFiles=@(Get-ChildItem -LiteralPath $sourceRoot -Recurse -File|ForEach-Object{& $rel $sourceRoot $_.FullName}|Where-Object{& $keep $_}) + $targetFiles=@(Get-ChildItem -LiteralPath $DeployedPackageRoot -Recurse -File|ForEach-Object{& $rel $DeployedPackageRoot $_.FullName}|Where-Object{& $keep $_}) + foreach($r in $sourceFiles){ + $native=$r.Replace('/',[IO.Path]::DirectorySeparatorChar) + $t=Join-Path $DeployedPackageRoot $native + if(-not(Test-Path -LiteralPath $t -PathType Leaf)){return $false} + if((Get-FileHash -LiteralPath (Join-Path $sourceRoot $native) -Algorithm SHA256).Hash-ne(Get-FileHash -LiteralPath $t -Algorithm SHA256).Hash){return $false} + } + if(@($targetFiles|Where-Object{$sourceFiles-notcontains$_}).Count-gt0){return $false} + return $true + }finally{ + if(Test-Path -LiteralPath $extract){Remove-Item -LiteralPath $extract -Recurse -Force -ErrorAction SilentlyContinue} + } +} + function Test-KICompleteMcpRuntimeCompliant { - # Mirrors Test-KICompleteOpenTerminalCompliant's own shape and its own reasoning: mcp-runtime - # already has a richer compliance function (McpRuntime.psm1's own Test-KIMcpRuntime -- marker - # version match, starter/stopper/workspace/credential presence, optional live uv check), but - # this orchestrator cannot reliably cross-import it either: in the packaged/shipped form, - # McpRuntime.psm1 lives inside Payload/McpRuntime/*.zip, not as a sibling file next to this - # module -- exactly the same packaging constraint Test-KICompleteOpenTerminalCompliant already - # documents for Open Terminal. This re-verifies directly against the real, on-disk artifacts - # Install-KIMcpRuntime actually writes (same four items Test-KIMcpRuntime itself checks minus - # the live uv/decryption checks, which -- like Open Terminal's own split -- are separate - # runtime concerns, not "is this package correctly deployed"). - param([Parameter(Mandatory)][string]$TargetRoot,[string]$ExpectedComponentVersion='0.1.0') + # Mirrors Test-KICompleteOpenTerminalCompliant's own shape and its own reasoning for the + # LIFECYCLE dimension (marker/starter/stopper/workspace/credential presence -- Open-WebUI + # reachability plus a live uv check are separate runtime concerns, not "is this package + # correctly deployed", exactly as Open Terminal's own split already documents). + # + # 2.19 Phase 1: ADDITIONALLY verifies the PACKAGE dimension -- mcp-runtime now deploys a + # persistent, source-parity-checked tree at \tools\mcp-runtime\current\ (see + # McpRuntime.psm1's own Get-KIMcpRuntimeInstallPaths/Test-KIMcpRuntimeDeployed), mirroring + # winapp's/desktop-control's own persistent-package shape. This orchestrator still cannot + # reliably cross-import McpRuntime.psm1 (packaged form: Payload/McpRuntime/*.zip, not a + # sibling file), so both dimensions are re-verified directly against the real, on-disk + # artifacts Install-KIMcpRuntime actually writes, never by importing that module. + # + # -InstallerPackageRoot (the installer package root of THIS run) additionally enforces + # source<->target parity against Payload/McpRuntime/*.zip via + # Test-KICompleteMcpRuntimePayloadParity, so an internally consistent OLD deployment at the + # same VERSION (0.1.0 == 0.1.0) is reported non-compliant and the isolated step actually runs + # -- this is the exact fix for the drift class that was previously reported + # SkippedAlreadyCompliant regardless of payload content. Omitted => the previous + # lifecycle-only + package-self-consistency behaviour (no cross-payload parity), never a + # weaker check than before this addition. + param([Parameter(Mandatory)][string]$TargetRoot,[string]$ExpectedComponentVersion='0.1.0',[string]$InstallerPackageRoot) $root=Join-Path $TargetRoot 'modules/mcp-runtime' $markerPath=Join-Path $root 'installation.json' $starter=Join-Path $root 'Start-KIStack-McpRuntime.cmd' @@ -549,8 +592,29 @@ function Test-KICompleteMcpRuntimeCompliant { if(-not(Test-Path -LiteralPath $workspace -PathType Container)){return $false} try{ $marker=Read-KICompleteJson $markerPath - return ([string]$marker.version-eq$ExpectedComponentVersion) + if([string]$marker.version-ne$ExpectedComponentVersion){return $false} }catch{return $false} + $packageRoot=Join-Path $TargetRoot 'tools/mcp-runtime/current' + $packageVersionFile=Join-Path $packageRoot 'VERSION' + $packageChecksumFile=Join-Path $packageRoot 'SHA256SUMS.txt' + if(-not(Test-Path -LiteralPath $packageRoot -PathType Container)){return $false} + if(-not(Test-Path -LiteralPath $packageVersionFile -PathType Leaf)){return $false} + if((Get-Content -LiteralPath $packageVersionFile -Raw).Trim()-ne$ExpectedComponentVersion){return $false} + if(-not(Test-Path -LiteralPath $packageChecksumFile -PathType Leaf)){return $false} + foreach($line in Get-Content -LiteralPath $packageChecksumFile){ + if([string]::IsNullOrWhiteSpace($line)){continue} + if($line-notmatch'^([0-9a-fA-F]{64})\s+\*?(.+)$'){return $false} + $file=Join-Path $packageRoot ($Matches[2].Replace('/',[IO.Path]::DirectorySeparatorChar)) + if(-not(Test-Path -LiteralPath $file -PathType Leaf)){return $false} + if((Get-FileHash -LiteralPath $file -Algorithm SHA256).Hash.ToLowerInvariant()-ne$Matches[1].ToLowerInvariant()){return $false} + } + foreach($required in @('Invoke-KIStackMcpRuntime.ps1','McpRuntime.psm1','MANIFEST.json','Config/mcp-runtime.config.json','Scripts/mcp_launcher.py','Scripts/ki_desktop_control_tools.py','Vendor/KIStackOpenWebUICredential.psm1','Vendor/KIStackPathContext.psm1')){ + if(-not(Test-Path -LiteralPath (Join-Path $packageRoot ($required.Replace('/',[IO.Path]::DirectorySeparatorChar))) -PathType Leaf)){return $false} + } + if(-not[string]::IsNullOrWhiteSpace($InstallerPackageRoot)){ + if(-not(Test-KICompleteMcpRuntimePayloadParity -PackageRoot $InstallerPackageRoot -DeployedPackageRoot $packageRoot)){return $false} + } + return $true } function Test-KICompleteWinAppCompliant { @@ -798,7 +862,7 @@ function New-KICompletePlan { if([string]$component.id-eq'openwebui-visual-pack'-and$null-eq$FixtureState){$compliant=$compliant-and(Test-KICompleteVisualPackCompliant -PackageRoot $PackageRoot -TargetRoot $TargetRoot)} if([string]$component.id-eq'codex-local'-and$null-eq$FixtureState){$compliant=$compliant-and(Test-KICompleteCodexLocalCompliant -TargetRoot $TargetRoot -ExpectedComponentVersion ([string]$component.version))} if([string]$component.id-eq'open-terminal'-and$null-eq$FixtureState){$compliant=$compliant-and(Test-KICompleteOpenTerminalCompliant -TargetRoot $TargetRoot -ExpectedComponentVersion ([string]$component.version))} - if([string]$component.id-eq'mcp-runtime'-and$null-eq$FixtureState){$compliant=$compliant-and(Test-KICompleteMcpRuntimeCompliant -TargetRoot $TargetRoot -ExpectedComponentVersion ([string]$component.version))} + if([string]$component.id-eq'mcp-runtime'-and$null-eq$FixtureState){$compliant=$compliant-and(Test-KICompleteMcpRuntimeCompliant -TargetRoot $TargetRoot -ExpectedComponentVersion ([string]$component.version) -InstallerPackageRoot $PackageRoot)} if([string]$component.id-eq'winapp'-and$null-eq$FixtureState){$compliant=$compliant-and(Test-KICompleteWinAppCompliant -TargetRoot $TargetRoot -ExpectedComponentVersion ([string]$component.version))} if([string]$component.id-eq'desktop-control'-and$null-eq$FixtureState){$compliant=$compliant-and(Test-KICompleteDesktopControlCompliant -TargetRoot $TargetRoot -ExpectedComponentVersion ([string]$component.version) -InstallerPackageRoot $PackageRoot)} if([string]$component.id-eq'integration'-and$null-eq$FixtureState){$compliant=$compliant-and(Test-KICompleteIntegrationCompliant -TargetRoot $TargetRoot -ExpectedComponentVersion ([string]$component.version))} @@ -1741,7 +1805,7 @@ function Invoke-KIStackCompleteInstaller { # so there is no 'deprecatedAliasUsed' field here: this shape is never produced by the # deprecated alias, which keeps the historical flat shape instead (see above). return [pscustomobject][ordered]@{ - version='2.18.2' + version='2.19.0' mode=$Mode operation='OperationsRestore' scope=@('Registry/Autostart (LM Studio competing autostart)','Desktop-Verknüpfungen (KI-Stack starten/stoppen/Status)','Docker-Restart-Policy (KI-Stack-eigene Container)') @@ -1757,13 +1821,13 @@ function Invoke-KIStackCompleteInstaller { [pscustomobject]@{passed=$true;status=if($rollbackRecovery.status-eq'PendingRollbackCompleted'-or$failedStateRecovery.status-eq'FailedTransactionStateRecovered'){'Recovered'}else{'NoPendingRecovery'};rollback=$rollbackRecovery;failedState=$failedStateRecovery} } else { [pscustomobject]@{passed=$true;status='NotApplicable';transactions=@()} } $plan = New-KICompletePlan -Mode $Mode -PackageRoot $PackageRoot -TargetRoot $TargetRoot -EnableOpenWebUIBallistics:$EnableOpenWebUIBallistics -ReplayComponent $ReplayComponent -PathContext $pathContext - if ($Mode -eq 'Audit' -or $DryRun) { return [pscustomobject]@{version='2.18.2';mode=$Mode;preflight=$preflight;plan=$plan;operations=(Test-KICompleteOperations $TargetRoot -DesktopPath $DesktopPath);mutatesTarget=$false} } - if ($Mode -eq 'Validate') { return [pscustomobject]@{version='2.18.2';mode='Validate';plan=$plan;health=(Invoke-KICompleteHealth $config);operations=(Test-KICompleteOperations $TargetRoot -DesktopPath $DesktopPath);mutatesTarget=$false} } + if ($Mode -eq 'Audit' -or $DryRun) { return [pscustomobject]@{version='2.19.0';mode=$Mode;preflight=$preflight;plan=$plan;operations=(Test-KICompleteOperations $TargetRoot -DesktopPath $DesktopPath);mutatesTarget=$false} } + if ($Mode -eq 'Validate') { return [pscustomobject]@{version='2.19.0';mode='Validate';plan=$plan;health=(Invoke-KICompleteHealth $config);operations=(Test-KICompleteOperations $TargetRoot -DesktopPath $DesktopPath);mutatesTarget=$false} } if(-not$Resume -and $plan.alreadyCompliant -and -not[bool]$plan.hasReplay -and (Test-KICompleteDeploymentCompliant $PackageRoot $TargetRoot)-and(Test-KICompleteOperations $TargetRoot -DesktopPath $DesktopPath).passed){ $needsReconciliation=@($plan.steps|Where-Object{$_.initialState.reconciliationNeeded}).Count-gt0-or[bool]$plan.stateHasOrphans $statePath=$null - if($needsReconciliation){$statePath=Update-KICompleteComponentState -Plan $plan -PathContext $pathContext -CompleteVersion '2.18.2'} - return [pscustomobject]@{version='2.18.2';mode=$Mode;status=if($needsReconciliation){'StateReconciled'}else{'SkippedAlreadyCompliant'};plan=$plan;statePath=$statePath;pendingRollback=$pendingRollback;transactionCreated=$false;backupCreated=$false;mutatesTarget=($needsReconciliation-or$pendingRollback.status-eq'Recovered')} + if($needsReconciliation){$statePath=Update-KICompleteComponentState -Plan $plan -PathContext $pathContext -CompleteVersion '2.19.0'} + return [pscustomobject]@{version='2.19.0';mode=$Mode;status=if($needsReconciliation){'StateReconciled'}else{'SkippedAlreadyCompliant'};plan=$plan;statePath=$statePath;pendingRollback=$pendingRollback;transactionCreated=$false;backupCreated=$false;mutatesTarget=($needsReconciliation-or$pendingRollback.status-eq'Recovered')} } $state = [string]$pathContext.StateRoot if ($Resume) { @@ -1800,7 +1864,7 @@ function Invoke-KIStackCompleteInstaller { $resumeCompliant=$resumeActual-eq[string]$step.version if([string]$step.id-eq'codex-local'){$resumeCompliant=$resumeCompliant-and(Test-KICompleteCodexLocalCompliant -TargetRoot $TargetRoot -ExpectedComponentVersion ([string]$step.version))} if([string]$step.id-eq'open-terminal'){$resumeCompliant=$resumeCompliant-and(Test-KICompleteOpenTerminalCompliant -TargetRoot $TargetRoot -ExpectedComponentVersion ([string]$step.version))} - if([string]$step.id-eq'mcp-runtime'){$resumeCompliant=$resumeCompliant-and(Test-KICompleteMcpRuntimeCompliant -TargetRoot $TargetRoot -ExpectedComponentVersion ([string]$step.version))} + if([string]$step.id-eq'mcp-runtime'){$resumeCompliant=$resumeCompliant-and(Test-KICompleteMcpRuntimeCompliant -TargetRoot $TargetRoot -ExpectedComponentVersion ([string]$step.version) -InstallerPackageRoot $PackageRoot)} if([string]$step.id-eq'winapp'){$resumeCompliant=$resumeCompliant-and(Test-KICompleteWinAppCompliant -TargetRoot $TargetRoot -ExpectedComponentVersion ([string]$step.version))} if([string]$step.id-eq'desktop-control'){$resumeCompliant=$resumeCompliant-and(Test-KICompleteDesktopControlCompliant -TargetRoot $TargetRoot -ExpectedComponentVersion ([string]$step.version) -InstallerPackageRoot $PackageRoot)} if([string]$step.id-eq'integration'){$resumeCompliant=$resumeCompliant-and(Test-KICompleteIntegrationCompliant -TargetRoot $TargetRoot -ExpectedComponentVersion ([string]$step.version))} @@ -2061,29 +2125,42 @@ function Invoke-KIStackCompleteInstaller { } } elseif ($step.id -eq 'mcp-runtime') { - # Mirrors open-terminal's own isolated shape exactly (same template, same - # Expand-KICompletePayload -> own entry-point Install/Upgrade/Repair via - # plannedMode, then Validate -> on any failure, roll back via that SAME entry - # point's own Rollback action against its own backupPath). Deliberately does - # NOT start the server process and does NOT register it with Open WebUI here -- - # Open Terminal's own primary-installer branch does neither either (its own - # Start/Stop is a separate, additive chain onto the root Start-/Stop-KIStack.cmd, - # see Invoke-KICompleteMcpRuntimeLifecycle below; its own OpenWebUI tool-server - # registration is documented as a separate, one-time step, automated or manual, - # never part of Install). Real Open-WebUI reachability plus a bootstrapped admin - # credential are runtime preconditions for Register only, never modeled as a - # dependency here -- Register can run before or after this step, or not at all - # yet, without blocking Install/Upgrade/Repair. + # Mirrors open-terminal's own isolated shape (same template: Expand-KICompletePayload + # -> own entry-point Install/Upgrade/Repair via plannedMode, then Validate -> on any + # failure, roll back via that SAME entry point's own Rollback action against its own + # backupPath). Deliberately does NOT start the server process and does NOT register + # it with Open WebUI here -- Open Terminal's own primary-installer branch does + # neither either (its own Start/Stop is a separate, additive chain onto the root + # Start-/Stop-KIStack.cmd, see Invoke-KICompleteMcpRuntimeLifecycle below; its own + # OpenWebUI tool-server registration is documented as a separate, one-time step, + # automated or manual, never part of Install). Real Open-WebUI reachability plus a + # bootstrapped admin credential are runtime preconditions for Register only, never + # modeled as a dependency here -- Register can run before or after this step, or + # not at all yet, without blocking Install/Upgrade/Repair. + # + # 2.19 Phase 1 structural fix: Install-KIMcpRuntime now deploys a persistent package + # tree (\tools\mcp-runtime\current\) the exact same way winapp's/ + # desktop-control's own Install-KI* functions do, so this step now (a) passes its + # own slice of the transaction's BackupRoot -- never the standalone + # \backups\mcp-runtime path -- mirroring desktop-control's own 2.18.1 + # BackupRoot-respect fix, and (b) runs Install/Upgrade/Repair and the immediately + # following Validate via Invoke-KICompleteJsonScriptIsolated (a genuinely fresh + # pwsh.exe process each), matching desktop-control's own hardening for the same + # risk class (a real Install-then-Validate pair against a freshly re-deployed + # package tree, in-process module/session state no longer guaranteed clean). + # Rollback stays on the shared in-process Invoke-KICompleteJsonScript, exactly like + # desktop-control's own asymmetry. $extract=Join-Path ([string]$pathContext.PayloadRoot) 'McpRuntime' $componentRoot=Expand-KICompletePayload -PackageRoot $PackageRoot -PayloadName 'McpRuntime' -Destination $extract $entry=Join-Path $componentRoot 'Invoke-KIStackMcpRuntime.ps1' if(-not(Test-Path -LiteralPath $entry -PathType Leaf)){throw 'MCP-Runtime-Einstieg fehlt.'} $action=if($step.plannedMode-eq'Repair'){'Repair'}elseif($step.plannedMode-eq'Upgrade'){'Upgrade'}else{'Install'} + $mcpRuntimeBackupRoot=Join-Path ([string]$pathContext.TransactionBackupRoot) 'mcp-runtime' $result=$null try{ - $result=Invoke-KICompleteJsonScript -Script $entry -Arguments @{Action=$action;TargetRoot=$TargetRoot} + $result=Invoke-KICompleteJsonScriptIsolated -Script $entry -Arguments @{Action=$action;TargetRoot=$TargetRoot;BackupRoot=$mcpRuntimeBackupRoot} if(-not[bool]$result.passed){throw "MCP-Runtime-$action fehlgeschlagen."} - $validation=Invoke-KICompleteJsonScript -Script $entry -Arguments @{Action='Validate';TargetRoot=$TargetRoot} + $validation=Invoke-KICompleteJsonScriptIsolated -Script $entry -Arguments @{Action='Validate';TargetRoot=$TargetRoot} if(-not[bool]$validation.passed){throw 'MCP-Runtime-Validierung fehlgeschlagen.'} # SkippedAlreadyCompliant carries no backupPath -- defensive, never assumed # present under StrictMode, mirroring open-terminal's own identical comment. @@ -2308,7 +2385,7 @@ function Invoke-KIStackCompleteInstaller { # can re-sync this exact same object into components.json too -- otherwise this file would # permanently keep reporting "ValidatedExistingInstallation" while transaction.json already # correctly shows CompletedWithWarnings, two persisted state files disagreeing forever. - $componentState=[ordered]@{schemaVersion='1.0';status=if($tx.status-eq'Completed'){'ValidatedExistingInstallation'}else{$tx.status};completeInstallerVersion='2.18.2';validatedAtUtc=[DateTime]::UtcNow.ToString('o');components=$componentVersions;evidence=[ordered]@{optionalBallisticsEnabled=[bool]$EnableOpenWebUIBallistics;manualStartupOnly=$true;containsSecrets=$false;containsPersonalPaths=$false;pendingRollback=$pendingRollback}} + $componentState=[ordered]@{schemaVersion='1.0';status=if($tx.status-eq'Completed'){'ValidatedExistingInstallation'}else{$tx.status};completeInstallerVersion='2.19.0';validatedAtUtc=[DateTime]::UtcNow.ToString('o');components=$componentVersions;evidence=[ordered]@{optionalBallisticsEnabled=[bool]$EnableOpenWebUIBallistics;manualStartupOnly=$true;containsSecrets=$false;containsPersonalPaths=$false;pendingRollback=$pendingRollback}} Write-KICompleteJson $componentStatePath $componentState Write-KICompleteJson $txPath $tx # Commit boundary: both required Final-State writes above succeeded. From this point on, diff --git a/tools/complete-installer/current/Config/complete-installer.config.json b/tools/complete-installer/current/Config/complete-installer.config.json index 421db58..65cdc61 100644 --- a/tools/complete-installer/current/Config/complete-installer.config.json +++ b/tools/complete-installer/current/Config/complete-installer.config.json @@ -1,6 +1,6 @@ { "schemaVersion": "1.0", - "version": "2.18.2", + "version": "2.19.0", "targetRoot": "C:\\KI-Stack", "stateDirectory": "C:\\KI-Stack\\state\\complete-installer", "backupDirectory": "C:\\KI-Stack\\backups\\complete-installer", diff --git a/tools/complete-installer/current/Contracts/COMPONENTS.json b/tools/complete-installer/current/Contracts/COMPONENTS.json index 751b345..037b778 100644 --- a/tools/complete-installer/current/Contracts/COMPONENTS.json +++ b/tools/complete-installer/current/Contracts/COMPONENTS.json @@ -17,8 +17,8 @@ {"id":"codex-local","name":"Codex Local","version":"0.2.1","order":140,"source":"Payload/CodexLocal","marker":"modules/codex-local/installation.json","probe":{"type":"json","path":"modules/codex-local/installation.json","fields":["version"]},"kind":"local-intelligence","installable":true,"isolation":"A","isolationReason":"Self-contained Invoke-KIStackCodexLocal.ps1 with its own Install/Validate/Rollback actions.","requires":[],"isolatedExecutionImplemented":true,"versionSourceType":"own-version-file","packageIdentity":{"kind":"file","path":"tools/codex-local/current/VERSION"}}, {"id":"rag","name":"RAG","version":"0.4.0","order":150,"source":"Payload/RAG","marker":"modules/rag/installation.json","probe":{"type":"json","path":"modules/rag/installation.json","fields":["version"]},"kind":"local-intelligence","installable":true,"isolation":"B","isolationReason":"Self-contained Install-KICompleteRAGModule with its own BackupRoot contract (redeploys module files/env wiring only; never triggers ingestion, which remains a separate, manual, user-controlled action) -- but it reads and rewrites modules/integration/Start-KIStack-OpenWebUI-WithSearch.cmd and throws if that file is missing, so Integration must already be present on the target.","requires":["integration"],"isolatedExecutionImplemented":true,"versionSourceType":"own-version-file","packageIdentity":{"kind":"file","path":"tools/rag/current/VERSION"}}, {"id":"open-terminal","name":"Open Terminal","version":"0.1.1","order":160,"source":"Payload/OpenTerminal","marker":"modules/open-terminal/installation.json","probe":{"type":"json","path":"modules/open-terminal/installation.json","fields":["version"]},"kind":"component","installable":true,"isolation":"A","isolationReason":"Self-contained Invoke-KIStackOpenTerminal.ps1 with its own Install/Upgrade/Repair/Validate/Start/Stop/Rollback actions and its own backup/rollback.json contract, mirroring codex-local's own isolated shape.","requires":["python-git"],"isolatedExecutionImplemented":true,"versionSourceType":"own-version-file","packageIdentity":{"kind":"file","path":"tools/open-terminal/current/VERSION"}}, - {"id":"mcp-runtime","name":"KI-Stack MCP Runtime","version":"0.1.0","order":170,"source":"Payload/McpRuntime","marker":"modules/mcp-runtime/installation.json","probe":{"type":"json","path":"modules/mcp-runtime/installation.json","fields":["version"]},"kind":"component","installable":true,"isolation":"A","isolationReason":"Self-contained Invoke-KIStackMcpRuntime.ps1 with its own Install/Upgrade/Repair/Validate/Start/Stop/Rollback/Register/Unregister/Uninstall actions and its own backup/rollback.json contract, mirroring open-terminal's own isolated shape; Open-WebUI reachability plus an admin API token is a runtime precondition for the Register action only (like openwebui-visual-pack), never a COMPONENTS.json dependency.","requires":["python-git"],"isolatedExecutionImplemented":true,"versionSourceType":"own-version-file","packageIdentity":{"kind":"file","path":"tools/mcp-runtime/current/VERSION"}}, - {"id":"winapp","name":"KI-Stack WinApp Provisioning","version":"0.6.1","order":180,"source":"Payload/WinApp","marker":"tools/winapp/installation.json","probe":{"type":"text","path":"tools/winapp/VERSION"},"kind":"tool-binary","installable":true,"isolation":"A","isolationReason":"Self-contained Invoke-KIStackWinApp.ps1 with its own Install/Upgrade/Repair/Validate/Status/Resolve/Rollback actions and its own backup/rollback.json contract, mirroring open-terminal's and mcp-runtime's own isolated shape. Provisions Microsoft's standalone-x64 winapp CLI (upstream microsoft/winappCli v0.6.1) into /tools/winapp/current/ by downloading winappcli-x64.zip from the single pinned upstream release URL in tools/winapp/current/Manifests/winapp.source.manifest.json, verifying sizeBytes+sha256 before extraction, then extracting the full package unchanged. Introduces no services, ports or credentials; performs no GUI automation and no application launch.","requires":[],"isolatedExecutionImplemented":true,"versionSourceType":"own-version-file","packageIdentity":{"kind":"file","path":"tools/winapp/current/VERSION"},"externalArtifact":{"tool":"winapp","upstreamProject":"microsoft/winappCli","upstreamReleaseTag":"v0.6.1","distribution":"standalone-x64-zip","vendoredInGit":false,"sourceManifest":"tools/winapp/current/Manifests/winapp.source.manifest.json"}}, - {"id":"desktop-control","name":"KI-Stack Desktop Control","version":"0.1.0","order":190,"source":"Payload/DesktopControl","marker":"tools/desktop-control/installation.json","probe":{"type":"text","path":"tools/desktop-control/VERSION"},"kind":"component","installable":true,"isolation":"A","isolationReason":"Self-contained Invoke-KIStackDesktopControl.ps1 with its own Install/Upgrade/Repair/Validate/Status/Rollback actions and its own backup/rollback.json contract, mirroring winapp's and mcp-runtime's isolated shape. Deploys only the wrapper + policy + config + dispatcher + vendored WinApp resolver + tests/docs into /tools/desktop-control/current/ (parametric; standard install yields C:/KI-Stack/tools/desktop-control/). Introduces no runtime, port, credential or service. winapp stays a separate dependency (order 180, before this component) and is verified at runtime through the central WinApp resolver, never re-provisioned here. The isolated step runs -Action Validate right after Install, which probes that resolver (a single winapp --version call); no GUI automation.","requires":["winapp"],"isolatedExecutionImplemented":true,"versionSourceType":"own-version-file","packageIdentity":{"kind":"file","path":"tools/desktop-control/current/VERSION"}} + {"id":"winapp","name":"KI-Stack WinApp Provisioning","version":"0.6.1","order":170,"source":"Payload/WinApp","marker":"tools/winapp/installation.json","probe":{"type":"text","path":"tools/winapp/VERSION"},"kind":"tool-binary","installable":true,"isolation":"A","isolationReason":"Self-contained Invoke-KIStackWinApp.ps1 with its own Install/Upgrade/Repair/Validate/Status/Resolve/Rollback actions and its own backup/rollback.json contract, mirroring open-terminal's and mcp-runtime's own isolated shape. Provisions Microsoft's standalone-x64 winapp CLI (upstream microsoft/winappCli v0.6.1) into /tools/winapp/current/ by downloading winappcli-x64.zip from the single pinned upstream release URL in tools/winapp/current/Manifests/winapp.source.manifest.json, verifying sizeBytes+sha256 before extraction, then extracting the full package unchanged. Introduces no services, ports or credentials; performs no GUI automation and no application launch.","requires":[],"isolatedExecutionImplemented":true,"versionSourceType":"own-version-file","packageIdentity":{"kind":"file","path":"tools/winapp/current/VERSION"},"externalArtifact":{"tool":"winapp","upstreamProject":"microsoft/winappCli","upstreamReleaseTag":"v0.6.1","distribution":"standalone-x64-zip","vendoredInGit":false,"sourceManifest":"tools/winapp/current/Manifests/winapp.source.manifest.json"}}, + {"id":"desktop-control","name":"KI-Stack Desktop Control","version":"0.1.1","order":180,"source":"Payload/DesktopControl","marker":"tools/desktop-control/installation.json","probe":{"type":"text","path":"tools/desktop-control/VERSION"},"kind":"component","installable":true,"isolation":"A","isolationReason":"Self-contained Invoke-KIStackDesktopControl.ps1 with its own Install/Upgrade/Repair/Validate/Status/Rollback actions and its own backup/rollback.json contract, mirroring winapp's and mcp-runtime's isolated shape. Deploys only the wrapper + policy + config + dispatcher + vendored WinApp resolver + tests/docs into /tools/desktop-control/current/ (parametric; standard install yields C:/KI-Stack/tools/desktop-control/). Introduces no runtime, port, credential or service. winapp stays a separate dependency (order 170, before this component) and is verified at runtime through the central WinApp resolver, never re-provisioned here. The isolated step runs -Action Validate right after Install, which probes that resolver (a single winapp --version call); no GUI automation.","requires":["winapp"],"isolatedExecutionImplemented":true,"versionSourceType":"own-version-file","packageIdentity":{"kind":"file","path":"tools/desktop-control/current/VERSION"}}, + {"id":"mcp-runtime","name":"KI-Stack MCP Runtime","version":"0.2.0","order":190,"source":"Payload/McpRuntime","marker":"modules/mcp-runtime/installation.json","probe":{"type":"json","path":"modules/mcp-runtime/installation.json","fields":["version"]},"kind":"component","installable":true,"isolation":"A","isolationReason":"Self-contained Invoke-KIStackMcpRuntime.ps1 with its own Install/Upgrade/Repair/Validate/Start/Stop/Rollback/Register/Unregister/Uninstall actions and its own backup/rollback.json contract, mirroring open-terminal's own isolated shape; Open-WebUI reachability plus an admin API token is a runtime precondition for the Register action only (like openwebui-visual-pack), never a COMPONENTS.json dependency. Since 2.19 Phase 1 also deploys a persistent, source-parity-checked package tree into /tools/mcp-runtime/current/ (McpRuntime.psm1, Invoke-KIStackMcpRuntime.ps1, Config/, Scripts/ incl. the ki_desktop_control_tools.py ui_* MCP tool layer, Vendor/) alongside its pre-existing modules/mcp-runtime lifecycle bookkeeping -- mirrors winapp's/desktop-control's own persistent-package + Test-KI*SourceParity shape (Test-KIMcpRuntimeDeployed/Test-KIMcpRuntimeSourceParity in McpRuntime.psm1), so a changed payload at an unchanged component VERSION is reconciled, never silently skipped. Reordered after desktop-control (was order 170, before winapp/desktop-control) and now REQUIRES desktop-control (transitively winapp) because its own ki_desktop_control_tools.py ui_* MCP tool surface calls Desktop Control's dispatcher at a fixed path under \\tools\\desktop-control\\current\\ with no PATH fallback -- a target that provisions mcp-runtime before desktop-control would (structurally, not just typically) have that dispatcher path missing at every ui_* tool call until desktop-control is later installed.","requires":["python-git","desktop-control"],"isolatedExecutionImplemented":true,"versionSourceType":"own-version-file","packageIdentity":{"kind":"file","path":"tools/mcp-runtime/current/VERSION"}} ] } diff --git a/tools/complete-installer/current/Documentation/INSTALLATION.de.md b/tools/complete-installer/current/Documentation/INSTALLATION.de.md index ae05f46..e34de19 100644 --- a/tools/complete-installer/current/Documentation/INSTALLATION.de.md +++ b/tools/complete-installer/current/Documentation/INSTALLATION.de.md @@ -1,6 +1,6 @@ # Installation, Upgrade und Betrieb -1. Prüfe `KI-Stack-Complete-Installer-vX.Y.Z.zip` (aktuell `v2.18.2`) mit `Get-FileHash -Algorithm SHA256` gegen das danebenliegende `.sha256`-Sidecar. Ein SPDX-2.3-SBOM (`....spdx.json`) wird vom Build-Skript automatisch neben dem ZIP erzeugt; dessen eigene Root-Package-Prüfsumme stimmt exakt mit ZIP und Sidecar überein. Beide Build-Ergebnisse stammen aus einem deterministischen Build: ein zweimaliger Build desselben Quellbaums erzeugt ein bytegleiches ZIP (gleicher SHA256). +1. Prüfe `KI-Stack-Complete-Installer-vX.Y.Z.zip` (aktuell `v2.19.0`) mit `Get-FileHash -Algorithm SHA256` gegen das danebenliegende `.sha256`-Sidecar. Ein SPDX-2.3-SBOM (`....spdx.json`) wird vom Build-Skript automatisch neben dem ZIP erzeugt; dessen eigene Root-Package-Prüfsumme stimmt exakt mit ZIP und Sidecar überein. Beide Build-Ergebnisse stammen aus einem deterministischen Build: ein zweimaliger Build desselben Quellbaums erzeugt ein bytegleiches ZIP (gleicher SHA256). 2. Entpacke das Paket und starte `Start-KIStack-Installer.cmd` mit PowerShell 7. 3. Meldet der Installer nach einer erstmaligen WSL-Aktivierung `NEUSTART ERFORDERLICH` (Exitcode 31), starte Windows neu und führe `Resume-KIStack-Installer.cmd ` aus. Der Zustand `WaitingForRestart` ist fortsetzbar und löst keinen Rollback aus. 4. Bestätige UAC. Ist der OpenWebUI-Visual-Pack-Schritt noch nicht konform, öffnet sich OpenWebUI im Standardbrowser, sobald es und ComfyUI erreichbar sind: führe die Erstanmeldung durch (Admin-Konto anlegen) bzw. melde dich an, erzeuge unter Einstellungen -> Konto -> API-Keys einen neuen API-Key, kehre dann zum Installer zurück und bestätige mit Enter. Gib diesen Key anschließend verdeckt ein; er bleibt ausschließlich im Arbeitsspeicher als `SecureString`, wird nie gespeichert und soll danach widerrufen werden. Der bestehende Visual-Pack-Install-/Validate-Pfad läuft danach mit diesem Key weiter, und eine abschließende Abfrage verlangt genau einen echten Bild- und einen echten Videotest in OpenWebUI, bevor der Schritt akzeptiert wird. diff --git a/tools/complete-installer/current/Documentation/INSTALLATION.md b/tools/complete-installer/current/Documentation/INSTALLATION.md index f45fd2b..5f2e8a0 100644 --- a/tools/complete-installer/current/Documentation/INSTALLATION.md +++ b/tools/complete-installer/current/Documentation/INSTALLATION.md @@ -1,6 +1,6 @@ # Installation, upgrade, and operations -1. Verify the `KI-Stack-Complete-Installer-vX.Y.Z.zip` (currently `v2.18.2`) against its adjacent `.sha256` sidecar with `Get-FileHash -Algorithm SHA256`. An SPDX 2.3 SBOM (`....spdx.json`) is generated automatically alongside the ZIP by the build script; its own root-package checksum matches the ZIP and sidecar exactly. Both build outputs are the product of a deterministic build: building the same source tree twice produces a byte-identical ZIP (same SHA256). +1. Verify the `KI-Stack-Complete-Installer-vX.Y.Z.zip` (currently `v2.19.0`) against its adjacent `.sha256` sidecar with `Get-FileHash -Algorithm SHA256`. An SPDX 2.3 SBOM (`....spdx.json`) is generated automatically alongside the ZIP by the build script; its own root-package checksum matches the ZIP and sidecar exactly. Both build outputs are the product of a deterministic build: building the same source tree twice produces a byte-identical ZIP (same SHA256). 2. Extract the package and run `Start-KIStack-Installer.cmd` with PowerShell 7. 3. If first-time WSL activation reports `RESTART REQUIRED` (exit code 31), restart Windows and run `Resume-KIStack-Installer.cmd `. `WaitingForRestart` is resumable and does not trigger rollback. 4. Confirm UAC. If the OpenWebUI Visual Pack step is not yet compliant, OpenWebUI opens in your default browser once it and ComfyUI are reachable: complete first login (create the admin account) or sign in, generate an API key under Settings -> Account -> API Keys, then return to the installer and press Enter to continue. Enter that key through the hidden prompt; it is held only in memory as a `SecureString`, never stored, and should be revoked afterwards. The existing Visual Pack install/validate path then runs with that key, and a final prompt asks for one real image test and one real video test in OpenWebUI before the step is accepted. diff --git a/tools/complete-installer/current/MANIFEST.json b/tools/complete-installer/current/MANIFEST.json index 967537f..24d2c1e 100644 --- a/tools/complete-installer/current/MANIFEST.json +++ b/tools/complete-installer/current/MANIFEST.json @@ -2,9 +2,9 @@ "schemaVersion": "2.0", "packageId": "KI-STACK-COMPLETE-INSTALLER", "name": "KI-Stack Complete Installer", - "version": "2.18.2", - "status": "ComponentIsolation_VersionRegistry_ReleaseAttestation_CodexLocalIsolatedHome_OpenWebUICredentialBootstrap_ResearchAgentWebSearchValidation", - "baseVersion": "2.18.1", + "version": "2.19.0", + "status": "ComponentIsolation_VersionRegistry_ReleaseAttestation_CodexLocalIsolatedHome_OpenWebUICredentialBootstrap_ResearchAgentWebSearchValidation_McpRuntimePersistentPackageDelivery_DesktopControlMcpToolIntegration", + "baseVersion": "2.18.2", "components": { "comfyUI": "1.2.4", "visualModelsWorkflows": "2.0.3", diff --git a/tools/complete-installer/current/README.de.md b/tools/complete-installer/current/README.de.md index 59dc41f..015e0bc 100644 --- a/tools/complete-installer/current/README.de.md +++ b/tools/complete-installer/current/README.de.md @@ -1,8 +1,8 @@ -# KI-Stack Complete Installer 2.18.2 +# KI-Stack Complete Installer 2.19.0 -`KI-Stack-Complete-Installer-v2.18.2.zip` ist das aktuell veröffentlichte Complete-Installer-Paket. +`KI-Stack-Complete-Installer-v2.19.0.zip` ist das aktuell veröffentlichte Complete-Installer-Paket. -Das aktuelle Paket stellt den vollständig verwalteten KI-Stack-Stand bis Release 2.18.2 bereit. Die aktuelle Architektur umfasst: +Das aktuelle Paket stellt den vollständig verwalteten KI-Stack-Stand bis Release 2.19.0 bereit. Die aktuelle Architektur umfasst: - Open WebUI `0.11.3` als Referenz-/Mindestversion; unterstützte neuere Installationen bleiben erhalten und werden nie automatisch zurückgestuft. - ComfyUI `v0.34.0` als Referenz-/Mindestversion; unterstützte neuere Installationen bleiben erhalten. @@ -27,7 +27,7 @@ Memory-Policy des aktuellen Pakets: Der 2.17-Datenbankschutz umfasst Online-Backup von `webui.db` über SQLite `VACUUM INTO`, Integritätsprüfung, kontrolliertes Restore-Werkzeug, Sicherheitsbackup vor dem Restore, WAL-/SHM-Behandlung sowie Health-Verifikation nach dem Restore. -Validierungsaussagen bleiben nach Umfang getrennt. Die letzte vollständige physische Windows-Greenfield-Installation auf einem leeren Zielsystem wurde mit 2.4.0 durchgeführt. Die späteren Releases bis 2.18.2 ergänzen Repository-Regression, deterministische Paket-, Komponenten-, Upgrade-/Reconcile- und Real-Target-Acceptance-Nachweise, ohne einen neueren vollständigen Greenfield-Lauf zu behaupten. +Validierungsaussagen bleiben nach Umfang getrennt. Die letzte vollständige physische Windows-Greenfield-Installation auf einem leeren Zielsystem wurde mit 2.4.0 durchgeführt. Die späteren Releases bis 2.19.0 ergänzen Repository-Regression, deterministische Paket-, Komponenten-, Upgrade-/Reconcile- und Real-Target-Acceptance-Nachweise, ohne einen neueren vollständigen Greenfield-Lauf zu behaupten. - Heretic ist das einzige Chat-LLM. - Nomic dient ausschließlich Embeddings. - Z-Image verwendet nur `Qwen3-4b-Z-Image-Engineer-V4-Q8_0.gguf`. diff --git a/tools/complete-installer/current/README.md b/tools/complete-installer/current/README.md index 8b438e0..e10cb21 100644 --- a/tools/complete-installer/current/README.md +++ b/tools/complete-installer/current/README.md @@ -1,8 +1,8 @@ -# KI-Stack Complete Installer 2.18.2 +# KI-Stack Complete Installer 2.19.0 -`KI-Stack-Complete-Installer-v2.18.2.zip` is the current published Complete Installer package. +`KI-Stack-Complete-Installer-v2.19.0.zip` is the current published Complete Installer package. -The current package provides the complete managed KI-Stack state through release 2.18.2. Its current architecture includes: +The current package provides the complete managed KI-Stack state through release 2.19.0. Its current architecture includes: - Open WebUI `0.11.3` as reference/minimum supported version; supported newer installations are preserved and never automatically downgraded. - ComfyUI `v0.34.0` as reference/minimum supported version; supported newer installations are preserved. @@ -27,7 +27,7 @@ Memory policy in the current package: The 2.17 database-protection contract includes online `webui.db` backup using SQLite `VACUUM INTO`, integrity validation, controlled restore tooling, pre-restore safety backup, WAL/SHM handling, and post-restore health verification. -Validation claims remain scope-specific. The last complete physical empty-target Windows Greenfield installation was performed with 2.4.0. Later releases through 2.18.2 add repository regression, deterministic package, component, upgrade/reconcile, and real-target acceptance evidence without claiming a newer complete Greenfield run. +Validation claims remain scope-specific. The last complete physical empty-target Windows Greenfield installation was performed with 2.4.0. Later releases through 2.19.0 add repository regression, deterministic package, component, upgrade/reconcile, and real-target acceptance evidence without claiming a newer complete Greenfield run. - Heretic is the only chat LLM. - Nomic is embedding-only. - Z-Image uses only `Qwen3-4b-Z-Image-Engineer-V4-Q8_0.gguf`. diff --git a/tools/complete-installer/current/SHA256SUMS.txt b/tools/complete-installer/current/SHA256SUMS.txt index 4ce1f52..a9cbaa1 100644 --- a/tools/complete-installer/current/SHA256SUMS.txt +++ b/tools/complete-installer/current/SHA256SUMS.txt @@ -1,112 +1,113 @@ -97a8826b8019d6d4d721ea2ef3ba1bc2536341f5a7736d2a9ba572f998d5deb6 *Bootstrap-KIStackPowerShell7.ps1 -de6e56dcfacc75aeb60c811a4a78f14a6d80e20450800cb6531972b387c523b1 *BUILD-REPORT.json -c8097b05bfc10069beabab879acfdcedc2c56666c092445ba41fceff85bd67c6 *CompleteInstaller.psm1 -64c357e93e8a0e3f0a96bbb2f48890c0e6134a7a7f5b45c74375cbf60dec5698 *Config/complete-installer.config.json -cbc385b2f9ed4eed91d89b5bfa21b6f2cdb991117a7630de17ecbb610016cabe *Contracts/COMPONENTS.json -505acbc2d0a59346155795b3c09e8231f2d27d5eca5db9cee0a418ef85a07433 *Contracts/PAYLOADS.json -e8629f125f23ba150dd43e9d12593456fbcb5a02ea3b6e15f25e76babfb6e759 *Contracts/REQUIRED-PAYLOADS.json -b4e04fcda1728976c759221602cc3b45d175c9d7aa702b413078d262191428e8 *Contracts/RESUME.schema.json -6cab1813a0270c2e5818bfe4d7f84e3550de2822608b92113cc3c4749c9b7f06 *Contracts/ROLLBACK.md -370e861950fe0a54d7ad01ac31e94dc0c13fb94cf2ab4ee793c663d66df95471 *Contracts/TRANSACTION.schema.json -07c39d243b57ea12b7f6dd2532774f08cf9daf6f8d6be179995f0fd5db81aac3 *Documentation/INSTALLATION.de.md -a136c6764e62d22a1aa1e75cb4f88915c117a53386d8895e515e9b2704b3dff2 *Documentation/INSTALLATION.md -dde81298a0e195d99145c623787b66fc0d29fe972f72e446e6c5bbedad366274 *Documentation/MODEL-CONTRACT.md -97d4e2e6f350b28ffa31b6f5440c56168de78f8b4e5f1d3675c1e6e3c79962b4 *Documentation/MODELLVERTRAG.md -c7e7ab5810b6c6921cde04d82b7cbe9090e5badd4cd2eacb918bf879d0985406 *ExternalModels/LMStudio/README.md -14b4792766c964dd425e35c57781011fbc29e108434e309d2d9da2381a4a26c8 *ExternalModels/README.de.md -1a88fddf332d4098bb48fa34e98a26d1d6dff5167a3ddbe47e4b56b175ccff89 *ExternalModels/README.md -8b05637c8f95d331a8ae79e474d1ff2c0e7838f252c926603e9550ec9b6aad93 *Import-KIStackExternalModels.ps1 -59b5a9c10c23f26005e693e9759f50b9748e5c3acee9beb0a5796ef27481d194 *Initialize-KIStackOpenWebUICredential.ps1 -97f106c6f8ede9c324000e568b62e50daa1921fa2fd9c99eb078e6e5845f0917 *Invoke-KIStackCompleteInstaller.ps1 -00a02c8ce77de30b3c7ab22839fddb46ce840b1641076fe5fffa5ead63177fd8 *LICENSE -a49db20f32d4dfb423a6218b87092aa2566a53482ddeaad4665d214f3f692faf *Lifecycle/Get-KIStackStatus.ps1 -9116d50bdd42050b54f38dc434c4e811e591a4e62e7095d9c501d4183f440563 *Lifecycle/KIStackComponentVersionRegistry.psm1 -006a2609b88f620eb420f3d26fd72cdfde7349c0029fac1316e4b463741c5c3c *Lifecycle/KIStackOpenWebUICredential.psm1 -121550cfe3f2d991f8cf14762a9cfb462349c98882f2b067b3804f6d6e863fb8 *Lifecycle/KIStackUpdateIsolation.psm1 -aebdbb1f7e0ca705dc3b2471e6e889c3b626071ae0959515a9751d3a404cdf73 *Lifecycle/Repair-KIStack.cmd -33f5b25475767fa07dbdad165d3248bbe27d6d3d786c8ccaba27bce900617a05 *Lifecycle/Show-KIStackStatus.ps1 -967b6993b66ce4946ee8101f6ad941da7f7df324fb5beb80f9d4360340d4d325 *Lifecycle/Start-KIStack.cmd -34fcc06c39e337d524fa259d063a12d12c3957900c50c662d947696f54feaabf *Lifecycle/Status-KIStack-Interactive.cmd -10b0bef6f972d2a36e2f711c53ffc525da3326f2a1872a3e051a354a0f6cbee5 *Lifecycle/Stop-KIStack-Managed.ps1 -ea5aaf7547a30778b3e10372c674559a8c5898db5394e7f576d0a956cb7b97a5 *Lifecycle/Stop-KIStack.cmd -1f7cb6cd91224e9e7c6ff7245e3521c9a2bfb928cf8a97062429a8d0e1c76cc7 *Lifecycle/Update-KIStack-All.cmd -c9f883cdd00f3c64339967730e09afb0f83783c0e49c8a2193762ff25498b31c *Lifecycle/Update-KIStack-All.ps1 -ff12565b65c4f044c28a2e6645cb99685879368da4c96fb3bc3695a9f2120d94 *Lifecycle/Update-KIStack-OpenWebUI.cmd -02056de23c82405f51d9011e5bba220337c0395291a3731ea3f21063d71bc921 *Lifecycle/Update-KIStack-OpenWebUI.ps1 -ac38dc9a58f9c80e93d6706f59708a4539e65a9e4b425dc385030fdd7f5df495 *Lifecycle/Validate-KIStack.cmd -7a59a65f930ae03c1b7b4b982f967eccdd74ddb9859916a97483b6b3b6c4cfd2 *MANIFEST.json -a064fe3025b54c35092b76c254ea878d024790310b785731bf999062e3b12fdf *MODEL_LICENSES.md -4a861c929352823410db58673ade60a92ada8cb9acbb677f95d5362a214f8a3d *New-KIStackCompleteInstallerArchive.ps1 -ffd135c29f2ee79cfeb07e4d2a0ac92a3d52a7ddbd2d881dc551a36f7bf8efef *NOTICE -81dc6f12826e9678f007d0cc9b2a6ba8a255be91aa984edf7fd92c9a8d1d28c7 *Operations/Remove-KIStackKnowledgeExperiment.ps1 -72b82522a025703c660d2941d92b89b2664ed69fa8676e9d8ecf9f125a658dae *Operations/Remove-KIStackKnowledgeExperimentCollections.ps1 -e1e93de720202fd5df5d5f764b78a6a3cb2c503d8f0294c9368c45a861091bdb *Operations/Restore-KIStackCodeInterpreter.ps1 -8fce69bde07785dd48bec2229bca7b58ba8563c5f538c4bf17e79c130a5ef69c *Operations/Restore-KIStackKnowledgeExperiment.ps1 -df1f084f64a594f18df09d43c8ebf4eb2e7b2e31a557ad58acce3c9cfc1cdbe3 *Operations/Set-KIStackCodeInterpreter.ps1 -45aba63596f2dd8424d8f08408890f9a1931e94592de8535e313139f10b10145 *README.de.md -9f8f72ca1e883f9f9721fb5ba70f20a9b8866373a0eadc84a83242ea08fe4235 *README.md -cf6c29bd76fb98aeeb5f16937720c6a4070c06c049c3ce19e29825f721b59cf0 *Remove-KIStackOpenWebUICredential.ps1 -e7e88d8ba02af4a2ed11bc3bd3e802ec849e92493c3a9fad505b2dd9d9ca550f *Resume-KIStack-Installer.cmd -15723cb1750aec021d08946e0cc316906925306fb69055d33c820159a17dc0ff *Runtime/KIStackPathContext.psm1 -247ec57b9303b8265e061369d2c50ba9d8c59f40b1059854c179de8e68fa9f36 *Start-KIStack-Audit.cmd -578c24fb219527415682f88da825b0050e5559fa9b52ad9c62df73e52d227874 *Start-KIStack-Complete-Installer-SelfTest.cmd -3f731ca49aca4af50ed4b6b678b3e6d5a5f705466a0f198ff0c6ac52bd387589 *Start-KIStack-DryRun.cmd -728ea2055179330a5767450a0b736225e6d3aa298b5aad7bb14e19fa1a5793fa *Start-KIStack-Installer.cmd -8b309600a852f4951db4e17c7f28c35feda5020f5d074239c2c1cd5df476f4cc *Start-KIStack-Model-Import.cmd -e1d856daed3b6ed9083e25f8e749a96f588a7694262637b3e28defe91e165b55 *Start-KIStack-Repair.cmd -0dd02f49ffcc557eb57c40da3cfbcfbadc6e9a42b57b563038814c47725792ec *Start-KIStack-Rollback.cmd -c5cc2dc25a1e3614001a01b7d3f1e721363b0692c8658eeafe1a2e88296fc4a2 *Start-KIStack-Validate.cmd -7ce2c17528cfb5e810ba2a042d4916c249d6e11b4fc536b8f850abe942c09b59 *Start-KIStack.cmd -61558797c46f32f57c6ad6e2a6e8a9487691a911d9e6d10f24cdc5bc30fea081 *Start-KIStackCompleteInstaller.ps1 -6d3e883485259a9b94e855e1c0989bd6d574508235e516c500bb17254ba17e69 *Status-KIStack.cmd -f11720db144256dd59d0e9e5431b03a4132979c06f6c051f49a763ac4f2a6261 *Stop-KIStack.cmd -be1b2a8b66f611fc243121a4f4ed42714d5d6d7904423163ee7b0856a549b051 *Test-KIStackApplicationsUpgradePlan.ps1 -bce620a101833f9f61482c28a48a6ff13a96d11113fa6b425fbfb167ca5b784f *Test-KIStackBootstrapLogging.ps1 -63b15c7421880ecc25f78b3e9ac9d49326e54dbb22ca473a541eb0d67b40e87c *Test-KIStackComfyUICompliance.ps1 -0e3039ce3cb82d522848615d43108ed7471f782f12547c7e631a4ed19c4e188d *Test-KIStackComfyUIOverlayProtection.ps1 -1b0db54af7ec6d2c4debe5239cc163cc765ce5d1a5024d6ff6a40345f3712d6c *Test-KIStackCompleteInstaller.ps1 -cde2fe7802e9beeb69c32e937584b024e6eae8e5dd91bbb91133fd480aed73b4 *Test-KIStackCompleteInstallerPathContext.ps1 -b4ce10cb2c79d666c5d9a769fc43b8dcd5ad4772911eed69ff2a235525ceedf9 *Test-KIStackCompleteInstallerTarget.ps1 -29477b1539fccc6b344316c512d1099e7a0308dfdf24d3b0bc06b4b9e25b3de8 *Test-KIStackComponentVersionRegistry.ps1 -a86bcc6e9856f3e4285b1494b0cd77f7a84a5a7f0e11f1de961fd8cc3a1e556f *Test-KIStackDesktopControlLifecycleIsolation.ps1 -9171c8529d50fb0fb7b9b953635b2755d9d3a19def5f8456f0e019b071a82edd *Test-KIStackDesktopControlReconcile.ps1 -6d2d0bbb8606f61579814502b1daa05f41b01d168198896534f8353187787b5c *Test-KIStackDesktopLinkIsolation.ps1 -b801f565595939fd4ddc389fb8c70bc6234b535cf77a56e97b675c85bfe09d27 *Test-KIStackExitCodePropagation.ps1 -4a32aa757851df923905077686179a34f2a67fe5c64b15163ddff86d461707bb *Test-KIStackFailedTransactionComfyUIRecovery.ps1 -d54c454d2564a8c5a5266834b1264af6380fe86f1dcd118b0e5a69a6bf22344d *Test-KIStackFinalizationRollback.ps1 -e86ce83ce6061cc6f800ef221c3a17cb52ca3c0309ea3c3e3b6ce6359a6e24cd *Test-KIStackInstallationContracts.ps1 -6c4c577935906ea17175bd64d90683b401b35f18dd135aa5b286a31b4d97016a *Test-KIStackInstallerHeartbeat.ps1 -1e498bd6362f98b574ffeaadaa06cbc0758846112015a07d6845ff57bf0c69ba *Test-KIStackIntegrationCutoverRuntimeUpgradePlan.ps1 -276134710bb962a5e4c76c26aabbdc00472b11a1a91baaa851832d3ec584782b *Test-KIStackLifecycleTargetRootIsolation.ps1 -9caafe2e0764b11f18fc02ae51da9ac2beb3bab9f36013b7312eb783721f61c6 *Test-KIStackMcpRuntimeCompleteInstallerIntegration.ps1 -9ec138657b6c65ba2abba3f0dd848d7db08d1da1ba896079d0577f17593f7252 *Test-KIStackMcpRuntimeLifecycleWiring.ps1 -841e3a4ed5fda36c9d4f4820ff77f37adcaacd4288c29dbc6b40f34432b7b5cd *Test-KIStackMcpRuntimeSeededPrimaryFlow.ps1 -313ee3f7654b54abcba522a49efd097fe885c4499a96a49255ec4e65626f996e *Test-KIStackModeRollbackOperations.ps1 -6bec1e67ce1ab6a1627f0c9bae08cebb2d8c867f40eb75f5026cf6a06882a024 *Test-KIStackOpenTerminalCompleteInstallerIntegration.ps1 -c1a2105fb87d26e2e4cb35695cd6a0ddf119e38510cc0bbf8dab35e82e30d3dd *Test-KIStackOpenTerminalCutoverRuntimeUpgradePlan.ps1 -5c4da010127b74c5ec6bb1ee7310227f68014a384ee248c31ff1c2ddf231dd19 *Test-KIStackOpenTerminalLifecycleWiring.ps1 -7c272d054321438b7227601a3fb926e6fe369b7044e286a354fdbd608e55a86a *Test-KIStackOpenWebUICredential.ps1 -2811446fd7cd0127bed5f02f9392e0e5f35ace646fb1abc8a5ddde64ddb8e5b0 *Test-KIStackOpenWebUICredentialBootstrap.ps1 -bf57600004c2abf152bd9a181ef72860a521e347b410a26bbb73d3760fcccbde *Test-KIStackOpenWebUIManagedUpdate.ps1 -bd576fcaa76576685aaabbc959e7244b57da3465e89c278bd7e04ea5bcf841d2 *Test-KIStackOpenWebUIVisualPackCutover.ps1 -f000b062a0306bb4b3967585b60a2ef414031a0d8c034fd7d1fe7a180f3be4f6 *Test-KIStackPathContext.ps1 -cea0192b6fda80c3b3260ddd234d06cf55d88079a131d34788f9e3fc3a64e75e *Test-KIStackPayloadDeploymentHygiene.ps1 -3cce9024e297b71c32411206fa0d25c61bb3d3670c3025d2fe8aa9e1311a58b1 *Test-KIStackPinnedReferenceReconciliation.ps1 -79443a3c16145a787bf335b8fa7c724dbd1598268ebd4d703bda000b0ef56538 *Test-KIStackPrimaryInstallerComponentParity.ps1 -6f0e02b589f5668e821282dac7c02ee70cf0367d94d692a718e5aca9522ba9f2 *Test-KIStackRAGPrefixIntegration.ps1 -c1d9ab4321310fa15169b17964b9e5a97fa5af4678221bc98b37c538128285b0 *Test-KIStackReplayComponent.ps1 -d3e9bf8b4a58523df595cb33dd363c3aff1fc4e09cf3bd608471dc268de371cb *Test-KIStackRequiredPayloads.ps1 -7ac86b38d787d77dc8cf9d11ad78581c07985d8cce79d5357106dca6e5afa0b4 *Test-KIStackStatusCodexDetection.ps1 -c5196727e7c2c3204db5bbbe0205387fa63d7076cead21707c729ff1509862d8 *Test-KIStackStatusOpenTerminalDetection.ps1 -fbf7dbae50b8cbb89e36ab473d5e31d4a56eb7dceff3fa97df1d926bab969b87 *Test-KIStackStatusWslKeeperDetection.ps1 -03f6b9627f275c2ba87e08fc032086272ffc3367d00d3ec22de04ecaf9bfc76e *Test-KIStackTransactionErrorLifecycle.ps1 -eaa0c0c62b546d1da7d3e9eec773da5df31098d5ccc80f075d7cde02767af1b7 *Test-KIStackUpdateAll.ps1 -589188cae1191aba7517cc4ae943396f28bf34a74e6bf06f4e701bf405362915 *Test-KIStackUpdateIsolation.ps1 -943319ad2b40161872b8201ccb1a8b670380896918fd6e1bf21940ba8dc71b3e *Test-RC12PendingComfyRollback.ps1 -9304b220ea906945184bed7ad83e7b21d215a98f373d82a5c760ce6aa3a6a1b9 *Test-RC13FailedStateRecovery.ps1 -c6bf97d58e967a32fd803ac4f2f63e156b6290a7d6640d12e063c2d3130f6a03 *THIRD_PARTY_NOTICES.md -503eaf83bebacb2c1895ade36d1014fd7f87f2aee67a7c1272092d268d5e9ead *VALIDATION-REPORT.json -ad712257b4a0ad48b87dcda9762f3be36ced4caf0112c9a4727f54de5d994827 *Validation/REGRESSION-COVERAGE.json -e8ff8093b62dba33beeff3f0ff6ec6ce6d1731ef57b2684da9ab62ce2f57d629 *Validation/TEST-PHASES.json -f12d38f421cab709ea6aee4e3c63c9ed6e449025376d34223aeba8068a1f2763 *Validation/VALIDATION-CONTRACT.json -8242edd45b1dd44760824c235b087bc5721bfb8e2a5a82bf8a003b370e5fffc9 *VERSION +97a8826b8019d6d4d721ea2ef3ba1bc2536341f5a7736d2a9ba572f998d5deb6 *Bootstrap-KIStackPowerShell7.ps1 +de6e56dcfacc75aeb60c811a4a78f14a6d80e20450800cb6531972b387c523b1 *BUILD-REPORT.json +629dcef3a166e222f5cfdfe19adb04d0d8325ec667365de06c88bec5d641f297 *CompleteInstaller.psm1 +ca4c28c0e5d7ed8a99813925bfe4e19e86b853d45e389a7a6f314eaa00f92ef0 *Config/complete-installer.config.json +3e1a1587472271901e6b8d7612857ba7bf30b8e5e22fd634f5d0f681d5700469 *Contracts/COMPONENTS.json +505acbc2d0a59346155795b3c09e8231f2d27d5eca5db9cee0a418ef85a07433 *Contracts/PAYLOADS.json +e8629f125f23ba150dd43e9d12593456fbcb5a02ea3b6e15f25e76babfb6e759 *Contracts/REQUIRED-PAYLOADS.json +b4e04fcda1728976c759221602cc3b45d175c9d7aa702b413078d262191428e8 *Contracts/RESUME.schema.json +6cab1813a0270c2e5818bfe4d7f84e3550de2822608b92113cc3c4749c9b7f06 *Contracts/ROLLBACK.md +370e861950fe0a54d7ad01ac31e94dc0c13fb94cf2ab4ee793c663d66df95471 *Contracts/TRANSACTION.schema.json +44f8242271769b63f5b8811871df0436f16cc65267fc42e56355c5cb307a59ca *Documentation/INSTALLATION.de.md +3367e7ccb7fa97425610197d262b2de162789025b13c1c1a09f6319942d13ba6 *Documentation/INSTALLATION.md +dde81298a0e195d99145c623787b66fc0d29fe972f72e446e6c5bbedad366274 *Documentation/MODEL-CONTRACT.md +97d4e2e6f350b28ffa31b6f5440c56168de78f8b4e5f1d3675c1e6e3c79962b4 *Documentation/MODELLVERTRAG.md +c7e7ab5810b6c6921cde04d82b7cbe9090e5badd4cd2eacb918bf879d0985406 *ExternalModels/LMStudio/README.md +14b4792766c964dd425e35c57781011fbc29e108434e309d2d9da2381a4a26c8 *ExternalModels/README.de.md +1a88fddf332d4098bb48fa34e98a26d1d6dff5167a3ddbe47e4b56b175ccff89 *ExternalModels/README.md +8b05637c8f95d331a8ae79e474d1ff2c0e7838f252c926603e9550ec9b6aad93 *Import-KIStackExternalModels.ps1 +59b5a9c10c23f26005e693e9759f50b9748e5c3acee9beb0a5796ef27481d194 *Initialize-KIStackOpenWebUICredential.ps1 +97f106c6f8ede9c324000e568b62e50daa1921fa2fd9c99eb078e6e5845f0917 *Invoke-KIStackCompleteInstaller.ps1 +00a02c8ce77de30b3c7ab22839fddb46ce840b1641076fe5fffa5ead63177fd8 *LICENSE +a49db20f32d4dfb423a6218b87092aa2566a53482ddeaad4665d214f3f692faf *Lifecycle/Get-KIStackStatus.ps1 +9116d50bdd42050b54f38dc434c4e811e591a4e62e7095d9c501d4183f440563 *Lifecycle/KIStackComponentVersionRegistry.psm1 +006a2609b88f620eb420f3d26fd72cdfde7349c0029fac1316e4b463741c5c3c *Lifecycle/KIStackOpenWebUICredential.psm1 +121550cfe3f2d991f8cf14762a9cfb462349c98882f2b067b3804f6d6e863fb8 *Lifecycle/KIStackUpdateIsolation.psm1 +aebdbb1f7e0ca705dc3b2471e6e889c3b626071ae0959515a9751d3a404cdf73 *Lifecycle/Repair-KIStack.cmd +33f5b25475767fa07dbdad165d3248bbe27d6d3d786c8ccaba27bce900617a05 *Lifecycle/Show-KIStackStatus.ps1 +967b6993b66ce4946ee8101f6ad941da7f7df324fb5beb80f9d4360340d4d325 *Lifecycle/Start-KIStack.cmd +34fcc06c39e337d524fa259d063a12d12c3957900c50c662d947696f54feaabf *Lifecycle/Status-KIStack-Interactive.cmd +10b0bef6f972d2a36e2f711c53ffc525da3326f2a1872a3e051a354a0f6cbee5 *Lifecycle/Stop-KIStack-Managed.ps1 +ea5aaf7547a30778b3e10372c674559a8c5898db5394e7f576d0a956cb7b97a5 *Lifecycle/Stop-KIStack.cmd +1f7cb6cd91224e9e7c6ff7245e3521c9a2bfb928cf8a97062429a8d0e1c76cc7 *Lifecycle/Update-KIStack-All.cmd +c9f883cdd00f3c64339967730e09afb0f83783c0e49c8a2193762ff25498b31c *Lifecycle/Update-KIStack-All.ps1 +ff12565b65c4f044c28a2e6645cb99685879368da4c96fb3bc3695a9f2120d94 *Lifecycle/Update-KIStack-OpenWebUI.cmd +02056de23c82405f51d9011e5bba220337c0395291a3731ea3f21063d71bc921 *Lifecycle/Update-KIStack-OpenWebUI.ps1 +ac38dc9a58f9c80e93d6706f59708a4539e65a9e4b425dc385030fdd7f5df495 *Lifecycle/Validate-KIStack.cmd +4f04649a473fc893ca98017871ffb31e838d571d83c16976662b060688569372 *MANIFEST.json +a064fe3025b54c35092b76c254ea878d024790310b785731bf999062e3b12fdf *MODEL_LICENSES.md +4a861c929352823410db58673ade60a92ada8cb9acbb677f95d5362a214f8a3d *New-KIStackCompleteInstallerArchive.ps1 +ffd135c29f2ee79cfeb07e4d2a0ac92a3d52a7ddbd2d881dc551a36f7bf8efef *NOTICE +81dc6f12826e9678f007d0cc9b2a6ba8a255be91aa984edf7fd92c9a8d1d28c7 *Operations/Remove-KIStackKnowledgeExperiment.ps1 +72b82522a025703c660d2941d92b89b2664ed69fa8676e9d8ecf9f125a658dae *Operations/Remove-KIStackKnowledgeExperimentCollections.ps1 +e1e93de720202fd5df5d5f764b78a6a3cb2c503d8f0294c9368c45a861091bdb *Operations/Restore-KIStackCodeInterpreter.ps1 +8fce69bde07785dd48bec2229bca7b58ba8563c5f538c4bf17e79c130a5ef69c *Operations/Restore-KIStackKnowledgeExperiment.ps1 +df1f084f64a594f18df09d43c8ebf4eb2e7b2e31a557ad58acce3c9cfc1cdbe3 *Operations/Set-KIStackCodeInterpreter.ps1 +13f7fd3e0d1272e8b4a14536bd78f4445a6dee4dffe82e32eac10dacf5a59b09 *README.de.md +f301f99f5ddb42e80f3948a7207817b8c7f76c0b0ed5624911c18bc1a1cb518a *README.md +cf6c29bd76fb98aeeb5f16937720c6a4070c06c049c3ce19e29825f721b59cf0 *Remove-KIStackOpenWebUICredential.ps1 +e7e88d8ba02af4a2ed11bc3bd3e802ec849e92493c3a9fad505b2dd9d9ca550f *Resume-KIStack-Installer.cmd +15723cb1750aec021d08946e0cc316906925306fb69055d33c820159a17dc0ff *Runtime/KIStackPathContext.psm1 +247ec57b9303b8265e061369d2c50ba9d8c59f40b1059854c179de8e68fa9f36 *Start-KIStack-Audit.cmd +578c24fb219527415682f88da825b0050e5559fa9b52ad9c62df73e52d227874 *Start-KIStack-Complete-Installer-SelfTest.cmd +3f731ca49aca4af50ed4b6b678b3e6d5a5f705466a0f198ff0c6ac52bd387589 *Start-KIStack-DryRun.cmd +728ea2055179330a5767450a0b736225e6d3aa298b5aad7bb14e19fa1a5793fa *Start-KIStack-Installer.cmd +8b309600a852f4951db4e17c7f28c35feda5020f5d074239c2c1cd5df476f4cc *Start-KIStack-Model-Import.cmd +e1d856daed3b6ed9083e25f8e749a96f588a7694262637b3e28defe91e165b55 *Start-KIStack-Repair.cmd +0dd02f49ffcc557eb57c40da3cfbcfbadc6e9a42b57b563038814c47725792ec *Start-KIStack-Rollback.cmd +c5cc2dc25a1e3614001a01b7d3f1e721363b0692c8658eeafe1a2e88296fc4a2 *Start-KIStack-Validate.cmd +7ce2c17528cfb5e810ba2a042d4916c249d6e11b4fc536b8f850abe942c09b59 *Start-KIStack.cmd +61558797c46f32f57c6ad6e2a6e8a9487691a911d9e6d10f24cdc5bc30fea081 *Start-KIStackCompleteInstaller.ps1 +6d3e883485259a9b94e855e1c0989bd6d574508235e516c500bb17254ba17e69 *Status-KIStack.cmd +f11720db144256dd59d0e9e5431b03a4132979c06f6c051f49a763ac4f2a6261 *Stop-KIStack.cmd +be1b2a8b66f611fc243121a4f4ed42714d5d6d7904423163ee7b0856a549b051 *Test-KIStackApplicationsUpgradePlan.ps1 +bce620a101833f9f61482c28a48a6ff13a96d11113fa6b425fbfb167ca5b784f *Test-KIStackBootstrapLogging.ps1 +63b15c7421880ecc25f78b3e9ac9d49326e54dbb22ca473a541eb0d67b40e87c *Test-KIStackComfyUICompliance.ps1 +0e3039ce3cb82d522848615d43108ed7471f782f12547c7e631a4ed19c4e188d *Test-KIStackComfyUIOverlayProtection.ps1 +4c8676a05d7c5f0121a0ae40e06c5b2f7aa25af6c21650e6cdef2d74d665c9df *Test-KIStackCompleteInstaller.ps1 +cde2fe7802e9beeb69c32e937584b024e6eae8e5dd91bbb91133fd480aed73b4 *Test-KIStackCompleteInstallerPathContext.ps1 +b4ce10cb2c79d666c5d9a769fc43b8dcd5ad4772911eed69ff2a235525ceedf9 *Test-KIStackCompleteInstallerTarget.ps1 +29477b1539fccc6b344316c512d1099e7a0308dfdf24d3b0bc06b4b9e25b3de8 *Test-KIStackComponentVersionRegistry.ps1 +a86bcc6e9856f3e4285b1494b0cd77f7a84a5a7f0e11f1de961fd8cc3a1e556f *Test-KIStackDesktopControlLifecycleIsolation.ps1 +9171c8529d50fb0fb7b9b953635b2755d9d3a19def5f8456f0e019b071a82edd *Test-KIStackDesktopControlReconcile.ps1 +6d2d0bbb8606f61579814502b1daa05f41b01d168198896534f8353187787b5c *Test-KIStackDesktopLinkIsolation.ps1 +b801f565595939fd4ddc389fb8c70bc6234b535cf77a56e97b675c85bfe09d27 *Test-KIStackExitCodePropagation.ps1 +4a32aa757851df923905077686179a34f2a67fe5c64b15163ddff86d461707bb *Test-KIStackFailedTransactionComfyUIRecovery.ps1 +d54c454d2564a8c5a5266834b1264af6380fe86f1dcd118b0e5a69a6bf22344d *Test-KIStackFinalizationRollback.ps1 +e86ce83ce6061cc6f800ef221c3a17cb52ca3c0309ea3c3e3b6ce6359a6e24cd *Test-KIStackInstallationContracts.ps1 +6c4c577935906ea17175bd64d90683b401b35f18dd135aa5b286a31b4d97016a *Test-KIStackInstallerHeartbeat.ps1 +1e498bd6362f98b574ffeaadaa06cbc0758846112015a07d6845ff57bf0c69ba *Test-KIStackIntegrationCutoverRuntimeUpgradePlan.ps1 +276134710bb962a5e4c76c26aabbdc00472b11a1a91baaa851832d3ec584782b *Test-KIStackLifecycleTargetRootIsolation.ps1 +9caafe2e0764b11f18fc02ae51da9ac2beb3bab9f36013b7312eb783721f61c6 *Test-KIStackMcpRuntimeCompleteInstallerIntegration.ps1 +9ec138657b6c65ba2abba3f0dd848d7db08d1da1ba896079d0577f17593f7252 *Test-KIStackMcpRuntimeLifecycleWiring.ps1 +c98a8736bc5c38d4ee463f9765ffc7d5608c9721392989bb40cd60ea512476a5 *Test-KIStackMcpRuntimePayloadParity.ps1 +e1ea327a6a5c7bf01068fd1c542804b0cfba83beb45541353a7da4fe8e3960b4 *Test-KIStackMcpRuntimeSeededPrimaryFlow.ps1 +313ee3f7654b54abcba522a49efd097fe885c4499a96a49255ec4e65626f996e *Test-KIStackModeRollbackOperations.ps1 +6bec1e67ce1ab6a1627f0c9bae08cebb2d8c867f40eb75f5026cf6a06882a024 *Test-KIStackOpenTerminalCompleteInstallerIntegration.ps1 +c1a2105fb87d26e2e4cb35695cd6a0ddf119e38510cc0bbf8dab35e82e30d3dd *Test-KIStackOpenTerminalCutoverRuntimeUpgradePlan.ps1 +5c4da010127b74c5ec6bb1ee7310227f68014a384ee248c31ff1c2ddf231dd19 *Test-KIStackOpenTerminalLifecycleWiring.ps1 +7c272d054321438b7227601a3fb926e6fe369b7044e286a354fdbd608e55a86a *Test-KIStackOpenWebUICredential.ps1 +2811446fd7cd0127bed5f02f9392e0e5f35ace646fb1abc8a5ddde64ddb8e5b0 *Test-KIStackOpenWebUICredentialBootstrap.ps1 +bf57600004c2abf152bd9a181ef72860a521e347b410a26bbb73d3760fcccbde *Test-KIStackOpenWebUIManagedUpdate.ps1 +bd576fcaa76576685aaabbc959e7244b57da3465e89c278bd7e04ea5bcf841d2 *Test-KIStackOpenWebUIVisualPackCutover.ps1 +f000b062a0306bb4b3967585b60a2ef414031a0d8c034fd7d1fe7a180f3be4f6 *Test-KIStackPathContext.ps1 +cea0192b6fda80c3b3260ddd234d06cf55d88079a131d34788f9e3fc3a64e75e *Test-KIStackPayloadDeploymentHygiene.ps1 +3cce9024e297b71c32411206fa0d25c61bb3d3670c3025d2fe8aa9e1311a58b1 *Test-KIStackPinnedReferenceReconciliation.ps1 +79443a3c16145a787bf335b8fa7c724dbd1598268ebd4d703bda000b0ef56538 *Test-KIStackPrimaryInstallerComponentParity.ps1 +6f0e02b589f5668e821282dac7c02ee70cf0367d94d692a718e5aca9522ba9f2 *Test-KIStackRAGPrefixIntegration.ps1 +c1d9ab4321310fa15169b17964b9e5a97fa5af4678221bc98b37c538128285b0 *Test-KIStackReplayComponent.ps1 +d3e9bf8b4a58523df595cb33dd363c3aff1fc4e09cf3bd608471dc268de371cb *Test-KIStackRequiredPayloads.ps1 +7ac86b38d787d77dc8cf9d11ad78581c07985d8cce79d5357106dca6e5afa0b4 *Test-KIStackStatusCodexDetection.ps1 +c5196727e7c2c3204db5bbbe0205387fa63d7076cead21707c729ff1509862d8 *Test-KIStackStatusOpenTerminalDetection.ps1 +fbf7dbae50b8cbb89e36ab473d5e31d4a56eb7dceff3fa97df1d926bab969b87 *Test-KIStackStatusWslKeeperDetection.ps1 +03f6b9627f275c2ba87e08fc032086272ffc3367d00d3ec22de04ecaf9bfc76e *Test-KIStackTransactionErrorLifecycle.ps1 +eaa0c0c62b546d1da7d3e9eec773da5df31098d5ccc80f075d7cde02767af1b7 *Test-KIStackUpdateAll.ps1 +589188cae1191aba7517cc4ae943396f28bf34a74e6bf06f4e701bf405362915 *Test-KIStackUpdateIsolation.ps1 +943319ad2b40161872b8201ccb1a8b670380896918fd6e1bf21940ba8dc71b3e *Test-RC12PendingComfyRollback.ps1 +9304b220ea906945184bed7ad83e7b21d215a98f373d82a5c760ce6aa3a6a1b9 *Test-RC13FailedStateRecovery.ps1 +c6bf97d58e967a32fd803ac4f2f63e156b6290a7d6640d12e063c2d3130f6a03 *THIRD_PARTY_NOTICES.md +503eaf83bebacb2c1895ade36d1014fd7f87f2aee67a7c1272092d268d5e9ead *VALIDATION-REPORT.json +fa80f827e6d5a64938a0afdf7333692e92625096cf2c82a82a97fd16fdd2dddd *Validation/REGRESSION-COVERAGE.json +e8ff8093b62dba33beeff3f0ff6ec6ce6d1731ef57b2684da9ab62ce2f57d629 *Validation/TEST-PHASES.json +631461aa2f4ce2e5bcd1b90d3a1a907a9cb806d4b01a946ac38266cc321c9543 *Validation/VALIDATION-CONTRACT.json +c5f1cbaf7d662d64f2e1654127e2a35aea10cb37d7601aaf4b004ea020786d54 *VERSION diff --git a/tools/complete-installer/current/Test-KIStackCompleteInstaller.ps1 b/tools/complete-installer/current/Test-KIStackCompleteInstaller.ps1 index 1325802..a730a8b 100644 --- a/tools/complete-installer/current/Test-KIStackCompleteInstaller.ps1 +++ b/tools/complete-installer/current/Test-KIStackCompleteInstaller.ps1 @@ -15,7 +15,7 @@ foreach($marker in @('KI-Stack-Installer-output.txt','Start-KIStackCompleteInsta if(-not$executeStarter.Contains($marker)){$fail.Add("Execute starter contract: $marker")} } -if ($manifest.version -ne '2.18.2' -or $manifest.baseVersion -ne '2.18.1') { $fail.Add('Version contract') } +if ($manifest.version -ne '2.19.0' -or $manifest.baseVersion -ne '2.18.2') { $fail.Add('Version contract') } if ($payloads.modelPolicy.chatModels.Count -ne 1 -or $payloads.modelPolicy.chatModels[0] -ne 'qwen3.6-27b-uncensored-heretic-v2-native-mtp-preserved') { $fail.Add('Heretic chat-only contract') } if ($payloads.modelPolicy.nomicRole -ne 'embedding-only' -or $payloads.modelPolicy.embeddingModels.Count -ne 1) { $fail.Add('Nomic embedding-only contract') } if ([string]$payloads.modelContractAuthority.packagedArchive -ne 'Payload/ModelsWorkflows/KI-Stack-Visual-Models-Workflows-v2.0.3.zip') { $fail.Add('Authoritative model contract') } @@ -355,7 +355,7 @@ if ($syntaxErrors.Count) { $fail.Add('PowerShell syntax') } [pscustomobject]@{ passed = ($fail.Count -eq 0) - version = '2.18.2' + version = '2.19.0' checks = 28 failures = $fail } | ConvertTo-Json -Depth 10 diff --git a/tools/complete-installer/current/Test-KIStackMcpRuntimePayloadParity.ps1 b/tools/complete-installer/current/Test-KIStackMcpRuntimePayloadParity.ps1 new file mode 100644 index 0000000..78298a5 --- /dev/null +++ b/tools/complete-installer/current/Test-KIStackMcpRuntimePayloadParity.ps1 @@ -0,0 +1,148 @@ +[CmdletBinding()] +param([string]$PackageRoot = $PSScriptRoot) + +Set-StrictMode -Version Latest +$ErrorActionPreference = 'Stop' + +# Direct, isolated tests of the REAL Test-KICompleteMcpRuntimePayloadParity (CompleteInstaller.psm1) +# -- imported and called as-is, never copied or reimplemented. Builds real Payload/McpRuntime/*.zip +# archives (this test's own throwaway ones, under a scratch installer root -- NEVER the real +# tools/complete-installer/current/Payload, and never left in the working tree) plus real deployed +# tools/mcp-runtime/current-shaped target directories, mirroring +# Test-KIStackMcpRuntimeCompleteInstallerIntegration.ps1's own real-zip style. + +Import-Module (Join-Path $PackageRoot 'CompleteInstaller.psm1') -Force +$mcpRuntimeSourceRoot = [IO.Path]::GetFullPath((Join-Path $PackageRoot '..\..\mcp-runtime\current')) +Import-Module (Join-Path $mcpRuntimeSourceRoot 'McpRuntime.psm1') -Force + +$fail = [Collections.Generic.List[string]]::new() +$checks = [ordered]@{} +$scratchBase = Join-Path ([IO.Path]::GetTempPath()) ('KIMRParity-' + [guid]::NewGuid().ToString('N').Substring(0, 10)) +New-Item -ItemType Directory -Path $scratchBase -Force | Out-Null + +function New-KIMRParityZip { + param([Parameter(Mandatory)][string]$SourceDir, [Parameter(Mandatory)][string]$ZipPath) + New-Item -ItemType Directory -Path (Split-Path -Parent $ZipPath) -Force | Out-Null + if (Test-Path -LiteralPath $ZipPath) { Remove-Item -LiteralPath $ZipPath -Force } + Add-Type -AssemblyName System.IO.Compression + $stream = [IO.File]::Open($ZipPath, [IO.FileMode]::CreateNew) + try { + $archive = [IO.Compression.ZipArchive]::new($stream, [IO.Compression.ZipArchiveMode]::Create, $false) + try { + $sep = [IO.Path]::DirectorySeparatorChar + foreach ($file in Get-ChildItem -LiteralPath $SourceDir -Recurse -File) { + $relative = ([IO.Path]::GetRelativePath($SourceDir, $file.FullName)).Replace($sep, [char]47) + $entry = $archive.CreateEntry($relative, [IO.Compression.CompressionLevel]::Fastest) + $in = [IO.File]::OpenRead($file.FullName) + $out = $entry.Open() + try { $in.CopyTo($out) } finally { $out.Dispose(); $in.Dispose() } + } + } finally { $archive.Dispose() } + } finally { $stream.Dispose() } +} + +function Sync-KIMRParitySums { + # Re-syncs a scratch copy's own SHA256SUMS.txt so mutation tests prove CROSS-TREE + # (installer-payload vs. deployed-target) drift detection, never a corrupt-source scenario. + param([Parameter(Mandatory)][string]$Root) + $sumsPath = Join-Path $Root 'SHA256SUMS.txt' + $lines = Get-ChildItem -LiteralPath $Root -Recurse -File | Where-Object { $_.Name -ne 'SHA256SUMS.txt' } | Sort-Object FullName | ForEach-Object { + $rel = ($_.FullName.Substring($Root.Length).TrimStart('\', '/') -replace '\\', '/') + "$((Get-FileHash -LiteralPath $_.FullName -Algorithm SHA256).Hash.ToLowerInvariant()) *$rel" + } + [IO.File]::WriteAllLines($sumsPath, $lines, [Text.UTF8Encoding]::new($false)) +} + +function New-KIMRParitySourceCopy { + param([Parameter(Mandatory)][string]$Destination) + New-Item -ItemType Directory -Path $Destination -Force | Out-Null + Get-ChildItem -LiteralPath $mcpRuntimeSourceRoot -Force | Where-Object { $_.Name -ne 'Payload' } | + Copy-Item -Destination $Destination -Recurse -Force + Sync-KIMRParitySums -Root $Destination + $Destination +} + +try { + $sourceDir = New-KIMRParitySourceCopy -Destination (Join-Path $scratchBase 'source') + + $installerRootA = Join-Path $scratchBase 'installer-a' + New-KIMRParityZip -SourceDir $sourceDir -ZipPath (Join-Path $installerRootA 'Payload/McpRuntime/McpRuntime.zip') + $deployedA = Join-Path $scratchBase 'deployed-a' + Copy-Item -LiteralPath $sourceDir -Destination $deployedA -Recurse -Force + + # === A: installer payload == deployed current\ -> parity = true =========================== + $checks.a_identicalPayloadAndDeployIsCompliant = [ordered]@{ + parity = [bool](Test-KICompleteMcpRuntimePayloadParity -PackageRoot $installerRootA -DeployedPackageRoot $deployedA) + } + if (-not [bool]$checks.a_identicalPayloadAndDeployIsCompliant.parity) { $fail.Add('a_identicalPayloadAndDeployIsCompliant failed: expected true') } + + # === B: one productive file in the INSTALLER PAYLOAD changed -> parity = false ============= + $sourceDirB = Join-Path $scratchBase 'source-b' + Copy-Item -LiteralPath $sourceDir -Destination $sourceDirB -Recurse -Force + Add-Content -LiteralPath (Join-Path $sourceDirB 'McpRuntime.psm1') -Value "# mutation-$([guid]::NewGuid().ToString('N'))" -Encoding utf8 + Sync-KIMRParitySums -Root $sourceDirB + $installerRootB = Join-Path $scratchBase 'installer-b' + New-KIMRParityZip -SourceDir $sourceDirB -ZipPath (Join-Path $installerRootB 'Payload/McpRuntime/McpRuntime.zip') + $checks.b_changedProductiveFileInPayloadIsNonCompliant = [ordered]@{ + parity = [bool](Test-KICompleteMcpRuntimePayloadParity -PackageRoot $installerRootB -DeployedPackageRoot $deployedA) + } + if ([bool]$checks.b_changedProductiveFileInPayloadIsNonCompliant.parity) { $fail.Add('b_changedProductiveFileInPayloadIsNonCompliant failed: expected false') } + + # === C: a file is MISSING from the deployed target -> parity = false ======================= + $deployedC = Join-Path $scratchBase 'deployed-c' + Copy-Item -LiteralPath $sourceDir -Destination $deployedC -Recurse -Force + Remove-Item -LiteralPath (Join-Path $deployedC 'Scripts/ki_desktop_control_tools.py') -Force + $checks.c_missingTargetFileIsNonCompliant = [ordered]@{ + parity = [bool](Test-KICompleteMcpRuntimePayloadParity -PackageRoot $installerRootA -DeployedPackageRoot $deployedC) + } + if ([bool]$checks.c_missingTargetFileIsNonCompliant.parity) { $fail.Add('c_missingTargetFileIsNonCompliant failed: expected false') } + + # === D: an EXTRA, unexpected file exists in the deployed target -> parity = false =========== + $deployedD = Join-Path $scratchBase 'deployed-d' + Copy-Item -LiteralPath $sourceDir -Destination $deployedD -Recurse -Force + Set-Content -LiteralPath (Join-Path $deployedD 'Scripts/unexpected-leftover.py') -Value '# leftover, must be dropped' -Encoding utf8 + $checks.d_extraTargetFileIsNonCompliant = [ordered]@{ + parity = [bool](Test-KICompleteMcpRuntimePayloadParity -PackageRoot $installerRootA -DeployedPackageRoot $deployedD) + } + if ([bool]$checks.d_extraTargetFileIsNonCompliant.parity) { $fail.Add('d_extraTargetFileIsNonCompliant failed: expected false') } + + # === E: VERSION differs between installer payload and the deployed target ================== + # E1: Test-KICompleteMcpRuntimePayloadParity ALONE already reports false for this drift -- + # VERSION is just another compared file, so a content mismatch there is caught by the + # exact same byte-for-byte mechanism as B. + $sourceDirE = Join-Path $scratchBase 'source-e' + Copy-Item -LiteralPath $sourceDir -Destination $sourceDirE -Recurse -Force + Set-Content -LiteralPath (Join-Path $sourceDirE 'VERSION') -Value '9.9.9' -Encoding ascii -NoNewline + $cfgPath = Join-Path $sourceDirE 'Config/mcp-runtime.config.json' + $cfg = Get-Content -LiteralPath $cfgPath -Raw | ConvertFrom-Json + $cfg.version = '9.9.9' + ($cfg | ConvertTo-Json -Depth 20) | Set-Content -LiteralPath $cfgPath -Encoding utf8 + Sync-KIMRParitySums -Root $sourceDirE + $installerRootE = Join-Path $scratchBase 'installer-e' + New-KIMRParityZip -SourceDir $sourceDirE -ZipPath (Join-Path $installerRootE 'Payload/McpRuntime/McpRuntime.zip') + $checks.e1_versionDriftAloneMakesPayloadParityFalse = [ordered]@{ + parity = [bool](Test-KICompleteMcpRuntimePayloadParity -PackageRoot $installerRootE -DeployedPackageRoot $deployedA) + } + if ([bool]$checks.e1_versionDriftAloneMakesPayloadParityFalse.parity) { $fail.Add('e1_versionDriftAloneMakesPayloadParityFalse failed: expected false') } + + # E2: the REAL, full contract (Test-KICompleteMcpRuntimeCompliant) -- a target genuinely + # deployed at version 9.9.9 (self-consistent: source VERSION == config version, real + # Install-KIMcpRuntime run, real lifecycle bookkeeping) must be reported non-compliant when + # asked about a DIFFERENT expected version, and compliant when asked about its own actual + # version -- the version-string gate runs before any payload/hash parity check at all. + $targetRootE = Join-Path $scratchBase 'target-e' + $installE = Install-KIMcpRuntime -PackageRoot $sourceDirE -TargetRoot $targetRootE -Action Install -SkipUvCheck + $checks.e2_realContractVersionGate = [ordered]@{ + installOfTheDriftedSourceSucceededAtItsOwnVersion = [bool]$installE.passed + complianceAgainstWrongExpectedVersionIsFalse = (-not [bool](Test-KICompleteMcpRuntimeCompliant -TargetRoot $targetRootE -ExpectedComponentVersion '0.1.0')) + complianceAgainstItsOwnActualVersionIsTrue = [bool](Test-KICompleteMcpRuntimeCompliant -TargetRoot $targetRootE -ExpectedComponentVersion '9.9.9') + complianceWithInstallerPackageRootAndWrongVersionStillFalse = (-not [bool](Test-KICompleteMcpRuntimeCompliant -TargetRoot $targetRootE -ExpectedComponentVersion '0.1.0' -InstallerPackageRoot $installerRootE)) + } + if ($checks.e2_realContractVersionGate.Values -contains $false) { $fail.Add('e2_realContractVersionGate failed: ' + ($checks.e2_realContractVersionGate | ConvertTo-Json -Compress)) } + + $passed = $fail.Count -eq 0 + [pscustomobject]@{ passed = $passed; checks = $checks; failures = @($fail) } | ConvertTo-Json -Depth 12 + if (-not $passed) { throw 'MCP-Runtime-Payload-Parity-Regression fehlgeschlagen.' } +} finally { + try { Remove-Item -LiteralPath $scratchBase -Recurse -Force -ErrorAction SilentlyContinue } catch {} +} diff --git a/tools/complete-installer/current/Test-KIStackMcpRuntimeSeededPrimaryFlow.ps1 b/tools/complete-installer/current/Test-KIStackMcpRuntimeSeededPrimaryFlow.ps1 index 55152cb..6d982d5 100644 --- a/tools/complete-installer/current/Test-KIStackMcpRuntimeSeededPrimaryFlow.ps1 +++ b/tools/complete-installer/current/Test-KIStackMcpRuntimeSeededPrimaryFlow.ps1 @@ -46,19 +46,21 @@ New-Item -ItemType Directory -Path $scratchBase -Force | Out-Null function Invoke-KISeededMcpRuntimeStep { # Manually replicates CompleteInstaller.psm1's own `elseif ($step.id -eq 'mcp-runtime')` branch - # body verbatim (same PathContext.PayloadRoot convention, same entry point, same Validate- - # then-rollback-on-failure shape) -- never a parallel reimplementation. + # body verbatim (same PathContext.PayloadRoot convention, same entry point, same + # BackupRoot-under-TransactionBackupRoot + Isolated-process Install/Validate + Validate-then- + # rollback-on-failure shape, 2.19 Phase 1) -- never a parallel reimplementation. param([Parameter(Mandatory)][string]$TargetRoot,[Parameter(Mandatory)][object]$PathContext,[Parameter(Mandatory)][string]$PlannedMode) $extract = Join-Path ([string]$PathContext.PayloadRoot) 'McpRuntime' $componentRoot = Expand-KICompletePayload -PackageRoot $PackageRoot -PayloadName 'McpRuntime' -Destination $extract $entry = Join-Path $componentRoot 'Invoke-KIStackMcpRuntime.ps1' if (-not (Test-Path -LiteralPath $entry -PathType Leaf)) { throw 'MCP-Runtime-Einstieg fehlt.' } $action = if ($PlannedMode -eq 'Repair') { 'Repair' } elseif ($PlannedMode -eq 'Upgrade') { 'Upgrade' } else { 'Install' } + $mcpRuntimeBackupRoot = Join-Path ([string]$PathContext.TransactionBackupRoot) 'mcp-runtime' $result = $null try { - $result = Invoke-KICompleteJsonScript -Script $entry -Arguments @{ Action = $action; TargetRoot = $TargetRoot } + $result = Invoke-KICompleteJsonScriptIsolated -Script $entry -Arguments @{ Action = $action; TargetRoot = $TargetRoot; BackupRoot = $mcpRuntimeBackupRoot } if (-not [bool]$result.passed) { throw "MCP-Runtime-$action fehlgeschlagen." } - $validation = Invoke-KICompleteJsonScript -Script $entry -Arguments @{ Action = 'Validate'; TargetRoot = $TargetRoot } + $validation = Invoke-KICompleteJsonScriptIsolated -Script $entry -Arguments @{ Action = 'Validate'; TargetRoot = $TargetRoot } if (-not [bool]$validation.passed) { throw 'MCP-Runtime-Validierung fehlgeschlagen.' } [pscustomobject]@{ passed = $true; action = $action; install = $result; validation = $validation } } catch { @@ -97,6 +99,11 @@ try { passed = [bool]$mcpInstall.passed actionWasInstall = ($mcpInstall.action -eq 'Install') hasBackupPath = (-not [string]::IsNullOrWhiteSpace([string]$mcpInstall.install.backupPath)) + # 2.19 Phase 1 (2.18.1 Desktop-Control lesson applied to mcp-runtime): the transaction's + # own TransactionBackupRoot must actually be honored, never the standalone + # \backups\mcp-runtime path a non-transactional caller would get. + backupUnderTransactionBackupRoot = ([string]$mcpInstall.install.backupPath).StartsWith([string]$pathContext2.TransactionBackupRoot, [StringComparison]::OrdinalIgnoreCase) + packageTreeDeployed = (Test-Path -LiteralPath (Join-Path $t2 'tools/mcp-runtime/current/VERSION') -PathType Leaf) } if ($checks.seededInstallSucceeds.Values -contains $false) { $fail.Add('seededInstallSucceeds failed: ' + ($checks.seededInstallSucceeds | ConvertTo-Json -Compress)) } diff --git a/tools/complete-installer/current/VERSION b/tools/complete-installer/current/VERSION index e9187f6..92aa628 100644 --- a/tools/complete-installer/current/VERSION +++ b/tools/complete-installer/current/VERSION @@ -1 +1 @@ -2.18.2 \ No newline at end of file +2.19.0 \ No newline at end of file diff --git a/tools/complete-installer/current/Validation/REGRESSION-COVERAGE.json b/tools/complete-installer/current/Validation/REGRESSION-COVERAGE.json index 4bbdb63..bdaaf75 100644 --- a/tools/complete-installer/current/Validation/REGRESSION-COVERAGE.json +++ b/tools/complete-installer/current/Validation/REGRESSION-COVERAGE.json @@ -1,7 +1,7 @@ { "schemaVersion": "1.0", "packageName": "KI-Stack-Complete-Installer", - "packageVersion": "2.18.2", + "packageVersion": "2.19.0", "coverage": [ { "id": "REG-023-ROOTLESS-NESTED-ARCHIVE", @@ -67,6 +67,11 @@ "id": "REG-041-UPDATE-KI-STACK-ALL", "status": "covered", "method": "There was no single, central place to check or safely update every KI-Stack-managed component; each had its own separate, undiscoverable path (or none at all), and nothing distinguished a genuine pin-vs-installed drift from upstream project churn. Update-KIStack-All.cmd/.ps1 (deployed to C:\\KI-Stack) now enumerates every Contracts/COMPONENTS.json component plus OpenWebUI (tracked separately via kernel-config.json/the managed venv) and reports InstalledVersion, PinnedVersion and AvailableVersion per component. classification (UpToDate/PinnedUpdatePending/DowngradeRequired/Blocked/NotManaged) compares only InstalledVersion against PinnedVersion and is the sole basis for automatic execution; upstreamStatus (Current/UpdateAvailableUpstream/Unknown), derived from real, read-only upstream lookups (PyPI's own JSON API for the PyPI-distributed openwebui package; GitHub's own commits API against the Git revision already pinned in a component's own payload contract for comfyui/integration; Unknown -- never guessed -- for every KI-Stack-authored bundle with no external upstream project), is purely informational and never triggers an unattended install. -CheckOnly shows the plan without asking for confirmation or mutating anything; -Component restricts the plan; -NonInteractive skips the EXECUTE confirmation. OpenWebUI runs through its own already-real-target-validated isolated adapter (Update-KIStack-OpenWebUI.cmd, see REG-038); every other component runs through the existing Complete-Installer-Upgrade batch, which has no per-component isolation -- if an explicit -Component selection would understate that batch's real scope (naming only some of the components a real run would actually touch), execution is refused before any confirmation prompt or mutation, with a clear error naming the omitted component(s). Separately, Remove-KIStackKnowledgeExperiment.ps1's Knowledge-rollback readback validation previously only set a passed:false field in its returned object without ever throwing, so a genuine validation failure (leftover Knowledge bindings after removal) could be silently recorded as if the whole finalization step had succeeded; it now throws a clear error on that same condition, matching the existing throw-on-validation-failure pattern already used by the sibling Code-Interpreter configuration step. Covered by Test-KIStackUpdateAll.ps1 (all components already at pin -> only UpToDate; a single component behind its pin -> PinnedUpdatePending; installed numerically above the pin -> DowngradeRequired; an explicitly requested unmanaged component id -> NotManaged, not silently dropped; upstream reporting UpdateAvailableUpstream on an already-pin-compliant component is surfaced but never auto-executed; sequential execution with OpenWebUI first then the batch for any remaining component; a failing OpenWebUI adapter stops before the batch ever runs, with its rollback detail surfaced rather than discarded; a non-EXECUTE confirmation cancels before anything runs; a partial -Component selection against a multi-component batch is blocked before mutation while a selection naming every affected component proceeds; -Component openwebui alone is never affected by an unrelated batch ambiguity; no secret handling is introduced; the normal installer's ReplayComponent/Upgrade dispatch and starter registration are unaffected). Real-target validated against C:\\KI-Stack: -CheckOnly reported all 14 Contracts/COMPONENTS.json components UpToDate and OpenWebUI Installed 0.11.1 against Pinned 0.11.0 -> DowngradeRequired, with the real PyPI upstream lookup additionally reporting Available 0.11.1 / UpdateAvailableUpstream; no mutation was performed." + }, + { + "id": "REG-042-MCPRUNTIME-PERSISTENT-PACKAGE-PARITY", + "status": "covered", + "method": "mcp-runtime (tools/mcp-runtime/current) never materialized a persistent, source-parity-checked package tree on a target the way winapp/desktop-control already do: Install-KIMcpRuntime only wrote lifecycle bookkeeping (modules/mcp-runtime marker + starter/stopper, state/mcp-runtime credential/workspace) and launched whatever $PackageRoot happened to be at call time -- driven by the Complete Installer, a transaction-scoped payload staging directory under state/complete-installer/transactions//payload/McpRuntime -- so the generated starter/stopper .cmd hard-coded a path into that ephemeral staging directory, and Test-KIMcpRuntime/Test-KICompleteMcpRuntimeCompliant never hashed deployed content against source, so a changed payload at an unchanged component VERSION was silently reported SkippedAlreadyCompliant instead of being reconciled. mcp-runtime now deploys a persistent package tree at \\tools\\mcp-runtime\\current\\ (Get-KIMcpRuntimeInstallPaths/Test-KIMcpRuntimeDeployed/Test-KIMcpRuntimeSourceParity in McpRuntime.psm1, mirroring DesktopControl.psm1's own functions), Install-KIMcpRuntime accepts an externally-owned -BackupRoot (the same 2.18.1 Desktop-Control BackupRoot-respect fix applied here), and the primary step loop's own 'mcp-runtime' branch passes its own TransactionBackupRoot slice and runs Install/Upgrade/Repair+Validate via Invoke-KICompleteJsonScriptIsolated, matching desktop-control's own hardening. Test-KICompleteMcpRuntimeCompliant gained an -InstallerPackageRoot parameter and a new Test-KICompleteMcpRuntimePayloadParity function (mirroring Test-KICompleteDesktopControlPayloadParity), wired into both New-KICompletePlan and the resume-recheck block. Also fixed in the same pass: mcp_launcher.py's OpenAPIProvider client construction used a plain httpx.AsyncClient (aliased as httpx2), which fastmcp>=4.0.3 flags with FastMCPDeprecationWarning -- switched to a real httpx2.AsyncClient (httpx2 is a genuine, separate package already resolved transitively by the same open-terminal[mcp] pin). Covered by Test-KIStackMcpRuntimeInstall.ps1 (fresh install deploys the persistent tree; same-version/same-payload Skip; same-version/CHANGED-payload reconciles -- the actual bug fix; missing/extra deployed file reconciles; state never treated as payload; external -BackupRoot respected; rollback restores the persistent tree; a failed fresh install leaves no orphaned tree; source/config VERSION mismatch fails closed; Uninstall removes the persistent tree), Test-KIStackMcpRuntimePayloadParity.ps1 (the REAL Test-KICompleteMcpRuntimePayloadParity, never a copy, against real Payload/McpRuntime/*.zip archives: identical payload/deploy -> compliant; changed productive file in the payload -> non-compliant; missing/extra deployed file -> non-compliant; VERSION drift -> non-compliant both via payload parity alone and via the full Test-KICompleteMcpRuntimeCompliant version gate), and the existing Test-KIStackMcpRuntimeCompleteInstallerIntegration.ps1/Test-KIStackMcpRuntimeSeededPrimaryFlow.ps1 (extended with explicit backupUnderTransactionBackupRoot/packageTreeDeployed assertions), all re-run and passing against real Payload/McpRuntime/*.zip archives (removed again after the test run, never left in the working tree)." } ] } diff --git a/tools/complete-installer/current/Validation/VALIDATION-CONTRACT.json b/tools/complete-installer/current/Validation/VALIDATION-CONTRACT.json index c2d17a3..1b6d943 100644 --- a/tools/complete-installer/current/Validation/VALIDATION-CONTRACT.json +++ b/tools/complete-installer/current/Validation/VALIDATION-CONTRACT.json @@ -1,7 +1,7 @@ { "schemaVersion": "1.0", "packageName": "KI-Stack-Complete-Installer", - "packageVersion": "2.18.2", + "packageVersion": "2.19.0", "packageType": "CompleteInstaller", "selfTestEntryPoint": "Test-KIStackCompleteInstaller.ps1", "selfTestArguments": [], diff --git a/tools/desktop-control/current/Config/desktop-control.config.json b/tools/desktop-control/current/Config/desktop-control.config.json index 0ea303d..da49e28 100644 --- a/tools/desktop-control/current/Config/desktop-control.config.json +++ b/tools/desktop-control/current/Config/desktop-control.config.json @@ -1,6 +1,6 @@ { "schemaVersion": "1.0", - "version": "0.1.0", + "version": "0.1.1", "targetRoot": "C:\\KI-Stack", "kiStackRoot": "C:\\KI-Stack", "winappResolver": { diff --git a/tools/desktop-control/current/DesktopControl.psm1 b/tools/desktop-control/current/DesktopControl.psm1 index 701595f..4b85315 100644 --- a/tools/desktop-control/current/DesktopControl.psm1 +++ b/tools/desktop-control/current/DesktopControl.psm1 @@ -743,7 +743,7 @@ function Test-KIDesktopControlDeployed { # VERSION is reported non-compliant instead of being silently skipped. Callers that only # ask "is a self-contained deployment present" (the Complete Installer's planning probe, # older tests) omit it and keep the previous behaviour. - param([Parameter(Mandatory)][string]$TargetRoot, [string]$ExpectedVersion = '0.1.0', [string]$SourceRoot) + param([Parameter(Mandatory)][string]$TargetRoot, [string]$ExpectedVersion = '0.1.1', [string]$SourceRoot) $p = Get-KIDesktopControlInstallPaths -TargetRoot $TargetRoot if (-not (Test-Path -LiteralPath $p.packageRoot -PathType Container)) { return [pscustomobject]@{ ok = $false; reason = 'package-root-missing'; paths = $p } } foreach ($stampPath in @($p.versionStamp, $p.marker)) { @@ -820,7 +820,7 @@ function Install-KIDesktopControl { # Idempotency gate. -SourceRoot makes SkippedAlreadyCompliant require that the deployed # content still equals THIS payload, not merely that the target is self-consistent -- so a - # changed payload at an unchanged VERSION (0.1.0 == 0.1.0) is reconciled, never ignored. + # changed payload at an unchanged VERSION (e.g. 0.1.1 == 0.1.1) is reconciled, never ignored. $existing = Test-KIDesktopControlDeployed -TargetRoot $TargetRoot -ExpectedVersion $expected -SourceRoot $PackageRoot if ([bool]$existing.ok) { return [pscustomobject]@{ passed = $true; status = 'SkippedAlreadyCompliant'; action = $Action; marker = (Get-Content -LiteralPath $p.marker -Raw | ConvertFrom-Json -Depth 20); mutatesTarget = $false } diff --git a/tools/desktop-control/current/MANIFEST.json b/tools/desktop-control/current/MANIFEST.json index dccd57d..1227764 100644 --- a/tools/desktop-control/current/MANIFEST.json +++ b/tools/desktop-control/current/MANIFEST.json @@ -2,7 +2,7 @@ "schemaVersion": "1.0", "packageId": "KI-STACK-DESKTOP-CONTROL", "name": "KI-Stack Desktop Control", - "version": "0.1.0", + "version": "0.1.1", "status": "Development_SourceValidationPending", "containsSecrets": false, "runtimeGitDependency": false, @@ -22,5 +22,5 @@ "send-input", "send-keys", "global keyboard injection", "system hotkeys", "mouse coordinate click", "arbitrary drag/touch/pen", "raw winapp command execution" ], - "mcpIntegration": "deferred -- see MCP-INTEGRATION.md; no new MCP server, port or credential" + "mcpIntegration": "implemented, 2.19 Phase 1 -- see MCP-INTEGRATION.md; ui_* tools on the existing ki-stack-mcp-runtime MCP server, no new MCP server, port or credential" } diff --git a/tools/desktop-control/current/MCP-INTEGRATION.md b/tools/desktop-control/current/MCP-INTEGRATION.md index 706b865..0fa211e 100644 --- a/tools/desktop-control/current/MCP-INTEGRATION.md +++ b/tools/desktop-control/current/MCP-INTEGRATION.md @@ -1,45 +1,144 @@ -# Desktop Control -> MCP Runtime integration (deferred) +# Desktop Control -> MCP Runtime integration -**Status: design note only. Not implemented in KI-Stack 2.18 Phase "Desktop Control wrapper".** +**Status: implemented, KI-Stack 2.19 Phase 1.** -## Why it is deferred +## History: why this was deferred through 2.18 -The existing MCP Runtime (`tools/mcp-runtime/current/`) does not host a KI-Stack-owned tool -registry or dispatcher. `Scripts/mcp_launcher.py` starts **Open Terminal's own FastAPI app** -unchanged and exposes it through FastMCP's `OpenAPIProvider`. Its "12 tools" are Open Terminal's -OpenAPI operations; no KI-Stack code is on the tool-call path at runtime (README.md, "Architektur- -regel"). `LOCAL-CONTROL-CONTRACT.md` §18 lists a *new MCP tool / new runtime / new port / new -component* as explicitly out of scope for that contract. +Through 2.18, the existing MCP Runtime (`tools/mcp-runtime/current/`) hosted no KI-Stack-owned +tool registry or dispatcher. `Scripts/mcp_launcher.py` started **Open Terminal's own FastAPI +app** unchanged and exposed it through FastMCP's `OpenAPIProvider`. Its tools were Open +Terminal's OpenAPI operations only; no KI-Stack code was on the tool-call path at runtime +(README.md, "Architekturregel"). `LOCAL-CONTROL-CONTRACT.md` §18 lists a *new MCP tool / new +runtime / new port / new component* as explicitly out of scope for that contract, and the 2.18 +Desktop Control wrapper shipped as a standalone, policy-complete component with the MCP wiring +left for a later step (see the "Open decision" note this section used to carry, and the +originally-considered two options below -- both were rejected). -Adding a `desktop_control` MCP tool therefore requires one of: +2.19 Phase 1 resolves that open decision **without** either originally-considered option: -1. a second FastMCP provider inside `mcp_launcher.py` backed by a small KI-Stack-owned FastAPI - sub-app that shells out to `Invoke-KIStackDesktopControl.ps1 -Operation ...`; or -2. a standalone OpenAPI shim (its own tiny FastAPI app on loopback) registered as an additional - `tool_server.connections` entry alongside `ki-stack-mcp-runtime`. +1. ~~a second FastMCP provider inside `mcp_launcher.py` backed by a small KI-Stack-owned FastAPI + sub-app~~ -- rejected: a second provider/sub-app is unnecessary machinery when FastMCP's own + `FastMCP.tool` decorator can register native Python tools directly on the same instance. +2. ~~a standalone OpenAPI shim on its own loopback port, registered as a second + `tool_server.connections` entry~~ -- rejected: it would have added a second port, which + `LOCAL-CONTROL-CONTRACT.md` §18 and this component's own README both rule out. -Both change the MCP Runtime component or add a component/endpoint. Per the task's own guidance -("Wenn eine direkte MCP-Tool-Erweiterung in dieser Phase architektonisch unsauber wäre: erst -Wrapper/Dispatcher + Tests bauen und Integration separat dokumentieren. Nicht erzwingen."), the -wrapper + policy + tests ship first (this component); the MCP wiring is a separate, later step. +## What actually shipped in 2.19 Phase 1 -## Contract the integration must honour +`Scripts/mcp_launcher.py` still builds the exact same single `FastMCP` instance it always has +(Open Terminal's `OpenAPIProvider`, unchanged, same auth-bridge fix as before) -- and, on that +**same instance**, additionally calls `register_ui_tools(mcp_server, target_root)` from the new +sibling module `Scripts/ki_desktop_control_tools.py`. `register_ui_tools` uses FastMCP's own +public `@mcp.tool` decorator (`fastmcp.server.server.FastMCP.tool` / `.add_tool`) -- the same +mechanism used for any local FastMCP tool, not a provider, not a bridge client, not a proxy. No +second MCP server, no second port (still exactly `127.0.0.1:8021`, `server:mcp:ki-stack-mcp- +runtime`), no second credential, no second runtime process. + +**Ten flat tools, not one `desktop_control(operation, ...)` tool** -- this is the one point where +2.19 deliberately diverges from what this document originally proposed. Each tool has a concrete, +typed schema for exactly one Desktop Control operation instead of a generic `operation` string +plus a loose parameter bag: + +| Tool | Desktop Control operation | Class | +|---|---|---| +| `ui_list_windows` | `list_windows` | read-only | +| `ui_inspect_window` | `inspect_window` | read-only | +| `ui_find_element` | `find_element` | read-only | +| `ui_get_properties` | `get_properties` | read-only | +| `ui_get_value` | `get_value` | read-only | +| `ui_screenshot` | `screenshot` | read-only | +| `ui_wait_for` | `wait_for` | read-only | +| `ui_set_value` | `set_value` | mutating | +| `ui_invoke` | `invoke` | mutating | +| `ui_focus` | `focus` | mutating | + +`scroll_into_view` / `scroll` (backend capability unverified, fails closed inside Desktop Control +itself) and the entire never-exposed surface (`send_input`, `send_keys`, `global_hotkey`, +`mouse_click_coordinate`, `drag`, `touch`, `pen`, `raw_winapp`, ...) have **no** corresponding +tool at all -- not merely policy-blocked, structurally absent from `ki_desktop_control_tools.py`. + +## Contract the integration honours - **No new MCP server, port, or credential.** The runtime stays `server:mcp:ki-stack-mcp-runtime` - on its existing port. Option 1 adds no port; option 2 would add a loopback port and is the - less preferred path for exactly that reason. -- **Exactly one tool**, `desktop_control`, taking `{ operation, ... }` and returning the wrapper's - JSON result contract verbatim. No `raw_winapp`, no `send_input`, no coordinate-click tool. -- The tool implementation is a thin transport that calls - `Invoke-KIStackDesktopControl.ps1 -Operation -RequestJson -TargetRoot ` and - returns its stdout JSON. All Resolve/Validate/Act/Re-observe/Verify and policy enforcement stay - inside this component; the MCP layer adds nothing but transport. -- Audit records continue to be written by this component under - `\state\desktop-control\logs\actions\.jsonl`. - -## Open decision - -Whether the desktop-control tool is exposed only to a dedicated capability/profile (so ordinary -`ki-stack-mcp-runtime`-bound profiles do not gain GUI control implicitly) is left to the MCP -integration step, alongside the profile-wiring question already tracked in -`LOCAL-CONTROL-CONTRACT.md` §21. + on its existing port (`127.0.0.1:8021`). +- Every `ui_*` tool is a thin transport that serializes its typed parameters to a Desktop Control + `RequestJson` object and calls + `Invoke-KIStackDesktopControl.ps1 -Operation -RequestJson -TargetRoot ` + (`ki_desktop_control_tools.run_desktop_control_operation`). All + Resolve -> Validate -> Act -> Re-observe -> Verify and policy enforcement stay inside Desktop + Control; the MCP layer adds nothing but transport and never re-classifies an operation, never + re-checks a window/element contract, never re-decides a secret context. +- The dispatcher is looked up at exactly one fixed path, + `\tools\desktop-control\current\Invoke-KIStackDesktopControl.ps1`, derived from the + MCP Runtime's own `TargetRoot` (now passed to `mcp_launcher.py` as its 4th argument by + `McpRuntime.psm1`'s `Get-KIMcpRuntimeStartArguments`) -- **never** a `PATH` search, never a + second candidate location. If it is missing, every `ui_*` call fails closed with a structured + MCP tool error (`fastmcp.exceptions.ToolError`); it is never silently skipped or resolved from + somewhere else. +- A **business-level** Desktop Control result -- including `success:false` statuses such as + `SecretContextBlocked`, `PostconditionNotProven`, `WindowNotFound`, or `ResolverError` -- is + valid JSON on the dispatcher's stdout and is returned to the MCP caller **unchanged**, exactly + like a `success:true` result. `Invoke-KIStackDesktopControl.ps1` itself exits `1` for every + `success:false` result (see its own tail), so the MCP layer treats a non-zero exit code as a + transport failure **only** when it is *also* not accompanied by parseable JSON on stdout (a + real crash) -- never on its own. Only a missing dispatcher, an unresolvable `pwsh.exe`, a + process that could not start or timed out, or stdout that does not parse as a JSON object + raises an MCP tool error. +- Audit records continue to be written by Desktop Control itself, unchanged, under + `\state\desktop-control\logs\actions\.jsonl` -- the MCP layer does not + duplicate or bypass that audit trail. + +## Files + +- `tools/mcp-runtime/current/Scripts/ki_desktop_control_tools.py` -- the ten `ui_*` tool + definitions, the transport function, and the fixed dispatcher-path resolution. +- `tools/mcp-runtime/current/Scripts/mcp_launcher.py` -- registers them on the existing FastMCP + instance via `register_ui_tools`. +- `tools/mcp-runtime/current/Scripts/test_ki_desktop_control_tools.py` -- unit tests (mocked + dispatcher process; no GUI, no real `winapp`, no real PowerShell dispatcher execution). +- `tools/mcp-runtime/current/McpRuntime.psm1` -- `Get-KIMcpRuntimeStartArguments` now also passes + `TargetRoot`; `Test-KIMcpRuntimeHealthy` additionally verifies, from the same `list_tools` + round-trip, that Open Terminal's baseline tools are still present, all ten `ui_*` tools are + present, and no never-exposed UI tool name is. + +## Dependency verification: fastmcp version actually resolved + +`Config/mcp-runtime.config.json`'s `packageSpec` (`open-terminal[mcp]==0.11.34`) pins Open +Terminal's own version, not fastmcp's -- fastmcp is a *transitive* dependency, resolved by `uv` +at launcher-start time. Verified reproducibly (2026-09-13), not merely inferred from a locally +cached wheel: + +``` +uv run --with "open-terminal[mcp]==0.11.34" python -c "import fastmcp; print(fastmcp.__version__)" +# -> 4.0.3 +``` + +`fastmcp==4.0.3`'s `OpenAPIProvider` (`fastmcp/server/providers/openapi/provider.py`) now detects +a plain `httpx.AsyncClient` (by walking the client's class MRO for a top-level `httpx` module) and +raises `FastMCPDeprecationWarning` — still accepted, but "will be removed in a future release" — +recommending a real `httpx2.AsyncClient` instead. `httpx2` is a genuine, separate PyPI package +(not an alias for `httpx`), already resolved transitively by this same `packageSpec` (it is +fastmcp's own dependency: verified `httpx2.__version__ == '2.12.0'` under the identical `uv run` +invocation above, with the same `AsyncClient`/`ASGITransport` surface `mcp_launcher.py` already +used). Fixed in the same branch, no architecture change and no new dependency: `mcp_launcher.py` +now does a real `import httpx2` instead of `import httpx as httpx2`. Re-verified with +`warnings.catch_warnings(record=True)` around the exact construction `mcp_launcher.py` performs +(`OpenAPIProvider(client=httpx2.AsyncClient(...))`, real `open_terminal.main.app`, +`register_ui_tools` included) under the real resolved environment: zero `FastMCPDeprecationWarning` +instances, 22 tools still constructed correctly. + +Re-verify this exact command after any future `packageSpec` version bump -- a pinned Open Terminal +version can still resolve a different transitive fastmcp/httpx2 version over time. + +## Still open: profile / capability exposure + +The 2.18 "open decision" about whether Desktop Control should be gated behind a dedicated +capability/profile is **not resolved by this phase**. As shipped, the `ui_*` tools are ordinary +native FastMCP tools on the same `server:mcp:ki-stack-mcp-runtime` connection every existing +profile already binds to (`LOCAL-CONTROL-CONTRACT.md` §2) -- so, structurally, every profile that +already has local control also gains GUI/UIA control the same way, with no separate profile-gating +mechanism added. Whether that is the *intended* exposure, or whether a later phase should restrict +`ui_*` to a subset of profiles, is a product decision this phase deliberately leaves for the user +to make explicitly -- it is called out again in the Phase-1 report's open points. The related, +still-open question in `LOCAL-CONTROL-CONTRACT.md` §21 about *where* per-profile agent guidance +should live is unaffected either way. diff --git a/tools/desktop-control/current/README.md b/tools/desktop-control/current/README.md index 036ecd6..fe6bf7f 100644 --- a/tools/desktop-control/current/README.md +++ b/tools/desktop-control/current/README.md @@ -82,10 +82,14 @@ result, `winapp` exitCode, evidence path, blocked reason. Screenshots (evidence) ## MCP integration -Deferred — see `MCP-INTEGRATION.md`. The existing MCP Runtime hosts Open Terminal's OpenAPI app -unchanged and has no KI-Stack-owned tool registry; adding a `desktop_control` tool changes that -component or adds an endpoint, so the wrapper + policy + tests ship first and the wiring is a -separate step. No new MCP server, port, or credential when it lands. +Implemented, KI-Stack 2.19 Phase 1 — see `MCP-INTEGRATION.md`. The existing MCP Runtime +(`tools/mcp-runtime/current/Scripts/mcp_launcher.py`) registers a flat `ui_*` tool surface +(`ui_list_windows`, `ui_inspect_window`, `ui_find_element`, `ui_get_properties`, `ui_get_value`, +`ui_screenshot`, `ui_wait_for`, `ui_set_value`, `ui_invoke`, `ui_focus`) as native FastMCP tools +on the SAME `FastMCP` instance and port Open Terminal's own tools already run on — no new MCP +server, no new port, no new credential. Each tool is a thin transport straight to this +component's own `Invoke-KIStackDesktopControl.ps1`; every policy decision documented above stays +inside this component unchanged. ## Tests diff --git a/tools/desktop-control/current/SHA256SUMS.txt b/tools/desktop-control/current/SHA256SUMS.txt index 3b2d6ba..312a2ce 100644 --- a/tools/desktop-control/current/SHA256SUMS.txt +++ b/tools/desktop-control/current/SHA256SUMS.txt @@ -1,11 +1,11 @@ -031e0b8d639ed4576072be426ff00cc125c822421d5e8c32bca1cb9297ffa576 *Config/desktop-control.config.json -864104dc39b1c354c3fc9e80aee9f07a274c248475675361a2eb46c6ef6c8f6a *Config/desktop-control.policy.json -26c35a8dd620d8f7428552d5c51f420fc7a702a4127914c9c69f0e5d3b3b3bb2 *DesktopControl.Policy.psm1 -8b5c4fcf22851601d759b3f0c746acc2dba43dda4fc8c77e7394af196284735e *DesktopControl.psm1 -61aa394c519dbac3101f0b8927ce9948b97970c5c9747304ff0bd68dc46a6a1f *Invoke-KIStackDesktopControl.ps1 -1473aef16030d09e4c2b6cd2508ac75ed6cbecc92c25abef0b195ba5b6eb20b3 *MANIFEST.json -f53ffa4f45e1d06dfb1cea12890d06925180ba6cbc3f5c7050d2ab732a5bf601 *MCP-INTEGRATION.md -1ad00985f6a8cdac5c26bb944b8f325d457c57b6414313d4a27de7438cf3bd94 *README.md -4523f70e586282e85f9b96b02be3ceb98792c17dcc7404476ff920b8edcac1af *Test-KIStackDesktopControl.ps1 -6e3e2beee88620c76cc62f1479d1c39cd1cf734f4ba46636b6b8a69eb091aa20 *Vendor/WinApp.Resolver.psm1 -e9dd8507f4bf0c6f42458e41aea833ad0bd3f6127272335eee9bf4d58541ed67 *VERSION +480ca737a707e65aa260578633e13699469331505879ca099b30208845ae0e6c *Config/desktop-control.config.json +864104dc39b1c354c3fc9e80aee9f07a274c248475675361a2eb46c6ef6c8f6a *Config/desktop-control.policy.json +26c35a8dd620d8f7428552d5c51f420fc7a702a4127914c9c69f0e5d3b3b3bb2 *DesktopControl.Policy.psm1 +a63d224ace241919b0397df316e9a20003d2dfe423969c9317a312860a321328 *DesktopControl.psm1 +61aa394c519dbac3101f0b8927ce9948b97970c5c9747304ff0bd68dc46a6a1f *Invoke-KIStackDesktopControl.ps1 +9d2609e5cb9cfcddd5ffea26170fa85b64173206444243130c2d7f9421696e76 *MANIFEST.json +0cbfc08177a73124b28658a2eb4994a4eb506af7f30cf017a025225c25f016ac *MCP-INTEGRATION.md +474aae52d62084dbcd9d23f40518c516c62fad67fe59c27040fae5921a3d114c *README.md +cddd280a2b5d1b3bc69a89c73a03c9146fed8669aa4f52dc5803e56fdaa5a1af *Test-KIStackDesktopControl.ps1 +6e3e2beee88620c76cc62f1479d1c39cd1cf734f4ba46636b6b8a69eb091aa20 *Vendor/WinApp.Resolver.psm1 +80447316529fc62014fbaa047fb67c51ac30e32830cda370405349d8d66d196c *VERSION diff --git a/tools/desktop-control/current/Test-KIStackDesktopControl.ps1 b/tools/desktop-control/current/Test-KIStackDesktopControl.ps1 index ad63ec7..76b0293 100644 --- a/tools/desktop-control/current/Test-KIStackDesktopControl.ps1 +++ b/tools/desktop-control/current/Test-KIStackDesktopControl.ps1 @@ -403,7 +403,7 @@ try { $provRoot = New-DCTestRoot 'provision' $inst1 = Install-KIDesktopControl -PackageRoot $PackageRoot -TargetRoot $provRoot -Action 'Install' $installPaths = Get-KIDesktopControlInstallPaths -TargetRoot $provRoot - $deployed = Test-KIDesktopControlDeployed -TargetRoot $provRoot -ExpectedVersion '0.1.0' + $deployed = Test-KIDesktopControlDeployed -TargetRoot $provRoot -ExpectedVersion '0.1.1' $inst2 = Install-KIDesktopControl -PackageRoot $PackageRoot -TargetRoot $provRoot -Action 'Repair' # Corrupt one deployed file -> Repair must re-deploy and pass again. Add-Content -LiteralPath (Join-Path $installPaths.packageRoot 'MANIFEST.json') -Value 'corruption' @@ -412,7 +412,7 @@ try { $instAlt = Install-KIDesktopControl -PackageRoot $PackageRoot -TargetRoot $altRoot -Action 'Install' $checks.centralProvisioning = [ordered]@{ installedUnderToolsDesktopControl = ([string]$inst1.status -eq 'Installed' -and $installPaths.installRoot -eq (Join-Path ([IO.Path]::GetFullPath($provRoot)) 'tools\desktop-control') -and (Test-Path -LiteralPath (Join-Path $provRoot 'tools\desktop-control\current\Invoke-KIStackDesktopControl.ps1') -PathType Leaf)) - probeStampWritten = ((Get-Content -LiteralPath (Join-Path $provRoot 'tools\desktop-control\VERSION') -Raw).Trim() -eq '0.1.0') + probeStampWritten = ((Get-Content -LiteralPath (Join-Path $provRoot 'tools\desktop-control\VERSION') -Raw).Trim() -eq '0.1.1') markerWritten = (Test-Path -LiteralPath $installPaths.marker -PathType Leaf) deployedComplianceOk = ([bool]$deployed.ok) idempotentRepairIsNoOp = ([string]$inst2.status -eq 'SkippedAlreadyCompliant' -and -not [bool]$inst2.mutatesTarget) @@ -436,7 +436,7 @@ try { $orchestrator = Get-Content -LiteralPath (Join-Path $repoRoot 'tools\complete-installer\current\CompleteInstaller.psm1') -Raw $dc = @($components | Where-Object id -eq 'desktop-control') $checks.completeInstallerWiring = [ordered]@{ - componentsJsonEntry = ($dc.Count -eq 1 -and [string]$dc[0].version -eq '0.1.0' -and [bool]$dc[0].installable -and [string]$dc[0].isolation -eq 'A' -and [string]$dc[0].source -eq 'Payload/DesktopControl') + componentsJsonEntry = ($dc.Count -eq 1 -and [string]$dc[0].version -eq '0.1.1' -and [bool]$dc[0].installable -and [string]$dc[0].isolation -eq 'A' -and [string]$dc[0].source -eq 'Payload/DesktopControl') requiresWinapp = ($dc.Count -eq 1 -and @($dc[0].requires) -contains 'winapp') orderAfterWinapp = ($dc.Count -eq 1 -and [int]$dc[0].order -gt [int](@($components | Where-Object id -eq 'winapp')[0].order)) textProbeOnVersionStamp = ($dc.Count -eq 1 -and [string]$dc[0].probe.type -eq 'text' -and [string]$dc[0].probe.path -eq 'tools/desktop-control/VERSION') @@ -473,7 +473,7 @@ try { # === 25: reconcile compliance is measured against the SOURCE payload, never merely against # the target's own SHA256SUMS.txt. A changed payload at an unchanged component - # VERSION (0.1.0 == 0.1.0) must make the deployed target non-compliant, a Repair must + # VERSION (0.1.1 == 0.1.1) must make the deployed target non-compliant, a Repair must # redeploy the source state, and a second Repair with no further change is a NoOp. $regenSums = { param([string]$Root) @@ -502,17 +502,17 @@ try { Add-Content -LiteralPath (Join-Path $pkgRoot 'DesktopControl.Policy.psm1') -Value "`n# drifted in place, sums realigned" & $regenSums $pkgRoot $bTargetSumsSelfConsistent = Test-KIDesktopControlChecksums -PackageRoot $pkgRoot -ChecksumFile (Join-Path $pkgRoot 'SHA256SUMS.txt') - $bDeployed = Test-KIDesktopControlDeployed -TargetRoot $parityRoot -ExpectedVersion '0.1.0' -SourceRoot $srcCopy + $bDeployed = Test-KIDesktopControlDeployed -TargetRoot $parityRoot -ExpectedVersion '0.1.1' -SourceRoot $srcCopy $bRepair = Install-KIDesktopControl -PackageRoot $srcCopy -TargetRoot $parityRoot -Action 'Repair' $bParityAfter = Test-KIDesktopControlSourceParity -SourceRoot $srcCopy -TargetPackageRoot $pkgRoot - # C: the SOURCE payload changes while the component VERSION stays 0.1.0 => the (still intact, + # C: the SOURCE payload changes while the component VERSION stays 0.1.1 => the (still intact, # still self-consistent) target is now non-compliant because it no longer matches source. Add-Content -LiteralPath (Join-Path $srcCopy 'DesktopControl.Policy.psm1') -Value "`n# source payload changed, same VERSION" & $regenSums $srcCopy $cVersionsEqual = ((Get-Content -LiteralPath (Join-Path $srcCopy 'VERSION') -Raw).Trim() -eq (Get-Content -LiteralPath (Join-Path $pkgRoot 'VERSION') -Raw).Trim()) $cTargetStillSelfConsistent = Test-KIDesktopControlChecksums -PackageRoot $pkgRoot -ChecksumFile (Join-Path $pkgRoot 'SHA256SUMS.txt') - $cDeployed = Test-KIDesktopControlDeployed -TargetRoot $parityRoot -ExpectedVersion '0.1.0' -SourceRoot $srcCopy + $cDeployed = Test-KIDesktopControlDeployed -TargetRoot $parityRoot -ExpectedVersion '0.1.1' -SourceRoot $srcCopy # D: Repair rebuilds the target from the current source payload. $dRepair = Install-KIDesktopControl -PackageRoot $srcCopy -TargetRoot $parityRoot -Action 'Repair' @@ -523,7 +523,7 @@ try { # F: an extra, unexpected file under current\ => non-compliant; Repair drops it. Set-Content -LiteralPath (Join-Path $pkgRoot 'rogue-extra.txt') -Value 'x' -Encoding ascii -NoNewline - $fDeployed = Test-KIDesktopControlDeployed -TargetRoot $parityRoot -ExpectedVersion '0.1.0' -SourceRoot $srcCopy + $fDeployed = Test-KIDesktopControlDeployed -TargetRoot $parityRoot -ExpectedVersion '0.1.1' -SourceRoot $srcCopy $fRepair = Install-KIDesktopControl -PackageRoot $srcCopy -TargetRoot $parityRoot -Action 'Repair' $fDropped = -not (Test-Path -LiteralPath (Join-Path $pkgRoot 'rogue-extra.txt')) diff --git a/tools/desktop-control/current/VERSION b/tools/desktop-control/current/VERSION index 6e8bf73..17e51c3 100644 --- a/tools/desktop-control/current/VERSION +++ b/tools/desktop-control/current/VERSION @@ -1 +1 @@ -0.1.0 +0.1.1 diff --git a/tools/mcp-runtime/current/Config/mcp-runtime.config.json b/tools/mcp-runtime/current/Config/mcp-runtime.config.json index f9dab65..2070288 100644 --- a/tools/mcp-runtime/current/Config/mcp-runtime.config.json +++ b/tools/mcp-runtime/current/Config/mcp-runtime.config.json @@ -1,6 +1,6 @@ { "schemaVersion": "1.0", - "version": "0.1.0", + "version": "0.2.0", "targetRoot": "C:\\KI-Stack", "host": "127.0.0.1", "port": 8021, diff --git a/tools/mcp-runtime/current/Invoke-KIStackMcpRuntime.ps1 b/tools/mcp-runtime/current/Invoke-KIStackMcpRuntime.ps1 index 064b498..eda8b2a 100644 --- a/tools/mcp-runtime/current/Invoke-KIStackMcpRuntime.ps1 +++ b/tools/mcp-runtime/current/Invoke-KIStackMcpRuntime.ps1 @@ -4,6 +4,13 @@ param( [ValidateSet('Audit', 'Install', 'Upgrade', 'Repair', 'Validate', 'Status', 'Start', 'Stop', 'Rollback', 'Register', 'Unregister', 'RegistrationStatus', 'Uninstall')][string]$Action = 'Audit', [string]$TargetRoot = 'C:\KI-Stack', [string]$BackupPath, + # Optional, transaction-bound backup root for Install/Upgrade/Repair (mirrors Desktop + # Control's own 2.18.1 hotfix, Invoke-KIStackDesktopControl.ps1's -BackupRoot). When set, + # Install-KIMcpRuntime creates its backup exclusively under this root instead of its own + # standalone \backups\mcp-runtime\ scheme -- so a caller that owns its + # own recovery contract (the Complete Installer's transaction-scoped BackupRoot) gets a + # BackupPath its own recovery logic actually accepts. Omitted => unchanged standalone behavior. + [string]$BackupRoot, [string]$OpenWebUIEndpoint, [switch]$DryRun, [switch]$SkipUvCheck @@ -15,9 +22,9 @@ $result = switch ($Action) { 'Audit' { Test-KIMcpRuntime -PackageRoot $PSScriptRoot -TargetRoot $TargetRoot -SkipUvCheck:$SkipUvCheck } 'Validate' { Test-KIMcpRuntime -PackageRoot $PSScriptRoot -TargetRoot $TargetRoot -SkipUvCheck:$SkipUvCheck } 'Status' { Get-KIMcpRuntimeStatus -PackageRoot $PSScriptRoot -TargetRoot $TargetRoot } - 'Install' { Install-KIMcpRuntime -PackageRoot $PSScriptRoot -TargetRoot $TargetRoot -Action 'Install' -DryRun:$DryRun -SkipUvCheck:$SkipUvCheck } - 'Upgrade' { Install-KIMcpRuntime -PackageRoot $PSScriptRoot -TargetRoot $TargetRoot -Action 'Upgrade' -DryRun:$DryRun -SkipUvCheck:$SkipUvCheck } - 'Repair' { Install-KIMcpRuntime -PackageRoot $PSScriptRoot -TargetRoot $TargetRoot -Action 'Repair' -DryRun:$DryRun -SkipUvCheck:$SkipUvCheck } + 'Install' { Install-KIMcpRuntime -PackageRoot $PSScriptRoot -TargetRoot $TargetRoot -Action 'Install' -BackupRoot $BackupRoot -DryRun:$DryRun -SkipUvCheck:$SkipUvCheck } + 'Upgrade' { Install-KIMcpRuntime -PackageRoot $PSScriptRoot -TargetRoot $TargetRoot -Action 'Upgrade' -BackupRoot $BackupRoot -DryRun:$DryRun -SkipUvCheck:$SkipUvCheck } + 'Repair' { Install-KIMcpRuntime -PackageRoot $PSScriptRoot -TargetRoot $TargetRoot -Action 'Repair' -BackupRoot $BackupRoot -DryRun:$DryRun -SkipUvCheck:$SkipUvCheck } 'Start' { Start-KIMcpRuntime -PackageRoot $PSScriptRoot -TargetRoot $TargetRoot } 'Stop' { Stop-KIMcpRuntime -PackageRoot $PSScriptRoot -TargetRoot $TargetRoot } 'Rollback' { Restore-KIMcpRuntime -BackupPath $BackupPath -PackageRoot $PSScriptRoot -TargetRoot $TargetRoot } diff --git a/tools/mcp-runtime/current/MANIFEST.json b/tools/mcp-runtime/current/MANIFEST.json index af773a4..c4d09aa 100644 --- a/tools/mcp-runtime/current/MANIFEST.json +++ b/tools/mcp-runtime/current/MANIFEST.json @@ -2,7 +2,7 @@ "schemaVersion": "1.0", "packageId": "KI-STACK-MCP-RUNTIME", "name": "KI-Stack MCP Runtime", - "version": "0.1.0", + "version": "0.2.0", "status": "Development_SourceValidationPending", "containsSecrets": false, "runtimeGitDependency": false, diff --git a/tools/mcp-runtime/current/McpRuntime.psm1 b/tools/mcp-runtime/current/McpRuntime.psm1 index ac6511a..1efc05b 100644 --- a/tools/mcp-runtime/current/McpRuntime.psm1 +++ b/tools/mcp-runtime/current/McpRuntime.psm1 @@ -107,6 +107,130 @@ function Get-KIMcpRuntimePaths { } } +# --- Persistent package deployment (2.19 Phase 1 structural fix) ------------------------------- +# +# Through 2.19 Phase 1's first draft, this module's own code (this file, Scripts/*.py, Config/*, +# Vendor/*) was NEVER copied onto a real target at all: Start-KIMcpRuntime only ever launched +# whatever $PackageRoot happened to be at call time, and Install-KIMcpRuntime only ever wrote +# lifecycle bookkeeping (the marker/starter/stopper under modules/mcp-runtime, credential + +# workspace under state/mcp-runtime) -- never a stable package tree. Driven by the Complete +# Installer, $PackageRoot at install time is a TRANSACTION-SCOPED payload staging directory +# (\state\complete-installer\transactions\\payload\McpRuntime\..., +# see Runtime/KIStackPathContext.psm1's PayloadRoot) -- so the generated starter/stopper .cmd +# ended up hard-coding a path into THAT staging directory, and re-running Install with a changed +# payload at an unchanged component VERSION was silently treated as SkippedAlreadyCompliant +# (Test-KIMcpRuntime never compared deployed content to source). Both are now fixed: mcp-runtime +# gets the exact same persistent, source-parity-checked package tree WinApp and Desktop Control +# already have, at \tools\mcp-runtime\current\ -- Get-KIMcpRuntimeInstallPaths below, +# mirroring Get-KIDesktopControlInstallPaths (DesktopControl.psm1) function-for-function. This is +# ADDITIVE to the existing lifecycle paths above (modules/mcp-runtime, state/mcp-runtime), which +# are unchanged: state/mcp-runtime/{credential.json,workspace,mcp-runtime.pid} are runtime state, +# never payload -- Install-KIMcpRuntime never treats them as part of the deployed package, never +# backs them up as "payload", and never deletes them on Repair. + +function Get-KIMcpRuntimeInstallPaths { + # \tools\mcp-runtime\current\ -- the persistent, deployed copy of this component's + # own code. Deliberately simpler than Desktop Control's own Get-KIDesktopControlInstallPaths + # (no separate installRoot-level VERSION/marker stamp one level above current\): this + # component's own VERSION file, copied as part of the deployed package contents, already IS + # the stable, probeable version stamp -- COMPONENTS.json's existing marker/probe convention + # (modules/mcp-runtime/installation.json, the lifecycle marker above) is left untouched by + # this addition, so this stays a purely additive package/parity concern. + param([Parameter(Mandatory)][string]$TargetRoot) + $root = [IO.Path]::GetFullPath($TargetRoot) + $installRoot = [IO.Path]::Combine($root, 'tools', 'mcp-runtime') + $packageRoot = [IO.Path]::Combine($installRoot, 'current') + [pscustomobject]@{ + targetRoot = $root + installRoot = $installRoot + packageRoot = $packageRoot + versionStamp = [IO.Path]::Combine($packageRoot, 'VERSION') + checksums = [IO.Path]::Combine($packageRoot, 'SHA256SUMS.txt') + } +} + +$script:KIMcpRuntimeRequiredDeployedFiles = @( + 'VERSION', 'MANIFEST.json', 'SHA256SUMS.txt', 'Invoke-KIStackMcpRuntime.ps1', 'McpRuntime.psm1', + 'Config/mcp-runtime.config.json', 'Scripts/mcp_launcher.py', 'Scripts/ki_desktop_control_tools.py', + 'Vendor/KIStackOpenWebUICredential.psm1', 'Vendor/KIStackPathContext.psm1' +) + +function Test-KIMcpRuntimeChecksums { + # Verbatim pattern of Test-KIDesktopControlChecksums (DesktopControl.psm1): every line in + # SHA256SUMS.txt must resolve to an existing file under PackageRoot with a matching hash. + param([Parameter(Mandatory)][string]$PackageRoot, [Parameter(Mandatory)][string]$ChecksumFile) + if (-not (Test-Path -LiteralPath $ChecksumFile -PathType Leaf)) { return $false } + foreach ($line in Get-Content -LiteralPath $ChecksumFile) { + if ([string]::IsNullOrWhiteSpace($line)) { continue } + if ($line -notmatch '^([0-9a-fA-F]{64})\s+\*?(.+)$') { return $false } + $file = Join-Path $PackageRoot ($Matches[2].Replace('/', [IO.Path]::DirectorySeparatorChar)) + if (-not (Test-Path -LiteralPath $file -PathType Leaf)) { return $false } + if ((Get-FileHash -LiteralPath $file -Algorithm SHA256).Hash.ToLowerInvariant() -ne $Matches[1].ToLowerInvariant()) { return $false } + } + return $true +} + +function Get-KIMcpRuntimeDeployableFile { + # Verbatim pattern of Get-KIDesktopControlDeployableFile: every file that would actually be + # copied by Install-KIMcpRuntime (the whole tree minus a Payload staging dir, which is never + # deployed), as forward-slash relative paths so a source tree and a deployed target can be + # compared key-for-key. + param([Parameter(Mandatory)][string]$Root) + $full = [IO.Path]::GetFullPath($Root) + if (-not (Test-Path -LiteralPath $full -PathType Container)) { return @() } + @(Get-ChildItem -LiteralPath $full -Recurse -File -Force | ForEach-Object { + ($_.FullName.Substring($full.Length).TrimStart('\', '/') -replace '\\', '/') + } | Where-Object { $_ -ne 'Payload' -and $_ -notmatch '^Payload/' }) +} + +function Test-KIMcpRuntimeSourceParity { + # Verbatim pattern of Test-KIDesktopControlSourceParity: every PRODUCTIVELY DEPLOYED file must + # be byte-identical (SHA256) between the current source/payload tree and the deployed target -- + # this is what makes a changed payload at an UNCHANGED component VERSION correctly + # non-compliant instead of a silent Skip. + # missing target file -> not compliant + # changed file (hash drift) -> not compliant + # extra unexpected target file -> not compliant (a Repair must drop it) + param([Parameter(Mandatory)][string]$SourceRoot, [Parameter(Mandatory)][string]$TargetPackageRoot) + $srcFiles = @(Get-KIMcpRuntimeDeployableFile -Root $SourceRoot) + if ($srcFiles.Count -eq 0) { return [pscustomobject]@{ ok = $false; reason = 'source-root-empty-or-missing' } } + $tgtFiles = @(Get-KIMcpRuntimeDeployableFile -Root $TargetPackageRoot) + $hash = { param($p) (Get-FileHash -LiteralPath $p -Algorithm SHA256).Hash.ToLowerInvariant() } + foreach ($rel in $srcFiles) { + $native = $rel -replace '/', '\' + $tgt = Join-Path $TargetPackageRoot $native + if (-not (Test-Path -LiteralPath $tgt -PathType Leaf)) { return [pscustomobject]@{ ok = $false; reason = "missing-in-target:$rel" } } + if ((& $hash (Join-Path $SourceRoot $native)) -ne (& $hash $tgt)) { return [pscustomobject]@{ ok = $false; reason = "content-drift:$rel" } } + } + $extra = @($tgtFiles | Where-Object { $srcFiles -notcontains $_ }) + if ($extra.Count -gt 0) { return [pscustomobject]@{ ok = $false; reason = "unexpected-target-file:$($extra -join ',')" } } + [pscustomobject]@{ ok = $true; reason = 'ok'; fileCount = $srcFiles.Count } +} + +function Test-KIMcpRuntimeDeployed { + # On-disk compliance of the *deployed* package copy under \tools\mcp-runtime\. + # Verbatim pattern of Test-KIDesktopControlDeployed. Used for Install-KIMcpRuntime's own + # idempotency gate and available for the Complete Installer's own re-verification. + # -SourceRoot is optional: when given, every PRODUCTIVELY DEPLOYED file is compared + # Source <-> Target by SHA256 (Test-KIMcpRuntimeSourceParity), so a changed payload at an + # unchanged component VERSION is correctly reported non-compliant instead of skipped. Omitted + # => the previous, weaker "is the target internally self-consistent" behaviour only. + param([Parameter(Mandatory)][string]$TargetRoot, [string]$ExpectedVersion = '0.2.0', [string]$SourceRoot) + $p = Get-KIMcpRuntimeInstallPaths -TargetRoot $TargetRoot + if (-not (Test-Path -LiteralPath $p.packageRoot -PathType Container)) { return [pscustomobject]@{ ok = $false; reason = 'package-root-missing'; paths = $p } } + if (-not (Test-Path -LiteralPath $p.versionStamp -PathType Leaf)) { return [pscustomobject]@{ ok = $false; reason = 'version-stamp-missing'; paths = $p } } + if ((Get-Content -LiteralPath $p.versionStamp -Raw).Trim() -ne $ExpectedVersion) { return [pscustomobject]@{ ok = $false; reason = 'version-stamp-mismatch'; paths = $p } } + foreach ($rel in $script:KIMcpRuntimeRequiredDeployedFiles) { + if (-not (Test-Path -LiteralPath (Join-Path $p.packageRoot ($rel -replace '/', '\')) -PathType Leaf)) { return [pscustomobject]@{ ok = $false; reason = "required-file-missing:$rel"; paths = $p } } + } + if (-not (Test-KIMcpRuntimeChecksums -PackageRoot $p.packageRoot -ChecksumFile $p.checksums)) { return [pscustomobject]@{ ok = $false; reason = 'checksums-invalid'; paths = $p } } + if (-not [string]::IsNullOrWhiteSpace($SourceRoot)) { + $parity = Test-KIMcpRuntimeSourceParity -SourceRoot $SourceRoot -TargetPackageRoot $p.packageRoot + if (-not [bool]$parity.ok) { return [pscustomobject]@{ ok = $false; reason = "source-parity:$($parity.reason)"; paths = $p } } + } + [pscustomobject]@{ ok = $true; reason = 'ok'; paths = $p } +} + # --- Credential (own MCP-server API key; separate from Open Terminal's production key) ------- function ConvertFrom-KIMcpRuntimeSecureStringTransient { @@ -241,22 +365,50 @@ function Assert-KIMcpRuntimeManagedUv { } function Get-KIMcpRuntimeStartArguments { - # `uv run --with open-terminal[mcp] python ` -- resolves - # the [mcp] extra on demand (Phase 0 finding: not installed by default), never installs a - # second, separate copy of open-terminal itself. - param([Parameter(Mandatory)][object]$Config, [Parameter(Mandatory)][string]$LauncherPath, [Parameter(Mandatory)][string]$WorkspacePath, [string[]]$ArgumentsPrefix = @()) + # `uv run --with open-terminal[mcp] python ` + # -- resolves the [mcp] extra on demand (Phase 0 finding: not installed by default), never + # installs a second, separate copy of open-terminal itself. TargetRoot (2.19 Phase 1) is + # passed through as its own trailing argument so mcp_launcher.py can derive the Desktop + # Control dispatcher path (\tools\desktop-control\current\...) without having to + # reverse-engineer it from the workspace path. + param([Parameter(Mandatory)][object]$Config, [Parameter(Mandatory)][string]$LauncherPath, [Parameter(Mandatory)][string]$WorkspacePath, [Parameter(Mandatory)][string]$TargetRootPath, [string[]]$ArgumentsPrefix = @()) @($ArgumentsPrefix) + @( 'run', '--with', [string]$Config.packageSpec, 'python', $LauncherPath, - [string]$Config.host, [string][int]$Config.port, $WorkspacePath + [string]$Config.host, [string][int]$Config.port, $WorkspacePath, $TargetRootPath ) } +function Get-KIMcpRuntimeExpectedUiTools { + # Single source of truth for the PowerShell-side tool-surface check -- mirrors + # Scripts/ki_desktop_control_tools.py's own UI_TOOL_TO_OPERATION keys exactly (2.19 Phase 1). + @('ui_list_windows', 'ui_inspect_window', 'ui_find_element', 'ui_get_properties', 'ui_get_value', 'ui_screenshot', 'ui_wait_for', 'ui_set_value', 'ui_invoke', 'ui_focus') +} + +function Get-KIMcpRuntimeForbiddenUiTools { + # Never-exposed surface (raw input / global hotkeys / coordinate clicks / unverified scroll / + # raw winapp) -- a healthy MCP Runtime must never report any of these as a callable tool. + @('ui_scroll', 'ui_scroll_into_view', 'send_input', 'send_keys', 'global_hotkey', 'system_hotkey', 'coordinate_click', 'mouse_click_coordinate', 'drag', 'touch', 'pen', 'raw_winapp') +} + +function Get-KIMcpRuntimeBaselineOpenTerminalTools { + # Spot-check subset of Open Terminal's own OpenAPI-derived tools (Test-KIStackMcpRuntime.ps1's + # own validation-gate calls these by these exact names) -- proves the pre-existing surface + # was not displaced by adding the ui_* tools, without hardcoding Open Terminal's full tool list. + @('run_command', 'write_file', 'read_file', 'get_process_status', 'kill_process') +} + function Test-KIMcpRuntimeHealthy { # No /openapi.json-equivalent readiness endpoint exists for a native MCP server (Phase 0 # finding). The only meaningful readiness signal is a real MCP protocol round-trip: # initialize + list_tools. Shells out to the SAME `mcp` Python client library Open WebUI # itself uses (open_webui/utils/mcp/client.py), via the Open-WebUI venv's own python.exe -- # never a bare TCP-port check, which would pass even for a process that is up but 401-broken. + # + # 2.19 Phase 1: also verifies, from the SAME list_tools round-trip (no second connection), + # that Open Terminal's pre-existing tools are still present, all ten ui_* tools are present, + # and none of the never-exposed UI tool names are. `reachable` keeps its pre-2.19 meaning + # (a working MCP connection) so existing callers (Wait-/Start-/Get-Status) are unaffected; + # the new fields are purely additive. param([Parameter(Mandatory)][object]$Config, [Parameter(Mandatory)][Security.SecureString]$ApiKey, [int]$RequestTimeoutSeconds = 5, [string]$OpenWebUIPythonExe = 'C:\KI-Stack\python\venvs\openwebui\Scripts\python.exe') $uri = "http://$($Config.host):$($Config.port)/mcp" if (-not (Test-Path -LiteralPath $OpenWebUIPythonExe -PathType Leaf)) { @@ -276,7 +428,8 @@ async def main(): async with ClientSession(read, write) as session: await session.initialize() tools = await session.list_tools() - print(json.dumps({"ok": True, "toolCount": len(tools.tools)})) + names = sorted(t.name for t in tools.tools) + print(json.dumps({"ok": True, "toolCount": len(tools.tools), "toolNames": names})) asyncio.run(main()) '@ @@ -288,7 +441,22 @@ asyncio.run(main()) $global:LASTEXITCODE = 0 if ($exitCode -eq 0) { $parsed = $output | Select-Object -Last 1 | ConvertFrom-Json - [pscustomobject]@{ reachable = [bool]$parsed.ok; uri = $uri; toolCount = [int]$parsed.toolCount } + $toolNames = @($parsed.toolNames) + $missingUiTools = @(Get-KIMcpRuntimeExpectedUiTools | Where-Object { $toolNames -notcontains $_ }) + $presentForbiddenUiTools = @(Get-KIMcpRuntimeForbiddenUiTools | Where-Object { $toolNames -contains $_ }) + $missingOpenTerminalTools = @(Get-KIMcpRuntimeBaselineOpenTerminalTools | Where-Object { $toolNames -notcontains $_ }) + [pscustomobject]@{ + reachable = [bool]$parsed.ok + uri = $uri + toolCount = [int]$parsed.toolCount + toolNames = $toolNames + uiToolsPresent = ($missingUiTools.Count -eq 0) + missingUiTools = $missingUiTools + forbiddenUiToolsAbsent = ($presentForbiddenUiTools.Count -eq 0) + presentForbiddenUiTools = $presentForbiddenUiTools + openTerminalToolsPresent = ($missingOpenTerminalTools.Count -eq 0) + missingOpenTerminalTools = $missingOpenTerminalTools + } } else { [pscustomobject]@{ reachable = $false; uri = $uri; error = ($output -join "`n") } } @@ -341,7 +509,7 @@ function Start-KIMcpRuntime { $managedUv = Assert-KIMcpRuntimeManagedUv -TargetRoot $TargetRoot $launcherPath = Join-Path $PackageRoot 'Scripts/mcp_launcher.py' - $resolvedArguments = Get-KIMcpRuntimeStartArguments -Config $config -LauncherPath $launcherPath -WorkspacePath $paths.workspace -ArgumentsPrefix $managedUv.argumentsPrefix + $resolvedArguments = Get-KIMcpRuntimeStartArguments -Config $config -LauncherPath $launcherPath -WorkspacePath $paths.workspace -TargetRootPath $TargetRoot -ArgumentsPrefix $managedUv.argumentsPrefix $plainKey = $null $previousEnv = $env:OPEN_TERMINAL_API_KEY @@ -492,31 +660,66 @@ function Test-KIMcpRuntime { function Install-KIMcpRuntime { # Serves Install, Upgrade, and Repair alike -- a same-version re-run is a safe no-op via the - # SkippedAlreadyCompliant fast path (Install-KIOpenTerminal's exact reconcile contract). + # SkippedAlreadyCompliant fast path, but (2.19 Phase 1 structural fix) that fast path now + # requires BOTH the lifecycle bookkeeping (Test-KIMcpRuntime: marker/starter/stopper/ + # workspace/credential) AND the persistent package tree to actually match this run's own + # source content (Test-KIMcpRuntimeDeployed -SourceRoot $PackageRoot) -- a changed payload at + # an unchanged component VERSION is reconciled (Repair-shaped: clean re-deploy), never + # silently skipped, closing the exact drift class Desktop Control's own + # Test-KIDesktopControlSourceParity already closes for that component. param( [string]$PackageRoot = $PSScriptRoot, [string]$TargetRoot, [ValidateSet('Install', 'Upgrade', 'Repair')][string]$Action = 'Install', + # 2.18.1-pattern (Desktop Control's own hotfix, applied here for the same reason): + # optional, externally-owned backup root. When set, the backup is created EXCLUSIVELY + # under this root (a timestamped subfolder of it), never under the standalone + # \backups\mcp-runtime\ scheme -- so a caller with its own transaction-scoped + # recovery contract (the Complete Installer) gets a BackupPath its own recovery logic + # actually accepts. Omitted => unchanged standalone behavior. + [string]$BackupRoot, [switch]$DryRun, [switch]$SkipUvCheck ) $config = Get-KIMcpRuntimeConfig -PackageRoot $PackageRoot if ([string]::IsNullOrWhiteSpace($TargetRoot)) { $TargetRoot = [string]$config.targetRoot } + $expected = [string]$config.version + # Source/config version consistency guard -- mirrors Install-KIDesktopControl's own check -- + # so a source tree whose VERSION file and Config/mcp-runtime.config.json have drifted apart + # fails closed here rather than silently deploying a mislabeled package. + $sourceVersionFile = Join-Path $PackageRoot 'VERSION' + if (Test-Path -LiteralPath $sourceVersionFile -PathType Leaf) { + $sourceVersion = (Get-Content -LiteralPath $sourceVersionFile -Raw).Trim() + if ($sourceVersion -ne $expected) { throw "Quell-VERSION ($sourceVersion) und Config-Version ($expected) sind nicht deckungsgleich." } + } $paths = Get-KIMcpRuntimePaths -TargetRoot $TargetRoot - if ($DryRun) { return [pscustomobject]@{ passed = $true; status = 'DryRun'; action = $Action; plan = [pscustomobject]@{ moduleRoot = $paths.moduleRoot; stateRoot = $paths.stateRoot }; mutatesTarget = $false } } - - $existing = Test-KIMcpRuntime -PackageRoot $PackageRoot -TargetRoot $TargetRoot -SkipUvCheck:$SkipUvCheck - if ($existing.passed) { return [pscustomobject]@{ passed = $true; status = 'SkippedAlreadyCompliant'; action = $Action; marker = (Read-KIMcpRuntimeJson $paths.marker); mutatesTarget = $false } } + $installPaths = Get-KIMcpRuntimeInstallPaths -TargetRoot $TargetRoot + if ($DryRun) { return [pscustomobject]@{ passed = $true; status = 'DryRun'; action = $Action; plan = [pscustomobject]@{ moduleRoot = $paths.moduleRoot; stateRoot = $paths.stateRoot; packageRoot = $installPaths.packageRoot }; mutatesTarget = $false } } + + # Idempotency gate -- BOTH dimensions must already be correct, or this is not a no-op. + $packageDeployed = Test-KIMcpRuntimeDeployed -TargetRoot $TargetRoot -ExpectedVersion $expected -SourceRoot $PackageRoot + $lifecycleReady = Test-KIMcpRuntime -PackageRoot $PackageRoot -TargetRoot $TargetRoot -SkipUvCheck:$SkipUvCheck + if ([bool]$packageDeployed.ok -and [bool]$lifecycleReady.passed) { + return [pscustomobject]@{ passed = $true; status = 'SkippedAlreadyCompliant'; action = $Action; marker = (Read-KIMcpRuntimeJson $paths.marker); mutatesTarget = $false } + } if (-not $SkipUvCheck) { Assert-KIMcpRuntimeManagedUv -TargetRoot $TargetRoot | Out-Null } + $markerExistedBefore = Test-Path -LiteralPath $paths.marker -PathType Leaf + New-KIMcpRuntimeDirectory $installPaths.installRoot New-KIMcpRuntimeDirectory $paths.moduleRoot New-KIMcpRuntimeDirectory $paths.stateRoot New-KIMcpRuntimeDirectory $paths.workspace - $backupRoot = Join-Path $TargetRoot ('backups/mcp-runtime/' + [DateTime]::UtcNow.ToString('yyyyMMdd-HHmmss-fffffff')) + $backupRootBase = if (-not [string]::IsNullOrWhiteSpace($BackupRoot)) { $BackupRoot } else { Join-Path $TargetRoot 'backups/mcp-runtime' } + $backupRoot = Join-Path $backupRootBase ([DateTime]::UtcNow.ToString('yyyyMMdd-HHmmss-fffffff')) New-KIMcpRuntimeDirectory $backupRoot + # Backs up the deployed PACKAGE tree ('current', which already contains its own VERSION and + # SHA256SUMS.txt) and the LIFECYCLE bookkeeping (marker/starter/stopper). Deliberately never + # includes state/mcp-runtime (credential.json, workspace, mcp-runtime.pid) -- that is runtime + # state, never payload, and must survive an Upgrade/Repair untouched. $items = @() foreach ($definition in @( + @{ path = $installPaths.packageRoot; name = 'current' }, @{ path = $paths.marker; name = 'installation.json' }, @{ path = $paths.starter; name = 'Start-KIStack-McpRuntime.cmd' }, @{ path = $paths.stopper; name = 'Stop-KIStack-McpRuntime.cmd' } )) { $items += @(Copy-KIMcpRuntimeBackupItem -Path $definition.path -BackupRoot $backupRoot -Name $definition.name) } @@ -524,19 +727,51 @@ function Install-KIMcpRuntime { Write-KIMcpRuntimeJson $backupPath ([ordered]@{ schemaVersion = '1.0'; createdAtUtc = [DateTime]::UtcNow.ToString('o'); targetRoot = $TargetRoot; items = $items }) try { - $invokeScript = Join-Path $PackageRoot 'Invoke-KIStackMcpRuntime.ps1' + # Clean re-deploy of the package tree (a Repair must drop drifted/extra files) -- + # mirrors Install-KIDesktopControl's own "wipe current\, copy everything except Payload" + # shape exactly. + if (Test-Path -LiteralPath $installPaths.packageRoot) { Remove-Item -LiteralPath $installPaths.packageRoot -Recurse -Force } + New-KIMcpRuntimeDirectory $installPaths.packageRoot + Get-ChildItem -LiteralPath $PackageRoot -Force | Where-Object { $_.Name -ne 'Payload' } | + Copy-Item -Destination $installPaths.packageRoot -Recurse -Force + + if (-not (Test-KIMcpRuntimeChecksums -PackageRoot $installPaths.packageRoot -ChecksumFile $installPaths.checksums)) { + throw 'SHA256SUMS.txt der deployten Komponente stimmt nicht mit dem kopierten Inhalt ueberein (fail closed).' + } + foreach ($rel in $script:KIMcpRuntimeRequiredDeployedFiles) { + if (-not (Test-Path -LiteralPath (Join-Path $installPaths.packageRoot ($rel -replace '/', '\')) -PathType Leaf)) { throw "Pflichtdatei fehlt nach dem Deploy: $rel" } + } + + # Starter/stopper reference the PERSISTENT, deployed package root -- never $PackageRoot + # (which, driven by the Complete Installer, is a transaction-scoped payload staging + # directory that is never guaranteed to still exist by the time these scripts next run). + $invokeScript = Join-Path $installPaths.packageRoot 'Invoke-KIStackMcpRuntime.ps1' [IO.File]::WriteAllText($paths.starter, (Get-KIMcpRuntimeStarterScriptContent -InvokeScriptPath $invokeScript -TargetRoot $TargetRoot -Action 'Start'), [Text.UTF8Encoding]::new($false)) [IO.File]::WriteAllText($paths.stopper, (Get-KIMcpRuntimeStarterScriptContent -InvokeScriptPath $invokeScript -TargetRoot $TargetRoot -Action 'Stop'), [Text.UTF8Encoding]::new($false)) Assert-KIMcpRuntimeApiKey -TargetRoot $TargetRoot -Bytes ([int]$config.apiKeyBytes) | Out-Null - $marker = [ordered]@{ schemaVersion = '1.0'; version = [string]$config.version; host = [string]$config.host; port = [int]$config.port; installedAtUtc = [DateTime]::UtcNow.ToString('o') } + $deployedFileCount = @(Get-ChildItem -LiteralPath $installPaths.packageRoot -Recurse -File).Count + $marker = [ordered]@{ schemaVersion = '1.0'; version = $expected; host = [string]$config.host; port = [int]$config.port; installedAtUtc = [DateTime]::UtcNow.ToString('o'); deployedFileCount = $deployedFileCount } Write-KIMcpRuntimeJson $paths.marker $marker - $readback = Test-KIMcpRuntime -PackageRoot $PackageRoot -TargetRoot $TargetRoot -SkipUvCheck:$SkipUvCheck - if (-not $readback.passed) { throw 'MCP-Runtime-Readback nach Installation ist fehlgeschlagen.' } + + $readback = Test-KIMcpRuntime -PackageRoot $installPaths.packageRoot -TargetRoot $TargetRoot -SkipUvCheck:$SkipUvCheck + if (-not $readback.passed) { throw 'MCP-Runtime-Readback (Lifecycle) nach Installation ist fehlgeschlagen.' } + $packageReadback = Test-KIMcpRuntimeDeployed -TargetRoot $TargetRoot -ExpectedVersion $expected -SourceRoot $PackageRoot + if (-not [bool]$packageReadback.ok) { throw "MCP-Runtime-Readback (Package) nach Installation ist fehlgeschlagen: $($packageReadback.reason)" } + $resultStatus = switch ($Action) { 'Upgrade' { 'Upgraded' }; 'Repair' { 'Repaired' }; default { 'Installed' } } - [pscustomobject]@{ passed = $true; status = $resultStatus; action = $Action; marker = $marker; backupPath = $backupPath; readback = $readback; mutatesTarget = $true } + [pscustomobject]@{ passed = $true; status = $resultStatus; action = $Action; marker = $marker; backupPath = $backupPath; deployedFileCount = $deployedFileCount; readback = $readback; mutatesTarget = $true } } catch { $rollbackStatus = 'Failed' try { $rollback = Restore-KIMcpRuntimeBackup -BackupPath $backupPath; $rollbackStatus = [string]$rollback.status } catch {} + if (-not $markerExistedBefore) { + # A fresh install that failed partway must not leave a half-deployed package/module + # tree behind -- mirrors Install-KIDesktopControl's own catch-block cleanup. + foreach ($cleanupPath in @($installPaths.packageRoot, $paths.moduleRoot)) { + if ((-not (Test-Path -LiteralPath $paths.marker -PathType Leaf)) -and (Test-Path -LiteralPath $cleanupPath)) { + Remove-Item -LiteralPath $cleanupPath -Recurse -Force -ErrorAction SilentlyContinue + } + } + } $_.Exception.Data['KIStackRollbackStatus'] = $rollbackStatus $_.Exception.Data['KIStackBackupPath'] = $backupPath throw @@ -551,13 +786,16 @@ function Restore-KIMcpRuntime { function Uninstall-KIMcpRuntime { # Stops the process (if running), unregisters ONLY this module's own Open-WebUI tool-server # entry (never touches any other entry -- see Unregister-KIMcpRuntimeOpenWebUI), then removes - # this module's own module/state trees. Never touches Open Terminal's production install. + # this module's own module/state/package trees. Never touches Open Terminal's production + # install. Removes the persistent \tools\mcp-runtime\ package tree (2.19 Phase 1) + # in addition to the pre-existing modules/state trees. param([string]$PackageRoot = $PSScriptRoot, [string]$TargetRoot = 'C:\KI-Stack') $paths = Get-KIMcpRuntimePaths -TargetRoot $TargetRoot + $installPaths = Get-KIMcpRuntimeInstallPaths -TargetRoot $TargetRoot $stopResult = Stop-KIMcpRuntime -PackageRoot $PackageRoot -TargetRoot $TargetRoot $unregisterResult = $null try { $unregisterResult = Unregister-KIMcpRuntimeOpenWebUI -PackageRoot $PackageRoot -TargetRoot $TargetRoot } catch { $unregisterResult = [pscustomobject]@{ passed = $false; error = $_.Exception.Message } } - foreach ($path in @($paths.moduleRoot, $paths.stateRoot)) { + foreach ($path in @($paths.moduleRoot, $paths.stateRoot, $installPaths.installRoot)) { if (Test-Path -LiteralPath $path) { Remove-Item -LiteralPath $path -Recurse -Force -ErrorAction SilentlyContinue } } [pscustomobject]@{ passed = $true; status = 'Uninstalled'; stopResult = $stopResult; unregisterResult = $unregisterResult; mutatesTarget = $true } diff --git a/tools/mcp-runtime/current/README.md b/tools/mcp-runtime/current/README.md index 35a705a..9909035 100644 --- a/tools/mcp-runtime/current/README.md +++ b/tools/mcp-runtime/current/README.md @@ -74,7 +74,11 @@ implementiert Install/Repair/Health eigenständig (Phase-1-Recherchebefund). `Test-KIStackMcpRuntime.ps1` — 13-Punkte-Check (Server läuft, Health, Open-WebUI-Registrierung, Tool Discovery, `run_command`/Exitcode/Dateien/Prozesssteuerung/`kill_process` direkt gegen den -MCP-Server, ein echter Open-WebUI-Agententest **mit `stream:true`**, vollständiger Cleanup danach). +MCP-Server, ein echter Open-WebUI-Agententest **mit `stream:true`**, vollständiger Cleanup danach), +seit 2.19 Phase 1 zusätzlich um drei `ui_*`-Tool-Oberflächen-Checks aus demselben +`list_tools`-Roundtrip ergänzt (Open-Terminal-Basistools weiterhin vorhanden, alle 10 `ui_*` +Tools vorhanden, keine verbotenen UI-Tools exponiert). Die `ui_*`-Transportlogik selbst hat ihre +eigene, dispatcherfreie Unit-Test-Suite: `Scripts/test_ki_desktop_control_tools.py`. **Wichtig:** Ein Test mit `stream:false` ist laut Phase-0-Befund kein gültiger Nachweis für einen MCP-Runtime-Defekt — Open WebUIs `non_streaming_chat_response_handler` durchläuft die @@ -82,6 +86,25 @@ Tool-Ausführungsschleife für keinen Tool-Typ, unabhängig von MCP. Deshalb erz Validation-Gate-Test an der einzigen Stelle, die einen echten Agentenzyklus über Open WebUI prüft, explizit `stream:true`. +## Desktop Control (`ui_*` Tools, 2.19 Phase 1) + +`Scripts/mcp_launcher.py` registriert zusätzlich zum unveränderten Open-Terminal-`OpenAPIProvider` +zehn native FastMCP-Tools (`Scripts/ki_desktop_control_tools.py`, `register_ui_tools`) auf +DERSELBEN `FastMCP`-Instanz — kein zweiter Server, kein zweiter Port, kein zweiter Prozess: +`ui_list_windows`, `ui_inspect_window`, `ui_find_element`, `ui_get_properties`, `ui_get_value`, +`ui_screenshot`, `ui_wait_for`, `ui_set_value`, `ui_invoke`, `ui_focus`. Jedes Tool ist ein +reiner Transport auf `tools/desktop-control/current/Invoke-KIStackDesktopControl.ps1` — Policy, +Zielauflösung und Verifikation bleiben vollständig dort. Details, Kontrakt und die bewusst nicht +exponierte Oberfläche (`scroll`, `send_input`, `raw_winapp`, ...) siehe +`tools/desktop-control/current/MCP-INTEGRATION.md`. + +Der Dispatcher wird ausschließlich unter `\tools\desktop-control\current\ +Invoke-KIStackDesktopControl.ps1` gesucht (kein PATH-Fallback) — `TargetRoot` wird dafür seit +2.19 als viertes Argument an `mcp_launcher.py` durchgereicht (`Get-KIMcpRuntimeStartArguments`). +Fehlt der Dispatcher, schlägt jeder `ui_*`-Aufruf einzeln mit einem strukturierten MCP-Fehler +fehl; ein bereits vom Dispatcher selbst gemeldetes `success:false` (z. B. +`SecretContextBlocked`, `PostconditionNotProven`, `ResolverError`) wird unverändert durchgereicht. + ## Bekannte Grenzen (Phase 1, dokumentiert, nicht blockierend) - Working-Directory-Persistenz über getrennte `run_command`-Aufrufe: nicht unterstützt (Baseline- diff --git a/tools/mcp-runtime/current/SHA256SUMS.txt b/tools/mcp-runtime/current/SHA256SUMS.txt index 471ec46..7ecb9a2 100644 --- a/tools/mcp-runtime/current/SHA256SUMS.txt +++ b/tools/mcp-runtime/current/SHA256SUMS.txt @@ -1,12 +1,15 @@ -bba0892b6f94ca8841c802c4828c7147d4bb9e12e6b03649bf69f94c246c856d *Config/mcp-runtime.config.json -163f8e67b28485f04aebba62fd4dfa69a26c65a0de7995371e21dc50818ddb75 *Invoke-KIStackMcpRuntime.ps1 +b1f64c941eb71b617d9f9a0a14577d2cc8fc00a0d587f97106808fab0c29f9bd *Config/mcp-runtime.config.json +3bc087da182623be53d69d5f7035bd4826f0bd978496ea5d2c8a01a171adff44 *Invoke-KIStackMcpRuntime.ps1 15c2b390693a8c5d2d5004cdfe4f45cf8529a6ac3d7a4cf77edfab1087388fef *LOCAL-CONTROL-CONTRACT.md -411cc950adccc771cea467aaf9ab6dbac240d050e1816383e930dbd6b3a0b6cc *MANIFEST.json -39195e5b1c61b64d67eea6442a38f32726f72257572a603caf9bfbcdac06da96 *McpRuntime.psm1 -9568d8fb1f17865b2ed59072a02d98d6c3840f4e5a2ec93a55060df7f9c67d8b *README.md -c61abd5942e83902e56fe604fa9412239f872c88b7e1c1b7916c7fc2e0e2a249 *Scripts/mcp_launcher.py +89c31c7cf845ac447f23c65786b578d0707f33a276e674f00013e048839624cb *MANIFEST.json +1b3d6b57394b23e5bdaaecebf463bd88277c23659bc386a2ed3dfc0d4e5111db *McpRuntime.psm1 +0664bece0eb86227272051442b17c65fbb6db48bf1eacc8c275bb20209bbc783 *README.md +a07de0eb796e165c92f6bf7296d641766f185cee3b92b759012b9870b750fa11 *Scripts/mcp_launcher.py +c3a9dc95a1679a4cfba457f6dfd0cb854e76714df436ce9339ce2e857201d3d1 *Scripts/ki_desktop_control_tools.py +75a86a331a86ff5c75c284ffa1a3e13b7d3e0fb7e636e453c46e34b0558a5197 *Scripts/test_ki_desktop_control_tools.py 97e78b81eed2284549ee165b0f74bdd041199879e09a9ec2effcb3cda1950399 *Test-KIStackLMStudioRuntimeBaseline.ps1 -89a7f0dd55c92d917ba0870d31f4db88834edf6fb29f09f7080af7136d901a6b *Test-KIStackMcpRuntime.ps1 +e1c2f5284d6e503aa36382316e09e507f60e7f55534fee2095ba5ca23761d8cd *Test-KIStackMcpRuntime.ps1 +4cdf6449f64eb23b2b12861b6b3c606b83fe8b9ee1bf153e3b85e9c77f3342a2 *Test-KIStackMcpRuntimeInstall.ps1 6af0d029842a60460b14b5ff7b786fe264c32eeec185bde5350d93afb171bfd8 *Vendor/KIStackOpenWebUICredential.psm1 79b082d875a35f8fed88b45d863d6b7b1db3b8846f5e540b9a028db13b833098 *Vendor/KIStackPathContext.psm1 -3d097c315a1c543040f548da4c7051265e94c77211a2411ced1d4621344270de *VERSION +c58a749f70f5e1ba5394ebcdbfcfa02fa938c4a173d8e41e6c476a7c8b2acddd *VERSION diff --git a/tools/mcp-runtime/current/Scripts/ki_desktop_control_tools.py b/tools/mcp-runtime/current/Scripts/ki_desktop_control_tools.py new file mode 100644 index 0000000..1f96ad1 --- /dev/null +++ b/tools/mcp-runtime/current/Scripts/ki_desktop_control_tools.py @@ -0,0 +1,370 @@ +"""KI-Stack Desktop Control tools -- native FastMCP tool layer, KI-Stack 2.19 Phase 1. + +Exposes a flat `ui_*` tool surface on the SAME FastMCP instance `mcp_launcher.py` already +starts (no second MCP server, no second port, no second runtime process). Every tool here is a +thin transport that maps exactly onto one existing Desktop Control operation and calls the +existing, productive dispatcher: + + \\tools\\desktop-control\\current\\Invoke-KIStackDesktopControl.ps1 + +Desktop Control's own Resolve -> Validate -> Act -> Re-observe -> Verify pipeline +(tools/desktop-control/current/DesktopControl.psm1 + DesktopControl.Policy.psm1) makes every +policy decision. This module never re-implements or duplicates any of it: it does not classify +operations, does not check window/element contracts, does not decide what is a secret. It only +(1) builds a RequestJson object from typed tool parameters, (2) invokes the dispatcher, and +(3) returns the dispatcher's own JSON result unchanged -- including a business-level failure +such as `SecretContextBlocked`, `PostconditionNotProven`, or `ResolverError`, which is valid +JSON and is passed through exactly as-is, never turned into an MCP tool error. + +An MCP tool error (`fastmcp.exceptions.ToolError`) is raised ONLY for a transport-level failure: +the dispatcher script is missing (checked at exactly one fixed path -- no PATH fallback, ever), +`pwsh.exe` cannot be resolved, the process could not be started or timed out, or stdout does not +parse as a JSON object. A non-zero dispatcher exit code alone is NOT such a failure: the +dispatcher's own `Invoke-KIStackDesktopControl.ps1` (see its own tail) exits 1 for every +business-level `success:false` result too (e.g. `SecretContextBlocked`), so a valid JSON object +on stdout is always returned to the caller regardless of exit code. + +Only the ten semantic, already-productive UIA operations are exposed. `scroll` / `scroll_into_view` +(backend capability unverified, fails closed inside Desktop Control itself) and any raw +send-input / send-keys / global-hotkey / coordinate-click / drag / touch / pen / raw-winapp +surface are deliberately never wired up here -- there is no tool function for them at all. +""" + +from __future__ import annotations + +import json +import os +import shutil +import subprocess +from typing import Any + +from fastmcp import FastMCP +from fastmcp.exceptions import ToolError +from pydantic import BaseModel + +# Fixed, single lookup path for the dispatcher -- relative to the MCP Runtime's own TargetRoot. +# Never a PATH search, never a second candidate location (LOCAL-CONTROL-CONTRACT-style +# fail-closed dependency: see MCP-INTEGRATION.md). +_DISPATCHER_RELATIVE_PARTS = ("tools", "desktop-control", "current", "Invoke-KIStackDesktopControl.ps1") + +# Wall-clock ceiling for one dispatcher invocation. Desktop Control's own config +# (Config/desktop-control.config.json) caps window/element resolution and postcondition waits at +# 8-10s each; 45s leaves headroom for a mutating operation's Resolve+Act+Re-observe+Verify +# sequence without hanging an MCP tool call indefinitely on a wedged dispatcher. +_DISPATCHER_TIMEOUT_SECONDS = 45 + +# The exact, flat tool-name -> Desktop-Control-operation mapping. Single source of truth for +# both the tool registration below and its own test suite (test_ki_desktop_control_tools.py) -- +# asserting against this dict is how "no namespace/prefix drift" is verified mechanically rather +# than by hand-reading ten function definitions. +UI_TOOL_TO_OPERATION: dict[str, str] = { + "ui_list_windows": "list_windows", + "ui_inspect_window": "inspect_window", + "ui_find_element": "find_element", + "ui_get_properties": "get_properties", + "ui_get_value": "get_value", + "ui_screenshot": "screenshot", + "ui_wait_for": "wait_for", + "ui_set_value": "set_value", + "ui_invoke": "invoke", + "ui_focus": "focus", +} + + +class UiElementIdentity(BaseModel): + """Identity of one UI Automation element, re-matched fresh on every call. + + A bare `selector` alone is a transport detail, never durable identity -- Desktop Control's + own Target Contract (DesktopControl.Policy.psm1) rejects a stale selector-only identity on a + mutating operation. Prefer `automationId` / `name` / `controlType` / `className`. + """ + + automationId: str | None = None + name: str | None = None + controlType: str | None = None + className: str | None = None + selector: str | None = None + + +def _compact(values: dict[str, Any]) -> dict[str, Any]: + """Drops None entries so RequestJson only carries fields the caller actually supplied.""" + return {key: value for key, value in values.items() if value is not None} + + +def _resolve_pwsh() -> str: + """Resolves pwsh.exe: managed PowerShell-7 install path first, PATH as the last resort. + + (This is a generic PowerShell-7 host lookup, not the Desktop-Control-dispatcher lookup -- + that one below is intentionally never allowed a PATH fallback.) + """ + program_files = os.environ.get("ProgramFiles", r"C:\Program Files") + candidate = os.path.join(program_files, "PowerShell", "7", "pwsh.exe") + if os.path.isfile(candidate): + return candidate + found = shutil.which("pwsh.exe") or shutil.which("pwsh") + if found: + return found + raise ToolError( + "PowerShell 7 (pwsh.exe) wurde nicht gefunden -- Desktop Control kann nicht aufgerufen werden." + ) + + +def resolve_dispatcher_path(target_root: str) -> str: + """The one, fixed Desktop Control dispatcher path under TargetRoot. No PATH fallback.""" + return os.path.join(target_root, *_DISPATCHER_RELATIVE_PARTS) + + +def run_desktop_control_operation( + operation: str, request: dict[str, Any], target_root: str +) -> dict[str, Any]: + """Invokes Invoke-KIStackDesktopControl.ps1 and returns its JSON result verbatim. + + Raises `fastmcp.exceptions.ToolError` only for a transport-level failure (dispatcher + missing, pwsh missing, process could not run, stdout not valid JSON). See the module + docstring for why a non-zero exit code alone is not such a failure. + """ + dispatcher = resolve_dispatcher_path(target_root) + if not os.path.isfile(dispatcher): + raise ToolError( + f"Desktop Control Dispatcher nicht gefunden unter '{dispatcher}'. Kein PATH-Fallback -- " + "Desktop Control muss unter \\tools\\desktop-control\\current installiert und " + "compliant sein (siehe Invoke-KIStackDesktopControl.ps1 -Action Validate)." + ) + pwsh = _resolve_pwsh() + request_json = json.dumps(request or {}) + arguments = [ + pwsh, + "-NoLogo", + "-NoProfile", + "-ExecutionPolicy", + "Bypass", + "-File", + dispatcher, + "-Operation", + operation, + "-RequestJson", + request_json, + "-TargetRoot", + target_root, + ] + try: + completed = subprocess.run( + arguments, + capture_output=True, + text=True, + timeout=_DISPATCHER_TIMEOUT_SECONDS, + encoding="utf-8", + errors="replace", + ) + except subprocess.TimeoutExpired as exc: + raise ToolError( + f"Desktop Control Dispatcher hat innerhalb von {_DISPATCHER_TIMEOUT_SECONDS}s nicht " + f"geantwortet (Operation '{operation}')." + ) from exc + except OSError as exc: + raise ToolError(f"Desktop Control Dispatcher konnte nicht gestartet werden: {exc}") from exc + + stdout = (completed.stdout or "").strip() + if not stdout: + stderr_excerpt = (completed.stderr or "").strip()[:2000] + raise ToolError( + f"Desktop Control Dispatcher lieferte keine Ausgabe (exitCode={completed.returncode}, " + f"Operation '{operation}'). stderr: {stderr_excerpt}" + ) + try: + result = json.loads(stdout) + except json.JSONDecodeError as exc: + raise ToolError( + f"Desktop Control Dispatcher lieferte kein gueltiges JSON (exitCode={completed.returncode}, " + f"Operation '{operation}'): {exc}. stdout (gekuerzt): {stdout[:2000]}" + ) from exc + if not isinstance(result, dict): + raise ToolError( + f"Desktop Control Dispatcher lieferte kein JSON-Objekt (exitCode={completed.returncode}, " + f"Operation '{operation}'). stdout (gekuerzt): {stdout[:2000]}" + ) + return result + + +def register_ui_tools(mcp: FastMCP, target_root: str) -> None: + """Registers the flat ui_* tool surface on the given (already-running) FastMCP instance. + + Adds ten native FastMCP tools alongside whatever providers/tools the caller already + registered (in practice, mcp_launcher.py's Open-Terminal OpenAPIProvider) -- it never + replaces or wraps them. + """ + + def _call(operation: str, **fields: Any) -> dict[str, Any]: + return run_desktop_control_operation(operation, _compact(fields), target_root) + + def _element_payload(element: UiElementIdentity | None) -> dict[str, Any] | None: + return element.model_dump(exclude_none=True) if element is not None else None + + @mcp.tool + def ui_list_windows(application: str | None = None) -> dict[str, Any]: + """List top-level windows, optionally filtered by application/process name. Read-only.""" + return _call("list_windows", application=application) + + @mcp.tool + def ui_inspect_window( + application: str | None = None, + hwnd: str | None = None, + titlePattern: str | None = None, + expectedProcessName: str | None = None, + ) -> dict[str, Any]: + """Resolve exactly one window (fresh, never cached) and return its UI Automation element tree. Read-only.""" + return _call( + "inspect_window", + application=application, + hwnd=hwnd, + titlePattern=titlePattern, + expectedProcessName=expectedProcessName, + ) + + @mcp.tool + def ui_find_element( + application: str | None = None, + hwnd: str | None = None, + titlePattern: str | None = None, + expectedProcessName: str | None = None, + element: UiElementIdentity | None = None, + ) -> dict[str, Any]: + """Find all elements in one window's current tree matching the given identity. Read-only.""" + return _call( + "find_element", + application=application, + hwnd=hwnd, + titlePattern=titlePattern, + expectedProcessName=expectedProcessName, + element=_element_payload(element), + ) + + @mcp.tool + def ui_get_properties( + application: str | None = None, + hwnd: str | None = None, + titlePattern: str | None = None, + expectedProcessName: str | None = None, + element: UiElementIdentity | None = None, + ) -> dict[str, Any]: + """Read the current UI Automation properties of exactly one, unambiguously identified element. Read-only.""" + return _call( + "get_properties", + application=application, + hwnd=hwnd, + titlePattern=titlePattern, + expectedProcessName=expectedProcessName, + element=_element_payload(element), + ) + + @mcp.tool + def ui_get_value( + application: str | None = None, + hwnd: str | None = None, + titlePattern: str | None = None, + expectedProcessName: str | None = None, + element: UiElementIdentity | None = None, + ) -> dict[str, Any]: + """Read one element's current value. Read-only. Blocked by the secret/credential guard for a secret-context element.""" + return _call( + "get_value", + application=application, + hwnd=hwnd, + titlePattern=titlePattern, + expectedProcessName=expectedProcessName, + element=_element_payload(element), + ) + + @mcp.tool + def ui_screenshot( + application: str | None = None, + hwnd: str | None = None, + titlePattern: str | None = None, + expectedProcessName: str | None = None, + ) -> dict[str, Any]: + """Capture a screenshot of exactly one, unambiguously resolved window as evidence. Read-only.""" + return _call( + "screenshot", + application=application, + hwnd=hwnd, + titlePattern=titlePattern, + expectedProcessName=expectedProcessName, + ) + + @mcp.tool + def ui_wait_for( + element: UiElementIdentity, + application: str | None = None, + hwnd: str | None = None, + titlePattern: str | None = None, + expectedProcessName: str | None = None, + timeoutMs: int | None = None, + ) -> dict[str, Any]: + """Poll a fresh UI Automation tree until an element identity appears. Read-only.""" + return _call( + "wait_for", + application=application, + hwnd=hwnd, + titlePattern=titlePattern, + expectedProcessName=expectedProcessName, + element=_element_payload(element), + timeoutMs=timeoutMs, + ) + + @mcp.tool + def ui_set_value( + element: UiElementIdentity, + value: str, + application: str | None = None, + hwnd: str | None = None, + titlePattern: str | None = None, + expectedProcessName: str | None = None, + ) -> dict[str, Any]: + """Set one element's value; succeeds only once independently verified by readback. Mutating.""" + return _call( + "set_value", + application=application, + hwnd=hwnd, + titlePattern=titlePattern, + expectedProcessName=expectedProcessName, + element=_element_payload(element), + value=value, + ) + + @mcp.tool + def ui_invoke( + element: UiElementIdentity, + application: str | None = None, + hwnd: str | None = None, + titlePattern: str | None = None, + expectedProcessName: str | None = None, + expectTreeChange: str | None = None, + ) -> dict[str, Any]: + """Invoke (click-equivalent) one element; succeeds only once an independently observable tree change is proven. Mutating.""" + return _call( + "invoke", + application=application, + hwnd=hwnd, + titlePattern=titlePattern, + expectedProcessName=expectedProcessName, + element=_element_payload(element), + expectTreeChange=expectTreeChange, + ) + + @mcp.tool + def ui_focus( + element: UiElementIdentity, + application: str | None = None, + hwnd: str | None = None, + titlePattern: str | None = None, + expectedProcessName: str | None = None, + ) -> dict[str, Any]: + """Focus one element; succeeds only once independently confirmed via get-focused. Mutating.""" + return _call( + "focus", + application=application, + hwnd=hwnd, + titlePattern=titlePattern, + expectedProcessName=expectedProcessName, + element=_element_payload(element), + ) diff --git a/tools/mcp-runtime/current/Scripts/mcp_launcher.py b/tools/mcp-runtime/current/Scripts/mcp_launcher.py index cd6fab3..bbc10cd 100644 --- a/tools/mcp-runtime/current/Scripts/mcp_launcher.py +++ b/tools/mcp-runtime/current/Scripts/mcp_launcher.py @@ -1,7 +1,9 @@ """KI-Stack MCP Runtime launcher. Starts Open Terminal's FastAPI app as a native MCP server (streamable-http), -with a correctly auth-configured internal bridge client. +with a correctly auth-configured internal bridge client, and (2.19 Phase 1) +KI-Stack's own flat `ui_*` Desktop Control tool surface on the SAME FastMCP +instance -- same process, same port, no second MCP server. Why this file exists (Phase 0 finding, docs/proposals/2.15-mcp-foundation.md): `open-terminal mcp` alone (FastMCP.from_fastapi(app=app)) builds its internal @@ -16,27 +18,44 @@ This is a STARTUP-TIME configuration wrapper, not a tool-execution or proxy layer: it does not intercept, inspect, or transform any tool call. Open WebUI -still talks directly to this MCP server as an ordinary native MCP client. +still talks directly to this MCP server as an ordinary native MCP client. The +`ui_*` tools added below are plain native FastMCP tools registered on the same +`mcp_server` object via its own public `@mcp_server.tool` decorator (the exact +mechanism `fastmcp.server.server.FastMCP` documents for adding local tools to +an existing server) -- not a second provider, not a second bridge client, and +not a proxy in front of Open Terminal's OpenAPIProvider tools. -Usage: python mcp_launcher.py +Usage: python mcp_launcher.py The API key is read exclusively from the OPEN_TERMINAL_API_KEY environment variable (never a command-line argument, so it never appears in a process command-line listing), and is never written to disk by this script. +`target_root` is this MCP Runtime's own TargetRoot (see +McpRuntime.psm1's Get-KIMcpRuntimePaths) -- Desktop Control's dispatcher is +looked up at exactly `\\tools\\desktop-control\\current\\ +Invoke-KIStackDesktopControl.ps1`, never via PATH. """ import os import sys -host, port, cwd = sys.argv[1], int(sys.argv[2]), sys.argv[3] +host, port, cwd, target_root = sys.argv[1], int(sys.argv[2]), sys.argv[3], sys.argv[4] api_key = os.environ["OPEN_TERMINAL_API_KEY"] os.chdir(cwd) -import httpx as httpx2 # fastmcp's OpenAPIProvider expects an httpx2-compatible client +import httpx2 # fastmcp>=4.0.3's OpenAPIProvider expects a real httpx2.AsyncClient -- passing a +# plain httpx.AsyncClient still works but raises FastMCPDeprecationWarning ("will be removed in a +# future release"). httpx2 is a genuine, separate PyPI package (not an alias for httpx) already +# resolved transitively by this launcher's own `open-terminal[mcp]==0.11.34` pin (it is fastmcp's +# own dependency) -- verified 2026-09-13, `python -c "import httpx2; print(httpx2.__version__)"` +# under that exact packageSpec resolves httpx2==2.12.0, with the same AsyncClient/ASGITransport +# surface this launcher already uses. No new dependency, no packageSpec change needed. from fastmcp import FastMCP from fastmcp.server.providers.openapi import OpenAPIProvider from open_terminal.main import app # import after cwd/env are set, matching the `open-terminal mcp` CLI's own order +from ki_desktop_control_tools import register_ui_tools # sibling module in this same Scripts/ dir + client = httpx2.AsyncClient( transport=httpx2.ASGITransport(app=app), base_url="http://fastapi", @@ -45,5 +64,6 @@ provider = OpenAPIProvider(openapi_spec=app.openapi(), client=client) mcp_server = FastMCP(name="KI-Stack MCP Runtime (Open Terminal)", providers=[provider]) +register_ui_tools(mcp_server, target_root) mcp_server.run(transport="streamable-http", host=host, port=port) diff --git a/tools/mcp-runtime/current/Scripts/test_ki_desktop_control_tools.py b/tools/mcp-runtime/current/Scripts/test_ki_desktop_control_tools.py new file mode 100644 index 0000000..2508040 --- /dev/null +++ b/tools/mcp-runtime/current/Scripts/test_ki_desktop_control_tools.py @@ -0,0 +1,283 @@ +"""Unit tests for ki_desktop_control_tools.py (KI-Stack MCP Runtime, 2.19 Phase 1). + +No GUI, no real dispatcher, no real winapp: `subprocess.run` is mocked throughout. These tests +cover exactly the Phase-1 test list (see MCP-INTEGRATION.md): exact tool names with no +namespace/prefix drift, request -> Desktop-Control-operation mapping, invalid JSON, a missing +dispatcher, a non-zero dispatcher exit, `SecretContextBlocked`/`PostconditionNotProven` passed +through unchanged, a read-only and a mutating success, and that no raw-winapp/send-input surface +is ever registered. + +Run (from this Scripts/ directory) against the SAME dependency resolution +`Config/mcp-runtime.config.json`'s `packageSpec` actually produces at runtime -- never a +hand-picked fastmcp version. `open-terminal[mcp]==0.11.34` resolves fastmcp 4.0.3 as of this +writing (verified 2026-09-13; re-verify after any `packageSpec` version bump, since a pinned +extra can still resolve a different transitive version over time): + + uv run --with "open-terminal[mcp]==0.11.34" python -m unittest test_ki_desktop_control_tools.py -v +""" + +from __future__ import annotations + +import asyncio +import json +import subprocess +import unittest +from unittest import mock + +from fastmcp import FastMCP +from fastmcp.exceptions import ToolError + +import ki_desktop_control_tools as uitools + +FAKE_TARGET_ROOT = r"C:\FakeKIStack" +FAKE_DISPATCHER = uitools.resolve_dispatcher_path(FAKE_TARGET_ROOT) + + +def _completed(stdout: str = "", stderr: str = "", returncode: int = 0) -> subprocess.CompletedProcess: + return subprocess.CompletedProcess(args=["pwsh"], returncode=returncode, stdout=stdout, stderr=stderr) + + +class RunDesktopControlOperationTests(unittest.TestCase): + """Direct tests of the transport function, independent of FastMCP tool registration.""" + + def setUp(self) -> None: + self._isfile_patch = mock.patch("os.path.isfile") + self.mock_isfile = self._isfile_patch.start() + self.addCleanup(self._isfile_patch.stop) + + def isfile_side_effect(path: str) -> bool: + return path == FAKE_DISPATCHER + + self.mock_isfile.side_effect = isfile_side_effect + + self._pwsh_patch = mock.patch.object(uitools, "_resolve_pwsh", return_value=r"C:\pwsh.exe") + self._pwsh_patch.start() + self.addCleanup(self._pwsh_patch.stop) + + def test_dispatcher_missing_raises_tool_error_no_path_fallback(self) -> None: + self.mock_isfile.side_effect = lambda path: False + with self.assertRaises(ToolError) as ctx: + uitools.run_desktop_control_operation("list_windows", {}, FAKE_TARGET_ROOT) + self.assertIn(FAKE_DISPATCHER, str(ctx.exception)) + self.assertIn("PATH-Fallback", str(ctx.exception)) + + def test_invalid_json_raises_tool_error(self) -> None: + with mock.patch.object(uitools.subprocess, "run", return_value=_completed(stdout="not json", returncode=0)): + with self.assertRaises(ToolError) as ctx: + uitools.run_desktop_control_operation("list_windows", {}, FAKE_TARGET_ROOT) + self.assertIn("gueltiges JSON", str(ctx.exception)) + + def test_empty_stdout_with_nonzero_exit_raises_tool_error(self) -> None: + # A real crash: dispatcher exits non-zero AND produces no parseable JSON at all. + with mock.patch.object(uitools.subprocess, "run", return_value=_completed(stdout="", stderr="boom", returncode=1)): + with self.assertRaises(ToolError) as ctx: + uitools.run_desktop_control_operation("list_windows", {}, FAKE_TARGET_ROOT) + self.assertIn("boom", str(ctx.exception)) + + def test_nonzero_exit_with_valid_json_is_not_an_error_secret_context_blocked(self) -> None: + # Invoke-KIStackDesktopControl.ps1 exits 1 for every business-level success:false too -- + # a valid JSON object must be returned unchanged, never turned into an MCP error. + payload = { + "schemaVersion": "1.0", "operation": "get_value", "mode": "mutating", + "success": False, "status": "SecretContextBlocked", "blockedReason": "secret", + } + with mock.patch.object(uitools.subprocess, "run", return_value=_completed(stdout=json.dumps(payload), returncode=1)): + result = uitools.run_desktop_control_operation("get_value", {}, FAKE_TARGET_ROOT) + self.assertEqual(result, payload) + + def test_nonzero_exit_with_valid_json_is_not_an_error_postcondition_not_proven(self) -> None: + payload = { + "schemaVersion": "1.0", "operation": "set_value", "mode": "mutating", + "success": False, "status": "PostconditionNotProven", "blockedReason": "readback failed", + } + with mock.patch.object(uitools.subprocess, "run", return_value=_completed(stdout=json.dumps(payload), returncode=1)): + result = uitools.run_desktop_control_operation("set_value", {}, FAKE_TARGET_ROOT) + self.assertEqual(result, payload) + + def test_read_only_success_returned_unchanged(self) -> None: + payload = {"schemaVersion": "1.0", "operation": "list_windows", "mode": "read-only", "success": True, "status": "OK", "result": {"windowCount": 1, "windows": [{"hwnd": "1001"}]}} + with mock.patch.object(uitools.subprocess, "run", return_value=_completed(stdout=json.dumps(payload), returncode=0)) as mock_run: + result = uitools.run_desktop_control_operation("list_windows", {"application": "notepad"}, FAKE_TARGET_ROOT) + self.assertEqual(result, payload) + called_args = mock_run.call_args.args[0] + self.assertIn("-Operation", called_args) + self.assertEqual(called_args[called_args.index("-Operation") + 1], "list_windows") + self.assertIn("-RequestJson", called_args) + self.assertEqual(json.loads(called_args[called_args.index("-RequestJson") + 1]), {"application": "notepad"}) + self.assertIn("-TargetRoot", called_args) + self.assertEqual(called_args[called_args.index("-TargetRoot") + 1], FAKE_TARGET_ROOT) + self.assertEqual(called_args[called_args.index("-File") + 1], FAKE_DISPATCHER) + + def test_mutating_success_returned_unchanged(self) -> None: + payload = {"schemaVersion": "1.0", "operation": "invoke", "mode": "mutating", "success": True, "status": "OK", "postcondition": {"proven": True}} + with mock.patch.object(uitools.subprocess, "run", return_value=_completed(stdout=json.dumps(payload), returncode=0)): + result = uitools.run_desktop_control_operation("invoke", {"element": {"automationId": "OkButton"}}, FAKE_TARGET_ROOT) + self.assertEqual(result, payload) + + def test_timeout_raises_tool_error(self) -> None: + with mock.patch.object(uitools.subprocess, "run", side_effect=subprocess.TimeoutExpired(cmd="pwsh", timeout=45)): + with self.assertRaises(ToolError): + uitools.run_desktop_control_operation("list_windows", {}, FAKE_TARGET_ROOT) + + def test_process_start_failure_raises_tool_error(self) -> None: + with mock.patch.object(uitools.subprocess, "run", side_effect=OSError("no such file")): + with self.assertRaises(ToolError): + uitools.run_desktop_control_operation("list_windows", {}, FAKE_TARGET_ROOT) + + +class RegisterUiToolsSurfaceTests(unittest.TestCase): + """Verifies the registered tool surface itself: names, no drift, no forbidden tools.""" + + def setUp(self) -> None: + self.mcp = FastMCP(name="test-server") + uitools.register_ui_tools(self.mcp, FAKE_TARGET_ROOT) + + def _tool_names(self) -> set[str]: + tools = asyncio.run(self.mcp.list_tools()) + return {t.name for t in tools} + + def test_exact_tool_names_no_drift(self) -> None: + expected = set(uitools.UI_TOOL_TO_OPERATION.keys()) + self.assertEqual(self._tool_names(), expected) + self.assertEqual( + expected, + { + "ui_list_windows", "ui_inspect_window", "ui_find_element", "ui_get_properties", + "ui_get_value", "ui_screenshot", "ui_wait_for", "ui_set_value", "ui_invoke", "ui_focus", + }, + ) + + def test_no_forbidden_ui_surface_registered(self) -> None: + forbidden = { + "ui_scroll", "ui_scroll_into_view", "send_input", "send_keys", "global_hotkey", + "system_hotkey", "coordinate_click", "mouse_click_coordinate", "drag", "touch", + "pen", "raw_winapp", "desktop_control", + } + self.assertEqual(self._tool_names() & forbidden, set()) + + def test_flat_prefix_no_namespacing(self) -> None: + for name in self._tool_names(): + self.assertTrue(name.startswith("ui_"), name) + self.assertNotIn(":", name) + self.assertNotIn(".", name) + + +class RegisterUiToolsMappingTests(unittest.TestCase): + """Each ui_* tool maps to exactly one Desktop-Control operation with the right RequestJson.""" + + def setUp(self) -> None: + self.mcp = FastMCP(name="test-server") + uitools.register_ui_tools(self.mcp, FAKE_TARGET_ROOT) + self._run_patch = mock.patch.object(uitools, "run_desktop_control_operation", return_value={"success": True}) + self.mock_run = self._run_patch.start() + self.addCleanup(self._run_patch.stop) + + def _fn(self, tool_name: str): + tools = {t.name: t for t in asyncio.run(self.mcp.list_tools())} + return tools[tool_name].fn + + def test_ui_list_windows_maps_to_list_windows(self) -> None: + self._fn("ui_list_windows")(application="notepad") + operation, request, target_root = self.mock_run.call_args.args + self.assertEqual(operation, "list_windows") + self.assertEqual(request, {"application": "notepad"}) + self.assertEqual(target_root, FAKE_TARGET_ROOT) + + def test_ui_list_windows_omits_none_fields(self) -> None: + self._fn("ui_list_windows")(application=None) + _, request, _ = self.mock_run.call_args.args + self.assertEqual(request, {}) + + def test_ui_inspect_window_maps_to_inspect_window(self) -> None: + self._fn("ui_inspect_window")(hwnd="1001", titlePattern="Editor") + operation, request, _ = self.mock_run.call_args.args + self.assertEqual(operation, "inspect_window") + self.assertEqual(request, {"hwnd": "1001", "titlePattern": "Editor"}) + + def test_ui_find_element_maps_element_identity(self) -> None: + element = uitools.UiElementIdentity(automationId="TextBox1", name="Body") + self._fn("ui_find_element")(hwnd="1001", element=element) + operation, request, _ = self.mock_run.call_args.args + self.assertEqual(operation, "find_element") + self.assertEqual(request, {"hwnd": "1001", "element": {"automationId": "TextBox1", "name": "Body"}}) + + def test_ui_get_properties_maps_to_get_properties(self) -> None: + element = uitools.UiElementIdentity(selector="sel-1") + self._fn("ui_get_properties")(hwnd="1001", element=element) + operation, _, _ = self.mock_run.call_args.args + self.assertEqual(operation, "get_properties") + + def test_ui_get_value_maps_to_get_value(self) -> None: + element = uitools.UiElementIdentity(automationId="TextBox1") + self._fn("ui_get_value")(hwnd="1001", element=element) + operation, _, _ = self.mock_run.call_args.args + self.assertEqual(operation, "get_value") + + def test_ui_screenshot_maps_to_screenshot(self) -> None: + self._fn("ui_screenshot")(hwnd="1001") + operation, request, _ = self.mock_run.call_args.args + self.assertEqual(operation, "screenshot") + self.assertEqual(request, {"hwnd": "1001"}) + + def test_ui_wait_for_maps_timeout_and_element(self) -> None: + element = uitools.UiElementIdentity(name="Ready") + self._fn("ui_wait_for")(element=element, hwnd="1001", timeoutMs=5000) + operation, request, _ = self.mock_run.call_args.args + self.assertEqual(operation, "wait_for") + self.assertEqual(request, {"hwnd": "1001", "element": {"name": "Ready"}, "timeoutMs": 5000}) + + def test_ui_set_value_maps_value_and_element(self) -> None: + element = uitools.UiElementIdentity(automationId="TextBox1") + self._fn("ui_set_value")(element=element, value="hello", hwnd="1001") + operation, request, _ = self.mock_run.call_args.args + self.assertEqual(operation, "set_value") + self.assertEqual(request, {"hwnd": "1001", "element": {"automationId": "TextBox1"}, "value": "hello"}) + + def test_ui_invoke_maps_expect_tree_change(self) -> None: + element = uitools.UiElementIdentity(automationId="OkButton") + self._fn("ui_invoke")(element=element, hwnd="1001", expectTreeChange="Saved") + operation, request, _ = self.mock_run.call_args.args + self.assertEqual(operation, "invoke") + self.assertEqual(request, {"hwnd": "1001", "element": {"automationId": "OkButton"}, "expectTreeChange": "Saved"}) + + def test_ui_focus_maps_to_focus(self) -> None: + element = uitools.UiElementIdentity(automationId="TextBox1") + self._fn("ui_focus")(element=element, hwnd="1001") + operation, request, _ = self.mock_run.call_args.args + self.assertEqual(operation, "focus") + self.assertEqual(request, {"hwnd": "1001", "element": {"automationId": "TextBox1"}}) + + def test_every_mapped_operation_is_in_the_allowed_desktop_control_set(self) -> None: + allowed = { + "list_windows", "inspect_window", "find_element", "get_properties", "get_value", + "screenshot", "wait_for", "set_value", "invoke", "focus", + } + self.assertEqual(set(uitools.UI_TOOL_TO_OPERATION.values()), allowed) + + +class ToolResultPassthroughTests(unittest.TestCase): + """End-to-end through FastMCP's own tool.run(): a real dispatcher JSON result survives the + full FunctionTool schema-validation + serialization path unchanged as structured_content.""" + + def setUp(self) -> None: + self.mcp = FastMCP(name="test-server") + uitools.register_ui_tools(self.mcp, FAKE_TARGET_ROOT) + + def _tool(self, name: str): + tools = {t.name: t for t in asyncio.run(self.mcp.list_tools())} + return tools[name] + + def test_read_only_result_passes_through_tool_run(self) -> None: + payload = {"schemaVersion": "1.0", "operation": "list_windows", "success": True, "status": "OK", "result": {"windowCount": 0, "windows": []}} + with mock.patch.object(uitools, "run_desktop_control_operation", return_value=payload): + result = asyncio.run(self._tool("ui_list_windows").run({"application": "notepad"})) + self.assertEqual(result.structured_content, payload) + + def test_dispatcher_missing_surfaces_as_tool_error_through_tool_run(self) -> None: + with mock.patch("os.path.isfile", return_value=False): + with self.assertRaises(ToolError): + asyncio.run(self._tool("ui_list_windows").run({})) + + +if __name__ == "__main__": + unittest.main() diff --git a/tools/mcp-runtime/current/Test-KIStackMcpRuntime.ps1 b/tools/mcp-runtime/current/Test-KIStackMcpRuntime.ps1 index f2c0db9..43b6812 100644 --- a/tools/mcp-runtime/current/Test-KIStackMcpRuntime.ps1 +++ b/tools/mcp-runtime/current/Test-KIStackMcpRuntime.ps1 @@ -52,6 +52,11 @@ try { # 4. Tool Discovery erfolgreich (bereits ueber Health-Check-toolCount indirekt belegt, hier explizit erneut pruefen) Add-KIMcpRuntimeCheck 'Tool Discovery (>0 Tools gefunden)' ([int]$health.toolCount -gt 0) "toolCount=$($health.toolCount)" + # 4a-4c. Desktop-Control ui_* Tool-Oberflaeche (2.19 Phase 1) -- aus demselben list_tools-Roundtrip + Add-KIMcpRuntimeCheck 'Open-Terminal-Basistools weiterhin vorhanden' ([bool]$health.openTerminalToolsPresent) ("missing=" + ($health.missingOpenTerminalTools -join ',')) + Add-KIMcpRuntimeCheck 'Alle 10 ui_* Tools vorhanden' ([bool]$health.uiToolsPresent) ("missing=" + ($health.missingUiTools -join ',')) + Add-KIMcpRuntimeCheck 'Keine verbotenen UI-Tools exponiert' ([bool]$health.forbiddenUiToolsAbsent) ("present=" + ($health.presentForbiddenUiTools -join ',')) + # 5-11: run_command / Exitcode / write_file / read_file / Prozess starten / get_process_status / kill_process # -- ueber einen direkten MCP-Client-Aufruf gegen den gerade gestarteten Server, NICHT ueber # OpenWebUI (das ist Punkt 12) -- isoliert die MCP-Runtime-Komponente selbst von der diff --git a/tools/mcp-runtime/current/Test-KIStackMcpRuntimeInstall.ps1 b/tools/mcp-runtime/current/Test-KIStackMcpRuntimeInstall.ps1 new file mode 100644 index 0000000..e9b8dad --- /dev/null +++ b/tools/mcp-runtime/current/Test-KIStackMcpRuntimeInstall.ps1 @@ -0,0 +1,212 @@ +[CmdletBinding()] +param([string]$PackageRoot = $PSScriptRoot) + +Set-StrictMode -Version Latest +$ErrorActionPreference = 'Stop' + +# Self-contained contract/unit suite for McpRuntime.psm1's persistent-package deployment logic +# (2.19 Phase 1 structural fix). NO live infra, NO real uv/Python/MCP process, NO Payload/*.zip -- +# everything runs against a scratch copy of the real source tree and a scratch TargetRoot, +# mirroring Test-KIStackDesktopControl.ps1's own scratch/fake style exactly. This is the +# self-contained counterpart to the real, payload-zip-based +# Test-KIStackMcpRuntimeCompleteInstallerIntegration.ps1 (which needs a built Payload/McpRuntime/ +# *.zip and is therefore not part of this suite). + +Import-Module (Join-Path $PackageRoot 'McpRuntime.psm1') -Force + +$fail = [Collections.Generic.List[string]]::new() +$checks = [ordered]@{} +$scratchBase = Join-Path ([IO.Path]::GetTempPath()) ('KIMcpInstall-' + [guid]::NewGuid().ToString('N').Substring(0, 10)) +New-Item -ItemType Directory -Path $scratchBase -Force | Out-Null + +function New-KIMcpInstallScratchSource { + # A real, self-contained copy of this component's own current source tree -- so a mutation + # test can safely edit ITS OWN scratch copy without ever touching the real repository. + param([Parameter(Mandatory)][string]$Destination) + New-Item -ItemType Directory -Path $Destination -Force | Out-Null + Get-ChildItem -LiteralPath $PackageRoot -Force | Where-Object { $_.Name -notin @('Payload') } | + Copy-Item -Destination $Destination -Recurse -Force + # Re-generate SHA256SUMS.txt from THIS scratch copy so it is internally self-consistent + # (the real repo's own checksums file only covers the real repo's own current byte content). + $sumsPath = Join-Path $Destination 'SHA256SUMS.txt' + $lines = Get-ChildItem -LiteralPath $Destination -Recurse -File | Where-Object { $_.Name -ne 'SHA256SUMS.txt' } | Sort-Object { $_.FullName } | ForEach-Object { + $rel = ($_.FullName.Substring($Destination.Length).TrimStart('\', '/') -replace '\\', '/') + "$((Get-FileHash -LiteralPath $_.FullName -Algorithm SHA256).Hash.ToLowerInvariant()) *$rel" + } + [IO.File]::WriteAllLines($sumsPath, $lines, [Text.UTF8Encoding]::new($false)) + $Destination +} + +$src = New-KIMcpInstallScratchSource -Destination (Join-Path $scratchBase 'source') + +try { + # === 1: fresh target -> real Install, package deployed persistently, starter points at it === + $t1 = Join-Path $scratchBase 'target-fresh' + $install1 = Install-KIMcpRuntime -PackageRoot $src -TargetRoot $t1 -Action Install -SkipUvCheck + $installPaths1 = Get-KIMcpRuntimeInstallPaths -TargetRoot $t1 + $paths1 = Get-KIMcpRuntimePaths -TargetRoot $t1 + $starterContent1 = Get-Content -LiteralPath $paths1.starter -Raw + $checks.freshInstallDeploysPersistentPackage = [ordered]@{ + installPassed = [bool]$install1.passed + installStatus = ([string]$install1.status -eq 'Installed') + packageRootExists = (Test-Path -LiteralPath $installPaths1.packageRoot -PathType Container) + versionStampCorrect = ((Get-Content -LiteralPath $installPaths1.versionStamp -Raw).Trim() -eq (Get-Content -LiteralPath (Join-Path $src 'VERSION') -Raw).Trim()) + mcpModulePresent = (Test-Path -LiteralPath (Join-Path $installPaths1.packageRoot 'McpRuntime.psm1') -PathType Leaf) + launcherPresent = (Test-Path -LiteralPath (Join-Path $installPaths1.packageRoot 'Scripts/mcp_launcher.py') -PathType Leaf) + uiToolsModulePresent = (Test-Path -LiteralPath (Join-Path $installPaths1.packageRoot 'Scripts/ki_desktop_control_tools.py') -PathType Leaf) + } + if ($checks.freshInstallDeploysPersistentPackage.Values -contains $false) { $fail.Add('freshInstallDeploysPersistentPackage failed: ' + ($checks.freshInstallDeploysPersistentPackage | ConvertTo-Json -Compress)) } + + $checks.starterScriptHasNoStagingOrRepoPath = [ordered]@{ + referencesDeployedPackageRoot = $starterContent1.Contains($installPaths1.packageRoot) + neverReferencesScratchSourceRoot = (-not $starterContent1.Contains($src)) + neverReferencesTransactionStaging = ($starterContent1 -notmatch '(?i)state[\\/]complete-installer[\\/]transactions') + neverReferencesRealRepoPath = (-not $starterContent1.Contains($PackageRoot)) + } + if ($checks.starterScriptHasNoStagingOrRepoPath.Values -contains $false) { $fail.Add('starterScriptHasNoStagingOrRepoPath failed: ' + ($checks.starterScriptHasNoStagingOrRepoPath | ConvertTo-Json -Compress)) } + + # === 2: same version, same payload -> Skip (no re-copy, no churn) ========================== + $beforeUpgradeHash = (Get-FileHash -LiteralPath (Join-Path $installPaths1.packageRoot 'McpRuntime.psm1') -Algorithm SHA256).Hash + $upgrade1 = Install-KIMcpRuntime -PackageRoot $src -TargetRoot $t1 -Action Upgrade -SkipUvCheck + $afterUpgradeHash = (Get-FileHash -LiteralPath (Join-Path $installPaths1.packageRoot 'McpRuntime.psm1') -Algorithm SHA256).Hash + $checks.sameVersionSamePayloadSkips = [ordered]@{ + upgradePassed = [bool]$upgrade1.passed + upgradeStatus = ([string]$upgrade1.status -eq 'SkippedAlreadyCompliant') + packageUntouched = ($beforeUpgradeHash -eq $afterUpgradeHash) + } + if ($checks.sameVersionSamePayloadSkips.Values -contains $false) { $fail.Add('sameVersionSamePayloadSkips failed: ' + ($checks.sameVersionSamePayloadSkips | ConvertTo-Json -Compress)) } + + # === 3: THE core bug fix -- same version, CHANGED payload -> reconciled, never skipped ====== + $srcMutated = New-KIMcpInstallScratchSource -Destination (Join-Path $scratchBase 'source-mutated') + $marker = "# mutation-marker-$([guid]::NewGuid().ToString('N'))" + Add-Content -LiteralPath (Join-Path $srcMutated 'McpRuntime.psm1') -Value $marker -Encoding utf8 + # Re-sync SHA256SUMS.txt after the mutation (the source itself must stay internally valid -- + # this test proves DEPLOYED-vs-SOURCE drift detection, not a corrupt-source scenario). + $sumsPathMutated = Join-Path $srcMutated 'SHA256SUMS.txt' + $linesMutated = Get-ChildItem -LiteralPath $srcMutated -Recurse -File | Where-Object { $_.Name -ne 'SHA256SUMS.txt' } | Sort-Object { $_.FullName } | ForEach-Object { + $rel = ($_.FullName.Substring($srcMutated.Length).TrimStart('\', '/') -replace '\\', '/') + "$((Get-FileHash -LiteralPath $_.FullName -Algorithm SHA256).Hash.ToLowerInvariant()) *$rel" + } + [IO.File]::WriteAllLines($sumsPathMutated, $linesMutated, [Text.UTF8Encoding]::new($false)) + + $preComplianceCheck = Test-KIMcpRuntimeDeployed -TargetRoot $t1 -ExpectedVersion (Get-Content -LiteralPath (Join-Path $srcMutated 'VERSION') -Raw).Trim() -SourceRoot $srcMutated + $upgrade2 = Install-KIMcpRuntime -PackageRoot $srcMutated -TargetRoot $t1 -Action Upgrade -SkipUvCheck + $deployedModuleContent = Get-Content -LiteralPath (Join-Path $installPaths1.packageRoot 'McpRuntime.psm1') -Raw + $checks.sameVersionChangedPayloadReconciles = [ordered]@{ + preCheckCorrectlyNonCompliant = (-not [bool]$preComplianceCheck.ok) + preCheckReasonIsContentDrift = ([string]$preComplianceCheck.reason -match '^source-parity:content-drift:') + upgradePassed = [bool]$upgrade2.passed + upgradeStatusNotSkipped = ([string]$upgrade2.status -ne 'SkippedAlreadyCompliant') + deployedContentActuallyUpdated = $deployedModuleContent.Contains($marker) + } + if ($checks.sameVersionChangedPayloadReconciles.Values -contains $false) { $fail.Add('sameVersionChangedPayloadReconciles failed: ' + ($checks.sameVersionChangedPayloadReconciles | ConvertTo-Json -Compress)) } + + # === 4: missing deployed file -> reconciled ================================================ + Remove-Item -LiteralPath (Join-Path $installPaths1.packageRoot 'Scripts/ki_desktop_control_tools.py') -Force + $missingCheck = Test-KIMcpRuntimeDeployed -TargetRoot $t1 -ExpectedVersion (Get-Content -LiteralPath (Join-Path $srcMutated 'VERSION') -Raw).Trim() -SourceRoot $srcMutated + $upgrade3 = Install-KIMcpRuntime -PackageRoot $srcMutated -TargetRoot $t1 -Action Upgrade -SkipUvCheck + $checks.missingDeployedFileReconciles = [ordered]@{ + preCheckDetectsMissing = (-not [bool]$missingCheck.ok) + upgradePassed = [bool]$upgrade3.passed + upgradeStatusNotSkipped = ([string]$upgrade3.status -ne 'SkippedAlreadyCompliant') + fileRestored = (Test-Path -LiteralPath (Join-Path $installPaths1.packageRoot 'Scripts/ki_desktop_control_tools.py') -PathType Leaf) + } + if ($checks.missingDeployedFileReconciles.Values -contains $false) { $fail.Add('missingDeployedFileReconciles failed: ' + ($checks.missingDeployedFileReconciles | ConvertTo-Json -Compress)) } + + # === 5: extra, unexpected deployed file -> reconciled (Repair drops it) ==================== + $extraFile = Join-Path $installPaths1.packageRoot 'Scripts/unexpected-leftover.py' + Set-Content -LiteralPath $extraFile -Value '# should not survive a repair' -Encoding utf8 + $extraCheck = Test-KIMcpRuntimeDeployed -TargetRoot $t1 -ExpectedVersion (Get-Content -LiteralPath (Join-Path $srcMutated 'VERSION') -Raw).Trim() -SourceRoot $srcMutated + $repair1 = Install-KIMcpRuntime -PackageRoot $srcMutated -TargetRoot $t1 -Action Repair -SkipUvCheck + $checks.extraDeployedFileReconciles = [ordered]@{ + preCheckDetectsExtra = (-not [bool]$extraCheck.ok) + preCheckReasonIsUnexpectedFile = ([string]$extraCheck.reason -match '^source-parity:unexpected-target-file:') + repairPassed = [bool]$repair1.passed + extraFileDropped = (-not (Test-Path -LiteralPath $extraFile -PathType Leaf)) + } + if ($checks.extraDeployedFileReconciles.Values -contains $false) { $fail.Add('extraDeployedFileReconciles failed: ' + ($checks.extraDeployedFileReconciles | ConvertTo-Json -Compress)) } + + # === 6: credential/workspace/pid are never treated as payload, never deleted on Repair ====== + $paths1Fresh = Get-KIMcpRuntimePaths -TargetRoot $t1 + Set-Content -LiteralPath (Join-Path $paths1Fresh.workspace 'user-created-file.txt') -Value 'must survive' -Encoding utf8 + $credentialBefore = Get-KIMcpRuntimeCredential -TargetRoot $t1 + $keyBefore = ConvertFrom-KIMcpRuntimeSecureStringTransient -Value $credentialBefore.apiKey + $repair2 = Install-KIMcpRuntime -PackageRoot $srcMutated -TargetRoot $t1 -Action Repair -SkipUvCheck + $credentialAfter = Get-KIMcpRuntimeCredential -TargetRoot $t1 + $keyAfter = ConvertFrom-KIMcpRuntimeSecureStringTransient -Value $credentialAfter.apiKey + $checks.stateNeverTreatedAsPayload = [ordered]@{ + repairRanAgainWithoutError = [bool]$repair2.passed + workspaceFileSurvived = (Test-Path -LiteralPath (Join-Path $paths1Fresh.workspace 'user-created-file.txt') -PathType Leaf) + credentialUnchanged = ($keyBefore -ceq $keyAfter) + } + if ($checks.stateNeverTreatedAsPayload.Values -contains $false) { $fail.Add('stateNeverTreatedAsPayload failed: ' + ($checks.stateNeverTreatedAsPayload | ConvertTo-Json -Compress)) } + $keyBefore = $null; $keyAfter = $null + + # === 7: -BackupRoot is respected (2.18.1 Desktop-Control lesson) =========================== + $t2 = Join-Path $scratchBase 'target-backuproot' + $externalBackupRoot = Join-Path $scratchBase 'external-backup-root' + New-Item -ItemType Directory -Path $externalBackupRoot -Force | Out-Null + $installExternalBackup = Install-KIMcpRuntime -PackageRoot $src -TargetRoot $t2 -Action Install -BackupRoot $externalBackupRoot -SkipUvCheck + $checks.externallyProvidedBackupRootRespected = [ordered]@{ + installPassed = [bool]$installExternalBackup.passed + backupPathUnderExternalRoot = ([string]$installExternalBackup.backupPath).StartsWith($externalBackupRoot, [StringComparison]::OrdinalIgnoreCase) + noStandaloneBackupCreated = (-not (Test-Path -LiteralPath (Join-Path $t2 'backups/mcp-runtime'))) + } + if ($checks.externallyProvidedBackupRootRespected.Values -contains $false) { $fail.Add('externallyProvidedBackupRootRespected failed: ' + ($checks.externallyProvidedBackupRootRespected | ConvertTo-Json -Compress)) } + + # === 8: rollback restores the persistent package tree to its exact prior content =========== + $upgradeBeforeRollback = Install-KIMcpRuntime -PackageRoot $srcMutated -TargetRoot $t2 -Action Upgrade -SkipUvCheck + $preRollbackContent = Get-Content -LiteralPath (Join-Path (Get-KIMcpRuntimeInstallPaths -TargetRoot $t2).packageRoot 'McpRuntime.psm1') -Raw + $rollback1 = Restore-KIMcpRuntime -BackupPath ([string]$upgradeBeforeRollback.backupPath) -TargetRoot $t2 + $postRollbackContent = Get-Content -LiteralPath (Join-Path (Get-KIMcpRuntimeInstallPaths -TargetRoot $t2).packageRoot 'McpRuntime.psm1') -Raw + $checks.rollbackRestoresPersistentPackageTree = [ordered]@{ + rollbackPassed = [bool]$rollback1.passed + contentActuallyChangedByUpgrade = ($preRollbackContent -ne $postRollbackContent) + contentNoLongerContainsMutationMarker = (-not $postRollbackContent.Contains($marker)) + } + if ($checks.rollbackRestoresPersistentPackageTree.Values -contains $false) { $fail.Add('rollbackRestoresPersistentPackageTree failed: ' + ($checks.rollbackRestoresPersistentPackageTree | ConvertTo-Json -Compress)) } + + # === 9: a fresh install that fails partway leaves no half-deployed package/module tree ====== + $t3 = Join-Path $scratchBase 'target-failure' + $srcBroken = New-KIMcpInstallScratchSource -Destination (Join-Path $scratchBase 'source-broken') + # Corrupt SHA256SUMS.txt so the post-copy checksum verification fails closed. + Set-Content -LiteralPath (Join-Path $srcBroken 'SHA256SUMS.txt') -Value '0000000000000000000000000000000000000000000000000000000000000000 *McpRuntime.psm1' -Encoding ascii + $thrown = $null + try { Install-KIMcpRuntime -PackageRoot $srcBroken -TargetRoot $t3 -Action Install -SkipUvCheck | Out-Null } catch { $thrown = $_ } + $installPaths3 = Get-KIMcpRuntimeInstallPaths -TargetRoot $t3 + $paths3 = Get-KIMcpRuntimePaths -TargetRoot $t3 + $checks.failedFreshInstallLeavesNoOrphanedTree = [ordered]@{ + threw = ($null -ne $thrown) + packageRootCleanedUp = (-not (Test-Path -LiteralPath $installPaths3.packageRoot)) + moduleRootCleanedUp = (-not (Test-Path -LiteralPath $paths3.moduleRoot)) + } + if ($checks.failedFreshInstallLeavesNoOrphanedTree.Values -contains $false) { $fail.Add('failedFreshInstallLeavesNoOrphanedTree failed: ' + ($checks.failedFreshInstallLeavesNoOrphanedTree | ConvertTo-Json -Compress)) } + + # === 10: source/config version mismatch fails closed before touching the target ============ + $srcMismatched = New-KIMcpInstallScratchSource -Destination (Join-Path $scratchBase 'source-mismatched') + $cfg = Get-Content -LiteralPath (Join-Path $srcMismatched 'Config/mcp-runtime.config.json') -Raw | ConvertFrom-Json + $cfg.version = '9.9.9' + ($cfg | ConvertTo-Json -Depth 20) | Set-Content -LiteralPath (Join-Path $srcMismatched 'Config/mcp-runtime.config.json') -Encoding utf8 + $t4 = Join-Path $scratchBase 'target-mismatch' + $thrownMismatch = $null + try { Install-KIMcpRuntime -PackageRoot $srcMismatched -TargetRoot $t4 -Action Install -SkipUvCheck | Out-Null } catch { $thrownMismatch = $_ } + $checks.sourceConfigVersionMismatchFailsClosed = [ordered]@{ + threw = ($null -ne $thrownMismatch) + targetUntouched = (-not (Test-Path -LiteralPath $t4)) + } + if ($checks.sourceConfigVersionMismatchFailsClosed.Values -contains $false) { $fail.Add('sourceConfigVersionMismatchFailsClosed failed: ' + ($checks.sourceConfigVersionMismatchFailsClosed | ConvertTo-Json -Compress)) } + + # === 11: Uninstall removes the persistent package tree too ================================= + $uninstall1 = Uninstall-KIMcpRuntime -TargetRoot $t1 + $checks.uninstallRemovesPersistentPackageTree = [ordered]@{ + passed = [bool]$uninstall1.passed + packageTreeRemoved = (-not (Test-Path -LiteralPath (Get-KIMcpRuntimeInstallPaths -TargetRoot $t1).installRoot)) + } + if ($checks.uninstallRemovesPersistentPackageTree.Values -contains $false) { $fail.Add('uninstallRemovesPersistentPackageTree failed: ' + ($checks.uninstallRemovesPersistentPackageTree | ConvertTo-Json -Compress)) } + + $passed = $fail.Count -eq 0 + [pscustomobject]@{ passed = $passed; checks = $checks; failures = @($fail) } | ConvertTo-Json -Depth 12 + if (-not $passed) { throw 'MCP-Runtime-Install-Regression fehlgeschlagen.' } +} finally { + try { Remove-Item -LiteralPath $scratchBase -Recurse -Force -ErrorAction SilentlyContinue } catch {} +} diff --git a/tools/mcp-runtime/current/VERSION b/tools/mcp-runtime/current/VERSION index 6e8bf73..0ea3a94 100644 --- a/tools/mcp-runtime/current/VERSION +++ b/tools/mcp-runtime/current/VERSION @@ -1 +1 @@ -0.1.0 +0.2.0 From 2c999060e317ef2e8b5d702514dc640065147f7e Mon Sep 17 00:00:00 2001 From: Robert Backhaus Date: Sun, 13 Sep 2026 13:09:28 +0200 Subject: [PATCH 2/3] fix: enforce payload version consistency for 2.19 --- .../current/CompleteInstaller.psm1 | 119 ++++++++++ .../current/Contracts/REQUIRED-PAYLOADS.json | 4 +- .../New-KIStackCompleteInstallerArchive.ps1 | 16 ++ .../complete-installer/current/SHA256SUMS.txt | 7 +- .../Test-KIStackPayloadVersionContract.ps1 | 203 ++++++++++++++++++ 5 files changed, 344 insertions(+), 5 deletions(-) create mode 100644 tools/complete-installer/current/Test-KIStackPayloadVersionContract.ps1 diff --git a/tools/complete-installer/current/CompleteInstaller.psm1 b/tools/complete-installer/current/CompleteInstaller.psm1 index 3e5a379..33804f7 100644 --- a/tools/complete-installer/current/CompleteInstaller.psm1 +++ b/tools/complete-installer/current/CompleteInstaller.psm1 @@ -527,6 +527,125 @@ function Test-KICompleteOpenTerminalCompliant { }catch{return $false} } +function Get-KICompletePayloadEmbeddedVersion { + # Reads the version embedded INSIDE an already-built payload zip, via the exact same + # packageIdentity.kind contract ('file'/'jsonField'/'jsonComposite') + # Lifecycle/KIStackComponentVersionRegistry.psm1's own Get-KIStackPublishedComponentVersion + # already uses to read a component's PUBLISHED (GitHub-hosted) version -- here from a local + # zip entry instead of a raw.githubusercontent.com URL, so one contract shape validates both + # meanings of "the version this component's own source claims" consistently. No component- + # specific branching: the same three `kind` cases this repo already defines. + param( + [Parameter(Mandatory)][string]$ZipPath, + [Parameter(Mandatory)][object]$PackageIdentity, + # REQUIRED-PAYLOADS.json's own 'source' for this payload (e.g. 'tools/mcp-runtime/current') + # -- packageIdentity.path is repo-relative (e.g. 'tools/mcp-runtime/current/VERSION'); a + # payload zip's own entries are relative to ITS OWN source root instead (optionally + # archiveRoot-prefixed), so this prefix must be stripped to get the in-zip entry name. + [Parameter(Mandatory)][string]$SourceRootRelative, + [string]$ArchiveRoot + ) + $kind = [string]$PackageIdentity.kind + $pathInsideSource = ([string]$PackageIdentity.path).Substring($SourceRootRelative.Length).TrimStart('/', '\') + $entryName = if (-not [string]::IsNullOrWhiteSpace($ArchiveRoot)) { "$ArchiveRoot/$pathInsideSource" } else { $pathInsideSource } + Add-Type -AssemblyName System.IO.Compression.FileSystem + $archive = [IO.Compression.ZipFile]::OpenRead($ZipPath) + try { + $entry = @($archive.Entries | Where-Object { $_.FullName -eq $entryName }) | Select-Object -First 1 + if ($null -eq $entry) { throw "Payload-Eintrag fehlt: $entryName" } + $reader = [IO.StreamReader]::new($entry.Open()) + try { $raw = $reader.ReadToEnd() } finally { $reader.Dispose() } + switch ($kind) { + 'file' { + $version = $raw.Trim() + if ([string]::IsNullOrWhiteSpace($version)) { throw 'Leere VERSION-Datei im Payload.' } + return $version + } + 'jsonField' { + $obj = $raw | ConvertFrom-Json -Depth 20 + $prop = $obj.PSObject.Properties[[string]$PackageIdentity.field] + if ($null -eq $prop -or [string]::IsNullOrWhiteSpace([string]$prop.Value)) { throw "Feld '$([string]$PackageIdentity.field)' fehlt oder ist leer im Payload." } + return [string]$prop.Value + } + 'jsonComposite' { + $obj = $raw | ConvertFrom-Json -Depth 20 + $parts = @() + foreach ($fieldName in @($PackageIdentity.fields)) { + $prop = $obj.PSObject.Properties[[string]$fieldName] + if ($null -eq $prop -or [string]::IsNullOrWhiteSpace([string]$prop.Value)) { throw "Feld '$fieldName' fehlt oder ist leer im Payload." } + $parts += [string]$prop.Value + } + $separator = if ($PackageIdentity.PSObject.Properties['separator']) { [string]$PackageIdentity.separator } else { '-' } + return ($parts -join $separator) + } + default { throw "Unbekannter packageIdentity.kind: '$kind'" } + } + } finally { + $archive.Dispose() + } +} + +function Test-KICompletePayloadVersionContract { + # Generic, contract-driven build-gate (2.19.0 packaging-metadata fix): for every + # REQUIRED-PAYLOADS.json entry whose matching COMPONENTS.json component(s) declare a real, + # checkable own version source (packageIdentity.kind 'file'/'jsonField'/'jsonComposite' -- + # NEVER the 'bundled-reference-only' pinned/shared-payload components such as + # foundation-runtime/python-git/applications, which have no packageIdentity at all and are + # therefore correctly skipped, not special-cased), verifies BOTH that the version embedded + # INSIDE the just-built payload zip (via Get-KICompletePayloadEmbeddedVersion above) matches + # the component's own pinned COMPONENTS.json version, AND that any version token embedded in + # the payload's own declared `file`/`archiveRoot` (REQUIRED-PAYLOADS.json) matches it too. + # This is exactly the check that would have caught a stale packaging filename/archiveRoot + # left behind after a component version bump (the real, reproduced 2.19.0 McpRuntime/ + # DesktopControl defect this closes) -- for ANY component this contract shape applies to, not + # only those two. Every failure is collected (never fails fast on the first one) so a single + # build run reports every inconsistency at once. + param( + [Parameter(Mandatory)][object]$ComponentContract, + [Parameter(Mandatory)][object]$PayloadDefinition, + [Parameter(Mandatory)][string]$PayloadZipPath + ) + $failures = [Collections.Generic.List[string]]::new() + $matchedComponents = @($ComponentContract.components | Where-Object { [string]$_.source -eq ('Payload/' + [string]$PayloadDefinition.key) }) + $versionCheckable = @($matchedComponents | Where-Object { + $_.PSObject.Properties['packageIdentity'] -and $null -ne $_.packageIdentity -and + [string]$_.packageIdentity.kind -in @('file', 'jsonField', 'jsonComposite') + }) + + foreach ($component in $versionCheckable) { + $expected = [string]$component.version + $embedded = $null + try { + $embedded = Get-KICompletePayloadEmbeddedVersion -ZipPath $PayloadZipPath -PackageIdentity $component.packageIdentity -SourceRootRelative ([string]$PayloadDefinition.source) -ArchiveRoot ([string]$PayloadDefinition.archiveRoot) + } catch { + $failures.Add("$($component.id): eingebettete Payload-Version konnte nicht gelesen werden: $($_.Exception.Message)") + continue + } + if ($embedded -ne $expected) { + $failures.Add("$($component.id): Payload enthaelt Version '$embedded', COMPONENTS.json erwartet '$expected'") + } + + foreach ($fieldName in @('file', 'archiveRoot')) { + $fieldValue = [string]$PayloadDefinition.$fieldName + if ([string]::IsNullOrWhiteSpace($fieldValue)) { continue } + # Strict: bare X.Y.Z, optionally the one real pre-release-style suffix this repo's + # own component versions use elsewhere ('-rN', e.g. production-recovery's '1.7.0-r7') + # -- deliberately NOT a generic '-' suffix capture, which previously + # misread a purely descriptive filename qualifier (Cutover Runtime's own + # 'KI-Stack-Cutover-Execute-v1.6.16-core.zip', where '-core' disambiguates the + # payload variant and is never part of the version) as if it were part of the version. + if ($fieldValue -match '-v(\d+\.\d+\.\d+(?:-r\d+)?)') { + $embeddedInName = $Matches[1] + if ($embeddedInName -ne $expected) { + $failures.Add("$($component.id): REQUIRED-PAYLOADS.json-Feld '$fieldName' ('$fieldValue') enthaelt Version '$embeddedInName', COMPONENTS.json erwartet '$expected'") + } + } + } + } + + [pscustomobject]@{ ok = ($failures.Count -eq 0); failures = @($failures); checkedComponents = @($versionCheckable.id) } +} + function Test-KICompleteMcpRuntimePayloadParity { # Verbatim pattern of Test-KICompleteDesktopControlPayloadParity (s/DesktopControl/McpRuntime/, # Payload key 'McpRuntime', deployed target 'tools/mcp-runtime/current'): desired-state parity diff --git a/tools/complete-installer/current/Contracts/REQUIRED-PAYLOADS.json b/tools/complete-installer/current/Contracts/REQUIRED-PAYLOADS.json index 8c0aa05..09324f6 100644 --- a/tools/complete-installer/current/Contracts/REQUIRED-PAYLOADS.json +++ b/tools/complete-installer/current/Contracts/REQUIRED-PAYLOADS.json @@ -8,9 +8,9 @@ {"key":"CodexLocal","source":"tools/codex-local/current","file":"KI-Stack-Codex-Local-v0.2.1.zip","archiveRoot":"KI-Stack-Codex-Local-v0.2.1","required":true}, {"key":"RAG","source":"tools/rag/current","file":"KI-Stack-RAG-v0.4.0.zip","archiveRoot":"KI-Stack-RAG-v0.4.0","required":true}, {"key":"OpenTerminal","source":"tools/open-terminal/current","file":"KI-Stack-Open-Terminal-v0.1.1.zip","archiveRoot":"KI-Stack-Open-Terminal-v0.1.1","required":true}, - {"key":"McpRuntime","source":"tools/mcp-runtime/current","file":"KI-Stack-MCP-Runtime-v0.1.0.zip","archiveRoot":"KI-Stack-MCP-Runtime-v0.1.0","required":true}, + {"key":"McpRuntime","source":"tools/mcp-runtime/current","file":"KI-Stack-MCP-Runtime-v0.2.0.zip","archiveRoot":"KI-Stack-MCP-Runtime-v0.2.0","required":true}, {"key":"WinApp","source":"tools/winapp/current","file":"KI-Stack-WinApp-v0.6.1.zip","archiveRoot":"KI-Stack-WinApp-v0.6.1","required":true}, - {"key":"DesktopControl","source":"tools/desktop-control/current","file":"KI-Stack-Desktop-Control-v0.1.0.zip","archiveRoot":"KI-Stack-Desktop-Control-v0.1.0","required":true}, + {"key":"DesktopControl","source":"tools/desktop-control/current","file":"KI-Stack-Desktop-Control-v0.1.1.zip","archiveRoot":"KI-Stack-Desktop-Control-v0.1.1","required":true}, {"key":"OpenWebUIAgentPack","source":"tools/openwebui-agent-pack/current","file":"KI-Stack-OpenWebUI-Agent-Pack-v1.9.0.zip","archiveRoot":null,"required":true}, {"key":"OpenWebUIBallisticsPack","source":"tools/openwebui-ballistics-pack/current","file":"KI-Stack-OpenWebUI-Ballistics-Pack-v1.0.0.zip","archiveRoot":"KI-Stack-OpenWebUI-Ballistics-Pack-v1.0.0","required":false}, {"key":"OpenWebUIVisualPack","source":"tools/openwebui-visual-pack/current","file":"KI-Stack-OpenWebUI-Visual-Pack-v2.0.5.zip","archiveRoot":"KI-Stack-OpenWebUI-Visual-Pack-v2.0.5","required":true}, diff --git a/tools/complete-installer/current/New-KIStackCompleteInstallerArchive.ps1 b/tools/complete-installer/current/New-KIStackCompleteInstallerArchive.ps1 index feda9a1..052f5c5 100644 --- a/tools/complete-installer/current/New-KIStackCompleteInstallerArchive.ps1 +++ b/tools/complete-installer/current/New-KIStackCompleteInstallerArchive.ps1 @@ -81,11 +81,21 @@ function Initialize-GeneratedBuildPayloads { $payloadContract=Get-Content -LiteralPath (Join-Path $PSScriptRoot 'Contracts/REQUIRED-PAYLOADS.json') -Raw|ConvertFrom-Json -Depth 30 $payloadDefinitions=@($payloadContract.payloads) +$componentContract=Get-Content -LiteralPath (Join-Path $PSScriptRoot 'Contracts/COMPONENTS.json') -Raw|ConvertFrom-Json -Depth 30 +# Test-KICompletePayloadVersionContract (CompleteInstaller.psm1): generic, contract-driven +# build gate -- verifies the version embedded inside each just-built payload zip, and any +# version token in that payload's own declared file/archiveRoot, both match the pinned +# COMPONENTS.json component version. Fails the build closed on any mismatch; this is exactly +# the check that would have caught the real, reproduced 2.19.0 defect where McpRuntime's and +# DesktopControl's packaging filenames/archiveRoots still said v0.1.0 after their component +# versions had already advanced to 0.2.0/0.1.1. +Import-Module (Join-Path $PSScriptRoot 'CompleteInstaller.psm1') -Force $tempRoot = Join-Path ([IO.Path]::GetTempPath()) ('ki-stack-complete-build-' + [guid]::NewGuid().ToString('N')) $payloadRoot = Join-Path $tempRoot 'payloads' $stage = Join-Path $tempRoot $packageName New-Item -ItemType Directory -Path $payloadRoot,$stage -Force | Out-Null +$versionContractResults = [Collections.Generic.List[object]]::new() try { foreach ($definition in $payloadDefinitions) { $source = Join-Path $repositoryRoot $definition.source @@ -100,6 +110,11 @@ try { Update-SourceChecksums -SourceRoot $buildSource $payloadArchive = Join-Path $payloadRoot $definition.file New-DeterministicArchive -SourceRoot $buildSource -Destination $payloadArchive -ArchiveRoot $definition.archiveRoot + $versionContract = Test-KICompletePayloadVersionContract -ComponentContract $componentContract -PayloadDefinition $definition -PayloadZipPath $payloadArchive + $versionContractResults.Add([pscustomobject]@{ key = $definition.key; ok = [bool]$versionContract.ok; checkedComponents = $versionContract.checkedComponents; failures = $versionContract.failures }) | Out-Null + if (-not [bool]$versionContract.ok) { + throw "Payload-Versionsvertrag verletzt fuer '$($definition.key)': $($versionContract.failures -join '; ')" + } $destination = Join-Path $stage ('Payload\' + $definition.key) New-Item -ItemType Directory -Path $destination -Force | Out-Null Copy-Item -LiteralPath $payloadArchive -Destination $destination @@ -128,6 +143,7 @@ try { sha256 = $hash payloads = @($payloadDefinitions.key) payloadValidation = $payloadValidation + payloadVersionContract = @($versionContractResults) sourceOnlyBuild = $true targetSystemAccessed = $false } diff --git a/tools/complete-installer/current/SHA256SUMS.txt b/tools/complete-installer/current/SHA256SUMS.txt index a9cbaa1..fa6556d 100644 --- a/tools/complete-installer/current/SHA256SUMS.txt +++ b/tools/complete-installer/current/SHA256SUMS.txt @@ -1,10 +1,10 @@ 97a8826b8019d6d4d721ea2ef3ba1bc2536341f5a7736d2a9ba572f998d5deb6 *Bootstrap-KIStackPowerShell7.ps1 de6e56dcfacc75aeb60c811a4a78f14a6d80e20450800cb6531972b387c523b1 *BUILD-REPORT.json -629dcef3a166e222f5cfdfe19adb04d0d8325ec667365de06c88bec5d641f297 *CompleteInstaller.psm1 +4deb46fea851e787729de897633c8ff4dff10b828deba6c086f77f5f053a1337 *CompleteInstaller.psm1 ca4c28c0e5d7ed8a99813925bfe4e19e86b853d45e389a7a6f314eaa00f92ef0 *Config/complete-installer.config.json 3e1a1587472271901e6b8d7612857ba7bf30b8e5e22fd634f5d0f681d5700469 *Contracts/COMPONENTS.json 505acbc2d0a59346155795b3c09e8231f2d27d5eca5db9cee0a418ef85a07433 *Contracts/PAYLOADS.json -e8629f125f23ba150dd43e9d12593456fbcb5a02ea3b6e15f25e76babfb6e759 *Contracts/REQUIRED-PAYLOADS.json +789194eeee6e45a8f577654a36ecb4cdda61a90d99c376e6ed5fc869458c5061 *Contracts/REQUIRED-PAYLOADS.json b4e04fcda1728976c759221602cc3b45d175c9d7aa702b413078d262191428e8 *Contracts/RESUME.schema.json 6cab1813a0270c2e5818bfe4d7f84e3550de2822608b92113cc3c4749c9b7f06 *Contracts/ROLLBACK.md 370e861950fe0a54d7ad01ac31e94dc0c13fb94cf2ab4ee793c663d66df95471 *Contracts/TRANSACTION.schema.json @@ -36,7 +36,7 @@ ff12565b65c4f044c28a2e6645cb99685879368da4c96fb3bc3695a9f2120d94 *Lifecycle/Upda ac38dc9a58f9c80e93d6706f59708a4539e65a9e4b425dc385030fdd7f5df495 *Lifecycle/Validate-KIStack.cmd 4f04649a473fc893ca98017871ffb31e838d571d83c16976662b060688569372 *MANIFEST.json a064fe3025b54c35092b76c254ea878d024790310b785731bf999062e3b12fdf *MODEL_LICENSES.md -4a861c929352823410db58673ade60a92ada8cb9acbb677f95d5362a214f8a3d *New-KIStackCompleteInstallerArchive.ps1 +386d7019285c872ed267497d0a1102b3f58f54177bebd921e6f7556b2d62ff69 *New-KIStackCompleteInstallerArchive.ps1 ffd135c29f2ee79cfeb07e4d2a0ac92a3d52a7ddbd2d881dc551a36f7bf8efef *NOTICE 81dc6f12826e9678f007d0cc9b2a6ba8a255be91aa984edf7fd92c9a8d1d28c7 *Operations/Remove-KIStackKnowledgeExperiment.ps1 72b82522a025703c660d2941d92b89b2664ed69fa8676e9d8ecf9f125a658dae *Operations/Remove-KIStackKnowledgeExperimentCollections.ps1 @@ -92,6 +92,7 @@ bf57600004c2abf152bd9a181ef72860a521e347b410a26bbb73d3760fcccbde *Test-KIStackOp bd576fcaa76576685aaabbc959e7244b57da3465e89c278bd7e04ea5bcf841d2 *Test-KIStackOpenWebUIVisualPackCutover.ps1 f000b062a0306bb4b3967585b60a2ef414031a0d8c034fd7d1fe7a180f3be4f6 *Test-KIStackPathContext.ps1 cea0192b6fda80c3b3260ddd234d06cf55d88079a131d34788f9e3fc3a64e75e *Test-KIStackPayloadDeploymentHygiene.ps1 +355dd0587b9e7fc1bc181da29353bbc5499555af1b24d819a86d010801198ade *Test-KIStackPayloadVersionContract.ps1 3cce9024e297b71c32411206fa0d25c61bb3d3670c3025d2fe8aa9e1311a58b1 *Test-KIStackPinnedReferenceReconciliation.ps1 79443a3c16145a787bf335b8fa7c724dbd1598268ebd4d703bda000b0ef56538 *Test-KIStackPrimaryInstallerComponentParity.ps1 6f0e02b589f5668e821282dac7c02ee70cf0367d94d692a718e5aca9522ba9f2 *Test-KIStackRAGPrefixIntegration.ps1 diff --git a/tools/complete-installer/current/Test-KIStackPayloadVersionContract.ps1 b/tools/complete-installer/current/Test-KIStackPayloadVersionContract.ps1 new file mode 100644 index 0000000..104def8 --- /dev/null +++ b/tools/complete-installer/current/Test-KIStackPayloadVersionContract.ps1 @@ -0,0 +1,203 @@ +[CmdletBinding()] +param([string]$PackageRoot = $PSScriptRoot) + +Set-StrictMode -Version Latest +$ErrorActionPreference = 'Stop' + +# Direct, isolated tests of the REAL, generic Test-KICompletePayloadVersionContract / +# Get-KICompletePayloadEmbeddedVersion (CompleteInstaller.psm1) -- imported and called as-is, +# never copied or reimplemented. This is the build-gate that closes the real, reproduced 2.19.0 +# packaging defect: Contracts/REQUIRED-PAYLOADS.json's `file`/`archiveRoot` for McpRuntime and +# DesktopControl still said v0.1.0 after those components' own COMPONENTS.json version and +# source VERSION file had already advanced to 0.2.0/0.1.1 -- the existing +# Test-KIStackRequiredPayloads.ps1 only checks that a uniquely-named zip file EXISTS, never that +# its name or its own embedded content actually matches the pinned component version. +# +# Scenarios 1-4 use a wholly synthetic "fake-component" (proving genericity -- no +# McpRuntime/DesktopControl-specific code path exists in the function under test). Scenarios 5-6 +# run the exact same function against REAL, freshly-built McpRuntime/DesktopControl payload zips +# and the REAL Contracts/COMPONENTS.json + Contracts/REQUIRED-PAYLOADS.json. + +Import-Module (Join-Path $PackageRoot 'CompleteInstaller.psm1') -Force + +$fail = [Collections.Generic.List[string]]::new() +$checks = [ordered]@{} +$scratchBase = Join-Path ([IO.Path]::GetTempPath()) ('KIPayloadVerContract-' + [guid]::NewGuid().ToString('N').Substring(0, 10)) +New-Item -ItemType Directory -Path $scratchBase -Force | Out-Null + +function New-KIPVCZip { + # Builds a minimal zip with the given entries (relative paths -> text content), optionally + # prefixed by ArchiveRoot -- mirrors New-DeterministicArchive's own entry-naming shape. + param([Parameter(Mandatory)][string]$ZipPath, [Parameter(Mandatory)][hashtable]$Entries, [string]$ArchiveRoot) + New-Item -ItemType Directory -Path (Split-Path -Parent $ZipPath) -Force | Out-Null + if (Test-Path -LiteralPath $ZipPath) { Remove-Item -LiteralPath $ZipPath -Force } + Add-Type -AssemblyName System.IO.Compression + $stream = [IO.File]::Open($ZipPath, [IO.FileMode]::CreateNew) + try { + $archive = [IO.Compression.ZipArchive]::new($stream, [IO.Compression.ZipArchiveMode]::Create, $false) + try { + foreach ($relativePath in $Entries.Keys) { + $entryName = if (-not [string]::IsNullOrWhiteSpace($ArchiveRoot)) { "$ArchiveRoot/$relativePath" } else { $relativePath } + $entry = $archive.CreateEntry($entryName, [IO.Compression.CompressionLevel]::Fastest) + $writer = [IO.StreamWriter]::new($entry.Open()) + try { $writer.Write([string]$Entries[$relativePath]) } finally { $writer.Dispose() } + } + } finally { $archive.Dispose() } + } finally { $stream.Dispose() } +} + +function New-KIPVCComponentContract { + # A minimal, synthetic COMPONENTS.json-shaped object with exactly one component -- proves + # the function under test works from the CONTRACT SHAPE alone, not from any hardcoded id. + param([Parameter(Mandatory)][string]$Id, [Parameter(Mandatory)][string]$Version, [Parameter(Mandatory)][string]$Source, [Parameter(Mandatory)][string]$PackageIdentityPath) + [pscustomobject]@{ + components = @([pscustomobject]@{ + id = $Id + version = $Version + source = $Source + packageIdentity = [pscustomobject]@{ kind = 'file'; path = $PackageIdentityPath } + }) + } +} + +try { + # === 1: correct payload filename + correct internal VERSION -> pass ======================= + $z1 = Join-Path $scratchBase '1\Fake-Component-v1.2.3.zip' + New-KIPVCZip -ZipPath $z1 -ArchiveRoot 'Fake-Component-v1.2.3' -Entries @{ 'VERSION' = '1.2.3' } + $cc1 = New-KIPVCComponentContract -Id 'fake-component' -Version '1.2.3' -Source 'Payload/FakeComponent' -PackageIdentityPath 'tools/fake-component/current/VERSION' + $pd1 = [pscustomobject]@{ key = 'FakeComponent'; source = 'tools/fake-component/current'; file = 'Fake-Component-v1.2.3.zip'; archiveRoot = 'Fake-Component-v1.2.3' } + $r1 = Test-KICompletePayloadVersionContract -ComponentContract $cc1 -PayloadDefinition $pd1 -PayloadZipPath $z1 + $checks.correctNameAndVersionPasses = [ordered]@{ ok = [bool]$r1.ok; checkedFakeComponent = (@($r1.checkedComponents) -contains 'fake-component') } + if ($checks.correctNameAndVersionPasses.Values -contains $false) { $fail.Add('correctNameAndVersionPasses failed: ' + ($r1.failures -join '; ')) } + + # === 2: stale filename/archiveRoot (old version) while the internal VERSION is already the + # NEW one -> fail (the exact real-world 2.19.0 defect shape) ======================== + $z2 = Join-Path $scratchBase '2\Fake-Component-v1.2.2.zip' + New-KIPVCZip -ZipPath $z2 -ArchiveRoot 'Fake-Component-v1.2.2' -Entries @{ 'VERSION' = '1.2.3' } + $cc2 = New-KIPVCComponentContract -Id 'fake-component' -Version '1.2.3' -Source 'Payload/FakeComponent' -PackageIdentityPath 'tools/fake-component/current/VERSION' + $pd2 = [pscustomobject]@{ key = 'FakeComponent'; source = 'tools/fake-component/current'; file = 'Fake-Component-v1.2.2.zip'; archiveRoot = 'Fake-Component-v1.2.2' } + $r2 = Test-KICompletePayloadVersionContract -ComponentContract $cc2 -PayloadDefinition $pd2 -PayloadZipPath $z2 + $checks.staleFilenameAtNewInternalVersionFails = [ordered]@{ + notOk = (-not [bool]$r2.ok) + reasonMentionsFilename = (($r2.failures -join ' ') -match "REQUIRED-PAYLOADS\.json-Feld 'file'") + } + if ($checks.staleFilenameAtNewInternalVersionFails.Values -contains $false) { $fail.Add('staleFilenameAtNewInternalVersionFails failed: ' + ($r2.failures -join '; ')) } + + # === 3: wrong archiveRoot -- the payload zip's real entries live under a different prefix + # than REQUIRED-PAYLOADS.json declares -> fail (structural: entry not found) ========= + $z3 = Join-Path $scratchBase '3\Fake-Component-v1.2.3.zip' + New-KIPVCZip -ZipPath $z3 -ArchiveRoot 'ActualRoot' -Entries @{ 'VERSION' = '1.2.3' } + $cc3 = New-KIPVCComponentContract -Id 'fake-component' -Version '1.2.3' -Source 'Payload/FakeComponent' -PackageIdentityPath 'tools/fake-component/current/VERSION' + $pd3 = [pscustomobject]@{ key = 'FakeComponent'; source = 'tools/fake-component/current'; file = 'Fake-Component-v1.2.3.zip'; archiveRoot = 'DifferentDeclaredRoot' } + $r3 = Test-KICompletePayloadVersionContract -ComponentContract $cc3 -PayloadDefinition $pd3 -PayloadZipPath $z3 + $checks.wrongArchiveRootFails = [ordered]@{ + notOk = (-not [bool]$r3.ok) + reasonMentionsUnreadableVersion = (($r3.failures -join ' ') -match 'konnte nicht gelesen werden') + } + if ($checks.wrongArchiveRootFails.Values -contains $false) { $fail.Add('wrongArchiveRootFails failed: ' + ($r3.failures -join '; ')) } + + # === 4: correct filename/archiveRoot, but the internal VERSION content itself is wrong ===== + $z4 = Join-Path $scratchBase '4\Fake-Component-v1.2.3.zip' + New-KIPVCZip -ZipPath $z4 -ArchiveRoot 'Fake-Component-v1.2.3' -Entries @{ 'VERSION' = '1.2.2' } + $cc4 = New-KIPVCComponentContract -Id 'fake-component' -Version '1.2.3' -Source 'Payload/FakeComponent' -PackageIdentityPath 'tools/fake-component/current/VERSION' + $pd4 = [pscustomobject]@{ key = 'FakeComponent'; source = 'tools/fake-component/current'; file = 'Fake-Component-v1.2.3.zip'; archiveRoot = 'Fake-Component-v1.2.3' } + $r4 = Test-KICompletePayloadVersionContract -ComponentContract $cc4 -PayloadDefinition $pd4 -PayloadZipPath $z4 + $checks.wrongInternalVersionFails = [ordered]@{ + notOk = (-not [bool]$r4.ok) + reasonMentionsPayloadVersion = (($r4.failures -join ' ') -match "Payload enthaelt Version '1\.2\.2'") + } + if ($checks.wrongInternalVersionFails.Values -contains $false) { $fail.Add('wrongInternalVersionFails failed: ' + ($r4.failures -join '; ')) } + + # === 4b: a descriptive, non-version filename suffix (e.g. Cutover Runtime's own real + # '...-v1.6.16-core.zip', where '-core' disambiguates the payload variant and is + # never part of the version) must NOT be misread as part of the version -> pass. + # Real, reproduced regression: this exact shape once made the generic gate itself + # throw a false positive against the real Cutover Runtime payload. ================== + $z4b = Join-Path $scratchBase '4b\Fake-Component-v1.6.16-core.zip' + New-KIPVCZip -ZipPath $z4b -ArchiveRoot 'Fake-Component-v1.6.16' -Entries @{ 'VERSION' = '1.6.16' } + $cc4b = New-KIPVCComponentContract -Id 'fake-component' -Version '1.6.16' -Source 'Payload/FakeComponent' -PackageIdentityPath 'tools/fake-component/current/VERSION' + $pd4b = [pscustomobject]@{ key = 'FakeComponent'; source = 'tools/fake-component/current'; file = 'Fake-Component-v1.6.16-core.zip'; archiveRoot = 'Fake-Component-v1.6.16' } + $r4b = Test-KICompletePayloadVersionContract -ComponentContract $cc4b -PayloadDefinition $pd4b -PayloadZipPath $z4b + $checks.descriptiveFilenameSuffixNotMisreadAsVersion = [ordered]@{ ok = [bool]$r4b.ok } + if (-not [bool]$r4b.ok) { $fail.Add('descriptiveFilenameSuffixNotMisreadAsVersion failed: ' + ($r4b.failures -join '; ')) } + + # === 5 & 6: the REAL fix, against REAL freshly-built McpRuntime/DesktopControl payloads and + # the REAL Contracts/COMPONENTS.json + Contracts/REQUIRED-PAYLOADS.json -- mirrors + # New-KIStackCompleteInstallerArchive.ps1's own build-one-payload shape exactly. ====== + function New-KIPVCRealPayload { + param([Parameter(Mandatory)][string]$SourceRoot, [Parameter(Mandatory)][object]$Definition, [Parameter(Mandatory)][string]$Destination) + $build = Join-Path $Destination ('source-' + $Definition.key) + Copy-Item -LiteralPath $SourceRoot -Destination $build -Recurse -Force + Get-ChildItem -LiteralPath $build -Recurse -File -Filter '*.zip' -ErrorAction SilentlyContinue | Remove-Item -Force + $sumsPath = Join-Path $build 'SHA256SUMS.txt' + $lines = Get-ChildItem -LiteralPath $build -Recurse -File | Where-Object { $_.FullName -ne $sumsPath } | Sort-Object { [IO.Path]::GetRelativePath($build, $_.FullName).Replace('\', '/') } | ForEach-Object { + $rel = [IO.Path]::GetRelativePath($build, $_.FullName).Replace('\', '/') + "$((Get-FileHash -LiteralPath $_.FullName -Algorithm SHA256).Hash.ToLowerInvariant()) *$rel" + } + [IO.File]::WriteAllLines($sumsPath, $lines, [Text.ASCIIEncoding]::new()) + $zipPath = Join-Path $Destination $Definition.file + New-KIPVCZip -ZipPath $zipPath -ArchiveRoot $Definition.archiveRoot -Entries ( + @(Get-ChildItem -LiteralPath $build -Recurse -File) | ForEach-Object -Begin { $h = @{} } -Process { + $rel = [IO.Path]::GetRelativePath($build, $_.FullName).Replace('\', '/') + $h[$rel] = [IO.File]::ReadAllText($_.FullName) + } -End { $h } + ) + $zipPath + } + + $repoRoot = [IO.Path]::GetFullPath((Join-Path $PackageRoot '..\..\..')) + $realComponents = Get-Content -LiteralPath (Join-Path $PackageRoot 'Contracts/COMPONENTS.json') -Raw | ConvertFrom-Json -Depth 30 + $realPayloads = Get-Content -LiteralPath (Join-Path $PackageRoot 'Contracts/REQUIRED-PAYLOADS.json') -Raw | ConvertFrom-Json -Depth 30 + + $mcpDefinition = @($realPayloads.payloads | Where-Object key -eq 'McpRuntime')[0] + $mcpSourceRoot = Join-Path $repoRoot 'tools/mcp-runtime/current' + $mcpZip = New-KIPVCRealPayload -SourceRoot $mcpSourceRoot -Definition $mcpDefinition -Destination (Join-Path $scratchBase 'mcpruntime') + $mcpResult = Test-KICompletePayloadVersionContract -ComponentContract $realComponents -PayloadDefinition $mcpDefinition -PayloadZipPath $mcpZip + $checks.realMcpRuntime020Passes = [ordered]@{ + ok = [bool]$mcpResult.ok + checkedMcpRuntime = (@($mcpResult.checkedComponents) -contains 'mcp-runtime') + declaredFileIsV020 = ([string]$mcpDefinition.file -eq 'KI-Stack-MCP-Runtime-v0.2.0.zip') + } + if ($checks.realMcpRuntime020Passes.Values -contains $false) { $fail.Add('realMcpRuntime020Passes failed: ' + ($mcpResult.failures -join '; ')) } + + $dcDefinition = @($realPayloads.payloads | Where-Object key -eq 'DesktopControl')[0] + $dcSourceRoot = Join-Path $repoRoot 'tools/desktop-control/current' + $dcZip = New-KIPVCRealPayload -SourceRoot $dcSourceRoot -Definition $dcDefinition -Destination (Join-Path $scratchBase 'desktopcontrol') + $dcResult = Test-KICompletePayloadVersionContract -ComponentContract $realComponents -PayloadDefinition $dcDefinition -PayloadZipPath $dcZip + $checks.realDesktopControl011Passes = [ordered]@{ + ok = [bool]$dcResult.ok + checkedDesktopControl = (@($dcResult.checkedComponents) -contains 'desktop-control') + declaredFileIsV011 = ([string]$dcDefinition.file -eq 'KI-Stack-Desktop-Control-v0.1.1.zip') + } + if ($checks.realDesktopControl011Passes.Values -contains $false) { $fail.Add('realDesktopControl011Passes failed: ' + ($dcResult.failures -join '; ')) } + + # === 6b: real Cutover Runtime contract -- the exact real-world case that exposed the + # '-core' false positive above; a minimal real payload proves the fix against the + # actual repository contract, not only a synthetic fixture. ========================= + $cutoverDefinition = @($realPayloads.payloads | Where-Object key -eq 'CutoverRuntime')[0] + $cutoverComponent = @($realComponents.components | Where-Object id -eq 'cutover-runtime')[0] + $cutoverVersion = [string]$cutoverComponent.version + $cutoverZip = Join-Path $scratchBase 'cutoverruntime\CutoverProbe.zip' + New-KIPVCZip -ZipPath $cutoverZip -ArchiveRoot $cutoverDefinition.archiveRoot -Entries @{ 'VERSION' = $cutoverVersion } + $cutoverResult = Test-KICompletePayloadVersionContract -ComponentContract $realComponents -PayloadDefinition $cutoverDefinition -PayloadZipPath $cutoverZip + $checks.realCutoverRuntimeCoreSuffixPasses = [ordered]@{ + ok = [bool]$cutoverResult.ok + fileHasCoreSuffix = ([string]$cutoverDefinition.file -match '-core\.zip$') + checkedCutoverRuntime = (@($cutoverResult.checkedComponents) -contains 'cutover-runtime') + } + if ($checks.realCutoverRuntimeCoreSuffixPasses.Values -contains $false) { $fail.Add('realCutoverRuntimeCoreSuffixPasses failed: ' + ($cutoverResult.failures -join '; ')) } + + # === 7: negative control on the REAL contract -- reverting to the old, stale filename/ + # archiveRoot against the SAME real, freshly-built zip must fail (proves the gate + # would have caught the actual 2.19.0 defect before this fix). ======================= + $staleMcpDefinition = [pscustomobject]@{ key = 'McpRuntime'; source = $mcpDefinition.source; file = 'KI-Stack-MCP-Runtime-v0.1.0.zip'; archiveRoot = 'KI-Stack-MCP-Runtime-v0.1.0'; required = $true } + $staleResult = Test-KICompletePayloadVersionContract -ComponentContract $realComponents -PayloadDefinition $staleMcpDefinition -PayloadZipPath $mcpZip + $checks.negativeControlStaleMcpRuntimeFilenameFails = [ordered]@{ notOk = (-not [bool]$staleResult.ok) } + if ($checks.negativeControlStaleMcpRuntimeFilenameFails.Values -contains $false) { $fail.Add('negativeControlStaleMcpRuntimeFilenameFails failed -- gate would not have caught the real defect') } + + $passed = $fail.Count -eq 0 + [pscustomobject]@{ passed = $passed; checks = $checks; failures = @($fail) } | ConvertTo-Json -Depth 12 + if (-not $passed) { throw 'Payload-Version-Contract-Regression fehlgeschlagen.' } +} finally { + try { Remove-Item -LiteralPath $scratchBase -Recurse -Force -ErrorAction SilentlyContinue } catch {} +} From 0b89a710cb10e76b489cae0f04d9153165251c59 Mon Sep 17 00:00:00 2001 From: Robert Backhaus Date: Sun, 13 Sep 2026 13:41:45 +0200 Subject: [PATCH 3/3] fix: make MCP runtime validation profile idempotent --- tools/mcp-runtime/current/McpRuntime.psm1 | 56 ++++++++++ tools/mcp-runtime/current/SHA256SUMS.txt | 5 +- .../current/Test-KIStackMcpRuntime.ps1 | 14 ++- ...KIStackMcpRuntimeValidationGateProfile.ps1 | 101 ++++++++++++++++++ 4 files changed, 173 insertions(+), 3 deletions(-) create mode 100644 tools/mcp-runtime/current/Test-KIStackMcpRuntimeValidationGateProfile.ps1 diff --git a/tools/mcp-runtime/current/McpRuntime.psm1 b/tools/mcp-runtime/current/McpRuntime.psm1 index 1efc05b..077372e 100644 --- a/tools/mcp-runtime/current/McpRuntime.psm1 +++ b/tools/mcp-runtime/current/McpRuntime.psm1 @@ -921,4 +921,60 @@ function Test-KIMcpRuntimeOpenWebUIRegistration { [pscustomobject]@{ passed = $true; registered = ($null -ne $ownEntry); entry = $ownEntry; mutatesTarget = $false } } +# --- Validation-gate Open-WebUI model/profile idempotency (Test-KIStackMcpRuntime.ps1 own use) - +# +# Distinct concern from the tool-server registration above: Test-KIStackMcpRuntime.ps1's own +# point 12 (the real Open-WebUI agent test) creates a throwaway model/profile +# ('mcp-runtime-validation-gate-test') to run a chat completion against. That profile id is +# fixed (never randomized -- a fixed, well-known validation-gate profile id is the whole point), +# so a prior run's profile left behind by -SkipCleanup must be detected and reused, never +# blindly re-created (Open WebUI's own /api/v1/models/create rejects a duplicate id outright). + +function Get-KIMcpRuntimeOpenWebUIModelById { + # Existence check via the SAME read route already used in production elsewhere in this repo + # for exactly this purpose (OpenWebUIBallisticsPack.psm1's Get-BallisticsModel, + # OpenWebUIAgentPack.psm1's per-id lookup, Complete Installer's Operations/*.ps1) -- + # GET /api/v1/models/model?id=. Verified live against a real KI-Stack Open-WebUI + # instance (2026-09-13): 200 with the full model body when it exists, 404 with a JSON + # {"detail":"..."} body when it does not. Returns {found=$true;model=} or + # {found=$false;model=$null} for a genuine 404 -- any OTHER failure (network, 5xx, auth, + # malformed response) is never swallowed here; it propagates so the caller fails closed. + param( + [Parameter(Mandatory)][string]$OpenWebUIEndpoint, + [Parameter(Mandatory)][hashtable]$Headers, + [Parameter(Mandatory)][string]$ModelId, + [int]$TimeoutSec = 15 + ) + try { + $model = Invoke-RestMethod -Uri "$OpenWebUIEndpoint/api/v1/models/model?id=$([Uri]::EscapeDataString($ModelId))" -Headers $Headers -TimeoutSec $TimeoutSec + [pscustomobject]@{ found = $true; model = $model } + } catch { + if ($null -ne $_.Exception.Response -and [int]$_.Exception.Response.StatusCode.value__ -eq 404) { + [pscustomobject]@{ found = $false; model = $null } + } else { + throw + } + } +} + +function Resolve-KIMcpRuntimeValidationGateProfile { + # Pure create-or-reuse decision, deliberately separated from the real HTTP calls above so it + # is unit-testable without a live Open WebUI target: -GetProfile is expected to return the + # exact {found;model} shape Get-KIMcpRuntimeOpenWebUIModelById returns (or throw, for a + # genuine read failure); -CreateProfile performs the actual POST /api/v1/models/create (or + # throws, for a genuine create failure). Never invents a random profile id and never retries + # past a create failure -- both fail closed by simply propagating whatever their scriptblock + # throws, exactly like every other unguarded step in Test-KIStackMcpRuntime.ps1. + param( + [Parameter(Mandatory)][scriptblock]$GetProfile, + [Parameter(Mandatory)][scriptblock]$CreateProfile + ) + $lookup = & $GetProfile + if ([bool]$lookup.found) { + return [pscustomobject]@{ status = 'Reused'; model = $lookup.model } + } + & $CreateProfile + [pscustomobject]@{ status = 'Created'; model = $null } +} + Export-ModuleMember -Function * diff --git a/tools/mcp-runtime/current/SHA256SUMS.txt b/tools/mcp-runtime/current/SHA256SUMS.txt index 7ecb9a2..3329a39 100644 --- a/tools/mcp-runtime/current/SHA256SUMS.txt +++ b/tools/mcp-runtime/current/SHA256SUMS.txt @@ -2,14 +2,15 @@ b1f64c941eb71b617d9f9a0a14577d2cc8fc00a0d587f97106808fab0c29f9bd *Config/mcp-run 3bc087da182623be53d69d5f7035bd4826f0bd978496ea5d2c8a01a171adff44 *Invoke-KIStackMcpRuntime.ps1 15c2b390693a8c5d2d5004cdfe4f45cf8529a6ac3d7a4cf77edfab1087388fef *LOCAL-CONTROL-CONTRACT.md 89c31c7cf845ac447f23c65786b578d0707f33a276e674f00013e048839624cb *MANIFEST.json -1b3d6b57394b23e5bdaaecebf463bd88277c23659bc386a2ed3dfc0d4e5111db *McpRuntime.psm1 +ecdf45b368df130849792d3184f49711882dbe105c573265210291b69b65923e *McpRuntime.psm1 0664bece0eb86227272051442b17c65fbb6db48bf1eacc8c275bb20209bbc783 *README.md a07de0eb796e165c92f6bf7296d641766f185cee3b92b759012b9870b750fa11 *Scripts/mcp_launcher.py c3a9dc95a1679a4cfba457f6dfd0cb854e76714df436ce9339ce2e857201d3d1 *Scripts/ki_desktop_control_tools.py 75a86a331a86ff5c75c284ffa1a3e13b7d3e0fb7e636e453c46e34b0558a5197 *Scripts/test_ki_desktop_control_tools.py 97e78b81eed2284549ee165b0f74bdd041199879e09a9ec2effcb3cda1950399 *Test-KIStackLMStudioRuntimeBaseline.ps1 -e1c2f5284d6e503aa36382316e09e507f60e7f55534fee2095ba5ca23761d8cd *Test-KIStackMcpRuntime.ps1 +e6027bf6558815a98368d7117421358a4e3e5af495482211db223556a98bd357 *Test-KIStackMcpRuntime.ps1 4cdf6449f64eb23b2b12861b6b3c606b83fe8b9ee1bf153e3b85e9c77f3342a2 *Test-KIStackMcpRuntimeInstall.ps1 +3b6107af39ea02df624d5a8e59f9755764bdb2c40d874827fa6accf3f1c7cd80 *Test-KIStackMcpRuntimeValidationGateProfile.ps1 6af0d029842a60460b14b5ff7b786fe264c32eeec185bde5350d93afb171bfd8 *Vendor/KIStackOpenWebUICredential.psm1 79b082d875a35f8fed88b45d863d6b7b1db3b8846f5e540b9a028db13b833098 *Vendor/KIStackPathContext.psm1 c58a749f70f5e1ba5394ebcdbfcfa02fa938c4a173d8e41e6c476a7c8b2acddd *VERSION diff --git a/tools/mcp-runtime/current/Test-KIStackMcpRuntime.ps1 b/tools/mcp-runtime/current/Test-KIStackMcpRuntime.ps1 index 43b6812..c497ee5 100644 --- a/tools/mcp-runtime/current/Test-KIStackMcpRuntime.ps1 +++ b/tools/mcp-runtime/current/Test-KIStackMcpRuntime.ps1 @@ -135,7 +135,19 @@ asyncio.run(main()) } access_grants = @(); is_active = $true } | ConvertTo-Json -Depth 10 - Invoke-RestMethod -Uri "$OpenWebUIEndpoint/api/v1/models/create" -Method Post -Headers $adminHeaders -Body $profilePayload -TimeoutSec 15 | Out-Null + # Idempotent create-or-reuse (2.19.0 fix): GET /api/v1/models/model?id= -- the same + # existence-check route already used in production elsewhere in this repo for exactly this + # purpose, verified live against a real KI-Stack Open-WebUI instance -- decides Create vs + # Reuse, so a profile left behind by a prior -SkipCleanup run is detected and reused instead + # of Open WebUI's own /api/v1/models/create rejecting the now-duplicate fixed profile id + # ("Uh-oh! This model id is already registered."). The profile id itself stays fixed, never + # randomized -- a stable, well-known validation-gate profile id is the point. + $profileResolution = Resolve-KIMcpRuntimeValidationGateProfile -GetProfile { + Get-KIMcpRuntimeOpenWebUIModelById -OpenWebUIEndpoint $OpenWebUIEndpoint -Headers $adminHeaders -ModelId $profileId + } -CreateProfile { + Invoke-RestMethod -Uri "$OpenWebUIEndpoint/api/v1/models/create" -Method Post -Headers $adminHeaders -Body $profilePayload -TimeoutSec 15 | Out-Null + } + Add-KIMcpRuntimeCheck 'Testprofil angelegt oder wiederverwendet (idempotent)' $true "status=$($profileResolution.status) id=$profileId" $chatPayload = @{ chat = @{ title = 'mcp-runtime-validation-gate'; models = @($profileId); messages = @(); history = @{ messages = @{}; currentId = $null } } } | ConvertTo-Json -Depth 10 $chat = Invoke-RestMethod -Uri "$OpenWebUIEndpoint/api/v1/chats/new" -Method Post -Headers $adminHeaders -Body $chatPayload -TimeoutSec 15 diff --git a/tools/mcp-runtime/current/Test-KIStackMcpRuntimeValidationGateProfile.ps1 b/tools/mcp-runtime/current/Test-KIStackMcpRuntimeValidationGateProfile.ps1 new file mode 100644 index 0000000..9d0f2ca --- /dev/null +++ b/tools/mcp-runtime/current/Test-KIStackMcpRuntimeValidationGateProfile.ps1 @@ -0,0 +1,101 @@ +[CmdletBinding()] +param([string]$PackageRoot = $PSScriptRoot) + +Set-StrictMode -Version Latest +$ErrorActionPreference = 'Stop' + +# Self-contained regression suite for the 2.19.0 idempotency fix in Test-KIStackMcpRuntime.ps1's +# own OpenWebUI-agent-test step (point 12): a fixed, never-randomized validation-gate profile id +# ('mcp-runtime-validation-gate-test') previously caused "Uh-oh! This model id is already +# registered." on any run after a prior run (or a -SkipCleanup run) left the profile behind, +# because /api/v1/models/create was called unconditionally with no existence check. +# +# Exercises the REAL Resolve-KIMcpRuntimeValidationGateProfile (McpRuntime.psm1) -- the pure +# create-or-reuse decision Test-KIStackMcpRuntime.ps1 itself now calls -- via injected +# -GetProfile/-CreateProfile scriptblocks, so no live Open WebUI target, no real HTTP call, and +# no LM Studio/MCP server are needed to prove the decision logic itself is correct. The +# underlying HTTP existence-check route it is built on +# (Get-KIMcpRuntimeOpenWebUIModelById -> GET /api/v1/models/model?id=) is the SAME route +# already used in production by OpenWebUIBallisticsPack.psm1/OpenWebUIAgentPack.psm1/Complete +# Installer's Operations/*.ps1, and was additionally verified live against a real, running +# KI-Stack Open-WebUI instance (2026-09-13): 200 with the model body when it exists, 404 with a +# JSON {"detail":"..."} body when it does not. + +Import-Module (Join-Path $PackageRoot 'McpRuntime.psm1') -Force + +$fail = [Collections.Generic.List[string]]::new() +$checks = [ordered]@{} + + # === A: profile fehlt -> Create, kein vorzeitiger Abbruch ================================== + $createCalledA = $false + $resultA = Resolve-KIMcpRuntimeValidationGateProfile -GetProfile { + [pscustomobject]@{ found = $false; model = $null } + } -CreateProfile { + $script:createCalledA = $true + } + $checks.missingProfileCreates = [ordered]@{ + statusIsCreated = ([string]$resultA.status -eq 'Created') + createWasCalled = $createCalledA + } + if ($checks.missingProfileCreates.Values -contains $false) { $fail.Add('missingProfileCreates failed: ' + ($checks.missingProfileCreates | ConvertTo-Json -Compress)) } + + # === B: profile existiert -> kein Create, Reuse ============================================ + $createCalledB = $false + $fakeExistingModel = [pscustomobject]@{ id = 'mcp-runtime-validation-gate-test'; name = 'MCP Runtime Validation Gate Test' } + $resultB = Resolve-KIMcpRuntimeValidationGateProfile -GetProfile { + [pscustomobject]@{ found = $true; model = $fakeExistingModel } + } -CreateProfile { + $script:createCalledB = $true + throw 'CreateProfile must never be invoked when the profile already exists' + } + $checks.existingProfileReuses = [ordered]@{ + statusIsReused = ([string]$resultB.status -eq 'Reused') + createWasNotCalled = (-not $createCalledB) + returnsExistingModel = ($resultB.model.id -eq 'mcp-runtime-validation-gate-test') + } + if ($checks.existingProfileReuses.Values -contains $false) { $fail.Add('existingProfileReuses failed: ' + ($checks.existingProfileReuses | ConvertTo-Json -Compress)) } + + # === C: Read-API schlaegt fehl (nicht 404, z.B. 500/Netzwerkfehler) -> fail closed ========== + $thrownC = $null + try { + Resolve-KIMcpRuntimeValidationGateProfile -GetProfile { + throw 'simulated read failure (HTTP 500)' + } -CreateProfile { + throw 'CreateProfile must never be invoked when the read itself failed' + } | Out-Null + } catch { $thrownC = $_ } + $checks.readApiFailureFailsClosed = [ordered]@{ + threw = ($null -ne $thrownC) + neverReachedCreate = ($null -ne $thrownC -and $thrownC.Exception.Message -notmatch 'CreateProfile must never be invoked') + } + if ($checks.readApiFailureFailsClosed.Values -contains $false) { $fail.Add('readApiFailureFailsClosed failed: ' + ($checks.readApiFailureFailsClosed | ConvertTo-Json -Compress)) } + + # === D: Profile fehlt, aber Create schlaegt aus einem ANDEREN Grund fehl -> fail closed ===== + $thrownD = $null + try { + Resolve-KIMcpRuntimeValidationGateProfile -GetProfile { + [pscustomobject]@{ found = $false; model = $null } + } -CreateProfile { + throw 'simulated create failure (HTTP 400 Bad Request)' + } | Out-Null + } catch { $thrownD = $_ } + $checks.createFailureFailsClosed = [ordered]@{ + threw = ($null -ne $thrownD) + reasonSurfaced = ($null -ne $thrownD -and $thrownD.Exception.Message -match 'simulated create failure') + } + if ($checks.createFailureFailsClosed.Values -contains $false) { $fail.Add('createFailureFailsClosed failed: ' + ($checks.createFailureFailsClosed | ConvertTo-Json -Compress)) } + + # === E: Get-KIMcpRuntimeOpenWebUIModelById itself -- 404 maps to found=$false, any other + # HTTP failure is never swallowed (structural check on the real function, no live + # target -- Invoke-RestMethod is redirected to a fake endpoint that never resolves, + # proving a genuine connection failure is NOT misread as "not found"). ================ + $thrownE = $null + try { + Get-KIMcpRuntimeOpenWebUIModelById -OpenWebUIEndpoint 'http://127.0.0.1:1' -Headers @{ Authorization = 'Bearer x' } -ModelId 'whatever' -TimeoutSec 2 | Out-Null + } catch { $thrownE = $_ } + $checks.connectionFailureNeverMisreadAs404 = [ordered]@{ threw = ($null -ne $thrownE) } + if ($checks.connectionFailureNeverMisreadAs404.Values -contains $false) { $fail.Add('connectionFailureNeverMisreadAs404 failed: expected a thrown connection error, got none') } + +$passed = $fail.Count -eq 0 +[pscustomobject]@{ passed = $passed; checks = $checks; failures = @($fail) } | ConvertTo-Json -Depth 12 +if (-not $passed) { throw 'MCP-Runtime-ValidationGate-Profile-Idempotency-Regression fehlgeschlagen.' }