leanos/tasks/lessons.md
Thomas Lutz Kolter ee203fd9fb leanOS: initial import (basiert auf NEON NOODLE OS)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-09-15 14:58:54 +02:00

58 lines
3.7 KiB
Markdown

# Lessons
## Runtime-kompilierte React-Komponenten: Identität stabil halten
**Kontext:** Generierte Apps werden via `new Function(...)` zu einer `App`-Komponente
kompiliert und gerendert.
**Fehler:** Die `App`-Funktion wurde im Render-Body des Wrapper-Components per
`factory(...)` neu erzeugt. Jeder Render → neue Funktionsidentität → React unmountet/
remountet den Teilbaum → `useState` resettet, `useEffect` feuert erneut → Endlos-
Schleife ("Maximum update depth exceeded") bei jeder App mit State.
**Regel:** Dynamisch erzeugte Komponenten **einmal** memoisieren
(`useMemo(() => factory(...), [stableDeps])`), nie pro Render neu bauen. Galt hier
für die Sandbox-Runtime; gilt generell für jedes `React.createElement` mit einer
zur Laufzeit konstruierten Komponente.
## Verifikation: innerText enthält keine Placeholder
Beim Headless-Test gegen Spotlight schlug die Assertion fehl, weil sie auf den
`placeholder`-Text eines `<input>` prüfte — `document.body.innerText` liefert
Placeholder/Attribut-Text nicht. Für UI-Smoke-Tests auf echte Text-Knoten oder
Selektoren (`querySelector('input[placeholder*="…"]')`) prüfen, nicht auf innerText.
## Anthropic-kompatible Provider (z.ai): URL-Pfad + CORS beim Streaming
Zwei aufeinanderfolgende Stolpersteine beim Umstellen auf z.ai/GLM:
1. **URL ohne Pfad:** `VITE_ANTHROPIC_API_URL=https://api.z.ai/api/anthropic` ist nur die
Basis-URL → Request landete auf `…/anthropic` statt `…/anthropic/v1/messages`
(z.ai: `{"code":500,"msg":"404 NOT_FOUND"}`). Fix: `resolveApiUrl()` hängt
`/v1/messages` an, wenn nicht vorhanden (Basis- oder Voll-URL beide ok).
2. **Doppelte CORS-Header beim Streaming:** Die `stream:true`-Antwort von z.ai enthält
ZWEI `access-control-allow-origin`-Header (`http://localhost:5173` UND `*`) plus
`allow-credentials: true`. Das ist laut CORS-Spec ungültig → Browser wirft
"TypeError: Failed to fetch". Die Nicht-Stream-Antwort hat nur einen Header,
deshalb ging App-Generierung, aber der Assistant nicht.
**Fix:** Vite-Dev-Proxy (`server.proxy["/__ai"]` → Upstream-Origin+Pfad aus env,
`changeOrigin:true`). Browser ruft same-origin `/__ai/v1/messages` → kein CORS,
Streaming bleibt erhalten. Client nutzt im DEV den Proxy, im Prod-Build die
absolute URL. Merke: curl testet CORS NICHT — Browser-Reproduktion ist Pflicht.
## X-Frame-Options / CSP beim Einbetten externer Seiten (in-OS Browser)
`X-Frame-Options` und CSP `frame-ancestors` werden vom **Browser** anhand der
**Ziel-Response** erzwungen — client-seitig NICHT umgehbar (kein iframe-Trick, kein
Header-Override). Einzige saubere Lösung: **Server-Proxy**, der die Seite serverseitig
holt (dort sind die Header wirkungslos) und sie ohne Framing-Header zurückgibt.
Umsetzung hier: Vite-Dev-Middleware `/__proxy?url=…` → fetch upstream, droppt
`x-frame-options`/`content-security-policy`/`content-encoding`/`content-length`,
injiziert `<base href>` (relative Assets) + ein Klick-Interceptor-Script (Navigation
bleibt im Proxy). SDK-Helfer `os.net.proxyUrl(url)` + Generierungs-Prompt weisen
Browser-Apps an, NIE eine rohe URL in den iframe zu setzen. `<iframe>` im
Forbidden-Guard freigeben. End-to-end verifiziert: en.wikipedia/wiki/Cat lädt im
iframe (innerTitle "Cat - Wikipedia"). Prod-Build hat keinen Proxy → Fallback auf
rohe URL (dann wieder geblockt); für Prod bräuchte es einen echten Backend-Proxy.
## Verifikation lohnt sich auch ohne externen Key
Die KI-App-Generierung braucht einen API-Key. Statt "kann ich nicht testen" wurde der
**konsumierende** Runtime (Babel→Sandbox→Render→Error-Boundary→Forbidden-Guard) mit
synthetischem KI-Output headless verifiziert. Das fand den Remount-Bug, den der
echte API-Pfad sonst erst beim Nutzer gezeigt hätte.