致命發現(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>
46 KiB
ADR-015:visionA → converter 採 pre-shared API key 認證(取代 OAuth client_credentials)— 範圍縮限至 visionA → converter
狀態
Accepted — 2026-05-11 / 範圍縮限 — 2026-05-16 (v2.0) / §2 visionA → FAA 整段再次撤回 — 2026-05-16 (v2.1)
v2.1 撤回摘要(2026-05-16 下午):v2.0 §2「visionA → FAA 回到 ADR-014 §2(MC service token + delegated download token)」整段再次撤回。原因:對 MC source 完整驗證後發現 MC 從未有 issue / validate delegated download token endpoint—— ADR-014 §2 從 2026-05-02 起即為 broken design、v2.0 沿用該設計也是 fictional。
v2.1 新設計:visionA download 改走 ADR-016(converter 新增
GET /api/v1/jobs/{id}/result+ visionA stream 中轉)。visionA 端不再有任何 visionA → MC / visionA → FAA 的 server-to-server 路徑。visionA → converter API key 路線(§1)維持完全不變。v2.0 範圍縮限摘要(保留歷史):v1.x 原本決策「visionA → converter / FAA 兩條 server-to-server 線都改 API key」。2026-05-16 使用者拍板撤回 visionA → FAA 改 API key 部分,FAA 線回到 ADR-014 §2 原設計。v2.0 後 v2.1 再次撤回 — FAA 線完全交由 ADR-016 取代(converter 中轉,不再有 visionA → FAA / visionA → MC 鏈)。
上位 / 同層 ADR
- 部分 supersedes:ADR-014 §5「Service token cache — 仿 converter scheduler 模式」中 converter 部分(visionA → converter 的 service token / scope
converter:job.write/read取消)、§7「失敗模式 retry 矩陣」中 converter MC token row(converter 線不再經 MC)。- v2.0 修正(保留歷史):v2.0 一度把 FAA 部分維持 ADR-014 §2 原設計,但 v2.1 整段撤回——FAA 部分的 supersede 改由 ADR-016 接手(visionA download 改走 converter result endpoint,視 ADR-014 §2 / 本 ADR v2.0 §2 為 broken design)。
- v2.1 後:本 ADR 對 ADR-014 的 supersede 範圍維持「§5 中 converter 部分 / §7 中 converter MC token row」;ADR-014 §2 / §5 中 FAA 部分 / §6 / §7 中 FAA + MC delegated token row 由 ADR-016 統一 supersede。
- ADR-014 的其他段落(upload streaming proxy、半自動 promote 原則、不擴 model schema、模組劃分)仍有效。
- 被 supersede(v2.1 新增):本 ADR §2「visionA → FAA」整段被 ADR-016 supersede。visionA download 不再有 visionA → FAA / visionA → MC 任何鏈路。
- 不影響:ADR-013 — user login 的 OIDC public PKCE client 仍照舊。本 ADR v2.1 後只動「server-to-server visionA → converter」這條線;「user login」維持不變;「server-to-server visionA → FAA」這條線直接不存在(由 ADR-016 撤回)。
- 沿用:ADR-006(in-memory state)、ADR-010(user login 的 OIDC BFF Pattern)、ADR-011(取代 StaticAuth)
背景 (Context)
Phase 0.8 OAuth client_credentials 鏈路失敗事件(2026-05-09 ~ 11)
ADR-014 原本設計 visionA backend → converter / FAA 的 server-to-server 認證走 Member Center(MC)的 OAuth client_credentials grant:
- visionA backend 啟動時讀
VISIONA_OIDC_SERVICE_CLIENT_ID/SECRET - 第一次需要時打 MC
POST /oauth/token換 service token(scopeconverter:job.write/read、files:download.read/delegate) - 帶
Authorization: Bearer <service-token>打 converter / FAA - converter / FAA 端 middleware 驗 JWKS 簽章 + 驗 scope + 驗 tenant
Phase 0.8 stage 部署實際跑到才發現整條鏈路有 4 個串行 blocker:
| # | Blocker | 影響 |
|---|---|---|
| 1 | MC stage 沒註冊 converter:job.read/write 兩個 scope(progress.md 5/2 assume 都有 證實是錯的) |
POST /oauth/token 直接 400 invalid_scope |
| 2 | stage 上 converter image 是 5 週前舊版,沒 OAuth middleware、沒 /api/v1/jobs endpoint |
即使 MC 補 scope,converter 仍無法接受 service token |
| 3 | converter 缺 MEMBER_CENTER_* env 設定 |
converter 無法 init OIDC middleware |
| 4 | FAA stage 也可能要 OAuth 整合(warrenchen 維護) | 不確定狀態,需跨人協調 |
要修齊這 4 個 blocker 必須跨 3 個 repo 改 + 同步 MC team + redeploy converter + warrenchen 配合 FAA — 對「2 條 1:1 trust 關係的 server-to-server 整合」明顯過度設計。
v1.x 過度設計的訊號(converter 線適用)
OAuth client_credentials + JWKS + scope 機制適合的場景:
- 多個 client 對同一個 resource server(如 SaaS 平台對外開放 API、第三方 dev 串接),需要區分不同 client 權限、需要短 TTL token 限制 blast radius、需要 scope 細粒度
- 不同團隊 / 不同信任邊界,client 端的 secret 不能由 server 端管理
visionA → converter 的場景完全不符合上面任一個:
- 1:1 trust 關係(visionA 是 converter 唯一的 server-to-server caller,沒有第三方)
- 使用者同時維護 visionA + converter(jimchen),可單方拍板改 middleware
- 全部 internal trust(不是給外部 dev 用,沒有 untrusted client)
- 無需 scope 細分(converter 只關心「是否為 visionA」,單一布林)
而成本:
- 複雜度:MC 端要 onboard scope、issuer JWKS 要可達、token cache 要寫對、TTL 邊界要處理、4 個 5xx / 4xx 失敗模式要 retry & graceful degrade
- 鏈路長度:visionA → MC → cache → converter,任一節點掛掉都不能轉檔
- 可觀測性負擔:要追的 metrics 包含 token cache hit rate、MC 失敗率、scope 對齊狀態
為什麼 v2.0 撤回 FAA 改 API key(FAA 線適用相反邏輯)
v1.0 把同一套「過度設計」邏輯外推到 FAA,但 FAA 線的實際情況不同:
- FAA 是 warrenchen 維護的公司共用 repo:協調成本不對稱(converter 是 jimchen 自己的、可單方改;FAA 改一行都要走跨人協調)
- MC 端針對 FAA 的 scope 早已備妥:使用者於 2026-05-16 提供的 stage service client(
4242ba63099d4f318dd3f143d27ef4c5)含files:upload.write files:metadata.read files:delete files:download.delegate4 個 scope,完整 cover FAA 4 個 endpoint(PUT / GET metadata / HEAD / DELETE / GET file),不需要 MC team 額外 onboard - FAA 已內建 dual-auth 設計:
/Users/jimchen/file_access_agent/src/FileAccessAgent.Api/Program.csline 184-254 顯示GET /files/{key}下載 endpoint 沒掛RequireAuthorization(),改用IDelegatedDownloadTokenValidator.ValidateAsync(...)驗 delegated download token;其他 endpoint(PUT/metadata/HEAD/DELETE)才走 JWT Bearer +EnsureJwtScopeAndTenant。 → FAA 設計上 download 就是要 delegated token、不接 service token;如果硬要把 FAA 全 endpoint 改成 API key middleware,等於要 warrenchen 重寫整套 dual-auth、把既有 delegated token validator 拔掉,遠超「補 middleware」的成本 - 5/9 撞 MC scope 沒註冊的痛主因在 converter 線(
converter:job.read/write兩個 scope 不存在);FAA 線 4 個 scope 已備妥的事實在當時尚未驗證 - converter 線 v1.0 改 API key 已能解決「最痛的」blocker(不必動 MC、不必動 FAA、不必跨人);FAA 線本就 1:N(FAA 之後可能服務多個 visionA-like 產品線),維持 OAuth 框架對 FAA 端架構演進更友善
→ v2.0 縮限至「只動 converter 線;FAA 線回到 ADR-014 §2 原設計」是更精確的責任邊界劃分。
已洩漏的 stage service client secret(v1.x 觀察事實 — v2.0 部分仍適用)
RciRUyiCkbd60ikkZGkfQ2xV4r02VW3/j0ASKV/DD/E= 已在對話中外洩(progress.md 2026-05-11 紀錄)。
- v1.x 的處理:改用 API key 後此 secret 直接作廢、不需 rotate
- v2.0 的處理:FAA 線改回 service token 路線後,使用者於 2026-05-16 提供的新 stage service client
4242ba63099d4f318dd3f143d27ef4c5取代舊 client(舊的洩漏值仍作廢、不重用);新 secret 僅放 stage host.env.stage與部署 secret store、絕不進 git / 文件(本 ADR、TDD、env example 一律不寫真實 secret 值)
決策 (Decision)
v2.0 範圍:採 pre-shared API key + Authorization: Bearer <api-key> header 取代 OAuth client_credentials,僅適用於 visionA → converter。visionA → FAA 維持 ADR-014 §2 原設計(MC service token + delegated download token)。
1. visionA → converter(v1.0 採用 / v2.0 維持)
visionA backend 啟動時
↓
讀 env VISIONA_CONVERTER_API_KEY
↓
[轉檔請求進來]
↓
打 converter:
Authorization: Bearer <VISIONA_CONVERTER_API_KEY>
converter 端 middleware:
讀 env CONVERTER_API_KEY
↓
[收到請求]
↓
parse Authorization header → 取 token
↓
subtle.ConstantTimeCompare(token, CONVERTER_API_KEY)
↓
match → 放行;mismatch → 401
2. ⚠️ visionA → FAA(v2.1:整段撤回,改走 ADR-016 converter 中轉)
v2.1 撤回(2026-05-16 下午):v2.0 在本節「FAA 線回到 ADR-014 §2 原設計(MC service token + delegated download token)」整段撤回。
理由(致命發現 2026-05-16):
- MC source 沒有
POST /file-access/download-tokensendpoint(visionA 無法跟 MC 換 delegated token)- MC source 沒有 FAA
IDelegatedDownloadTokenValidatorassume 的 introspection endpoint(即使有 token 也無法 validate)- FAA
GET /files/{key}強制只接 delegated token、不接 service token→ ADR-014 §2 與本 ADR v2.0 §2 描述的「visionA → MC → FAA delegated token 鏈」完全是 fictional(從 2026-05-02 起未曾 e2e 跑通過)。
v2.1 採用的設計:visionA download 改走 ADR-016(converter 新增
GET /api/v1/jobs/{id}/resultendpoint + visionA stream 中轉)。visionA 端不再有任何 visionA → MC server-to-server 路徑、不再有任何 visionA → FAA 直接呼叫。本節以下內容僅作歷史保留、實作以 ADR-016 為準;v2.1 後對應的 visionA 端 code 應撤回(mc_token_client.go 不需復活,已於 commit
86b7175砍除 → 維持砍除;OIDCConfig.ServiceClientID/Secret + ConversionConfig.TenantID 維持 v1.x 廢棄狀態)。
v2.0 原採用的設計(即 ADR-014 §2 原設計,v2.1 整段撤回,僅作歷史保留):
visionA backend 啟動時
↓
讀 OIDCConfig.ServiceClientID / ServiceClientSecret + ConversionConfig.TenantID
↓
[需要打 FAA,例如「加到模型庫」server-to-server pull、或「下載」proxy]
↓
visionA → MC POST {issuer}/oauth/token
grant_type=client_credentials
client_id=<ServiceClientID>
client_secret=<ServiceClientSecret>
scope=files:upload.write files:metadata.read files:delete files:download.delegate
↓
MC 回 service access_token(cache 至 exp - 15s)
↓
A 路線(FAA 寫 / metadata / delete / s2s download):
visionA 帶 Authorization: Bearer <service-token> 打 FAA
FAA 端 .RequireAuthorization() + EnsureJwtScopeAndTenant 驗
├─ JWT 簽章(FAA `AddJwtBearer` Authority = MC issuer,自動 JWKS)
├─ Audience(FAA `Auth:Audience`)
├─ scope claim(PUT 要 files:upload.write;GET metadata / HEAD 要 files:metadata.read;DELETE 要 files:delete)
└─ tenant_id claim 必須等於 instanceOptions.TenantId
→ 通過則放行
B 路線(download stream proxy — visionA backend 中轉到 browser):
1. visionA 帶 service-token 打 MC POST /file-access/download-tokens
(scope: files:download.delegate;針對特定 object_key + GET method + 5 分鐘 TTL)
2. MC 回 delegated download token (opaque)
3. visionA 帶 Authorization: Bearer <delegated-token> 打 FAA GET /files/{key}
4. FAA 端 GET /files/{key} 沒掛 .RequireAuthorization(),
改走 IDelegatedDownloadTokenValidator.ValidateAsync(...)
├─ token active
├─ tenant_id match
├─ object_key match
└─ method == "GET"
→ 通過則 stream NEF binary
5. visionA backend io.CopyN(...) 中轉回 browser
為什麼 v2.0 設計「download 線必須是 delegated token」而非「service token」:
FAA 端的 dual-auth 設計(已實作於 /Users/jimchen/file_access_agent/src/FileAccessAgent.Api/Program.cs)強制如此:
| FAA endpoint | line range | auth 機制 | 適用 scope / token type |
|---|---|---|---|
GET /files/metadata/{**objectKey} |
80-111 | .RequireAuthorization() + EnsureJwtScopeAndTenant |
files:metadata.read (service token) |
HEAD /files/{**objectKey} |
113-148 | .RequireAuthorization() + EnsureJwtScopeAndTenant |
files:metadata.read (service token) |
PUT /files/{**objectKey} |
150-182 | .RequireAuthorization() + EnsureJwtScopeAndTenant |
files:upload.write (service token) |
GET /files/{**objectKey} |
184-254 | 無 .RequireAuthorization();用 IDelegatedDownloadTokenValidator.ValidateAsync(...) |
files:download.delegate 換出來的 delegated token |
DELETE /files/{**objectKey} |
256-287 | .RequireAuthorization() + EnsureJwtScopeAndTenant |
files:delete (service token) |
→ FAA GET /files/{key} 不接 service token,必須用 MC 簽的 delegated download token。
→ visionA download flow 必須做「換 delegated token」這一步、不能省。
→ visionA「加到模型庫」server-to-server pull 流程因為走的也是 GET /files/{key} 下載端點,也要走 delegated download token 路徑(v2.0 修正:v1.x 與 ADR-014 §3 「scope files:download.read」描述不精確;FAA 端 source 真相是 download endpoint 一律用 delegated token,scope files:download.delegate 是 service client 用來「向 MC 換 delegated token」的能力,不是 FAA 端 endpoint 接收的 scope)
stage 端證據(使用者 2026-05-16 提供):
- FAA stage URL:
https://stage-9527.innovedus.com:5081 - TenantId:
732270c0-449c-489c-bfad-321e9bf89b3d - ServiceClientId:
4242ba63099d4f318dd3f143d27ef4c5(取代 v1.x 提到的舊 client23605e14...) - ServiceScopes:
files:upload.write files:metadata.read files:delete files:download.delegate - ServiceClientSecret:放 stage host
.env.stage,不進 git / 文件
待 verify(合規性段落追蹤):MC stage 端是否確實註冊上述 4 個 scope 並對該 service client 生效,需 stage redeploy 前實測 POST /oauth/token 拿到含 4 scope 的 access_token。
3. 單一下游:converter(v1.x「每個下游各自獨立的 key」表格 v2.0 縮限)
v1.x 的 key 表格 縮限至 converter 一條:
| Key | 持有者 | 用途 |
|---|---|---|
VISIONA_CONVERTER_API_KEY(visionA 端) / CONVERTER_API_KEY(converter 端) |
jimchen | visionA → converter |
v1.x 中的 FAA key row(VISIONA_FAA_API_KEY / FAA_API_KEY)撤回——FAA 改回 MC service token 路徑、不需要 pre-shared API key。
理由(converter 維持):每條 trust boundary 各自獨立。converter 線 1:1 trust,雙方都由 jimchen 維護,rotate 對齊成本可控。
3.5 Reference Middleware Implementation(v2.0 縮限至 converter 端)
本節提供 converter 端 middleware 的可直接照抄 reference snippet。
3.5.1 converter 端(Go — net/http 標準 middleware pattern)
採 net/http 標準 middleware pattern,可直接套用 chi / gorilla/mux / 原生 http.ServeMux。subtle.ConstantTimeCompare 是 Go 標準庫提供的 constant-time 比較函式(避免 timing attack)。
// internal/middleware/apikey.go
package middleware
import (
"crypto/subtle"
"errors"
"log/slog"
"net/http"
"strings"
)
// ErrAPIKeyNotConfigured 啟動時 server 端 API key 未設定 — 應在 main() init 時 fail-fast、
// 不要等到第一個 request 才發現
var ErrAPIKeyNotConfigured = errors.New("CONVERTER_API_KEY env not set")
// NewAPIKeyAuth 回傳一個驗證 Authorization: Bearer <api-key> 的 middleware。
// 若 expectedKey 為空字串,會直接 panic(啟動 fail-fast)— 避免「未設定 = 全部放行」的災難。
func NewAPIKeyAuth(expectedKey string, logger *slog.Logger) func(http.Handler) http.Handler {
if expectedKey == "" {
// 啟動時偵測 — 不允許 server 在沒有 key 的狀態下啟動
panic(ErrAPIKeyNotConfigured)
}
expectedBytes := []byte(expectedKey)
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
authHeader := r.Header.Get("Authorization")
// missing Authorization header
if authHeader == "" {
logger.Warn("api key auth failed",
"reason", "missing_authorization_header",
"path", r.URL.Path,
"remote", r.RemoteAddr)
writeUnauthorized(w)
return
}
// 不是 Bearer prefix(必須有 "Bearer " 前綴 + 空格)
const prefix = "Bearer "
if !strings.HasPrefix(authHeader, prefix) {
logger.Warn("api key auth failed",
"reason", "missing_bearer_prefix",
"path", r.URL.Path,
"remote", r.RemoteAddr)
writeUnauthorized(w)
return
}
token := strings.TrimSpace(authHeader[len(prefix):])
// token 為空("Bearer " 後面什麼都沒有)
if token == "" {
logger.Warn("api key auth failed",
"reason", "empty_token",
"path", r.URL.Path,
"remote", r.RemoteAddr)
writeUnauthorized(w)
return
}
// constant-time compare — 即使長度不同 ConstantTimeCompare 也會回 0,但為了
// 提早 short-circuit、先檢查長度可避免 hash 不必要的工作(長度本身不是 secret)
if subtle.ConstantTimeCompare([]byte(token), expectedBytes) != 1 {
logger.Warn("api key auth failed",
"reason", "token_mismatch",
"path", r.URL.Path,
"remote", r.RemoteAddr)
// 注意:log 絕對不印 token 本身
writeUnauthorized(w)
return
}
// 通過 — 放行到 next handler
next.ServeHTTP(w, r)
})
}
}
// writeUnauthorized 統一回 401 — 不洩漏「key 對 / 不對」/ 「missing / mismatch」的差異
func writeUnauthorized(w http.ResponseWriter) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusUnauthorized)
_, _ = w.Write([]byte(`{"error":"unauthorized"}`))
}
main 端使用範例:
// cmd/converter/main.go(節錄)
expectedKey := os.Getenv("CONVERTER_API_KEY")
authMiddleware := middleware.NewAPIKeyAuth(expectedKey, logger)
// 套用到所有 /api/v1/* routes
mux.Handle("/api/v1/", authMiddleware(apiHandler))
3.5.2 FAA 端(C# ASP.NET Core middleware)(v2.0 撤回 — 整段刪除)
v2.0 撤回:v1.1 在此處提供的 FAA 端 ASP.NET Core middleware snippet(含 Classic Middleware Class 寫法 A + Minimal API Inline Middleware 寫法 B)整段刪除。
理由:FAA 線回到 ADR-014 §2 原設計(MC service token + delegated download token)後,FAA 端不需要新增任何 API key middleware:
- 既有
AddJwtBearer+RequireAuthorization()+EnsureJwtScopeAndTenant已涵蓋 PUT / metadata / HEAD / DELETE 4 個 endpoint- 既有
IDelegatedDownloadTokenValidator已涵蓋 GET download endpoint- FAA 端零變更——這是 v2.0 撤回的核心收益(不必動公司共用 FAA repo、不必跟 warrenchen 協調)
3.5.3 部署檢查清單(v2.0 縮限至 converter 端)
不分 client 端 / server 端,部署前 converter 兩側必須逐項確認:
| # | 檢查項 | 為什麼 |
|---|---|---|
| 1 | env 已設定且非空(啟動 fail-fast) | 避免「未設定 = 全部放行」災難;server 應在啟動時 panic / throw、不要等到第一個 request 才發現 |
| 2 | constant-time compare(Go subtle.ConstantTimeCompare) |
避免 timing attack 反推 key |
| 3 | 401 response body 統一(不洩漏「key 對 / 不對」/ 「missing / mismatch」差異) | 對外只回 {"error":"unauthorized"},差異只記在 server 端 log |
| 4 | log 絕對不印 token 本身 | 即使是失敗的 token 也不印(攻擊者可能用半正確的 token 試探);只印 reason / path / remote |
| 5 | Bearer prefix 嚴格驗證(缺 prefix 也 401) | 不允許 Authorization: <token> 這種格式(即使內容對也 reject、強制 client 用標準 Bearer scheme) |
| 6 | 每環境(dev / stage / prod)獨立 key | 嚴格分環境產 key(openssl rand -hex 32 各 64 字元 hex),不重用 |
| 7 | key 不進 git | .gitignore 嚴格 ignore .env*;CI / CD secret 從 Secrets Manager / Vault 注入 |
| 8 | 健康檢查 endpoint 是否要 bypass auth | 通常 /healthz / /readyz 應 bypass(讓 LB / k8s 可探測),但業務 endpoint 全部要 auth |
FAA 端不需要本清單——v2.0 起 FAA 端使用既有 OAuth + delegated token 機制,無新增 API key middleware。FAA 端的 OAuth / delegated token 部署檢查請參考 ADR-014 §2 與
conversion.md§3.2。
4. 不再有 scope 概念(converter 線適用;FAA 線 v2.0 撤回)
converter 線:OAuth client_credentials 設計中兩個 converter scope(converter:job.write/read)取消。
- 單一 API key 就是「visionA 有權打 converter」的完整證明
- 不再有「同一個 client 但拿不同 scope」的細粒度區分(在 1:1 trust 中本來就沒意義)
- converter 端 middleware 也不需要驗 scope
FAA 線:v1.x 取消 files:upload.write / files:metadata.read / files:delete / files:download.delegate 4 個 scope 的決策撤回。FAA 4 個 scope 全部恢復、由 MC service client 4242ba63... 持有,並由 FAA EnsureJwtScopeAndTenant 驗。
5. tenant 概念
converter 線:visionA → converter 不再帶 tenant_id。converter 端的 user_id 從 multipart body user_id field 拿(仍由 visionA 從 OIDC sub 灌入,這條 trust boundary 不變)。converter 端不需要 tenant 概念。
FAA 線(v2.0 修正 — 從 v1.x「不再有 tenant」改為「FAA 線需要 tenant」):
- FAA 端
EnsureJwtScopeAndTenant函式會驗 service token 內的tenant_idclaim 等於instanceOptions.TenantId(見 FAAProgram.csline 303-312) - delegated download token 路徑也驗
validationResult.TenantId.Value != instanceOptions.TenantId(line 218-221) - → visionA 的 service token / delegated download token 必須含正確的
tenant_idclaim - 來源:MC service client
4242ba63...註冊時對應的 tenant,stage 為732270c0-449c-489c-bfad-321e9bf89b3d - →
VISIONA_OIDC_TENANT_IDenv 重新啟用(v1.x 標廢棄;v2.0 撤回廢棄)
6. visionA backend 移除的程式碼(v1.x 移除清單,v2.0 部分復活)
| 項目 | v1.x 處理 | v2.0 處理 |
|---|---|---|
internal/conversion/mc_token_client.go(整個 package) |
整個檔案刪除(~440 行) | 部分復活 — 保留 service token cache + delegated download token issue 邏輯(給 FAA 用),但不再被 converter_client 引用 |
internal/conversion/converter_client.go 內呼叫 MCTokenClient.ServiceToken() |
改成讀 cfg.Conversion.ConverterAPIKey 直接 set header |
同 v1.x(不變)—— converter 線維持 API key |
internal/conversion/faa_client.go 內呼叫 MCTokenClient.ServiceToken() |
改成讀 cfg.Conversion.FAAAPIKey 直接 set header |
撤回——回到 v1.x 之前:呼叫 MCTokenClient.ServiceToken() 拿 service token、帶 Authorization: Bearer <service-token>;download 路徑額外呼叫 MCTokenClient.IssueDelegatedDownload(...) |
internal/conversion/flow.go 內呼叫 mc.IssueDelegatedDownload() |
「delegated download token 路徑取消」 | 撤回——download stream proxy 路徑要呼叫 IssueDelegatedDownload(...) 拿 token、再呼叫 faa.DownloadWithDelegated(token, objectKey) |
internal/config/config.go 內 OIDCConfig.ServiceClientID / ServiceClientSecret 兩個欄位 |
廢棄(保留 struct field 為 backward compat、不再使用);env VISIONA_OIDC_SERVICE_CLIENT_ID / SECRET 從 .env*.example 移除 |
重新啟用——FAA 線需要;env 加回 .env.stage.example |
internal/config/config.go 內 ConversionConfig.TenantID 欄位 + env VISIONA_OIDC_TENANT_ID |
conversion 模組不再依賴;如其他模組未使用即可移除 | 重新啟用——FAA 端 EnsureJwtScopeAndTenant 驗 tenant_id claim、token 必須帶 |
config 欄位 v2.0 樣貌:
// internal/config/config.go
type ConversionConfig struct {
ConverterBaseURL string // 既有
FAABaseURL string // 既有
ConverterAPIKey string // v1.0 新增 — env VISIONA_CONVERTER_API_KEY(v2.0 維持)
// FAAAPIKey string // v1.0 加的;v2.0 撤回(不再需要)
TenantID string // v1.x 廢棄;v2.0 重新啟用 — env VISIONA_OIDC_TENANT_ID
}
// OIDCConfig 維持有 ServiceClientID / ServiceClientSecret 兩欄位(v2.0 重新啟用)
type OIDCConfig struct {
// ... user login 相關欄位(不變)...
ServiceClientID string // v2.0 重新啟用 — env VISIONA_OIDC_SERVICE_CLIENT_ID
ServiceClientSecret string // v2.0 重新啟用 — env VISIONA_OIDC_SERVICE_CLIENT_SECRET
}
// Enabled 改判定:
func (c ConversionConfig) Enabled() bool {
return c.ConverterBaseURL != "" &&
c.FAABaseURL != "" &&
c.ConverterAPIKey != "" &&
// FAA 線判定 OIDCConfig 有 ServiceClientID / Secret + ConversionConfig.TenantID
// — 由 main.go 啟動時組合判斷,這裡只判 conversion 自己的欄位
c.TenantID != ""
}
7. Delegated download token 路徑的處理(v2.0 撤回 v1.x 的撤回,回到 ADR-014 §2 設計)
ADR-014 §2 原設計 download 流程:
browser → visionA /download → MC issue delegated token → 302 → browser → FAA?access_token=...
v1.x 的選項 A(短期 server-side proxy with API key)撤回:v1.0 / v1.1 在本節原本提案「visionA backend 直接用 Authorization: Bearer <FAA_API_KEY> 拉 FAA、stream 回 browser」整段全部撤回。
v2.0 採用的設計(保留 server-side stream proxy 不退回 302、但 token 來源改回 delegated download token):
browser → visionA /download → visionA backend
↓
ownership 檢查
↓
ensurePromoted(拿 target_object_key)
↓
MCTokenClient.ServiceToken() → MC service access_token
↓
MCTokenClient.IssueDelegatedDownload(token, object_key, "GET", 5min)
→ MC POST /file-access/download-tokens
↓
FAA GET /files/{key}
Authorization: Bearer <delegated-token>
↓
FAA IDelegatedDownloadTokenValidator.ValidateAsync(...)
↓
FAA stream NEF binary
↓
visionA backend io.CopyN → browser
為什麼保留 server-side stream proxy(不退回 ADR-014 §2 的 302 redirect):
- T4 已經在 Phase 0.8 把 download 改成 server-side stream proxy(
conversion.mdv0.4 /api/api-conversion.mdv0.4),實測 frontend<a href download>流程已驗證 - 退回 302 redirect 等於 frontend 行為改變、要重做 e2e 驗證、無收益
- delegated token 在 server-side(不洩漏給 frontend JS / browser URL bar)反而比 302 模式更安全
- 流量成本(每次下載繞 visionA backend N×)Phase 0.8 MVP 量小可接受;Phase 1 量大時再評估升級
為什麼 token 來源改回 delegated download token(不繼續用 v1.x 的 visionA API key):
- FAA
GET /files/{key}endpoint 強制使用 delegated token(line 184-254 沒掛RequireAuthorization()、改用IDelegatedDownloadTokenValidator),FAA 端不接受其他 token type - v1.x 若要用 visionA API key,需要 warrenchen 在 FAA 端為 download endpoint 額外實作「API key middleware」並改寫 dual-auth 路由——成本遠超 v2.0 撤回的收益
選項 B(Phase 1+ HMAC token 升級路徑)保留為 follow-up:
如果 Phase 1 流量壓力大要回 302 redirect 模式,visionA 可以自己簽 short-TTL HMAC token(不需要 MC 介入),FAA 端 middleware 多加一條「驗 visionA HMAC token」的路徑:
browser → visionA /download → visionA 用 HMAC_KEY 簽 short-TTL token
↓
302 → browser
↓
browser → FAA?access_token=<visionA-signed-hmac>
↓
FAA middleware:JWT (s2s) OR delegated (current) OR HMAC (browser direct) 三選一
此升級路徑與本 ADR v2.0 決策無衝突,記入 Phase 1 follow-up(同 v1.x 規劃)。
8. user_id 注入 trust boundary 不變(v1.x / v2.0 一致)
ADR-014 §6「visionA backend 是唯一灌 user_id 的點」邏輯不變:
- user_id 仍從 OIDC cookie session 拿(OIDC sub)
- 仍透過 multipart streaming 注入 converter request 的
user_idfield(converter 端視 visionA 為 trusted caller) - API key(converter)/ service token(FAA)證明的是「caller 是 visionA」,user_id 的真實性由 visionA 內部的 OIDC 機制保證 — 兩條獨立鏈
9. 部署層的 env 注入(v2.1 修訂)
Phase 0.8b v2.1 採用(visionA 端 server-to-server secret 只剩 converter API key 一把):
| Env | Stage | Production | v2.1 變更 |
|---|---|---|---|
VISIONA_CONVERTER_API_KEY(visionA 端) |
.env.stage(jimchen 持有,不進 git) |
AWS Secrets Manager / Vault | 維持(v1.0 新增、v2.0 維持、v2.1 維持) |
CONVERTER_API_KEY(converter 端) |
.env(jimchen 持有,不進 git) |
同上 | 維持 |
VISIONA_CONVERTER_BASE_URL |
.env.stage |
Secrets Manager | 維持(既有) |
VISIONA_FAA_API_KEY(visionA 端) |
— | — | 撤回(v1.0 加的;v2.0 移除;v2.1 維持移除) |
FAA_API_KEY(FAA 端) |
— | — | 撤回(v1.0 加的;v2.0 移除;v2.1 維持移除) |
VISIONA_OIDC_SERVICE_CLIENT_ID |
— | — | v2.1 再次撤回(v1.x 廢棄;v2.0 重新啟用;v2.1 撤回 v2.0 啟用) |
VISIONA_OIDC_SERVICE_CLIENT_SECRET |
— | — | v2.1 再次撤回(同上) |
VISIONA_OIDC_TENANT_ID |
— | — | v2.1 再次撤回(同上) |
VISIONA_FAA_BASE_URL |
— | — | v2.1 撤回(visionA 端不再直接打 FAA、走 converter 中轉) |
key 產生方式:
- converter API key(兩端對齊):
openssl rand -hex 32(64 字元 hex) service client secret(v2.1 不需要)
v2.1 後 visionA 端 server-to-server 鏈路收斂為單條:visionA → converter(API key),download 也走同一條(converter GET /api/v1/jobs/{id}/result,詳見 ADR-016)。
考慮過的替代方案 (Alternatives Considered)
方案 A:維持 OAuth client_credentials(ADR-014 原方案 — converter / FAA 兩條都走)
| 評估 | 內容 |
|---|---|
| 優點 | 標準化、跨團隊可重用、短 TTL token、scope 細粒度可控 |
| 缺點 | 需要 MC team 配合 onboard converter scope、converter / FAA 都要重寫 middleware、JWKS 取得失敗時 graceful degrade 複雜、stage e2e 鏈路 4 個 blocker 全要修齊 |
| 排除原因 | 對 1:1 internal trust 場景(converter)過度設計;Phase 0.8 stage 部署實際遇到的 4 個 blocker 證明這條路 ROI 不好 |
方案 B:mTLS(mutual TLS)
| 評估 | 內容 |
|---|---|
| 優點 | 不需傳遞 secret in plaintext(憑證綁定)、cert rotation 機制成熟 |
| 缺點 | converter / FAA 都要支援 mTLS、需要 CA 管理、ingress(nginx / Caddy / ALB)也要配合 client cert termination、stage 環境部署成本高 |
| 排除原因 | 對 1:1 trust 過度設計;公司 stage 環境 ingress(host nginx)未對外開放 mTLS 配置;維運成本不成比例 |
方案 C:API key + IP allowlist 雙層防護
| 評估 | 內容 |
|---|---|
| 優點 | 即使 API key 洩漏,攻擊者也需從特定 IP 才能用 |
| 缺點 | visionA 上 AWS 後 IP 不固定(NAT gateway / ALB 對外多 IP);converter / FAA 在公司內網 IP allowlist 維護成本高;對「不是內網」的 prod 場景幾乎沒用 |
| 排除原因 | Phase 0.8 stage 仍在公司內網(visionA stage 在 192.168.0.x),加 IP allowlist 在 stage 可行但對 prod 沒有延展性;不採用 |
方案 D:共用一把 API key(不分 converter / FAA)
| 評估 | 內容 |
|---|---|
| 優點 | env 少一個、部署設定簡單 |
| 缺點 | 一處洩漏兩處連坐;converter rotate 必須同步 FAA;違反「每條 trust boundary 各自獨立」原則 |
| 排除原因 | 在 v1.x 兩條都 API key 的設計中作為反方案被排除;v2.0 因 FAA 撤回 API key、議題自然不存在 |
方案 E(v2.0 採用):visionA → converter API key、visionA → FAA 仍走 MC service token + delegated download token
| 評估 | 內容 |
|---|---|
| 優點 | (1) 不必動 FAA repo(warrenchen 維護成本歸零);(2) 不必動 MC 端 scope onboarding(4242ba63 service client 已備妥 4 scope);(3) FAA 既有 dual-auth 設計(JWT for write / metadata / delete + delegated token for download)零修改;(4) converter 線 v1.0 已得到的「不必動 MC、不必協調」收益對 converter 而言維持;(5) FAA 線維持 OAuth 框架對 FAA 多 client 演進更友善(FAA 之後可能服務多個 visionA-like 產品線) |
| 缺點 | (1) visionA 仍要保留 mc_token_client 的 service token cache + delegated download token issue 邏輯(v1.x 砍掉的 ~440 行要部分復活);(2) MC 仍是 FAA 線的依賴(MC 掛 → FAA 用不了,但 converter 不受影響);(3) 兩條線的認證機制不對稱(converter API key、FAA OAuth),心智負擔略高 |
| 排除 v1.0 方案(兩條都 API key)的原因 | 使用者 2026-05-16 拍板:(1) 不希望動 FAA(共用 repo、warrenchen 維護);(2) 不希望動 MC(5/9 撞 scope 沒註冊的痛);(3) 但 5/16 提供的 service client 4242ba63... 證明 MC 端針對 FAA 的 4 個 scope 已備妥 — v1.0 拒絕走 OAuth 的「MC scope 沒備妥」前提在 FAA 線不成立 |
| 採用 | v2.0 採用 |
後果 (Consequences)
正面影響
- converter 線實作大幅簡化(v1.0 收益保留):visionA backend 對 converter 的呼叫不查 cache、不打 MC、不重簽
- converter / FAA stage e2e blocker 收斂:converter 線 0 個 blocker;FAA 線靠使用者已備妥的 service client
4242ba63...驗證後即可上線(待 verify) - 不必動 FAA repo、不必動 warrenchen(v2.0 新增收益):v1.x 規劃要 warrenchen 配合改 FAA middleware 的工作完全取消
- 不必動 MC scope onboarding(部分 v2.0 新增收益):FAA 線 4 個 scope 已備妥;converter 線本來就不依賴 MC
- converter middleware 極簡(v1.0 收益保留):只需「比對單一字串 + constant-time compare」
- converter 失敗模式收斂:原本「MC 5xx / MC 4xx / token cache miss / scope mismatch」四個失敗類型,收斂為「API key 對 / 不對」單一布林
- 可觀測性減負(部分):converter 線不需追 token cache hit rate / MC 失敗率;FAA 線仍需追,但範圍縮小一半
- 已洩漏的 stage service client secret
RciRUyi...直接作廢:使用者於 v2.0 提供新 client4242ba63...取代
負面影響(接受的取捨)
- converter 線 API key 是 long-lived secret:不像 OAuth token 有 TTL(通常 1 小時);rotate 需要 visionA + converter 同步換 env 並 redeploy;secret 管理責任更重
- converter 線沒有 scope 細粒度:未來如果 visionA 內部要區分「能 init job 但不能 promote」這種需求,要回頭加(但 1:1 trust 場景幾乎不會有此需求)
- converter 線沒有 audit trail(誰用 token 做什麼):OAuth + JWT 的 sub claim 提供天然 audit;API key 模式下 converter 只知道「是 visionA」,需要靠 visionA 內部 log + request_id 串接才能追到 user_id(既有 request_id 機制可滿足)
- FAA 線維持 OAuth client_credentials 鏈條的部分複雜度(v2.0 新增):visionA 仍需 mc_token_client(service token cache + delegated download token issue + retry policy);MC 仍是 FAA 線的單點依賴;FAA 線的可觀測性負擔(token cache hit rate / MC 失敗率)保留
- 兩條線認證機制不對稱(v2.0 新增):converter 線 API key、FAA 線 OAuth;運維 / 排查時需區分兩種失敗模式(converter 401 = key 不同步;FAA 401 = service token 取得失敗 / scope mismatch / tenant mismatch / delegated token 過期)
風險
| 風險 | 緩解 |
|---|---|
| converter API key 洩漏(git log、log shipping、Slack 訊息等) | (1) .gitignore 嚴格 ignore .env*;(2) log 不印 token;(3) stage / prod 用不同 key;(4) 一旦發現洩漏 → 換 env → redeploy(雙方協調 < 1 小時可完成) |
| converter API key Rotate 流程缺失 | 配套必須產出 rotate runbook(jimchen 對 converter) |
| FAA 線 service client secret 洩漏(v2.0 新增) | 同 v1.x 處理:MC team 換 client secret → visionA 同步 env → restart;運維事件,需跨 MC team 協調 |
| MC stage 端 4242ba63 service client 4 個 scope 是否真的有效 / 啟用(v2.0 新增) | 待 verify:stage redeploy 前實測 POST /oauth/token 帶 4 scope 拿到 access_token;若部分 scope 缺失需 MC team 補(合規性段落追蹤) |
| FAA 線 MC 不可達 → 無法 issue delegated token → 下載失敗(v2.0 新增) | graceful degradation:對 frontend 回 502 download_token_failed / mc_token_unavailable(仍維持 ADR-014 §7 原 retry 矩陣) |
| 開發環境 / stage / prod 用同一把 converter API key | 嚴格分環境產 key(dev / stage / prod 各自 openssl rand -hex 32),不重用 |
合規性
- 與使用者確認:v2.0 範圍縮限至 visionA → converter API key;visionA → FAA 回到 ADR-014 §2 原設計(2026-05-16)
- 與 jimchen 確認(同時為 visionA + converter 維護者):converter middleware 改寫由 jimchen 處理(沿用 v1.0 步驟 4 規劃)
與 warrenchen 確認:FAA 端 middleware 改寫由 warrenchen 處理(v2.0 撤回此項;FAA repo 不需改動)- 與 MC team 驗證 stage 端 4242ba63 service client 4 個 scope 都可用(v2.0 新增;step 6 stage redeploy 前必驗):
files:upload.write✅ 對應 FAAPUT /files/...files:metadata.read✅ 對應 FAAGET /files/metadata/...+HEAD /files/...files:delete✅ 對應 FAADELETE /files/...files:download.delegate✅ 用來向 MCPOST /file-access/download-tokens換 delegated download token,再打 FAAGET /files/{key}
- 與 ADR-014 對齊(v2.0 修訂):本 ADR v2.0 起對 ADR-014 的 supersede 範圍縮限至「§5 中 converter 部分 service token /
converter:job.write/readscope」;ADR-014 §2 / §5 中 FAA 部分 / §6 / §7 中 FAA 與 MC delegated token row 全部恢復有效 - 與 ADR-013 對齊:本 ADR 不影響 user login 的 public PKCE client
- DevOps rotate runbook 待產出(Phase 0.9 follow-up;範圍縮限至 converter API key)
- 已洩漏的 stage service client secret
RciRUyi...自動作廢(不需 MC rotate;v2.0 由新 client4242ba63...取代)
配套產出(給後續 Phase)
Phase 0.8b 範圍內(v2.0 修訂)
- visionA backend 程式碼改造(backend agent 任務):
- converter_client.go 維持 v1.0 改造(API key)
- faa_client.go 改回呼叫 MCTokenClient.ServiceToken() + 為 download 路徑增 DownloadWithDelegated 變體
- mc_token_client.go 部分復活(service token cache + delegated download token issue 邏輯)
- flow.go download 路徑改回呼叫 IssueDelegatedDownload + DownloadWithDelegated
- config.go 重新啟用 OIDCConfig.ServiceClientID/Secret + ConversionConfig.TenantID
- converter middleware 改造(jimchen 跨 repo) — 維持 v1.0 規劃
FAA middleware 改造(warrenchen 跨 repo)— v2.0 撤回.env.stage.example更新(v2.0 修訂):- 維持
VISIONA_CONVERTER_API_KEY新增 - 撤回
VISIONA_FAA_API_KEY - 加回
VISIONA_OIDC_SERVICE_CLIENT_ID/VISIONA_OIDC_SERVICE_CLIENT_SECRET/VISIONA_OIDC_TENANT_ID/VISIONA_FAA_BASE_URL - 這部分 .env.example 更新由 backend agent 下次任務(複活 mc_token_client 時)一併處理;本次架構任務範圍只動共享文件*
- 維持
- 設計文件更新(conversion.md / api-conversion.md / oidc-tdd.md — 本 ADR v2.0 同步產出)
Phase 0.9 / Phase 1 follow-up
- converter API key rotate runbook(含「產新 key → 雙方同步 env → restart → 驗證 → 拔舊 key」步驟;範圍縮限至 converter)
- 是否設 converter API key 有效期(例如 1 年到期自動提醒)— 由 SRE 流程決定
- FAA 上「visionA 自己簽 HMAC token」delegated 機制(§7 選項 B),用於 download 路徑回 302 redirect(同 v1.x 規劃)
- 觀察 server-side download proxy 在 Phase 1 量大時的效能 / 頻寬 cost(同 v1.x 規劃)
相關文件
- 部分 supersedes:
adr-014-conversion-integration.md(v2.0 範圍縮限:僅 §5 中 converter 部分 service token / scope;§2 / §5 中 FAA 部分 / §6 / §7 中 FAA + MC delegated token row 全部恢復有效) - 不影響:
adr-013-public-client.md(user login 部分) - 詳細實作(本 ADR v2.0 同步更新):
conversion.mdv0.5、api/api-conversion.mdv0.5、oidc-tdd.mdv0.3 - 觸發背景:
progress.md「Phase 0.8b 啟動原因(2026-05-11)」+ 「Phase 0.8b 步驟 2 — Backend agent 任務範圍盤點」+ 「v2.0 範圍縮限(2026-05-16)」段落 - FAA dual-auth 設計參考:
/Users/jimchen/file_access_agent/src/FileAccessAgent.Api/Program.csline 41-57(JWT Bearer 設定)、line 184-254(download endpoint 不掛 RequireAuthorization、用 IDelegatedDownloadTokenValidator)、line 291-322(EnsureJwtScopeAndTenant + HasScope)
版本記錄
| 日期 | 版本 | 變更 |
|---|---|---|
| 2026-05-11 | 1.0 | 初版 — Phase 0.8b 將 server-to-server 認證從 OAuth client_credentials 改 pre-shared API key(visionA → converter / FAA 兩條線都改) |
| 2026-05-15 | 1.1 | 補 §3.5 Reference Middleware Implementation — Go (converter) + C# (FAA) snippet + 部署檢查清單,給跨 repo 改 middleware 時照抄 |
| 2026-05-16 | 2.0 | 範圍縮限 — 撤回 visionA → FAA API key 部分(FAA 不動、改回 ADR-014 §2 MC service token + delegated download token);visionA → converter API key 路線保留。撤回原因:(1) 使用者明示不希望動 FAA / MC、(2) 5/9 撞 MC scope 沒註冊的痛尚在、但 (3) 使用者今天提供 FAA stage service client (4242ba63099d4f318dd3f143d27ef4c5) 證明 MC 端 4 個 scope 都已備好(含 files:download.delegate)、舊路線可走通。涉及修訂:§2 整段標撤回 + 加 v2.0 設計(FAA dual-auth + delegated token download)、§3.5.2 C# FAA snippet 整段刪除、§5 tenant 概念分 converter/FAA 兩段、§6 mc_token_client 改部分復活 + OIDCConfig.ServiceClientID/Secret + ConversionConfig.TenantID 重新啟用、§7 download 路徑改回 delegated token(保留 server-side proxy 不退回 302)、§9 env 表加回 service client + tenant id + 撤回 FAA API key、新增方案 E 排除 v1.0 兩條都 API key 路線、後果重估、合規性「跟 warrenchen 確認」改為「跟 MC team 驗 4 scope」。對 commit 86b7175 影響:visionA backend faa_client.go 要從 API key 改回 service token + delegated token(待下次 backend agent 處理);mc_token_client.go 要部分復活(保留 ServiceToken cache + IssueDelegatedDownload 邏輯);config.go 加回 ServiceClientID/Secret/TenantID 三欄位;.env.stage.example 加回對應 env、撤回 VISIONA_FAA_API_KEY。本次純文件修訂、source code 改造留給 backend agent 下次任務。 |
| 2026-05-16 | 2.1 | §2 visionA → FAA 整段再次撤回 — 對 MC source 完整驗證後發現:(1) MC source 沒有 POST /file-access/download-tokens endpoint(visionA 無法跟 MC 換 delegated token)、(2) MC source 沒有 FAA IDelegatedDownloadTokenValidator assume 的 introspection endpoint(即使有 token 也無法 validate)、(3) FAA GET /files/{key} 強制只接 delegated token、不接 service token。→ ADR-014 §2 與本 ADR v2.0 §2 描述的「visionA → MC → FAA delegated token 鏈」完全是 fictional(從 2026-05-02 寫定起即為 broken design、未曾 e2e 跑通過)。v2.1 採用 ADR-016(visionA download 改走 converter 新增的 GET /api/v1/jobs/{id}/result + visionA stream 中轉)。涉及修訂:狀態行加 v2.1 撤回說明、上位 ADR 區註明 §2 被 ADR-016 supersede、§2 整段標 v2.1 撤回(v2.0 內容保留歷史)、§9 env 表撤回 v2.0 加回的 VISIONA_OIDC_SERVICE_CLIENT_ID/_SECRET / VISIONA_OIDC_TENANT_ID / VISIONA_FAA_BASE_URL、改寫「v2.1 visionA 端 server-to-server 鏈路收斂為單條」。對 commit 86b7175 影響:visionA backend mc_token_client.go 維持砍除狀態(撤回 v2.0 「部分復活」規劃)、faa_client.go 改名為 converter_result_client.go(或併入 converter_client.go)、config.go 不需加回 ServiceClient* / TenantID / FAABaseURL、.env.stage.example 維持只有 converter env。本次純文件修訂、source code 改造留給 backend agent 下次任務(範圍含「converter 跨 repo 新增 GET result endpoint」)。§1 visionA → converter API key(v1.0 / v2.0 / v2.1 都維持不變)。 |