KI-Stack 2.18: Wenn eine KI plötzlich Windows bedienen darf

Mit KI-Stack 2.18 bekommt der lokale KI-Stack erstmals eine kontrollierte Desktop-Steuerung auf Basis von Windows UI Automation. Der Artikel zeigt, welche Probleme dabei aufgetreten sind, warum plausible Annahmen scheiterten, wie die Fehler entdeckt wurden und welche davon vermeidbar gewesen wären.

Mit KI-Stack 2.18 bin ich an einen Punkt gekommen, an dem sich das Projekt für mich deutlich verändert hat. Bis dahin konnte der Stack lokale Sprachmodelle betreiben, Wissen über RAG einbinden, recherchieren, Bilder und Videos erzeugen, über MCP auf definierte Funktionen zugreifen und kontrolliert Kommandos ausführen. Das sind durchaus mächtige Fähigkeiten, aber sie bewegen sich trotzdem in vergleichsweise klar definierten technischen Grenzen.

Mit Desktop Control kommt eine andere Klasse von Funktion hinzu: Der KI-Stack kann Anwendungen über ihre grafische Oberfläche bedienen.

Das klingt zunächst weniger spektakulär, als es technisch tatsächlich ist. Ein Textfeld finden, einen Wert eintragen und anschließend einen Button auslösen scheint keine besonders schwierige Aufgabe zu sein. Für einen Menschen stimmt das auch. Für ein automatisiertes System stellt sich aber sofort eine ganze Reihe unangenehmer Fragen: Ist es wirklich das richtige Fenster? Ist das gefundene Element noch dasselbe wie vor einer Sekunde? Ist das vermeintliche Eingabefeld vielleicht ein Passwortfeld? Hat der Klick tatsächlich etwas ausgelöst? Und was passiert, wenn Windows oder die Anwendung über ihre Accessibility-Schnittstelle Informationen liefert, die schlicht falsch sind?

Genau an diesen Punkten wurde KI-Stack 2.18 wesentlich interessanter als ursprünglich erwartet.

Warum überhaupt Desktop Control?

Mein Ziel war nicht, eine weitere Variante von Maus- und Tastaturautomation zu bauen. Davon gibt es genug, und gerade im Zusammenhang mit einem LLM halte ich globale Tastatureingaben oder Mausklicks auf Bildschirmkoordinaten für eine ziemlich schlechte Abstraktion.

Wenn eine KI einen Button bei Position 1247, 683 anklickt, weiß sie letztlich nicht, was sie dort anklickt. Eine andere Bildschirmauflösung, ein verschobenes Fenster oder ein zusätzlicher Dialog können reichen, um aus einer eigentlich harmlosen Aktion etwas völlig anderes zu machen.

Deshalb basiert Desktop Control in 2.18 auf Windows UI Automation und Microsofts winapp. Statt „klicke an Position X/Y“ geht es beispielsweise darum, ein bestimmtes Fenster zu identifizieren, darin ein Element mit bestimmten Eigenschaften zu finden und anschließend eine semantische Aktion wie set_value, invoke oder focus auszuführen.

winapp 0.6.1 wird dafür zentral im KI-Stack bereitgestellt. Darüber liegt eine eigene Desktop-Control-Schicht. Das LLM bekommt also keinen direkten Zugriff auf winapp, sondern muss durch einen Wrapper, der Ziel, Policy und Ergebnis überprüft.

Das Prinzip dahinter ist inzwischen sehr einfach formuliert:

Resolve → Validate → Act → Re-observe → Verify

In der Implementierung steckt hinter diesen fünf Begriffen allerdings ein erheblicher Teil der Arbeit von 2.18.

Funktionieren und nachweisen, dass etwas funktioniert hat, sind zwei verschiedene Dinge

Eine der wichtigsten Erkenntnisse während der Entwicklung war eigentlich banal: Ein Prozess, der mit Exitcode 0 endet, beweist lediglich, dass der Prozess keinen Fehler gemeldet hat.

