Kann eine KI meinen Rechner bedienen, ohne auf ihm zu wohnen?

Die Frage vom Wochenende

Am Wochenende habe ich mit einem Bekannten diskutiert, ob eine KI einen Rechner steuern kann, ohne dass auf diesem Rechner irgendetwas installiert wird. 

Auf dem Schreibtisch stand seit ein paar Tagen ein JetKVM, ursprünglich für einen anderen Zweck bestellt. Damit lag das Experiment nahe. Ein verlängertes Wochenende, ein Mac mini im Homelab, ein paar Stunden zum Spielen. Was draus geworden ist, beschreibe ich hier — mit den Stolperstellen, an denen ich kürzer hängengeblieben wäre, wenn ich sie vorher gekannt hätte.

Was ein JetKVM ist und warum er für KI interessant ist

Ein JetKVM ist ein kleines Kästchen. Auf der einen Seite HDMI rein und USB-C raus, auf der anderen Ethernet. Man hängt es an einen Rechner wie einen Monitor mit zusätzlicher Tastatur: das HDMI-Signal geht in das Kästchen hinein, das USB-Kabel meldet sich am Rechner als ganz normale Tastatur und Maus. Bedient wird das Ganze über ein Web-UI im Browser, das den Bildschirminhalt anzeigt und Tastatur-Eingaben sowie Mausklicks zurückschickt.

Für den Zielrechner sieht das aus wie ein normaler Monitor und ein normales Eingabegerät. Kein Agent, kein VNC-Server, kein RDP-Client. Genau das macht den JetKVM als Hardware-Brücke interessant, sobald man eine KI als Bediener einsetzen will. Der Zielrechner muss nichts wissen und nichts können — er braucht nur Strom und ein HDMI-Kabel.

Erste Sackgasse: die Python-Library, die nicht reicht

Suche nach einer Python-Bibliothek, die mit JetKVM redet. Erstes Ergebnis: JetKey von David Horner. Beschreibung: „jetKVM RPC client that is able to send keyboard input to jetkvm”. Klingt nach dem richtigen Werkzeug. War es nicht.

Ein kurzer Blick in die jetkey.py zeigt: alles dreht sich um keyboardReport. Kein absMouseReport, kein wheelReport, kein Video-Empfänger. Für eine KI, die „klicke auf den roten Button” ausführen können soll, reicht das nicht. Maus brauche ich, Klicks brauche ich, und ich brauche ein Bild davon, was die KI sehen soll.

Bleibt der Selbstbau. JetKey ist trotzdem nützlich: die ~700 Zeilen sind eine gute Vorlage für den WebRTC-Aufbau zum JetKVM — HTTP-Login, WebSocket-Signaling, SDP-Tausch, DataChannel-Öffnen. Den Aufbau-Code habe ich übernommen und um Maus, Scroll und Video-Empfang ergänzt.

Wie das Ganze zusammensteckt

Drei Komponenten, eine pro Modul:

  • jetkvm_client.py baut die WebRTC-Verbindung auf, abonniert den Video-Stream und stellt drei Funktionen bereit: Snapshot holen, Maus bewegen und klicken, Tastatur drücken. JSON-RPC 2.0 läuft auf einem DataChannel.
  • ai_brain.py schickt den aktuellen Screenshot zusammen mit der Anweisung des Anwenders an Claude Sonnet 4.6 und parst die strukturierte JSON-Antwort. Zurück kommt eine Liste mit Schritten — click, type, key, wait — jeweils mit Pixel-Koordinaten oder Tippinhalt.
  • app.py ist die Streamlit-Oberfläche im Browser. Drei Spalten: Screenshot links, Eingabefeld plus Plan-Vorschau in der Mitte, Log rechts.

Eine Iteration läuft so: Screenshot vom JetKVM holen, mit Anweisung an Claude schicken, Plan in der UI anzeigen. Ich bestätige, die Aktionen gehen einzeln an den JetKVM, danach holt das Skript automatisch einen frischen Screenshot. Fertig oder weiter.

Der Code liegt unter https://github.com/staude/jetkvm-ai-demo, MIT lizenziert, JetKey-Vorlage entsprechend attributiert.

