Multi-Hoster-Upload/tasks/lessons.md
Administrator 20b53f9587 docs(lessons): security-E2E must drive every collector default path, not just the aggregate hub
Captures the two redaction leaks from the remote-diagnostics build: a hoster-
returned opaque token surviving because value-scrub only covers stored config
secrets, and the get_config_redacted/get_queue_state default paths skipping
pattern-scrub entirely. The discriminator the first E2E missed, plus the rule:
exercise each collector with its DEFAULT args and seed fixtures with a NON-config
secret so you test pattern-scrub, not value-scrub by accident.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-19 17:49:14 +02:00

24 KiB
Raw Permalink Blame History

Lessons

2026-04-21 — DOM-Doppelrender bei Bulk-State-Changes

Symptom: User klickt auf "Erneut versuchen" mit 500+ Jobs → App hängt sekundenlang. Root cause: retrySelectedJobs() ruft renderQueueTable + updateQueueActionButtons + updateStatusBar auf, startSelectedUpload() ruft direkt danach genau dieselben Funktionen nochmal auf. Regel: Wenn ein Click-Handler await anotherHandler() aufruft und der innere Handler seinen eigenen kompletten Render-Zyklus hat, NIEMALS noch einen davor. Einmal ist genug — der folgende innere Render sieht die frischen State-Mutationen ohnehin. Wie anwenden: Vor jeder await fn()-Folge in einem Handler prüfen: macht fn schon renderQueueTable()? Wenn ja, äußere Render-Calls löschen.

2026-04-21 — State-Checks MÜSSEN hinter die Semaphore-Queue

Symptom: Pre-Job-Swap prüfte _failedAccounts vor semaphore.acquire. Bei N parallelen Workers war der Check zum Start für ALLE leer — niemand hat geswapt. Erst nachdem alle im Semaphore ordentlich gewartet hatten und einer fehlschlug, wurde _failedAccounts befüllt, aber die anderen hatten ihren Check längst hinter sich. Regel: State-basierte Entscheidungen (failed accounts, overrides, cached stats) gehören direkt vor die Aktion die sie betreffen — nach jeder async await die die Position in der Queue bestimmt. Nicht am Task-Start für später wichtigen State abfragen. Wie anwenden: Bei Queue-basierten Pipelines prüfen: "Was kann sich zwischen Task-Start und dem tatsächlichen Execute ändern?" Alles was sich ändern kann, muss direkt vor dem Execute geprüft werden, nicht davor.

2026-04-21 — Reaktive Config-Updates für laufende State-Maschinen

Symptom: User fügt mid-batch einen neuen Account hinzu, aber der UploadManager merkt nicht dass die Config sich geändert hat. account-failed Event feuert nur einmal pro Account → keine zweite Re-Resolve-Chance. Regel: Wenn ein State nur bei Events neu evaluiert wird und Events "nur einmal" feuern, muss jede externe Zustandsänderung (Config-Save, User-Action) den State explizit triggern. Wie anwenden: Save-Handler müssen aktive State-Maschinen informieren. Lieber einen überflüssigen Re-Resolve-Call als einen verpassten. Für Upload-Manager: nach saveConfig → re-evaluate failed accounts ohne Override.

2026-04-21 — Error-Klassifikation: fileRejected vs accountError

Symptom: Voller Byse-Account wurde nicht rotiert — skip-rotation-file-rejected geloggt für jede Datei. Root cause: Generisches Match auf Prefix-String ("lehnte Datei ab") klassifizierte ALLE Byse-Errors als file-level, inklusive Account-voll-Meldungen. Regel: Hoster-Parser setzen den spezifischen Flag (fileRejected ODER accountError), nicht beide nie. Classifier matcht konkrete Phrasen (Duplicate, Not video format, …), niemals generische Wrapper-Strings die für mehrere Fehlerarten benutzt werden. Wie anwenden:

  • Bei neuen Hostern: per-status-Klassifikation bereits im Parser, nicht erst im Upload-Manager.
  • Classifier-Regexes auf Rejection-Kernphrasen, nicht auf UI-Prefix.
  • Defensive: accountError === true gewinnt immer gegen fileRejected — Account-Rotation ist weniger schlimm als endlose Fails auf einem toten Account.

