# 端到端密封工作階段（sealed sessions）

> HTML: https://remote.ivjn.us/docs/zh-TW/sealed-sessions/

Android Console 的端到端加密入口位於設定頁，裝置操作可在頁面內選擇目標。Android 與 Web Console 的帳號設定支援設定或變更密碼、綁定或更換信箱；新信箱完成驗證前原信箱仍有效。Console 預設以空口令將根私鑰備份至 Exchange；不會自動覆寫已有口令備份。刪除備份會關閉本用戶端的自動儲存。空口令表示 Exchange 或資料庫持有者可解密根私鑰。手動還原會先核對工作區根，再永久儲存並同步授權。

目標：即使 Exchange 主機被攻破或營運方好奇，也**讀不到**命令、輸出、PTY 位元組、檔案內容、螢幕截圖和 ubus 資料，也**無法決定或偽造**誰能檢視/管理裝置。Exchange 退化為「僅轉送密文 + 路由後設資料」的中繼。

定義：`crates/proto/src/seal.rs`（線路型別、本地授權規則）、`crates/proto/src/seal/crypto.rs`（工作階段密碼學，僅 ring）、`crates/proto/src/seal/backup.rs`（通關密語備份：Argon2id + ChaCha20-Poly1305，agent 不連結）、`crates/agent/src/seal.rs`（裝置端）、`crates/cli/src/seal.rs`（CLI 端）、`shared/seal-session.js`（Web Console 與 Android Console 共用的工作階段實作，@noble/curves · @noble/hashes · @noble/ciphers）、`shared/seal-backup.js`（同一套庫上的 Argon2id 通關密語備份）。跨語言測試向量：`shared/seal-vectors.json`（JS 生成，Rust `javascript_vectors_interoperate` 驗證簽章/HKDF/密文逐位元組一致）、`shared/seal-rust-vectors.json`（Rust 裝置回應，JS `shared/seal-session.test.js` 完成交握並解密）。

`seal approve` 仍會自動發布授權。之後執行 `seal grant --publish`（或登入後的預設發布）時，若已有相同簽發者公鑰、接收者公鑰、裝置與 level 的未到期授權，則提示「已發布」並跳過，不輸出新權杖。可用 `--renew-before DAYS` 主動續簽即將到期的授權，`--no-publish` 仍可離線簽發。此檢查防止依序重試造成重複，不提供跨客戶端並行發布的交易保證，也不刪除歷史重複記錄。`seal show` 會建立本機身分，並於登入時登記公鑰；試驗身分請使用獨立 profile。


## 威脅模型

| 專案 | 密封會話的保護與邊界 |
|---|---|
| 命令列、stdout/stderr、退出碼 | **不可見**；審計只記 `sealed:true`、操作員公鑰、授權級別與到期時間、結束結果 |
| PTY 鍵入與螢幕輸出 | **不可見**，密文幀不進審計 |
| 檔案內容、截圖、介面樹、ubus 引數與結果 | **不可見** |
| 誰能做什麼（超級管理員/管理員/運維/只讀、僅檢視/可管理） | **裝置判定**：只認本機固定的工作區根公鑰簽發的授權；Exchange 無法簽發、擴大或偽造 |
| 冒充裝置 | 不可能：原生裝置身份金鑰簽名 + CLI 固定（pin）裝置公鑰；瀏覽器擴充套件驗證工作區根鏈證明與應答簽名 |
| 向會話注入按鍵/命令 | 不可能：AEAD 認證 + 嚴格遞增計數器；agent 丟棄發往密封會話編號的任何明文 |
| 重放、重排、丟包、截斷 | 被檢測並關閉會話（fail closed） |
| 仍可見的後設資料 | 裝置 ID、操作員賬號、會話編號、時間、包大小與數量、是否 stdout/stderr 不可見（型別在密文內） |
| 仍可做的事 | 拒絕服務：不轉發、斷開、關閉會話（`sealed_close`）。這是可用性問題，不是許可權 |

不在保護範圍：裝置本身被攻破；操作員電腦被攻破；首次連線（TOFU）時 Exchange 已經在冒充裝置——可透過帶外比對指紋避免。所有裝置操作只接受密封會話；固定指令目錄的定義和瀏覽器根鏈授權的邊界見下表。

## 路徑覆蓋與設計

| 路徑 | 密封會話行為 | 裝置端授權與邊界 |
|---|---|---|
| HTTP `POST /api/devices/{id}/exec` | **明確拒絕**，HTTP 409，提示使用 `open_sealed` WebSocket 會話 | 即使 Exchange 忽略策略，裝置仍拒絕明文 `exec_req` |
| HTTP 固定指令 `/commands/{cmd}/run` | **明確拒絕**，同樣提示密封 WS | 伺服器資料庫裡的指令不是裝置端授權 |
| Web Console / CLI 固定指令 | 從目錄取模板，由操作員在密封 WS 內傳送 `exec_req`，輸出也密封 | 裝置驗證 hello、根簽名 grant、撤銷、有效期和 `manage` 級別；`view` 不能執行模板 |
| LAN `/ws/operator` | 只接受密封會話，hello 內聯 grant 與證書鏈；沒有 Bearer 令牌 | 與 Exchange 相同的根鏈校驗和 view/manage。外層只檢查私網地址與 Origin。`/api/devices` 只返回裝置 ID 和 seal 公鑰/模式 |
| 瀏覽器 Agent / 原生密封 routes | 使用 `open_sealed` / `sealed_input` / `sealed_event`，拒絕舊 browser channel | 擴充套件驗證操作員根簽名 grant / 證書鏈；CLI / Console 驗證擴充套件 `SealAccept.peer` 的根鏈，並要求其公鑰等於應答簽名公鑰 |
| CLI `exec-attach` / `exec-hold-tail` / `exec-hold-cat` | 原生密封請求、輸出幀與結束事件 | 需要 `manage`；密封作業不能被明文讀取/附加 |
| CLI `sensors --live` | 每次取樣使用獨立密封會話（單操作/會話），Ctrl-C 結束重新整理 | `view` 即可；不把多個取樣塞進單操作隧道 |
| CLI `net` | `net_req` 和 `net_resp` 密封，包括 URL、headers、body、結果 | 需要 `manage` |
| CLI `device update` | `update_req`、所有進度和最終狀態密封 | 需要 `manage`；CLI 頂層 `update` 更新本機客戶端，不是遠端會話 |
| exec / PTY / file / ubus / Android / sensors 單次 / hold list、kill | 已有原生密封實現，保持不變 | 使用同一 `required_level` 和裝置端 dispatcher |
| 裝置列表、指標、線上狀態、Exchange sessions 列表 | 路由/監控後設資料，不是密封裝置操作 | Exchange 可見；不作為裝置操作授權 |

