visionA/docs/autoflow/04-architecture/adr/adr-017-model-library-access.md
jim800121chen 3e45532f55 fix(model-download): 改 redirect/query-string token 下載,根除 CORS preflight 405
模型庫下載原本前端用 fetch + Authorization: Bearer 跨 origin 直連 FAA,
觸發 CORS preflight(OPTIONS);FAA 未設 CORS、OPTIONS 回 405 → 下載失敗。

FAA 設計本就支援「token 放 query string(access_token) + redirect 導航下載」,
故不需 FAA 設 CORS。改採 ADR-017 §11 v1.3 乙案:

- backend: download_url 組成含 ?access_token={url.QueryEscape(token)} 完整 FAA URL;
  ModelDownloadResponse.Token 標 deprecated 保留(向下相容);token 不進 log
- frontend: 移除 downloadModelFile/deriveDownloadFilename(fetch+blob),
  改 triggerNavDownload 用 <a download href> 導航;ModelDownloadGrant 移除 token 欄
- 整條鏈無 CORS:前端→backend 同 origin、前端→FAA 導航無 preflight

docs: ADR-017 增補 §11(v1.3) + TDD.md §9.5
tests: backend 8 PASS(含 token escape 邊界)、frontend 35 PASS;Reviewer 通過(0C/0M)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-27 06:18:05 +08:00

68 KiB
Raw Blame History

ADR-017: 模型庫存取架構File Access Agent 重設計)

狀態

Proposed待使用者裁決v1.3 修訂2026-06-27瀏覽器下載改 query-string token + redirect推翻 v1.2 決策 2 / §10.4 的「fetch + Authorization Bearer」徹底解 CORS 405。詳見 §11。 v1.2 修訂2026-06-07(a) 已用 stage 真實環境 + 真 secret + 真 user e2e 實測打通,跨團隊 blocking 歸零,剩餘全是 visionA 端純實作。 保留 v1.0(推 (c)/ v1.1(改推 (a)、待跨團隊驗證)歷史。

日期

2026-06-06v1.0/ 2026-06-06v1.1 修訂)/ 2026-06-07v1.2 修訂)

作者

Architect Agent


0. v1.1 修訂註記(重要 — 推翻 v1.0 前提)

v1.0(同日稍早)推薦決策 1 = (c) visionA 自簽 token,核心理由是「(a) 回到 MC token 那條鏈一年沒生出來、是 fictional見 ADR-016 致命發現)」。

這個前提在 v1.1 被推翻。 重新驗證 source 後發現上週ADR-016判 (a) fictional 時只 grep 了 MC master 分支,沒看 develop。實際上

  1. MC develop 分支已實作 POST /file-access/download-tokensIssue+ POST /file-access/download-tokens/validateValidate兩個 endpointcommit e77fdecmaster 尚未 merge。
  2. FAA 端 MemberCenterDelegatedDownloadTokenValidator.cs 本來就是配套:它打 MC validate 的 payloadtoken/tenant_id/file_id/object_key/method)與 responseactive/...)形狀,與 MC develop 的 validate endpoint 契約吻合(見本 ADR §9 驗證證據)。
  3. 所以 ADR-014 那條 visionA → MC → FAA delegated token 鏈,在 source level 是通的,缺的只是「部署 + scope 註冊 + 跨 repo 設定」,不是「程式碼不存在」。

使用者原本就想 follow pptx 設計(走 MC token = (a))。 既然 (a) 不再是「賭 MC 一年生不出來的 endpoint」、而是「已寫好、待部署」本 ADR v1.1 改推 (a),並把 (c) 降為「跨團隊部署若卡住的 fallback」。

但 v1.1 必須誠實標記source 就緒 ≠ 零風險。(a) 仍有跨團隊部署依賴warrenchen 要 merge develop→master + 部署 MC stage + 設 FAA stage MemberCenterOptions + MC 端註冊 visionA service client 的 files:download.read/delegate scope。這條鏈過去 5/9 就因「scope 沒在 MC 註冊」撞 invalid_scope——同類風險仍在。詳見 §4 決策 1改寫與 §9可行性驗證

被 supersede 的內容v1.0 §4 決策 1 的「推薦 (c)」結論、決策 2 的「visionA 自簽 JWT」具體流程、§5 落地順序 P0、§6 Q1、§7 R1。原文保留於下方並標 刪除線 / 修訂框,保留歷史脈絡。


0.1 v1.2 修訂註記(重要 — (a) 已 stage 真實環境 e2e 實證打通)

v1.1 時 (a) 還是「source 就緒、但跨團隊部署 + scope 註冊是未知風險」P0 標「warrenchen 主導、不可控」、Q1.5 標「待跟 warrenchen 確認」、R1 標「5/9 撞過 invalid_scope 的同類最高風險」)。

v1.2 的事實MC stage 已部署、scope 機制已驗證、整條 (a) e2e 認證鏈用真 secret + 真 user_id 跑通了HTTP 200 簽出 fdt_ token、FAA 只因測試用假 object_key 回 404 file_not_found即認證鏈全綠。跨團隊 blocking 歸零,剩餘全是 visionA 端純實作。 完整證據見新增的 §10(a) 端到端 stage 實測證據)

v1.1 → v1.2 的核心轉變

  1. MC 部署確認stage POST /file-access/download-tokens/validate 回 401非 404→ endpoint 已部署(推翻 v1.0/v1.1「只看 git master 判 fictional」stage 實際版本已含 develop 功能)。
  2. scope 機制已驗證MC 用 usage→scope 模型(file_api usage 自動帶 5 個 files scopeFAA 既有 service client 4242ba63... 實測能拿 files:download.delegate token。R1 的「scope 沒在 MC 註冊」風險已實證可繞過(短期共用 FAA client、正式上線換 visionA 專屬)。
  3. 🏆 e2e 全綠:使用者提供真實 OIDC user_id整條 Issue→簽 fdt→FAA validate 鏈 stage 實測通過B3 tenant 一致、B4 真實 user、FAA validate 鏈全部實證)。

被 v1.2 supersede 的內容v1.1 §4 決策 1 的「(a) 待跨團隊部署驗證」、§5 P0「跨團隊部署鏈warrenchen 主導不可控」、§6 Q1/Q1.5/Q9「待確認」狀態、§7 R1「最高風險」定性。原文保留並標 v1.2 更新框,保留歷史脈絡。