2026-04-21 — Keine fake Build-ETAs

Symptom: User wartet 5+ min auf Tauri-Build den ich mit "1-2min" angekündigt habe. Regel: Tauri-Release-Builds brauchen real 3-6 min (Rust + NSIS + MSI). Keine Zeitangabe oder ehrlich "kann 3-6min dauern" schreiben. Wie anwenden: Wenn User nach Status fragt: sofort tail des Logs + ls des Bundle-Ordners zitieren, nicht raten.

2026-05-24 — Packaged-Electron Log-Pfade: nie __dirname/.. zum Schreiben

Symptom: doodstream-debug.log hatte auf dem Server null aktuelle Einträge; nur alte Dev-Logs. Fehler "kein Filecode" war nicht diagnostizierbar. Root cause: path.join(__dirname, '..', 'x.log') zeigt im gepackten Build in resources/app.asar (read-only). fs.appendFileSync wirft EACCES, der try/catch schluckt es → null Production-Logs. Regel: Schreibbare Pfade IMMER über app.getPath('userData') (lazy require('electron'), Fallback __dirname/.. nur für Tests/plain-node). Gilt für jede Datei die der gepackte App schreibt. Wie anwenden: Bei jedem neuen Log/Cache/State-File prüfen: wohin schreibt das im NSIS-Build? Nicht ins Install-Verzeichnis, nicht in asar.

2026-05-24 — Hoster-Fehler: echten Status surfacen, nicht generisch schlucken

Symptom: "upload_result Seite hat keinen filecode ()" — nichtssagend; User dachte doodstream-Format geändert. Root cause: XFileSharing liefert den echten Grund im st-Feld (Error: duplicate / file too big / …). Code ignorierte st komplett und warf nur den leeren Body. Regel: Bei Hoster-Parsefehlern immer die Server-Statusfelder (st/msg/code) + Kontext (welcher CDN-Node, war filecode da) in die Fehlermeldung packen. Format-Struktur unverändert + leerer Inhalt = Backend-Ablehnung, kein Parsing-Bug.

2026-05-25 — Queue leer nach Update: Auto-Dedup zu aggressiv (nicht Save/Restore)

Symptom: Queue gestoppt, App-Update -> nach Neustart Queue leer ("Dateien hierhin ziehen"). User dachte Save/Restore kaputt. Root cause: Queue WIRD korrekt gespeichert (pendingQueue) + restored. ABER _autoDeduplicateFromLog (läuft bei init nach restore) entfernte Jobs per fileName|hoster-Match gegen das GESAMTE Lifetime-fileuploader.log — UNABHÄNGIG vom Status. Pending 'preview'-Jobs, deren Datei früher mal hochgeladen wurde, flogen alle raus -> komplette Queue weg. "Update-spezifisch" nur weil der Server-App nur beim Update neustartet (normaler Restart hätte dasselbe getan). Verifiziert: Reale electron-config.json: 4 preview-Jobs, alle 4 Keys im Log -> alte Logik entfernt 4/4. Neue Logik (nur status==='done' droppen) entfernt 0/4. Regel: Auto-Cleanup/Dedup darf NIE pending/actionable User-Arbeit löschen. Nur genuin abgeschlossene ('done') Jobs decluttern. Lifetime-Logs sind Historie, nicht Session-Fortschritt — nicht als "schon erledigt"-Quelle für pending Jobs missbrauchen. Wie anwenden: Bei jeder Filter/Remove-Logik auf User-State: nach Status gaten, nicht nur nach Identitäts-Match gegen historische Daten.

2026-05-28 — Doodstream "kein Filecode": Web-Scraping ist die falsche Ebene, API ist der Fix