### HTTP 與固定指令

原生密封操作現已支援純 HTTP 呼叫端。使用 `remote-cli --transport http` 或 `REMOTE_TRANSPORT=http`。`POST /api/sealed/connections` 建立連線；`POST /input` 提交簽章交握或密文，`GET /events` 長輪詢結果，`DELETE` 關閉連線。用戶端繼續負責端到端加解密與裝置授權驗證。原有裝置操作介面仍回傳 409，裝置端連線保持不變。

https://remote.ivjn.us/sealed-http.md


固定指令的建立、編輯和列表仍是 Exchange 資料庫管理面，**目錄中的模板文字、名稱和描述不是密封資料，也不是根簽名的命令 allow-list**。控制檯和 CLI 在持有金鑰時把取出的模板作為普通密封 exec（非 hold）執行；裝置只信本機策略和操作員 grant，不信目錄 ID、伺服器角色或“這是固定指令”的說法。僅持有 `view` grant 的成員不能執行固定指令；需要根授予 `manage`。目錄被篡改不能擴大 grant，但 `manage` 本就允許任意 exec，所以請核對模板，勿把密碼/令牌寫入伺服器目錄。輸出僅在操作員本地解密並展示；該執行路徑審計僅記 sealed 後設資料，不記錄命令和輸出。

### LAN 與用戶端選擇

CLI 裝置會話只使用端到端密封，不依賴 Exchange 上報 `seal_key`、capability 或策略提示。缺少或被刪除的能力資訊不會降級為明文。讀取本地固定資料庫失敗時直接報錯，不忽略損壞的信任記錄。

密封 held / 非 held exec 和檔案工作在發布到全域工作目錄前即標為密封，不能借助“尚未處理首條出站事件便關閉工作階段”的競爭條件讀取明文預覽。裝置記錄不記錄 HTTP 探測的目標或方法、ubus 物件或方法。

LAN 複用原生 envelope、簽名 transcript、計數器、AEAD、裝置固定和撤銷規則，沒有另造密碼演算法。LAN 出站先經過 `Tunnels::seal_outbound` 再翻譯為 `OperatorDown`；密封會話編號的明文注入被 device dispatcher 丟棄。連線關閉仍分離 held exec、取消其它操作，晚到輸出被 fenced id 丟棄。沒有區域網令牌：客戶端把離線快取的 grant 和證書鏈放進 hello，裝置按與 Exchange 相同的 `authorize` 決定 view/manage。外層只檢查私網地址和 Origin。明文會話在 LAN 上始終拒絕，不論歷史策略值為何。託管網頁控制檯仍連線 Exchange。

可選裝置資訊 `info.seal_mode`（`allow` / `require`）、`seal_key` 和 `sealed` capability 保持相容。它們不是授權，也不是可信裝置公鑰；最終由裝置執行策略。CLI、Web Console、Android Console 的命令、終端、檔案、ubus、感測器、作業、網路探測、桌面、裝置更新、固定指令和瀏覽器操作全部使用原生密封會話，不提供明文路徑。策略值 `allow` 也拒絕明文操作。握手、裝置固定、grant、解密失敗即停止，不重試明文。線上策略變更後，LAN 資訊即時重新整理，雲端資訊在重連註冊時重新整理。

### 瀏覽器 Agent 的工作區根鏈認證

瀏覽器擴充功能回報 `sealed` capability、Ed25519 `seal_key` 與 `seal_mode: require`。配對或裝置設定後，透過裝置憑據及簽章持鑰證明呼叫 `POST /api/seal/device`；Exchange 依裝置決定工作區並綁定公開金鑰。持有可信工作區根私密金鑰的用戶端為此金鑰簽發僅限該裝置的 view grant。登記與裝置 bearer 用於接入及取得簽章憑據，不能取代工作階段的憑證驗證。

擴充功能驗證操作人員的簽章、根憑證鏈、裝置範圍、權限、有效期及本機撤銷原則，再簽署 `SealAccept` 並附上自身簽署的 `SealHello` 作為 `peer`。CLI 的 `finish_workspace_operator` 與共用 `finishBrowser` 驗證證明，要求 `peer.operator_key == accept.device_key`，並驗證回應簽章。證明缺少或無效即關閉；雙方驗證通過後自動建立工作階段。Raw CDP 另需本機逐次確認或依操作人員金鑰、伺服器和裝置儲存的無人值守許可；view 無法使用 CDP。

擴充功能依伺服器 origin、工作區與裝置儲存授權記錄；本機身分種子僅對可信擴充功能內容開放。首次工作區根在本機固定，變更時拒絕。CLI 與 Console 使用通用 seal 身分及工作區根鏈，不需獨立的瀏覽器 pin 編輯器。根金鑰簽發、憑證權限與裝置接入憑據分別檢查；原生裝置公開金鑰 TOFU 仍單獨適用。

**安全邊界：** 首次根 TOFU 不能識別惡意中繼提供的假根，應頻外核對；PoP 只證明持有金鑰，不證明成員獲批。根用戶端的自動批准依賴名冊，敏感部署應稽核新金鑰。保護擴充功能 profile / 種子與網頁控制台發布鏈；端點被攻破或向惡意操作人員開放 full CDP 不在密封保護內。刪除目錄記錄不撤銷已快取的簽章憑據；裝置本機撤銷與到期規則仍重要。Exchange 仍可觀察路由後設資料、扣留憑據和拒絕服務。

**身份不可用時：** 裝置身份無法載入或寫入時密封會話不可用，所有明文操作仍被拒絕（失敗即關閉）。無策略檔案、策略損壞或舊策略值都不會開啟明文路徑；必須在裝置本機修復金鑰儲存。

HTTP 連線須固定至建立它的 Exchange 執行個體，90 秒無活動後到期。循序提交輸入，重試時保持序號與密文一致；處理結果後以 `after` 確認。Web Console、行動端與 LAN 保持現有傳輸。執行預設指令仍需 `manage` 權限。

### 迴歸驗證

Rust 覆蓋裝置 grant 的實際握手、exec/attach/tail/cat/sensors/net/update 的密封結果、所有策略值下的明文拒絕、LAN WebSocket 的密封握手/幀/策略修改、HTTP 409 指引、browser require 關閉路由，以及客戶端與裝置資訊的密封強制不變條件。JavaScript 覆蓋原有 Rust/JS 雙向向量、操作 envelope、固定指令密封輸出與拒絕後不回退、瀏覽器配置/註冊/裝置拒絕/本機策略變化和客戶端固定錯誤。測試中的程序級 seal singleton 在隔離子程序內初始化，避免併發測試修改彼此的裝置策略。

