- .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>
8.4 KiB
Design Document: Credential Primitives TS → WASM 改寫
Overview
將 cypher-executor 中以 TypeScript 實作的 credential 注入邏輯,改寫為 4 個獨立的 WASM 零件。這是 credential_parts.md 長期規格的實現,不再是「未來 Phase」。
動機:TS 實作無法在地端(workerd)和邊緣端(Wazero)執行。WASM 零件跨 runtime 可攜,符合 u6u 三層部署架構。
嚴格規範(richblack 2026-04-19 確認):cypher-executor TS 完全不實作任何 credential / auth / template / JWT / 解密邏輯。所有業務邏輯必須在 TinyGo WASM 零件內。TS 僅負責 HTTP routing + 呼叫 WASM + host function 提供 runtime primitive(crypto.subtle / KV / fetch)。
現有 TS 實作(要刪除的)
| 檔案 | 功能 | 對應 WASM Primitive |
|---|---|---|
credential-injector.ts — injectFromAuthRecipe() |
static_key template 展開 | auth_static_key |
credential-injector.ts — service_account 分支 |
JWT signing + token exchange | auth_service_account |
credential-injector.ts — decryptCredential() |
AES-GCM 解密 | host function(所有 primitive 共用) |
credential-injector.ts — interpolateTemplate() |
{{secret.KEY}} 替換 |
內建在各 primitive |
jwt-signer.ts — exchangeGoogleJwt() |
PEM→PKCS8→RS256→token | auth_service_account |
component-loader.ts — BUILTIN_API_RECIPES |
gmail/telegram/line/gsheets 寫死邏輯 | 刪除,改用 auth recipe + http_request 零件 |
credential-injector.ts — BUILTIN_CREDENTIALS_MAP |
舊路徑 flat injection | 刪除,統一走 auth recipe |
arcrun/credentials/ |
重複的 credentials Worker | 刪除,路由已在 cypher-executor |
4 個 WASM Primitive 設計
統一 I/O 介面(stdin/stdout JSON)
stdin(Worker → WASM):
{
"action": "authenticate" | "needs_refresh" | "refresh" | "test",
"api_key": "ak_xxx", // 租戶識別,用來組 KV key
"service": "openai", // 對應 auth_recipe:{service}
"request": { "method": "GET", "url": "/path", "headers": {}, "body": null }
}
WASM 內部流程:
1. recipeJSON = kv_get("auth_recipe:" + service)
2. 依 recipe.required_secrets 逐一 kv_get("{api_key}:cred:{name}") → {encrypted, iv}
3. secrets[name] = crypto_decrypt(encrypted, iv)
4. (service_account)crypto_sign_rs256(jwt, pkcs8) + http_request 換 token
5. 展開 recipe.inject 的 {{secret.X}} / {{runtime.X}} 模板
stdout(WASM → Worker):
{
"success": true,
"auth_headers": { "Authorization": "Bearer xxx" },
"auth_query": {},
"auth_body": {},
"runtime": { ... updated runtime state,供下次 refresh 用 }
}
auth_static_key
位置:arcrun/registry/components/auth_static_key/
語言:TinyGo 或 AssemblyScript
功能:
- 讀取
recipe.inject.header/query/body模板 - 用
secrets展開{{secret.KEY}}模板 - 回傳
auth_headers/auth_query/auth_body
涵蓋:~80% 服務(Bearer token, API Key, Basic Auth, custom header)
auth_service_account
位置:arcrun/registry/components/auth_service_account/
語言:TinyGo 或 AssemblyScript
功能:
- 從
secrets.service_account_json解析 private key - JWT signing(RS256:PEM→PKCS8→sign)
- POST token exchange endpoint → 取得 access_token
- 展開
{{runtime.access_token}}模板
crypto 考量:
- TinyGo 的
crypto/rsa+crypto/x509支援有限 - 若 TinyGo 不支援 RS256:使用 host function 讓 Worker 的
crypto.subtle代簽 - 或改用 AssemblyScript(有 as-crypto 套件)
auth_oauth2(新建)
位置:arcrun/registry/components/auth_oauth2/
功能:
needs_refresh:檢查runtime.expires_at是否過期refresh:用runtime.refresh_token+secrets.client_secret換新 tokenauthenticate:展開{{runtime.access_token}}到 headers
auth_mtls(新建)
位置:arcrun/registry/components/auth_mtls/
功能:
- 從
secrets讀取 client cert + key - 回傳 TLS 設定(由 Worker runtime 執行實際 mTLS handshake)
cypher-executor 改動
保留(TS routing 層)
routes/credentials.ts— HTTP CRUD for credentials(接收加密的 payload)routes/recipes.ts— HTTP CRUD for auth recipesroutes/auth.ts— OAuth flow routinggraph-executor.ts— workflow 執行排程lib/wasi-shim.ts— WASM runtime + host functions(加解密 / KV / 簽章 / HTTP 實際由crypto.subtle/ env binding / fetch 執行,但呼叫時機由 WASM 決定)
修改
-
actions/credential-injector.ts— 整檔刪除,改為新檔actions/auth-dispatcher.ts(約 30 行):- 查
resolveAuthRecipe(componentId)取得primitive名稱(static_key / service_account / oauth2 / mtls) - 載入對應的
auth_{primitive}.wasm - 送 stdin:
{ action, api_key, service, request }(不送 secrets、不送 recipe plaintext) - WASM 透過 host function 自行
kv_get讀 recipe + 加密 secret,crypto_decrypt解密 - 讀 stdout → 合併
_auth_headers/_auth_query/_auth_body進 ctx
- 查
-
lib/component-loader.ts— 刪除BUILTIN_API_RECIPES(含 http_request / gmail / telegram / line_notify / google_sheets 的 TS 實作),全部改走 WASM runner。每個.wasm零件都已編譯並以獨立 Worker 部署({canonical-id-kebab}.arcrun.dev)。loader 新增的「WASM runner」路徑就是「canonical_id → HTTP URL 查表後 fetch」,不做 WASM instantiate。- R2 動態注入 WASM 路徑作廢(richblack 2026-04-19 確認:CF workerd 無法以 R2 物件臨時 instantiate WASM)。用戶自製零件(Phase 5)同樣走「產生獨立 Worker」流程,不從 R2 讀。
刪除
lib/jwt-signer.ts— 整檔刪除,RS256 簽章移入auth_service_accountWASM(透過 host functioncrypto_sign_rs256)credential-injector.ts整檔刪除(見上)component-loader.ts的BUILTIN_API_RECIPES整段刪除BUILTIN_CREDENTIALS_MAP已在credential-injector.ts內,隨檔一併刪
Host Functions(WASM ↔ Worker 的橋接)
auth primitive WASM 需要呼叫外部能力時,透過 host function。全部放 u6u namespace。錯誤回傳非零 uint32;成功 = 0 且把結果寫入 outPtr 指向的 buffer。
| Host Function | TinyGo 簽章 | 用途 |
|---|---|---|
http_request |
(urlPtr/Len, methodPtr/Len, headersPtr/Len, bodyPtr/Len, outPtr, outLenPtr) uint32 |
HTTP 請求(已實作) |
kv_get |
(keyPtr, keyLen, outPtr, outLenPtr) uint32 |
讀 KV。Worker 依 key 前綴路由到 CREDENTIALS_KV / RECIPES |
crypto_decrypt |
(encPtr, encLen, ivPtr, ivLen, outPtr, outLenPtr) uint32 |
AES-GCM 解密。encryption key 由 Worker 從 env.ENCRYPTION_KEY 內部讀取,永遠不暴露給 WASM |
crypto_sign_rs256 |
(dataPtr, dataLen, pkcs8Ptr, pkcs8Len, outPtr, outLenPtr) uint32 |
Worker 用 crypto.subtle.sign('RSASSA-PKCS1-v1_5' + SHA-256);private key 以 PKCS8 bytes 傳入 |
這些 host function 在 lib/wasi-shim.ts 中以 WASI import 提供。
安全邊界
ENCRYPTION_KEY只在crypto_decrypthost function 內部使用,絕不經 stdin / 回傳值 / 任何路徑傳給 WASMapi_key經 stdin 傳入 WASM(讓 WASM 自己組{api_key}:cred:{name}KV key)kv_get在 Worker 側檢查 key 前綴:auth_recipe:*→ 讀RECIPES{api_key}:cred:*→ 讀CREDENTIALS_KV,且{api_key}必須等於 stdin 傳入的 api_key(防越權)- 其他前綴 → 回傳錯誤
關於解密位置
採用方案 B(唯一方案):WASM 透過 host function crypto_decrypt() 自行解密。
- cypher-executor TS 完全不解密、不知道 plaintext
ENCRYPTION_KEY永遠留在 Worker host function 內- WASM 知道要解哪份 ciphertext(經
kv_get讀到的{encrypted, iv}),但拿不到 encryption key - 這樣 TS 層完全沒有零件業務邏輯,符合 CLAUDE.md §禁止行為 1/6
(歷史註記:曾規劃方案 A「TS 先解密再送 stdin」,已廢棄 — 違反「TS 不得實作零件邏輯」。)
不做的事
- ❌ 不改 recipe YAML schema — 沿用現有格式
- ❌ 不改 KV 儲存結構 —
auth_recipe:{service}/{api_key}:cred:{name}不變 - ❌ 不改 SDK API — SDK 仍是 HTTP thin wrapper
- ❌ 不建新的 Worker — 在 cypher-executor 內完成