致命發現(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>
37 KiB
ADR-016:visionA 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 NEF(scope
files:download.read)」的部分:本 Phase 0.8 範圍內 visionA 不主動 pull NEF;如 Phase 1+ 需要 server-side pull,改走 converterGET /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」三 row(visionA 端不再觸發;converter 端有自己的 retry 矩陣,不在本 ADR 範圍)。
- ADR-015 v2.0 §2「visionA → FAA(MC service token + delegated download token)」整段(v2.0 於 §2 維持 ADR-014 §2 設計,本 ADR-016 於 §2 撤回;visionA 端 mc_token_client 整檔再次砍除,OIDC ServiceClient* / TenantID env 廢棄)。
- ADR-014 §2「Download — FAA delegated token」整段(visionA → FAA delegated token 鏈撤回;visionA download 改走 converter
-
沿用(不受本 ADR 影響):
- ADR-013:user login 的 OIDC public PKCE client(與 server-to-server 完全解耦)
- ADR-014 §1(upload 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 → converter(init / poll / promote / result download):API key(ADR-015 §1)
- converter → FAA(promote 內部 PUT NEF):converter 自己用 OAuth client_credentials + scope
files:upload.write(converter 既有實作,與 visionA 無關) - visionA ↔ FAA:Phase 0.8b 範圍內不存在這條鏈(本 ADR 撤回)
- visionA ↔ MC(server-to-server):Phase 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 Center(MC)與 File Access Agent(FAA)的 source code 做全面驗證,發現 ADR-014 §2 設計的 delegated download token 鏈路從 2026-05-02 寫文件至今一直是斷的、只是因為 visionA 從未實際 e2e 跑通 download 而沒人發現。
致命發現 1:MC 沒有「issue delegated download token」endpoint
對 /Users/jimchen/member_center/src/MemberCenter.Api/Controllers/ 完整 grep(8 個 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/authorize(TokenController.cs / OAuthController.cs,OpenIddict OAuth/OIDC 標準)POST /auth/login,/auth/refresh,/auth/logout,/auth/register,/auth/password/{forgot,reset},/auth/email/verify(AuthController.cs)GET/PUT /user/profile(UserController.cs)- Admin endpoints(tenants / oauth-clients / newsletter-lists)
POST /integrations/send-engine/webhook-clients/upsert(SendEngineIntegrationController.cs)POST/GET /subscriptions(SubscriptionsController.cs)POST/GET /newsletter(NewsletterController.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」流程從未存在。
致命發現 2:MC 沒有「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。
致命發現 3:ADR-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 token(scope
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-153(2026-05-16 上午寫)也沿用這個 assumption(雖然 v2.0 描述的設計「正確的」是 ADR-014 §2 原意圖,但 ADR-014 §2 原意圖本身就建立在錯誤的 MC 能力假設上)。
致命發現 4:FAA download endpoint 對 service token 也不接受
/Users/jimchen/file_access_agent/src/FileAccessAgent.Api/Program.cs line 184-254:GET /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 blocker(converter :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 跑到 e2e:T4 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 自己維護的 repo(visionA + converter)內完成、不觸碰 MC / FAA。
既有可用基礎建設
| 元件 | 狀態 | 可用性 |
|---|---|---|
converter promote → FAA PUT NEF(OAuth + files:upload.write) |
Phase 1 已上線 | ✅ work(已在 prod 跑 / apps/task-scheduler/src/fileAccessAgent/client.js) |
| converter MinIO 存 NEF | converter scheduler 既有 | ✅ promote 後 NEF 同時保留在 converter MinIO + FAA(converter expires_at = 7 天) |
| visionA → converter API key(ADR-015 §1) | commit 86b7175 已實作 |
✅ work(init / 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 中轉 解決 download;promote 路徑不變。
1. converter 新增 endpoint:GET /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 id(UUID v4) |
| Auth | Authorization: Bearer <CONVERTER_API_KEY>(同 ADR-015 §1 既有 middleware;converter 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 stream(converter 從 MinIO get object 後直接 stream 出來) |
1.3 Response — 錯誤
| HTTP | code | 條件 | converter 回 body(JSON) |
|---|---|---|---|
| 401 | unauthorized |
API key 不對 / 缺 / 格式錯 | {"error":"unauthorized"}(同 ADR-015 §3.5.1 既有 middleware) |
| 404 | job_not_found |
job_id 不存在 / 已被 GC(converter 7 天 expires_at) | {"error":"job_not_found"} |
| 409 | job_not_completed |
job 尚未 completed(status ≠ 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 cap(converter 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 預留 501(見 apps/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.jsrouter.use(...)) - 套用既有
requireReadAuthmiddleware(與GET /api/v1/jobs/:id同一 scope;API key 比對由 middleware 統一) - handler 流程:
- 從 jobService 查 job(status 必須是
completed、未過 expires_at) - 從 MinIO get object(
target_object_key或 promote 後同步保留在 MinIO 的物件 key) - set Content-Type / Content-Length / Content-Disposition / Cache-Control headers
pipeline(minioStream, res)直接 stream(不暫存記憶體)
- 從 jobService 查 job(status 必須是
- Test 加
apps/task-scheduler/src/routes/v1/__tests__/result.integration.test.js(mock 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.goDownloadStream 內部:移除「IssueDelegatedDownload」步驟、直接呼叫converter.GetResultinternal/conversion/mc_token_client.go整檔刪除(ADR-015 v2.0 §6 部分復活 → 本 ADR 再次砍除;Phase 0.8b visionA 端不再有任何 visionA → MC server-to-server 路徑)internal/conversion/conversion.goService interface:DownloadStream簽名不變(仍回(io.ReadCloser, *DownloadMetadata, error))- handler
internal/api/conversion.go:conversionDownloadHandler完全不動
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 scope(converter 自己管,與 visionA 無關;本 ADR 不影響)。
4. 認證統一:visionA 端只需 VISIONA_CONVERTER_API_KEY
| 認證 secret | 用途 | Phase 0.8b v0.5(ADR-015 v2.0) | Phase 0.8b v0.6(本 ADR) |
|---|---|---|---|
VISIONA_CONVERTER_API_KEY |
visionA → converter(init / 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 key(jimchen 自己管 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 bottleneck(visionA 跨 internet 從 converter stream、再中轉給 user,雙倍流量);(2) converter MinIO 的 retention 變成 download 可用性 SLA(converter 7 天 expires_at = 7 天後 user 不能 download);(3) converter API 變 download 的單點故障(converter down → user 不能 download,即使 FAA 上有 NEF);(4) 跨 repo 任務(converter scheduler 加 endpoint)— 但因 jimchen 維護兩端、cost 可控 |
| 排除原因 | 未排除 — 採用 |
方案 B:metadata-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 預期能「永久持有」的動作 |
方案 C:converter 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-trigger(user 可能 promote 完很久才下載)、把 stream 綁在 promote 不符合使用場景 |
| 排除原因 | promote 設計大改、UX 倒退 |
方案 D:visionA 自己簽 HMAC token、要求 FAA 加新 auth path
| 評估 | 內容 |
|---|---|
| 優點 | visionA 不依賴 MC、token 機制比 API key 細粒度(短 TTL + 綁 object_key + 綁 method) |
| 缺點 | (1) 動 FAA:warrenchen 要在 FAA dual-auth(JWT + 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) 動 MC:MC team 設計 → onboard scope → 註冊 token endpoint → 配 tenant → redeploy;2026-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 明確含 download(wireframe 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 SLA:converter 7 天 expires_at = 7 天後 user 不能 download。對應的 frontend UX:
expires_at仍由 visionA 透傳給 frontend(conversion.md§2.6.2 既有設計)- frontend 在 success card 顯示倒數(「6 天 21 小時後自動清除」)
- 過期後按「下載」會收 410
result_expired(visionA backend 把 converter 410 透傳)
- converter 是 download 單點故障:converter scheduler 掛 → user 不能 download,即使 FAA 上有 NEF(沒辦法繞 converter 直接拉 FAA,因為 visionA 沒有 delegated token 路徑)
- 「加到模型庫」流程也依賴 converter:visionA 端 server-side pull NEF 從「直接 FAA」改成「converter result endpoint」、多一 hop(converter 從 MinIO get object 後 stream 給 visionA)
- 與 Phase 1+「browser 直連 converter」路徑稍有不一致:converter Phase 2 預留的
POST /api/v1/jobs/:id/download-tokensendpoint 是給 browser 直連 converter 用、與本 ADR 新增的GET /api/v1/jobs/:id/result是兩條設計不同的 path。Phase 2 升級時需評估兩條 path 是否共存或合併
風險
| 風險 | 緩解 |
|---|---|
| converter MinIO 容量規劃失準(大量 user promote 後 NEF 累積) | converter 既有 GC(7 天 expires_at)即會自動清理;Phase 1+ 可加 retention 監控 |
| converter scheduler 在 Phase 0.8b 量小可承受 download bottleneck、但 Phase 1+ 量大時不行 | Phase 1+ 升級到方案 D(visionA 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_expired);visionA backend 透傳給 frontend |
| converter 端 result endpoint 實作 bug(stream pipeline 寫錯導致記憶體爆) | 加 integration test(mock MinIO 大檔,驗 stream 不暫存) |
配套產出(給後續 Phase)
Phase 0.8b 範圍內(本 ADR 觸發的 follow-up)
A. converter 跨 repo(jimchen)
- 新增
apps/task-scheduler/src/routes/v1/result.js(或加進jobs.js):GET /api/v1/jobs/:id/resulthandler - 套用既有
requireReadAuthmiddleware(API 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}/resultpath) - 更新
apps/task-scheduler/README.md(API 清單加新 endpoint)
B. visionA backend(backend agent 下次任務)
- 刪
internal/conversion/mc_token_client.go(commit86b7175已砍、確認狀態) - 刪
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.GetResultPromoteToModels內部(如需要從 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:移除 wireMCTokenClient、移除傳 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 模式 → 重新評估方案 D(visionA HMAC token + FAA 加第三條 auth path)
- 若 Phase 2+ 要回 OAuth 框架 → 重新評估方案 E(協調 MC / FAA team 補完 delegated token 鏈)
合規性
- 與使用者確認:本 ADR 採方案 A(converter 加 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 ship);rotate runbook follow-up
相關文件
- 部分 supersedes:
adr-014-conversion-integration.md(§2 download 整段 + §3 加到模型庫的 FAA pull 部分 + §5 FAA service token cache 部分 + §7 retry 矩陣 FAA/MC row)adr-015-server-to-server-api-key.mdv2.0(§2 visionA → FAA 整段)
- 不影響:
adr-013-public-client.md(user login 部分)adr-014-conversion-integration.md§1(upload streaming proxy)、§4(模組劃分)、§6(user_id trust boundary)adr-015-server-to-server-api-key.mdv2.0 §1(visionA → converter API key)
- 詳細實作(本 ADR 同步更新):
conversion.mdv0.6(§1 整體 flow、§2 模組設計、§3 服務間認證、§4.1 download handler、§6 錯誤碼、§9 retry 矩陣、§10 安全考量、變更影響清單)api/api-conversion.mdv0.6(檔頭 Auth metadata、§3 promote 與 §4 download 錯誤碼、錯誤碼總覽、版本記錄)oidc-tdd.mdv0.4(Metadata 區範圍說明、§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/*.cs(8 個檔、無 file/download/token issuance endpoint) - FAA delegated token validator:
/Users/jimchen/file_access_agent/src/FileAccessAgent.Infrastructure/Services/MemberCenterDelegatedDownloadTokenValidator.csline 27-72 - FAA download endpoint:
/Users/jimchen/file_access_agent/src/FileAccessAgent.Api/Program.csline 184-254(無RequireAuthorization()) - 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(既有 endpoint:POST jobs / GET jobs / GET jobs/:id / POST jobs/:id/promote;POST jobs/:id/download-tokens 預留 501;DELETE jobs/:id 預留 501)
- MC controller 列表:
- 觸發背景:
.autoflow/progress.md2026-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 為部分 supersede;user login(ADR-013)/ upload streaming(ADR-014 §1)/ converter API key(ADR-015 §1)全部不變。涉及 visionA + converter 兩個 repo 改造;不動 MC、不動 FAA、不動 warrenchen。 |