arcrun — AI workflow execution engine (clean history)
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>
This commit is contained in:
@@ -0,0 +1,178 @@
|
||||
# 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 內完成
|
||||
@@ -0,0 +1,168 @@
|
||||
# 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)
|
||||
|
||||
- [x] 0.1 把 `u6u-core/builtins/` 搬到 `arcrun/builtins/`
|
||||
- [x] 0.2 確認 `arcrun/registry/components/` 21 個零件的 contract.yaml 完整(21/21)
|
||||
- [x] 0.3 刪除 `arcrun/credentials/` 整個目錄(重複,credential route 已在 cypher-executor)
|
||||
- [x] 0.4 更新 `arcrun/cypher-executor/wrangler.toml`:確認 CREDENTIALS_KV binding 存在
|
||||
- [x] 0.5 刪除 `matrix/u6u-core/` 整個目錄(2026-04-19 完成,只剩 credentials/ 已被 cypher-executor 取代)
|
||||
- [x] 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) 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 通過。
|
||||
- [x] 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_RECIPES` fallback 之後,避免 Phase 3 尚未完成時 API 零件 Worker 未部署造成 404;Phase 3 刪除 `BUILTIN_API_RECIPES` 後,API 零件會自然落到此層)。auth primitive 從此層進入。`tsc --noEmit` 通過。
|
||||
|
||||
---
|
||||
|
||||
## Phase 1:auth_static_key WASM(優先,涵蓋 80% 服務)
|
||||
|
||||
方案 B:WASM 自行讀 KV + 解密,TS 不碰 plaintext。
|
||||
|
||||
- [x] 1.1 建立 `arcrun/registry/components/auth_static_key/` 目錄
|
||||
- [x] 1.2 寫 `component.contract.yaml`(input: `{action, api_key, service, request}` → output: `{success, auth_headers, auth_query, auth_body, runtime}`)
|
||||
- [x] 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}`
|
||||
- [x] 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 執行
|
||||
- [x] 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
|
||||
- `wasmWorkerUrl` 從 `component-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 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_request` POST `token_uri` → 拿 `access_token`
|
||||
- 展開 `{{runtime.access_token}}` 模板
|
||||
- [x] 2.4 `tinygo build -o auth_service_account.wasm -target=wasi main.go` — 2026-04-20 編譯通過(1.1MB,在 contract 限制 2MB 內)
|
||||
- [x] 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.ts` 的 `SUPPORTED_PRIMITIVES` 加入 `'service_account'`,workflow 用 google SA recipe 會自動走新 WASM 路徑
|
||||
- [ ] 2.6 端對端測試:google_sheets_sa recipe → 成功取得 access_token → 注入 header
|
||||
- [x] 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 runner(HTTP URL 路徑)。
|
||||
|
||||
- [x] 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 齊備
|
||||
- [x] 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 執行
|
||||
- [x] 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 → 找不到)
|
||||
- [x] 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-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 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_decrypt` host function
|
||||
- [ ] 5.6 `cypher-executor/src/` 全域搜尋 `crypto.subtle.sign` 只出現在 `wasi-shim.ts` 的 `crypto_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
|
||||
|
||||
- [x] 6.1 改寫 `.github/workflows/deploy.yml`:動態掃描所有含 `wrangler.toml` 的目錄(排除 `node_modules/` + Pages 專案),用 matrix job fanout 部署;分兩層(tier1=`.component-builds/*`,tier2=其他),tier1 全綠後才 tier2(避免 service binding target 未存在)
|
||||
- [x] 6.2 加上 TinyGo build 步驟:tier1 matrix 一律 setup-tinygo + 從 `registry/components/{name}/main.go` rebuild `.wasm` → copy 到 `.component-builds/{name}/component.wasm`
|
||||
- [x] 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` 選項
|
||||
- [x] 6.4 統一 pnpm:刪除 `cypher-executor/package-lock.json` + `registry/package-lock.json`;workflow 優先 `pnpm install --frozen-lockfile`,若該目錄無 `pnpm-lock.yaml` 則 fallback 到 `--no-frozen-lockfile`(混合期容錯)
|
||||
- [x] 6.5 加 `max-parallel: 5` 控制 Workers API rate limit(tier1 和 tier2 各自)
|
||||
- [x] 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'` 對 legacy `package-lock.json` 目錄失效(改為不用 cache);再修 tier2 三個 package.json(cypher-executor/registry/builtins)遺漏 `wrangler` devDependency + regen pnpm-lock.yaml
|
||||
- ENCRYPTION_KEY secret 已由 richblack 授權、CC 從 .env pipe 到三個 Worker:`arcrun-auth-static-key`、`arcrun-auth-service-account`、`arcrun-cypher-executor`(不顯示內容)
|
||||
- [x] 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
|
||||
Reference in New Issue
Block a user