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,87 @@
|
||||
# 2026-05-29 credential 解密失敗(兩個 Worker 的 ENCRYPTION_KEY 漂移)
|
||||
|
||||
> **症狀**:`acr recipe test kbdb`(credential 注入)回 HTTP 500,`auth_static_key` 回 `credential kbdb_api_key 解密失敗`
|
||||
> **根因(主)**:`arcrun-auth-static-key` Worker 的 `ENCRYPTION_KEY` secret 跟正本(cypher-executor / CLI 用的那把)值不同、格式也不同(44-char base64 vs 64-char hex)。AES-GCM 用錯 key 必然解密失敗。
|
||||
> **根因(附)**:`component-loader.ts` 用 `res.json().catch(() => res.text())` 讀 response body → body 被讀兩次 → `Body has already been used`。
|
||||
> **修法**:(1) `wrangler secret put ENCRYPTION_KEY` 把 auth-static-key 對齊正本 64-hex;(2) 新增 `readBodyOnce()` 先取 text 再 parse JSON。
|
||||
> **影響**:BACKLOG 步驟 2(credential 注入鏈路)阻擋;Phase 3 降級假零件成 recipe 的前置。
|
||||
|
||||
---
|
||||
|
||||
## 症狀
|
||||
|
||||
`acr recipe test kbdb` 端到端打不到 2xx。直接 probe `auth_static_key`:
|
||||
|
||||
```
|
||||
POST https://auth-static-key.arcrun.dev/ {action:"authenticate", api_key:"ak_…", service:"kbdb"}
|
||||
→ {"success":false, "error":"credential kbdb_api_key 解密失敗", ...}
|
||||
```
|
||||
|
||||
前置都綠(排除誤判方向):
|
||||
- `auth_recipe:kbdb` 存在、`primitive=static_key`(kv_get 命中 410 bytes)
|
||||
- `kbdb_api_key` credential 存在 KV(kv_get 命中 108 bytes 的 `{encrypted, iv}`)
|
||||
- 失敗精準落在「解密」這一步
|
||||
|
||||
## 定位(key-fingerprint 診斷,只印 SHA-256 前綴,不印 key/明文)
|
||||
|
||||
在 `aesGcmDecrypt`(`wasi-shim.ts`)暫加:
|
||||
|
||||
```
|
||||
console.error(`[decrypt] ENCRYPTION_KEY sha256_prefix=${fpHex} keyLen=${len}`)
|
||||
```
|
||||
|
||||
deploy auth-static-key + `wrangler tail` 抓到:
|
||||
|
||||
| 來源 | keyLen | sha256 前綴 | 格式 |
|
||||
|---|---|---|---|
|
||||
| 加密端(CLI `~/.arcrun/config.yaml` 的 `encryption_key`) | 64 | `fa84f2ce9027` | hex(→32 bytes)✓ |
|
||||
| 解密端(`arcrun-auth-static-key` 的 `ENCRYPTION_KEY` secret) | **44** | **`ff219b123c89`** | base64 ✗ |
|
||||
|
||||
**兩個 mismatch 同時存在**:值不同 + 格式不同。`hexToUint8Array` 套在 44-char base64 上會解成垃圾 bytes,AES-GCM 必失敗。
|
||||
|
||||
漂移源頭:`arcrun/.env` 裡的 `ENCRYPTION_KEY` 就是那把錯的 base64(`ff219b123c89`),有人拿它去 `wrangler secret put` 設進 auth-static-key。
|
||||
|
||||
## 為什麼正本是 64-hex
|
||||
|
||||
`/register`(register.ts:42)把 `encryption_key: c.env.ENCRYPTION_KEY` 原樣回給用戶 —— 即 **cypher-executor 的** `ENCRYPTION_KEY`。用戶 config 是 64-hex(`fa84f2ce9027`),所以正本 = cypher-executor 那把 64-hex。CLI 加密 credential 也用這把。auth-static-key 必須跟它一致才能解開。
|
||||
|
||||
診斷用完即移除(`wasi-shim.ts` 還原,git diff 為空)。
|
||||
|
||||
## 附帶 bug:Body has already been used
|
||||
|
||||
修對 key 後,`/execute` 端到端從 500 變成「Node n1 failed: Body has already been used」。
|
||||
|
||||
`component-loader.ts` 的 `makeRecipeRunner` / `makeAuthRecipeRunner`:
|
||||
|
||||
```ts
|
||||
const data = await res.json().catch(() => res.text()); // ✗ res.json() 失敗時 body 已消費
|
||||
```
|
||||
|
||||
KBDB `/health` 回非 JSON(純文字)→ `res.json()` throw → `.catch(() => res.text())` 第二次讀 body → throw。
|
||||
|
||||
修法 — 讀一次:
|
||||
|
||||
```ts
|
||||
async function readBodyOnce(res: Response): Promise<unknown> {
|
||||
const text = await res.text();
|
||||
try { return JSON.parse(text); } catch { return text; }
|
||||
}
|
||||
```
|
||||
|
||||
## 修法步驟
|
||||
|
||||
1. `cd .component-builds/auth_static_key && wrangler secret put ENCRYPTION_KEY`,貼正本 64-hex(= `~/.arcrun/config.yaml` 的 `encryption_key`)。**richblack 手動**(rule 05:runtime secret 不進 CI、CC 不碰)。
|
||||
2. `component-loader.ts` 加 `readBodyOnce()`,兩處 `res.json().catch(...)` 換掉。`tsc --noEmit` 綠,deploy cypher-executor。
|
||||
3. 修正源頭文件 `arcrun/.env` 的 `ENCRYPTION_KEY` 改成 64-hex(避免下次再設錯)。
|
||||
|
||||
## 驗證證據
|
||||
|
||||
- 直接 probe auth-static-key:**HTTP 200**, `success:true`, 產出 `Authorization: Bearer …`
|
||||
- 端到端 `/execute`:**HTTP 200**, trace 乾淨
|
||||
- auth 確證:直接 curl KBDB `/blocks` 不帶 token → `401 {"error":"Missing token"}`;經 cypher-executor(注入 token)→ 過 auth,進 KBDB handler 回 ZodError(缺 `content`)。**無 401 = token 被接受**。
|
||||
|
||||
## 教訓
|
||||
|
||||
- **同一把 key 出現在 ≥2 個 Worker 的 secret = 漂移風險**。auth-static-key / auth_service_account / cypher-executor 都讀 `ENCRYPTION_KEY`,靠人各設一次必漂。長期應有單一發放來源或部署時自動同步。
|
||||
- **debug 加密問題,先比 key 指紋(SHA-256 前綴),不要碰 key 明文**。一個 fingerprint log 就分辨出「值錯」vs「格式錯」vs「資料壞」。
|
||||
- **`res.json().catch(() => res.text())` 是反模式** —— body 只能讀一次。永遠先 `res.text()` 再 `JSON.parse`。
|
||||
Reference in New Issue
Block a user