Files
Leo 8d8b01d240 docs(sdd): credential-primitives-wasm 封存進 archive(T10 完成,卷已結案)
leo 2026-07-21 明令封存:卷已完成,留主目錄會讓未來 session 誤以為進行中。
最後一項 T10(廢除自管加密金鑰)已於 20c7610 完成(移除約 2500 行)。

- git mv 整卷 → system-dev/docs/3-specs/archive/credential-primitives-wasm/
  design.md status: closed;superseded_by 留空(據實:非被另一卷取代,是機制整個換掉)
- 卷首補「封存時仍未完成的項目」——逐條查 code 核實,不當作完成:
  真缺口=auth_mtls 從未實作、7.6 self-hosted auth 鏈端到端從未驗;
  另有勾選過期(auth_oauth2 其實已完成)與驗收條件已作廢(與現行 rule 07 牴觸)者
- 修好 14 處引用(原盤點 10 處,實際更多):session-start-load-sdd.sh 內容嚴重過期
  (把已完成 Phase 寫成進行中)→ 改為以 frontmatter 為判準;四份 rules、CLAUDE.md、
  BACKLOG、3-specs/README、deploy.ts、credentials.ts、wrangler.toml 逐一按性質處理
- 額外:system-dev/docs/2-architecture/ 有四個規則檔重複副本(06-08 遷移遺留)
  → 同步修好,否則留一份過期真相(今日第三次撞到「同一資訊兩份副本」的債)
- 02-forbidden.md §2.1 列的三個「待刪違規 TS」其實早已不存在
  → 改標刪除線+禁止重新引入(過期文件同時製造假待辦與假進行中)

驗證:sdd-active-check exit 0(active 仍恰一份=portal-auth);
session-start-load-sdd.sh exit 0;cypher-executor 與 cli typecheck 全綠;
測試 187/188(唯一 fail 為 pre-existing,stash 覆驗相同)。

未動:pre-write-guard.sh 的 KNOWN_SDDS 白名單不含 archive 路徑
(放寬 guardrail 需明確授權,留待決定)。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-21 01:57:34 +08:00

27 KiB
Raw Permalink Blame History

Implementation Tasks: Credential Primitives TS → WASM

