# Ukládání ve vzdáleném editoru a sledování latence ## Hlavní příčiny Předchozí editor nahrával svůj pracovní soubor do nadřazeného adresáře cíle, do kterého účet SSH pod `/etc` nebo `/var` často nemůže zapisovat. Přejmenování přes SFTP neposkytovalo autorizovanou cestu přes sudo, kontrolu konfliktu vůči výchozímu stavu, ověření bezpečnostních metadat ani spolehlivou sémantiku přepsání. Obsluha chyb mazala dočasnou kopii, a tím odstraňovala materiál pro obnovu i tehdy, když dokončení nebylo jisté. Stav relace dříve ukazoval pouze stav spojení. Indikátor terminálu nedostával žádné měření ICMP ani měření odezvy autentizovaného SSH. ## Protokol ukládání 1. Pevný pomocný program SSH otevře běžný soubor s jediným pevným odkazem pomocí operací relativních k deskriptoru a `O_NOFOLLOW` pro každou složku cesty. Zaznamená obsah, hash SHA-256, zařízení/inode, UID, GID, všechny bity oprávnění, nanosekundové časy změny obsahu a stavu a všechny čitelné rozšířené atributy. Core ověří hash a dekóduje striktní UTF-8 se zachováním BOM. Každý dokument dostane neprůhledné `editId` omezené na svůj plugin, relaci terminálu a identitu serveru. 2. Neprivilegovaná přípravná operace zjistí domovský adresář autentizovaného účtu přes `pwd.getpwuid(os.getuid())`, vytvoří `$HOME/ch-j-sm/` s režimem `0700` a vytvoří soukromý výhradní deník obnovy. Nebezpečný domovský adresář, pracovní prostor jako symbolický odkaz nebo pracovní prostor vlastněný jiným účtem je odmítnut. 3. Core nahraje přes SFTP skrytý náhodný 128bitový pracovní soubor `.tmp` s výhradním vytvořením (`wx`), režimem `0600` a šedesátisekundovým limitem kanálu/nahrávání. Deník obsahuje cíl, původní výchozí stav a očekávaný hash pracovní kopie; neobsahuje žádné přihlašovací údaje. 4. **Celé dokončení provede jediné spuštění přes SSH exec**, volitelně přes stávající vykonavatel sudo. Pomocný program bezpečně otevře soukromou pracovní kopii, spočítá její hash a zapíše ji na disk. Uzamkne původní inode, zkontroluje výchozí stav, vytvoří výhradní cílový soubor vedle cíle, zapíše celý obsah, nastaví a ověří metadata, znovu zkontroluje originál a zapíše náhradu na disk. 5. `os.replace` ve stejném adresáři atomicky nahradí cíl. Nadřazený adresář se zapíše na disk a ověří se konečná cesta, hash, inode a metadata. Původní cesta zůstává dostupná až do nahrazení. Pracovní soubor se nikdy nepřesouvá mezi souborovými systémy a dokončení jej nikdy neodstraňuje. 6. Jakmile Core obdrží ověřený úspěch, samostatný úklid znovu zkontroluje konečný obsah a bezpečnostní metadata a teprve poté odstraní pracovní soubor a deník. Selhání úklidu stále hlásí potvrzené uložení s `recoveryAvailable: true`. Neúspěšné nahrání, neúspěšné sudo, konflikt, chyba metadat nebo neúspěšné nahrazení zachová originál i veškerý připravený materiál. Neúplná nahrání se jako taková označí a nelze je obnovit jako úplný upravený dokument. Ztracená komunikace SSH, chybějící návratový stav, vypršení časových limitů a neočekávané výsledky dokončení se hlásí jako **neznámé**, nikdy jako uložené. Core u nejistého dokumentu blokuje další uložení; automatické opakování nahrazení neexistuje. ## Metadata, oprávnění a bezpečnostní hranice Pomocný program zachovává a ověřuje UID, GID a všechny bity oprávnění POSIX, včetně bitů spustitelnosti, setuid, setgid a sticky. Rozšířené atributy Linux reprodukuje bajt po bajtu, včetně POSIX ACL, capabilities a značek SELinux. Vlastnictví a obsah souboru se nastavují před bezpečnostními atributy, protože zápis/chown může vymazat capabilities nebo speciální bity oprávnění. Zděděné atributy cíle, které originál nemá, se odstraní. Čas změny odpovídá novému zápisu a nekopíruje se z originálu. Nečitelný nebo nepřenositelný atribut operaci přeruší; metadata se nikdy tiše nezahazují. Podpisy IMA/EVM jsou výslovně odmítnuty, protože kopírování podpisů integrity vázaných na obsah/inode by je zneplatnilo. Symbolické odkazy v cílové nebo domovské cestě, soubory s více pevnými odkazy a jiné než běžné soubory jsou odmítnuty. Nepoužívá se žádné přepsání na místě ani slabší náhradní přepsání přes SFTP. Běžná zapisovatelná uložení běží pod účtem SSH. Chráněná uložení vyžadují výslovné `{ sudo: true }`; heslo pro sudo je u sudo bez hesla volitelné. Hesla se ověřují a předávají pouze přes stdin kanálu, nikdy v příkazu, deníku, pracovním souboru, logu ani ve vzdálené chybě. Relace SSH jako root používají stávající přímý vykonavatel root. Neúspěšná autorizace ponechá originál i pracovní kopii beze změny. Všechna uložení prováděná jako root (přímé SSH jako root nebo sudo) navíc odmítají cílové adresáře/předky vlastněné jinými účty nebo jimi zapisovatelné či opatřené přístupovým ACL, místo aby důvěřovala privilegované cestě ovládané jiným účtem. Pevný pomocný program běží s `python3 -I -B`: ignoruje uživatelské cesty Pythonu, pracovní adresář a uživatelské site packages a nezapisuje bytecode. Cesty a argumenty JSON se v Core uvozují pro shell. Pluginy nikdy nedostávají libovolný SSH exec ani handly SFTP. Stávající sandbox, CSP, oprávnění manifestu, ověřování klíče hostitele a ověřování aktualizací zůstávají v platnosti. ## Plugin API a zbývající integrace Monaco **Samostatně distribuované UI Monaco/File Manager v tomto checkoutu chybí. Jeho integraci do UI, výzvu sudo, práci s neuloženým bufferem a překlady zde nebylo možné implementovat ani otestovat.** Přibaleným zdrojem pluginu je plugin Hash & Checksum. Žádný náhradní zdroj File Manageru nebyl vymyšlen. Stávající metody zůstávají dostupné. Doplňkové metody/volby zpřístupňuje preload v sandboxu a běhové prostředí s kontrolou oprávnění: ```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. ``` Metody obnovy přijímají volitelný poslední argument `{ sudo: true, sudoPassword }`, pokud kontrola cíle vyžaduje zvýšení oprávnění. Ponechávají pracovní prostor autentizovaného uživatele i při spuštění přes sudo. Deníky přežijí restart aplikace i nové připojení SSH; obnova nikdy automaticky neopakuje dokončení. Obnovený buffer před výslovným uložením porovnejte s nově otevřeným cílem. `files.read` chrání čtení, kontrolu a uvolnění handlů. `files.write` chrání uložení a úklid obnovy. `writeText` u stávajících pluginů při selhání nadále odmítá; `saveText` vrací strukturovanou obálku `{ ok, value/error }`, protože Electron u odmítnutých objektů Error zahazuje vlastní vlastnosti. Starší tříargumentové volání `writeText` může použít výchozí stav jediného otevřeného dokumentu. Více živých dokumentů pro stejnou cestu vyžaduje `editId`; nejednoznačná starší volání bezpečně selžou, místo aby použila výchozí stav jiného dokumentu. Pluginy by měly nepoužívané handly uvolňovat. Externí UI musí ukládání zobrazit okamžitě, při selhání ponechat buffery jako neuložené, nabízet sudo až po selhání oprávnění/autentizace, zobrazit konflikty bez přepsání a po ztrátě spojení nebo neznámém výsledku nabídnout obnovu. Tyto stavy musí překládat do češtiny, němčiny a angličtiny. Navržené texty stavů: | Stav | Čeština | Němčina | Angličtina | | --- | --- | --- | --- | | Ukládání | Ukládání… | Wird gespeichert… | Saving… | | Potvrzeno | Uloženo | Gespeichert | Saved | | Selhání | Uložení selhalo | Speichern fehlgeschlagen | Save failed | | Vyžadováno sudo | 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 | | Odpojeno | Spojení bylo přerušeno | Verbindung unterbrochen | Connection lost | | Neznámé | Výsledek uložení není znám | Speicherergebnis unbekannt | Save result unknown | | Obnova | Obnova je dostupná | Wiederherstellung verfügbar | Recovery available | ## Definice latence a životní cyklus **Ping** je latence hlášená skutečnou odpovědí ICMP echo na straně klienta na přeloženou IP adresu, kterou používá SSH. Core spouští systémový program ping asynchronně s pevným polem argumentů, bez shellu a bez nutnosti spouštět aplikaci jako správce. Argumenty pro Linux, macOS (včetně `ping6`) a Windows se volí zvlášť. Blokované ICMP, chybějící program, vypršení času, neplatný výstup nebo odpověď Windows udávající jen horní mez, například `<1ms`, vedou k nedostupné hodnotě, nikoli k vymyšlenému číslu. **SSH RTT** je monotónní doba `performance.now()` pevného požadavku exec `true` přes stávajícího autentizovaného klienta SSH. Zahrnuje vytvoření kanálu, zpracování na serveru a doručení odpovědi; nejde o čisté síťové RTT ani o dobu přihlášení SSH. Přeložená nápověda indikátoru tento rozdíl vysvětluje. Sledování začíná, jakmile se terminál připojí. Sondy běží souběžně a asynchronně s pětisekundovým limitem požadavku/procesu. Další vzorek se plánuje dvanáct sekund po dokončení předchozího, takže se nepřekrývají. Odpojení přeruší ICMP, vymaže časovače, zruší probíhající práci SSH a vymaže hodnoty. Kontroly identity záznamu brání tomu, aby předchozí spojení publikovalo hodnoty po novém připojení. Selhání ICMP nikdy nemění stav spojení SSH. Latence se přenáší uvnitř stávající události `state` / datové části IPC `ssh:state`: ```js latency: { pingMs: 24.1, sshRttMs: 31.25, measuredAt: "…" } ``` Nedostupná čísla jsou `null`. Kompaktní indikátor terminálu používá milisekundy, lokalizovaný text pro nedostupnost a popisky v češtině/němčině/angličtině. Po odpojení se skryje. Nebyla potřeba žádná další schopnost spouštění v rendereru ani požadavek IPC. ## Ověření a provozní omezení - Spusťte z `app/`: `npm ci`, poté `npm test`. - Konečný místní výsledek: **179 testů; 163 prošlo, 0 selhalo, 16 přeskočeno**. Všechna přeskočení jsou případy souborového systému Linux na tomto hostiteli macOS. `npm ci`, kontrola syntaxe Pythonu a `git diff --check` proběhly úspěšně. - Testy pokrývají ukládání/obnovu v Core, protokol sudo a skrývání hesel, skutečné uvozování nebezpečných cest pro shell POSIX, konflikty, nahrávání, neúspěšný úklid, nejisté výsledky, souběžné dokumenty, oprávnění IPC a životní cyklus latence. - Testy souborového systému Linux spouštějí dodávaný vstupní bod v Pythonu se soukromým testovacím vyhledáním domovského adresáře účtu: vlastnictví/režim/xattrs, POSIX ACL, symbolické/pevné odkazy, FIFO, selhání nahrávání a metadat, selhání nahrazení, souběhy na cíli, neznámé výsledky po nahrazení a obnovu. Příprava napříč souborovými systémy používá `/dev/shm` a další souborový systém, pokud je dostupný. Na jiných hostitelích než Linux se tyto testy přeskočí; chybějící podpora ACL/samostatného souborového systému se výslovně přeskočí. - Přidaná úloha GitHub Actions pro Linux spouští celou sadu a opakuje případy souborového systému jako root. Tento workflow z tohoto pracovního stromu spuštěn nebyl. - Místní ověření proběhlo na macOS. Syntaxe zdrojů v Pythonu byla zkontrolována přímo spustitelným souborem Pythonu z Command Line Tools. Skutečná smoke kontrola ICMP na loopbacku v macOS vrátila `0.071 ms`. Místně nebyl testován žádný živý vzdálený server SSH, běhové prostředí Windows ani Linux, nasazení SELinux ani externí UI Monaco. - Vzdálené úpravy textu nyní vyžadují Linux a Python 3 s podporou souborového systému/xattr relativní k deskriptoru. Chybějící interpret selže s výslovnou chybou. Core záměrně odmítá nebezpečná metadata, odkazy a konfigurace adresářů. - Zamykání je pouze doporučující: nespolupracující procesy mohou stále soupeřit v úzkém intervalu mezi poslední kontrolou výchozího stavu a atomickým přejmenováním. Linux neposkytuje obecnou atomickou operaci porovnání hashe obsahu a nahrazení. Kontroly odhalí změny před tímto intervalem; ponechané pracovní kopie umožňují pozdější obnovu. - Data pro obnovu zůstávají ve vzdáleném účtu až do potvrzeného úklidu. Neúplné, konfliktní a nekontrolovatelné deníky se nikdy tiše nezahazují. Neexistuje automatická expirace souborů obnovy ani možnost vynuceného přepsání. - `npm ci` hlásil 17 nálezů auditu závislostí (9 středních, 8 vysokých). Tato změna nemění závislosti ani model důvěry aktualizací aplikace. Uživatel požadoval zachovat změny ve stávajícím pracovním stromu, nyní `feature/hash-checksum-plugin`; nedošlo k přepnutí větve ani k nesouvisejícímu úklidu. ## Soubory změněné v tomto úkolu - `src/main/files/remoteFileService.js` - `src/main/files/remoteEditor.py` (nový) - `src/main/sessions/sessionManager.js` - `src/main/sessions/latencyMonitor.js` (nový) - `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` (nový) - `test/remoteEditorProtocol.test.js` (nový) - `test/remoteEditorLinux.test.js` (nový) - `test/fixtures/remoteEditorLinux.py` (nový) - `test/latencyMonitor.test.js` (nový) - `README.md` - `docs/remote-editor-latency.md` (tento soubor) - `../.github/workflows/remote-editor-tests.yml` (nový)