實作經完整審查流程(security 1C+3M → 契約修正 → confirm-only → 逐 WP reviewer → security code-level APPROVED → testing 270 測試全綠) 並 merge(PR #1 / 3eaf3dc)後轉 Accepted。 明確記錄「Accepted ≠ 端到端啟用」:僅底層能力已 merge,WP-4 影片分頁 接線未做,影片仍走 tunnel、90MB 過渡上限仍生效。含兩筆 backlog。 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
30 KiB
ADR-019: 影片 / 圖片 / 批次上傳走同機 localhost 直連 local-agent(混合路徑)
狀態
Accepted。
底層能力已於 2026-07-30 完成並 merge 進 main(github PR #1、merge commit 3eaf3dc)。 走過完整審查流程:security pre-implementation review(1 Critical + 3 Major)→ 契約修正(C1 / M1 / M2 / M3 / 議題1)→ security confirm-only 複審 → 逐 WP reviewer 通過 → security code-level 複審 APPROVED → testing 回歸(270 測試全綠、既有 tunnel 路徑未打斷、無 regression)。C1 定案採方案 A(route 分離),見 §2.4。
⚠️ 「Accepted」≠「端到端功能已對使用者啟用」。 本次 merge 的是 backend/frontend 的底層能力(local-agent CORS/PNA/token/新 route、雲端 ticket endpoint、前端 port 探測抽象、批次合計檢查)。前端影片分頁 UI 尚未切到新的 localhost 路徑(WP-4 未做),故:
- 影片上傳目前仍走既有雲端 tunnel 路徑,端到端「localhost 直連」尚未對使用者啟用。
- 90MB 過渡上限仍在生效(見
.autoflow/相關過渡處置)。- WP-4 接線完成前,不可視為「ADR-019 功能已上線可用」。詳見 §7 合規性與 §8 WP 清單狀態標記。
日期
2026-07-24
作者
Architect Agent
1. 背景與範圍 (Context)
1.1 觸發問題
visionA 的 media 上傳(影片 / 圖片 / 批次)目前全部經雲端 tunnel forward(見 api/api-spec.md §6:「Camera / Media — 與 local-tool 相同,全部透過 tunnel forward」)。對影片這種大檔,此路徑有三個實質問題:
| 問題 | 說明 | 證據 |
|---|---|---|
| 頻寬雙倍佔用 | 500MB 影片經 tunnel = 上傳(本機→雲端)500MB + 結果下行(雲端→本機)500MB ≈ 1000MB 總傳輸,兩次都佔用使用者本地頻寬 | 第一輪資料流調查 §5.3 |
| 撞 nginx 上限 | stage nginx 兩處 client_max_body_size 100M,500MB 影片直接 413 |
docker/nginx.stage.conf:97, 417 |
| api-server 300s timeout + stage 磁碟落地 | 即使調大 nginx,10 Mbps 上行需 400s > 300s timeout;且 proxy_request_buffering 預設 on,每次上傳在 stage 落地 500MB 暫存檔 |
visionA-backend/internal/api/proxy.go:32, 89-94;camera.go:47 |
1.2 被忽略的邏輯漏洞(本 ADR 修補的核心)
design-doc.md §5.3(模型上傳)早已建立原則:
「模型檔可達百 MB,走 tunnel = 占用使用者本地頻寬兩次(上傳→雲端→下載)」,故模型上傳不走 tunnel(改 presigned URL 瀏覽器直連)。
這個「大檔不走 tunnel」的原則從未套用到 media 上傳——影片同樣是大檔、同樣佔用雙倍頻寬,卻仍走 tunnel。本 ADR 把同一原則延伸到 media,修補此文件內部邏輯漏洞。
1.3 有利前提(讀 code 後確認)
- local-agent 已有完整的 loopback HTTP server(Gin,強制綁
127.0.0.1:<port>,local-agent/server/main.go:298-305、config.go:34-35,45)。 POST /api/media/upload/video(及 image / batch)route 已存在於 local-agent,路徑與雲端完全一致(local-agent/server/internal/api/router.go:90-94)。handler(camera_handler.go:232-336)與 tunnel 完全解耦(tunnel client 只做透明轉發,internal/tunnel/client.go:337-371)——瀏覽器直連走的是同一個 handler、同一 code path,handler 零改動。
1.4 使用者已拍板的前提
- 跨機不存在:使用者確認「都是同機操作」,故非同機情境採「停用該分頁 + 明確提示」,不做 tunnel fallback。這是產品定位,不是能力缺口。
- 瀏覽器範圍:僅需支援 Chrome / Edge(Chromium 系)。Safari 不在支援範圍。
- 認證必做:本機 endpoint 經既有 tunnel 下發 one-time token。
2. 決策 (Decision)
我們將讓瀏覽器在同機情境下,直接 POST 影片 / 圖片 / 批次到 local-agent 的 localhost HTTP endpoint,繞過雲端 tunnel。控制面(token 取得)與結果面(MJPEG 串流、推論 WS)仍走雲端 tunnel——這是「上傳走本機、結果走雲端」的混合路徑,不是把整條資料流搬到本機。
具體包含五塊決策:
2.1 混合路徑(只搬「上行大檔」,控制面 / 結果面不動)
- 改走 localhost:影片 / 圖片 / 批次的檔案上傳 POST。
- 仍走 tunnel(刻意保留):token 取得(控制面)、MJPEG 結果串流(
streamUrl是相對路徑/api/camera/stream,前端用getApiBaseUrl()組雲端絕對路徑,camera_handler.go:325;visionA-frontend/src/lib/camera.ts:35-42)、推論結果 WS(/ws/devices/:id/inference)。 - 理由:MJPEG 結果資料量遠小於原始影片(15fps down-scaled JPEG),走 tunnel 成本可接受;強行把結果也搬本機需在前端維護兩套連線來源,複雜度大增。保持「控制面 + 結果面走雲端、只有大檔上傳走 localhost」是風險最低的最小切口。
2.2 範圍:影片 + 圖片 + 批次全部改走 localhost
- 三個分頁全部改走 localhost 直連(不只影片)。前端上傳抽象設計為 endpoint 無關(
uploadToLocalAgent(path, form, options)),三個 caller 換 path 即可,不需重構。 - 批次額外補「合計大小檢查」:現況
validateBatchFiles(visionA-frontend/src/lib/media.ts:301-309)只逐張檢查 ≤20MB、不加總——50 張各 19MB(合計 950MB)會通過前端驗證後撞 413。這是已上膛的地雷,須補合計上限(建議MAX_BATCH_TOTAL_BYTES = 80MB,留 nginx 100M 的 20% 餘裕)。
2.3 同機偵測 + port 探測(前端掃描候選 port + 身分驗證)
- local-agent 的 port 是動態的(
pickPort從 3721 fallback 到 3740,app.go:46-47, 1450-1468),前端不可寫死 3721。 - 前端探測策略:
sessionStorage 快取→3721→3722–3740 並發(Promise.any取第一個成功者),每次 timeout 500ms。 - 探測動作本身就是同機偵測:任一 port 回應且身分相符 = 同機;無回應 = 非同機。
- 新增專用探測 endpoint
GET /api/local/hello(不用既有/api/system/health——後者對非白名單 origin 的 GET 不回 ACA*,瀏覽器 JS 讀不到內容)。 - 身分驗證必做:
/api/local/hello回傳 agent 管理的裝置 serial 的 salted SHA-256 雜湊(SHA-256("visiona-local-v1" || serial),已裁決見 §5 / api-spec §6.3),前端重算比對selectedDevice.serialNumber是否相符。防止「同機跑著另一台 agent、影片被送到錯的裝置」這種靜默錯誤資料流。沿用 ADR-018 的 serial 路由慣例,不新造識別體系。
2.4 認證:經既有 tunnel 下發 one-time token(縱深防禦)
開放雲端 origin 打 local-agent 後,威脅模型從「只有本機 loopback 網頁能打」變成「同機任何網頁只要通過 origin 檢查就可能控制 Kneron 裝置」。CORS 白名單擋不住 XSS(合法 origin)與 DNS rebinding(繞過 DNS 解析層),故必須有認證作為第二道防線。
流程(契約):
[1] 前端進影片分頁 → 向雲端要 token(經既有 OIDC 認證 + 既有 tunnel)
POST /api/devices/:serial/local-upload-ticket(新增雲端 endpoint)
→ api-server 驗 OIDC session + 裝置歸屬 → 經 tunnel 打 local-agent
POST /api/local/issue-token → 產 token 回傳
[2] 前端掃描找到 localhost port(§2.3)
[3] 前端 POST 影片 → http://127.0.0.1:<port>/api/local/media/upload/video ← 新的、一律要 token 的 route
Header: X-Visiona-Local-Token: <token>
[4] local-agent 驗 token(存在 / 未過期 / 未用過 / deviceId 相符)+ 驗 Host = loopback(§2.5)
→ 消耗 token → 內部轉呼叫既有 handler(handler 零改動)
Token 設計方向(已經 security agent 審,核心設計通過,見 review 議題 2):
- 生成
crypto/rand32 bytes → base64url(不准math/rand);TTL 120s;one-time(驗證後立即刪);綁 deviceId;記憶體 store(map + sync.Mutex,不持久化);上限同時 32 個未使用 token(防 DoS);比對用crypto/subtle.ConstantTimeCompare(防 timing attack)。 - 實作時注意項(security review m2 / m3,不改契約但工程師必守):consume 與 issue 各自在單一
Lock()內完成(consume = 查存在 + ConstantTimeCompare + 刪除三步一次持鎖;issue = 查 len<32 + 插入一次持鎖),避免併發雙重消費 / 上限突破;token 驗證放在讀 multipart body(FormFile)之前(用 middleware 更佳),避免「未驗證就先收大檔」放大 DoS。
2.4.1 [C1 — 已定案:方案 A route 分離] 認證判準不得依賴攻擊者可控的 Origin
背景(security review C1,Critical blocker):原契約訂「來自 tunnel 的請求(無 Origin header)不得要求 token」,把「有無 Origin」當成「是不是可信 tunnel 來源」的唯一判準。但 Origin header 由請求發送方完全控制,同機任何程序(curl / 惡意 App / 被入侵的其他本機服務)都能發出不帶 Origin 的 HTTP 請求到
127.0.0.1:<port>(local-agent 綁 loopback、同機所有程序皆可連),等於「不帶 Origin 就免 token」的後門,整層 token 認證被繞過。
查證結論(讀 local-agent/visiona-agent/internal/tunnel/client.go):tunnel client 的 handleStream 把收到的 HTTP request 直接 http.DefaultTransport.RoundTrip(req)、req.URL.Host = c.localAddr(= 127.0.0.1:<port>,普通 HTTP over loopback,見 client.go:337-355、73)。tunnel 未在轉發時注入任何可辨識標記,且 tunnel client(Wails app shell module visiona-agent)與 HTTP server(獨立子行程 module server)是兩個獨立行程。結論:同機程序可完全偽裝成「tunnel 來的請求」,server 端無法用 request 本身區分。
定案:採方案 A(route 分離),不採方案 B(tunnel 注入 process-local secret)。理由:
- tunnel client 與 HTTP server 是兩個獨立行程 / 獨立 Go module(見 client.go 檔頂註解),方案 B 的「process-local 共享 secret」實際上需要跨行程 IPC 傳遞 nonce,脆弱且增加耦合;方案 A 不需任何跨行程協調。
- 方案 A 把「要不要 token」交給 server 端固定的 route 決定,不由攻擊者可控的 Origin 決定。
契約鐵則(取代原「無 Origin 免 token」硬規則):
- 媒體上傳 endpoint 絕不能有「無任何憑證即放行」的 code path。 刪除原「來自 tunnel 的請求(無 Origin)不得要求 token」的無條件放行語意。
- 瀏覽器 localhost 直連走新的、一律要求 token 的 route:
POST /api/local/media/upload/{video|image|batch-images}。此 route 不看 Origin、一律驗 token(token 無效 / 缺失 → 401),並經 §2.5 Host 驗證。內部轉呼叫既有 handler(handler 零改動)。 - 既有 tunnel 轉發路徑維持既有
POST /api/media/upload/{video|image|batch-images}(tunnel client 內部轉發至此,路徑不變、行為不變)。此路徑不因本 ADR 新開放給任何雲端 origin(CORS 白名單擴充只作用於直連情境的/api/local/*行為)。
誠實揭露的殘留風險:因 loopback 對同機程序無隔離,同機惡意程序仍可直接打舊 tunnel-path route(/api/media/upload/*)——但這是 ADR-019 之前就存在的既有 loopback 攻擊面(非本 ADR 新增),且舊 route 未被本 ADR 新開放給雲端 origin。緩解:舊 tunnel-path route 也套用 §2.5 Host 驗證(Host=loopback 才放行),把「同機直打舊 route」限制在同機、且不影響 tunnel 轉發(tunnel 轉發的 Host 本就是 127.0.0.1:<port>)。完整消除同機程序攻擊面超出本 ADR 範圍(需 OS 層 socket 權限 / peer credential 驗證),列為 backlog。
2.5 CORS 白名單擴充 + Host 驗證 + PNA 緩解(全部必做)
- Origin 白名單改為可設定(env
VISIONA_CLOUD_ORIGINS,逗號分隔完整 origin),stage 需同時允許雙入口:https://stage-9527.innovedus.com:9527(公網 HTTPS,nginx.stage.conf:86-89)http://192.168.0.130:9527(內網純 HTTP 入口,nginx.stage.conf:411-414)- (dev)
http://localhost:3000
- loopback 舊規則(
middleware.go:17-22)保留不動。 - [M3 — 契約必做] 雲端 origin 採「完整 origin 精確比對」(scheme+host+port 全等逐字比對),不可沿用既有
isAllowedOrigin的 hostname-only + 任意 port + 放寬 scheme 邏輯(middleware.go:41-45)。白名單裡存的是完整 origin 字串(如https://stage-9527.innovedus.com:9527),比對時整串相等才通過。原因:既有allowedHosts只比 hostname、放任任意 port,若直接把雲端網域塞進去會變成「該網域任意 port 都放行」,攻擊面比預期大;放寬 scheme 若寫成「不檢查 scheme」則https://127.0.0.1、http://混用可繞過。故雲端 origin 必須獨立於既有 loopback 邏輯,走完整字串精確比對。 - [M3 — 契約必做]
Access-Control-Allow-Credentials: false(對雲端 origin):本路徑用 header(X-Visiona-Local-Token)帶 token、不需 cookie。不可沿用既有 middleware 對雲端 origin 回Allow-Credentials: true(middleware.go:97)——那會無謂讓瀏覽器願意帶 credential、擴大 CSRF / 憑證面。雲端 origin 一律回false(api-spec §6.4 已列)。 - [M2 — 契約必做,從「待審 / optional」升為「必做」] Host header 驗證 = loopback:對所有
/api/local/*(含新的/api/local/media/upload/*)與舊 tunnel-path media route,加 Host 驗證 middleware——Hostheader(去掉 port 後)必須 ∈{127.0.0.1, localhost, ::1},否則400 Bad Request。這是獨立於 CORS 的第二道、擋 DNS rebinding(rebinding 後Host為攻擊者網域 ≠ loopback → 400),成本低、對本機服務屬 baseline 而非 optional。tunnel 轉發的 Host 本就是127.0.0.1:<port>(見 client.go:347)故不受影響。驗證邏輯(契約,不寫進 production code):host := c.Request.Host // 含 port h, _, _ := net.SplitHostPort(host) // 去 port(無 port 時退回原值) if h ∉ {"127.0.0.1", "localhost", "::1"} → 400 Bad Request - PNA 緩解(必做,非 optional):對帶
Access-Control-Request-Private-Network: true且通過白名單的 preflight,回Access-Control-Allow-Private-Network: true。原因見 §4.3 R-1——即使只支援 Chrome/Edge,Chrome 未來可能把 PNA 從 warning 升為 blocking,而已安裝的舊版 local-agent 不會自動升級,此 header 現在補、成本 <30 分鐘,省未來一顆不定時炸彈。 - preflight 帶
Access-Control-Max-Age: 600(api-spec §6.4,緩解 port 掃描的大量 preflight)。
3. 考慮過的替代方案 (Alternatives Considered)
| 方案 | 優點 | 缺點 | 排除原因 |
|---|---|---|---|
維持 tunnel + 調大 nginx client_max_body_size |
改動最小、不動前端架構 | 不解決頻寬雙倍;批次分頁仍是地雷;stage 每次落地 500MB 暫存檔;還需 IT 改公司邊界 nginx(STAGE-DEPLOY.md:397-401 待辦未完成);api-server 300s timeout 仍會炸 |
治標不治本,且引入「改 nginx→改 timeout→關 request buffering→改公司邊界 nginx」的連鎖,與「最小改動」背道而馳 |
| 傳檔案路徑給 local-agent(不傳檔案內容) | 傳輸量 ~100 bytes | 瀏覽器基於安全拿不到真實路徑(File.path 已被所有現代瀏覽器移除);需在 local-agent 加業務 UI,違反 server_control.go:1023-1033「使用者不感知 server 存在」的既有決策;path traversal 風險 |
技術不可行(拿不到路徑)+ 違反既有架構方向 |
| 前端壓縮 / 抽 frame 後上傳(縮小檔案再走 tunnel) | 沿用現有 tunnel 路徑 | 失真影響推論準確度;瀏覽器端影片處理相容性差、耗時;不解決架構問題(只是把大檔變小檔) | 犧牲推論品質,且沒有真正解決頻寬 / 落地問題 |
| localhost 直連(採用) | 傳輸量 1000MB→loopback(0.1–2.5s,改善約 100–1000 倍);不受 nginx / timeout 約束;handler 零改動 | 需同機操作;local-agent 新增對外攻擊面(需認證);PNA 未來風險 | ✅ 採用 |
4. 後果 (Consequences)
4.1 正面影響
- 頻寬:500MB 影片從「1000MB 經雲端 tunnel」降為「loopback 本機傳輸」(現代 SSD 筆電 0.1–0.5s、較舊機器 1–2.5s)。
- 不受基礎設施約束:繞過 nginx 100M 上限、api-server 300s timeout、stage 磁碟落地。
- handler 零改動:local-agent 的
UploadVideohandler 一行不用改(route 已存在、與 tunnel 解耦)。 - 修補文件邏輯漏洞:把
design-doc.md §5.3的「大檔不走 tunnel」原則正確延伸到 media。 - 順帶修批次地雷:補合計大小檢查,消除 50×19MB 撞 413 的隱患。
4.2 負面影響與限制(接受的取捨)
- 影片 / 圖片 / 批次上傳需同機操作:必須在執行 visionA Agent 的那台電腦上操作。非同機時該分頁停用 + 提示。Camera(即時攝影機)仍支援遠端檢視——只有「上行大檔」需同機限制。
- 依使用者拍板「跨機不存在」,此限制不影響實際使用;且現況跨機使用者本來就傳不了 >100MB(413),此限制是「把隱性限制變成明確提示」,非新增限制。
- 不動 PRD:使用者已明確跨機不存在,此限制在 ADR 記錄即可,不需 PM 改 PRD。
- local-agent 新增對外攻擊面:首次開放雲端 origin 直連 local-agent,需維護 CORS 白名單 + token 認證 + PNA header 的安全性。
- 僅支援 Chromium 系(Chrome / Edge):Safari 不支援(使用者已確認不需要)。
- 狀態分裂風險:上傳走 localhost、結果走雲端 tunnel,兩條路徑可能不同步(見 §4.3 R-3)。
4.3 風險
| # | 風險 | 等級 | 緩解 |
|---|---|---|---|
| R-1 | PNA 未來變硬阻擋,已安裝的舊版 local-agent 無法自動升級 → 影片功能無預警壞掉 | 高 | 第一版就加 Access-Control-Allow-Private-Network: true(§2.5,成本 <30min) |
| R-3 | 狀態分裂:上傳走 localhost 成功,但結果 tunnel 剛好斷線 → 上傳成功卻看不到結果 | 中 | 上傳前檢查既有 tunnel_online 欄位(visionA-backend/internal/api/devices.go:66-67),離線時 disable 上傳 |
| R-4 | 同機多個 visionA agent(多裝置場景),掃描命中錯的 agent | 中 | §2.3 serial 身分驗證;掃描收集所有回應再挑 serial 相符者 |
| R-6 | 影片 / 圖片 / 批次上傳完全無 size 上限 + temp 檔洩漏(stopActivePipeline 切 pipeline 時把 videoPath="" 但不 os.Remove,batch 成功路徑後也未刪),反覆上傳可磁碟 DoS(配合 C1 未修時為 High) |
中→高 | [M1 — 契約必做,非查證] 見 §4.3.1 |
4.3.1 [M1 — 契約必做] media 上傳 size 上限 + temp 檔清理
security review M1:temp 檔洩漏已確認為實錘(非待查證)——
camera_handler.go的stopActivePipeline(line 535)把h.videoPath = ""但從未對前一支影片 temp 檔os.Remove;batch 路徑在NewMultiImageSource成功後也未追蹤刪除。結合無 size 上限 → 反覆上傳磁碟必爆。
契約必做項(WP-2,非「查證」):
- size 上限(各自值,必做):video / image / batch-images 各自的上傳 size 上限納入契約。
- video 上限:與前端 90MB 對齊,硬上限不超過 500MB(security 明確要求「不要因為走 loopback 就給 1GB」——攻擊面不因 loopback 縮小)。建議實作值 = 前端 90MB(正常上限);server 端
http.MaxBytesReader硬牆設 ≤ 500MB 作為 DoS 上界。 - image / batch 單張沿用既有逐張限制;batch 合計上限 =
MAX_BATCH_TOTAL_BYTES = 80MB(§2.2,補 nginx 100M 的 20% 餘裕;此上限對 localhost 直連仍保留,作為資源上界)。 - 驗證時機:size 檢查與 token 驗證都放在讀 multipart body(
FormFile)之前(review m3)。
- video 上限:與前端 90MB 對齊,硬上限不超過 500MB(security 明確要求「不要因為走 loopback 就給 1GB」——攻擊面不因 loopback 縮小)。建議實作值 = 前端 90MB(正常上限);server 端
- temp 檔清理(必做):
stopActivePipeline切換 / 停止 pipeline 時對前一支videoPath補os.Remove;batch 的MultiImageSource在生命週期結束時刪除其filePaths。反覆上傳不得殘留 temp 檔(testing 寫回歸測試驗證)。 | R-5 | 企業防火牆 / EDR 攔截 localhost HTTP | 低 | 明確錯誤訊息讓使用者知道要檢查什麼 |
註:第一輪風險表中的 R-2(Safari 相容性)因使用者確認僅支援 Chromium,已移除。
5. security agent 裁決事項(已裁決)
依 Architect CLAUDE.md §14.3 邊界表,token / secret 管理與隱私洩露屬 security agent 審查範圍。以下兩點已經 security agent 審查裁決(.autoflow/05-implementation/review/adr-019-security-review.md 議題 1 / 議題 2),契約內容已依裁決更新:
| # | 議題 | 裁決結果(已落實於本 ADR / api-spec) |
|---|---|---|
| 1 | GET /api/local/hello 回傳完整 serial 還是雜湊 serial? |
已裁決:回「salted SHA-256 雜湊」,不回完整 serial。 serialHashes[i] = SHA-256(公開固定 salt || fullSerial) 的 hex,salt 常數 = visiona-local-v1(前後端共用、非 server 私有,讓前端可獨立重算比對);agentVersion 粗化或移除(改回布林 supportsLocalUpload: true 已足夠);hello 不回任何其他欄位(最小揭露)。契約見 api-spec §6.3。殘留風險(序號熵低 + salt 公開 → 有心攻擊者可枚舉回推)security 判定「與威脅相稱、不需過度設計」(序號非 credential、控制裝置仍需 token+OIDC)。 |
| 2 | one-time token 設計是否足夠 + DNS rebinding 完整緩解 | 已裁決:token 核心設計通過。 附加要求已落實:(a)C1 route 分離(§2.4.1,token 認證不得被無 Origin 繞過,是 token 設計的前置 blocker);(b)Host 驗證必做(§2.5,rebinding 主防護,非 optional);(c)CORS 完整 origin 精確比對 + Allow-Credentials: false(§2.5);(d)token race 用 single-flight 持鎖(實作注意項 m2)。upload handler 不需在內部再綁 Origin 二次驗證(CORS 已做、且避免與 C1 方案衝突)。 |
6. 本 ADR 修正 / supersede 的內容
| 文件 | 章節 | 原內容 | 修正為 |
|---|---|---|---|
api/api-spec.md |
§6 Camera / Media | 「與 local-tool 相同,全部透過 tunnel forward」 | 影片 / 圖片 / 批次上傳走 localhost 直連;Camera 與推論結果串流仍走 tunnel(見本 ADR) |
design-doc.md |
§5.3 之後 | 「大檔不走 tunnel」原則只涵蓋模型上傳 | 新增一節說明 media 上傳套用同原則(localhost 直連),修補邏輯漏洞 |
註:本 ADR 不 supersede 任何既有 ADR,僅修正上述兩份設計文件的敘述。ADR-018(serial 路由)的識別慣例被本 ADR 沿用(§2.3),無衝突。
7. 合規性
- 與使用者確認方向(同機 localhost 直連 + 混合路徑 + 範圍含影片/圖片/批次 + 非同機停用 + 僅 Chromium + 認證必做)— ✅ 已裁決
- security agent 審 token / 隱私設計(§5 兩點)— ✅ 已審(初審 REQUEST CHANGES [1C+3M] → 契約已依 C1/M1/M2/M3/議題1 修正 → confirm-only 複審 → code-level 複審 APPROVED)
- backend agent 落地 local-agent CORS/PNA/token/endpoints + 雲端 ticket endpoint — ✅ 已 merge(PR #1)
- frontend agent 落地 port 探測抽象(WP-3)+ 批次合計檢查(WP-7)— ✅ 已 merge(PR #1)
- frontend agent 影片分頁接線(WP-4) — ⏳ 未做(下一批)。WP-3 port 探測抽象與 WP-7 批次合計檢查已完成,但「影片分頁 UI 切到
/api/local/media/upload/video新路徑」屬 WP-4,尚未落地 → 端到端功能未對使用者啟用、90MB 過渡上限仍生效 - testing agent 回歸:既有 tunnel 路徑(無 Origin)不受 token 影響 — ✅ 270 測試全綠、既有 tunnel 未打斷、無 regression
- 成本影響:無新雲端資源(沿用現有 DB / tunnel),主要是三 module 開發工時(見 §8 WP 清單);反而降低 stage 磁碟與頻寬壓力 — ✅ 已實作,符合預期(無新增雲端資源)
Backlog(本次 merge 未處理,記錄於此供後續追蹤):
- 同機程序存取控制(優先級 Low):既有 loopback 攻擊面(非本 ADR 新增,見 §2.4.1 誠實揭露的殘留風險)。同機惡意程序仍可直打舊 tunnel-path route;根治需 OS 層 peer credential / socket 權限驗證,超出本 ADR 範圍。
- WP-4 接線時的真實 Kneron 序號實機驗收:需做真實序號的前後端 hash 實機驗證——後端
SHA-256輸入的序號格式(0x%08X大寫)與前端serialNumber須同形,否則 hash 不符會誤判非同機。此為 fail-closed(不符即停用分頁),WP-4 接線時必驗。
8. WP 工作包清單(給 Orchestrator 排程)
完整依賴圖 / 關鍵路徑 / work-stream 同步點見
.autoflow/04-architecture/video-localhost-wp-breakdown.md(個人層)。此處為摘要。
本清單已依 security review 更新(C1 route 分離、M1 size 上限 + temp 清理必做、M2 Host 驗證必做、M3 CORS 完整 origin 精確比對)。
實作狀態(2026-07-30,PR #1 merge commit
3eaf3dc):WP-1 / WP-2 / WP-3 / WP-5 / WP-7 ✅ 已完成並 merge;WP-0 / WP-4 / WP-6 為下一批(未做)。底層能力已具備,但 WP-4(影片分頁接線)未做 → 前端 UI 尚未切到新 localhost 路徑、端到端功能未對使用者啟用、90MB 過渡上限仍生效。
| WP | 內容 | 負責 agent | 依賴 | 人時 | 狀態 |
|---|---|---|---|---|---|
| WP-0 | Spike:Chrome/Edge 實測 https/http 入口 → http://127.0.0.1:3721 + PNA console warning |
frontend | 無 | 2–4 | ⬜ 下一批(未做) |
| WP-1 | local-agent CORS / Host / PNA:雲端 origin 完整 origin 精確比對(獨立於既有 hostname-only 邏輯,不放寬既有 scheme 檢查)+ Allow-Credentials: false(雲端 origin) + Host header 驗證 = loopback(M2 必做,套用 /api/local/* 與舊 media route) + PNA header + Access-Control-Max-Age: 600 |
backend(Go) | 契約 §2.5 | 5–7 | ✅ 已完成並 merge |
| WP-2 | local-agent:新 route /api/local/media/upload/{video|image|batch-images}(一律要 token、不看 Origin、內部轉呼叫既有 handler) + /api/local/hello(回 salted SHA-256 serialHashes) + /api/local/issue-token + token store(single-flight 持鎖)+ token 驗證中介(放 FormFile 前)+ media size 上限(video ≤500MB 硬牆 / batch 合計 80MB,M1 必做) + temp 檔清理(stopActivePipeline 補 os.Remove + batch 生命週期刪檔,M1 必做) |
backend(Go) | WP-1 | 9–13 | ✅ 已完成並 merge |
| WP-3 | 前端:lib/local-agent.ts(port 探測並發+快取+timeout、同機判定用 salted SHA-256 比對 serialHashes、uploadToLocalAgent() endpoint 無關通用函式;上傳目標改新 route /api/local/media/upload/*) |
frontend | 契約 §2.3/§6.3 | 6–10 | ✅ 已完成並 merge |
| WP-4 | 前端:影片分頁接線(取 token、改上傳目標為 /api/local/media/upload/video、三種錯誤訊息 i18n、tunnel 離線檢查 R-3) |
frontend | WP-3 | 4–6 | ⬜ 下一批(未做) — 端到端啟用的關鍵缺口;接線時須做真實 Kneron 序號前後端 hash 實機驗收(後端 0x%08X 大寫 vs 前端 serialNumber 須同形、fail-closed) |
| WP-5 | 雲端:POST /api/devices/:serial/local-upload-ticket(經 tunnel 轉發 issue-token) |
backend(Go) | 契約 C-1 | 2–4 | ✅ 已完成並 merge |
| WP-6(可選) | 圖片 + 批次接上 localhost 路徑 | frontend | WP-4 驗證通過 | 3–5 | ⬜ 下一批(未做) |
| WP-7(獨立,建議先做) | validateBatchFiles 加合計大小檢查 |
frontend | 無 | 0.5–1 | ✅ 已完成並 merge |
推薦範圍(WP-0..WP-5 + WP-7)總計:26.5–43 小時 ≈ 3.3–5.4 人天。
關鍵路徑(串行、決定最短工期):契約定稿 → WP-0 → WP-1 → WP-2 → 整合 → E2E ≈ 14–22 小時。 可平行:WP-3(前端對契約 mock)與 WP-1/WP-2(backend)完全平行;WP-5(雲端 ticket)與 WP-1/WP-2 平行;WP-7 完全獨立。建議開 2–3 條 work-stream(frontend / local-agent backend / cloud backend),不宜再切。
9. 一句話總結
影片 / 圖片 / 批次上傳從「經雲端 tunnel(頻寬雙倍 + 撞 nginx 100M + 300s timeout + stage 落地)」改為同機瀏覽器直連 local-agent 的 localhost endpoint(handler 零改動、route 已存在),修補 design-doc.md §5.3「大檔不走 tunnel」原則未套用到 media 的邏輯漏洞;採混合路徑(上傳走 localhost、控制面 + MJPEG 結果 + 推論 WS 仍走 tunnel);動態 port 用前端掃描 3721–3740 + serial 身分驗證偵測同機;認證用經既有 tunnel 下發的 one-time token(縱深防禦,擋 XSS / DNS rebinding);CORS 擴充雙入口白名單 + PNA header(防未來 Chrome 升級 blocking);非同機停用該分頁(產品定位「跨機不存在」,不做 fallback)。token / 隱私設計待 security agent 審。