Sechs Stolperstellen, die ich gerne früher gewusst hätte

Bis dieser einfach klingende Ablauf funktioniert hat, haben sechs Sachen Zeit gekostet. Hier in der Reihenfolge, in der sie aufgetreten sind.

Cookie-Auth für IP-Adressen. Mein erster Snapshot-Versuch lief zwanzig Sekunden ins Leere. Login klappte (POST /auth/login-local antwortet mit „Login successful”), aber die anschließende WebSocket-Verbindung bekam 401 zurück. Ursache: aiohttp weigert sich standardmäßig, Cookies für IP-Hosts zu speichern. RFC 6265 (Abschnitt 5.1.3, Domain Matching) schließt IP-Adressen vom Domain-Matching aus — Cookies werden nur für Hostnamen verteilt. Bei jetkvm.local hätte es geklappt, bei 192.168.178.195 nicht. Lösung ist CookieJar(unsafe=True) beim Session-Erzeugen. Zwei Stunden Suche für vier Zeichen Code.

Eine Aktion pro Antwort vs. echte Aufgaben. Mein erstes Prompt-Schema zwang die KI in genau eine Aktion pro Antwort. Bei „URL eintippen und Enter drücken” sind das aber drei Schritte. Die KI klickte also nur in die Adressleiste, in der Hoffnung, der Mensch fragt nochmal. Tat ich auch. Nur: den Screenshot zwischendurch hatte ich nicht aktualisiert. Die KI sah dasselbe Bild, schlug denselben Klick vor. Vier Mal hintereinander. Lösung: das Antwort-Schema von einer Aktion auf eine Aktion-Sequenz umstellen und nach jedem ausgeführten Plan automatisch einen frischen Screenshot holen.

Streamlit zeigt das alte Bild. Frischer Snapshot war geholt, in st.session_state.snapshot gelegt — und in der UI tauchte trotzdem das alte Bild auf. Grund: die linke Spalte mit st.image() ist in derselben Run-Phase schon gerendert worden. Streamlit rendert nicht nochmal, nur weil sich State später ändert. Lösung ist ein st.rerun()-Aufruf nach jeder Plan-Ausführung. Kostet einen ganzen Script-Durchlauf, fühlt sich aber konsistent an.

Verrutschte Koordinaten. Diese Stolperstelle hat mich am meisten Zeit gekostet. Die Symptome waren zuverlässig, der Grund nicht offensichtlich. Vor dem Upload zu Claude skaliere ich den Screenshot auf 1280×720 herab, um Tokens zu sparen. Im System-Prompt stand aber: „Du siehst einen Screenshot mit Auflösung 1920×1080 Pixel”. Die KI tat das Pragmatische: sie antwortete mit Pixel-Koordinaten, die zum tatsächlich gezeigten Bild passen, also in 1280er-Skala. Mein Code nahm diese Werte für bare Münze und schickte sie ungeprüft als Bildschirm-Pixel an den JetKVM. Resultat: jeder Klick landete immer wieder zu weit links oben — die Maus traf den Browser-Inhalt statt die URL-Leiste. Lösung in zwei Teilen: die echte Bildgröße in den Prompt schreiben (aus dem JPEG selbst auslesen) und die zurückkommenden Koordinaten im Code proportional auf die Zielauflösung skalieren.

Deutsche Tastatur. HID-Scancodes sind nach Positionen auf einer US-Tastatur fest definiert. Welches Zeichen das Betriebssystem daraus macht, hängt vom aktiven Layout ab. Auf einem deutschen System ist die Taste mit Scancode 0x37 die .-Taste — Shift+0x37 produziert :. Mein US-Mapping schickte für : aber Shift+0x33. Auf US-Layout wäre das korrekt: ; und : sitzen dort auf derselben Taste. Auf dem deutschen Mac mini wurde daraus Ö. Aus https://staude.net wurde damit irgendetwas anderes. Lösung ist ein eigenes Layout-Mapping in einer keymaps.py, mit US- und deutscher Tabelle einschließlich AltGr-Kombinationen für @, , {, [, ], }, , ~, |.