Symptom: Wiederkehrend "kein Filecode — Server gab leeren Link zurueck" bei großen Dateien (~1GB/7min Upload), trotz 3.3.26-3.3.29. Queue voll roter Fehler. Root cause (recherchiert + verifiziert): Der Web-Upload holt den Filecode aus einem XFileSharing-HTML-Formular. Bei langen Uploads kommt das Formular leer zurück, weil (a) der per-Seitenaufruf sess_id-Token über den 7min-Upload altert UND (b) der server-seitige File-Registration-Callback (cgi-bin/fs.cgi-Äquivalent) unter Last timeoutet → kein file_code gemintet. Wichtig: Das ist KEIN async-delay — die Datei taucht NICHT später in der Liste auf (die Registrierung, die sie listen würde, ist genau das was failt). File-list-Polling (wie Byse) hilft hier also kaum. Fix: Die offizielle doodapi.co JSON-API nutzen, wenn ein API-Key da ist — sie liefert result[0].filecode DIREKT in JSON (kein HTML-Formular) und nutzt einen persistenten api_key (kein alternder sess_id). Git-Historie: die API war der ORIGINAL-Pfad (initial commit); Web-Login kam später nur "als Alternative zum API-Key" — Key-Bevorzugung stellt also den gedachten Primärpfad wieder her, kämpft nicht gegen eine bewusste Entscheidung. Regel: Bei Hoster-Integrationen die offizielle API der Web-Scraping-Ebene vorziehen wo möglich. Empty-form/codeless-2xx = Hoster-Backend-Flake (hosterTransient), Account NICHT als tot markieren — auf BEIDEN Pfaden (Web + API) gleich klassifizieren. Voraussetzung: Engagiert nur wenn der Doodstream-Account einen gültigen API-Key hat (doodstream.com/settings). Keyless-Accounts bleiben beim Web-Pfad.

2026-05-28 — Doodstream empty-form: live diagnosis confirmed API path is the fix

Verifiziert mit echtem Account-Key (read-only API-Calls):

  • account/info → status 200, Key gültig, Storage unlimited. Premium ABGELAUFEN (2025-10-03) — Uploads gehen TROTZDEM.
  • upload/server → liefert gültigen Node (cv1130ed.cloudatacdn.com) auch ohne Premium → API-Upload-Pfad nutzbar.
  • file/list → 90.548 Dateien; Uploads landen server-seitig INTERMITTIEREND (viele Burn-Notice-Folgen genau im "Fehler"-Zeitfenster vorhanden). Das leere Formular ist also nicht "immer kaputt", sondern manchmal — der Web-Form-Registrierungs-Callback (fs-public.intconnect.net) timeoutet sporadisch. Konsequenz: API-Weg (result[0].filecode inline) umgeht den failenden Callback → richtiger Fix. file/list-Recovery ist NICHT tote Last (Dateien erscheinen ja) — aber bei 90k-Accounts MUSS man sort=created&order=desc erzwingen, sonst ist die frische Datei nicht auf Seite 1. Regel: Bei "geht manchmal/manchmal nicht" + Hoster mit offizieller API: erst per read-only API-Call (account/info, file/list) gegen den ECHTEN Account verifizieren statt am Client weiterzuraten. Das beendet Spekulations-Schleifen.

2026-06-17 — Rotation-State pro-Call neu zu bauen = stiller No-Op beim Drip-Feed

Symptom: Account-Rotation (v3.3.74) verteilte korrekt bei einem Drag-Drop-Batch (N Dateien auf einmal), aber bei Folder-Monitor / Einzeldatei-Uploads landete IMMER alles auf Account 1 — die Sekundär-Accounts bekamen nichts. Root cause: Der Picker wurde INNERHALB jedes buildUploadTasks/buildUploadTasksFromJobs-Calls frisch erzeugt → rotIdx startete jedes Mal bei 0. Der Folder-Monitor speist neue Dateien einzeln via add-jobs-to-batch in einen laufenden Batch (und per-Detection-autoStart feuert start-upload pro Datei). Jeder dieser Calls baute den Picker neu von Index 0 → 0 % len = 0 = Account 1, immer. Verifiziert: Unit-Tests gaben falsche Sicherheit — alle 10 nutzten EINEN Picker über N Calls (testet nur Intra-Batch). Der Bug lebte genau im Cross-Call-Verhalten, das kein Test abdeckte. Fix: Rotation-Cursor lebt AUSSERHALB des Pickers: modul-level _rotationCursors in main.js als authoritative Quelle (einmal aus config geseedet), config.rotationCursors als Restart-Survival-Backing (das 30-Tage-Quota überlebt App-Neustarts). Picker bekommt { indices }-Seed + exponiert indices()/dirty(). Persist nur wenn dirty(). Regel: Rotations-/Round-Robin-/Cursor-State, der über mehrere Aufruf-Eintrittspunkte fair verteilen soll, MUSS die Aufrufe überdauern — und wenn das Ziel ein Zeitfenster über Sessions hinweg ist (Quota), auch Restarts. Picker/Iterator pro Call neu instanziieren ist nur korrekt, wenn jeder Call die GESAMTE zu verteilende Menge sieht. Wie anwenden: Bei jeder "verteile reihum"-Funktion fragen: über wie viele Eintrittspunkte/Calls läuft die zu verteilende Menge real? (Hier: drag-drop=1 Call ABER folder-monitor=1 Call pro Datei.) Tests MÜSSEN den Cross-Call/Drip-Feed-Pfad mit frisch geseedetem State nachstellen, nicht nur einen langlebigen Picker.