剩餘 visionA 端 blockingv1.2 標清楚、為 backend agent 接下來的工作)B1 object_key 斷層(最關鍵,第一階段 (a) 只支援轉檔→promote 類 model+ B2 visionA 加「打 MC /oauth/token + 打 MC Issue 簽 fdt」的 client code + .env 設定。詳見 §10.4。


1. 背景與範圍

visionA Cloud 的「模型庫」要處理的是大檔案(轉檔/訓練好的模型、APP 套件),這類資產的存取設計有兩個本質問題綁在一起:

  1. 流量費用 — S3 容量便宜但流量貴,大檔反覆下載不該每次都經過 AWS egress。
  2. 權限保護 — Bucket 位址不能曝露;就算有人拿到連結也不能直接下載,每次存取都必須先做身份/權限查驗。

本 ADR 收斂三個面向,並把它們放進使用者目標架構(模型庫存取架構.pptx,引入 File Access Agent + NAS Bucket的框架下一起決策而不是各自打 patch

  • B 模型存取權限/分享:現況 owner-only → 目標 share/workspace/role。
  • C 模型下載/檔案存取:現況沒有 model download endpoint,要設計 pptx 的「Client 直連 FAA 下載」。
  • D promote-to-models 修補stage 環境 promote 按鈕卡 OAuth 401要收斂進新架構而非孤立修補。

1.1 pptx 目標 vs 現況的根本衝突(本 ADR 存在的理由)

pptx 的上傳/下載流程都假設一條 token 鏈:「visionA 向 MC 取 Token → 帶 Token 給 FAA → FAA 向 MC 驗 Token」

但歷史盤點(見 §2顯示MC 沒有簽發 delegated download token 的 endpoint也沒有 FAA 驗 token 所需的 introspection endpoint。ADR-014 §2 描述的這條鏈從 2026-05-02 起就是 fictional、從未 e2e 通過,上週 ADR-016 才改走「converter 中轉」繞開。

v1.1 修正上述「MC 沒有 endpoint」的判斷只成立於 MC master 分支。MC develop 分支commit e77fdec已實作 Issue + Validate 兩個 endpointFAA 端 validator 本來就是配套設計。所以這條鏈在 source level 是通的、只差部署。詳見 §0 修訂註記與 §9 可行性驗證。本 ADR 改在「使用者想 follow pptx走 MC token」+「MC develop 已就緒、待部署」的新事實下,決定推動 (a) 並列出跨團隊部署 checklist。


2. 現況盤點

面向 現況事實 Source
A. FAA 端能力 GET /files/{key}(下載)沒掛 RequireAuthorization(),改用 IDelegatedDownloadTokenValidator.ValidateAsync() 驗 delegated download token。PUT/GET metadata/HEAD/DELETE 走 JWT Bearer + EnsureJwtScopeAndTenantscope: files:upload.write / files:metadata.read / files:delete。即 FAA 是 dual-authdownload=delegated token、其他 op=service JWT。 file_access_agent/.../Program.cs:184-254
A'. FAA download 原生支援 FAA download 本來就是為「帶 delegated token 直接下載」設計 → pptx 的「Client 直連 FAA 下載」FAA 端原生支援FAA 不是阻礙。 同上
B-新事實. MC develop 已有 token 簽發 MC developcommit e77fdec已實作 POST /file-access/download-tokensIssue, [Authorize(Policy="FilesDownloadDelegate")],回 opaque fdt_<base64url> token + scope files:download.read + 自訂 TTLmaster 尚未 merge——上週只 grep master 才誤判不存在。 MC develop FileAccessController.csOrchestrator 已驗)
B-新事實'. MC develop 已有 token 驗證 MC develop 已實作 POST /file-access/download-tokens/validateValidate, [Authorize(Policy="FilesDownloadRead")],驗 TokenHash + RevokedAt + ExpiresAt + boundary(tenant/file/object_key/method),回 {active, scope, expires_at}。FAA validator 的 payload/response 形狀與此契約吻合 MC develop / FAA validator本 ADR §9
B-結論修正. token 鏈 source 已就緒、待部署 ADR-014 §2 的「visionA→MC→FAA delegated token 鏈」在 source level 是通的MC develop + FAA validator 配套。缺的是MC merge develop→master + 部署 stage + 註冊 visionA service client 的 download scope + FAA stage MemberCenterOptions 設定。不再是 fictional而是「跨團隊部署待辦」。 MC develop / FAA / ADR-014
C. promoteconverter→FAAprod work converter promote → FAA PUT NEF 走 OAuth client_credentials + scope files:upload.writeprod 已上線可用 apps/task-scheduler/.../fileAccessAgent/client.js
C'. promote stage 卡 401 stage 上 promote 按鈕卡 OAuth 401 → 研判是 stage converter 的 OAuth client 設定 / FAA accepted scope 問題(設定問題,非設計問題)。 progress.md
C''. NEF 雙存 converter MinIO 存 NEFpromote 後 NEF 同時在 converter MinIOexpires_at=7天+ FAA。 ADR-016
D. visionA download 現況 visionA → converterinit/poll/promote/result download全走 API keyADR-015 §1。visionA backend 有 stream proxy 結構(flow.go DownloadStreamio.CopyN size cap + Content-Disposition + context cancellation現況 stream 來源是 converter /result ADR-015 / internal/conversion/flow.go
D'. 無 model download endpoint internal/api/models.go/api/models/* 有 list/get/init/finalize/deleteload-to-device 是 stub無 model download endpoint internal/api/models.go
E. 資料模型現況 DB models table 是 owner_user_id UUID NOT NULL(單一擁有者)+ CREATE INDEX ON models (owner_user_id) WHERE deleted_at IS NULLmodel.go Model struct 只有 OwnerUserIDRepository.List 用 OwnerUserID 過濾,無 share/workspace/role/ACL internal/model/model.go / database.md:344-363

現況一句話總結FAA 設計上 readydownload 收 delegated token、op 收 service JWTconverter→FAA 上傳 prod work但「誰簽發/驗證 download token」這一環MC 從未提供visionA 也還沒自建所以模型下載至今走不通、model download endpoint 根本還沒長出來。


3. 目標架構pptx解讀

3.1 核心設計

引入 File Access Agent (FAA) + NAS Bucket搭配 AWS S3。大檔存取統一收口到 FAA

  • Bucket 位址不曝露Client 只跟 FAA 互動,不知道也碰不到底層 Bucket
  • 每次存取都過 FAA 的權限查驗。
  • 未來可在 FAA 層加密。
  • 省流量:除了「上傳那一次」大檔流量不經 AWS下載走 FAANAS/公司網路),不吃 AWS egress。
  • 轉檔/訓練好的模型可以只傳網址讓 FAA 自己去抓converter→FAA 已經是這個模式的雛形)。

3.2 pptx 上傳流程

  1. POST visionA endpoint
  2. visionA 向 MC 取 Token
  3. 檔案(連結) + Token 送 FAA
  4. FAA 向 MC 驗 Token
  5. FAA 回檔案 path 給 visionA

3.3 pptx 下載流程

  1. visionA 驗存取權限
  2. visionA 向 MC 取 Token
  3. Client 帶 Token 直接向 FAA 發 GET
  4. FAA 向 MC 驗 Token
  5. Response 檔案到 Client不經 visionA、不經 AWS

3.4 pptx 灰色地帶(必須在 §4 解掉)

# 灰色地帶 問題
G1 「visionA 向 MC 取 Token」 MC 沒有簽發 download token 的 endpoint v1.1MC develop 已有 Issue endpoint事實 B-新事實)。可做到,但 visionA 需「復活 MC service token client」+ MC 端註冊 download scope見 §9 缺口)。
G2 「FAA 向 MC 驗 Token」 MC 沒有 introspection endpoint v1.1MC develop 已有 Validate endpoint、FAA validator 本來就是配套(事實 B-新事實'。可做到,但需 FAA stage 設好 MemberCenterOptions(見 §9 缺口)。
G3 Client 直連 FAA 的憑證怎麼到 Client 手上 pptx 說「Client 帶 Token」但沒講 token 範圍(單檔/全庫)、有效期、防盜用(被截走能不能重放)。
G4 Bucket 不曝露但 FAA 對外 FAA 要對 Client含桌面 local-tool / 瀏覽器開放CORS、FAA 對外網段、HA 都得想。
G5 pptx 自列待解 ①HANAS/公司網路掛掉就全掛)②頻寬(下載走公司網路,先 router 限流,未來移轉便宜雲端空間)。

G1 + G2 是本 ADR 決策 1/決策 2 的核心使用者已拍板「Client 直連 FAA」+「要驗權限」,但簽發/驗證 token 的 MC 能力不存在。所以必須決定 delegated token 由誰簽、由誰驗


4. 關鍵決策點

決策 1認證方向download token 誰簽發、誰驗證)— v1.2 改寫((a) 已 stage e2e 實證)

v1.2 重大更新(a) 已用 stage 真實環境 + 真 secret + 真 user_id e2e 實測打通HTTP 200 簽出 fdt_ token、FAA validate 鏈全綠,僅因測試用假 object_key 回 404 file_not_found見新增 §10(a) 從 v1.1 的「已寫好、待跨團隊部署驗證」升級為「已實證可行、跨團隊 blocking 歸零」。 剩餘全是 visionA 端純實作object_key 斷層 + 打 MC Issue 的 client code見 §10.4)。(c) fallback 實測後已無需動用——除非未來改用正式專屬 file_api client 時受阻,否則 (a) 直接落地。

v1.1 原文(保留供對照)v1.0 在「(a) 是 fictional」的錯誤前提下推 (c)。新事實MC develop 已實作 Issue+Validate、FAA validator 配套,見 §0/§9(a) 從「賭一年沒生出來的 endpoint」變成「已寫好、待跨團隊部署」。使用者想 follow pptx= (a),故 v1.1 改推 (a)(c) 降為 fallback。

pptx 預設 MC 簽發 + MC 驗證。以下三案v1.1 重新評估):

維度 (a) MC delegated tokenpptx 原圖★v1.1 改推 (b) 延續 API key / pre-shared secret (c) visionA 簽短期 token + FAA 本地驗v1.0 原推v1.1 降 fallback
做法 visionA 打 MC POST /file-access/download-tokens 拿 opaque fdt_ token → Client 帶 token 打 FAA GET /files/{key} → FAA IDelegatedDownloadTokenValidator 打 MC validate Client 帶固定 pre-shared secret 直連 FAAFAA 比對 secret visionA 自簽短期 JWT含 file key + exp + 權限 claimFAA 用 visionA JWKS 本地驗簽
source 現況v1.1 新增) MC develop + FAA validator 都已寫好commit e77fdec),缺部署/scope 註冊/FAA stage 設定 無現成 FAA validator 目前 impl 是「打 MC validate」、不是「本地驗 visionA JWKS」 — 要 (c) 反而得叫 warrenchen 改寫 FAA validator(比 (a) 重!)
與 pptx 相容 完全相容(就是 pptx 原圖、使用者想要的) 半相容 相容token 由 visionA 取代 MC 角色)
對 MC / FAA team 依賴 — 不需「新寫」endpoint已寫好但需 warrenchen merge develop→master + 部署 MC stage + 設 FAA stage MemberCenterOptions + MC 註冊 visionA service client 的 download scope 中-高v1.1 重估) — FAA 現成 validator 是打 MC 的;改成本地驗 visionA JWKS 要 warrenchen 改 validator impl,這也是動 FAA、不比 (a) 輕
與現況落差 中 — visionA 端要復活 MC service token clientADR-016 剛砍掉 mc_token_client.go要逆轉+ 新增「打 MC Issue」邏輯 中 — visionA 要建簽發+JWKSFAA 要改 validator
防盜用(被截走) 佳 — MC 簽 opaque token + 短 TTL可自訂 expires_in_seconds+ boundary(tenant/file/object_key/method) + 可 revokeRevokedAt — secret 外洩=全庫淪陷 佳 — 短 exp + 綁 file key + 綁 user
HA / 延遲 每次下載 FAA→MC 同步 validate多一跳 + MC 成驗證單點(但 FAA validator 有 service token cachetoken validate 是輕量呼叫) 無外部依賴 無外部依賴FAA 本地驗簽,延遲最低
成本/工時 中 — visionA 復活 MC client逆轉 ADR-016+ 跨團隊部署協調(工期受 warrenchen 排程影響) 低(但留安全債) 中 — visionA 簽發+JWKS + FAA validator 改寫(跨團隊)
opaque token 限制v1.1 新增) token 是 fdt_<random> 不是 JWT → FAA 無法本地驗簽,每次必 call MC validate這是 MC 設計,符合 FAA 現成 validator JWT 可本地驗(但 FAA 現成 validator 不是這樣做的)