Tipp-Geschwindigkeit und Modifier-Hänger. Die erste Version tippte mit 10 ms Pause zwischen den Zeichen. Bei doppelten Buchstaben wie tt in https verschluckte das OS gelegentlich den zweiten Anschlag. Außerdem: wenn ein Zeichen mit Shift oder AltGr endete und der Release-Report aus irgendeinem Grund verzögert ankam, blieb der Modifier am JetKVM hängen. Das nächste Zeichen kam dann mit ungewolltem Shift an. Lösung: 60 ms Pause zwischen Zeichen, und vor jedem Tasten-Chord vorsichtshalber ein expliziter „alles loslassen”-Report.

Erster sauberer Durchlauf

Nach dem dritten oder vierten Stolperer lief die Pipeline durch. Anweisung: „Klicke in das URL-Feld des Browsers, übermittle die Adresse https://staude.net und schicke sie mit Return ab.” Die KI lieferte einen Plan zurück, drei Schritte:

  1. Klick auf Pixel (886, 61) — URL-Feld anklicken.
  2. Tippe „https://staude.net” — Adresse eingeben.
  3. Drücke Enter — bestätigen.

Die KI hatte ein Bild gesehen, eine Aufgabe verstanden, in HID-kompatible Mikro-Schritte heruntergebrochen und zurückgegeben. Mein Streamlit-Skript hat sie nur noch durchgereicht.

 

Sicherheit: Vorschau-Modus von Anfang an

In der Demo geht keine KI-Aktion direkt an den JetKVM. Die KI schlägt einen Plan vor, der Plan steht als Liste in der UI, ich klicke auf „Plan ausführen”, erst dann werden die Schritte gesendet. Zusätzlich: eine Datei STOP im Projekt-Ordner blockt jede Ausführung — touch STOP ist der schnellste Notaus, der nicht auf eine bestimmte Tastenkombination angewiesen ist.

Das ist prophylaktisch eingebaut. Eine konkrete Situation, in der die KI etwas vorgeschlagen hat, das ich nicht ausgeführt sehen wollte, habe ich beim Testen nicht erlebt. Aber sobald aus einer Demo ein Werkzeug werden soll, müsste die Schwelle noch tiefer liegen: Bounding-Boxen einbauen, in denen Klicks zulässig sind, Iterations-Budget pro Auftrag hart begrenzen, Logging der gesendeten HID-Reports an einer separaten Stelle, an die die KI nicht ranreicht.

Spielzeug oder Werkzeug

Aktuell ist das eine Spielerei. „Ob es geht” als Antwort auf die Wochenend-Frage, mehr nicht. Ob daraus produktiv etwas wird, weiß ich nicht.

Wenn doch, dann mit lokaler KI, nicht mit einer kommerziellen Vision-API. Der Grund ist nüchtern: nach jedem Schritt braucht die KI ein neues Bild zur Analyse. Eine kleine Aufgabe wie „URL eingeben” sind schon drei Schritte plus mindestens ein Verifizierungs-Screenshot, größere Aufgaben werden schnell zwanzig Iterationen. Bei kommerziellen Tokenpreisen für 1280×720 JPEG ist die Spielerei-Schwelle nach ein paar Demos erreicht; produktiver Einsatz würde sich finanziell sofort bemerkbar machen.

Der JetKVM selbst war ohnehin für einen anderen Anwendungsfall gekauft. Die KI-Steuerung ist nur ein Nebenexperiment am verlängerten Wochenende.

Antwort, jetzt mit Belegen

Ja, eine KI kann einen Rechner bedienen, ohne dass auf ihm irgendetwas installiert ist. Es braucht ein Hardware-Stück zwischen Quelle und Ziel, ein Vision-Modell für die Wahrnehmung und ein paar hundert Zeilen Python als Brücke. Das Hardware-Stück gibt es ab ca. 100 USD, das Vision-Modell als API oder lokal, das Python sowieso.

Schwierig sind die Mappings zwischen den Schichten — Bild auf Koordinaten, Zeichen auf Tasten, Aktion auf Sequenz. Vier der sechs Stolperstellen oben drehen sich darum. Die KI-Anbindung selbst ist der einfache Teil.

 

Code und Werkzeuge

Standards und Spezifikationen

JetKVM-Architektur (Hintergrund)

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert