# 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 檔仍有 `interpolate`(`graph-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-19)**:cypher-executor TS 不得實作任何 credential / auth / template / JWT / 解密邏輯。全部走 TinyGo WASM + host functions(方案 B)。 **封測狀態**:推遲(richblack 2026-04-19 決定)。先完成 Phase 1-3 清除違規 TS,再啟動封測。 --- ## Phase 0:核心合併(u6u-core → arcrun) - [x] 0.1 把 `u6u-core/builtins/` 搬到 `arcrun/builtins/` - [x] 0.2 確認 `arcrun/registry/components/` 21 個零件的 contract.yaml 完整(21/21) - [x] 0.3 刪除 `arcrun/credentials/` 整個目錄(重複,credential route 已在 cypher-executor) - [x] 0.4 更新 `arcrun/cypher-executor/wrangler.toml`:確認 CREDENTIALS_KV binding 存在 - [x] 0.5 刪除 `matrix/u6u-core/` 整個目錄(2026-04-19 完成,只剩 credentials/ 已被 cypher-executor 取代) - [x] 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) uint32` — `crypto.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 通過。 - [x] 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_IDS`(10 個 canonical_id,6 個 API 零件 + 4 個 auth primitive)+ `wasmWorkerUrl()` URL 慣例輔助函數;解析鏈新增為第 8 層(放在 `BUILTIN_API_RECIPES` fallback 之後,避免 Phase 3 尚未完成時 API 零件 Worker 未部署造成 404;Phase 3 刪除 `BUILTIN_API_RECIPES` 後,API 零件會自然落到此層)。auth primitive 從此層進入。`tsc --noEmit` 通過。 --- ## Phase 1:auth_static_key WASM(優先,涵蓋 80% 服務) 方案 B:WASM 自行讀 KV + 解密,TS 不碰 plaintext。 - [x] 1.1 建立 `arcrun/registry/components/auth_static_key/` 目錄 - [x] 1.2 寫 `component.contract.yaml`(input: `{action, api_key, service, request}` → output: `{success, auth_headers, auth_query, auth_body, runtime}`) - [x] 1.3 實作 `main.go`(TinyGo): - 宣告 host imports:`kv_get` / `crypto_decrypt`(static_key 不需要 http_request) - 從 stdin 讀 `{action, api_key, service}` - `kv_get("auth_recipe:" + service)` → recipe JSON → 驗證 `primitive == "static_key"` - 對每個 non-optional `recipe.required_secrets`:`kv_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}` - [x] 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-shim` 的 `createWasiShim` + `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 執行 - [x] 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 - `wasmWorkerUrl` 從 `component-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 ` - [ ] 1.8 端對端測試:twilio recipe(Basic Auth)→ 成功注入 - [ ] 1.9 **刪除 `credential-injector.ts` 整檔**(`decryptCredential` / `decryptSecrets` / `interpolateTemplate` / `BUILTIN_CREDENTIALS_MAP` 全刪) --- ## Phase 2:auth_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_key`(PEM) - PEM → PKCS8 bytes(純 Go,base64 decode + 去 header/footer) - 組 JWT header + payload(base64url),呼叫 `crypto_sign_rs256(signingInput, pkcs8)` 拿 signature - 組完整 JWT → `http_request` POST `token_uri` → 拿 `access_token` - 展開 `{{runtime.access_token}}` 模板 - [x] 2.4 `tinygo build -o auth_service_account.wasm -target=wasi main.go` — 2026-04-20 編譯通過(1.1MB,在 contract 限制 2MB 內) - [x] 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.ts` 的 `SUPPORTED_PRIMITIVES` 加入 `'service_account'`,workflow 用 google SA recipe 會自動走新 WASM 路徑 - [ ] 2.6 端對端測試:google_sheets_sa recipe → 成功取得 access_token → 注入 header - [x] 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 runner(HTTP URL 路徑)。 - [x] 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 齊備 - [x] 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 執行 - [x] 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 → 找不到) - [x] 3.4 **刪除 `BUILTIN_API_RECIPES` 整個 Record**(`http_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 4:auth_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.ts` 無 `BUILTIN_API_RECIPES` / `BUILTIN_CREDENTIALS_MAP` - [ ] 5.5 `cypher-executor/src/` 全域搜尋 `crypto.subtle.decrypt` 只出現在 `wasi-shim.ts` 的 `crypto_decrypt` host function - [ ] 5.6 `cypher-executor/src/` 全域搜尋 `crypto.subtle.sign` 只出現在 `wasi-shim.ts` 的 `crypto_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 - [x] 6.1 改寫 `.github/workflows/deploy.yml`:動態掃描所有含 `wrangler.toml` 的目錄(排除 `node_modules/` + Pages 專案),用 matrix job fanout 部署;分兩層(tier1=`.component-builds/*`,tier2=其他),tier1 全綠後才 tier2(避免 service binding target 未存在) - [x] 6.2 加上 TinyGo build 步驟:tier1 matrix 一律 setup-tinygo + 從 `registry/components/{name}/main.go` rebuild `.wasm` → copy 到 `.component-builds/{name}/component.wasm` - [x] 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` 選項 - [x] 6.4 統一 pnpm:刪除 `cypher-executor/package-lock.json` + `registry/package-lock.json`;workflow 優先 `pnpm install --frozen-lockfile`,若該目錄無 `pnpm-lock.yaml` 則 fallback 到 `--no-frozen-lockfile`(混合期容錯) - [x] 6.5 加 `max-parallel: 5` 控制 Workers API rate limit(tier1 和 tier2 各自) - [x] 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-node` 的 `cache: '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-key`、`arcrun-auth-service-account`、`arcrun-cypher-executor`(不顯示內容) - [x] 6.7 文件:在 `.claude/rules/` 加一份 `05-deploy-convention.md`(「新增 Worker = 新目錄 + wrangler.toml,不用改 CI」) --- ## Phase 7:auth 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 binding(HTTP URL)。 - 後來架構演化:用戶要的是 **recipe(資料,存 KV)+ 固定 primitive**,不是新零件。`acr recipe push` 進 KV **不 deploy**。用戶用「http_request primitive + 不同 recipe」打各種服務,永不新增 primitive。 - 結論:「用戶建 workflow 要 deploy」這個禁令前提**在 recipe 模型下不成立**。primitive(http_request + 4 個 auth)是**平台固定基礎設施**,和 13 個邏輯零件同類,給它 service binding 的 deploy 成本只在平台部署時付一次,**不落用戶身上**。 **根因(壓測階段 11,唯讀證據)**: - `auth-dispatcher.ts:67-68` 用 `fetch(wasmWorkerUrl('auth_service_account', WORKER_SUBDOMAIN))` 打 workers.dev → CF 回 **404 + "error code: 1042"**(CF 邊緣頁,非 auth worker 回應)。 - **1042 精確定義(官方 docs,2026-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 踩**:cypher(`arcrun-cypher-executor.{sub}.workers.dev`)與 auth worker(`arcrun-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 522(P0#9)**與**同帳號 workers.dev 子請求 1042**,比 workers.dev fetch 乾淨(13 邏輯零件正是用 binding 才不踩)。 **技術前提(已驗證 ✅)**: - CF API 查 leo21c(全新自架帳號)的 `arcrun-cypher-executor` settings → **實綁 13 個 service binding**(SVC_SET→arcrun-set …)。證明 `deploy.ts` 的 binding 注入在 self-hosted 生效,auth binding 走同路綁得上。 ### 修法演進:A(service binding)→ 廢 → B(global_fetch_strictly_public flag) **先做了 A(service binding)後評估廢棄,改用 B(flag)**——richblack 2026-06-06 拍板。 - **A(service binding)為何廢**:service binding 靜態,加/改要重 deploy cypher(官方 docs 證實)。richblack 判定不夠乾淨,且有更簡單的 B。 - **B(`global_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——查證後子域同 zone(zone=註冊域名)照踩 1042,且跨帳號 route 落地等於 PaaS 轉向,廢。 ### Tasks - [x] 7.1 規範:`02-forbidden §3.1` / `03` / `CLAUDE.md` 鐵律4 / `pre-bash-guard 3.1`——**禁令維持原狀(不解禁 service binding)**,僅加註「same-zone 1042 用 flag 解」。(先前一度改成「平台 primitive 例外允許 binding」,因改用 B 已**全部還原**) - [x] 7.2 ~~hook 解禁~~ → **還原**:`pre-bash-guard.sh` 規則 3.1 回原本「禁止新增 service binding」(B 不需解禁) - [x] 7.3 `cypher-executor/wrangler.toml`:`compatibility_flags` 加 `global_fetch_strictly_public`(+ 註解病因/安全)。**移除先前加的 SVC_AUTH_***(grep SVC_AUTH=0) - [x] 7.4 `auth-dispatcher.ts` / `types.ts`:**還原**到 A 之前(移除 binding 優先邏輯與 SVC_AUTH_* 型別,回單純 `fetch(workers.dev)`——flag 讓它同 zone 也通);tsc exit 0 - [x] 7.5 `cli/src/lib/deploy.ts`:**無需改**——flag 在 cypher wrangler.toml,`stripOfficialOnlyBindings()` 不碰 `compatibility_flags`,self-hosted 部署自動帶上 - [ ] 7.6 驗收(客觀證據):全新自架帳號 `POST /webhooks/named/{ns}/{wf}/trigger` 讓 `append_row` 的 auth 注入回 200、真的寫進 Google Sheets(第一個端到端驗 self-hosted auth 鏈的測試);`tsc --noEmit` exit 0 - [x] 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 確認 change,design §8) > 根因:壓測 401 = workflow 寫 `{{credential.X}}` 但三條 template 展開路徑都不認此 namespace。 > 修法:auth_static_key 加 `resolve_credentials` action(WASM 解密),graph-executor 偵測+回填(不解密)。 - [x] 8.1 `registry/components/auth_static_key/main.go`:input 加 `Names []string`;main 在 `service` 必填檢查**之前**分流 `action == "resolve_credentials"` → 走新 `handleResolveCredentials`(不查 recipe、 不要求 service),每個 name `kvGet("{api_key}:cred:{name}")` + `cryptoDecrypt` → 回 `{success, credentials:{name: plaintext}}`;缺 name → success:false + error 指明(不假綠) - [x] 8.2 `tinygo build -target=wasi -o auth_static_key.wasm main.go` 編譯通過(1.1MB)+ copy 到 `.component-builds/auth_static_key/component.wasm` - [x] 8.3 `cypher-executor/src/graph-executor.ts`:node.data interpolate 後、node 執行前,掃描殘留 `{{credential.NAME}}` → 有則 POST `wasmWorkerUrl('auth_static_key')` `{action:"resolve_credentials", api_key, names}` → 回填 node.data。**不解密、不碰 ENCRYPTION_KEY**(rule 02 §2.2) 實作放 `auth-dispatcher.ts` 的 `resolveCredentialRefs`(遞迴掃描+回填,無 {{credential.}} 則零開銷不打 WASM),graph-executor 只呼叫 - [x] 8.4 `tsc --noEmit` exit 0 + grep 確認 graph-executor / auth-dispatcher 無 crypto.subtle / {{secret. / ENCRYPTION_KEY 業務邏輯 - [x] 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 達成同等證據;機制與服務無關,故視為驗收完成) - [x] 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.6(host functions)+ 0.7(WASM 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