922a57fe34
Self-hosted 開源:WASM 零件 + recipe + cypher-executor,跑在你自己的 Cloudflare。 此為重建的乾淨歷史起點(移除曾誤 commit 的 GCP SA 金鑰,舊歷史保留在 richblack/arcrun 與本地 backup 分支)。含: - acr init --self-hosted installer(建 KV/R2 + codeload 拉預編譯 wasm + wrangler deploy + seed recipe) - recipe push 把關(資料外流提醒 + 打通檢查) - 19 個正當零件預編譯 wasm(claude_api/km_writer/kbdb_upsert_block 排除:違反 DECISIONS §1) - CLI / cypher-executor / registry / 完整 SDD Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
179 lines
8.4 KiB
Markdown
179 lines
8.4 KiB
Markdown
# 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
|
||
|
||
功能:
|
||
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 signing(RS256:PEM→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 + 加密 secret,`crypto_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.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_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 內完成
|