visionA/docs/autoflow/04-architecture/adr/adr-016-download-via-converter.md
jim800121chen dab13ed984 docs(autoflow): ADR-016 — visionA download 改走 converter GetResult,撤回 FAA delegated token 鏈
致命發現(grep MC + FAA source 確認):
- MC source 沒有 issue delegated download token endpoint
- MC source 沒有 validate delegated download token endpoint
- FAA MemberCenterDelegatedDownloadTokenValidator.cs 假設的 MC introspection endpoint 不存在
- ADR-014 §2 從 5/2 寫完到現在這條鏈一直是斷的、只是因為從未實際 e2e 跑通過所以沒被發現

使用者拍板硬約束:不動 MC + 不動 FAA

新增 ADR-016:
- visionA download 改用 converter GET /api/v1/jobs/{id}/result(新 endpoint)
- visionA backend 用既有 ConverterAPIKey 認證(不需新增 secret)
- 維持 T4 已實作的 stream proxy 結構(io.CopyN + Content-Disposition + size cap)
- promote 仍 PUT FAA(converter 內部用自己的 OAuth、與 visionA 無關)
- 不需動 MC + FAA + warrenchen
- 6 個替代方案逐一說明排除理由

修訂既有文件:
- ADR-014 v1.1 → v1.2:§2 download flow 標註被 ADR-016 部分 supersede
- ADR-015 v2.0 → v2.1:§2 visionA → FAA delegated token 設計(v2.0 從 v1.x 撤回的設計)再次撤回;§9 env 表撤回 v2.0 加回的 OIDC ServiceClient* / TenantID / FAABaseURL;visionA 端 server-to-server 只剩 ConverterAPIKey 一把
- conversion.md v0.5 → v0.6:§1 sequence diagram 重畫(移除 MC node)、§2 模組設計(mc_token_client.go 整檔刪除確認、faa_client.go 改名 converter_result_client.go)、§3.2 visionA → FAA 整段標撤回、§4.1 download handler 改 converter.GetResult、§6 錯誤碼撤回 mc/faa 三個 code 加 result_not_found / result_expired
- api-conversion.md v0.5 → v0.6:檔頭 Auth 段落改寫、§4 download endpoint 改述、error code 表撤回 mc_token_unavailable / download_token_failed
- oidc-tdd.md v0.3 → v0.4:§13.1 環境變數表 OIDC ServiceClient* / TenantID / FAABaseURL 從「重新啟用」改回「再次廢棄」、§13.1.1 stage env 範例移除 service client / tenant_id / FAA URL、§13.1.3 改寫為「v0.4 單線設計」說明

整體影響:
- 不需復活 mc_token_client.go(commit 86b7175 砍除狀態維持)
- 不需復活 OIDCConfig.ServiceClientID/Secret/TenantID(commit 86b7175 移除狀態維持)
- visionA backend faa_client.go 要改名為 converter_result_client.go、改呼叫 converter.GetResult
- visionA backend flow.go DownloadStream / PromoteToModels 改用 converter.GetResult
- jimchen 跨 repo 任務:converter scheduler 加 GET /api/v1/jobs/{id}/result endpoint

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 12:30:46 +08:00

37 KiB
Raw Blame History

ADR-016visionA download NEF 改走 converter GET /api/v1/jobs/{id}/result 中轉(撤回 FAA 鏈)

狀態

Accepted — 2026-05-16

上位 / 同層 ADR

  • 部分 supersedes

    • ADR-014 §2「Download — FAA delegated token」整段visionA → FAA delegated token 鏈撤回visionA download 改走 converter GET /api/v1/jobs/{id}/result。ADR-014 §2 中關於「server-side proxy / 不退回 302 / <a href download> frontend pattern」的決策沿用、由本 ADR 接手
    • ADR-014 §3「半自動 — 加到模型庫」中「visionA backend 跟 FAA pull NEFscope files:download.read)」的部分:本 Phase 0.8 範圍內 visionA 不主動 pull NEF如 Phase 1+ 需要 server-side pull改走 converter GET /api/v1/jobs/{id}/result 同一條路徑。
    • ADR-014 §5「Service token cache — 仿 converter scheduler 模式」中 FAA 部分scope files:download.read / delegate 不再由 visionA 取;對應 service client / tenant_id env 撤回)。
    • ADR-014 §7「失敗模式 retry 矩陣」中「FAA pull加到模型庫」、「MC token endpoint」、「MC delegated token」三 rowvisionA 端不再觸發converter 端有自己的 retry 矩陣,不在本 ADR 範圍)。
    • ADR-015 v2.0 §2「visionA → FAAMC service token + delegated download token」整段v2.0 於 §2 維持 ADR-014 §2 設計,本 ADR-016 於 §2 撤回visionA 端 mc_token_client 整檔再次砍除OIDC ServiceClient* / TenantID env 廢棄)。
  • 沿用(不受本 ADR 影響):

    • ADR-013user login 的 OIDC public PKCE client與 server-to-server 完全解耦)
    • ADR-014 §1upload streaming proxy、§3「加到模型庫」走 visionA /api/models/init+finalize、§4 模組劃分(不擴 model schema、internal/conversion/ 包裝、§6 user_id trust boundary
    • ADR-015 v2.0 §1「visionA → converter 採 pre-shared API key」本 ADR 在 download 路徑沿用同一條認證機制)
  • 同層:本 ADR 與 ADR-014 / ADR-015 v2.0 三者並存後的最終態為:

    • visionA → converterinit / poll / promote / result downloadAPI keyADR-015 §1
    • converter → FAApromote 內部 PUT NEFconverter 自己用 OAuth client_credentials + scope files:upload.writeconverter 既有實作,與 visionA 無關)
    • visionA ↔ FAAPhase 0.8b 範圍內不存在這條鏈(本 ADR 撤回)
    • visionA ↔ MCserver-to-serverPhase 0.8b 範圍內不存在這條鏈(本 ADR 撤回;只有 user login 的 OIDC 公開 client 仍接 MC

背景 (Context)

起因致命發現2026-05-16

2026-05-16 在 backend agent 準備依 ADR-015 v2.0 §6「mc_token_client 部分復活」實作之前,對 Innovedus Member CenterMC與 File Access AgentFAA的 source code 做全面驗證,發現 ADR-014 §2 設計的 delegated download token 鏈路從 2026-05-02 寫文件至今一直是斷的、只是因為 visionA 從未實際 e2e 跑通 download 而沒人發現

致命發現 1MC 沒有「issue delegated download token」endpoint

/Users/jimchen/member_center/src/MemberCenter.Api/Controllers/ 完整 grep8 個 controller

AdminNewsletterListsController.cs   AdminOAuthClientsController.cs   AdminTenantsController.cs
AuthController.cs                   NewsletterController.cs           OAuthController.cs
SendEngineIntegrationController.cs  SubscriptionsController.cs        TokenController.cs
UserController.cs

全部 endpoint 清單:

  • POST /oauth/token, POST /oauth/authorizeTokenController.cs / OAuthController.csOpenIddict OAuth/OIDC 標準)
  • POST /auth/login, /auth/refresh, /auth/logout, /auth/register, /auth/password/{forgot,reset}, /auth/email/verifyAuthController.cs
  • GET/PUT /user/profileUserController.cs
  • Admin endpointstenants / oauth-clients / newsletter-lists
  • POST /integrations/send-engine/webhook-clients/upsertSendEngineIntegrationController.cs
  • POST/GET /subscriptionsSubscriptionsController.cs
  • POST/GET /newsletterNewsletterController.cs

MC source 完全沒有 /file-access/download-tokens endpoint也沒有任何 file / download / token issuance 相關的 controller。ADR-014 §2 line 60-68 描述的「MC POST /file-access/download-tokens」流程從未存在。

致命發現 2MC 沒有「validate delegated download token」endpoint

FAA 端 /Users/jimchen/file_access_agent/src/FileAccessAgent.Infrastructure/Services/MemberCenterDelegatedDownloadTokenValidator.cs 線 27-72

public async Task<DelegatedDownloadTokenValidationResult> ValidateAsync(
    DelegatedDownloadTokenValidationRequest request,
    CancellationToken cancellationToken)
{
    var accessToken = await GetServiceAccessTokenAsync(cancellationToken);
    // ...
    using var response = await http.PostAsJsonAsync(
        _options.DownloadTokenValidationPath,   // ← 對 MC 打的 validation endpoint
        payload,
        cancellationToken);
    // ...
}

FAA assume MC 有 _options.DownloadTokenValidationPath(一個 token introspection endpoint例如 POST /file-access/validate-file-download-token),但對 MC source 全 grep download-token / file-access / validate 都找不到對應 controller。

→ 即使 visionA 哪天真的拿到一個 delegated download token、帶去打 FAA GET /files/{key}FAA 也會在 IDelegatedDownloadTokenValidator.ValidateAsync() 階段因為 MC 不回 200 而把 token 視為 invalid回 403。

致命發現 3ADR-014 從一開始就 assume 而沒驗 source

對 ADR-014 line 17-18 寫的:

Innovedus Member Center (MC) 是 OAuth/OIDC IdP同時負責簽 service-to-service token 與 delegated download token

line 75

server-to-server 跟 MC 換 delegated tokenscope files:download.delegate

→ 2026-05-02 寫 ADR-014 時沒實際讀 MC source、直接 assume「MC 有 delegated token endpoint」。整條 delegated token 鏈路是 fictional。

ADR-015 v2.0 §2 line 109-1532026-05-16 上午寫)也沿用這個 assumption雖然 v2.0 描述的設計「正確的」是 ADR-014 §2 原意圖,但 ADR-014 §2 原意圖本身就建立在錯誤的 MC 能力假設上)。

致命發現 4FAA download endpoint 對 service token 也不接受

/Users/jimchen/file_access_agent/src/FileAccessAgent.Api/Program.cs line 184-254GET /files/{**objectKey} 沒掛 .RequireAuthorization(),改用 IDelegatedDownloadTokenValidator.ValidateAsync(...) 驗 delegated download token。

→ FAA download endpoint 強制只接 delegated token、不接 service token。 → 即使 visionA 從 MC 拿到 valid service token這條鏈是真的 work 的已有實證——converter promote 走 client_credentials + files:upload.write scope 正常 work帶去打 FAA GET /files/{key} 也會 401 / 403。 → visionA 端唯一能 download FAA 上的 NEF 的方法是先取得 delegated token但 MC 沒有 issue delegated token 的 endpoint致命發現 1

為什麼這個缺口從 2026-05-02 一直沒被發現

時間軸:

日期 事件 影響
2026-05-02 ADR-014 v1.0 寫定 assume MC 有 delegated token endpoint
2026-05-09 ~ 11 Phase 0.8 stage 部署遇 4 個 OAuth client_credentials blockerconverter :read/write scope 沒 onboard / converter image 舊版 / converter MC env / FAA OAuth 整合不明) 發現的是 converter 線、不是 FAA delegated token 線
2026-05-11 ADR-015 v1.0:兩條都改 API key FAA 線改 API key、跳過了 delegated token 鏈、致命缺口被掩蓋
2026-05-15 ADR-015 v1.1:補 reference middleware 仍是 API key 路徑
2026-05-16上午 ADR-015 v2.0:撤回 FAA API key、回到 ADR-014 §2 delegated token 設計 重新踩到 2026-05-02 的缺口(但寫 v2.0 時還沒驗 MC source、再次 assume
2026-05-16下午 / 本 ADR 對 MC source 全 grep 確認 → 缺口從未修復 撤回 v2.0 §2、改走 converter 中轉

Phase 0.8 從來沒有真的把 download 跑到 e2eT4 frontend <a href> 觸發 download 的測試都是對 stub backend從未真的觸發 visionA → MC issue delegated token → FAA validate 整條鏈,所以 4 處致命缺口疊起來成為一個從未被執行的 dead path。

使用者拍板的硬約束

2026-05-16 在發現上述缺口後,使用者明確拍板:

約束 理由
不動 MC MC 是公司共用 IdP新加 endpoint 需 MC team 設計 + onboard scope + redeploy + 配 tenant跨人協調週期長過去 5/9 撞 scope 沒註冊已驗證此風險
不動 FAA FAA 是 warrenchen 維護的公司共用 repo既有 dual-auth 設計JWT for write / delegated for download改一行都要走跨人協調
但要解決 download PRD Phase 0.8 範圍內 user 要能下載 NEF半自動「下載」按鈕 + Wireframe success card 都有)

→ 解決方案必須在 jimchen 自己維護的 repovisionA + converter內完成、不觸碰 MC / FAA。

既有可用基礎建設

元件 狀態 可用性
converter promote → FAA PUT NEFOAuth + files:upload.write Phase 1 已上線 work已在 prod 跑 / apps/task-scheduler/src/fileAccessAgent/client.js
converter MinIO 存 NEF converter scheduler 既有 promote 後 NEF 同時保留在 converter MinIO + FAAconverter expires_at = 7 天)
visionA → converter API keyADR-015 §1 commit 86b7175 已實作 workinit / poll / promote 都已驗證)
converter scheduler 是 jimchen 自己 repo 加新 endpoint 對 coordination cost 低
visionA backend stream proxy 結構T4 已實作 internal/conversion/faa_client.go DownloadStream + io.CopyN size cap + Content-Disposition + context cancellation commit e02059e 已實作 結構保留、只是 stream 來源從 FAA 改 converter

決策 (Decision)

converter 加 GET /api/v1/jobs/{id}/result endpoint + visionA stream 中轉 解決 downloadpromote 路徑不變。

1. converter 新增 endpointGET /api/v1/jobs/{id}/result

1.1 完整 API spec

GET /api/v1/jobs/{id}/result HTTP/1.1
Host: 192.168.0.130:9501
Authorization: Bearer <CONVERTER_API_KEY>
Accept: application/octet-stream
元素 規格
Method GET
Path /api/v1/jobs/:id/result
Path param id converter job idUUID v4
Auth Authorization: Bearer <CONVERTER_API_KEY>(同 ADR-015 §1 既有 middlewareconverter middleware subtle.ConstantTimeCompare 比對 env
Query params
Request body

1.2 Response — 成功200

HTTP/1.1 200 OK
Content-Type: application/octet-stream
Content-Length: <NEF binary 真實大小>
Content-Disposition: attachment; filename="<source_filename_stem>_<target_chip_lower>.nef"
Cache-Control: no-store, no-cache, must-revalidate, max-age=0

<NEF binary stream>
Header 來源 / 規格
Content-Type 固定 application/octet-stream
Content-Length converter MinIO 物件實際大小(必填,給 visionA backend 設 Content-Length 透傳到 browser
Content-Disposition converter 自行構造 filename<source_filename_stem>_<target_chip_lower>.nef(例 yolov5s_kl720.nef,對齊 wireframe success card 顯示範例)。visionA backend 收到後可選擇透傳或覆寫visionA 端有自己的 defaultDownloadFilename(cj) helper
Cache-Control 固定 no-store, no-cache, must-revalidate, max-age=0
Body NEF binary streamconverter 從 MinIO get object 後直接 stream 出來)

1.3 Response — 錯誤

HTTP code 條件 converter 回 bodyJSON
401 unauthorized API key 不對 / 缺 / 格式錯 {"error":"unauthorized"}(同 ADR-015 §3.5.1 既有 middleware
404 job_not_found job_id 不存在 / 已被 GCconverter 7 天 expires_at {"error":"job_not_found"}
409 job_not_completed job 尚未 completedstatus ≠ completed {"error":"job_not_completed","status":"<current>"}
410 result_expired job completed 但 MinIO 內 NEF 已被 GC超 expires_at {"error":"result_expired"}
500 internal_error converter / MinIO 故障 {"error":"internal_error"}
502 storage_unavailable MinIO 5xx / 不可達 {"error":"storage_unavailable"}
503 service_busy converter 過載 {"error":"service_busy"}

1.4 Size cap

converter 端不設 size capconverter MinIO 容量為準。visionA backend 端用 io.CopyN(w, stream, 1 GiB) 設 1 GiB 上限保護(同 conversion.md §4.1 既有設計)。

1.5 與既有 endpoint 的關係

converter 既有 POST /api/v1/jobs/:id/download-tokens 是 Phase 2 預留 501apps/task-scheduler/src/routes/v1/jobs.js line 1059-1066。本 ADR 新增的 GET /api/v1/jobs/:id/result 是 Phase 0.8b 的「直接 stream 中轉」實作,不取代 Phase 2 的 download-tokens endpoint後者是「給 browser 直連 converter」設計Phase 2 才評估)。

1.6 converter 端實作要點(給 jimchen 跨 repo 任務)

  • Route 加在 apps/task-scheduler/src/routes/v1/jobs.js(或新建 result.js 並在 v1/index.js router.use(...)
  • 套用既有 requireReadAuth middlewareGET /api/v1/jobs/:id 同一 scopeAPI key 比對由 middleware 統一)
  • handler 流程:
    1. 從 jobService 查 jobstatus 必須是 completed、未過 expires_at
    2. 從 MinIO get objecttarget_object_key 或 promote 後同步保留在 MinIO 的物件 key
    3. set Content-Type / Content-Length / Content-Disposition / Cache-Control headers
    4. pipeline(minioStream, res) 直接 stream不暫存記憶體
  • Test 加 apps/task-scheduler/src/routes/v1/__tests__/result.integration.test.jsmock MinIO + jobService

2. visionA download flow撤回 FAA / MC 鏈)

2.1 流程

Browser ──GET /api/conversion/{job_id}/download (Cookie: visiona_session)──► visionA backend
                                                                              ↓
                                                                         AuthMiddleware (OIDC cookie)
                                                                              ↓
                                                                         ownership 檢查 (user_id ↔ job_id)
                                                                              ↓
                                                                         ensurePromoted (對 converter 冪等 promote)
                                                                              ↓
                                                                         converter_client.GetResult(ctx, job_id)
                                                                              GET {CONVERTER_BASE_URL}/api/v1/jobs/{id}/result
                                                                              Authorization: Bearer <VISIONA_CONVERTER_API_KEY>
                                                                              ↓
                                                                         (converter middleware: ConstantTimeCompare)
                                                                              ↓
                                                                         200 NEF stream + Content-Length + Content-Disposition
                                                                              ↓
                                                                         visionA backend handler
                                                                           - Content-Type: application/octet-stream
                                                                           - Content-Length: <size>
                                                                           - Content-Disposition: attachment; filename="..."
                                                                              visionA 用自己的 defaultDownloadFilename(cj) 覆寫、保留與 wireframe 對齊)
                                                                           - Cache-Control: no-store, no-cache, must-revalidate
                                                                           - io.CopyN(c.Writer, stream, 1 GiB)
                                                                              ↓
                                                                         Browser 收到 200 + attachment + 自動觸發下載

2.2 visionA backend 既有 stream proxy 結構保留

T4 已實作的 internal/conversion/faa_client.go 的 stream proxy 結構io.CopyN + size cap + Content-Disposition + context cancellation全部沿用,只是 stream 來源從 FAA 改 converter。

對應的程式碼變更(給 backend agent 下次任務):

  • internal/conversion/faa_client.go改名 internal/conversion/converter_result_client.go(或合併進 converter_client.go);移除 DownloadWithDelegated 簽名;新增 GetResult(ctx, jobID) (io.ReadCloser, *DownloadMetadata, error)
  • internal/conversion/flow.go DownloadStream 內部移除「IssueDelegatedDownload」步驟、直接呼叫 converter.GetResult
  • internal/conversion/mc_token_client.go 整檔刪除ADR-015 v2.0 §6 部分復活 → 本 ADR 再次砍除Phase 0.8b visionA 端不再有任何 visionA → MC server-to-server 路徑)
  • internal/conversion/conversion.go Service interfaceDownloadStream 簽名不變(仍回 (io.ReadCloser, *DownloadMetadata, error)
  • handler internal/api/conversion.goconversionDownloadHandler 完全不動

3. Promote 路徑完全不變

promote 仍走 visionA → converter POST /api/v1/jobs/{id}/promote

Browser ──POST /api/conversion/{job_id}/promote-to-models──► visionA backend
                                                                ↓
                                                           converter POST /api/v1/jobs/{id}/promote
                                                           Authorization: Bearer <VISIONA_CONVERTER_API_KEY>
                                                                ↓
                                                           converter 內部:用自己的 OAuth client + scope files:upload.write
                                                           PUT FAA /files/{target_object_key}
                                                                ↓
                                                           promote 成功NEF 同時在 FAA 與 converter MinIO
                                                                ↓
                                                           回 {target_object_key} 給 visionA
                                                                ↓
                                                           visionA backend 用既有 /api/models/init + /api/models/finalize 流程把 NEF 寫進 visionA model store
                                                                ↓
                                                           但注意visionA 自己不 pull FAA走的是 converter.GetResult 同一條路徑)

「加到模型庫」流程中 visionA 需要拿到 NEF binary 寫進 model store——本 ADR 後此路徑也走 converter.GetResult(與 download 共用同一條visionA 完全不直接打 FAA。

converter → FAA 這條鏈仍是 OAuth client_credentials + files:upload.write scopeconverter 自己管,與 visionA 無關;本 ADR 不影響)。

4. 認證統一visionA 端只需 VISIONA_CONVERTER_API_KEY

認證 secret 用途 Phase 0.8b v0.5ADR-015 v2.0 Phase 0.8b v0.6(本 ADR
VISIONA_CONVERTER_API_KEY visionA → converterinit / poll / promote / download 使用中 使用中(新增 download 走同一把
VISIONA_OIDC_SERVICE_CLIENT_ID visionA → MC 換 service token給 FAA 線用) 重新啟用 再次廢棄(撤回 v2.0 復活)
VISIONA_OIDC_SERVICE_CLIENT_SECRET 同上 重新啟用 再次廢棄
VISIONA_OIDC_TENANT_ID FAA tenant_id claim 驗證 重新啟用 再次廢棄visionA 端不再需要)
VISIONA_FAA_BASE_URL visionA → FAA 的 base URL 使用中 再次廢棄visionA 端不再直接打 FAA
VISIONA_FAA_API_KEY v1.0 加的) 撤回 維持撤回v2.0 已撤回)

→ visionA 端 server-to-server 認證 secret 只剩 VISIONA_CONVERTER_API_KEY 一把user login 的 OIDC public PKCE 仍維持 ADR-013 設計、不變)。

5. visionA backend mc_token_client 整檔刪除(撤回 v2.0 部分復活)

ADR-015 v2.0 §6 規劃「mc_token_client.go 部分復活」(保留 ServiceToken cache + IssueDelegatedDownload 邏輯)—— 本 ADR 撤回此規劃。

  • mc_token_client.go 整檔刪除commit 86b7175 已經砍掉、不需重建)
  • mc_token_client_test.go 整檔刪除
  • OIDCConfig.ServiceClientID / ServiceClientSecret 兩欄位從 struct 刪除
  • ConversionConfig.TenantID 從 struct 刪除
  • main.go 不再 wire MCTokenClient

考慮過的替代方案 (Alternatives Considered)

方案 A採用converter 新增 GET /api/v1/jobs/{id}/result + visionA stream 中轉

評估 內容
優點 (1) 不動 MC、不動 FAA、不動 warrenchen、不動公司 MC team(2) 認證統一 API keyjimchen 自己管 rotate(3) visionA backend 既有 stream proxy 結構保留、改動範圍小;(4) 失敗模式少(只剩 converter 401 / 4xx / 5xx(5) 修復 ADR-014 §2 從 2026-05-02 起的設計缺口delegated token 鏈本來就 broken(6) converter 與 visionA 都是 jimchen 維護、coordination 成本最低;(7) 與 Phase 1 follow-up「converter 加 download-tokens」路徑相容Phase 2 升級時這個 GET result endpoint 可保留為「server-side pull」用途
缺點 (1) converter 變 download bottleneckvisionA 跨 internet 從 converter stream、再中轉給 user雙倍流量(2) converter MinIO 的 retention 變成 download 可用性 SLAconverter 7 天 expires_at = 7 天後 user 不能 download(3) converter API 變 download 的單點故障converter down → user 不能 download即使 FAA 上有 NEF(4) 跨 repo 任務converter scheduler 加 endpoint— 但因 jimchen 維護兩端、cost 可控
排除原因 未排除 — 採用

方案 Bmetadata-only 模型庫NEF 永遠在 converter MinIO、不 promote 到 FAA

評估 內容
優點 (1) 不必動 FAA / MC(2) promote 流程簡化converter 自己保留 NEF、不 PUT FAA
缺點 (1) Phase 1 已上線的 converter → FAA promote 流程要拆掉converter 端改造範圍大);(2) retention 議題嚴重converter MinIO 7 天 expires_at = NEF 7 天後消失、但 visionA model store 設計是「model 是持久資產」、矛盾;(3) 若要延長 converter MinIO retention 必須改 converter scheduler GC 邏輯、影響其他 converter 用途;(4) visionA 「加到模型庫」流程從「pull NEF 進 visionA 自己 storage」變成「永遠依賴 converter MinIO」違反 ADR-014 §3「加到模型庫 = 進 visionA storage 給後續 device load 用」原意
排除原因 retention 模型不可接受——「加到模型庫」是 user 預期能「永久持有」的動作

方案 Cconverter promote 改成「直接 stream NEF 回 visionA」

評估 內容
優點 promote 同時完成「NEF 已給 visionA」少一次 round-trip
缺點 (1) promote 是 POST + idempotent 設計、回 stream 違反 REST semantic(2) promote 同時要做兩件事(推 FAA + 回 stream失敗模式爆炸推 FAA 成功但回 stream 失敗時怎麼辦?);(3) frontend wireframe 已設計「先 promote、後 user 選下載 / 加到模型庫」的兩步流程、要改 UX(4) 「下載」按鈕本來就是 demand-triggeruser 可能 promote 完很久才下載)、把 stream 綁在 promote 不符合使用場景
排除原因 promote 設計大改、UX 倒退

方案 DvisionA 自己簽 HMAC token、要求 FAA 加新 auth path

評估 內容
優點 visionA 不依賴 MC、token 機制比 API key 細粒度(短 TTL + 綁 object_key + 綁 method
缺點 (1) 動 FAAwarrenchen 要在 FAA dual-authJWT + IDelegatedDownloadTokenValidator外再加第三條「visionA HMAC」路徑、改公司共用 repo(2) HMAC secret 需 visionA + FAA 同步管理(與 API key 一樣 long-lived但多一層複雜度(3) ADR-015 v2.0 §7 「選項 B」已記為 Phase 1+ follow-up量大需回 302 redirect 時才考慮、Phase 0.8b 範圍內動 FAA cost 過高
排除原因 使用者硬約束「不動 FAA」;保留為 Phase 1+ 選項(量大時回 302

方案 E協調 MC team 加 「issue + validate delegated download token」endpoint

評估 內容
優點 修復 ADR-014 §2 原設計(讓那條鏈真的可走通)、長期看是 OAuth 框架的正確補完
缺點 (1) 動 MCMC team 設計 → onboard scope → 註冊 token endpoint → 配 tenant → redeploy2026-05-09 撞 MC scope 沒註冊已驗證跨人協調週期長;(2) 即使 MC 補了 endpoint、FAA 端 MemberCenterDelegatedDownloadTokenValidator._options.DownloadTokenValidationPath 還要對齊配置;(3) 為了 Phase 0.8b 一個 download path 動兩個公司共用 repo 不划算;(4) Phase 1+ 後若決定走 OAuth 框架可重新評估
排除原因 使用者硬約束「不動 MC、不動 FAA」長期可重啟此方案Phase 1+

方案 F放棄 download 功能(只保留「加到模型庫」)

評估 內容
排除原因 PRD Phase 0.8 明確含 downloadwireframe success card「下載」按鈕 + flow-conversion.md 半自動分流);不能放棄

後果 (Consequences)

正面影響

  • 修復 ADR-014 §2 從 2026-05-02 起的設計缺口delegated download token 鏈從未存在,本 ADR 撤回對其的依賴後 visionA download 不再有 dead path
  • 不動 MC、不動 FAA、不動公司 MC team / warrenchen:跨人協調成本歸零,落地速度最快
  • 認證統一visionA 端 server-to-server secret 只剩 VISIONA_CONVERTER_API_KEY 一把MC service client / tenant_id 配置全部撤回env 表簡化
  • 失敗模式收斂原本「visionA → MC 4xx/5xx + MC → service token + MC → delegated token + FAA → service token validate + FAA → delegated token validate」5 條 fail path 收斂為「converter 401 / 4xx / 5xx」3 條
  • 可觀測性減負visionA 端不需追 MC token cache hit rate、MC 失敗率、delegated token issue 失敗率
  • mc_token_client.go 整檔可刪commit 86b7175 已砍掉、本 ADR 確認不需復活(撤回 ADR-015 v2.0 §6 部分復活規劃visionA backend 程式碼庫進一步簡化
  • 與 frontend 對 user 完全透明response shape / call pattern / 錯誤文字一律不變;只是 visionA backend 內部 stream 來源演進

負面影響(接受的取捨)

  • converter 變 download bottleneck:原 ADR-014 §2 設計是「browser 直連 FAA」、流量 1×ADR-015 v2.0 改 server-side stream proxy 後變 2×FAA → visionA → browser本 ADR 後仍是 2× 但路徑改 converter MinIO → visionA → browser(流量 cost 與 v2.0 相同、但 hop 改變)
  • converter MinIO retention 變 download SLAconverter 7 天 expires_at = 7 天後 user 不能 download。對應的 frontend UX
    • expires_at 仍由 visionA 透傳給 frontendconversion.md §2.6.2 既有設計)
    • frontend 在 success card 顯示倒數「6 天 21 小時後自動清除」)
    • 過期後按「下載」會收 410 result_expiredvisionA backend 把 converter 410 透傳)
  • converter 是 download 單點故障converter scheduler 掛 → user 不能 download即使 FAA 上有 NEF沒辦法繞 converter 直接拉 FAA因為 visionA 沒有 delegated token 路徑)
  • 「加到模型庫」流程也依賴 convertervisionA 端 server-side pull NEF 從「直接 FAA」改成「converter result endpoint」、多一 hopconverter 從 MinIO get object 後 stream 給 visionA
  • 與 Phase 1+「browser 直連 converter」路徑稍有不一致converter Phase 2 預留的 POST /api/v1/jobs/:id/download-tokens endpoint 是給 browser 直連 converter 用、與本 ADR 新增的 GET /api/v1/jobs/:id/result 是兩條設計不同的 path。Phase 2 升級時需評估兩條 path 是否共存或合併