```sh
CARGO_PROFILE_DEV_DEBUG=0 CARGO_PROFILE_TEST_DEBUG=0 cargo test --workspace
cargo fmt --all --check
CARGO_PROFILE_DEV_DEBUG=0 CARGO_PROFILE_TEST_DEBUG=0 cargo clippy --workspace --all-targets -- -D warnings
npm test --prefix shared
npm test --prefix website/console
npm test --prefix browser-agent
node --test scripts/test-pages-worker.mjs
```

JavaScript 使用 Node 24+。新增密封 LAN 迴歸透過記憶體 duplex 流完成真實 HTTP Upgrade / WebSocket，能夠在禁止網路 socket 的 sandbox 中驗證握手、幀和 所有策略值下的密封強制行為。已有的 loopback WS/TLS/HTTP 測試仍需要允許繫結埠的環境，否則會以 `Operation not permitted` 失敗，不能作為全套測試透過的證據。網路探測和更新路徑測試驗證密封成功/錯誤結果，不下載或安裝更新。

**Web Console 的特殊風險：** 網頁程式碼本身由 Pages 網站下發，被攻破的網站/CDN 可以下發惡意腳本讀走本頁的明文或用本瀏覽器的金鑰簽章（但拿不走不可匯出的私密金鑰）。對 Exchange 營運方保密的前提是你信任靜態網站的發布鏈；對此不放心時請用 CLI 或 Android Console。

## 身分與金鑰

| 金鑰 | 型別 | 儲存位置 | 誰能看到公開金鑰 |
|---|---|---|---|
| 裝置身分 | Ed25519 | agent：`agent.seal.key`（0600）。狀態檔案在 tmpfs（`/var/run`、`/run`、`/tmp`）時改存 `/etc/remote-agent/`，可用 `REMOTE_SEAL_DIR` 覆蓋；OpenWrt 已列入 conffiles | 註冊時 `info.seal_key` 上報（僅供顯示，CLI 不信任這份副本） |
| 操作人員身分 | Ed25519 | CLI：`config.toml` 同目錄 `seal-identity.pk8`（0600）；Web Console：IndexedDB `remote-seal` 中**不可匯出**的 WebCrypto Ed25519 `CryptoKeyPair`（瀏覽器不支援 WebCrypto Ed25519 時退回 IndexedDB 中的隨機種子）；Android Console：`expo-secure-store`（Android Keystore 加密）中的種子 | 交握時傳送 |
| 工作區根 | Ed25519 | 就是超級管理員用戶端上的那把操作人員金鑰。Web Console 上必須是可匯出的種子（超級管理員生成根時會把不可匯出的 WebCrypto 金鑰換成本地種子）；可匯出為 `rsk1.` 文字、由 Console 預設以空口令備份到 Exchange | `rsr1.` 根記錄（Exchange 公佈、裝置固定） |
| 工作階段金鑰 | X25519 臨時金鑰 → HKDF-SHA256 → 每方向一把 ChaCha20-Poly1305 | 只在記憶體，工作階段結束即丟棄（前向保密） | 無 |

操作人員私鑰保存在本機。Console 預設上傳根私鑰的空口令備份；Exchange 或資料庫持有者可以解密。可手動設定備份口令，或刪除備份以關閉本用戶端的自動保存。

## 授權（Exchange 不是權威）

裝置只信任**一把工作區根公開金鑰**。本地原則 `agent.seal.json`：

```json
{"mode":"require","root_key":"<base64 Ed25519>","revoked_keys":[]}
```

agent 對每個 `sealed_open` 自行判定級別：

1. 裝置還沒有固定根 → 拒絕（`no workspace root pinned on this device`）。
2. 操作人員公開金鑰在 `revoked_keys` → 拒絕。
3. 操作人員公開金鑰就是 `root_key` → `owner`（可管理 + 修改本地原則）。
4. 攜帶授權 `grant`：簽章有效、`operator_key` 與交握一致、`device_id` 為本機或 `*`、在有效期內（±5 分鐘時鐘偏差）。簽章者是已固定根，或者 hello 帶一條 `rsc1.` 憑證鏈（`hello.chain`，不包含在交握簽章裡）：從根簽發的那張排到 grant 的簽章者，每張都能繼續簽發，路徑長度、級別、裝置範圍和到期時間都不超過上一張。授權級別是 `view` 或 `manage`，永遠不能是 `owner`。
5. 其他 → 拒絕。

## 憑證鏈約束

裝置認的不是單獨一張中間憑證，而是一條能驗回已固定工作區根的 `rsc1.` 鏈（`hello.chain`，從根簽發的那張排到 grant 的簽章者）。鏈的層數**不設業務上限**（組織可以有 10 層以上），只有一道防 DoS 的寬鬆護欄：`MAX_CERT_CHAIN` = **64** 張，且整條鏈的編碼大小（每張憑證規範 `rsc1.` 權杖長度之和）`MAX_CERT_CHAIN_BYTES` ≤ **64 KiB**（`crates/proto/src/seal.rs` / `shared/seal-cert.js` 的 `check_chain_limits` / `checkChainLimits`，裝置、Exchange、CLI、Web、Android 規則相同）。超出直接拒絕。

每張憑證還有自己的 `max_path_len`（0–255），是頒發者**可選**的約束，限制它下面還能再掛幾張憑證；`seal cert issue` 不帶 `--max-path` 時預設不限（取頒發者剩餘的全部深度，255 實際等於不限）。驗證時，這張之後剩餘的張數不能超過 `max_path_len`。根給管理員簽發的中間憑證 `max_path_len` 為 0：管理員可以籤 grant，不能再簽發下一張憑證。`can_issue` 為假也不能繼續簽發。

子憑證必須落在父憑證裡面（裝置上的 `cert_within_parent` / `verify_chain`）：

- **級別**不超過父級。根是 `owner`；被授予的級別只能是 `view` 或 `manage`，且不能高於父憑證。`owner` 不能出現在憑證或 grant 裡。
- **裝置範圍**是父級的子集。只有父級包含 `*` 時，子級才能覆蓋全部裝置；否則子級的每臺裝置都必須在父級清單中。子級寫成 `*` 僅當父級也是 `*`。
- **到期時間**不能晚於父憑證。根沒有到期上限。grant 同樣不能晚於簽發它的那張憑證，裝置也不能超出那張憑證的範圍。

**撤銷一張中間憑證會級聯到它下面的一切。** 裝置把鏈上每一張的 `subject_key` 和 `serial` 對原則裡的 `revoked_keys` 檢查。中間那張被撤銷後，任何仍然帶著它的更長鏈都失敗，因此它簽發的下級憑證、以及那些憑證籤出的授權，一併失效。從 Exchange 刪除憑證只停止繼續分發，裝置上已經持有的副本在過期前仍然有效；要讓下面整條鏈立刻失效，把該憑證的主體公開金鑰（或序列號）寫進裝置原則的 `revoked_keys`。