Er beweist nicht, dass in der Oberfläche das passiert ist, was ich wollte.

Gerade bei GUI-Automation ist dieser Unterschied entscheidend. Ein Aufruf kann technisch erfolgreich verarbeitet werden, obwohl sich der Zustand der Anwendung nicht wie erwartet verändert hat. Noch unangenehmer wird es bei Aktionen, die wiederum neue Fenster, Dialoge oder veränderte UI-Bäume erzeugen.

Deshalb wertet KI-Stack einen erfolgreichen winapp-Aufruf ausdrücklich nicht automatisch als erfolgreiche Desktop-Aktion.

Nach einer schreibenden Aktion wird die Oberfläche erneut untersucht. Bei set_value wird beispielsweise nicht nur ein Wert gesetzt, sondern anschließend unabhängig wieder ausgelesen. Bei invoke wird der UI-Baum erneut betrachtet, wenn durch die Aktion eine Änderung erwartet wird. Auch focus wird nicht allein aufgrund eines erfolgreichen Befehls als erledigt betrachtet, sondern über den anschließend tatsächlich fokussierten Zustand geprüft.

Das klingt rückblickend nach einer offensichtlichen Architekturentscheidung. Sie ist aber ein gutes Beispiel dafür, warum ich inzwischen zwischen einem funktionierenden technischen Spike und einer produktiv verwendbaren Komponente sehr deutlich unterscheide.

Ein Spike beantwortet die Frage: „Kann ich das grundsätzlich machen?“

Eine produktive Komponente muss die wesentlich unangenehmere Frage beantworten: „Kann ich beweisen, dass genau das passiert ist, was passieren sollte – und nichts anderes?“

Der Fehler, der eigentlich keiner sein dürfte

Einer der interessantesten Fälle während der Entwicklung hatte mit Passwortfeldern zu tun.

Ein System, das Oberflächen lesen und verändern darf, sollte selbstverständlich nicht unkontrolliert auf Passwortfelder oder andere sensible Eingabeelemente zugreifen können. Windows UI Automation liefert dafür Eigenschaften, unter anderem IsPassword.

Die naheliegende Implementierung wäre also:

IsPassword = true bedeutet: Zugriff blockieren.

Sauber, einfach und vermeintlich sicher.

Leider meldete die Adresszeile des Windows Explorers während der Tests ebenfalls IsPassword=true.

Damit war eine Annahme gebrochen, auf der eine Sicherheitsentscheidung hätte beruhen können. Nicht weil unser Code das falsche Element gefunden hatte, sondern weil die darunterliegende Schnittstelle eine Eigenschaft lieferte, die in diesem Kontext schlicht nicht der Realität entsprach.

Genau solche Fehler interessieren mich bei diesem Projekt inzwischen besonders. Der Code kann formal korrekt sein und trotzdem ein falsches Ergebnis produzieren, weil die Welt unterhalb des Codes nicht so sauber ist wie das Datenmodell.

Die erste Reaktion darauf darf natürlich nicht sein, IsPassword einfach zu ignorieren. Noch schlimmer wäre eine globale Ausnahme nach dem Muster „Element mit dem Namen Address darf immer gelesen werden“. Damit würde ich einen False Positive beseitigen und mir gleichzeitig eine wesentlich größere Sicherheitslücke bauen.

Die Lösung in 2.18 ist deshalb ein zusammengesetzter Secret-Check. IsPassword ist ein Signal, aber nicht das einzige. Name, Automation ID, Klassenname und Prozesskontext werden ebenfalls berücksichtigt. Ausnahmen sind eng an einen bekannten Kontext gebunden und greifen nur dann, wenn genau dieses eine fehlerhafte Signal vorliegt.

Das macht die Implementierung komplizierter. Es macht sie aber auch näher an der Realität.

