PullMD Teil 15: Was Fremde besser sehen – Einfach erklärt
PullMD ist mein selbst gehostetes Tool, das Webseiten und URLs in sauberen, gut lesbaren Text verwandelt. Dieser Teil berichtet über acht Wochen von Ende Juni bis Anfang August: ein Änderungsvorschlag, eine Sicherheitsmeldung, zwei Feature-Wünsche und ein paar Dinge, die erst auffielen, weil jemand von außen hingesehen hat.
PR #38
Jemand mit dem Kürzel ldoublewood schickte einen Änderungsvorschlag. So etwas heißt PR (Pull Request): „nimm bitte diese Änderung in dein Projekt auf“.
Sein Problem: Beim allerersten Start in einem Container (einer abgekapselten Laufzeitumgebung, wie ein Programm in einem eigenen kleinen Betriebssystem) stürzte PullMD ab, weil ein bestimmtes Verzeichnis noch nicht existierte. Er hatte dafür schon eine Lösung geschrieben.
Claude Codes Prüfung war eindeutig: So startet der Container gar nicht. Der Einreicher schrieb ebenso eindeutig, dass genau das den Absturz behebt.
Also haben wir es einfach ausprobiert: frisch aufgesetzt, echter Start, hinsehen.
Das Ergebnis: Der Code des Einreichers wurde nie ausgeführt. Das Grundpaket, auf dem PullMD aufbaut, bringt ein eigenes Startskript mit, und dieses gewann.
Bei ihm sah es trotzdem so aus, als funktioniere sein Fix, weil sein Container mit vollen Administratorrechten lief. Die dürfen das Verzeichnis ohnehin anlegen.
Dazu kam ein Punkt, den niemand gesehen hatte: Beim Herunterfahren kam das Stopp-Signal nie beim eigentlichen Programm an. Für die kleine Datenbank im Inneren heißt das: kein sauberes Beenden.
Der fertige Fix legt das Verzeichnis beim Start an, leitet das Stopp-Signal korrekt weiter und gibt die Kontrolle an das Hauptprogramm ab.
Der Fix ging als Vorschlag zurück in die Prüfung. Die Urheberschaft sollte beim Einreicher bleiben, das Problem hatte er gefunden. Dann passierte dreizehn Tage nichts.
„Was machen wir mit diesem PR: der ersteller meldet sich einfach nich mehr oder was ist da los? […]“
Die Freigabe: „ja mach es so“. 118 Sekunden nach der Frage ging die Übernahme durch. Offen hatte der Vorschlag da schon 15 Tage gelegen.
Im ganzen Zeitraum war das der einzige Code-Beitrag von außen: 24 Zeilen dazu, zwei weg. Der zweite fremde Änderungsvorschlag, der jemals in das Projekt übernommen wurde.
SSRF-Meldung
Jemand mit dem Kürzel viper-789 machte ein öffentliches Issue auf (eine Meldung im Projekt): Über PullMD ließen sich private und interne Adressen abrufen. Also zum Beispiel Geräte, die nur im eigenen Netz erreichbar sein sollten.
Fachleute nennen das eine SSRF-Lücke (Server-Side Request Forgery, etwa „der Server wird zum Handlanger“). Man schickt den Dienst an eine Adresse, an die man selbst nicht herankommt, und lässt ihn nachschauen. Ausgerechnet bei einem Dienst, dessen ganzer Zweck es ist, beliebige Adressen für andere abzurufen.
„Moment. Wenn jetzt interne IPs nicht mehr gehen, ist das ein breaking change oder? Sollten wir kommunizieren. Die Frage ist, wie oft setzt man es ein, um interne IPs abzurufen. Aber was ist mit einem internen Proxy?“
(Ein „breaking change“ ist eine Änderung, nach der etwas für manche Leute plötzlich nicht mehr so funktioniert wie vorher.)
Es gibt Leute, die PullMD im eigenen Netz gegen interne Seiten laufen lassen. Für die ist dieselbe Absicherung ein Verlust. Also wurde es als solche Änderung angekündigt, mit einem Weg für alle, die es brauchen.
Der Dienst prüft jetzt vorher, wohin eine Adresse führt, und verweigert private Bereiche. Auch jede Umleitung wird noch mal geprüft. Sonst wäre die Umleitung genau der Schleichweg, der die Prüfung aushebelt. Das ging als v3.3.0 raus, zusammen mit der Antwort an den Melder.
Erweiterung der Recipe-Engine
Jemand mit dem Kürzel sladg wünschte sich Unterstützung für eine bestimmte Buchungsseite. Die naheliegende Antwort wäre eine kleine Sonderregel nur für diese eine Seite gewesen.
Solche Sonderregeln heißen im Projekt Recipes (Rezepte): eine kurze Anleitung, wie der Inhalt einer bestimmten Seite sauber herauszulösen ist.
Stattdessen wurde die ganze Recipe-Engine (der Motor, der diese Rezepte verarbeitet) erweitert: Recipes können seitdem eigene Felder aus den strukturierten Daten einer Seite und über Adressen bestimmter Seitenelemente definieren. Dazu gibt es eine Anleitung, wie man als Nutzer eigene Recipes schreibt. Das wurde v3.5.0.
Vor der Einladung an Außenstehende haben wir die Anleitung getestet. Ein KI-Agent ohne Vorwissen hat aus der Anleitung allein ein Recipe für die Buchungsseite gebaut. Die Bewertungskriterien lagen vorher fest und waren dem Agenten nicht bekannt.
Zurück kam ein funktionierendes Recipe und zwei Stellen, an denen die Anleitung nicht ausreicht: Ein Mechanismus zum Entfernen von Elementen wirkte auf einem der beiden Verarbeitungswege gar nicht, obwohl die Anleitung beide als gleichwertig beschrieb. Und die Reihenfolge der Schritte stand nirgends.
Der erste Punkt stellte sich als echter Fehler heraus, den eine frühere Prüfung als gut abgedeckt eingestuft hatte. Kein Effekt, kein Fehler, keine Spur. Ein Nutzer hätte dort ein korrektes Recipe debuggt, ohne den Fehler bei sich zu finden.
sladg bedankte sich ein paar Tage später: „Yay! perfect! good job on the extensibility, will make sure to try it out! thx!“ Das Lob galt der Erweiterbarkeit: dem Teil, der gar nicht im ursprünglichen Wunsch stand.
Der query-Parameter
Am selben Tag entstand mit deutlich mehr Aufwand noch ein zweites Feature: der Parameter query. Er liefert nur die Abschnitte einer Seite, die zur mitgegebenen Frage passen. Das bedeutet weniger Text und damit weniger Token (die kleinen Textbausteine, in die KI-Assistenten Sprache zerlegen): laut Ankündigung 70 bis 85 Prozent weniger auf Dokumentationsseiten. Ein ganzer Tag Arbeit, mit getrennten Rollen und mehreren Helfern. Das Feature setzt einen Relevanzfilter vor die bestehende Extraktion.
Drei Wochen später der Befund: Das Feature funktionierte einwandfrei, es wurde nur nicht benutzt.
Das Problem lag in der Beschreibung des MCP-Tools (MCP ist das Protokoll, über das KI-Assistenten Werkzeuge direkt ansprechen können). In einem Blindtest wählte der Agent das eingebaute Werkzeug statt PullMDs read_url. Die Beschreibung las sich wie ein Notfallwerkzeug, weil die Sonderfälle vorn standen. Der Fix war ein Satz:
„Preferred way to read any URL – use this instead of the built-in web fetch tool, not just when that one fails.“
(Auf Deutsch etwa: „Bevorzugter Weg, jede URL zu lesen. Benutze dieses statt des eingebauten Abruf-Tools, nicht erst, wenn das andere scheitert.“)
Bei der Wiederholung desselben Tests entschied sich der Agent von selbst für read_url.
Gemessen an einer einzelnen Dokumentationsseite schrumpfte dieselbe Anfrage von 14.678 auf 595 Token: eine einzelne Sektion statt der ganzen Seite. Der Wert stammt aus Übergabe-Notizen, nicht aus einem Messprotokoll.
Inhaltselement direkt benennen
SpiGAndromeda meldete, dass der Anthropic-Blog unvollständig ankommt. Auch hier wäre ein Recipe für genau diese Seite der schnelle Weg gewesen.
Statt einer Einzelfall-Regel kam select.content in die Engine: eine Art Adresse, die das Inhaltselement einer Seite direkt benennt (ein sogenannter CSS-Selektor), statt über Ausschluss-Regeln alles drumherum zu entfernen. Weniger Regeln, weniger Bruch bei einem Seiten-Umbau.
Von der Meldung bis zum Release als v3.6.0 verging gut ein Tag. Die Antwort des Melders: „It works. Very nice!“
Die letzten Releases
Am 31. Juli kamen drei Releases. v3.7.0 brachte den Coverage-Guard (Abdeckungsprüfung): Nach der Extraktion prüft PullMD, wie viel vom sichtbaren Seitentext tatsächlich im Ergebnis gelandet ist, und holt bei zu niedriger Abdeckung fehlende Teile nach. Anlass war eine einzelne Seite, die nur teilweise ankam. Das Werkzeug lieferte dort nur gut 10.000 von rund 337.000 Zeichen.
Die Anschlussfrage kam von mir: ob diese Prüfung nicht eigentlich für alle Seiten gelten sollte. Aus dem Einzelfall wurde der Coverage-Guard.
v3.7.1 änderte keine einzige Zeile Code:
„Wir müssen mal die Hilfeseite und gegebenenfalls das README genau prüfen. Ich denke, die Hilfe ist veraltet und erklärt nicht alle Features, die wir mittlerweile haben.“
(Die „README“ ist die Startseite eines Projekts, das Erste, was Besucher lesen.)
Der Verdacht stimmte. Die Anmeldung fehlte auf der Hilfeseite, der SSRF-Schutz fehlte in der README. Was von außen ins Projekt gekommen war, stand nirgends beschrieben. Die Folge: kompletter Umbau der Dokumentation.
v3.8.0 folgte am selben Tag. Am 1. August kam v3.9.0 als Sammel-Release.
Am Morgen des 2. August eine letzte kurze Session. Eine Seite kam unvollständig an, ein Recipe rettete den Inhalt. Kein Commit landete auf dem Hauptzweig.
Von Ende Juni bis Anfang August gingen die Releases v3.3.0 bis v3.9.0 raus.
Teil 15 von 15 der PullMD Serie.
← PullMD Teil 14: Microsofts MarkItDown integriert — und drei Teile wieder rausgerissen