級別與操作的對應（`proto::seal::required_level`）：`view`（僅檢視）允許感測器、hold 作業清單、Android 唯讀動作（status/tree/screenshot/find_text）；其他一切（exec、PTY、檔案、ubus、網路探測、自更新、Android 輸入）需要 `manage`。

授權是根簽章的 JSON，Exchange 篡改任何欄位（例如 view→manage、換裝置、延長有效期、換被授權人）都會讓簽章失效；交握簽章還把授權綁定到本次工作階段。Exchange 側的帳號角色只決定「能不能把訊息路由到這臺裝置」（租戶可見性），不會擴大或縮小密封工作階段的權限。沒有裝置級「認領」「所有者」「共同所有者」：唯一的 `owner` 就是根。

**裝置怎樣固定根：**

- **TOFU：** 支援密封工作階段的裝置連上 Exchange 後，Exchange 下發 `seal_root {token, rotation?}`（`rsr1.` 自簽章根記錄）。裝置驗證自簽章；本地還沒有根時固定它並寫入 `agent.seal.json`。
- **輪換：** 已固定根之後，Exchange 公佈的根變了，裝置只在同時收到一份**由已固定根簽章**的 `rsx1.` 輪換記錄（`old_root` = 已固定根，`new_root` = 新根，同一工作區）時改信新根；否則忽略並記記錄。根也可以在密封工作階段裡用 `seal_policy_set {root_key}` 改（只有當前根有 `owner` 級別）。
- **本機設定：** `remote-agent seal root <key>` 直接寫入根；`remote-agent seal forget-root` 清掉，下次連上 Exchange 時重新 TOFU（重啟 agent 生效）。

**風險：** TOFU 意味著裝置第一次連上時 Exchange 若已被攻破，它可以提供假根給裝置。擔心這一點時，在裝置上線前用 `remote-agent seal root <key>` 預先指定根公開金鑰（指紋見 `remote-cli seal root`）。固定之後 Exchange 就換不掉根了。

**撤銷：** 根 `remote-cli seal policy <设备> --revoke <key>`，或在裝置上 `remote-agent seal revoke <key>`。根自己不能被撤銷。授權自帶有效期（預設 90 天）。從 Exchange 刪除一份已發布的 grant 只是不再分發；已經匯入本地的副本在過期前仍有效，必須以裝置原則撤銷。

## Exchange 作為授權中繼（不可信）

同事不必再把 `rsg1.` 權杖或公開金鑰抄到聊天視窗：Exchange 提供工作區級的**操作人員公開金鑰目錄**和 **grant 中繼**。這不改變威脅模型——Exchange 仍然看不到明文，也不能簽發、擴大或偽造授權。它只儲存：

1. 操作人員用本機私密金鑰證明持有的 Ed25519 公開金鑰（`POST /api/seal/keys`）
2. 已經簽好的 `rsg1.` 權杖原文（`POST /api/seal/grants`）
3. 已經簽好的 `rsc1.` 中間憑證原文（`POST /api/seal/certs`）。Exchange 驗證頒發者的 Ed25519 簽章，但不能偽造憑證。`GET /api/seal/certs/chain?subject_key=` 沿頒發關係取回（受同一 64 張 / 64 KiB 護欄限制）權杖，根簽發的在前

裝置授權仍然只認已固定根，或一條能驗回這把根的 `rsc1.` 憑證鏈。Exchange 解析 token 只是為了按 `owner_key` / `operator_key` / 到期時間建立索引，**從不把上傳當作授權**。最壞情況是營運方扣留權杖（對方拿不到 grant，表現為 `no grant`），不能把 `view` 改成 `manage`，也不能冒充根。

任何工作區成員都可以上傳 grant：權杖自帶簽章，上傳不是授權行為；只有根簽章的 grant 對裝置有效。CLI 在已登入時預設 `--publish`；Web / Android 控制台在「簽發並行布」時走同一介面。

金鑰登記的 possession proof 綁定：

```text
remote-seal-key-register:v1:<workspace_id>:<operator_id>:<public_key>:<issued_at_ms>
```

由該公開金鑰做 Ed25519 簽章，`issued_at_ms` 須在伺服器時間 ±10 分鐘內。CLI 在 `login` 和 `seal show` 時盡力登記；控制台在登入後登記。

操作人員在密封操作前若本地沒有匹配 grant，會盡力 `seal sync`；交握因 `no grant` 被拒時再同步一次並重試一次。

簽發者用戶端上線時會自動續簽自己簽發、即將過期的已發布授權。簽章仍在本機完成；Exchange 只儲存新的 `rsg1.` 權杖並刪除舊記錄，不能改級別或換被授權人。

- **何時執行：** CLI 在登入成功後，以及任意已登入命令開始時（`seal renew` 走手動路徑，不再套一層自動續簽）。Web Console 在登入並進入工作區時（頁面載入後若工作階段已存在，同樣會進入工作區）。Android Console 在登入後，以及應用程式回到前臺時。未登入，或這把公開金鑰沒有已發布的 grant 時，不續簽。
- **續簽規則：** `GET /api/seal/grants?owner_key=<本机公钥>`。將在 30 天內到期的授權，用原來的 `device_id`、`operator_key`、`level` 重新簽發 90 天，發布成功後再 `DELETE` 舊 grant。若 `GET /api/seal/keys` 成功，公開金鑰目錄裡已經沒有的 `operator_key` 會跳過；目錄請求失敗時不按目錄過濾。
- **節流：** Web/Android: 同一簽發公開金鑰最多每 12 小時嘗試一次。CLI 記在配置目錄的 `seal-renew-state.json`，Web Console 用 `localStorage`，Android Console 用 SecureStore。失敗不影響當前操作：CLI 只在 stderr 打一行警告，控制台使用 `console.warn`。續簽成功時 CLI 在 stderr 打一行簡短說明。持有根的 Web Console 進入工作區時會先簽發一輪，Android 開啟「端到端加密」時也會先簽發一輪；這兩次不受 12 小時節流。同一次停留裡之後的重新整理才節流。
- **CLI 續期：** 依伺服器及簽發公開金鑰分別記錄成功時間；成功後 12 小時內略過續期網路請求，失敗或中斷只退避 5 分鐘。整輪續期最多等待 30 秒，錯誤包含階段及耗時，主命令失敗時立即取消背景續期。未完成的裝置撤銷不受成功節流影響，並使用獨立的 4 秒預算。可執行 `remote-cli seal renew` 明確續期。 登入成功後會強制重新整理一次。
- **根用戶端：** 持有工作區根的超級管理員用戶端用下一節的按角色簽發代替這次通用續簽，以免把剛刪除的授權又簽回去。
- **手動：** 仍可用 `remote-cli seal renew`（預設 `--before 30 --days 90`），規則與自動續簽相同。