Warum ist dieser Fehler überhaupt entstanden?

Die kurze Antwort lautet: weil eine plausible Annahme falsch war.

Und genau hier wird die Frage interessant, ob der Fehler vermeidbar gewesen wäre.

Mit genügend Misstrauen hätte ich natürlich von Anfang an davon ausgehen können, dass jede einzelne Eigenschaft von Windows UI Automation falsch sein kann. Damit kommt man architektonisch allerdings auch nicht besonders weit. Irgendwann muss ein System externe Informationen als Eingangsdaten akzeptieren.

Der eigentliche Fehler wäre deshalb aus meiner Sicht nicht gewesen, IsPassword zunächst ernst zu nehmen. Der Fehler wäre gewesen, dieses Verhalten nicht mit realen Anwendungen zu testen und anschließend davon auszugehen, dass die dokumentierte Bedeutung einer Eigenschaft immer der tatsächlichen Implementierung entspricht.

Genau dafür gab es den Desktop-Control-Spike und später die realen End-to-End-Tests.

Der False Positive wurde nicht durch theoretische Betrachtung entdeckt, sondern weil die Komponente tatsächlich gegen Windows und reale UI-Strukturen getestet wurde.

Das ist ein wichtiger Unterschied.

Ein UI-Element ist keine Datenbank-ID

Ein zweites Problem liegt tiefer in der Natur grafischer Oberflächen.

Wenn ich in einer Datenbank einen Datensatz über eine eindeutige ID anspreche, kann ich normalerweise davon ausgehen, dass diese ID zumindest innerhalb ihres definierten Gültigkeitsbereichs stabil ist. Grafische Oberflächen funktionieren nicht so.

Fenster entstehen und verschwinden. Elemente werden neu erzeugt. UI-Bäume ändern sich. Anwendungen rendern Teile ihrer Oberfläche dynamisch. Zwei Elemente können ähnliche Namen tragen. Selektoren können vom Backend erzeugt werden, ohne eine dauerhaft belastbare Identität darzustellen.

Deshalb akzeptiert Desktop Control einen von winapp erzeugten Selektor nicht als alleinigen Identitätsnachweis.

Vor einer Aktion wird das Fenster frisch aufgelöst. Der Prozess wird erneut geprüft. Der Titelkontext wird betrachtet. Anschließend wird das Element im aktuellen UI-Baum neu gesucht und anhand mehrerer Eigenschaften validiert. Es muss eindeutig sein, aktiv sein und darf beispielsweise nicht außerhalb des sichtbaren UI-Kontexts liegen.

Danach wird gehandelt.

Und danach wird erneut beobachtet.

Dieses Vorgehen kostet zusätzliche Aufrufe und macht Desktop Control langsamer als eine möglichst direkte Automation. Dafür verhindert es eine ganze Klasse von Fehlern, die sonst besonders unangenehm wären: technisch korrekte Aktionen gegen das falsche Ziel.

Bei einer KI halte ich diese zusätzliche Vorsicht für wichtiger als einige hundert Millisekunden weniger Laufzeit.

Warum einige Funktionen in 2.18 bewusst fehlen

Während der Entwicklung war es verlockend, möglichst viele Funktionen direkt freizugeben. winapp kann mehr, als Desktop Control momentan nach außen anbietet.

Ich habe mich dagegen entschieden.

In 2.18 sind unter anderem list_windows, inspect_window, find_element, get_properties, get_value, wait_for, set_value, invoke und focus Teil der kontrollierten Oberfläche. Andere Funktionen, insbesondere scroll beziehungsweise scroll_into_view, bleiben zunächst gesperrt, weil ich das Backend-Verhalten dafür noch nicht ausreichend verifizieren konnte.

Das ist kein vergessenes Feature, sondern Absicht.

Wenn eine Capability nicht zuverlässig nachgewiesen ist, liefert Desktop Control BackendCapabilityUnverified statt zu versuchen, irgendwie weiterzumachen.

