83a01fe028
- auth_static_key WASM: 偵測 Authorization header "Basic <x>:<y>" (含冒號
的 user:pass 原文), 自動 base64 編碼; 無冒號則維持原樣 (向後相容
已 base64 過的值).
這涵蓋 twilio / jira / mailgun 三個 Basic Auth recipe, 用戶 recipe
只需寫 'Basic {{secret.user}}:{{secret.key}}' 直覺語法.
- 新增 3 個 recipe (auth-recipe-seeds.ts):
• gemini — static_key / header x-goog-api-key (單 secret)
• trello — static_key / QUERY key+token (雙 secret, 第一個 query
injection 測試覆蓋)
• mailgun — static_key / HEADER Basic api:<key> (雙 secret Basic Auth)
- hook fix (pre-write-guard.sh): 放行 auth-recipe-seeds.ts 的 {{secret.X}}
字面值. 該檔是 RECIPES KV 的 seed 資料, 不是 TS 展開邏輯;
真正展開仍在 WASM 完成.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
16 KiB
16 KiB
Implementation Tasks: Credential Primitives TS → WASM
嚴格規範(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)
- 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_keyu6u.crypto_decrypt(encPtr, encLen, ivPtr, ivLen, outPtr, outLenPtr) uint32— 用env.ENCRYPTION_KEY+crypto.subtleAES-GCM 解密;key 不暴露給 WASMu6u.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 通過。
- 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_RECIPESfallback 之後,避免 Phase 3 尚未完成時 API 零件 Worker 未部署造成 404;Phase 3 刪除BUILTIN_API_RECIPES後,API 零件會自然落到此層)。auth primitive 從此層進入。tsc --noEmit通過。
- 所有 WASM 零件(含 auth primitive、API 零件、未來用戶自製)一律走 HTTP URL(
Phase 1:auth_static_key WASM(優先,涵蓋 80% 服務)
方案 B:WASM 自行讀 KV + 解密,TS 不碰 plaintext。
- 1.1 建立
arcrun/registry/components/auth_static_key/目錄 - 1.2 寫
component.contract.yaml(input:{action, api_key, service, request}→ output:{success, auth_headers, auth_query, auth_body, runtime}) - 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}
- 宣告 host imports:
- 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 執行
- 2026-04-20 完成建置部分:
- 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新建,exporttryAuthDispatch(componentId, input, env, apiKey) - 流程:查
resolveAuthRecipe→ primitive 在SUPPORTED_PRIMITIVES(目前只有static_key)→ fetchwasmWorkerUrl('auth_static_key')→ 合併_auth_headers/_auth_query/_auth_body - 自引用防護:
AUTH_PRIMITIVE_IDSset 排除 4 個auth_*componentId wasmWorkerUrl從component-loader.tsexport 出來共用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通過
- 2026-04-20 完成:
- 1.7 端對端測試:openai recipe → 成功注入
Authorization: Bearer <openai_key> - 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_requestPOSTtoken_uri→ 拿access_token - 展開
{{runtime.access_token}}模板
- 從 stdin 讀
- 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_requesthost 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 路徑
- 2026-04-20 完成建置部分:
- 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 23import { exchangeGoogleJwt }移除credential-injector.ts原 line 140-150 service_account 分支改為 throw(任何 service_account recipe 已被 auth-dispatcher 攔截;這條 TS fallback 若被觸發即表架構錯亂,直接爆錯比沈默解密更安全)cypher-executortsc --noEmit 通過
Phase 3:清理 component-loader 的 TS 實作(全刪)
目標:BUILTIN_API_RECIPES 整段刪除,所有服務走 WASM runner(HTTP 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 執行
- 6 個 Worker 建置到位:
- 3.3
component-loader.ts的內建路徑改為查對應 Worker URL → HTTP POST — 2026-04-20 完成- 原本第 7 層是
BUILTIN_API_RECIPESfallback、第 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 → 找不到)
- 原本第 7 層是
- 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-326BUILTIN_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-executortsc --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_decrypthost function - 5.6
cypher-executor/src/全域搜尋crypto.subtle.sign只出現在wasi-shim.ts的crypto_sign_rs256host 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.gorebuild.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-node的cache: 'pnpm'對 legacypackage-lock.json目錄失效(改為不用 cache);再修 tier2 三個 package.json(cypher-executor/registry/builtins)遺漏wranglerdevDependency + regen pnpm-lock.yaml - ENCRYPTION_KEY secret 已由 richblack 授權、CC 從 .env pipe 到三個 Worker:
arcrun-auth-static-key、arcrun-auth-service-account、arcrun-cypher-executor(不顯示內容)
- 6.7 文件:在
.claude/rules/加一份05-deploy-convention.md(「新增 Worker = 新目錄 + wrangler.toml,不用改 CI」)
Notes
- 方案 B 是唯一方案(方案 A 已廢棄,違反 CLAUDE.md §禁止行為)
- 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