v1.2 推薦:(a) MC delegated tokenpptx 原圖)— 已 stage e2e 實證可行。 (c) 仍保留為 fallback,但實測後已無需動用(除非未來改正式專屬 client 受阻)。

改推 (a) 的理由

  1. (a) 不再是 fictional — MC develop + FAA validator 是配套的、已寫好§9 驗證。v1.0 推 (c) 的唯一核心理由「MC 那條鏈一年沒生出來」)已不成立。
  2. (c) 在新事實下反而更重 — FAA 現成的 MemberCenterDelegatedDownloadTokenValidator 是「打 MC validate」的 impl。要走 (c) 必須請 warrenchen 改寫 validator 成本地驗 visionA JWKS——這同樣是動 FAA、且是「改既有正確配套」比「部署既有 (a)」更不划算。
  3. 符合使用者意圖 — 使用者原本就想 follow pptx 設計(走 MC token。(a) = pptx 原圖。
  4. FAA / MC 端零程式碼改動 — (a) 在 source level 已就緒FAA / MC 不需寫新 code只需「部署 + 設定 + scope 註冊」。
  5. token 安全性最強 — MC 簽 opaque token支援 boundary 檢查 + revoke + 自訂 TTL比 visionA 自簽 JWT無法 revoke更完整。

(a) 的關鍵前置(跨團隊部署 checklist非程式碼

  1. [MC / warrenchen] merge develop(含 e77fdec+5f32452)→ master + 部署 MC stage。
  2. [MC / warrenchen] 在 MC 端把 visionA service client23605e14...)的 files:download.read + files:download.delegate scope 註冊到 AuthResourceRegistry(這是 5/9 撞 invalid_scope 的同類風險點——ADR-014 line 35 雖列了 4 scope但「列在 visionA 文件」≠「在 MC 端註冊」)。
  3. [FAA / warrenchen] FAA stage 部署 + 設好 MemberCenterOptionsBaseUrl / ClientId / ClientSecret / Scope / DownloadTokenValidationPath 指向 MC develop 的 /file-access/download-tokens/validate)。
  4. [visionA / jimchen] 復活 MC service token client(逆轉 ADR-016 §5 砍除的 mc_token_client.go+ 新增「打 MC Issue endpoint 拿 fdt_ token」邏輯。
  5. [三方] 對齊 boundaryvisionA Issue 時帶的 object_key 必須 = FAA GET /files/{key} 的 key = MC validate 的 object_key(見決策 2 與 §9 的 object_key 斷層分析)。

v1.2 更新:此前置清單 step 13、5 已 stage 實測驗證完成。 MC stage 已部署step 1§10.1scope 機制已驗證——FAA service client 4242ba63... 實測能拿 files:download.delegate token、boundarytenant/object_key/methode2e 通過step 2/5§10.2§10.3FAA stage 已部署且 MemberCenterOptions 已設好step 3§10.3 FAA validate 鏈全綠)。原「step 2 是最高風險、5/9 撞 invalid_scope」已實證可繞過(短期共用 FAA client 拿 token正式上線換 visionA 專屬 usage=file_api client見 §7 R1 改寫)。剩 step 4visionA 復活 MC client + 打 Issue= 唯一剩餘工作、純 visionA 端、自己可控。

何時 fallback 回 (c)v1.2:實測後已無需動用)v1.1 設想「若跨團隊部署卡住則 fallback (c)」。v1.2 跨團隊部署已 e2e 通、無卡點,(c) 不再需要動用。僅保留為「未來 visionA 改用正式專屬 file_api client、若 MC 端無法配發專屬 client 時」的理論退路。(b) pre-shared secret 仍不採用

v1.0 原推薦((c))保留供對照

推薦:(c) visionA 簽短期 download token + FAA 本地驗。 理由:(1) 解除對 MC 的歷史依賴——MC 那兩個 endpoint 一年沒生出來;(2) FAA 本來就是 dual-auth、有 validator 抽象,換 impl 即可;(3) 滿足 pptx 兩大訴求;(4) 未來不排斥 (a)。

v1.1 supersede 原因:理由 (1) 的前提「MC endpoint 一年沒生出來」是錯的(只看了 master理由 (2) 「FAA 換 impl 即可」低估了——FAA 現成 validator 正是 (a) 要的、(c) 才需要改寫它。


決策 2Client↔FAA 憑證傳遞與 Bucket 保護 — v1.2 改寫(流程已 stage 實測確認)

v1.2 實測確認:以下流程已用 stage 真實環境 + 真 secret + 真 OIDC user_id 跑通(見 §10.3 e2e。token 確認為 MC 簽的 opaque fdt_<base64url>120s、scope files:download.read、token_type file_download)。FAA download endpoint 確切用法(給 backend 實作):GET {FAA}/files/{objectKey}token 放 Authorization: Bearer {fdt_token}FAA TryReadAccessToken 只認 Authorization Bearer不認 query / 自訂 header)。objectKey 必須與簽 token 時的 object_key 完全一致FAA validate boundary 檢查、不一致回 object_key_mismatch)。

採用決策 1**(a)** 後下載流程具體化token 由 MC 簽,不是 visionA 自簽):

  1. Clientlocal-tool / browservisionA 請求下載某 model帶 visionA 使用者 session
  2. visionA 先驗存取權限owner / share / workspace / role見決策 3
  3. 通過 → visionA 用 MC service token 打 MC POST /file-access/download-tokensIssuetenant_id / user_id / file_id / object_key / method=GET / expires_in_secondsMC 回 opaque fdt_<base64url> token。
  4. visionA 回給 Client「FAA 下載 URL + fdt_ token」。
  5. Client 帶 token 直接 GET FAA /files/{object_key}Authorization: Bearer fdt_...——v1.2 實測FAA 只認 Authorization Bearer、不認 ?access_token= query不經 visionA、不經 AWS
  6. FAA MemberCenterDelegatedDownloadTokenValidator 打 MC POST /file-access/download-tokens/validate(帶 FAA 自己的 MC service token、instanceOptions.TenantId + 從 URL path 取的 ObjectKey + Method=GET)驗 token + boundarytenant/object_key/method→ MC 回 {active:true} → FAA 回檔。

注意:fdt_opaque random token、不是 JWT → FAA 無法本地驗簽,每次下載一定要 call MC validate這是 MC 刻意設計:可即時 revoke。延遲多一跳 FAA→MC但 FAA validator 有 service token cache見 FAA validator source line 74-129validate 本身是輕量 POST。

防盜用 / 不曝露設計

風險 對策
token 被截走重放 短 TTLvisionA Issue 時帶 expires_in_seconds,建議 60300s待裁決 Q2MC 端 boundary 綁 object_key + method(截走也只能下載那一個檔、那個 methodMC 端可 RevokedAt 即時撤銷
Bucket 位址曝露 Client 只拿到 FAA URL/files/{object_key}object_key 是 FAA 內部 storage key、不是 S3/NAS 真實路徑
CORSbrowser 直連 FAA FAA 需設允許 visionA 前端 origin 的 CORS待裁決 Q3是否需瀏覽器直連只 local-tool 直連無 CORS 問題)
FAA 對外網段 / HA 見決策 5
token 簽發濫用 visionA 端對「打 MC Issue」做 rate limit + audit log誰、何時、要下載哪個 model

⚠️ object_key 三方對映斷層v1.1 新增 — (a) 最大實作風險)

(a) 要求 visionA Issue 時帶的 object_key = FAA GET /files/{key} 的 key = MC validate 的 object_key,三方必須一致。但驗證現有 source 發現斷層