Noch deutlicher ist die Grenze bei klassischen Eingabemechanismen. Globale Tastaturinjektion, Send-Keys, System-Hotkeys, koordinatenbasierte Mausklicks, beliebige Drag-Aktionen oder die direkte Ausführung beliebiger winapp-Kommandos werden überhaupt nicht exponiert.

Damit ist Desktop Control sicherlich weniger universell als eine klassische Desktop-Automation.

Das soll es aber auch sein.

Die unspektakulären Fehler waren teilweise die wichtigeren

Nicht alle Probleme von 2.18 hatten mit Windows UI Automation zu tun.

Mit winapp kam erstmals eine weitere externe Binärkomponente in diesen Teil des Stacks. Ich wollte sie nicht irgendwie auf einem Rechner installieren und anschließend voraussetzen, dass sie vorhanden ist. Sie sollte genauso reproduzierbar Teil der KI-Stack-Installation werden wie die anderen Komponenten.

Damit entstanden plötzlich Fragen zu Versionierung, Downloadquelle, Hashes, Installationszustand, SBOM und Reconcile-Verhalten.

Gerade hier zeigte sich erneut ein Problem, das ich im KI-Stack mittlerweile mehrfach gesehen habe: Eine Versionsnummer allein beschreibt keinen Zustand zuverlässig.

Angenommen, Desktop Control 0.1.0 ist installiert. Anschließend wird innerhalb der Entwicklung noch eine Datei korrigiert, ohne die Component-Version zu ändern. Ein Installer, der lediglich prüft, ob auf dem Ziel ebenfalls „0.1.0“ steht, könnte daraus schließen, dass nichts zu tun ist.

Version stimmt.

Inhalt stimmt möglicherweise nicht.

Deshalb wurde die Prüfung erweitert. Der Complete Installer betrachtet nicht nur Versionsinformationen und Installationsmarker, sondern prüft auch die tatsächliche Payload-Parität und Hashes der installierten Dateien.

Das ist weniger sichtbar als die eigentliche Desktop-Steuerung, aber für einen reproduzierbaren Stack mindestens genauso wichtig.

Hätten wir uns die Fehler sparen können?

Teilweise ja.

Ein paar Dinge hätte man früher und strenger modellieren können. Externe UI-Eigenschaften grundsätzlich als nicht vertrauenswürdige Eingangsdaten zu betrachten, hätte beispielsweise von Anfang an zu einer zusammengesetzten Secret-Erkennung führen können. Auch der Gedanke, dass „gleiche Version“ nicht automatisch „gleicher Inhalt“ bedeutet, ist nicht neu und hätte unmittelbar in die Installer-Logik einfließen können.

Andere Probleme halte ich dagegen praktisch nicht für vollständig vermeidbar.

Dass ausgerechnet die Explorer-Adresszeile als Passwortfeld auftaucht, kann ich in einer Architekturzeichnung nicht herleiten. Auch das konkrete Verhalten semantischer UI-Aktionen lässt sich nicht vollständig aus einer Dokumentation ableiten. Dafür muss die Software gegen reale Oberflächen laufen.

Man kann solche Fehler also nicht unbedingt verhindern.

Man kann aber entscheiden, wann man sie finden möchte.

Ich möchte sie im Spike, im Contract-Test oder spätestens im End-to-End-Test finden – nicht irgendwann durch einen Agenten auf einem produktiv genutzten Desktop.

Das ist für mich der eigentliche Qualitätsunterschied.

Tests als Gegenmodell zur eigenen Annahme