2026-06-19 — Transiente Infra-Fehler (5xx/ECONNRESET) NIE als Account-Fehler klassifizieren

Symptom: User: "byse bissl unstabiler als sonst", Queue voll "Upload-Antwort von byse.sx war kein JSON (HTTP 502): <!doctype html>" und "read ECONNRESET". EIN byse-Gateway-Aussetzer kaskadierte durch ALLE Failover-Accounts (Primär→Fallback #3, jeder kriegt denselben 502) und blacklistete sie für den Batch. Root cause: Die Upload-POST-Throw-Sites (hosters.js) warfen PLAIN Errors OHNE Klassifikations-Flag. Ein 502 matchte keinen der Classifier (_isTransientNetworkError hatte nur errno-Regexes ENOTFOUND/ECONNRESET/..., KEIN HTTP-5xx-Pattern; _shouldSkipRetryOnAccountError nur /\b(401|403|429)\b/, kein 5xx) → GENERISCH → while(task.accountId) → mark-failed + emit('account-failed') → Failover-Kaskade + Blacklist. (Der Server-Lookup-Pfad war schon geschützt via hosterTransient — nur der POST-Pfad nicht.) Wichtig — Diagnose-Disziplin: "Hat byse die API geändert?" → NEIN, per Live-Probe verifiziert (GET /upload/server liefert dokumentiertes JSON; Upload-Contract unverändert). 502/HTML + ECONNRESET = Gateway-Infra, kein Contract-Bruch. Bei "manchmal kaputt" + offizielle API: Fehler-BODY anschauen (502-HTML ≠ API-Änderung) statt raten. Fix: Expliziter err.transientNetwork-Flag am Throw-Site gesetzt wenn statusCode >= 500 (bzw. payload.status===500); 401/403/429 bleiben PLAIN (Account-Fehler). Flag wird in _isTransientNetworkError ZUERST geprüft (vor dem !err.message-Guard) und macht _isFileRejectedError/_shouldSkipRetryOnAccountError autoritativ false (gegen Keyword-False-Positives im HTML-Snippet). transientNetwork (NICHT hosterTransient) = retry SAME account, dann clean fail vor der Rotation-Schleife → keine Kaskade, kein Blacklist. Server-Lookup-Pfad symmetrisch gehärtet (apiGet taggt 5xx, getUploadServer reicht den Flag auf den gewrappten Error durch). Regel: Hoster-Infra-Fehler (HTTP 5xx/Bad Gateway/Service Unavailable, Connection-Reset/Timeout) sind NIE Account-Faults — alle Accounts treffen dasselbe Gateway, Failover ist sinnlos, Blacklist schädlich. Fail open: retry same account, dann clean fail. Klassifikation per explizitem Flag am Throw-Site (autoritativ), Message-Regex nur als Defensive-Fallback. Throw-Site-Tagging IMMER end-to-end testen (uploadFile mit gestubbtem 5xx-Response treiben), nicht nur den Flag im Mock injizieren. Nebenbefund: byse-Recovery-Poll war tot — _fetchByseFileList baute api.byse.sx/api/file/list (das /api/-Präfix gilt NUR für doodstreams doodapi.co-Host; byse hat schon die api.-Subdomain). Live: /api/file/list→302 auf Docs, /file/list→200 JSON. Copy-paste vom doodstream-Pattern. Fix: Präfix raus.

2026-06-19 — DNS-Auflösung + Cert ≠ legitime Domain (Parking-Falle); kaputte Hoster-Daten nicht raten, sondern verwerfen

Symptom: byse-Uploads scheiterten an getaddrinfo ENOTFOUND s1065.filemoon — der von byses /upload/server gelieferte Host hatte KEINE TLD (sNNNN.filemoon), unauflösbar. Intermittierend (manche Uploads klappten = vollständiger Host). Beinahe-Fehler: Ich wollte fast .art anhängen — s1065.filemoon.art löste auf UND hatte ein dediziertes *.filemoon.art-Cert. ABER: PTR der IP = k8s-svc-lander-...parklogic.net = DOMAIN-PARKING. filemoon.nl (auch auflösend) = Shared-Catch-all-Cert mit Müll-SAN (kaobei.cc/babesex.xyz). BEIDE geparkt/gesquattet, nicht byses echte Server. Uploads dahin = auf Parking-Page oder zu Fremden. Regel: „Löst auf + hat HTTPS-Cert" beweist NICHT, dass eine Domain der echte Service ist. Vor dem Behandeln einer Domain als legitim: PTR/Reverse-DNS (parklogic/lander/sedoparking = geparkt) UND Cert-SAN (riesige unzusammenhängende SAN-Liste = Shared-Parking-Cert) prüfen. Bei Hoster-Migrationen besetzen Squatter alte/neue Marken-TLDs. Regel (kaputte Hoster-Daten): Wenn ein Hoster strukturell kaputte Daten liefert (Host ohne TLD), den korrekten Wert NICHT raten/halluzinieren (würde der User-Regel „niemals halluzinieren, immer verifizieren" widersprechen). Stattdessen das offensichtlich Kaputte als ungültig VERWERFEN und die bestehende Retry-/Cache-Maschinerie nach einem gültigen Wert fragen lassen (hier: normalizeAbsoluteUrl returnt null bei /(^|\.)filemoon$/ → getUploadServer retried + LAST_UPLOAD_SERVERS-Cache). Fail-open bleibt: alles kaputt → clean fail, kein Cascade.