```bash
# 按工作区用户名授权对方每一把已登记的钥匙，并发布到 Exchange（默认 90 天）
remote-cli seal grant --device lab-router --to alice --level view
# 只打印 token，不上传
remote-cli seal grant --device lab-router --operator-key <KEY> --no-publish
remote-cli seal keys          # 工作区公钥目录
remote-cli seal sync          # 拉取签给自己钥匙的 grant 并导入本地
remote-cli seal renew         # 手动续签，默认 --before 30 --days 90
```

## 工作區超級管理員是信任根

工作區根就是超級管理員用戶端上的 Ed25519 金鑰。裝置只信任這把根公開金鑰，以及能沿 `rsc1.` 憑證鏈驗回這把根的 `rsg1.` 授權；根用戶端按帳號角色自動簽發、續簽成員授權。Exchange 仍然不可信：它只儲存自簽章的 `rsr1.` 根記錄、`rsx1.` 輪換記錄和 `rsi1.` 邀請，並檢查格式與 Ed25519 簽章，**不決定誰被批准**。惡意 Exchange 可以扣留記錄，或在名冊裡塞進假成員；它不能偽造根簽章。CLI、Web 與 Android 上自動批准預設開啟，名冊裡的新公開金鑰會被這些根用戶端直接簽章。只有明確關閉自動批准後，從未批准、也沒有被這把根發布過的新公開金鑰才會留在待批清單。

### 信任模型與取捨

- **根公開金鑰首次使用時固定（TOFU）。** 用戶端第一次看到驗證透過的根記錄時，把公開金鑰固定在本地。CLI 寫在 `config.toml` 旁邊的 `seal-root.json`（0600），鍵是 `服务器|工作区 id`。Web Console 用 `localStorage`（`remote.seal.root.<工作区 id>`）。Android Console 用 SecureStore（`remote.seal.rootstate`，按伺服器和工作區放在同一份 JSON 裡）。之後每次核對自簽章和指紋。已固定的根公開金鑰變了就明確警告，**用戶端不會自動改信**（裝置會按 `rsx1.` 輪換記錄自動改信，見上文）。核對指紋後顯式執行 `remote-cli seal root --trust <key>`，或在控制台點「信任新根公開金鑰」。
- **邀請只開啟登記。** 根公開金鑰簽發 `rsi1.`（工作區 id、根公開金鑰、16 位元組 nonce、到期時間）。管理員、操作人員和唯讀帳號的用戶端在登入、後續命令、頁面載入或應用程式回到前臺時拉取邀請，用**已固定**的根公開金鑰驗籤，然後走原有的 `POST /api/seal/keys` 登記自己的操作人員公開金鑰。邀請不是授權。
- **按角色簽發，本地批准新金鑰。** 根公開金鑰所在的超級管理員用戶端讀取 `GET /api/seal/roster`（僅超級管理員；含已禁用成員、角色、`operator_device_grants`、已登記公開金鑰）和自己已發布的 grant，算出期望授權。同一把已登記公開金鑰共用這一組範圍：
  - 超級管理員，以及未受限的管理員：`manage` `*`
  - 受限管理員：按裝置授權，`can_manage` 為 `manage`，否則 `view`
  - 未受限操作人員：`view` `*`，外加每條 `can_manage` 裝置的 `manage`
  - 受限操作人員：按裝置授權 `manage` 或 `view`
  - 未受限唯讀：`view` `*`
  - 受限唯讀：每臺已授權裝置 `view`。唯讀帳號不能操作裝置，因此即使 `can_manage` 也不發 `manage`
  - 已禁用的成員：不發
  - 預設 90 天，剩餘不足 30 天續簽
- **管理員中間憑證。** 根用戶端給啟用的管理員簽發 `rsc1.`（`manage`，`can_issue`，`max_path_len` 為 0，90 天）。未受限管理員的裝置範圍是 `*`；受限管理員是其被委派 `can_manage` 的裝置。只剩 30 天時續簽。尚未批准的新公開金鑰不發憑證。管理員用戶端持有仍有效的中間憑證時，用自己的金鑰和這條鏈，給自己管理的成員簽發 grant，範圍是憑證與裝置委派的交集；若根已經有一份仍有充足有效期的 grant 覆蓋同一操作人員、裝置和級別（根的 `*` 覆蓋具體裝置，`manage` 覆蓋 `view`），就不再次簽發。從 Exchange 撤銷憑證只停止繼續分發；裝置上已有的副本在過期前仍然有效。硬撤銷靠裝置原則裡的 `revoked_keys`。
- **自動批准預設開啟。** Web Console（`localStorage`）和 Android Console（SecureStore）裡，記錄沒有這個欄位時視為開啟（absent = on），只有明確的 `false` 保持關閉，已經關掉的不會被預設邏輯重新開啟。開啟後根用戶端按名冊直接簽章，**較弱**：被攻破的 Exchange 可以塞進假成員並立刻得到簽章。開關文案是「自動批准新公開金鑰」。CLI 與 Web/Android Console 一樣預設開啟：沒有 `seal-approved.json` 檔案或缺少 `auto_approve` 欄位時視為開啟，明確的 `"auto_approve": false` 保持關閉。用 `remote-cli seal approve --no-auto-approve` 關掉、`remote-cli seal approve --auto-approve` 重新開啟；選擇會持久儲存。關掉之後，從未批准過、也沒有被這把根公開金鑰發布過的金鑰進入本機待批清單（CLI `seal-approved.json`，Web `localStorage`，Android SecureStore）。`remote-cli seal pending` 顯示使用者名稱和指紋；`seal approve --all` 或 `seal approve <key>…` 批准後立即簽發。已經批准過的金鑰，以及這把根公開金鑰已經發布過的金鑰，續簽和角色變化自動完成。簽發用戶端自己的公開金鑰不需要再批准一次。
- **開啟就簽發。** 持有根的用戶端在 Web Console 進入工作區、以及 Android 開啟「端到端加密」時立刻簽發，這一輪不受 12 小時節流。同一次停留裡之後的自動重新整理仍節流。改開關或點「批准」會強制再次簽發一次。
- **帳號對話方塊、徽章和失敗行。** Web Console 的開關和待批名單在裝置頁的「帳號」對話方塊裡（工具欄「帳號」）。有金鑰等待批准時，同一工具欄顯示徽章「N 個公開金鑰待批准」，點徽章開啟這個對話方塊。簽發失敗時工具欄有一行狀態：「工作區根簽發失敗：…」。密封權杖和長金鑰材料會從這行裡移除。Android 的開關、同一句徽章和同一行失敗說明在「端到端加密」頁。
- **降級不弔銷裝置上的舊授權。** 成員被移除、禁用或公開金鑰從目錄消失時：從 Exchange 刪除 grant、停止續簽，並盡力在線上且支援密封工作階段的裝置上寫入 `revoked_keys`。角色降級只換一份新 grant，不把金鑰放進 `revoked_keys`。裝置上已經匯入的舊 grant 在過期前仍然有效。根公開金鑰自己永遠不會被放進撤銷清單。沒在 4 秒預算裡做完的撤銷記在本地，下次命令繼續，不受 12 小時簽發節流限制。金鑰重新變成期望授權時，只在已經成功撤銷過的裝置上撤銷撤銷。
- **根記錄自動生成。** 超級管理員帳號建立（`register`/工作區第一個帳號）或之後任何一次超級管理員登入時，若 Exchange 上還沒有根記錄，超級管理員的用戶端（CLI / Web / Android 任一）在本地把自己的金鑰公佈為根，並立刻發一份 90 天邀請。之後邀請缺失或將在 30 天內到期時，隨簽發節流重新整理。`remote-cli seal root --publish` 仍可手動執行。

