jim800121chen 3f0175f1a9 feat(local-agent): Phase 0.5 visionA Agent — Wails 桌面 + tunnel client + 配對 UI
從 local-tool 複製出獨立的「visionA Agent」桌面應用(A3 純橋樑:
tunnel client + 配對 UI + 設定,不開 HTTP port、不做本機裝置/推論 UI)。
Bundle ID 與 local-tool 不同(com.innovedus.visiona-agent vs visiona-local),
雙 app 可共存。fork 後不主動 sync,需要時手動 cherry-pick。

Backend / Wails Go(AB1-AB13):
- internal/tunnel:6 狀態機(Idle/Connecting/Connected/Reconnecting/Failed/Stopped)
  + Pair/Unpair/Reconnect/Disconnect binding + ClientHooks event
- internal/auth:encrypted file token store(AES-GCM + scrypt + machineID
  fallback salt + 13 tests)
- internal/config:YAML validation + atomic write + 11 tests
- internal/log:ring buffer + ExportLog 升級 zip
- visionA-backend /api/pairing/exchange:SessionTokenStore + 17 new tests
- 三平台 build 驗證(macOS DMG 160 MB / Windows EXE / Linux AppImage)
- end-to-end 5 milestone 全綠(pairing → tunnel → forward → reuse 防護
  → tunnel drop failover)

Frontend / Next.js(AF1-AF7,沿用 visionA-frontend 基礎):
- AppShell + Header + TabNav(StatusView / PairView / SettingsView 三 tab)
- ConnectionStatusBadge 5 種狀態
- TokenInput regex 驗證 + 7 種錯誤 + 0.5s auto-switch 到狀態頁
- 設定頁 4 區塊(含重新配對 AlertDialog)
- agent-api.ts 封裝 Wails bindings(mock/real 雙實作)+ 90 tests

Phase 0.7 review-driven fix(Round 2):
- A1 Session fixation 防護(RotateSessionID)
- A3 mock pairing 預設改 false(必須明確 opt-in)+ startup log
- A4 Pair 失敗後 state 清理矩陣(exchange/Save/Start fail 各自終態)
- A5 Pair/Unpair/Reconnect lifecycleMu + 50 goroutine race test
- F1 重新配對次按鈕 / F2 PairView Esc cancel / F3 Wails BrowserOpenURL
  / F4 Settings draft 持久 + 未儲存 badge

驗證:agent backend go test -race -count=3 ./... 4 packages 全綠 /
agent frontend pnpm test 119 tests 全綠

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 11:22:01 +08:00

94 lines
3.8 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// tokenstore.go — Session Token 儲存介面AB5 範圍)。
//
// 本檔只定義 interface 與一個測試用的 MemoryTokenStore。真正的 OS Keychain /
// encrypted file 實作留到 AB7見 ADR-009、TDD §9。AB7 完成後,只要把 Wails
// app 啟動時建立的 TokenStore 換成 keychain 版Manager 不用動。
//
// 設計原則(對齊 §9 Token 儲存策略):
//
// - Session Token 是長效憑證(雛形無 TTLPhase 1 = 90 天),洩漏等於整台
// 電腦被遠端控制AB7 之前用 MemoryTokenStore 只保證 process 內不落檔。
// - 介面故意極簡Save / Load / Delete避免 AB7 換實作時 Manager 要改。
// - Load 在「尚未儲存」時回傳空字串 + nil error非 error 情境),與
// os.IsNotExist 語義對齊,讓呼叫端用 `if token == ""` 判斷更直覺。
// - 任何 I/O / 加解密錯誤才回 errorAB7 檔案讀取失敗、keychain 拒絕等)。
//
// Thread safety
//
// - Manager 可能在 main goroutinePair與 reconnect goroutine401 清 token
// 同時觸碰,實作必須 thread-safe。MemoryTokenStore 已內建 sync.RWMutex。
package tunnel
import "sync"
// TokenStore 是 Session Token 的持久化介面。
//
// AB7 會提供三個平台實作:
// - macOSgo-keychainKeychain Services
// - WindowswincredCredential Manager
// - Linuxsecret-service 或 encrypted file fallback
//
// 雛形AB5 / AB6先用 MemoryTokenStore重啟 Wails app 會遺失 token、需要
// 重新配對——這是雛形可接受的代價。
//
// 介面契約(所有實作必須遵守):
//
// - Save(token) 覆蓋既有值。**Save("") 等同 Delete()**:傳入空字串 token
// 視為「清除已儲存值」,不留下空字串條目,與 Delete() 行為等價。
// 這個契約讓呼叫端在「不確定 token 是空還是要清掉」的場景可以統一走 Save
// 而不必先檢查 token 是否為空。
// - Load() 在「從未儲存」或「已 Delete」時回傳 ("", nil)**非 error**。
// 對齊 os.IsNotExist 語義,呼叫端用 `if token == ""` 即可判斷未儲存。
// 僅 I/O / 解密失敗AB7 keychain 拒絕、檔案損壞等)才回 non-nil error。
// - Delete() 是 idempotent不存在時視為成功不回 error
// - 所有方法必須 thread-safeManager 會在 main / reconnect goroutine 同時呼叫)。
type TokenStore interface {
// Save 儲存 Session Token。覆蓋既有值。
// Save("") 等同 Delete()(見介面 doc 「介面契約」第 1 點)。
Save(token string) error
// Load 回傳已儲存的 Session Token未儲存時回傳 ("", nil)。
// 只有 I/O / 解密錯誤才回 non-nil error。
Load() (string, error)
// Delete 清除儲存的 token。不存在時視為成功idempotent
Delete() error
}
// MemoryTokenStore 是測試與 AB5/AB6 雛形用的 in-memory 實作。
// AB7 完成後會被平台 keychain 實作取代。
//
// ⚠️ 不持久化process 結束 token 就消失。
type MemoryTokenStore struct {
mu sync.RWMutex
token string
}
// NewMemoryTokenStore 建立一個空的 in-memory TokenStore。
func NewMemoryTokenStore() *MemoryTokenStore {
return &MemoryTokenStore{}
}
// Save 儲存 token空字串視為清除對齊 TokenStore 介面語義)。
func (s *MemoryTokenStore) Save(token string) error {
s.mu.Lock()
defer s.mu.Unlock()
s.token = token
return nil
}
// Load 回傳目前儲存的 token未儲存回 ("", nil)。
func (s *MemoryTokenStore) Load() (string, error) {
s.mu.RLock()
defer s.mu.RUnlock()
return s.token, nil
}
// Delete 清除儲存的 token。idempotent。
func (s *MemoryTokenStore) Delete() error {
s.mu.Lock()
defer s.mu.Unlock()
s.token = ""
return nil
}