風險

風險 緩解
converter MinIO 容量規劃失準(大量 user promote 後 NEF 累積) converter 既有 GC7 天 expires_at即會自動清理Phase 1+ 可加 retention 監控
converter scheduler 在 Phase 0.8b 量小可承受 download bottleneck、但 Phase 1+ 量大時不行 Phase 1+ 升級到方案 DvisionA HMAC token + FAA 加第三條 auth path + 回 302 redirect本 ADR 不阻擋此升級路徑
converter 加新 endpoint 期間jimchen 跨 repo若部署不同步 → visionA download 404 (1) 加 endpoint 是純增量改動、不影響既有 endpoint(2) visionA backend 在 converter 端 endpoint 上線前部署也安全(既有 download path 會直接 404、不會破壞其他功能(3) 部署順序:先 converter ship → 再 visionA ship
converter MinIO 物件不在job 過期、或 promote 沒同步寫到 MinIO converter 端 handler 處理410 result_expiredvisionA backend 透傳給 frontend
converter 端 result endpoint 實作 bugstream pipeline 寫錯導致記憶體爆) 加 integration testmock MinIO 大檔,驗 stream 不暫存)

配套產出(給後續 Phase

Phase 0.8b 範圍內(本 ADR 觸發的 follow-up

A. converter 跨 repojimchen

  • 新增 apps/task-scheduler/src/routes/v1/result.js(或加進 jobs.jsGET /api/v1/jobs/:id/result handler
  • 套用既有 requireReadAuth middlewareAPI key 比對)
  • handler 內job status / expires_at 檢查 → MinIO stream pipeline → 設 headers
  • 整合測試:apps/task-scheduler/src/routes/v1/__tests__/result.integration.test.js
  • 更新 apps/task-scheduler/openapi.yaml(加 GET /api/v1/jobs/{id}/result path
  • 更新 apps/task-scheduler/README.mdAPI 清單加新 endpoint

B. visionA backendbackend agent 下次任務)

  • internal/conversion/mc_token_client.gocommit 86b7175 已砍、確認狀態)
  • internal/conversion/mc_token_client_test.go(確認狀態)
  • internal/conversion/faa_client.go改名 converter_result_client.go 或併入 converter_client.go
    • 移除 DownloadWithDelegated(ctx, delegatedToken, objectKey)
    • 新增 GetResult(ctx, jobID) (io.ReadCloser, *DownloadMetadata, error),內部打 GET {ConverterBaseURL}/api/v1/jobs/{jobID}/result + Authorization: Bearer <ConverterAPIKey>
  • internal/conversion/flow.go
    • 移除 tokens *MCTokenClient 欄位(撤回 v2.0 §6 加回)
    • DownloadStream 內部:移除 IssueDelegatedDownload 步驟、改呼叫 converter.GetResult
    • PromoteToModels 內部(如需要從 FAA pull NEF同樣改呼叫 converter.GetResult
  • internal/config/config.go
    • ConversionConfig.TenantID 欄位刪除(撤回 v2.0 復活)
    • ConversionConfig.FAABaseURL 欄位刪除visionA 不再直接打 FAA
    • OIDCConfig.ServiceClientID / ServiceClientSecret 欄位刪除
    • Enabled() 簡化:只判 ConverterBaseURL != "" && ConverterAPIKey != ""
  • cmd/api-server/main.go:移除 wire MCTokenClient、移除傳 FAA 相關 config
  • .env.stage.example / .env.dev.example
    • 移除 VISIONA_OIDC_SERVICE_CLIENT_ID / _SECRET / VISIONA_OIDC_TENANT_ID / VISIONA_FAA_BASE_URL
    • 保留 VISIONA_CONVERTER_BASE_URL / VISIONA_CONVERTER_API_KEY
  • 改對應 unit / integration test移除 mc_token / faa_client_test 中對 service token / delegated token 的測試;補 converter_result_client_test.go

Phase 0.9 / Phase 1+ follow-up

  • converter API key rotate runbook範圍縮限至 converter沿用 ADR-015 v2.0 規劃)
  • 觀察 converter result endpoint 在 Phase 1 量大時的效能 / 頻寬 cost
  • 若量大需回 302 redirect 模式 → 重新評估方案 DvisionA HMAC token + FAA 加第三條 auth path
  • 若 Phase 2+ 要回 OAuth 框架 → 重新評估方案 E協調 MC / FAA team 補完 delegated token 鏈)