### 一次性設定

1. 超級管理員註冊或登入任一用戶端：根自動生成並公佈（CLI 在 stderr 列印提示；控制台彈出提示）。
2. 保管根私密金鑰（二選一或都做）：
   - 只自己保管、什麼都不上傳：`remote-cli seal root --export root.rsk`（0600 檔案；`-` 輸出到 stdout），或控制台「匯出根私密金鑰」。
   - 手動設定雲端備份：`remote-cli seal backup`，或控制台「根私密金鑰雲端備份」。通關密語可留空，但**通關密語為空或太弱時 Exchange 能解開根私密金鑰並控制所有裝置**。
3. 裝置只要支援密封工作階段並連上 Exchange，就會自動固定根（TOFU）；更穩妥的做法是上線前在裝置上 `remote-agent seal root <根公钥>`。
4. 新成員登入後會登記公開金鑰。CLI、Web 與 Android 上自動批准預設開啟，根用戶端會直接簽章。若已關掉自動批准：`remote-cli seal pending` 檢視，`remote-cli seal approve --all`（或控制台「批准」）批准後立即簽發。

```bash
remote-cli seal root                       # 显示已固定的根公钥和指纹
remote-cli seal root --export root.rsk     # 导出根私钥（rsk1. 文本）
remote-cli seal root --import root.rsk     # 在另一台机器装上根私钥（或 seal restore）
remote-cli seal root --rotate              # 生成新根，旧根签 rsx1. 轮换，自动按新根重签成员授权
remote-cli seal invite                     # 单独刷新邀请（默认 90 天，--days N）
remote-cli seal pending
remote-cli seal approve --all              # 或 seal approve <key>…；--no-auto-approve 关闭自动批准（保持开启较弱）
```

### 換另一臺機器 / 換根

- **在別的裝置上用根：** 超級管理員登入後，若本機還沒有已公佈的根、且 Exchange 上有 `rsb1.` 雲端備份，CLI / Web / Android 會**自動嘗試恢復**（見「登入時自動恢復」）。否則用戶端提示手動匯入：`seal root --import <文件|->`、控制台貼上 `rsk1.` 後「匯入根私密金鑰」，或從 Exchange 恢復加密備份（`seal restore` / 控制台「從 Exchange 恢復」）。匯入的就是當前根時只是裝到本機，不改任何記錄。
- **輪換：** `seal root --rotate`（控制台「生成新根並輪換」）或匯入一把**不同於當前根**的 `rsk1.` 金鑰。本機持有當前根時，用戶端用舊根簽一份 `rsx1.` 輪換記錄，與新的 `rsr1.` 一起 `PUT /api/seal/root`；Exchange 推給線上裝置，裝置驗證輪換由已固定根簽章後改信新根，離線裝置下次連上時收到。舊根批准過的成員在本機繼續算已批准，隨後用新根重新簽發全部成員授權（90 天）。舊本地金鑰保留為 `seal-identity.<毫秒>.previous.pk8`。輪換後請重新匯出或備份新根。
- **舊根已經丟失：** 無法籤輪換記錄。用戶端仍會公佈新根，但已固定舊根的裝置不會接受；需要在每臺裝置上執行 `remote-agent seal root <新根公钥>` 並重啟 agent。
- **超級管理員移交：** 見「超管交接 = 根憑證交接」。只改帳號角色不夠，裝置信任的是根私密金鑰。

## 登入時自動恢復根私密金鑰

超級管理員登入成功後，**在建立本機密封身分之前**，CLI / Web Console / Android Console 會按同一套純決策表（Rust `decide_auto_restore`，JS `decideAutoRestore`）嘗試從 Exchange 的 `rsb1.` 備份裝回已公佈的工作區根：

1. **跳過：** 非超級管理員；本機已持有與已公佈根相同的金鑰；雲端備份已關閉（CLI：`REMOTE_SEAL_CLOUD_BACKUP=0`；Web：`localStorage.seal.cloudBackup=off`；Android：SecureStore `remote.seal.cloudBackup=off`）；Exchange 上沒有已公佈的根。
2. **拉取一次備份：** `GET /api/seal/backup`（受每小時次數限制）。沒有備份時提示根在別處，不阻塞登入。
3. **空通關密語靜默嘗試：** 先用空字串解密。成功且解密出的公開金鑰等於已公佈根、且本機沒有另一把不同金鑰時，裝入本機並固定為工作區根。
4. **否則提示通關密語：** CLI 再試 `REMOTE_SEAL_BACKUP_PASSPHRASE`，TTY 上隱藏輸入；Web / Android 只發**非阻塞**提示，請到密封頁輸入通關密語後點「從 Exchange 恢復」。不彈阻塞對話方塊，登入照常完成。
5. **安全邊界：** 從不靜默覆蓋本機已有的另一把密封金鑰（需 `seal restore --force` 或控制台確認恢復）；解密出的公開金鑰必須等於已公佈根；失敗只警告，不讓登入失敗；從不列印通關密語、種子或 `rsb1.` 密文。
6. **風險提示（非阻塞）：** 裝上根後提示「多臺機器持有根會增加洩露風險」；若空通關密語解開成功，再提示 Exchange 能解開根私密金鑰。

## 超管交接 = 根憑證交接