2026-06-19 — Dedup-/Skip-Key ändern: ALLE Re-Queue-Pfade prüfen (mutate-in-place vs. preview-rebuild)

Kontext: Der _completedUploadKeys-Guard gated buildQueuePreview() — ein Key dort heißt "erzeuge KEINE Preview für file|hoster". Beim Fix für die Ghost-Wiederkehr (Key über Auto-Remove behalten + über Restart persistieren) bestand die echte Gefahr, dass ein persistierter Key einen absichtlichen Re-Upload still verschluckt. Beinahe-Fehler (Advisor gefangen): Ich hatte den Clear nur im Modal-Add-Pfad (applyHosterSelection, pendingPaths.size>0). Frage: hängt IRGENDEIN Re-Upload-/Retry-Pfad an buildQueuePreview? Wenn ja → persistierter Key = stiller No-Op nach Restart, für den User unsichtbar bis er drauf stößt (er kann nicht testen). Verifikation: retrySelectedJobs (reuploadBtn) und _retryFailedFromBuckets (Retry-Failed) mutieren BEIDE den bestehenden Job in-place (j.status='queued'/'preview') und starten direkt — sie konsultieren _completedUploadKeys NIE. Also sicher. Nur das Hinzufügen einer frischen Datei läuft über buildQueuePreview, und genau dort clears applyHosterSelection. Regel: Bevor du einen Key/Flag änderst, der eine Rebuild-Funktion gated, JEDEN Pfad lesen, der einen erledigten Job zurück in den Upload bringt. Mutate-in-place-Pfade sind immun; nur Rebuild-via-Preview-Pfade brauchen einen Key-Clear. Nicht auf Symptome schließen — die Handler lesen. Wie anwenden: Skip-Guard-Change → grep alle Caller der gegateten Funktion + alle „erneut/retry/reupload"-Handler; pro Handler entscheiden: mutiert er Jobs oder baut er neu? Persistierte Skip-Keys NUR bei explizitem User-Re-Add clearen, nicht bei In-Place-Retry.

2026-06-19 — „Alle Re-Add-Pfade" heißt WIRKLICH alle: es gab DREI, nicht zwei (v3.3.83)