合規性

  • 與使用者確認:本 ADR 採方案 Aconverter 加 GET result endpoint+ 撤回 visionA → FAA / MC 所有 server-to-server 路徑2026-05-16
  • 與 jimchen 確認(同時為 visionA + converter 維護者converter scheduler 加新 endpoint 由 jimchen 處理
  • 對 MC source 完整 grep 驗證MC 沒有 issue / validate delegated download token 的 endpoint致命發現 1 + 2 + 4
  • 對 FAA source 驗證:MemberCenterDelegatedDownloadTokenValidator.cs 確實 assume MC 有 validation endpoint、且 FAA download endpoint 強制只接 delegated token致命發現 4
  • 對 ADR-014 / ADR-015 v2.0 對齊:本 ADR 部分 supersede 兩者ADR-014 §2 整段 + ADR-015 v2.0 §2 整段)
  • 對 ADR-013 對齊user login 的 OIDC public PKCE client 不受影響
  • 與 converter Phase 1 對齊promote 流程不變、converter → FAA 路徑OAuth + files:upload.write)不變
  • 與 MC team 協調補 delegated token endpoint(撤回,本 ADR 不動 MC
  • 與 warrenchen 協調 FAA dual-auth 改造(撤回,本 ADR 不動 FAA
  • DevOps確認 converter 新 endpoint 上線時序(先 converter ship → 再 visionA shiprotate runbook follow-up

相關文件

  • 部分 supersedes
  • 不影響:
  • 詳細實作(本 ADR 同步更新):
    • conversion.md v0.6§1 整體 flow、§2 模組設計、§3 服務間認證、§4.1 download handler、§6 錯誤碼、§9 retry 矩陣、§10 安全考量、變更影響清單)
    • api/api-conversion.md v0.6(檔頭 Auth metadata、§3 promote 與 §4 download 錯誤碼、錯誤碼總覽、版本記錄)
    • oidc-tdd.md v0.4Metadata 區範圍說明、§13.1 環境變數表、§13.1.1 stage env 範例、§13.1.3 server-to-server 雙線設計改寫)
  • Source code 證據:
    • MC controller 列表:/Users/jimchen/member_center/src/MemberCenter.Api/Controllers/*.cs8 個檔、無 file/download/token issuance endpoint
    • FAA delegated token validator/Users/jimchen/file_access_agent/src/FileAccessAgent.Infrastructure/Services/MemberCenterDelegatedDownloadTokenValidator.cs line 27-72
    • FAA download endpoint/Users/jimchen/file_access_agent/src/FileAccessAgent.Api/Program.cs line 184-254RequireAuthorization()
    • converter promote → FAA PUT 已實作:/Users/jimchen/kneron_model_converter/apps/task-scheduler/src/fileAccessAgent/client.js
    • converter v1 routes/Users/jimchen/kneron_model_converter/apps/task-scheduler/src/routes/v1/jobs.js(既有 endpointPOST jobs / GET jobs / GET jobs/:id / POST jobs/:id/promotePOST jobs/:id/download-tokens 預留 501DELETE jobs/:id 預留 501
  • 觸發背景:.autoflow/progress.md 2026-05-16「致命發現 → 拍板選 A → ADR-016」段落

版本記錄

日期 版本 變更
2026-05-16 1.0 初版 — 撤回 visionA → FAA delegated download token 鏈(從 ADR-014 §2 起即為 broken design改走 converter 新增 GET /api/v1/jobs/{id}/result + visionA stream 中轉。視 ADR-014 §2 / ADR-015 v2.0 §2 為部分 supersedeuser loginADR-013/ upload streamingADR-014 §1/ converter API keyADR-015 §1全部不變。涉及 visionA + converter 兩個 repo 改造;不動 MC、不動 FAA、不動 warrenchen。