超級管理員這個帳號角色，和裝置信任的工作區根，是兩回事。只把角色交給對方，新超級管理員沒有根私密金鑰，裝置不會改信他。交接就是把**根私密金鑰**交給對方，再立刻輪換，讓交出去的那一份作廢。

1. 現任超級管理員在本機持有已公佈的根時轉讓：`POST /api/superadmin/transfer`，本文 `{operator_id, root_handover?}`。`root_handover` 是通關密語加密的 `rsb1.`，演算法與雲端備份相同（Argon2id 64 MiB、t=3、p=1，ChaCha20-Poly1305）。通關密語可以留空。CLI：`remote-cli seal handover <账号>`。Web / Android 控制台在「轉讓超級管理員」時，若本機持有根私密金鑰，會提示可選通關密語並帶上這個包。
2. Exchange **只儲存、不解密**。它檢查字首是 `rsb1.` 或 `rsk1.`、長度不超過 16384、以及只含 ASCII URL 安全字元。請求裡沒有 `root_handover` 時，刪掉該工作區殘留的交接包。
3. 新超級管理員 `GET /api/superadmin/handover` 取回 `{blob, from_operator, created_at}`（沒有則 404），用通關密語解開，裝到本機，然後走和「生成新根並輪換」相同的輪換：舊根簽發 `rsx1.`，公佈新根。最後 `DELETE /api/superadmin/handover`。CLI：`remote-cli seal accept-handover`。控制台的密封頁在有交接包時顯示「接收根私密金鑰交接」。
4. **輪換讓舊副本失效。** 交出去的是輪換前的根。線上裝置只在輪換記錄由它們已經信任的舊根簽章時改信新根。前任手裡的舊私密金鑰籤不出新根的授權，也改不了裝置已經改信的根。輪換完成前，空通關密語或弱通關密語的 `rsb1.` 仍能被解開；通關密語請分開傳遞。

**頻外：** 不想把密文放進 Exchange 時，CLI 用 `remote-cli seal handover <账号> --out-of-band`。標準輸出只有一行 `rsb1.`，轉讓請求不帶 `root_handover`。把這一行和通關密語經其他渠道交給對方，對方執行 `remote-cli seal accept-handover <那一行>`。控制台在通關密語提示上點取消（Android 為「只轉讓帳號」）同樣只轉讓角色、不上傳根私密金鑰；根私密金鑰仍要另交（匯出 `rsk1.`，或頻外的 `rsb1.`）。

## 交握與線路格式

```text
CLI                         Exchange                       agent
 │ open_sealed{session_id,    │                               │
 │   device_id, hello}        │──sealed_open{session_id,hello}▶│ 验证 hello 签名、授权 → 级别
 │                            │◀─sealed_accept{session_id,     │
 │◀─sealed_accept (原样转发) ──│        accept}                │
 │ 验证设备签名 + pin          │                               │
 │ sealed_input{packet} ─────▶│──sealed_input────────────────▶│ 解密、校验级别、执行
 │ 二进制帧 kind=4 (PTY 键入)─▶│── 只改写帧头编号 ─────────────▶│
 │◀── sealed_event{packet,last}│◀── sealed_event ──────────────│ 所有输出在出站队列加密
 │◀── 二进制帧 kind=4 ─────────│◀── 二进制帧 kind=4 ───────────│
```

- `hello` = `{v, operator_key, operator_ephemeral, nonce, grant?, sig}`，簽章覆蓋 `["remote-seal-v1/hello", v, device_id, operator_key, operator_ephemeral, nonce, grant.sig|""]`（JSON 陣列，即規範位元組）。
- 根記錄 `rsr1.` 簽章覆蓋 `["remote-seal-v1/workspace-root", v, workspace_id, root_key, issued_at_ms]`；輪換 `rsx1.` = `{v, workspace_id, old_root, new_root, issued_at_ms, sig}`，由 `old_root` 簽章覆蓋 `["remote-seal-v1/root-rotation", v, workspace_id, old_root, new_root, issued_at_ms]`。Exchange → agent：`{"type":"seal_root","token":"rsr1.…","rotation":"rsx1.…"?}`（只接受來自 Exchange 的明文控制訊息，隧道內無效）。
- `accept` = `{v, device_key, device_ephemeral, nonce, level, sig}`，簽章覆蓋 `["remote-seal-v1/accept", v, device_id, operator_key, operator_ephemeral, hello.nonce, hello.sig, device_key, device_ephemeral, nonce, level]`。Exchange 改 `level` 會使簽章失效。
- 工作階段金鑰：`salt = SHA256(accept 签名字节)`，`PRK = HKDF-Extract(salt, X25519(o, b))`，`K_od = Expand("remote-seal-v1/operator_to_device")`，`K_do = Expand("remote-seal-v1/device_to_operator")`。
- AEAD：ChaCha20-Poly1305，AAD = `remote-seal-v1`，nonce = 通道位元組（0 文字 / 1 二進位）‖ 3 個 0 ‖ `u64` 計數器（大端）。每方向、每通道計數器從 0 嚴格加一；任何不符直接關閉工作階段。
- 文字：`{"counter":n,"ciphertext":"<base64>"}`。明文是 JSON：操作人員 → 裝置為 `SealedRequest`（現有 `DeviceDown` 訊息，或根管理訊息 `seal_policy_get` / `seal_policy_set {mode?, root_key?, revoke?, unrevoke?}`）；裝置 → 操作人員為 `SealedResponse`（現有 `DeviceUp` 訊息或 `seal_policy`）。內層 `session_id` 由 agent 改寫為隧道編號。
- 二進位影格：`StreamKind::Sealed = 4`，載荷 = `counter(u64 BE) ‖ AEAD(inner_kind ‖ bytes)`，`inner_kind` 為 1/2/3（stdout/stderr/PTY）。
- 每個密封工作階段承載**一個**操作（一次 exec、一個 PTY、一次檔案傳輸…），沿用 1:1 工作階段模型。結束訊息由 agent 加密並在外層標 `last:true`，Exchange 據此釋放路由和回填稽核，無需看懂內容。
- `close_session` → `sealed_close{detach:false}`（終止）；維運斷開 → `sealed_close{detach:true}`（由 agent 判斷：hold exec 只 detach，其他終止）。
- 大小上限：密文 base64 約 2.8 MiB（容納 2 MiB Android 截圖），外層包裝 `MAX_SEALED_WIRE`。

agent 側防線：出站佇列中屬於密封工作階段編號的明文只會被加密或丟棄，工作階段結束後該編號永久隔離（hold 程序的遲到輸出被丟棄而不是明文發出）；透過密封工作階段啟動/附加的 hold 作業不出現在舊路徑的清單中，也不能被舊路徑 attach/tail/cat/kill。

