# 遠端編輯器的儲存與延遲監控 ## 根本原因 先前的編輯器會將工作檔案上傳到目標檔案的上層目錄,而在 `/etc` 或 `/var` 下,SSH 帳戶通常沒有該目錄的寫入權限。SFTP 重新命名既不提供經授權的 sudo 路徑,也沒有基準衝突檢查、安全性中繼資料驗證或可靠的覆寫語意。其錯誤處理程式會刪除暫存副本,即使在無法確定作業是否完成時,也會移除可供復原的資料。 工作階段狀態先前只顯示連線狀態。沒有任何 ICMP 或經驗證的 SSH 回應量測可提供給終端機指示器。 ## 儲存通訊協定 1. 固定的 SSH 輔助程式使用以描述元為基準的相對操作,並對每個路徑元件使用 `O_NOFOLLOW`,開啟一個一般的單一連結檔案。它會記錄內容、SHA-256 雜湊、裝置/inode、UID、GID、完整的權限位元、奈秒級的修改/變更時間,以及所有可讀取的延伸屬性。Core 會驗證雜湊,並以嚴格的 UTF-8 解碼,同時保留 BOM。每份文件都會取得一個不透明的 `editId`,其範圍限定於對應的外掛程式、終端機工作階段與伺服器身分。 2. 一個非特權的準備作業會透過 `pwd.getpwuid(os.getuid())` 解析已驗證帳戶的家目錄,以 `0700` 模式建立 `$HOME/ch-j-sm/`,並建立一份私有、獨占的復原日誌。不安全的家目錄、以符號連結構成的工作區,或由其他帳戶擁有的工作區都會被拒絕。 3. Core 透過 SFTP 上傳一個隱藏的、隨機 128 位元的 `.tmp` 工作檔案,使用獨占建立(`wx`)、`0600` 模式,以及六十秒的通道/上傳逾時。日誌包含目標、原始基準與預期的工作檔案雜湊;不包含任何登入憑據。 4. **一次 SSH exec 完成整個最終處理**,並可選擇透過既有的 sudo 執行器執行。輔助程式會安全地開啟私有工作副本、計算其雜湊並將其寫入磁碟。它會鎖定原始 inode、檢查其基準、在目標旁建立一個獨占的目的地檔案、寫入全部內容、套用並驗證中繼資料、再次檢查原始檔案,然後將替換檔案寫入磁碟。 5. 在同一目錄中使用 `os.replace` 以不可分割的方式替換目標。上層目錄會被寫入磁碟,並驗證最終路徑、雜湊、inode 與中繼資料。在替換完成之前,原始路徑一律可用。工作檔案絕不會跨檔案系統移動,也絕不會在最終處理過程中被刪除。 6. Core 收到經驗證的成功結果後,會由獨立的清理步驟再次檢查最終內容與安全性中繼資料,然後才刪除工作檔案與日誌。即使清理失敗,仍會回報已確認的儲存,並附帶 `recoveryAvailable: true`。 上傳失敗、sudo 失敗、衝突、中繼資料錯誤或替換失敗時,會保留原始檔案以及所有已暫存的資料。未完成的上傳會如實標示,無法當作完整的已編輯文件復原。SSH 通訊中斷、缺少結束狀態、逾時以及非預期的最終處理結果,都會回報為**未知**,絕不會回報為已儲存。對於結果不確定的文件,Core 會阻止再次儲存;不會自動重試替換。 ## 中繼資料、權限與安全性邊界 輔助程式會保留並驗證 UID、GID 以及所有 POSIX 權限位元,包括可執行、setuid、setgid 與 sticky 位元。它會逐位元組重現 Linux 延伸屬性,包括 POSIX ACL、capabilities 與 SELinux 標籤。擁有者與檔案內容會先於安全性屬性設定,因為寫入/chown 可能會清除 capabilities 或特殊權限位元。原始檔案中不存在、但目的地檔案繼承而來的屬性會被移除。修改時間反映新的寫入,不會從原始檔案複製。 無法讀取或無法表示的屬性會中止作業;中繼資料不會在未告知的情況下被捨棄。IMA/EVM 簽章會被明確拒絕,因為複製與內容/inode 綁定的完整性簽章會使其失效。目標路徑或家目錄路徑中的符號連結、硬連結檔案以及非一般檔案都會被拒絕。不使用任何就地覆寫或較弱的 SFTP 覆寫備援方式。 一般的可寫入儲存會以 SSH 帳戶身分執行。受保護的儲存需要明確指定 `{ sudo: true }`;對於不需要密碼的 sudo,sudo 密碼為選用。密碼經驗證後只會透過通道的 stdin 傳遞,絕不會出現在指令、日誌、工作檔案、記錄器或遠端錯誤中。root SSH 工作階段使用既有的直接 root 執行器。授權失敗時,原始檔案與工作副本維持不變。所有以 root 身分執行的儲存(直接 root SSH 或 sudo)也會拒絕由其他帳戶擁有或可寫入、或帶有存取 ACL 的目標目錄/上層目錄,而不會信任由其他帳戶控制的特權路徑名稱。 固定的輔助程式以 `python3 -I -B` 執行:它會忽略使用者的 Python 路徑、工作目錄與使用者 site-packages,且不寫入位元組碼。路徑與 JSON 參數在 Core 中進行 shell 跳脫。外掛程式絕不會取得任意的 SSH exec 或 SFTP 控制代碼。既有的沙箱、CSP、資訊清單權限、主機金鑰驗證與更新驗證仍持續有效。 ## Plugin API 與剩餘的 Monaco 整合 **另行散布的 Monaco/File Manager UI 不在此簽出內容中。其 UI 整合、sudo 提示、未儲存緩衝區的處理與翻譯無法在此實作或測試**。內建的外掛程式原始碼是 Hash & Checksum 外掛程式。並未虛構任何替代的 File Manager 原始碼。 既有的方法仍可使用。新增的方法/選項由沙箱化的 preload 與經過權限檢查的執行環境提供: ```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. ``` 當檢查目標需要提升權限時,復原方法可接受一個選用的最後參數 `{ sudo: true, sudoPassword }`。即使透過 sudo 執行,它們也會保留已驗證使用者的工作區。日誌在應用程式重新啟動與 SSH 重新連線後仍然存在;復原絕不會自動重複最終處理。在明確儲存復原的緩衝區之前,請先將其與重新開啟的目標進行比對。 `files.read` 保護讀取、檢查與控制代碼釋放。`files.write` 保護儲存與復原清理。對於既有的外掛程式,`writeText` 在失敗時仍會拒絕;`saveText` 則傳回結構化的 `{ ok, value/error }` 封裝,因為 Electron 會捨棄被拒絕之 Error 物件上的自訂屬性。舊式的三參數 `writeText` 可使用單一已開啟文件的基準。同一路徑上有多份作用中的文件時需要 `editId`;不明確的舊式呼叫會安全地失敗,而不會選用另一份文件的基準。外掛程式應釋放不再使用的控制代碼。 外部 UI 必須立即顯示儲存狀態、在失敗時讓緩衝區保持未儲存、僅在發生權限/驗證失敗後才提供 sudo、在不覆寫的情況下顯示衝突,並在連線中斷或結果未知後提供復原。它必須以捷克語、德語和英語翻譯這些狀態。建議的狀態文字: | 狀態 | 捷克語 | 德語 | 英語 | | --- | --- | --- | --- | | 儲存中 | Ukládání… | Wird gespeichert… | Saving… | | 已確認 | Uloženo | Gespeichert | Saved | | 失敗 | Uložení selhalo | Speichern fehlgeschlagen | Save failed | | 需要 sudo | Vyžadováno oprávnění sudo | sudo-Autorisierung erforderlich | Sudo authorization required | | 衝突 | Původní soubor byl změněn | Originaldatei wurde geändert | Original file changed | | 連線中斷 | Spojení bylo přerušeno | Verbindung unterbrochen | Connection lost | | 未知 | Výsledek uložení není znám | Speicherergebnis unbekannt | Save result unknown | | 復原 | Obnova je dostupná | Wiederherstellung verfügbar | Recovery available | ## 延遲的定義與生命週期 **Ping** 是向 SSH 所使用之已解析 IP 傳送 ICMP echo 要求、並由用戶端實際收到回覆所回報的延遲。Core 以固定的參數陣列非同步呼叫平台的 ping 執行檔,不使用 shell,也不要求以管理員身分啟動。Linux、macOS(包括 `ping6`)與 Windows 的參數會分別選擇。ICMP 遭封鎖、缺少執行檔、逾時、輸出無效,或 Windows 只提供上限值的回覆(例如 `<1ms`)時,都會產生「無法使用」的值,而不是編造一個數字。 **SSH RTT** 是透過既有的已驗證 SSH 用戶端執行固定 `true` exec 要求所花費的單調 `performance.now()` 時間長度。它包含通道建立、伺服器處理與回應傳遞;它既不是純粹的網路 RTT,也不是 SSH 登入所需的時間。翻譯後的指示器提示會說明這項差異。 終端機一連線,監控就會開始。各項探測會同時且非同步地執行,要求/程序逾時為五秒。下一次取樣會排定在上一次取樣完成後十二秒進行,以避免重疊。中斷連線會中止 ICMP、清除計時器、取消待處理的 SSH 作業並清空數值。紀錄身分檢查可防止舊連線在重新連線後繼續發布資料。ICMP 失敗絕不會改變 SSH 連線狀態。 延遲資料包含在既有的 `state` 事件 / `ssh:state` IPC 承載資料中: ```js latency: { pingMs: 24.1, sshRttMs: 31.25, measuredAt: "…" } ``` 無法取得的數值為 `null`。精簡的終端機指示器使用毫秒、在地化的「無法使用」文字,以及捷克語/德語/英語標籤。中斷連線時會隱藏。不需要額外的轉譯器執行能力或 IPC 要求。 ## 驗證與作業限制 - 在 `app/` 中執行:`npm ci`,接著執行 `npm test`。 - 最終本機結果:**179 項測試;163 項通過、0 項失敗、16 項略過**。所有略過的項目都是此 macOS 主機上的 Linux 檔案系統案例。`npm ci`、Python 語法編譯與 `git diff --check` 皆已成功完成。 - 測試涵蓋 Core 的儲存/復原、sudo 通訊協定與密碼遮蔽、對惡意路徑的實際 POSIX shell 跳脫、衝突、上傳、清理失敗、不確定的結果、同時開啟的文件、IPC 權限與延遲生命週期。 - Linux 檔案系統測試會執行隨附的 Python 進入點,並使用私有的測試帳戶家目錄查詢:擁有者/模式/xattr、POSIX ACL、符號連結/硬連結、FIFO、上傳與中繼資料失敗、替換失敗、目標競爭、替換後結果未知以及復原。跨檔案系統的暫存會在可用時使用 `/dev/shm` 與另一個檔案系統。這些測試在非 Linux 主機上會略過;缺少 ACL/獨立檔案系統功能時會明確略過。 - 新增的 Linux GitHub Actions 工作會執行完整的測試套件,並以 root 身分重複檔案系統案例。此工作流程尚未從此工作樹觸發執行。 - 本機驗證在 macOS 上進行。Python 原始碼語法使用 Command Line Tools 隨附的 Python 執行檔直接檢查。一次實際的 macOS 回送介面 ICMP 冒煙檢查傳回 `0.071 ms`。本機並未測試任何即時遠端 SSH 伺服器、Windows 執行環境、Linux 執行環境、SELinux 部署或外部 Monaco UI。 - 遠端文字編輯現在需要 Linux,以及支援以描述元為基準之相對檔案系統/xattr 操作的 Python 3。缺少直譯器時會明確回報錯誤。此 Core 會刻意拒絕不安全的中繼資料、連結與目錄設定。 - 鎖定僅具建議性質:在最後一次基準檢查與不可分割的重新命名之間的短暫時間內,不配合的程序仍可能產生競爭。Linux 並未提供通用的不可分割「內容雜湊比較並替換」操作。檢查會在該時間範圍之前偵測變更;保留的工作副本則支援之後的復原。 - 復原資料會保留在遠端帳戶中,直到完成經確認的清理為止。未完成、有衝突以及無法檢查的日誌絕不會在未告知的情況下被捨棄。復原檔案不會自動過期,也沒有強制覆寫的選項。 - `npm ci` 回報了 17 項相依套件稽核結果(9 項中度、8 項高度)。此次變更不會改變相依套件或應用程式的更新信任模型。 使用者要求在既有的工作樹(目前為 `feature/hash-checksum-plugin`)中保留變更;並未切換分支,也未進行無關的清理。 ## 此工作中變更的檔案 - `src/main/files/remoteFileService.js` - `src/main/files/remoteEditor.py`(新增) - `src/main/sessions/sessionManager.js` - `src/main/sessions/latencyMonitor.js`(新增) - `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`(新增) - `test/remoteEditorProtocol.test.js`(新增) - `test/remoteEditorLinux.test.js`(新增) - `test/fixtures/remoteEditorLinux.py`(新增) - `test/latencyMonitor.test.js`(新增) - `README.md` - `docs/remote-editor-latency.md`(本文件) - `../.github/workflows/remote-editor-tests.yml`(新增)