Files
Arcrun/.agents/specs/arcrun/credential-primitives-wasm/tasks.md
T
Leo 13b01328c1 docs: add SDD specs + user requirements + tests
- .agents/specs/: spec-driven-dev docs for arcrun MVP, auth-recipe,
  credential-primitives-wasm (active refactor), landing-page,
  sdk-and-website, u6u-core-mvp, u6u-platform-evolution.
- .agents/steerings/tech.md: detailed tech stack rationale.
- docs/user_requirements/: long-form requirements incl. credential
  primitives, pages spec, py strategy analysis.
- tests/: end-to-end harness scaffolding.

These are the durable context backing CLAUDE.md's SDD protocol.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-20 17:48:24 +08:00

15 KiB
Raw Blame History

Implementation Tasks: Credential Primitives TS → WASM

嚴格規範(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(tier1 21 個 + tier2 3 個)全綠(待 richblack 手動觸發)
  • 6.7 文件:在 .claude/rules/ 加一份 05-deploy-convention.md(「新增 Worker = 新目錄 + wrangler.toml,不用改 CI」)

Notes

  • 方案 B 是唯一方案(方案 A 已廢棄,違反 CLAUDE.md §禁止行為)
  • 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