visionA/docs/autoflow/04-architecture/adr/adr-019-local-direct-media-upload.md
jim800121chen 4c962dfec1 docs(arch): ADR-019 影片/圖片/批次改走同機 localhost 直連(契約定稿)
新增 ADR-019:影片/圖片/批次上傳從經雲端 tunnel 改為同機瀏覽器直連
local-agent 的 localhost endpoint,修補 design-doc §5.3「大檔不走 tunnel」
原則未套用到 media 的邏輯漏洞。採混合路徑(上傳走 localhost、控制面 +
MJPEG 結果 + 推論 WS 仍走 tunnel)。

已完成 security pre-implementation review(1 Critical + 3 Major)+ architect
依審查修正契約 + security confirm-only 複審 APPROVED。契約含:
- C1: route 分離(新 route /api/local/media/upload/* 一律要 token、不看 Origin),
  消除「無 Origin 免 token」後門
- M2: Host header 驗證 = loopback 升為必做(DNS rebinding 緩解)
- M3: CORS 雲端 origin 完整字串精確比對 + Allow-Credentials: false
- 議題1: serial 回 SHA-256(visiona-local-v1‖serial) 雜湊、移除 agentVersion
- M1: media size 上限(video≤500MB / batch 合計 80MB)+ temp 檔清理升為必做

ADR 維持 Proposed,待實作完成 + code-level security 複審後轉 Accepted。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-30 03:31:16 +08:00

241 lines
27 KiB
Markdown
Raw 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.

# ADR-019: 影片 / 圖片 / 批次上傳走同機 localhost 直連 local-agent混合路徑
## 狀態
Proposed尚未實作。**已依 security review`.autoflow/05-implementation/review/adr-019-security-review.md`修正契約C1 / M1 / M2 / M3 / 議題1待 security confirm-only 複審 + 實作完成後轉 Accepted。** C1 定案採方案 Aroute 分離),見 §2.4。
## 日期
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 磁碟落地** | 即使調大 nginx10 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 serverGin強制綁 `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 / EdgeChromium 系。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``37223740 並發``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 → 內部轉呼叫既有 handlerhandler 零改動)
```
Token 設計方向(已經 security agent 審,核心設計通過,見 review 議題 2
- 生成 `crypto/rand` 32 bytes → base64url**不准 `math/rand`**TTL 120sone-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 C1Critical 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-35573)。tunnel **未在轉發時注入任何可辨識標記** tunnel clientWails app shell module `visiona-agent` HTTP server獨立子行程 module `server`**兩個獨立行程**。結論**同機程序可完全偽裝成tunnel 來的請求」**server 端無法用 request 本身區分
**定案:採方案 Aroute 分離)**不採方案 Btunnel 注入 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 驗證內部轉呼叫既有 handlerhandler 零改動)。
- **既有 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——`Host` header去掉 port 必須 `{127.0.0.1, localhost, ::1}`否則 `400 Bad Request`這是**獨立於 CORS 的第二道**、 DNS rebindingrebinding `Host` 為攻擊者網域 loopback 400成本低對本機服務屬 baseline 而非 optionaltunnel 轉發的 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/EdgeChrome 未來可能把 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→loopback0.12.5s,改善約 1001000 倍);不受 nginx / timeout 約束handler 零改動 | 需同機操作local-agent 新增對外攻擊面需認證PNA 未來風險 | ✅ 採用 |
---
## 4. 後果 (Consequences)
### 4.1 正面影響
- **頻寬**500MB 影片從「1000MB 經雲端 tunnel」降為「loopback 本機傳輸」(現代 SSD 筆電 0.10.5s、較舊機器 12.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即時攝影機仍支援遠端檢視**——只有「上行大檔」需同機限制。
- 依使用者拍板「跨機不存在」,此限制不影響實際使用;且現況跨機使用者本來就傳不了 >100MB413此限制是「把隱性限制變成明確提示」非新增限制。
- **不動 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 M1temp 檔洩漏**已確認為實錘**(非待查證)——`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-2Safari 相容性)因使用者確認僅支援 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)` 的 hexsalt 常數 = `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.1token 認證不得被無 Origin 繞過,是 token 設計的前置 blockerb**Host 驗證必做**§2.5rebinding 主防護,非 optionalc**CORS 完整 origin 精確比對 + Allow-Credentials: false**§2.5dtoken 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-018serial 路由)的識別慣例被本 ADR **沿用**§2.3),無衝突。
---
## 7. 合規性
- [x] 與使用者確認方向(同機 localhost 直連 + 混合路徑 + 範圍含影片/圖片/批次 + 非同機停用 + 僅 Chromium + 認證必做)— ✅ 已裁決
- [x] **security agent 審 token / 隱私設計**§5 兩點)— ✅ 已審verdict: REQUEST CHANGES → 契約已依 C1/M1/M2/M3/議題1 修正,待 confirm-only 複審)
- [ ] backend agent 落地 local-agent CORS/PNA/token/endpoints + 雲端 ticket endpoint
- [ ] frontend agent 落地 port 探測抽象 + 影片分頁接線 + 批次合計檢查
- [ ] testing agent 回歸:既有 tunnel 路徑(無 Origin不受 token 影響跨瀏覽器Chrome/Edge驗證
- [ ] 成本影響:**無新雲端資源**(沿用現有 DB / tunnel主要是三 module 開發工時,粗估 3.35.4 人天(見 §8 WP 清單);反而降低 stage 磁碟與頻寬壓力
---
## 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 精確比對)。
| WP | 內容 | 負責 agent | 依賴 | 人時 |
|----|------|-----------|------|------|
| WP-0 | SpikeChrome/Edge 實測 `https/http 入口 http://127.0.0.1:3721` + PNA console warning | frontend | 無 | 24 |
| WP-1 | local-agent CORS / Host / PNA雲端 origin **完整 origin 精確比對**(獨立於既有 hostname-only 邏輯,**不放寬既有 scheme 檢查**+ **`Allow-Credentials: false`(雲端 origin** + **Host header 驗證 = loopbackM2 必做,套用 `/api/local/*` 與舊 media route** + PNA header + `Access-Control-Max-Age: 600` | backendGo | 契約 §2.5 | 57 |
| 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 storesingle-flight 持鎖)+ token 驗證中介(放 `FormFile` 前)+ **media size 上限video ≤500MB 硬牆 / batch 合計 80MBM1 必做)** + **temp 檔清理(`stopActivePipeline` 補 `os.Remove` + batch 生命週期刪檔M1 必做)** | backendGo | WP-1 | 913 |
| WP-3 | 前端:`lib/local-agent.ts`port 探測並發+快取+timeout、同機判定用 salted SHA-256 比對 serialHashes、`uploadToLocalAgent()` endpoint 無關通用函式;上傳目標改新 route `/api/local/media/upload/*` | frontend | 契約 §2.3/§6.3 | 610 |
| WP-4 | 前端:影片分頁接線(取 token、改上傳目標為 `/api/local/media/upload/video`、三種錯誤訊息 i18n、tunnel 離線檢查 R-3 | frontend | WP-3 | 46 |
| WP-5 | 雲端:`POST /api/devices/:serial/local-upload-ticket`(經 tunnel 轉發 issue-token | backendGo | 契約 C-1 | 24 |
| WP-6可選 | 圖片 + 批次接上 localhost 路徑 | frontend | WP-4 驗證通過 | 35 |
| WP-7獨立建議先做 | `validateBatchFiles` 加合計大小檢查 | frontend | 無 | 0.51 |
**推薦範圍WP-0..WP-5 + WP-7總計26.543 小時 ≈ 3.35.4 人天。**
**關鍵路徑(串行、決定最短工期)**:契約定稿 → WP-0 → WP-1 → WP-2 → 整合 → E2E ≈ 1422 小時。
**可平行**WP-3前端對契約 mock與 WP-1/WP-2backend完全平行WP-5雲端 ticket與 WP-1/WP-2 平行WP-7 完全獨立。建議開 23 條 work-streamfrontend / 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 用前端掃描 37213740 + serial 身分驗證偵測同機認證用經既有 tunnel 下發的 one-time token縱深防禦 XSS / DNS rebindingCORS 擴充雙入口白名單 + PNA header防未來 Chrome 升級 blocking非同機停用該分頁產品定位跨機不存在」,不做 fallback)。token / 隱私設計待 security agent