key 空間 source
visionA model storage key models/{userID}/{modelID}.nef internal/api/models.go:235visionA 自己的 S3/LocalFS key
converter promote → FAA 的 object_key flow.go 按命名規則組的 TargetObjectKeyFAA 內部 key converter_client.go PromoteReq.TargetObjectKey
FAA 實際存放的 object_key converter PUT 進去的 key FAA PUT /files/{objectKey}

斷層本質

  • visionA model store 的檔案是走 /api/models/init+finalize 上傳到 visionA 自己的 storagemodels/{userID}/{modelID}.nef從未經過 FAA
  • 只有「轉檔結果 NEF」會經 converter promote 進 FAA用另一套 TargetObjectKey)。
  • 所以「model download (a) 直連 FAA」的前提——model 必須在 FAA 上、且 visionA 知道它的 FAA object_key——目前不成立
    • 上傳類 modelSourceUploaded根本不在 FAA 上,無法走 (a) 直連 FAA。
    • 轉檔類 modelSourceConverted若有 promote 進 FAAvisionA 需記錄該 model 的 FAA object_key(目前 model.Model struct 沒有 faa_object_key 欄位,只有 visionA 自己的 StorageKey)。

(a) 要可行,必須補的 visionA 端工作(決策方向,細節由 backend agent 落地)

  1. model.ModelFAAObjectKeynullable欄位記錄 model 在 FAA 上的 keypromote 時寫入)。
  2. 釐清「哪些 model 在 FAA 上」:只有經 converter promote 的才有 FAAObjectKey純上傳的 model 要 (a) 直連 FAA 需先有「把上傳檔也 PUT 進 FAA」的路徑目前沒有
  3. download flow 用 FAAObjectKey(不是 StorageKey)去 Issue MC token + 組 FAA URL。

這個斷層是 (a) 比 (c) 多出的實作成本,且影響「上傳類 model 能不能走 (a)」——須在 Q1 裁決時一併考慮(見待裁決 Q7

v1.0 原決策 2visionA 自簽 JWT保留供對照

採用決策 1(c)visionA 簽發短期 JWTclaim: file_key/sub/exp/scope、FAA 用 visionA JWKS 本地驗簽。 v1.1 改為 MC 簽 opaque fdt_ token + FAA 打 MC validate。


決策 3B — 權限/分享資料模型owner-only → share/workspace/role

現況 models.owner_user_id NOT NULL 單一擁有者(事實 E。最小可行變更不一次做到完整 RBAC,只加「能分享 + 能用 workspace 群組 + 三種 role」

方案比較(最小可行 vs 完整):

維度 (B1) 只加 model_sharesper-model 直接分享)★第一階段推薦 (B2) 加 workspaces + memberships + model 歸屬 workspace (B3) 完整 ABAC/RBAC policy engine
變更面 加 1 張 model_shares(model_id, grantee_user_id, role, ...) workspaces / workspace_members / models.workspace_id 引入 policy 表 + evaluator
滿足 pptx 「分享給特定人」✓ 「團隊/組織共用」✓ 過度設計
工時
與現況落差 owner_user_id 保留share 疊加)

推薦:分兩步 — 先 (B1),後 (B2)(B3) 不做。

最小資料模型(對齊 database.md §2.3 + models DDLowner_user_id 保留不動):

-- 第一階段 (B1)per-model 分享
CREATE TABLE model_shares (
    model_id        UUID NOT NULL REFERENCES models(id),
    grantee_user_id UUID NOT NULL REFERENCES users(id),
    role            TEXT NOT NULL,   -- 'viewer' | 'editor'owner 仍記在 models.owner_user_id
    granted_by      UUID NOT NULL REFERENCES users(id),
    created_at      TIMESTAMPTZ NOT NULL DEFAULT now(),
    PRIMARY KEY (model_id, grantee_user_id)
);
CREATE INDEX ON model_shares (grantee_user_id);

-- 第二階段 (B2)workspace 群組共用(待第一階段穩定後)
-- workspaces(id, owner_user_id, name, ...)
-- workspace_members(workspace_id, user_id, role)
-- models 加 nullable workspace_idNULL=個人資產,非 NULL=workspace 資產)

role 語意(三種,最小集)

  • ownermodels.owner_user_id,可刪除/改權限/分享。
  • editor:可更新 model metadata、可 promote/覆蓋。
  • viewer:只能 list/get/download。

權限查驗點:決策 2 第 2 步「visionA 先驗存取權限」就是查 owner_user_id == user OR model_shares 命中 ORB2 後workspace_members 命中,通過才簽 download token。Repository.List 要從「只用 owner_user_id 過濾」改成「owner shared workspace」。

此為決策方向schema 細節由後續實作backend agent落地 migration本 ADR 只定模型形狀。


決策 4D — promote 收斂stage OAuth 401

事實 C/C'promoteconverter→FAA PUTOAuth client_credentials + scope files:upload.writeprod 已 work,只有 stage 卡 401,研判是設定問題stage OAuth client 設定 / FAA accepted scope非設計問題

方案 說明 評估
(D1) 修 stage OAuth 設定 ★推薦 對齊 stage converter 的 OAuth client_id/secret/scope 與 FAA 在 stage 接受的 scopefiles:upload.write),讓 stage 跟 prod 一致 promote 設計本身是對的prod 已證明),不該為了 stage 設定問題去改架構
(D2) 改走新存取路徑 把 promote 也改成決策 1(c) 的 token 模式 不推薦 — promote 是 server-to-serverconverter→FAA用 service JWT/OAuth 才對decision 1(c) 的短期 download token 是給 Client 下載用,兩者用途不同,不該混

推薦:(D1) 修 stage 設定,不動 promote 架構。 promote上傳/server-to-server與 downloadClient 直連)是兩條不同的 auth 路徑,正好對應 FAA 的 dual-auth 設計op=service JWT、download=delegated token不要強行統一。

promote 走 OAuth service token、download 走 visionA 簽的 delegated token — 這個分離是刻意的,符合事實 A 的 FAA dual-auth。


決策 4.5:與 ADR-016 converter 中轉的關係v1.1 新增 — 並存,非取代)

ADR-016 讓 轉檔結果 NEF downloadconverter GET /api/v1/jobs/{id}/result + visionA stream 中轉visionA 不直接打 FAA。本 ADR (a) 是 model library download 走 Client 直連 FAA。兩者是兩條不同用途的路徑、並存

路徑 用途 觸發點 來源 認證
ADR-016 converter 中轉 下載轉檔結果 NEFjob-scoped、剛轉完還沒進模型庫 GET /api/conversion/{job_id}/download converter MinIO7 天 expires_at visionA→converter API key
ADR-017 (a) 直連 FAA 下載模型庫 model(持久資產、已在模型庫) model download endpoint待建 FAA持久 MC fdt_ token

為什麼並存而非取代

  • ADR-016 的來源是 converter MinIO7 天就 GC——只適合「剛轉完、demand-trigger download」的短期場景不適合模型庫模型庫是持久資產7 天後 converter MinIO 沒了)。
  • ADR-017 (a) 的來源是 FAA持久——適合模型庫長期下載。
  • 兩條路徑的「檔案在哪」不同converter MinIO vs FAA、生命週期不同7 天 vs 持久),故並存。

但有交集需釐清(待裁決 Q8

  • 「轉檔結果」可以「加到模型庫」promote 進 FAA。一旦加到模型庫該 model 的 download 應走 (a)FAA 持久),而非 ADR-016converter MinIO 會過期)。
  • 建議分流model 在模型庫內(有 FAAObjectKey→ 走 (a)job 結果還沒進模型庫 → 走 ADR-016。兩者不互相取代依「是否已是模型庫資產」分流。
  • ADR-016 的 converter 中轉保留不動(轉檔結果 demand download 仍需要它);(a) 是新增模型庫的持久 download 路徑。

決策 5HA / 頻寬pptx 自列待解 G5

問題 短期(本架構落地時) 長期(標 Phase 後續,本 ADR 不深入)
HANAS / 公司網路掛掉全掛) 接受單點風險 + 監控告警FAA health check關鍵 model 可在 converter MinIO 保留副本(事實 C'' NEF 雙存已是雛形)作為降級來源 移轉便宜雲端空間做 FAA 後端 / 多節點 FAA
頻寬(下載走公司網路) router 限流pptx 已提visionA 端對 download token 簽發做 rate limit 移轉便宜雲端空間,下載改走雲端 egress屆時重評「省流量」前提

