Files
Arcrun/.agents/specs/arcrun/credential-primitives-wasm/design.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

8.4 KiB
Raw Blame History

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 primitivecrypto.subtle / KV / fetch)。


現有 TS 實作(要刪除的)

檔案 功能 對應 WASM Primitive
credential-injector.tsinjectFromAuthRecipe() static_key template 展開 auth_static_key
credential-injector.ts — service_account 分支 JWT signing + token exchange auth_service_account
credential-injector.tsdecryptCredential() AES-GCM 解密 host function(所有 primitive 共用)
credential-injector.tsinterpolateTemplate() {{secret.KEY}} 替換 內建在各 primitive
jwt-signer.tsexchangeGoogleJwt() 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

stdinWorker → 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_accountcrypto_sign_rs256(jwt, pkcs8) + http_request 換 token
  5. 展開 recipe.inject 的 {{secret.X}} / {{runtime.X}} 模板

stdoutWASM → 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

功能:

  1. 讀取 recipe.inject.header/query/body 模板
  2. secrets 展開 {{secret.KEY}} 模板
  3. 回傳 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

功能:

  1. secrets.service_account_json 解析 private key
  2. JWT signingRS256PEM→PKCS8→sign
  3. POST token exchange endpoint → 取得 access_token
  4. 展開 {{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/

功能:

  1. needs_refresh:檢查 runtime.expires_at 是否過期
  2. refresh:用 runtime.refresh_token + secrets.client_secret 換新 token
  3. authenticate:展開 {{runtime.access_token}} 到 headers

auth_mtls(新建)

位置arcrun/registry/components/auth_mtls/

功能:

  1. secrets 讀取 client cert + key
  2. 回傳 TLS 設定(由 Worker runtime 執行實際 mTLS handshake

cypher-executor 改動

保留(TS routing 層)

  • routes/credentials.ts — HTTP CRUD for credentials(接收加密的 payload
  • routes/recipes.ts — HTTP CRUD for auth recipes
  • routes/auth.ts — OAuth flow routing
  • graph-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 行):

    1. resolveAuthRecipe(componentId) 取得 primitive 名稱(static_key / service_account / oauth2 / mtls
    2. 載入對應的 auth_{primitive}.wasm
    3. 送 stdin{ action, api_key, service, request }不送 secrets、不送 recipe plaintext
    4. WASM 透過 host function 自行 kv_get 讀 recipe + 加密 secretcrypto_decrypt 解密
    5. 讀 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_account WASM(透過 host function crypto_sign_rs256
  • credential-injector.ts 整檔刪除(見上)
  • component-loader.tsBUILTIN_API_RECIPES 整段刪除
  • BUILTIN_CREDENTIALS_MAP 已在 credential-injector.ts 內,隨檔一併刪

Host FunctionsWASM ↔ 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_decrypt host function 內部使用,絕不經 stdin / 回傳值 / 任何路徑傳給 WASM
  • api_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 內完成