依 autoflow-agent workspace v2 設計把 PRD / 設計 / 架構 / 交付類 共享文件從個人層 .autoflow/(ignored)搬到 docs/autoflow/(進 git), 讓團隊可共享產品與架構文件,個人層只留 progress / review / testing 等 per-branch 筆記。 - 02-prd/ 21 個檔(PRD、features、market-analysis 等) - 03-design/ 18 個檔(design-spec、wireframes、flows 等) - 04-architecture/ 31 個檔(TDD、design-doc、ADR×14、API 規格等) - 07-delivery/ 3 個檔(project-summary、phase-0.6-handover、stage-deployment-setup) 合計 73 檔。原檔已從 .autoflow/ 移除(migration 工具執行 git mv, 但因 .autoflow/ 在 .gitignore 中、git 將此操作視為新增、無 rename history)。
137 lines
6.1 KiB
Markdown
137 lines
6.1 KiB
Markdown
# 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 兩階段)
|