標記為後續 Phase,本 ADR 先讓核心存取路徑(決策 14跑通HA/頻寬不阻擋第一階段,但須在 progress 留風險登記。


5. 建議落地順序

依賴關係:決策 1(a) 的跨團隊部署是地基v1.2:跨團隊部署已 stage e2e 實測完成、不再是地基/不確定性C下載依賴 visionA 端實作B權限是下載前的查驗點D 與 B/C 無依賴可平行。

v1.2 改寫 P0v1.1 的 P0 是「MC merge+部署 + scope 註冊 + FAA stage 設定」這條跨團隊部署鏈、標為 (a) 最大不確定性。v1.2:這條鏈已用 stage 真實環境 e2e 實測打通§10、跨團隊 blocking 歸零。 P0 縮減為「visionA 端實作」+「(正式上線前)請 MC 給 visionA 專屬 file_api client」。短期 PoC 直接共用 FAA service client 4242ba63...(已實測可拿 token

階段 內容 依賴 可平行?
P0跨團隊部署 v1.2 已驗證完成 ① MC merge+部署 stage ② scope 註冊 ③ FAA stage 設定 三項已 stage e2e 實測通過§10。剩餘 P0 = 僅「正式上線前請 MC 配發 visionA 專屬 usage=file_api client」短期 PoC 共用 4242ba63...,非 blocking
P0.5visionA 端,現為起點) B2:復活 MC client打 MC /oauth/token + 打 MC Issue 簽 fdt逆轉 ADR-016 §5比原版單純——token 由 MC 簽、不自簽)+ .env 設定client 4242ba63... / MC base url / FAA base url / tenant 732270c0...)。B1model.ModelFAAObjectKey 欄位(解 object_key 斷層,見決策 2 + §10.4 P0 已完成) 與 P1a/P1b 平行
P1a D — 修 stage OAuth 設定(決策 4 D1v1.2 線索4242ba63...files:upload.write、promote 卡的 401 很可能同類問題converter 用的 client 沒 upload.write→ 可一併驗證 與 P0.5/P1b 平行
P1b B 第一階段 — model_shares 表 + 權限查驗(決策 3 B1 與 P1a 平行
P2 C — visionA 打 MC Issue + model download flow決策 1a + 決策 2FAA 用法見決策 2 v1.2 確認框);第一階段 (a) 只支援轉檔→promote 類 modelobject_key 斷層§10.4 B1 P0.5 + P1b 接 P0.5/P1b
P3 B 第二階段 — workspaces/workspace_members決策 3 B2 P1b 穩定後 獨立
P4後續 Phase HA / 頻寬(決策 5 長期方向) 不阻擋前面 獨立

fallback 觸發點v1.2跨團隊部署已通、fallback 不再需要)v1.1 設想「P0 卡住則 fallback (c)」。v1.2 P0 已 stage e2e 通、無卡點,(c) 不動用。僅保留「正式上線改用 visionA 專屬 file_api client、若 MC 端無法配發時」的理論退路。

不寫 task 級細節;各階段的實作 task 拆分由 backend agent 在進開發時做。


6. 待使用者裁決清單

# 待裁決項 推薦選項 理由
Q1v1.2:已確認走 (a) 決策 1 認證方向:(a) MC delegated token vs (c) visionA 自簽fallback 已確認走 (a) — stage e2e 實測打通§10(c) 無需動用 MC stage 已部署 + scope 機制 + FAA validate 鏈全部 e2e 實證;符合使用者 follow pptx 意圖。(c) 僅留理論退路
Q1.5v1.2:已實測 P0 可行) (a) 的 P0 跨團隊前置 checklist 能否打通 已實測通過 — MC/FAA stage 已部署、scope 用 FAA client 4242ba63... 實測可拿 token、e2e 全綠§10 原「5/9 撞 invalid_scope」風險已實證可繞過共用 FAA client。跨團隊 blocking 歸零
Q2 download token 有效期visionA Issue 時帶的 expires_in_seconds 60300 秒(傾向 120s 夠 Client 發起下載、又短到截走也很快失效
Q3 是否需支援瀏覽器直連 FAA牽涉 CORS或只 local-tool 直連 只 local-tool(無 CORS瀏覽器需求出現再加 FAA CORS 縮小第一階段範圍
Q4 B 權限模型做到哪 先 B1model_sharesB2 排後續B3 不做 最小可行先上
Q5 promote stage 401 修法 D1 修 stage 設定,不動 promote 架構 prod 已證明設計對
Q6 HA/頻寬是否阻擋第一階段 不阻擋,標後續 Phase + 風險登記 先讓核心存取路徑跑通
Q7v1.1 新增) object_key 斷層:上傳類 model不在 FAA 上)能否走 (a) 直連 FAA 第一階段 (a) 只支援「轉檔→promote 進 FAA 的 model」有 FAAObjectKey純上傳 model 的 FAA download 排後續(需先有「上傳檔也 PUT 進 FAA」的路徑 純上傳 model 目前只在 visionA 自己 storage、不在 FAA無法直連避免第一階段範圍爆炸
Q8v1.1 新增) model download 走 (a) 直連 FAA vs 沿用 ADR-016 converter 中轉的分流 已在模型庫(有 FAAObjectKey→ (a)job 結果還沒進模型庫 → ADR-016(兩者並存) ADR-016 來源 converter MinIO 7 天就 GC、不適合模型庫持久資產(a) 來源 FAA 持久
Q9v1.2:已同意復活 MC client 是否接受「復活 MC service token client」逆轉 ADR-016 §5 剛砍的 mc_token_client.go 已同意接受(a) 必要成本;邏輯比原版單純:打 MC /oauth/token 拿 service token + 打 MC Issue 簽 fdttoken 由 MC 簽、不需自簽) (a) 要 visionA 打 MC Issue必須有 MC service token使用者已同意
Q10v1.2 新增) 是否接受「短期共用 FAA service client 4242ba63... 做 PoC、正式上線前換 visionA 專屬 usage=file_api client」 接受(使用者已傾向) — 短期解 blocking、正式上線換專屬降技術債 4242ba63... 是現成 usage=file_api client實測可拿 download.delegate token共用違反 MC「secret 不共用」規範(技術債,見 R1正式上線換專屬 client。詳見 §7 R1 + §10.2

7. 後果

正面 — v1.1 改寫

  • 模型下載有可行路徑、且符合 pptx 原圖((a):不需 visionA 自建簽發 + JWKSMC/FAA source 已就緒。
  • 滿足 pptx 兩大訴求Bucket 不曝露 + 每次驗權限 + 下載不經 AWS。
  • FAA/MC 端零程式碼改動(只需部署 + 設定 + scope 註冊)——比 (c)「請 warrenchen 改寫 FAA validator」輕。
  • token 安全性最強MC 簽 opaque fdt_ token支援 boundary + revoke + 自訂 TTL比 visionA 自簽 JWT無法 revoke完整。
  • B 權限分階段,先解 owner-only 痛點,不過度設計。

負面(接受的取捨)— v1.1 改寫

  • visionA 要復活 MC service token client(逆轉 ADR-016 §5 剛砍的 mc_token_client.go——但邏輯比原版單純只需 service token cache + 打 Issue不需自簽 delegated token
  • (a) 依賴 MC 線上 validate:每次 FAA download 多一跳 FAA→MCFAA validator 有 token cache 緩解,但 MC 成驗證單點)。
  • object_key 斷層成本model.Model 要加 FAAObjectKey、釐清「哪些 model 在 FAA 上」(見決策 2
  • 跨團隊部署不可控(a) 的 P0 依賴 warrenchen 排程merge/部署/scope 註冊)。
  • HA/頻寬留後續 Phase第一階段 NAS/公司網路是單點。

風險 — v1.1 改寫

  • R1v1.2 改寫——原「scope 註冊最高風險」已 e2e 實證解除,改為共用 client 的技術債)v1.1 的 R1「scope 沒在 MC 註冊、5/9 撞 invalid_scope」已用 stage e2e 實測解除§10MC scope 機制驗證通過、FAA client 4242ba63... 實測可拿 files:download.delegate tokenv1.2 新的 R1 = 短期共用 FAA service client 4242ba63... 的技術債(i) 違反 MC source 明訂「OAuth client 禁止混用 usage、secret 不共用」;(ii) FAA 的 client secret 進 visionA .env → 擴大 secret 洩漏面(一份 secret 同時被 FAA 與 visionA 持有,任一邊洩漏波及兩個服務)。緩解:正式上線前請 MC 配發 visionA 專屬 usage=file_api client換掉 4242ba63...),把 secret 邊界收回 visionA 自己(待裁決 Q10、合規清單追蹤。實測共用實際上 e2e 可跑通是因 4242ba63... 本就是 usage=file_api client、自動帶 5 個 files scope。
  • R2v1.1 新增)object_key 三方斷層(決策 2——上傳類 model 不在 FAA 上,第一階段 (a) 只能支援轉檔→promote 的 model。若使用者期待「所有 model 都能直連 FAA」範圍大幅擴張。
  • R3v1.1 改寫)(a) 跨團隊部署任一卡住 → 整條 (a) 不通。緩解:保留 (c) 為 fallbackP0 設可接受期限,逾期 fallback。
  • R4opaque token 限制)fdt_ 是 opaque、FAA 無法本地驗,每次 download 必 call MC validate。MC validate 慢/掛 → 所有 model download 失敗。緩解FAA validator service token cache + MC validate SLA 監控。
  • R5pptx 矛盾,原 R3pptx「只傳網址讓 FAA 去抓」上傳省流量但來源converter MinIO也在公司網路HA/頻寬風險與下載同源。
  • R6原 R4NEF 在 converter MinIO7天+ FAA 雙存,長期雙存一致性與清理策略未定,須在 P2 釐清。
  • R1'(僅 fallback 回 (c) 才適用)(c) 把 token 簽發收回 visionAJWKS 私鑰外洩 = 全模型庫可被簽 token 下載,須 secret 管理 + key rotation。(a) 無此風險token 由 MC 簽、可 revoke

