# ADR-009:visionA Agent 的 Pairing / Session Token 儲存策略 ## 狀態 Accepted — 2026-04-22 ## 背景 (Context) visionA Agent 需要持久化存放兩種 token: - **Pairing Token**(`vAc_` + 32 hex):使用者貼上的配對憑證,15 分鐘內有效、一次性,僅短暫存在 - **Session Token**(`vAs_` + 64 hex):長期 90 天,agent 日後重連都用它,**核心安全資產** 儲存要求: - **機密性**:token 等同連線權限,若明文保存被任何能讀檔的 malware 拿走 = 裝置被劫持 - **跨平台**:macOS / Windows / Linux 都要能用 - **不依賴 root / sudo**:安裝必須是使用者權限 - **不依賴雲端**:離線時也要能讀 token 來嘗試重連 - **易於清除**:「重置所有設定」要能乾淨清除 ### OS 原生方案 | 平台 | 原生方案 | 安全強度 | |------|---------|---------| | macOS | Keychain (Security Framework) | 高;解鎖鑰匙圈後僅當前使用者可讀 | | Windows | Credential Manager (Wincred) / DPAPI | 高;DPAPI 綁定使用者帳號 | | Linux | Secret Service / libsecret(GNOME Keyring / KWallet) | 中;需要使用者 session 解鎖;SSH 登入可能沒有 | ### Go 生態 keyring library - `github.com/99designs/keyring` — 抽象 3 平台,API 清爽,但有 CGO 依賴 - `github.com/zalando/go-keyring` — 純 Go,但 Linux 只支援 Secret Service(無 KWallet) - `github.com/keybase/go-keychain` — 只 macOS,不跨平台 ## 決策 (Decision) ### 雛形階段(Phase 0) **採 AES-GCM encrypted file + OS machine ID 衍生 passphrase。不接任何 OS keychain**。 設計: ```go // internal/tokenstore/encrypted_file.go type EncryptedFileStore struct { path string // ~/Library/Application Support/visionA Agent/tokens.enc 等 passphrase []byte // sha256(machineID || app_salt) } // 用 AES-256-GCM:nonce 隨機,認證標籤保護完整性 // 明文結構: type tokenBlob struct { SchemaVersion int `json:"v"` SessionToken string `json:"session_token,omitempty"` PairingToken string `json:"pairing_token,omitempty"` AccountEmail string `json:"account_email,omitempty"` LastPairedAt time.Time `json:"last_paired_at,omitempty"` } ``` `machineID` 來源: | 平台 | 來源 | |------|------| | macOS | `ioreg -rd1 -c IOPlatformExpertDevice` 的 `IOPlatformUUID` | | Windows | Registry `HKLM\SOFTWARE\Microsoft\Cryptography` 的 `MachineGuid` | | Linux | `/etc/machine-id` 或 `/var/lib/dbus/machine-id` | `app_salt` 是 build 時寫進的常數(非秘密,只是讓 passphrase 不只依賴 machineID)。 ### Phase 1 切換到 OS 原生 keychain:`github.com/99designs/keyring`(CGO 可接受,因為 visionA Agent 本來就要編 Wails = CGO 大戶)。 `TokenStore` interface 設計讓 Phase 1 只需換實作: ```go type Store interface { GetSession() (string, error) SetSession(token string) error GetPairing() (string, error) SetPairing(token string) error Delete(kind string) error // "session" | "pairing" | "all" SetMetadata(m Metadata) error GetMetadata() (Metadata, error) } ``` ## 考慮過的替代方案 | 方案 | 優點 | 缺點 | 排除原因 | |------|------|------|---------| | **明文 JSON file** | 零成本 | malware / 同機其他使用者可讀 | 不可接受 | | **一次做完 OS keychain 3 平台(雛形)** | 最終狀態 | 跨平台測試成本高;Linux Secret Service 在 headless session 可能沒 daemon;CGO 編譯複雜度 | 雛形先簡化,分階段 | | **只做 macOS keychain**(因為雛形開發環境多半是 macOS) | 開發順 | Windows / Linux 仍需 fallback,等於還是要 encrypted file | 不如雛形就統一 encrypted file | | **讓使用者輸入 passphrase** | 真正的 Zero-Knowledge | UX 慘 — 每次重啟 agent 都要輸入 | 違反「開了就忘掉它」定位 | | **雲端存 token(只讓 agent 本機用 refresh token 換)** | 洩漏立刻可撤銷 | 需要 agent 本地仍存 refresh token — 問題沒消失;離線情境壞掉 | 沒解決根本問題 | ## 後果 (Consequences) ### 正面影響 - **雛形開發快**:encrypted file 純 Go,沒有 CGO / OS API 依賴,所有平台一視同仁 - **抽象清楚**:`TokenStore` interface 讓雛形 / Phase 1 切換零程式碼影響 - **完整性保護**:AES-GCM 自帶 auth tag,檔案被竄改會 decrypt 失敗 - **合理的威脅模型**:malware 拿到檔案還得拿到 machineID,且不同機器的檔案不互通(跨機器複製檔案會解不開) ### 負面影響(接受的取捨) - **machineID 可被同機 malware 讀取**:已經 escalate 到能讀 user home 的 malware 幾乎等同於整台淪陷,此時 token 其實是次要損失 - **備份難題**:使用者備份了 `~/Library/Application Support/visionA Agent/` 到新機器 → token 無法 decrypt,必須重新配對(這其實是**安全特性**,不是 bug) - **Phase 1 遷移需要一次性資料遷移**:升級後第一次啟動時,讀舊 encrypted file + decrypt + 寫入 keychain + 刪舊檔 ### 風險 - **machineID 取得失敗**:極端情況(沙盒 / 非標準 OS)→ fallback 到「隨機產生一次、寫在明文 checksum file」,此時檔案被竊 + passphrase 檔被竊才破(仍比單純明文好) - **Linux 多使用者共用機器**:machineID 相同但 file 在各自 `~/.config/`,OK - **跨 Linux distro(WSL2 / Flatpak)machineID 取得差異**:TODO 在 Phase 0 測試三大 distro(Ubuntu / Fedora / Arch) ## 雛形的明確限制(寫給未來) - 雛形不做 token rotation(Session Token 90 天內一直用同一個;過期才換) - 雛形不做「kill switch」(強制撤銷所有 agent)— 需 Phase 1 backend 支援 - 雛形不做使用者提示「你的 token 在這個檔案裡」— 太技術、嚇人;Phase 1 再評估 ## 合規性 - [x] 雛形不弱於明文檔案 - [ ] Security review:Phase 1 上線前必做 - [ ] Phase 1 遷移工具:TODO ## 相關文件 - `.autoflow/04-architecture/security.md` §9(Secret 管理) - `.autoflow/04-architecture/visiona-agent-tdd.md` §9(Token 儲存策略) - ADR-003(Pairing Token 兩階段)