# ADR-008:Tunnel Client 採「程式碼複製」策略在多個專案間共享 ## 狀態 Accepted — 2026-04-22(v1) **Updated** — 2026-04-22(v2,補充 §「visionA-backend 端 tunnel package 應刪除」) ## 背景 (Context) Tunnel client(WebSocket + yamux)邏輯會在以下地方出現: 1. **POC edge-ai-platform**:`server/internal/tunnel/client.go`(原始來源) 2. **visionA-backend**:`internal/tunnel/`(雛形 Q2 決策:從 POC **複製**到此,獨立演進) 3. **visionA Agent**(本次規劃):也需要一份 tunnel client,連雲端 remote-proxy 問題:visionA Agent 的 tunnel client 要從哪來?有三個選項: - A. **從 POC 再複製一份**(獨立於 visionA-backend) - B. **從 visionA-backend 複製**(已經搬過一次,從新的來源搬) - C. **共用同一份**:git submodule、go module replace、或抽成獨立 library ## 決策 (Decision) **v2 修正:採方案 A(從 POC 直接複製到 local-agent),同時刪除 `visionA-backend/internal/tunnel/`。** > ~~v1 原決策:方案 B 從 visionA-backend 複製到 local-agent~~(已撤銷,原因見下方「v2 修正:visionA-backend 端 tunnel package 應刪除」) 具體做法: - `cp -r edge-ai-platform/edge-ai-platform/server/internal/tunnel/ local-agent/internal/tunnel/` - 調整 import path:`edge-ai-platform/pkg/wsconn` → `visiona-agent/server/pkg/wsconn`(因為 agent 內已經從 local-tool 複製了 `server/pkg/wsconn`) - 參數化 `localAddr`:讓 `NewClient()` 可接受 `httpserver.Controller.LocalAddr()` 回傳的 random port - 同步刪除 `visionA-backend/internal/tunnel/` 整個 package(見下方說明) 與 ADR-007 一致:fork 後獨立演進,不主動 sync。 ## v2 修正:visionA-backend 端 tunnel package 應刪除(事實上已於 2026-04-21 刪除) ### 查證結論 經查證 visionA-backend 程式碼(2026-04-22): 1. **目錄已不存在**:`visionA-backend/internal/tunnel/` 在 2026-04-21 已被刪除(見 `visionA-backend/README.md` §Known Issues) 2. **歷史上從未被 import**:刪除前的版本,沒有任何 visionA-backend 程式碼 import 過這個 package 3. **README 已記錄此決策**,但 v0.1 版的 visiona-agent-tdd.md 仍然提到「從 visionA-backend 複製 tunnel client」,需要更正 ```bash $ ls visionA-backend/internal/ | grep tunnel (無任何結果 — 目錄不存在) $ grep -r "internal/tunnel" visionA-backend/cmd/ (無任何結果 — 沒有任何 import) $ grep "internal/tunnel" visionA-backend/README.md 324: ... 已於 2026-04-21 刪除 ... ``` ### 本 ADR v2 的角色 本次 v2 更新**不是觸發刪除動作**(刪除已發生),而是: 1. 把這個事實正式記錄進架構決策(之前只在 README) 2. 修正 v1 ADR 的方向(原 v1 寫「從 visionA-backend 複製到 local-agent」,現改為「從 POC 直接複製到 local-agent」) 3. 提供「為什麼當初會被複製進去 / 為什麼後來要刪」的完整脈絡,避免未來有人想加回去 ### 為什麼當初會被複製進來 B3 階段(cmd/remote-proxy 實作時),為了「預留給未來 local-agent 用」而從 POC 複製到 visionA-backend。但實際上: 1. **visionA-backend 自己不需要 tunnel client** — visionA-backend 是 tunnel 的**雲端伺服器端**,用 `internal/relay/` 接受 agent 的入站連線,職責與 tunnel client(agent 出站連線)完全相反 2. **local-agent 沒必要繞道** — local-agent 直接從 POC 複製就好,多一層 visionA-backend 中轉沒有任何好處(POC 才是原始來源) 3. **保留會誤導未來維護者** — 看到 `visionA-backend/internal/tunnel/` 會以為 visionA-backend 有 tunnel client 角色,實際上沒有 ### 決策 刪除 `visionA-backend/internal/tunnel/` 整個 package(在 AB1 階段執行)。 ### 職責對照(避免再混淆) | Package | 位置 | 職責 | 連線方向 | |---------|------|------|---------| | `internal/relay/` | visionA-backend | tunnel **server**:接受 agent 入站 WSS 連線,管理 yamux session pool | inbound(雲端被動接受)| | `internal/tunnel/` | local-agent(**新位置,從 POC 複製**)| tunnel **client**:主動撥 WSS 到雲端 relay,accept 反向 stream 並轉發到本機 HTTP server | outbound(agent 主動連出)| | ~~`internal/tunnel/`~~ | ~~visionA-backend~~ | ❌ **已於 2026-04-21 刪除**(從未被使用,B3 預留誤導)| — | ### 原則明確化 **程式碼不複製到不會用的地方。** 「未來可能會用到」不是複製的理由 — 真正要用時再從原始來源複製即可。預先複製會: - 增加維護負擔(要 sync POC 變更到一個沒人用的 package) - 誤導維護者對架構的理解 - 違反 YAGNI 這個原則應該在所有 visionA 子專案間遵守(特別是 fork / 複製模式下)。 ## 考慮過的替代方案 | 方案 | 優點 | 缺點 | 排除原因 | |------|------|------|---------| | **A. 從 POC 直接搬** | POC 是原始來源 | 代表 POC 要同時支援兩個下游,更新更分歧 | visionA-backend 已成為「雛形基準版」,從它搬更新 | | **C1. git submodule** | 真共享、單一改動點 | submodule 在 monorepo 中難管理、CI 綁定複雜、協作者 clone 需額外步驟 | 過度複雜 | | **C2. go module replace(`replace` directive)** | 語法乾淨 | 要求有共同的 module,agent 和 backend module 不同;跨 go.mod 易造成 ghost dependency | 不自然 | | **C3. 抽成獨立 `visiona-tunnel` repo** | 最乾淨的 library 模式 | 需要設計 stable API、發版、文件;維護 3 個 repo 的版本矩陣;**只有 2 個 consumer**不值得 | 過度工程 | | **C4. Monorepo root 放 `pkg/tunnel/`**,agent + backend 皆 import | 真共享且管理集中 | 整個 visionA monorepo Go module 結構需要重組(目前是每個子專案一個 module);對 local-tool 本來獨立的狀態有風險 | 風險大 | | **B(本決策):code copy** | 簡單、無新工具鏈、符合既有 fork 模式 | 未來 tunnel 協定要升版時要改 3 個地方(POC / backend / agent) | **tunnel 協定已穩定,改動頻率低**,維護成本可接受 | ## 後果 (Consequences) ### 正面影響 - **無新工具鏈**:純 `go.mod` + 普通目錄複製,沒有 submodule / replace 黑魔法 - **一致性風格**:與 ADR-007 的 fork 策略一致 - **改動自由**:visionA Agent 的 tunnel client 可以加專屬功能(Agent-Version header、重連事件 hook)而不影響 backend ### 負面影響(接受的取捨) - **三份程式碼共存**:POC / visionA-backend / visionA Agent 各一份 tunnel client - **Bug 修復需要 3 處同步**:例如修了 reconnect 退避演算法,3 邊都要 review 是否跟進 - **未來的協定升級(如 HTTP/3)需要 3 次修改** ### 風險 - **遺忘同步導致差異累積**:明確的 mitigation — **tunnel 協定變更必須寫新 ADR,同時 review 是否 cherry-pick** - **POC 已經是死分支**:預期 POC 不會再大改;主要 sync 路徑是 visionA-backend ↔ visionA Agent ### Phase 1 重新評估條件 若出現以下任一情形,重新評估(可能抽成 library): - tunnel 協定有 breaking change(HTTP/3 / gRPC 切換 / E2E 加密) - Consumer 增加到 4+ 個(例如出現 headless tunnel-only agent) - 實際出現 2+ 次的「bug 只修了 A 忘了修 B」事件 ## 合規性 - [x] 與 Q2 決策(POC → visionA-backend 複製)一致 - [x] 與 ADR-007(fork 模式)一致 - [ ] 建立 tunnel 變更 checklist:TODO(在第一次實際跟進 local-tool 或 backend 變動時產出) ## 相關文件 - `.autoflow/04-architecture/tunnel.md` - ADR-002(沿用 POC tunnel 協定) - ADR-007(visionA Agent 架構) - 相關程式碼: - `/Users/jimchen/Innovedus/edge-ai-platform/edge-ai-platform/server/internal/tunnel/client.go`(POC 原始 — 唯一存活來源) - ~~`visionA-backend/internal/tunnel/`~~(**v2 決策刪除**,從未被使用) - `visionA-backend/internal/relay/`(保留 — 這是 tunnel **server** 端,與 client 不同職責) - `local-agent/internal/tunnel/`(本次新增 — 從 POC 直接複製)