8. 合規性 — v1.2 改寫

  • (a) 的 P0 跨團隊部署 checklist v1.2 已 stage e2e 實測完成MC/FAA stage 已部署、scope 機制驗證、Issue→fdt→FAA validate 鏈全綠§10跨團隊 blocking 歸零
  • (正式上線前)請 MC 配發 visionA 專屬 usage=file_api client 換掉短期共用的 FAA client 4242ba63...R1 技術債、Q10
  • 與使用者確認 Q1 / Q1.5 / Q9 裁決 — 已確認走 (a) + 同意復活 MC client新增 Q10共用 client PoC使用者已傾向接受
  • 與 backend agent 確認 object_key 斷層解法(model.ModelFAAObjectKey、上傳類 model 範圍)
  • 與 security agent 評估 R4MC validate 單點 / SLA若 fallback (c) 則評估 R1'JWKS 私鑰)
  • 成本影響:第一階段無新雲端資源(沿用現有 FAA + NAS + MC主要是 visionA 開發工時 + 跨團隊部署協調

9. (a) 端到端可行性驗證v1.1 新增)

本節記錄改推 (a) 前所做的 source 驗證證據與三端缺口清單。

9.1 source 證據(已就緒的部分)

證據 結論
MC develop commit e77fdecFileAccessDownloadToken entity + migrationPOST /file-access/download-tokensIssue, [Authorize(Policy="FilesDownloadDelegate")],回 fdt_<base64url> + scope files:download.read + 自訂 expires_in_secondstoken hash 存 DBPOST /file-access/download-tokens/validateValidate, [Authorize(Policy="FilesDownloadRead")],驗 TokenHash + RevokedAt + ExpiresAt + boundary。commit 5f32452 AuthResourceRegistry 做 audience/scope mapping。master 尚未 merge。 Issue + Validate 已實作token 為 opaque非 JWT→ 必 call MC validate
FAAmasterworking tree 已驗) MemberCenterDelegatedDownloadTokenValidator.csValidateAsyncGetServiceAccessTokenAsyncFAA 自己拿 MC service tokenclient_credentials + _options.Scope)→ PostAsJsonAsync(_options.DownloadTokenValidationPath, payload) 打 MC。payload = {token, tenant_id, file_id, object_key, method}、response = {active, tenant_id, user_id, file_id, object_key, method, expires_at}Program.cs:33 註冊此 validatorProgram.cs:184 GET /files/{**objectKey} 用它驗 + boundary 檢查tenant_mismatch / object_key_mismatch / method_mismatch FAA validator 的 payload/response 形狀與 MC develop validate 契約吻合 → 配套設計FAA 端不需重寫
visionA已驗 internal/conversion/mc_token_client.go 確實已不存在ADR-016 §5 砍除生效。download 現走 converter GetResultconverter_client.go:998+ API key。model 上傳走 /api/models/init+finalize 進 visionA 自己 storagemodels.go:235 StorageKey=models/{userID}/{modelID}.nef)。 visionA 端要 (a) 須復活 MC service token client + 新增打 MC Issue 邏輯

9.2 三端缺口清單((a) 端到端「還缺什麼」)

缺口 誰做 風險
MC ① develop 尚未 merge master + 部署 stage warrenchen 排程不可控
MC ② visionA service client23605e14...)的 files:download.read/delegate scope 是否已在 MC AuthResourceRegistry 註冊未確認ADR-014 line 35 只證明「visionA 文件列了」非「MC 端註冊了」Issue 的 [Authorize(Policy="FilesDownloadDelegate")] 需 visionA token 帶對應 claim warrenchen 最高5/9 撞過 invalid_scope
FAA ③ FAA stage 是否部署 + MemberCenterOptionsBaseUrl/ClientId/ClientSecret/Scope/DownloadTokenValidationPath是否設好未確認FAA 自己也要有 MC service tokenscope 待確認) warrenchen
visionA ④ 復活 MC service token client逆轉 ADR-016 §5+ 打 MC Issue jimchen / backend agent 低(自己可控)
visionA ⑤ object_key 斷層:model.ModelFAAObjectKey、釐清上傳類 model 不在 FAA 上的範圍(見決策 2 jimchen / backend agent 中(範圍影響)
三方 ⑥ boundary 對齊visionA Issue 帶的 object_key = FAA URL key = MC validate object_key見決策 2 斷層分析) 三方協調

9.3 驗證結論

  • source level(a) 通MC develop Issue+Validate 已寫、FAA validator 配套、契約吻合)。v1.0 判 fictional 是因只 grep master。
  • 部署 level(a) 未通,缺 §9.2 六項,其中 ② scope 註冊是最高風險warrenchen 端、5/9 同類事故)。
  • visionA 端額外成本:復活 MC service token client逆轉 ADR-016+ 解 object_key 斷層(加 FAAObjectKey
  • (c) 在新事實下反而更重FAA 現成 validator 正是 (a) 要的;(c) 要請 warrenchen 改寫成本地驗 JWKS是「改既有正確配套」。
  • 最終建議:推 (a),但 P0 step ②③ 須先跟 warrenchen 確認可行Q1.5),逾期 fallback (c)。

⚠️ v1.2 後記§9 是 v1.1 時的「source level 驗證 + 待跨團隊驗證」紀錄。§9.2 六項缺口中的 ①②③MC merge/部署、scope 註冊、FAA stage 設定)已於 v1.2 用 stage 真實環境 e2e 實測全部通過(見 §10。§9.3「部署 level 未通」結論已被 §10 推翻。④⑤⑥visionA 端實作 + object_key 斷層 + boundary 對齊)轉為 §10.4 的 visionA 端 checklist。


10. (a) 端到端 stage 實測證據v1.2 新增 — 本 ADR 最有價值的部分)

本節記錄 v1.1 → v1.2 之間,用 stage 真實環境 + 真 secret + 真 OIDC user_id 把整條 (a) 鏈路打通的實證。這把 (a) 從「待驗證的可行性」升級為「已實證可行」。

10.0 環境與憑證(使用者由 MC team 取得)

項目
MC stage Web https://stage-9527.innovedus.com:7880/
MC stage API https://stage-9527.innovedus.com:7850/
FAA stage https://stage-9527.innovedus.com:5081/
visionA Login clientweb_login b8093fea1a504a5d8f0e04bee9f78f2ecallback https://stage-9527.innovedus.com:9527/api/auth/callback注意MC team 訊息少寫 /api,以 visionA 現用含 /api 的為準、勿改
visionA「Login 以外 API call」共用 client 23605e14a2c64660abd97e29963d8d58client_credentials
FAA service clientPoC 共用) 4242ba63099d4f318dd3f143d27ef4c5tenant 732270c0-449c-489c-bfad-321e9bf89b3dscopes files:upload.write files:metadata.read files:delete files:download.delegate
真實 OIDC user_id/api/auth/me b5332e51-c394-45c7-a28a-83e6da127ba9

10.1 MC 部署確認(推翻「只看 git master 判 fictional」

  • POST /file-access/download-tokensIssue/validate 在 stage 回 401非 404→ endpoint 已部署。對照亂打路徑回 404。
  • 推翻 v1.0/v1.1「只看 git master 判 fictional」stage 實際版本已含 develop 的 download token 功能(即 §9.2 缺口 ①「develop 尚未 merge master」對 stage 不成立——stage 跑的版本已有此功能)。

10.2 scope 模型診斷MC usage→scope 模型)

  • MC discovery scopes_supportedfiles:download.read/delegate/upload.write/metadata.read/delete
  • MC 用 usage→scope 模型AuthResourceRegistryServicefile_api 這個 usage 類型(不是現成 client自動帶 5 個 files scope。file_api 強制 confidential + client_credentials + 綁 tenant_id。source 明訂「OAuth client 禁止混用 usage、secret 不共用」(→ R1 技術債的根據)。
  • visionA 共用 client 23605e14... 實測:所有 files scope 全部 not allowedconverter:job.* 是 invalidMC 根本沒這 scope,呼應 converter 已改 API key。→ 共用 client 對 file_api 行不通(規範禁止混用 usage
  • FAA client 4242ba63... 實測:能拿 files:download.delegate tokenaud=file_access_api → 它就是個 usage=file_api 的 client(所以 PoC 直接共用它可行)。

10.3 🏆 (a) 端到端 e2e 全綠stage 真實環境、真 secret、真 user_id

Step 動作 結果
Step 1 4242ba63... 打 MC /oauth/tokenclient_credentialsfiles:download.delegate token 拿到
Step 2 帶該 token + 真 user_id 打 MC POST /file-access/download-tokensIssue HTTP 200簽出 fdt_... tokentoken_type=file_download、120s、scope=files:download.read、tenant/user/object_key 正確回填)
Step 3 fdt_ token 以 Authorization: Bearer 打 FAA GET /files/{objectKey} HTTP 404 file_not_found= 認證 + MC validate 全過、FAA 去 bucket 找檔、只因 test/probe.nef 是假 key 不存在;換真實檔就回檔案內容)