📦 本卷已封存(2026-07-21 移入 3-specs/archive/design.md status: closed。 下方勾選狀態不再維護;封存時的真實盤點見「封存時仍未完成的項目」段。

⚠️ 歷史記錄:文中 credential 加解密相關的完成記錄描述的是舊機制,該機制已於 2026-07-20 完全移除(見 credential-store-migration.md T10)。僅供考古,勿依此操作。


📌 封存時仍未完成的項目(2026-07-21 逐條核實,不當作完成)

核實方法=直接查 code 現況,不採信勾選狀態。分三類:

A. 未勾但實際已達成(勾選過期,非真缺口)

Task 核實證據
1.9 刪除 credential-injector.ts 檔案不存在(ls 確認);T10 於 20c7610 移除
5.2 credential-injector.ts 不存在 同上
5.3 jwt-signer.ts 不存在 檔案不存在
5.4 無 BUILTIN_API_RECIPES / BUILTIN_CREDENTIALS_MAP 全 repo grep 0 筆
5.5 crypto.subtle.decrypt 只在 wasi-shim grep 0 筆(該 host fn 已成 stub,連 decrypt 都沒了)
5.6 crypto.subtle.sign 只在 wasi-shim grep 僅 wasi-shim.ts:641 一處
4.1 / 4.2 auth_oauth2 registry/components/auth_oauth2/{main.go, .wasm, contract} 齊備,且 auth-dispatcher.ts:121 SUPPORTED_PRIMITIVES 已含 'oauth2'

B. 未勾且確實未完成(真缺口,日後要做需另立新 SDD

Task 現況
4.3 / 4.4 auth_mtls registry/components/auth_mtls/ 不存在,從未實作。mTLS 認證目前不支援
1.7 / 1.8 static_key 端到端測試(openai / twilio 無記錄。機制本身已由 Phase 8.5 的 Notion 端到端實證涵蓋,但這兩條指定服務未各別驗過
2.6 service_account 端到端(google_sheets_sa 無記錄;與 7.6 同一條驗收鏈
3.5 / 3.6 gmail / http_request 端到端 無記錄
7.6 self-hosted auth 鏈端到端驗收 未完成。這是 Phase 7 唯一的客觀驗收項(global_fetch_strictly_public flag 是否真解 same-zone 1042,在自架帳號上從未實測)
5.1 20 個 auth recipe seed 全數可用 未逐條驗證

C. 已作廢的驗收條件(criteria 本身過期,不算缺口)

Task 為何作廢
5.7 全域 interpolate 回 0 筆 現況 5 個 TS 檔仍有 interpolategraph-executor / recipe-expander / component-loader / routes/recipes.ts / auth-recipe-seeds)。但這是 recipe 層的 URL/參數展開,非 credential template 展開;當初訂此條時未區分兩者。credential 的 {{secret.X}} 展開確實已全在 WASM
5.8 TS 中 {{secret. 回 0 筆 現況 auth-recipe-seeds.ts 有 10+ 筆。但依 rule 07「*-seeds.ts 是資料宣告非呼叫實作」已明文豁免,此條與現行規則牴觸,以 rule 07 為準
5.9 起「封測門檻」整段 封測狀態早已不是本 repo 的活躍議題(現行 active SDD=portal-auth

結論:主線目標(清除 cypher-executor 的 credential/auth TS 業務邏輯)已達成,故收卷。 真正的殘留缺口是 auth_mtls 從未實作一批端到端驗收未跑(尤以 7.6 self-hosted auth 鏈為最)。這些不隨本卷封存而視為完成;要做請另立新 SDD 並依 SDD-LIFECYCLE.md 走。

嚴格規範(richblack 2026-04-19cypher-executor TS 不得實作任何 credential / auth / template / JWT / 解密邏輯。全部走 TinyGo WASM + host functions(方案 B)。

封測狀態:推遲(richblack 2026-04-19 決定)。先完成 Phase 1-3 清除違規 TS,再啟動封測。


Phase 0:核心合併(u6u-core → arcrun

  • 0.1 把 u6u-core/builtins/ 搬到 arcrun/builtins/
  • 0.2 確認 arcrun/registry/components/ 21 個零件的 contract.yaml 完整(21/21
  • 0.3 刪除 arcrun/credentials/ 整個目錄(重複,credential route 已在 cypher-executor
  • 0.4 更新 arcrun/cypher-executor/wrangler.toml:確認 CREDENTIALS_KV binding 存在
  • 0.5 刪除 matrix/u6u-core/ 整個目錄(2026-04-19 完成,只剩 credentials/ 已被 cypher-executor 取代)
  • 0.6 在 cypher-executor/src/lib/wasi-shim.ts 新增 host functions
    • u6u.kv_get(keyPtr, keyLen, outPtr, outLenPtr) uint32 — 依 key 前綴路由到 CREDENTIALS_KV / RECIPES,越權檢查 api_key
    • u6u.crypto_decrypt(encPtr, encLen, ivPtr, ivLen, outPtr, outLenPtr) uint32 — 用 env.ENCRYPTION_KEY + crypto.subtle AES-GCM 解密;key 不暴露給 WASM
    • u6u.crypto_sign_rs256(dataPtr, dataLen, pkcs8Ptr, pkcs8Len, outPtr, outLenPtr) uint32crypto.subtle.sign('RSASSA-PKCS1-v1_5' + SHA-256)
    • 2026-04-19 完成:wasi-shim.ts 新增 createArcrunHostFunctions(env, apiKey) factory,集中 AES-GCM 解密 + RSA sign + KV 前綴路由越權檢查。WASI imports 的 u6u namespace wiring 本來就已接好(只是當時沒有實作 factory)。typecheck 通過。
  • 0.7 在 cypher-executor/src/lib/component-loader.ts 新增 WASM runner 路徑:
    • 所有 WASM 零件(含 auth primitive、API 零件、未來用戶自製)一律走 HTTP URL({canonical-id-kebab}.arcrun.dev)到獨立 Worker
    • R2 動態注入路徑作廢richblack 2026-04-19 確認:CF workerd 不支援以 R2 物件臨時 instantiate WASM;用戶自製零件同樣走「產生獨立 Worker」流程,不走 R2)
    • cypher-executor 本身不做 WASM instantiate,也不直接呼叫 createArcrunHostFunctions;那個 factory 是零件 Worker 側.component-builds/{name}/src/index.ts)用的,在 Phase 1 建立 auth_static_key Worker 時接上
    • 2026-04-19 完成:component-loader.ts 新增 WASM_HTTP_RUNNER_IDS10 個 canonical_id6 個 API 零件 + 4 個 auth primitive)+ wasmWorkerUrl() URL 慣例輔助函數;解析鏈新增為第 8 層(放在 BUILTIN_API_RECIPES fallback 之後,避免 Phase 3 尚未完成時 API 零件 Worker 未部署造成 404Phase 3 刪除 BUILTIN_API_RECIPES 後,API 零件會自然落到此層)。auth primitive 從此層進入。tsc --noEmit 通過。

Phase 1auth_static_key WASM(優先,涵蓋 80% 服務)

方案 BWASM 自行讀 KV + 解密,TS 不碰 plaintext。

  • 1.1 建立 arcrun/registry/components/auth_static_key/ 目錄
  • 1.2 寫 component.contract.yamlinput: {action, api_key, service, request} → output: {success, auth_headers, auth_query, auth_body, runtime}
  • 1.3 實作 main.goTinyGo):
    • 宣告 host importskv_get / crypto_decryptstatic_key 不需要 http_request
    • 從 stdin 讀 {action, api_key, service}
    • kv_get("auth_recipe:" + service) → recipe JSON → 驗證 primitive == "static_key"
    • 對每個 non-optional recipe.required_secretskv_get("{api_key}:cred:{name}"){encrypted, iv}crypto_decrypt → plaintext
    • 展開 {{secret.X}} / {{runtime.X}} 模板於 inject.header/query/body;未知 key 展空字串(與 TS parity);其他 namespace 的 {{...}} 原樣保留
    • 輸出 stdout JSON {success, auth_headers, auth_query, auth_body, runtime}
  • 1.4 tinygo build -o auth_static_key.wasm -target=wasi main.go — 2026-04-19 編譯通過(1.1MB,在 contract 限制 2MB 內)
  • [🔄] 1.5 建立 .component-builds/auth_static_key/(用 component-worker-template)並部署到 auth-static-key.arcrun.dev
    • 2026-04-20 完成建置部分:.component-builds/auth_static_key/{wrangler.toml, package.json, tsconfig.json, src/index.ts, component.wasm} 全數到位
    • 方案 A:src/index.ts 直接 import ../../../cypher-executor/src/lib/wasi-shimcreateWasiShim + createArcrunHostFunctions(以 ArcrunHostEnv 結構型別相容);AES 解密邏輯仍只存在於 wasi-shim.ts 一處(rule 02 §2.2)
    • 綁同組 KV:CREDENTIALS_KV (e7f4320f88d343f187e35e3543dd74c9) / RECIPES (9cf9db905c6241f78503199e58b2ffe0);ENCRYPTION_KEY 走 wrangler secret put
    • wrangler deploy --dry-run 通過(1192 KiB, 419 KiB gzip);實際 wrangler deploy + secret put ENCRYPTION_KEY 留給 richblack 執行
  • 1.6 建立 auth-dispatcher.ts(取代 credential-injector.ts):查 auth recipe → HTTP POST 到對應 auth primitive URL → 合併 _auth_headers 進 ctx
    • 2026-04-20 完成:cypher-executor/src/actions/auth-dispatcher.ts 新建,export tryAuthDispatch(componentId, input, env, apiKey)
    • 流程:查 resolveAuthRecipe → primitive 在 SUPPORTED_PRIMITIVES(目前只有 static_key)→ fetch wasmWorkerUrl('auth_static_key') → 合併 _auth_headers/_auth_query/_auth_body
    • 自引用防護:AUTH_PRIMITIVE_IDS set 排除 4 個 auth_* componentId
    • wasmWorkerUrlcomponent-loader.ts export 出來共用
    • graph-executor.ts 改為:先試 tryAuthDispatch(新路徑),沒命中 fallback 到舊 injectCredentials(Phase 1.9 刪)
    • 檢查過 auth-dispatcher.ts 無 crypto.subtle / interpolate / {{secret. / hard-code API URL,符合 rule 02 §2.2
    • tsc --noEmit 通過
  • 1.7 端對端測試:openai recipe → 成功注入 Authorization: Bearer <openai_key>
  • 1.8 端對端測試:twilio recipeBasic Auth)→ 成功注入
  • 1.9 刪除 credential-injector.ts 整檔decryptCredential / decryptSecrets / interpolateTemplate / BUILTIN_CREDENTIALS_MAP 全刪)

Phase 2auth_service_account WASM

  • [🔄] 2.1 建立 arcrun/registry/components/auth_service_account/ 目錄
  • [🔄] 2.2 寫 component.contract.yaml
  • [🔄] 2.3 實作 main.go
    • 從 stdin 讀 {api_key, service} + kv_get 拿 recipe + 解密 SA JSON
    • 解析 SA JSON 取 client_email / private_keyPEM
    • PEM → PKCS8 bytes(純 Gobase64 decode + 去 header/footer
    • 組 JWT header + payloadbase64url),呼叫 crypto_sign_rs256(signingInput, pkcs8) 拿 signature
    • 組完整 JWT → http_request POST token_uri → 拿 access_token
    • 展開 {{runtime.access_token}} 模板
  • 2.4 tinygo build -o auth_service_account.wasm -target=wasi main.go — 2026-04-20 編譯通過(1.1MB,在 contract 限制 2MB 內)
  • 2.5 建立 .component-builds/auth_service_account/ 並部署到 auth-service-account.arcrun.dev
    • 2026-04-20 完成建置部分:.component-builds/auth_service_account/{wrangler.toml, package.json, tsconfig.json, src/index.ts, component.wasm} 全數到位
    • 方案 A:src/index.ts 重用 createArcrunHostFunctions 提供 kv_get/crypto_decrypt/crypto_sign_rs256,額外加 http_request host function(token exchange 用,非 crypto 不受 §2.2 約束)。http_request 直接回 response body 原文(WASM 端 json.Unmarshal 找 access_token)
    • 綁同組 KV:CREDENTIALS_KV / RECIPES;ENCRYPTION_KEY 走 wrangler secret put
    • wrangler deploy --dry-run 通過(1248 KiB, 440 KiB gzip);實際 wrangler deploy + secret put ENCRYPTION_KEY 留給 richblack 執行
    • auth-dispatcher.tsSUPPORTED_PRIMITIVES 加入 'service_account',workflow 用 google SA recipe 會自動走新 WASM 路徑
  • 2.6 端對端測試:google_sheets_sa recipe → 成功取得 access_token → 注入 header
  • 2.7 刪除 lib/jwt-signer.ts 整檔 — 2026-04-20 完成
    • cypher-executor/src/lib/jwt-signer.ts 已刪除(RS256 JWT 邏輯移入 auth_service_account.wasm)
    • credential-injector.ts 原 line 23 import { exchangeGoogleJwt } 移除
    • credential-injector.ts 原 line 140-150 service_account 分支改為 throw(任何 service_account recipe 已被 auth-dispatcher 攔截;這條 TS fallback 若被觸發即表架構錯亂,直接爆錯比沈默解密更安全)
    • cypher-executor tsc --noEmit 通過

Phase 3:清理 component-loader 的 TS 實作(全刪)

目標:BUILTIN_API_RECIPES 整段刪除,所有服務走 WASM runnerHTTP URL 路徑)。

  • 3.1 確認 http_request.wasm / gmail.wasm / telegram.wasm / line_notify.wasm / google_sheets.wasm 都在 registry/components/ 且可執行 — 2026-04-20 驗證 6 個(含 cron)全數存在,main.go + .wasm 齊備
  • 3.2 確認上述零件 Worker 都已部署({name}.arcrun.dev 可用) — 2026-04-20 完成建置部分
    • 6 個 Worker 建置到位:.component-builds/{http_request, gmail, telegram, line_notify, google_sheets, cron}/{wrangler.toml, package.json, tsconfig.json, src/index.ts, component.wasm}
    • 方案 A:5 個需 http_request 的零件(http_request/gmail/telegram/line_notify/google_sheets)src/index.ts 共用模板;cron 是純計算不註冊 host function
    • 全部透過 createWasiShim 複用 cypher-executor/src/lib/wasi-shim.ts(rule 02 §2.2 邊界)
    • 6 個 wrangler deploy --dry-run 全通過(~1.17 MB / ~413 KB gzip 每個);實際 wrangler deploy 留給 richblack 執行
  • 3.3 component-loader.ts 的內建路徑改為查對應 Worker URL → HTTP POST — 2026-04-20 完成
    • 原本第 7 層是 BUILTIN_API_RECIPES fallback、第 8 層是 WASM_HTTP_RUNNER_IDS (HTTP URL);兩層合併為第 7 層 WASM_HTTP_RUNNER_IDS 直接走 makeHttpRunner(wasmWorkerUrl(id))
    • 解析鏈新編號 1-8,順序不變(外部 URL → recipe hash → component hash → R2 → Service Binding → auth recipe runner → WASM HTTP runner → 找不到)
  • 3.4 刪除 BUILTIN_API_RECIPES 整個 Recordhttp_request / gmail / telegram / line_notify / google_sheets / cron 的 TS 實作全刪) — 2026-04-20 完成
    • cypher-executor/src/lib/component-loader.ts 原 line 253-326 BUILTIN_API_RECIPES 常數 + fallback lookup 全刪(約 80 行)
    • 全域搜尋確認:gmail.googleapis.com/...messages/send / api.telegram.org/bot.*sendMessage / sheets.googleapis.com/v4/spreadsheets / notify-api.line.me/api/notify 在 cypher-executor TS 中已不存在(auth-recipe-seeds.ts 的 base_url 是 recipe 資料欄位,不是 hard-coded API call)
    • cypher-executor tsc --noEmit 通過
  • 3.5 端對端測試:workflow 用 gmail auth recipe + gmail.wasm Worker → 成功發信
  • 3.6 端對端測試:workflow 用 http_request.wasm Worker + auth_static_key 注入 → 成功呼叫任意 API

Phase 4auth_oauth2 + auth_mtls WASM(封測後)

  • 4.1 建立 arcrun/registry/components/auth_oauth2/
  • 4.2 實作:needs_refresh / refresh / authenticate 三個 action
  • 4.3 建立 arcrun/registry/components/auth_mtls/
  • 4.4 實作:輸出 TLS cert/key(實際 mTLS handshake 由 Worker runtime 執行,WASM 無法做 socket

Phase 5:封測啟動門檻 — 核心穩定驗證

全部通過才能啟動封測

  • 5.1 所有 20 個 auth recipe seed 可正常運作(static_key 17 個 + service_account 3 個)
  • 5.2 cypher-executor/src/actions/credential-injector.ts 不存在
  • 5.3 cypher-executor/src/lib/jwt-signer.ts 不存在
  • 5.4 cypher-executor/src/lib/component-loader.tsBUILTIN_API_RECIPES / BUILTIN_CREDENTIALS_MAP
  • 5.5 cypher-executor/src/ 全域搜尋 crypto.subtle.decrypt 只出現在 wasi-shim.tscrypto_decrypt host function
  • 5.6 cypher-executor/src/ 全域搜尋 crypto.subtle.sign 只出現在 wasi-shim.tscrypto_sign_rs256 host function
  • 5.7 cypher-executor/src/ 全域搜尋 interpolate 回傳 0 筆(template 展開全在 WASM
  • 5.8 全域搜尋 {{secret\. / {{runtime\. 在 TS 檔案中回傳 0 筆

Phase 6:通用 CI/CD deploy workflow

背景(2026-04-20 richblack 決定):現 .github/workflows/deploy.yml 只部署 cypher-executor + registry + 已刪除的 credentials,漏掉 Phase 1-3 產出的 8 個 Worker,且硬編碼每個 job 導致未來新增 Worker 都要改 CI。改為通用掃描式 workflow:任何含 wrangler.toml 的目錄 = 部署單位,改到該目錄下任何檔案 = 觸發重新 deploy。

關鍵決策:

  • 零件 .wasm 由 CI build(不 commit):registry/components/{name}/main.go 改動時才重 build,用 timestamp / content hash 判斷
  • .component-builds/{name}/component.wasm 由 CI 從 registry/components/{name}/{name}.wasm 複製產生(deploy 前一步)
  • 統一用 pnpm(.component-builds/* 本來就是;順勢把 cypher-executor 的 package-lock.json 砍了)
  • runtime secret(ENCRYPTION_KEY)不進 CI,由 richblack 一次性 wrangler secret put
  • registry Worker 的 wrangler.toml 現階段不改(職責是合約管理,與封測無關;sandboxAcceptance.ts 的 rule 02 §2.2 審查留到 Phase 5 用戶自製零件啟動時)

Tasks

  • 6.1 改寫 .github/workflows/deploy.yml:動態掃描所有含 wrangler.toml 的目錄(排除 node_modules/ + Pages 專案),用 matrix job fanout 部署;分兩層(tier1=.component-builds/*,tier2=其他),tier1 全綠後才 tier2(避免 service binding target 未存在)
  • 6.2 加上 TinyGo build 步驟:tier1 matrix 一律 setup-tinygo + 從 registry/components/{name}/main.go rebuild .wasm → copy 到 .component-builds/{name}/component.wasm
  • 6.3 diff-aware:push 到 main 時比對 github.event.before..github.sha,只 deploy 有 diff 的 Worker(含 registry/components/{name}/ 連動 .component-builds/{name}/);workflow_dispatch 提供 force_all + only 選項
  • 6.4 統一 pnpm:刪除 cypher-executor/package-lock.json + registry/package-lock.json;workflow 優先 pnpm install --frozen-lockfile,若該目錄無 pnpm-lock.yaml 則 fallback 到 --no-frozen-lockfile(混合期容錯)
  • 6.5 加 max-parallel: 5 控制 Workers API rate limit(tier1 和 tier2 各自)
  • 6.6 驗證:workflow_dispatch + force_all=true 手動跑一次,24 個 Worker 全綠 — 2026-04-20 完成
    • 最終綠色 run 24668903627(28/28 jobs,含 discover + summary):tier1 24 個零件 Worker + tier2 2 個 orchestration Worker(cypher-executor / registry)全 success
    • 過程中修兩輪:先修 setup-nodecache: 'pnpm' 對 legacy package-lock.json 目錄失效(改為不用 cache);再修 tier2 三個 package.json(cypher-executor/registry/builtins)遺漏 wrangler devDependency + regen pnpm-lock.yaml
    • ENCRYPTION_KEY secret 已由 richblack 授權、CC 從 .env pipe 到三個 Worker:arcrun-auth-static-keyarcrun-auth-service-accountarcrun-cypher-executor(不顯示內容)
  • 6.7 文件:在 .claude/rules/ 加一份 05-deploy-convention.md(「新增 Worker = 新目錄 + wrangler.toml,不用改 CI」)

Phase 7auth primitive 改走 service binding(解 self-hosted CF 1042

2026-06-06 richblack 拍板。來源:壓測報告階段 11(test_arcrun/docs/壓測報告.md §11)。

背景(架構演化)

  • 當初設想「用戶對每個服務建零件」→ 零件用 service binding 要 redeploy → 用戶建 workflow 要 redeploy(不可接受)→ 故禁 service binding、改 cypher bindingHTTP URL)。
  • 後來架構演化:用戶要的是 recipe(資料,存 KV+ 固定 primitive,不是新零件。acr recipe push 進 KV 不 deploy。用戶用「http_request primitive + 不同 recipe」打各種服務,永不新增 primitive。
  • 結論:「用戶建 workflow 要 deploy」這個禁令前提在 recipe 模型下不成立。primitivehttp_request + 4 個 auth)是平台固定基礎設施,和 13 個邏輯零件同類,給它 service binding 的 deploy 成本只在平台部署時付一次,不落用戶身上

根因(壓測階段 11,唯讀證據)

  • auth-dispatcher.ts:67-68fetch(wasmWorkerUrl('auth_service_account', WORKER_SUBDOMAIN)) 打 workers.dev → CF 回 404 + "error code: 1042"CF 邊緣頁,非 auth worker 回應)。
  • 1042 精確定義(官方 docs2026-06-06 查證):「Worker tried to fetch from another Worker on the same zone, only supported when global_fetch_strictly_public flag is used」。關鍵是 same zone
    • self-hosted 踩cypherarcrun-cypher-executor.{sub}.workers.dev)與 auth workerarcrun-auth-*.{sub}.workers.dev同在 {sub}.workers.dev zone → 同 zone fetch → 1042。
    • 官方不踩:官方 cypher 在自訂域 cypher.arcrun.dev,打 auth 的 *.uncle6-me.workers.dev跨 zone,不觸發 same-zone 限制(非「官方有 flag」——官方 wrangler.toml 只有 nodejs_compat,無 global_fetch_strictly_public)。
    • 這是 P0#9「同 zone 522」的另一形態:之前解的是 *.arcrun.dev 同 zone,沒解到 *.workers.dev 同 zone。service binding 是內部 RPC 不經 zone → 徹底免疫(13 邏輯零件正是走 binding 才不踩)。
  • 直接從外部打 arcrun-auth-service-account.leo21c.workers.dev → 200 + 真 ya29... token → JWT/解密/token 換取鏈本身全正常,唯一卡點是 cypher 打不到自己帳號的 auth worker。
  • service binding 是 CF 內部 RPC,不經公網 → 同時繞開同 zone 522P0#9同帳號 workers.dev 子請求 1042,比 workers.dev fetch 乾淨(13 邏輯零件正是用 binding 才不踩)。

技術前提(已驗證

  • CF API 查 leo21c(全新自架帳號)的 arcrun-cypher-executor settings → 實綁 13 個 service bindingSVC_SET→arcrun-set …)。證明 deploy.ts 的 binding 注入在 self-hosted 生效,auth binding 走同路綁得上。

修法演進:Aservice binding)→ 廢 → Bglobal_fetch_strictly_public flag

先做了 Aservice binding)後評估廢棄,改用 Bflag)——richblack 2026-06-06 拍板。

  • Aservice binding)為何廢service binding 靜態,加/改要重 deploy cypher(官方 docs 證實)。richblack 判定不夠乾淨,且有更簡單的 B。
  • Bglobal_fetch_strictly_public flag)為何對:官方 docs——此 flag 讓 fetch() 走公網「前門」,same-zone fetch 也能通。cypher wrangler.toml 加一行即解,用戶無感、不用域名、不用重 deploy、不動 service binding 禁令
  • B 安全(查證官方 docs:唯一副作用是「Worker fetch 自己 hostname 會 self-loop」;cypher 只打外部 API + sibling auth worker(皆非自己 hostname)→ 不 self-loop。
  • B 官方/self-host 共用:官方 cypher 本就跨 zone,加 flag 行為不變;self-host 同 zone 被修好。同一份 toml 兩邊通用。
  • 評估但廢的他案arcrun.dev 子域給 self-host cypher——查證後子域同 zonezone=註冊域名)照踩 1042,且跨帳號 route 落地等於 PaaS 轉向,廢。

Tasks

  • 7.1 規範:02-forbidden §3.1 / 03 / CLAUDE.md 鐵律4 / pre-bash-guard 3.1——禁令維持原狀(不解禁 service binding,僅加註「same-zone 1042 用 flag 解」。(先前一度改成「平台 primitive 例外允許 binding」,因改用 B 已全部還原
  • 7.2 hook 解禁還原pre-bash-guard.sh 規則 3.1 回原本「禁止新增 service binding」(B 不需解禁)
  • 7.3 cypher-executor/wrangler.tomlcompatibility_flagsglobal_fetch_strictly_public+ 註解病因/安全)。移除先前加的 SVC_AUTH_*grep SVC_AUTH=0
  • 7.4 auth-dispatcher.ts / types.ts還原到 A 之前(移除 binding 優先邏輯與 SVC_AUTH_* 型別,回單純 fetch(workers.dev)——flag 讓它同 zone 也通);tsc exit 0
  • 7.5 cli/src/lib/deploy.ts無需改——flag 在 cypher wrangler.tomlstripOfficialOnlyBindings() 不碰 compatibility_flagsself-hosted 部署自動帶上
  • 7.6 驗收(客觀證據):全新自架帳號 POST /webhooks/named/{ns}/{wf}/triggerappend_row 的 auth 注入回 200、真的寫進 Google Sheets(第一個端到端驗 self-hosted auth 鏈的測試);tsc --noEmit exit 0
  • 7.7 design.md 同步(§修改 auth-dispatcher 補「binding 優先、fetch fallback」+ 架構演化 + 1042 根因):§修改 的 auth-dispatcher 補「binding 優先、fetch fallback」;記架構演化(recipe 取代零件 → primitive 可 binding

Phase 8{{credential.X}} 用戶面注入語法(2026-06-10 richblack 確認 changedesign §8

根因:壓測 401 = workflow 寫 {{credential.X}} 但三條 template 展開路徑都不認此 namespace。 修法:auth_static_key 加 resolve_credentials actionWASM 解密),graph-executor 偵測+回填(不解密)。

  • 8.1 registry/components/auth_static_key/main.goinput 加 Names []stringmain 在 service 必填檢查之前分流 action == "resolve_credentials" → 走新 handleResolveCredentials(不查 recipe、 不要求 service),每個 name kvGet("{api_key}:cred:{name}") + cryptoDecrypt → 回 {success, credentials:{name: plaintext}};缺 name → success:false + error 指明(不假綠)
  • 8.2 tinygo build -target=wasi -o auth_static_key.wasm main.go 編譯通過(1.1MB+ copy 到 .component-builds/auth_static_key/component.wasm
  • 8.3 cypher-executor/src/graph-executor.tsnode.data interpolate 後、node 執行前,掃描殘留 {{credential.NAME}} → 有則 POST wasmWorkerUrl('auth_static_key') {action:"resolve_credentials", api_key, names} → 回填 node.data。不解密、不碰 ENCRYPTION_KEYrule 02 §2.2 實作放 auth-dispatcher.tsresolveCredentialRefs(遞迴掃描+回填,無 {{credential.}} 則零開銷不打 WASM),graph-executor 只呼叫
  • 8.4 tsc --noEmit exit 0 + grep 確認 graph-executor / auth-dispatcher 無 crypto.subtle / {{secret. / ENCRYPTION_KEY 業務邏輯
  • 8.5 端對端驗收:機制已實證打通(2026-06-13 leo21c 壓測)。實際走 Notion 而非 OpenAI 路徑—— node.data {{credential.notion_token}} → 注入解密後 token → 真讀到 Notion Recipes 資料 (「蕃茄蘑菇燉雞」+ iCook 連結),同時 401 假綠根治全鏈驗證(host fn error envelope → 零件 parsed["error"] → cypher isFailure()401 回 success:false)。客觀證據:真服務 2xx + 真 body。 8.5 原訂 OpenAI 為驗收服務,實際以 Notion 達成同等證據;機制與服務無關,故視為驗收完成)
  • 8.6 design.md / status.md / BACKLOG.md 同步(§8 設計 + 8.1-8.4 done 標記)

Notes

  • 方案 B 是唯一方案(方案 A 已廢棄,違反 CLAUDE.md §禁止行為)
  • Phase 8 的 resolve_credentials 是用戶面入口,與 authenticate(recipe 自動注入)並存不重疊
  • Phase 0.6host functions+ 0.7WASM runner)是 Phase 1-3 的硬前置,必須先做
  • 若 TinyGo encoding/base64 可用就直接用;若不可用則自行實作(見 gmail/main.go 的 base64URLEncode
  • auth_mtls 的 TLS handshake 無法在 WASM 內做(WASI preview1 沒 socket),只能輸出 cert/key 讓 Worker 在 fetch 時用
  • 每個 auth primitive WASM 都是獨立部署的 Worker(透過 component-worker-template/),不是從 R2 動態載入
  • Cypher binding = workflow YAML 裡的 URL 清單,不是 Cloudflare service binding