# Speichern im Remote-Editor und Latenzüberwachung ## Ursachen Der bisherige Editor lud seine Arbeitsdatei in das übergeordnete Verzeichnis des Ziels hoch, in das das SSH-Konto unter `/etc` oder `/var` oft nicht schreiben darf. Das Umbenennen per SFTP bot weder einen autorisierten Weg über sudo noch eine Konfliktprüfung gegen den Ausgangszustand, eine Prüfung der Sicherheitsmetadaten oder eine verlässliche Überschreib-Semantik. Seine Fehlerbehandlung löschte die temporäre Kopie und entfernte damit Wiederherstellungsmaterial selbst dann, wenn der Abschluss ungewiss war. Der Sitzungsstatus zeigte bisher nur den Verbindungszustand. Die Terminalanzeige erhielt weder eine ICMP-Messung noch eine Messung der Antwortzeit des authentifizierten SSH. ## Speicherprotokoll 1. Ein festes SSH-Hilfsprogramm öffnet eine reguläre Datei mit genau einem Hardlink über deskriptorrelative Operationen und `O_NOFOLLOW` für jede Pfadkomponente. Es erfasst den Inhalt, den SHA-256-Hash, Gerät/Inode, UID, GID, alle Berechtigungsbits, die nanosekundengenauen Änderungs- und Statuszeiten sowie alle lesbaren erweiterten Attribute. Core prüft den Hash und dekodiert striktes UTF-8 unter Beibehaltung eines BOM. Jedes Dokument erhält eine undurchsichtige `editId`, die auf sein Plugin, seine Terminalsitzung und die Serveridentität beschränkt ist. 2. Ein nicht privilegierter Vorbereitungsschritt ermittelt das Home-Verzeichnis des authentifizierten Kontos über `pwd.getpwuid(os.getuid())`, legt `$HOME/ch-j-sm/` mit dem Modus `0700` an und erstellt ein privates, exklusives Wiederherstellungsjournal. Ein unsicheres Home-Verzeichnis, ein Arbeitsbereich als symbolischer Link oder ein Arbeitsbereich eines anderen Kontos wird abgelehnt. 3. Core lädt per SFTP eine versteckte, zufällige 128-Bit-Arbeitsdatei `.tmp` mit exklusiver Erstellung (`wx`), Modus `0600` und einem sechzigsekündigen Kanal-/Upload-Zeitlimit hoch. Das Journal enthält das Ziel, den ursprünglichen Ausgangszustand und den erwarteten Hash der Arbeitskopie; es enthält keine Anmeldeinformationen. 4. **Eine einzige SSH-Ausführung (exec) erledigt den gesamten Abschluss**, optional über den bestehenden sudo-Ausführer. Das Hilfsprogramm öffnet die private Arbeitskopie sicher, berechnet ihren Hash und schreibt sie auf den Datenträger. Es sperrt den ursprünglichen Inode, prüft dessen Ausgangszustand, erstellt exklusiv eine Zieldatei neben dem Ziel, schreibt den gesamten Inhalt, setzt und prüft die Metadaten, prüft das Original erneut und schreibt den Ersatz auf den Datenträger. 5. Ein `os.replace` im selben Verzeichnis ersetzt das Ziel atomar. Das übergeordnete Verzeichnis wird auf den Datenträger geschrieben, und endgültiger Pfad, Hash, Inode und Metadaten werden geprüft. Der ursprüngliche Pfad bleibt bis zum Ersetzen verfügbar. Die Arbeitsdatei wird nie zwischen Dateisystemen verschoben und vom Abschluss nie entfernt. 6. Nachdem Core einen geprüften Erfolg erhalten hat, prüft eine separate Bereinigung den endgültigen Inhalt und die Sicherheitsmetadaten erneut, bevor sie Arbeitsdatei und Journal entfernt. Schlägt die Bereinigung fehl, wird dennoch ein bestätigtes Speichern mit `recoveryAvailable: true` gemeldet. Ein fehlgeschlagener Upload, fehlgeschlagenes sudo, ein Konflikt, ein Metadatenfehler oder ein fehlgeschlagenes Ersetzen behält das Original und sämtliches vorbereitetes Material. Unvollständige Uploads werden als solche gekennzeichnet und können nicht als vollständig bearbeitetes Dokument wiederhergestellt werden. Verlorene SSH-Kommunikation, fehlender Exit-Status, Zeitüberschreitungen und unerwartete Abschlussergebnisse werden als **unbekannt** gemeldet, nie als gespeichert. Core blockiert weiteres Speichern eines ungewissen Dokuments; es gibt keine automatische Wiederholung des Ersetzens. ## Metadaten, Berechtigungen und Sicherheitsgrenzen Das Hilfsprogramm bewahrt und prüft UID, GID und alle POSIX-Berechtigungsbits einschließlich Ausführbarkeit, setuid, setgid und Sticky-Bit. Erweiterte Attribute von Linux werden Byte für Byte reproduziert, einschließlich POSIX-ACLs, Capabilities und SELinux-Labels. Eigentümer und Dateiinhalt werden vor den Sicherheitsattributen gesetzt, da Schreiben/chown Capabilities oder spezielle Berechtigungsbits löschen kann. Geerbte Attribute des Ziels, die das Original nicht hat, werden entfernt. Die Änderungszeit spiegelt den neuen Schreibvorgang wider und wird nicht vom Original kopiert. Ein unlesbares oder nicht darstellbares Attribut bricht den Vorgang ab; Metadaten werden nie stillschweigend verworfen. IMA/EVM-Signaturen werden ausdrücklich abgelehnt, weil das Kopieren inhalts-/inode-gebundener Integritätssignaturen diese ungültig machen würde. Symbolische Links im Ziel- oder Home-Pfad, Dateien mit mehreren Hardlinks und nicht reguläre Dateien werden abgelehnt. Es wird kein Überschreiben an Ort und Stelle und kein schwächeres SFTP-Überschreiben als Ausweichlösung verwendet. Gewöhnliche beschreibbare Speichervorgänge laufen unter dem SSH-Konto. Geschützte Speichervorgänge erfordern ausdrücklich `{ sudo: true }`; ein sudo-Passwort ist bei passwortlosem sudo optional. Passwörter werden geprüft und nur über stdin des Kanals übergeben, nie im Befehl, Journal, in der Arbeitsdatei, im Logger oder in einem entfernten Fehler. Root-SSH-Sitzungen verwenden den bestehenden direkten root-Ausführer. Eine fehlgeschlagene Autorisierung lässt Original und Arbeitskopie unverändert. Alle als root ausgeführten Speichervorgänge (direktes root-SSH oder sudo) lehnen zusätzlich Zielverzeichnisse/Vorgänger ab, die anderen Konten gehören, von ihnen beschreibbar sind oder eine Zugriffs-ACL tragen, statt einem privilegierten, von einem anderen Konto kontrollierten Pfadnamen zu vertrauen. Das feste Hilfsprogramm läuft mit `python3 -I -B`: Es ignoriert Python-Pfade des Benutzers, das Arbeitsverzeichnis und Benutzer-Site-Packages und schreibt keinen Bytecode. Pfade und JSON-Argumente werden in Core für die Shell maskiert. Plugins erhalten niemals beliebiges SSH-exec oder SFTP-Handles. Bestehende Sandbox, CSP, Manifest-Berechtigungen, Hostschlüsselprüfung und Aktualisierungsprüfung bleiben in Kraft. ## Plugin API und verbleibende Monaco-Integration **Die separat verteilte Oberfläche Monaco/File Manager ist in diesem Checkout nicht enthalten. Ihre UI-Integration, die sudo-Abfrage, die Behandlung ungespeicherter Puffer und die Übersetzungen konnten hier weder umgesetzt noch getestet werden.** Die mitgelieferte Plugin-Quelle ist das Plugin Hash & Checksum. Es wurde keine Ersatzquelle für den File Manager erfunden. Die bestehenden Methoden bleiben verfügbar. Zusätzliche Methoden/Optionen stellen das Preload in der Sandbox und die Laufzeitumgebung mit Berechtigungsprüfung bereit: ```js const opened = await chjPlugin.files.readText(sessionId, path); // For a protected read, explicitly pass { sudo: true, sudoPassword }. const result = await chjPlugin.files.saveText(sessionId, path, editor.getValue(), { editId: opened.editId, sudo: true, // Include only after the user authorizes elevation. sudoPassword // Omit for passwordless sudo; never persist this value. }); if (result.ok && result.value.status === "saved") { // Mark this exact submitted editor version as saved. Later edits stay dirty. } else { // Keep the Monaco model dirty. Translate result.error.code/status. // result.error.recoveryId/recoveryPath identify retained working material. } await chjPlugin.files.closeText(opened.editId); const recovery = await chjPlugin.files.listRecovery(sessionId); const copy = await chjPlugin.files.readRecovery(sessionId, recoveryId); // Review copy.status and copy.text; open a fresh document before another save. await chjPlugin.files.cleanupRecovery(sessionId, recoveryId); // Cleanup rejects anything whose target is not currently confirmed. ``` Wiederherstellungsmethoden akzeptieren ein optionales letztes Argument `{ sudo: true, sudoPassword }`, wenn die Prüfung eines Ziels erhöhte Rechte erfordert. Sie behalten den Arbeitsbereich des authentifizierten Benutzers auch bei Ausführung über sudo. Journale überstehen einen Neustart der Anwendung und eine erneute SSH-Verbindung; die Wiederherstellung wiederholt den Abschluss nie automatisch. Vergleichen Sie einen wiederhergestellten Puffer mit einem frisch geöffneten Ziel, bevor Sie ihn ausdrücklich speichern. `files.read` schützt Lesen, Prüfen und Freigeben von Handles. `files.write` schützt Speichern und Bereinigen der Wiederherstellung. `writeText` lehnt bei bestehenden Plugins im Fehlerfall weiterhin ab; `saveText` gibt eine strukturierte Hülle `{ ok, value/error }` zurück, weil Electron eigene Eigenschaften abgelehnter Error-Objekte verwirft. Ein älterer Aufruf von `writeText` mit drei Argumenten kann den Ausgangszustand eines einzigen geöffneten Dokuments verwenden. Mehrere aktive Dokumente für denselben Pfad erfordern `editId`; mehrdeutige ältere Aufrufe schlagen sicher fehl, statt den Ausgangszustand eines anderen Dokuments zu wählen. Plugins sollten ungenutzte Handles freigeben. Die externe Oberfläche muss das Speichern sofort anzeigen, Puffer bei Fehlern als ungespeichert behalten, sudo erst nach Berechtigungs-/Authentifizierungsfehlern anbieten, Konflikte ohne Überschreiben anzeigen und nach Verbindungsverlust oder unbekanntem Ergebnis eine Wiederherstellung anbieten. Sie muss diese Zustände auf Tschechisch, Deutsch und Englisch übersetzen. Vorgeschlagene Zustandstexte: | Zustand | Tschechisch | Deutsch | Englisch | | --- | --- | --- | --- | | Speichern | Ukládání… | Wird gespeichert… | Saving… | | Bestätigt | Uloženo | Gespeichert | Saved | | Fehler | Uložení selhalo | Speichern fehlgeschlagen | Save failed | | Sudo erforderlich | Vyžadováno oprávnění sudo | sudo-Autorisierung erforderlich | Sudo authorization required | | Konflikt | Původní soubor byl změněn | Originaldatei wurde geändert | Original file changed | | Getrennt | Spojení bylo přerušeno | Verbindung unterbrochen | Connection lost | | Unbekannt | Výsledek uložení není znám | Speicherergebnis unbekannt | Save result unknown | | Wiederherstellung | Obnova je dostupná | Wiederherstellung verfügbar | Recovery available | ## Latenzdefinitionen und Lebenszyklus **Ping** ist die Latenz, die eine tatsächliche clientseitige ICMP-Echo-Antwort von der aufgelösten IP-Adresse meldet, die SSH verwendet. Core ruft das Ping-Programm der Plattform asynchron mit einem festen Argument-Array auf, ohne Shell und ohne Start mit Administratorrechten. Die Argumente für Linux, macOS (einschließlich `ping6`) und Windows werden getrennt gewählt. Blockiertes ICMP, ein fehlendes Programm, eine Zeitüberschreitung, ungültige Ausgabe oder eine Windows-Antwort nur mit Obergrenze wie `<1ms` ergeben einen nicht verfügbaren Wert, keine erfundene Zahl. **SSH RTT** ist die monotone Dauer `performance.now()` einer festen exec-Anfrage `true` über den bestehenden authentifizierten SSH-Client. Sie umfasst Kanalaufbau, Verarbeitung auf dem Server und Zustellung der Antwort; sie ist weder reine Netzwerk-RTT noch die Dauer der SSH-Anmeldung. Der übersetzte Tooltip der Anzeige erklärt diesen Unterschied. Die Überwachung beginnt, sobald das Terminal verbunden ist. Messungen laufen parallel und asynchron mit einem Zeitlimit von fünf Sekunden pro Anfrage/Prozess. Die nächste Messung wird zwölf Sekunden nach Abschluss der vorherigen geplant, sodass sich nichts überschneidet. Das Trennen bricht ICMP ab, löscht Timer, bricht ausstehende SSH-Arbeit ab und löscht die Werte. Identitätsprüfungen der Einträge verhindern, dass eine frühere Verbindung nach dem erneuten Verbinden Werte veröffentlicht. Ein ICMP-Fehler ändert nie den Zustand der SSH-Verbindung. Die Latenz wird im bestehenden Ereignis `state` bzw. in den IPC-Daten von `ssh:state` übertragen: ```js latency: { pingMs: 24.1, sshRttMs: 31.25, measuredAt: "…" } ``` Nicht verfügbare Zahlen sind `null`. Die kompakte Terminalanzeige verwendet Millisekunden, einen lokalisierten Text für Nichtverfügbarkeit und Beschriftungen auf Tschechisch/Deutsch/Englisch. Beim Trennen wird sie ausgeblendet. Es waren keine zusätzlichen Ausführungsrechte im Renderer und keine IPC-Anfrage nötig. ## Prüfung und betriebliche Grenzen - Aus `app/` ausführen: `npm ci`, danach `npm test`. - Endgültiges lokales Ergebnis: **179 Tests; 163 bestanden, 0 fehlgeschlagen, 16 übersprungen**. Alle übersprungenen Fälle sind Linux-Dateisystemtests auf diesem macOS-Host. `npm ci`, die Syntaxprüfung von Python und `git diff --check` wurden erfolgreich abgeschlossen. - Die Tests decken Speichern/Wiederherstellen in Core, das sudo-Protokoll und das Schwärzen von Passwörtern, echtes POSIX-Shell-Quoting feindlicher Pfade, Konflikte, Uploads, fehlgeschlagene Bereinigung, ungewisse Ergebnisse, parallele Dokumente, IPC-Berechtigungen und den Latenz-Lebenszyklus ab. - Linux-Dateisystemtests führen den ausgelieferten Python-Einstiegspunkt mit einer privaten Test-Ermittlung des Home-Verzeichnisses aus: Eigentümer/Modus/xattrs, POSIX-ACLs, symbolische/harte Links, FIFOs, Upload- und Metadatenfehler, Ersetzungsfehler, Wettläufe am Ziel, unbekannte Ergebnisse nach dem Ersetzen und Wiederherstellung. Dateisystemübergreifende Vorbereitung verwendet `/dev/shm` und ein weiteres Dateisystem, sofern verfügbar. Auf Nicht-Linux-Hosts werden diese Tests übersprungen; fehlende ACL-/separate Dateisystem-Funktionen werden ausdrücklich übersprungen. - Der hinzugefügte Linux-Job für GitHub Actions führt die gesamte Suite aus und wiederholt die Dateisystem-Fälle als root. Dieser Workflow wurde aus diesem Arbeitsbaum nicht angestoßen. - Die lokale Prüfung erfolgte unter macOS. Die Python-Quellsyntax wurde direkt mit der ausführbaren Python-Datei der Command Line Tools geprüft. Ein echter ICMP-Smoke-Test über Loopback unter macOS ergab `0.071 ms`. Lokal wurden kein entfernter SSH-Server, keine Windows- oder Linux-Laufzeitumgebung, kein SELinux-Einsatz und keine externe Monaco-Oberfläche getestet. - Die Textbearbeitung auf dem Server erfordert jetzt Linux und Python 3 mit deskriptorrelativer Dateisystem-/xattr-Unterstützung. Ein fehlender Interpreter führt zu einem ausdrücklichen Fehler. Dieser Core lehnt unsichere Metadaten, Links und Verzeichniskonfigurationen bewusst ab. - Die Sperre ist nur kooperativ: Nicht kooperierende Prozesse können im schmalen Intervall zwischen der letzten Prüfung des Ausgangszustands und dem atomaren Umbenennen weiterhin konkurrieren. Linux bietet keine allgemeine atomare Operation zum Vergleichen eines Inhalts-Hashes und Ersetzen. Die Prüfungen erkennen Änderungen vor diesem Intervall; aufbewahrte Arbeitskopien ermöglichen danach die Wiederherstellung. - Wiederherstellungsdaten bleiben bis zu einer bestätigten Bereinigung im entfernten Konto. Unvollständige, konfliktbehaftete und nicht prüfbare Journale werden nie stillschweigend verworfen. Es gibt keinen automatischen Ablauf von Wiederherstellungsdateien und keine Option zum erzwungenen Überschreiben. - `npm ci` meldete 17 Befunde der Abhängigkeitsprüfung (9 mittel, 8 hoch). Diese Änderung ändert weder Abhängigkeiten noch das Vertrauensmodell für Aktualisierungen der Anwendung. Der Benutzer verlangte, die Änderungen im bestehenden Arbeitsbaum zu belassen, derzeit `feature/hash-checksum-plugin`; es gab keinen Branch-Wechsel und keine unbeteiligte Bereinigung. ## Für diese Aufgabe geänderte Dateien - `src/main/files/remoteFileService.js` - `src/main/files/remoteEditor.py` (neu) - `src/main/sessions/sessionManager.js` - `src/main/sessions/latencyMonitor.js` (neu) - `src/main/plugins/pluginRuntime.js` - `src/preload/pluginPreload.js` - `src/renderer/app.js` - `src/renderer/index.html` - `src/renderer/styles.css` - `src/renderer/i18n.js` - `test/remoteFileService.test.js` - `test/sessionManager.test.js` - `test/pluginRuntime.test.js` - `test/remoteEditor.test.js` (neu) - `test/remoteEditorProtocol.test.js` (neu) - `test/remoteEditorLinux.test.js` (neu) - `test/fixtures/remoteEditorLinux.py` (neu) - `test/latencyMonitor.test.js` (neu) - `README.md` - `docs/remote-editor-latency.md` (diese Datei) - `../.github/workflows/remote-editor-tests.yml` (neu)