B3tenant 一致、B4user 是 MC 真實 user、FAA validate 鏈,全部實證通過。 404 是「檔案不存在」而非「認證失敗」——這正是認證鏈全綠的證明。

10.4 剩餘 visionA 端 blockingbackend agent 接下來的工作)

# 項目 說明
B1最關鍵 object_key 斷層 visionA model StorageKey=models/{userID}/{modelID}.nefvisionA 自己 storage≠ FAA object_key。上傳類 modelSourceUploaded根本不在 FAA → 第一階段 (a) 只支援轉檔→promote 類 modelmodel.Model 要加 FAAObjectKey 欄位 + promote 時寫入。download flow 用 FAAObjectKey(非 StorageKey)組 FAA URL + Issue token
B2 visionA 加 MC client code 加「打 MC /oauth/token + 打 MC Issue 簽 fdt token」的 client code逆轉 ADR-016 砍除的 mc_token_client但更單純不自簽、token 由 MC 簽+ .env 設定client 4242ba63... / MC base url / FAA base url / tenant 732270c0...
D連帶 promote 401 可一併驗證 4242ba63...files:upload.write → promote 卡的 401 很可能同類問題converter 用的 client 沒 upload.write)。標為「可一併驗證」

FAA download endpoint 確切用法(給 backend 實作)

  • GET {FAA}/files/{objectKey}token 放 Authorization: Bearer {fdt_token}FAA TryReadAccessToken 只認 Authorization Bearer,不認 query / 自訂 header
  • FAA 自己用 IDelegatedDownloadTokenValidator=MemberCenterDelegatedDownloadTokenValidator)拿 token 打 MC validateinstanceOptions.TenantId + ObjectKey(從 URL path 取)+ Method=GET
  • objectKey 必須與簽 token 時的 object_key 完全一致FAA validate boundary 檢查、不一致回 object_key_mismatch)。

⚠️ v1.3 修正2026-06-27上面這行「FAA TryReadAccessToken 只認 Authorization Bearer,不認 query」已過時、被 §11 推翻。重讀 FAA repo 最新 master~/file_access_agent)發現 TryReadAccessTokensrc/FileAccessAgent.Api/Program.cs:324-341先讀 Authorization: Bearer、沒有才讀 ?access_token= query string——即 FAA 原生支援 query-string token。v1.2 的「只認 Bearer」結論是當時測 stage 沒測 query path 造成的誤判。下載對接因此改為 query-string token + 瀏覽器 redirect 導航(避開 CORS preflight決策與落地規格見 §11


11. v1.3:瀏覽器下載改 query-string token + redirect推翻決策 2 / §10.4 的「fetch + Bearer」

狀態Accepted2026-06-27。本節 supersede 決策 2 第 5 步、§10.4 的「FAA 只認 Authorization Bearer」結論以及前端 model-download.ts 現行的 fetch + Bearer + blob 實作。只改「Client→FAA 那一跳怎麼帶 token」不改認證鏈本體MC 簽 fdt / FAA 打 MC validate / boundary 檢查全部不變)。

11.1 背景(為什麼要改)

v1.2 決策 2 / §10.4 要求前端用 fetch(faaUrl, { headers: { Authorization: 'Bearer fdt_...' } }) + response.blob() + 動態 anchor 觸發下載。stage 實測(截圖實錘)顯示:

  • 前端(9527fetch 跨 origin 直連 FAA5081+ 帶自訂 Authorization header → 瀏覽器判定為 non-simple request、先送 CORS preflight OPTIONS
  • FAA 沒設 CORSAddCors / UseCors)、OPTIONS /files/...405 → 瀏覽器報 CORS error → 下載死。
  • 根因定位見 .autoflow/04-architecture/download-cors-405-diagnosis.md

FAA team 立場:不設 CORS。因為 FAA 設計就是「token 放 query string + redirect 導航下載」——這條路徑天生不觸發 CORS preflight瀏覽器導航不是 fetch、無自訂 header

11.2 FAA repo 實證(修正 v1.2 的誤判)

事實 證據(~/file_access_agent 最新 master
FAA download token 支援放 query string,參數名 access_token src/FileAccessAgent.Api/Program.cs:324-341 TryReadAccessToken:先讀 Authorization: BearerelseRequest.Query["access_token"]
FAA 官方 test page 的正解就是 query + redirect src/FileAccessAgent.TestSite/Controllers/HomeController.cs:255-282 DownloadFileDirect:組 /files/{encodedObjectKey}?access_token={Uri.EscapeDataString(token)}Redirect() 導航
FAA 確實沒設 CORS 全 repo 無 AddCors / UseCors
objectKey encode 規則 segment-wise URL encode每段各自 escape、保留 /token 用 Uri.EscapeDataString

query-string token 是 FAA 原生支援、且是官方建議用法。改用它 = 對齊 FAA 設計,不是 workaround。

11.3 決策 Atoken 放 URL 的責任切分 — 推薦「乙:後端組好完整 URL」

維度 甲:後端回 {download_url, token, expires_at}前端?access_token= 乙:後端直接回已含 ?access_token= 的完整 URL ★推薦
token escape 責任 前端做 encodeURIComponent(token)(多一處易錯點) 後端 url.QueryEscape(token)(與 objectKey escape 同一處、一致)
objectKey 知識 前端不需碰download_url 已含 objectKey但要懂「在尾巴 append query」 前端完全不碰 URL 組裝、拿到直接導航
職責清晰度 URL 由前後端各組一半(後端組 path、前端組 query→ 切割點尷尬 URL 完全由後端產生 = 單一真實來源、token 不外露給前端邏輯層處理
API 契約衝擊 不變(仍回 3 欄)→ 後端 test 不用大改 download_url 內含 tokentoken / expires_at 欄位處置見下
安全token 出現處) token 在前端 JS 變數 + URL前端任何 log 都可能印到 token 由後端直接埋進 URL前端只當「不透明連結」傳給瀏覽器不進前端邏輯層處理
既有 backend test 幾乎不動(仍驗 3 欄) 需改 models_download_test.go:驗 download_url?access_token=<escaped token>

推薦乙、理由

  1. URL 是單一真實來源FAA 下載 URL 的「pathobjectKey segment-escape+ querytoken escape」是一套 FAA encode 規則,由**同一端(後端)**用同一套邏輯組完最不易出錯。甲案把這套規則切兩半(後端組 path、前端 append query切割點本身就是 bug 溫床。
  2. token 不下放到前端邏輯層:乙案前端把 download_url 當「不透明可導航連結」token 不再是前端要 encodeURIComponent / 存變數 / 可能誤印 log 的東西。降低 token 在前端誤洩風險。
  3. 對齊 FAA 官方用法FAA test page§11.2)就是 server 端組好完整 URL 再 redirect。乙案 = 同模式。
  4. 甲案唯一好處API 契約不變)價值低契約本來就要改fetch→redirect 是行為大改、前端 test 一定要重寫),保留 3 欄結構省不了多少。