Ich versuche beim KI-Stack zunehmend, Tests nicht nur als Beweis dafür zu verwenden, dass mein Code funktioniert. Interessanter sind Tests, die versuchen, meine Annahmen zu widerlegen.

  • Was passiert bei zwei passenden Fenstern?
  • Was passiert mit einem minimierten Fenster?
  • Was passiert, wenn ein Element zwischen Observation und Aktion verschwindet?
  • Was passiert, wenn der Prozess nicht zu dem erwarteten Fenster gehört?
  • Was passiert, wenn ein angebliches Passwortfeld keines ist?
  • Was passiert, wenn ein Tool Exitcode 0 meldet, der erwartete Zustand anschließend aber nicht nachweisbar ist?
  • Was passiert, wenn eine Datei innerhalb einer bereits installierten Version verändert wurde?
  • Aus genau solchen Fragen sind Teile der heutigen Architektur entstanden.

Desktop Control enthält deshalb nicht nur Happy-Path-Tests, sondern arbeitet auch mit falschen und bewusst feindlichen UI-Bäumen. Dazu kamen reale End-to-End-Tests. list_windows, inspect_window, find_element, get_properties, get_value, wait_for, set_value, invoke und focus wurden dabei tatsächlich gegen die reale Komponente geprüft. Gerade die mutierenden Operationen waren für mich entscheidend, weil sich dort zeigt, ob Re-Observation und Postcondition wirklich funktionieren.

Das hat 2.18 langsamer entstehen lassen, als wenn ich einfach winapp an MCP gehängt hätte.

Ich halte das inzwischen für eine gute Sache.

Warum Desktop Control noch nicht direkt am MCP hängt

Auch hier habe ich mich gegen den schnellen Weg entschieden.

Der bestehende MCP Runtime des KI-Stacks hat bislang keine eigene beliebig erweiterbare Tool-Registry für solche KI-Stack-Funktionen. Desktop Control einfach irgendwie dort hineinzubauen hätte bedeutet, gleichzeitig die Desktop-Control-Komponente und die MCP-Architektur zu verändern.

Damit wären zwei Probleme vermischt worden.

Deshalb ist die Desktop-Control-Schicht zunächst eigenständig entstanden: Resolver, Policy, Wrapper, Audit, Tests und reale Aktionen funktionieren unabhängig von der späteren Agentenanbindung. Die eigentliche MCP-Integration ist ein separater Schritt.

Ich kann dadurch zuerst nachweisen, dass die Kontrollschicht funktioniert, bevor ich einem LLM die Möglichkeit gebe, sie selbstständig aufzurufen.

Gerade bei einer Funktion, die Anwendungen verändern kann, halte ich diese Reihenfolge für vernünftiger als „erst anbinden und anschließend absichern“.

Was ich aus 2.18 mitnehme

KI-Stack 2.18 hat mich weniger über GUI-Automation gelehrt als über Systemgrenzen.

Je näher ein KI-System an reale Aktionen kommt, desto weniger darf ich mich auf einfache Wahrheiten verlassen. Ein Exitcode ist kein Wirkungsnachweis. Ein Selektor ist keine Identität. Eine Versionsnummer ist kein Zustandsnachweis. Und selbst eine Betriebssystemeigenschaft namens IsPassword bedeutet offenbar nicht zwangsläufig, dass ich gerade vor einem Passwortfeld stehe.

Das klingt fast absurd, beschreibt aber ziemlich gut die Realität, in der Automatisierung funktionieren muss.

Entscheidend ist deshalb nicht, ein System zu bauen, in dem keine Fehler auftreten. Das halte ich bei hinreichend komplexer Software ohnehin für eine Illusion. Entscheidend ist, Fehler dort sichtbar zu machen, wo ihre Folgen noch beherrschbar sind.

Für mich ist genau das die wichtigste Änderung in KI-Stack 2.18.

Der Stack kann jetzt nicht einfach nur mehr.

Er beginnt, seine eigenen Aktionen wesentlich strenger zu hinterfragen.

Und bevor ich einem lokalen KI-Agenten einen Windows-Desktop überlasse, ist mir das deutlich wichtiger als eine möglichst lange Liste unterstützter Befehle.