## 根私密金鑰匯出與備份

根私密金鑰能控制工作區裡的所有裝置。保管方式由超級管理員決定，**Console 預設以空口令上傳備份，可刪除以關閉自動保存**：

- **匯出（不上傳）：** `rsk1.` + base64url（無填充）32 位元組種子。CLI `seal root --export FILE|-`（檔案 0600），控制台「匯出根私密金鑰」（Web 顯示在輸入框，Android 複製到剪貼簿）。任何拿到它的人都能控制全部裝置。
- **加密備份到 Exchange（Console 預設保存，CLI 手動操作）：** 只有超級管理員、且本機金鑰就是已公佈的根時才能上傳。密文是版本化的 `rsb1.` 權杖（base64url 編碼的 JSON，最長 4096 字元）。金鑰由 Argon2id 從通關密語派生，生產參數固定為記憶體 64 MiB、迭代 3、並行度 1，並寫在權杖裡。鹽 16 位元組，ChaCha20-Poly1305 的 nonce 12 位元組，每次備份隨機生成。附加認證資料是 `remote-seal-v1/key-backup:{version}:{public_key}`，改版本、公開金鑰或密文都會驗證失敗；解開之後還要檢查種子匯出的公開金鑰等於權杖裡的公開金鑰。解碼器在跑 Argon2 之前拒絕過弱或過大的參數（記憶體 8 KiB–256 MiB，生產解密至少 64 MiB；迭代 1–8，生產至少 3；並行度只能是 1）。
- **通關密語可選，不強制強度。** 空通關密語同樣走 Argon2id（通關密語為空字串），格式一致。用戶端只做提示：通關密語為空或較弱（少於 12 個字元、字元類別太少、重複、常見通關密語）時，CLI / Web / Android 都會明確警告：**Exchange（或能讀資料庫的人）可以解開根私密金鑰並控制所有裝置。** Rust 和 JavaScript 用同一套提示規則和同一組測試向量（含空通關密語向量）。

`PUT` / `GET` / `DELETE /api/seal/backup` 只服務當前登入帳號；`PUT` 和 `GET` 只允許超級管理員。請求本文是 `{blob}`。Exchange 檢查 `rsb1.` 字首和長度，**從不解密**。`GET` 每個帳號、每個程序、每小時最多 5 次，超出回傳 429；這隻減慢線上猜測，庫被拷走後弱通關密語可被離線窮舉。讀、寫、刪會記稽核，詳情只有位元組數。

- CLI：`remote-cli seal backup`（隱藏輸入通關密語兩次，可直接Enter留空；或 `--passphrase-stdin`）；`remote-cli seal restore`（已有不同金鑰時加 `--force`，舊金鑰保留為 `seal-identity.<毫秒>.previous.pk8`），恢復後把它固定為本機的工作區根。登入時也會自動嘗試恢復（見上節）。不列印私密金鑰、通關密語或密文。
- Web Console：根私密金鑰必須可匯出。超級管理員生成根時，瀏覽器若持有不可匯出的 WebCrypto 金鑰，會改用 IndexedDB 中的本地種子作為根。登入 bootstrap 在建立身分前自動恢復；`localStorage.seal.cloudBackup=off` 可關閉。
- Android Console：種子在 expo-secure-store。`bootstrapSeal` 同樣先自動恢復；`remote.seal.cloudBackup=off` 可關閉。

## 使用

```bash
remote-cli seal show                       # 生成/显示本机操作员公钥与指纹
remote-cli exec lab-router -- uname -a
remote-cli shell lab-router
remote-cli seal policy lab-router          # 根：查看设备授权与吊销策略

# 根给同事手动授权（通常不需要：根客户端按角色自动签发）（默认 90 天；对方已登录的客户端会自动登记公钥）
remote-cli seal grant --device lab-router --to alice --level view
# 或仍用公钥；--no-publish 只打印 token
remote-cli seal grant --device lab-router --operator-key <KEY> --level view --no-publish
# 手动续签即将到期的已发布授权（默认 --before 30 --days 90；上线时也会自动续签）
remote-cli seal renew
# 同事：拉取 Exchange 上签给自己的授权（登录后也会自动 sync）
remote-cli seal sync
# 仍可手动导入
remote-cli seal import-grant rsg1.xxxxx

# 根私钥：导出（不上传）或可选的加密云端备份
remote-cli seal root --export root.rsk
remote-cli seal backup
remote-cli seal restore                    # 已有不同钥匙时加 --force

# 带外固定设备指纹（在设备上 `remote-agent seal show`）
remote-cli seal trust-device lab-router <DEVICE_KEY>
```

**Web Console / Android Console：** 所有裝置操作只使用原生密封會話，命令、hold 列表/kill/tail/cat、終端、檔案、螢幕截圖與輸入（Android/桌面）、介面樹、感測器、ubus、系統日誌、自更新全部走密封隧道；頁面上顯示「🔒 端到端加密」。Web 裝置詳情或 Android 設定 →「端到端加密」面板與 CLI 等價：顯示本機公鑰/指紋、工作區根（超級管理員：匯出/匯入/輪換根私鑰、接收根私鑰交接、批准新公鑰）、檢視/修改裝置金鑰和吊銷策略（需要根）、簽發授權（輸出 `rsg1.` 令牌，CLI 可直接 `seal import-grant`）、匯入授權、信任裝置公鑰，以及預設保存的根私鑰雲端備份（口令可空，弱口令時明確警告 Exchange 能解開）。首次連線固定裝置指紋（TOFU），之後指紋變化直接拒絕並提示核對。普通成員在 CLI、瀏覽器、手機上是三把不同的鑰匙，各自登記後由根客戶端按角色簽發。超級管理員要在多臺裝置上當根，可上傳口令備份後在新裝置登入時自動恢復，或把根私鑰匯入每一臺。

裝置本機：`remote-agent seal show | root <key> | forget-root | revoke <key>`（修改後重啟 agent 生效；透過密封會話的 `seal policy` 修改即時生效）。

## 預設行為

| 範圍 | 當前行為 |
|---|---|
| CLI、Web Console、Android Console | 裝置操作全部使用原生密封會話，無關閉開關或明文回退 |
| 裝置本機策略 | 所有策略值（包括 `allow`）都拒絕明文操作 |

- 新裝置連上 Exchange 後固定工作區根（TOFU），根及根簽發的授權可操作。擔心首次連線時 Exchange 不可信，應先在裝置本機執行 `remote-agent seal root <key>`。
- 身份金鑰不可用或策略損壞時失敗即關閉；在裝置本機修復金鑰和策略後再連線。
