# 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:`,`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:/api/local/media/upload/video ← 新的、一律要 token 的 route Header: X-Visiona-Local-Token: [4] local-agent 驗 token(存在 / 未過期 / 未用過 / deviceId 相符)+ 驗 Host = loopback(§2.5) → 消耗 token → 內部轉呼叫既有 handler(handler 零改動) ``` Token 設計方向(已經 security agent 審,核心設計通過,見 review 議題 2): - 生成 `crypto/rand` 32 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:`**(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:`,普通 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)。理由: 1. tunnel client 與 HTTP server 是兩個獨立行程 / 獨立 Go module(見 client.go 檔頂註解),方案 B 的「process-local 共享 secret」實際上需要跨行程 IPC 傳遞 nonce,脆弱且增加耦合;方案 A 不需任何跨行程協調。 2. 方案 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:`)。完整消除同機程序攻擊面超出本 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——`Host` header(去掉 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:`(見 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 的 `UploadVideo` handler 一行不用改(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)。 - **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. 合規性 - [x] 與使用者確認方向(同機 localhost 直連 + 混合路徑 + 範圍含影片/圖片/批次 + 非同機停用 + 僅 Chromium + 認證必做)— ✅ 已裁決 - [x] **security agent 審 token / 隱私設計**(§5 兩點)— ✅ 已審(初審 REQUEST CHANGES [1C+3M] → 契約已依 C1/M1/M2/M3/議題1 修正 → confirm-only 複審 → code-level 複審 **APPROVED**) - [x] backend agent 落地 local-agent CORS/PNA/token/endpoints + 雲端 ticket endpoint — ✅ 已 merge(PR #1) - [x] 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 過渡上限仍生效 - [x] testing agent 回歸:既有 tunnel 路徑(無 Origin)不受 token 影響 — ✅ 270 測試全綠、既有 tunnel 未打斷、無 regression - [x] 成本影響:**無新雲端資源**(沿用現有 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 審。