Symptom: Nach v3.3.82 („retry mutiert in-place, ist immun") fand ein 2. Hunt: retrySelectedJobs von einem DONE-Job re-added den Pfad zu selectedFiles UND behält den completed-Key → nach Restart blockt buildQueuePreview die Wiederherstellung → absichtlicher Re-Upload still verloren. Plus: der Folder-Monitor-Pre-Selected-Branch ist ein VIERTER Add-Pfad, der applyHosterSelection komplett umgeht → Key-Clear lief nie. Root cause meiner Fehlannahme: In v3.3.82 hatte ich „retry mutiert in-place → immun" geschlossen — korrekt für den GHOST (Doppel-Preview), aber NICHT für den retry-of-DONE-Fall, wo der Job einen LIVE completed-Key trägt (Key wird nur auf 'done' gesetzt). Ich hatte 2 von 4 Re-Add-Pfaden gefunden (applyHosterSelection, manueller Delete), die anderen 2 (retry, folder-monitor) übersehen. Regel: Wenn ein Dedup-/Skip-Key existiert, exhaustiv ALLE Stellen enumerieren, die (a) den Key SETZEN und (b) einen Pfad zurück in selectedFiles bringen. Pro Re-Add-Pfad einzeln prüfen, ob er den Key clear. „In-place vs rebuild" ist NICHT die einzige Achse — auch ein in-place-Reset kann einen stale persistierten Key hinterlassen, der ERST nach Restart beißt. Wie anwenden: grep den Key-Namen → jede .add-Stelle und jede selectedFiles.push/selectedFiles =-Stelle auflisten → Kreuztabelle Add-Pfad × cleart-Key. Lücke = Bug. Clear-Logik in EINEN Helper (clearDedupKeysForPaths) ziehen und an JEDEM Re-Add-Pfad aufrufen, statt pro Pfad zu duplizieren (sonst wird der nächste Pfad wieder vergessen).

2026-06-19 — Per-Cell-Delete braucht eine eigene persistierte Suppression, nicht Überladung des completed-Keys (v3.3.83)

Symptom: Multi-Hoster-Datei F (Zeilen F|A, F|B); User löscht nur F|A. buildQueuePreview baut F|A beim nächsten Rebuild (neue Datei adden / Folder-Drop) wieder auf → F wird doch zu A hochgeladen (Quota verbrannt, ungewollter Link). _deletedJobIds half nicht (alte Job-ID, neue Preview-ID). Designentscheidung: Separates _suppressedPreviewKeys-Set statt _completedUploadKeys zu überladen — completed-Key hat Re-Upload-nach-Delete-Semantik (wird beim Delete GECLEART), suppression hat die GEGENTEILIGE (wird beim Delete GESETZT). Überladen hätte „re-upload nach delete" gebrochen. Korrektheits-Invarianten (Advisor-verifiziert): (1) Suppression NUR setzen wenn die Datei nach syncSelectedFilesFromQueue noch in selectedFiles ist (sonst sinnlos). (2) status==='done' AUSSCHLIESSEN (done hat eigene completed-Key-Semantik; nicht ungefragt das re-upload-after-delete-Feature ändern). (3) Re-Add-Clear ist PFAD-basiert (alle Hoster), retry-Clear ist KEY-basiert (exakter file|hoster) — pfad-basiertes Clear bei retry würde den Ghost eines Geschwister-Hosters auferstehen lassen. (4) Persistenz filtert auf selectedFileMap (wie completedKeys) → überlebt Restart solange Geschwister-Job existiert, wird inert wenn Datei selectedFiles verlässt. Verifikations-Ehrlichkeit: Renderer-Code ist nicht node:test-bar. Im Report „verifiziert per Logik + Regressions-Suite (362 grün) + identischer Smoke-Boot", NICHT „getestet" schreiben. Triviale Helper NICHT nur fürs Testen in ein lib extrahieren (schlechtes Risk/Reward).

2026-06-19 — Ein offener Flag ist KEINE Zustimmung: erzwinge die Entscheidung oder parke sie (Single-Instance-Lock)

Symptom: Single-Instance-Lock (G) über 3 Turns hinweg in Prosa „dem User vorgelegt"; User hat 0× darauf reagiert (stattdessen Hunt re-run). Drohte ein 4. Mal als vage Sorge im Summary aufzutauchen. Regel: Re-Bestätigung dass ein Bug ECHT ist ≠ Zustimmung zu einer Verhaltensänderung. Die offene Frage bei user-facing Changes ist nicht „ist es real" sondern „will der User dieses Verhalten" (hier: startet er je absichtlich 2 Instanzen?). Eine vierte Prosa-Erwähnung ist „ambient worry", keine Entscheidung. Wie anwenden: Nach dem Release EINE AskUserQuestion feuern (user-facing Change + ggf. weitere geparkte Punkte als Multi-Select gebündelt) ODER in EINER Zeile sagen „geparkt bis dein Wort" und aufhören es zu wiederholen. Nie denselben user-facing Flag 3+ Mal als Sorge raisen.

2026-06-19 — Security-E2E muss JEDEN Collector-Default-Pfad treiben, nicht nur den Aggregat-Hub (Remote-Diagnostics)

Kontext: Read-only Remote-Diagnose gebaut (Agent im App-Prozess + lokales MCP-Gateway, connect-by-code). Redaktion ist die EINZIGE Garantie, dass kein Secret die Box verlässt. Drei Scrub-Ebenen: (1) sanitizeConfig redactet CRED_KEY-gekeyte Felder strukturell, (2) valueScrub ersetzt bekannte Config-Secret-WERTE per split/join, (3) redactLogText pattern-scrubt Secret-SHAPES (Bearer/token=/cookie:/discord-webhook/?key=) in Freitext. Bug 1 (E2E-Gate gefangen): Ein Hoster-zurückgegebenes token=<opaque> in einem History-Error-String überlebte — es ist KEIN gespeichertes Config-Credential, also greift value-scrub nicht, und das line-48-Pattern kannte nur access_token, nicht bare token. Fix: token-Familie (token/auth_token/refresh_token/session_token + standalone Bearer <opaque>) ins Pattern. Bug 2 (Advisor gefangen, NACH grünem E2E): get_config_redacted section:'all' und get_queue_state {includeJobs:true} (DEFAULT-Pfad!) liefen nur durch value-scrub, nie pattern-scrub → derselbe opaque-Token-Leak. redactLogText direkt über kompaktes JSON laufen zu lassen geht NICHT (die cookie:/[^\n]*-Patterns fressen über Feldgrenzen → JSON kaputt). Fix: _deepRedact = per-String-Leaf-Walk, der redactLogText auf jedes Leaf einzeln anwendet (JSON-safe, da pro Leaf begrenzt). history aus get_config gedroppt (hat eigenen Collector). Warum der erste E2E es verfehlte (der Diskriminator): server_health ruft getQueueState({includeJobs:false}) → kein Job-Error je serialisiert; UND das Fixture nutzte apiKey=<SECRET> als Queue-Error → value-scrub fing es eh, auch ohne pattern-scrub. ZWEI Zufälle versteckten den Leak. Der direkte get_queue_state{includeJobs:true}-Pfad mit NICHT-Config-Token war nie getrieben. Regel: Bei einem Redaktions-Gate (a) jeden Collector EINZELN mit seinen DEFAULT-Args treiben, nicht nur den Aggregat-Hub, der bequeme Flags setzt; (b) Fixtures MÜSSEN ein NICHT-Config-Secret enthalten (opaque Token, der nur als Shape erkennbar ist), sonst testet man value-scrub und glaubt, pattern-scrub zu testen; (c) ein grüner E2E heißt nicht „dicht" — Advisor/Review über jeden Pfad laufen lassen, der einen Freitext-Fehlerstring serialisiert. Wie anwenden: Denylist-Redaktion kann einen bare opaque String OHNE key=/Bearer/URL-Kontext NICHT scrubben — das ist inhärent, kein Defekt. Commit-/Release-Notes ehrlich halten („common secret shapes pattern-scrubbed", nicht „kein Secret verlässt je die Box"). Default-Bind 127.0.0.1 erzwingen (_safeDiagBindAddress), 0.0.0.0-Option erst wenn wss/TLS existiert — nie plaintext ws:// auf allen Interfaces.