依 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)。
11 KiB
11 KiB
ADR-003:以 Pairing Token 取代 POC 的 SHA256(MAC) Token
狀態
Accepted — 2026-04-21(2026-04-22 修訂:明確兩階段 Token 流程 + 固定格式)
背景 (Context)
POC 的 tunnel 認證機制非常簡單:
- local agent 啟動時,計算
SHA256(MAC address)[:16],得到 16 字元 hex 當 token - 用
?token=xxx或X-Relay-Tokenheader 連線 relay - relay 以
map[token]*yamux.Session記憶體儲存,無任何驗證——任何人拿到 token 就能連上並註冊 session
這個設計在 POC 階段可接受,但不適合正式產品:
| POC 現況 | 問題 |
|---|---|
| Token = MAC 衍生值,永不變 | 洩漏後無法撤銷;換電腦 token 也跟著換(身分無法追蹤) |
| 無 expiry | 一次洩漏,永久曝險 |
| 無 ownership | Token 沒綁到「使用者」概念,無法實現「我的裝置列表」 |
| 無 revoke | 沒有「登出所有裝置」能力 |
| 無 rotation | 無法定期輪換降低風險 |
| Token == Identity | Token 本身即是身分,無法與 User Account 解耦 |
正式產品的雲端版需要:
- 綁定到「某個使用者帳號」
- 可撤銷
- 有 expiry,過期要 re-pair
- 可同一使用者有多台裝置(每台一個獨立 token)
- 可由使用者介面 list / revoke
決策 (Decision)
引入 兩階段 Token 機制:Pairing Token(短期,一次性) → 換取 Session Token(長期,可撤銷)。其生命週期如下:
Token 格式(統一規範,2026-04-22 修訂 M-1)
Pairing Token:vAc_ + 32 字元 hex (總長 36 字元;Admin-Credential)
Session Token:vAs_ + 64 字元 hex (總長 68 字元;Agent-Session)
- 字元集:
[0-9a-f]小寫 hex,前綴vAc_/vAs_ - 產生方式:
crypto/rand.Read(16 bytes)→hex.EncodeToString(...)(pairing);crypto/rand.Read(32 bytes)(session) - API 回傳值永遠是純字串,無空格、無分隔符
- UI 顯示層可視情況每 8 字元插入空格提升可讀性(例:
vAc_a1b2c3d4 e5f6g7h8 ...) - 使用者貼上時前端正規化
.replace(/\s/g, '') - 前綴讓 log / debug 一眼看出類型;正則驗證:
^vAc_[0-9a-f]{32}$/^vAs_[0-9a-f]{64}$
兩階段流程(Phase 1 完整;雛形簡化)
Stage 1 — Pairing Token(短期,一次性,15 分鐘 TTL)
- 使用者在雲端 Web 登入(
visionA-frontend) - 進入「Devices → Pair New Device」→ 前端呼叫
POST /api/pairing/token api-server產生 pairing token(vAc_+ 32 hex):- 寫入 DB:
token_hash = sha256(plain),kind='pairing',expires_at = now + 15min,used_at = NULL - 回傳 token 明文(只在此次回應中出現一次)
- 寫入 DB:
- 使用者把 token 貼到 local agent 的設定 UI / config / CLI flag
- local agent 持有此 Pairing Token,準備建立第一次連線
Stage 2 — Session Token(長期,90 天 TTL,可撤銷)
- local agent 首次用 Pairing Token 連
WS /tunnel/connect?token=vAc_xxx remote-proxy(透過api-server內部呼叫)處理:- 查 DB:token 是否
kind='pairing'且未撤銷、未過期、未使用 - 有效 → 原子升級:
- 產生 Session Token(
vAs_+ 64 hex) - 寫入新 DB row:
kind='session',parent_token = <pairing token_hash>,device_id = <new or resolved>,expires_at = now + 90d - 將 Pairing Token 標記
used_at = now()(作廢;無法再次升級) - 在 WS upgrade response header 或 首個 yamux control frame 回傳 Session Token 明文
- 產生 Session Token(
- 建立 yamux session,
SessionStore.Register(session_token_hash, handle) - 無效 → 拒絕並回 HTTP 401
- 查 DB:token 是否
- local agent 收到 Session Token 後持久化儲存(取代 Pairing Token),之後重連一律用 Session Token
- Session Token 過期、被撤銷 → local agent 連線被拒 → 使用者需在 Web UI 重新發 Pairing Token 走一次 Stage 1
狀態轉換時序(Phase 1)
使用者 Web UI api-server local agent remote-proxy / DB
│ │ │ │
│─ Pair New Device ────►│ │ │
│ │─ INSERT pairing ──►│ │
│ │ kind='pairing' │ │
│ │ TTL 15 min │ │
│◄── vAc_xxx (once) ────│ │ │
│ │
│─ 複製貼到 agent ────────────────────────────►│ │
│ │─ WS /tunnel/connect ─►│
│ │ ?token=vAc_xxx │
│ │ │─ Validate(pairing)
│ │ │─ 產生 vAs_yyy
│ │ │─ INSERT session
│ │ │ kind='session'
│ │ │ TTL 90 day
│ │ │─ mark pairing used
│ │◄─ upgrade + vAs_yyy ──│
│ │─ 持久化 vAs_yyy │
│ │ │
│ │─ 日後重連(always) ──►│
│ │ ?token=vAs_yyy │
│ │◄── 直接接受 ──────────│
│ │ │
│─ Revoke device ──────►│─ UPDATE session ──────────────────────────►│
│ │ revoked_at = now │
│ │◄── 下次連線 401 ──────│
- 使用者可在「Devices」頁面 list / revoke 任一 device 的 Session Token(
UPDATE revoked_at)
雛形版(當前要做的)
雛形不接 DB / 不接 Auth,但要把介面切好,未來替換實作即可:
// internal/auth/pairing.go
type PairingStore interface {
Validate(ctx, token) (*PairingInfo, error)
MarkUsed(ctx, token, deviceID) error
Create(ctx, userID) (string, *PairingInfo, error)
Revoke(ctx, token) error
List(ctx, userID) ([]*PairingInfo, error)
}
type PairingInfo struct {
TokenHash string
UserID string
DeviceID string
CreatedAt time.Time
ExpiresAt *time.Time
UsedAt *time.Time
RevokedAt *time.Time
}
雛形實作 StaticPairingStore:
- 從 env var 讀一組寫死的 token + 一個假 user_id(例:
VISIONA_PAIRING_TOKEN=abc123...,VISIONA_PAIRING_USER_ID=demo-user) Validate簡單比對;Create/Revoke/List回ErrNotImplemented- local agent 啟動 config 寫這個 token(或 CLI flag
--pairing-token=xxx)
未來替換為 DBPairingStore,其他程式碼(relay session、API handler)完全不改。
雛形 vs Phase 1 的單/雙階段處理(2026-04-22 修訂 M-2)
| 階段 | 做法 |
|---|---|
| 雛形(Phase 0) | 為簡化開發,以單一 Pairing Token 形式比對,不做升級步驟。StaticPairingStore 讀取 VISIONA_PAIRING_TOKEN env(格式仍為 vAc_ + 32 hex),agent 直接用它連 tunnel。無 TTL、無升級、無撤銷。這是刻意妥協以加速驗證。 |
| Phase 1 | 完整兩階段(見「兩階段流程」):Pairing Token 15 min TTL + Session Token 90 d TTL + 撤銷機制 |
雛形限制須明確告知使用者:雛形的單一 token 等同「永不過期的 pairing + session 合併體」,僅適合 dev 環境。Phase 1 前必須切到兩階段。
考慮過的替代方案
| 方案 | 優點 | 缺點 | 排除原因 |
|---|---|---|---|
| 沿用 SHA256(MAC) | POC 已驗證 | 不安全、不可撤銷、無 ownership | 無法做成產品 |
| mTLS 憑證(client certificate) | 業界標準、極安全 | 憑證管理複雜、使用者端安裝門檻高、Web 產生憑證 UX 差 | 使用者體驗門檻過高 |
| OAuth Device Flow | 標準化、適合 headless | 需外部 IdP(Auth0 / Cognito)、實作複雜 | Phase 2 可以評估,雛形過度複雜 |
| 短期 JWT + refresh token | 標準做法 | JWT 無法撤銷(除非建黑名單);refresh token 又變另一個長期 token | 仍需處理 revoke,不如直接用 opaque token |
| 使用者密碼 / API key | 簡單 | 無法限定 scope、難以細粒度撤銷 | 粗糙 |
後果 (Consequences)
正面影響
- 抽象清晰:
PairingStoreinterface 把 token 驗證與 tunnel session 管理解耦 - 雛形到 Phase 1 零程式碼修改:只換 store 實作
- 未來支援多種 token 形態:無論是 JWT、opaque token、還是 DB-backed session token,都實作同一個 interface
負面影響(接受的取捨)
- 雛形安全性差:env 寫死 token,任何能讀 env 的人都能冒充。可以接受,因為雛形只會在開發環境跑
- 沒有 rate limit:雛形不做;Phase 1 要做
/tunnel/connect的 IP / token rate limit - 無 logging of failed attempts:雛形只記 error log;Phase 1 要記到 audit log
風險
- Token 儲存明文 vs hash:產品版要把
token_hash = sha256(token)存 DB,不存明文。雛形 env 暫用明文 - Token 透過 URL query 傳遞會進 access log:POC 已這樣做;雛形保留;Phase 1 評估改用
Authorization: Bearer xxxheader(WebSocket 自訂 header 瀏覽器有限制,但 local agent 可以) - 如果使用者把 token 貼到 public repo:產品版需支援「自動撤銷洩漏 token」(secret scanning);雛形不處理
合規性
- Architect 確認
- Security Review:Phase 1 前必做
- 加入
.gitignore保證 token 不進 Git:工程師實作時落實
相關文件
- Design Doc §6(安全架構)
- TDD §5(Pairing Token 協定)
- TDD
security.md - 相關 ADR:ADR-002(Tunnel Protocol)、ADR-005(雛形不接 DB/Auth)