乙案的 API 契約處置(給 backend重要

  • download_url:改為 {FAABaseURL}/files/{segmentEscapedObjectKey}?access_token={queryEscapedToken}(在現有 §10.4 / models.go:550 組好的 URL 尾巴 append ?access_token=)。
  • token 欄位:保留但標記 deprecated(向下相容——舊版 local-tool / 既有呼叫方可能還讀它)。值仍回原始 fdt_ token。前端改為不使用它(不再需要拼 header / 拼 URL不要直接移除欄位,避免破壞既有 consumer。
  • expires_at保留(前端 / UI 仍可用來顯示「連結有效期」提示,雖然第一階段未用)。
  • response 結構只是 download_url 內容變化 + token 轉 deprecated,欄位不刪 → 對既有 consumer 向下相容。

11.4 決策 B前端導航方式 — 推薦「動態 <a download href> click」

方式 評估
動態 <a href download> + click ★推薦 明確語意「下載」;download 屬性提示瀏覽器存檔而非導頁;同分頁不會把目前 SPA 畫面導走
window.location.href = url 會嘗試把目前分頁導去該 URL。若 FAA 回 Content-Disposition: attachment,多數瀏覽器會轉成下載、不離開頁面;但若 header 缺失或瀏覽器行為差異,可能變成「整頁導去 FAA」破壞 SPA。風險較高

推薦動態 <a download href>、理由:跨 origin 時 <a download>download 屬性檔名提示會被瀏覽器忽略(安全限制),但**「觸發下載而非導頁」的行為仍比 window.location 穩**——尤其搭配 FAA 的 Content-Disposition: attachment,瀏覽器會以下載處理且不離開當前頁。可重用既有 triggerBlobDownload 的 anchor 建立/click/remove 樣式(但 href 改成 FAA URL、不再是 blob object URL不需要 URL.revokeObjectURL)。

檔名處理(重要簡化)

  • FAA 下載回 Content-Disposition: attachment; filename=...瀏覽器會用 FAA 給的檔名
  • 跨 origin 導航時 <a download="xxx">xxx 會被瀏覽器忽略(跨 origin 安全限制)→ 前端設不設 download 屬性的檔名都不影響結果。
  • deriveDownloadFilename 在 redirect 方案下失去作用(它原本是給 blob 方案命名用的blob 是 same-origin object URL、download 屬性有效。redirect 方案下檔名完全由 FAA 的 Content-Disposition 決定。
  • 建議:移除 deriveDownloadFilename 的呼叫store 不再需要),函式本身可留著標 deprecated 或一併刪(由 frontend agent 判斷,傾向刪以免死碼)。<a> 仍可設 download=""(空值)純粹表達「這是下載意圖」,但不依賴它命名。

11.5 決策 CobjectKey / URL encode 釐清

  • 後端現況models.go:550 downloadURL = {FAABaseURL}/files/{escapeFAAObjectKey(objectKey)}escapeFAAObjectKey 已做 segment-wise url.PathEscape、保留 /models.go:570-576)——與 FAA encode 規則對齊、已組好含 objectKey 的完整 path、不含 query
  • 結論:採乙案後,前端完全不碰 objectKey encode。後端在現有 URL 尾巴 append ?access_token={url.QueryEscape(token)} 即可(用 url.QueryEscape 對齊 FAA test page 的 Uri.EscapeDataString)。前端只是把這個完整 URL 丟給 <a href>

11.6 決策 D安全考量token 進 URL結論:可接受 + 輕量緩解

token 放 query string 會落在瀏覽器歷史、FAA server access log可能Referer(導航到 FAA但 FAA 是 token 的目的地、非第三方)。

風險評估(結論:可接受)

因素 評估
token TTL 120s 短效§10.3 實測 expires_in_seconds)→ 落歷史 / log 後很快失效
token 範圍 單檔 + 單 methodGETboundaryMC validate 綁 object_key + method→ 截走只能下載那一個檔
token 形態 opaque fdt_(非 JWT、不含可解析的 user/權限資訊)+ MC 可 RevokedAt 即時撤銷
使用次數 一次性下載場景(非長期憑證)

這正是 FAA 設計 query-string token 時就接受的取捨FAA 官方 test page 即如此用)。短 TTL + 單檔 boundary + opaque + 可 revoke四重特性使「token 落 URL」風險可接受。

輕量緩解(不過度工程)

  1. 不要在前端 console / log / 錯誤訊息印出含 token 的完整 URL(前端錯誤回報只記 modelId / status code不記 download_url
  2. 沿用短 TTL(維持 §10.3 的 120sQ2 已定 60300s
  3. 已天然滿足opaque token + MC boundary + revoke 能力,無需額外開發。
  4. 不需要做更重的緩解(如 one-time token / IP 綁定)——與 FAA 設計範圍不符、第一階段過度工程。

11.7 決策 ECORS 殘留確認 — 整條鏈乾淨

origin 關係 CORS 風險
前端 → visionA backend GET /api/models/:id/download 同 origin9527→9527、走 api.get、帶 cookie
前端 → FAA GET /files/...?access_token=(瀏覽器導航) 跨 origin9527→5081是導航、非 fetch、無自訂 header 無 preflight → 無 CORS

→ 改 redirect 後,整條下載鏈不再有任何 CORS preflight。FAA 不需設 CORS對齊 FAA team 立場)。徹底解決 405。

11.8 落地 checklist工程師照表施工

BackendvisionA-backend)— 採乙案:

  • internal/api/models.go modelsDownloadHandler~:550downloadURL 尾巴 append ?access_token= + url.QueryEscape(issued.Token)。即 downloadURL = {FAABaseURL}/files/{escapeFAAObjectKey(key)}?access_token={url.QueryEscape(token)}
  • ModelDownloadResponse:457Token 欄位註解標 deprecated(仍回原始值、向下相容、前端不再使用);DownloadURL 註解更新為「已含 ?access_token=、可直接瀏覽器導航下載」;ExpiresAt 保留。
  • 不刪欄位(向下相容)。

FrontendvisionA-frontend

  • src/lib/api/model-download.ts
    • 移除 downloadModelFilefetch + Bearer + blob 那套)。
    • 新增(或改造 triggerBlobDownload)成 triggerNavDownload(downloadUrl: string):動態建 <a href={downloadUrl} download rel="noopener"> + append + click + remove不需 createObjectURL / revokeObjectURL)。
    • getModelDownload:保留(仍打同 origin backend。回傳型別 ModelDownloadGranttoken 改標 deprecated / optional前端不再用
    • 移除 deriveDownloadFilenameredirect 跨 origin 下檔名由 FAA Content-Disposition 決定、前端命名無效;見 §11.4)。
    • 錯誤分層:導航式下載無法攔截 FAA 端 4xx/5xx(瀏覽器導航不回 Response 給 JS。故 network_error / download_failed 這類「FAA 回應錯誤」前端偵測不到——錯誤處理範圍縮小為「getModelDownload 階段的錯誤」backend 4xx/5xx。更新註解說明此限制。
  • src/stores/model-store.ts downloadModel action~:278
    • 改為 getModelDownload(model.id)triggerNavDownload(grant.downloadUrl)(拿掉 deriveDownloadFilename + downloadModelFile)。
    • downloadingId loading 狀態:導航觸發是同步瞬間完成(無 await blobloading 幾乎不可見——可保留作防重複點擊、但要意識到「下載進度」前端無法追蹤(瀏覽器接手)。
  • src/components/models/model-card.tsx:下載按鈕邏輯大致不變(仍呼叫 downloadModel);但 toast「下載開始」語意仍成立導航觸發即視為開始

Tests

  • visionA-frontend/src/lib/api/model-download.test.ts
    • 刪除 downloadModelFile 整個 describefetch happy / 非2xx / CORS network_error / abort / 空token——該函式移除。
    • 刪除 deriveDownloadFilename 整個 describe——函式移除。
    • triggerBlobDownload describe → 改為 triggerNavDownload:驗「建立 anchor、href = 傳入的 downloadUrl、有 download 屬性、click 後從 DOM 移除」(不再驗 createObjectURL / revokeObjectURL)。
    • getModelDownload describe保留仍驗 200 正規化 / 4xx / 5xx / parse
  • visionA-frontend/src/stores/model-store.test.tsdownloadModel 相關 case 改為驗「呼叫 getModelDownload 後觸發導航spy triggerNavDownload / spy anchor click而非「呼叫 downloadModelFile」。
  • visionA-frontend/src/components/models/model-card.test.tsx:互動 test 大致不變mock downloadModel 回傳),確認按鈕點擊 → toast 行為。
  • visionA-backend/internal/api/models_download_test.go TestModelsDownload_OKdownload_url 斷言改為?access_token=<url.QueryEscape(fdt token)>(如 .../files/models/demo-user/job-1.nef?access_token=fdt_abc123)。其餘 case501/404/403/502不受影響。

驗收(給 testing

  • 後端:download_url 結尾含 ?access_token= + token 正確 escape。
  • 前端:點下載 → 建立帶 FAA URL 的 <a> 並 click無 fetch 呼叫、無 CORS preflight
  • e2estage實際點下載按鈕、瀏覽器發起對 FAA 的導航 GET、檔案落地檔名來自 FAA Content-Disposition)。

11.9 後果

正面:徹底解決 405 / CORSFAA 不需設 CORS、對齊 FAA team 立場 + FAA 官方用法);前端大幅簡化(拿掉 fetch+blob+createObjectURL+檔名推導token escape / URL 組裝收口到後端單一真實來源。

負面(接受的取捨)(1) token 落瀏覽器歷史 / FAA log§11.6 評估可接受);(2) 前端無法攔截 FAA 端下載錯誤(導航式下載不回 Response 給 JS——FAA 4xx/5xx 時使用者看到的是瀏覽器原生錯誤頁 / 空白,而非 visionA 的 toast。第一階段接受短 TTL + owner-only 已驗權限FAA 端錯誤機率低若未來需要前端可控的錯誤體驗再評估「backend proxy 下載」(決策 2 §4 替代方案 A。(3) token / deriveDownloadFilename 留為 deprecated 死碼(建議下次清理)。