chore: D22 落地——docs/SDD/wiki/CLAUDE.md 進 repo(Gitea private 預設全 push)

頂層 D22 決策(leo 2026-07-03 拍板):推什麼由開發環境歸屬決定,
Gitea private=除機敏值/build 產物/.github 外全 push。
解 T1.5 卡點:雲端工人 clone 拿得到 credential-store-migration.md,可就地改寫 SDD。
機敏掃描兩輪通過(新增 189 檔約 2.1MB,node_modules/dist/wasm 照舊排除)。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
uncle6me-web
2026-07-03 07:13:15 +08:00
parent c830150da1
commit 5d00e71275
190 changed files with 39486 additions and 14 deletions
@@ -0,0 +1,116 @@
# 2026-05-13 cypher-executor multi-node chain context propagation 漏失
> **總耗時**:約 20 分鐘 debug(找 P0 #9 過程中順帶找到)
> **根因**4 個 edge type 沒 spread baseCtx 給下游節點,原始 context 從第 2 節點開始消失
> **修法**ON_SUCCESS / ON_FAIL / IF / ON_CLICK 套用 PIPE / FOREACH 同模式 `{...baseCtx, ...result}`
> **影響**:任何 chain workflow 從第 2 節點開始 interpolate context key 都失敗
---
## 症狀
mira `acr run wiki_synthesis`7 節點 workflow)回 `Unauthorized`。trace 顯示:
- `load_schema`(節點 1)✓ 200input 含 `api_key=ak_xxx`
- `load_skill`(節點 2)✗ 401 Unauthorizedinput 含 `api_key="{{api_key}}"`**模板原文未替換**
## 推測 → 驗證
**對照組**2 節點 chain 看 input keys
| 節點 | input keys |
|---|---|
| n1 | `api_key, b1, b2, block_id` ✓ 全 context 在 |
| n2 | `blocks, count, success, api_key, block_id`**`b1, b2` 不見**`api_key` 是原文 `"{{api_key}}"` |
n2 的 ctx **只有 n1 output spread**,原始 context`b1`, `b2`, ...)全消失。
## 根因
`graph-executor.ts` 在 outEdges 處理時,PIPE 跟 FOREACH 已有 baseCtx merge
```typescript
// PIPE (line 381-384)
const pipeContext = {
...(context as Record<string, unknown>),
...baseResult,
};
// FOREACH (line 438-444)
const baseCtx = ...;
const itemContext = {
...baseCtx,
...(result as Record<string, unknown>),
[iteratorKey]: item,
};
```
但 ON_SUCCESS / ON_FAIL / IF / ON_CLICK 直接傳 `result`
```typescript
case 'ON_SUCCESS':
result = await this.executeNode(nextNode, graph, result, ...); // ← bug
```
`result` 是上游節點的 output**沒有原始 context**。下游 interpolate 找不到原始 key 就原文留下。
## 歷史脈絡
2026-05-07 commit `e8fca33`"FOREACH preserves outer context")已意識到問題並修了 FOREACH。但**沒同步處理另外 4 個 edge type**。今天才被 mira 7 節點 workflow 踩到。
→ 教訓:架構級修法要全 edge type 一致掃過,不只修當下踩到的。
## 修法
`cypher-executor/src/graph-executor.ts` 4 個 edge case 補:
```typescript
// 改前
result = await this.executeNode(nextNode, graph, result, ...);
// 改後
const baseCtx = (typeof context === 'object' && context !== null) ? context as Record<string, unknown> : {};
const baseResult = (typeof result === 'object' && result !== null) ? result as Record<string, unknown> : {};
const mergedCtx = { ...baseCtx, ...baseResult };
result = await this.executeNode(nextNode, graph, mergedCtx, ...);
```
套用在 line 407 (ON_SUCCESS) / 415 (ON_FAIL) / 423 (IF) / 472 (ON_CLICK)。
## 驗證
`acr run wiki_synthesis` 7 節點 workflow 端對端跑通:
| 節點 | 修前 input | 修後 input |
|---|---|---|
| n2 (load_skill) | `api_key={{api_key}}` ✗ | `api_key=ak_xxx` ✓ |
| n7 (emit_result) | 上游 spread only | baseCtx + 各上游 spread ✓ |
整條 16.2 秒(含 claude_api 真實 Claude 呼叫),結果 `{success: true, data: {...}}`
## Known Limitation
`interpolateData()` 只展開 `node.data` top-level string values**不遞迴 nested object**
```yaml
# emit_result 的 values 內 {{...}} 不會展開
emit_result:
component: set
values:
text: "{{classify.data.text}}" # ← 原文傳,不展開
```
非阻擋 P1SDD 待開 `interpolate-nested-config`。當前 workaround:直接看上游節點的 trace output。
## 未來怎麼避免
1. **新 edge type 加進來時必須走 baseCtx merge 模式**——可以抽出 helper `mergeCtxForDownstream(context, result)` 強制所有 caller 用,避免漏
2. **interpolation 邊界要有測試**:寫一個 2 節點 chain 用 `{{api_key}}` 引用原始 context 的 e2e testCI 跑過
3. **trace output 要顯示 input** —— 沒看 trace 內 `input` 欄位很難看出 interpolation 失敗(目前 acr CLI 不顯示 input,只顯示 result)。CLI 應加 `--verbose` 顯示每節點 input
## Reference
- 對應 SDD`matrix/arcrun/.agents/specs/arcrun/arcrun.md` P0 #10
- 相關 commit`e8fca33` 2026-05-07FOREACH 同類修法)
- 受影響 SDD`polaris/mira/.agents/specs/mira-app/tasks.md` 7B.3c
- 同日另一個 incident[2026-05-13-cypher-outbound-522.md](./2026-05-13-cypher-outbound-522.md)
@@ -0,0 +1,143 @@
# 2026-05-13 cypher-executor outbound fetch 全失效(CF 同 zone 自循環死鎖)
> **總耗時**:約一整天 debug
> **根因**Cloudflare Workers 對「綁 custom domain 的 Worker fetch 同 zone 另一個 custom domain Worker」會撞 zone routing 死鎖 → 回 522
> **修法**cypher-executor fetch component worker 改走 `*.workers.dev` 子域(不同路由系統,繞過 zone proxy)
> **影響**mira 7B.3c 阻擋一整天;封測 P0 從「全綠」revert 成 #9 阻擋
---
## 症狀
mira 跑 `acr run wiki_synthesis`5 節點 workflow,含 kbdb_get / claude_api)回:
```
n1 → {"success": false, "status": 522, "error": "error code: 522"}
```
每節點精準 ~1000ms timeout 後 522。
進一步測試發現**所有 outbound HTTP fetch from cypher-executor 都 522**,無論目標是:
- 同 zone`kbdb-get.arcrun.dev``claude-api.arcrun.dev``cypher.arcrun.dev/health`self
- 外部:`httpbin.org``github.com``google.com`
**service binding 路徑(SVC_STRING_OPS 等 15 個邏輯零件)完全正常**
## 觀察矩陣
| 路徑 | 結果 |
|---|---|
| 本機 curl → kbdb-get.arcrun.dev | 200 ✓ |
| cypher-executor → kbdb-get.arcrun.dev (HTTP fetch) | **522** |
| cypher-executor → claude-api.arcrun.dev (HTTP fetch) | **522** |
| cypher-executor → httpbin.org (HTTP fetch) | **522** |
| cypher-executor → string_ops (Service Binding) | 200 ✓ |
| `acr run hello`(純 SB 1 節點)| 200 ✓ |
| `acr run`5 節點純 SB chainwallTime 2.2s| 200 ✓ |
| **wrangler dev(本機跑 cypher-executor)→ httpbin / kbdb-get** | **200 ✓** |
| 同 src 部署成 `arcrun-cypher-executor-probe`(純 workers.dev,無 routes 綁定)→ httpbin / kbdb-get / claude-api | **200 ✓** |
最關鍵兩條:
1. **本機 wrangler dev 跑同 srcfetch 全通** → 不是 code bug,是 prod 環境問題
2. **probe worker(同 src、不同 name、走 workers.dev、無 cypher.arcrun.dev route)→ 全通** → 是 `cypher.arcrun.dev/*` route 觸發的環境問題
## 誤判路徑(重要 — 避免重犯)
| 假設 | 為什麼錯 |
|---|---|
| **A. CF Free Tier 10ms CPU cap**(首先猜的)| 5 節點 SB chain 跑 2.2 秒卻通過,CPU cap 假設立刻被推翻。但前期 debug 走了不少彎路 |
| **B. 用戶從 Paid 掉到 Free** | 用戶確實掉到 Free,當下繳費恢復 Paid,**重測仍全 522**,徹底排除付費假設 |
| **C. compatibility_date 太舊(2025-02-19**| 沒嘗試 bump,但本機 dev 跟 prod 同個 compatibility_date 一通一不通,**不是這個** |
| **D. 5/8-5/9 manual deploy 推了壞掉的 unpushed commits** | git diff 本機 vs origin/main 確實有 3 個 unpushed commits,但檢查 prod bundle 內 `makeHttpRunner` 跟本機 src 一字不差。**code 沒問題** |
| **E. Bot Fight Mode / WAF 攔截 outbound fetch** | 用戶截圖 zone Security 設定,Bot Fight Mode 未開。**不是這個** |
| **F. 帳號層 outbound network policy 限制** | dashboard worker bindings 完全乾淨(無 outbound worker / tail consumer / Hyperdrive 等),**不是這個** |
| **G. CF Worker subrequest quota** | wrangler tail 顯示 `wallTime: 497ms, cpuTime: 2ms, outcome: ok` —— cypher-executor 自己沒撞任何 quota,是 fetch 出去就被攔 |
| **H. u6u-mcp service trigger 攔截** | 那是 reverse directionu6u-mcp 把 cypher-executor 當 service binding 呼叫),跟 outbound fetch 無關 |
**共通教訓**:522 來自「fetch 出去就被攔截,cypher-executor 不知情把 522 包成 component output 回 client」。wrangler tail 看 cypher-executor outcome 是 `ok` —— **不要被 worker 自己的 `ok` 騙了**,要看 trace 內 component output 才是真相。
## 真相
CF Workers 對「綁 custom domain Worker A 用 `fetch()` 打同 zone 另一個 custom domain Worker B」會撞 **zone reverse proxy 路由死鎖**。具體機制(推測):
1. cypher.arcrun.dev/* route 觸發 → cypher-executor 收到請求
2. cypher-executor 內部 `fetch("https://kbdb-get.arcrun.dev/")` 出去
3. CF edge 看「目標 hostname 是同 zone (`arcrun.dev`),有 worker route」 → 走 zone reverse proxy
4. 同 zone re-entry 觸發 CF 自循環防護 → fetch 不能完成
5. 對 cypher-executor 表現為「fetch 等到 timeout,得到一個假 522 response」
6. cypher-executor 不知情,把 522 當作 component 真實回應傳回 client
httpbin.org 也 522 不是 same-zone 問題,可能是「進入 outbound fetch code path 就被同樣機制 abort」的副作用(這部分仍不完全清楚,但實證 workers.dev 路徑無此問題)。
**`*.workers.dev` 不走 zone reverse proxy**,是 CF 內部的另一條路由(worker-to-worker internal routing),不撞死鎖。**Service Binding 同理**(不走網路,process 內呼叫)。
## 為什麼以前能跑
4/18 arcrun.md 記錄「httpbin_post 端對端驗證通過」,當時 cypher-executor 也是綁 cypher.arcrun.dev/*,也是 fetch *.arcrun.dev**卻能通**。推測 CF 之後某次平台更新收緊了 same-zone fetch 防護,但確切時間不可考。
**教訓**CF 平台行為會悄悄變,依賴 `fetch(*.same-zone.dev)` 是脆弱設計,從此架構上不該再依賴。
## 修法
**改 4 個檔案**commit see git log around 2026-05-13):
1. `cypher-executor/src/lib/component-loader.ts`
- `wasmWorkerUrl(canonicalId, subdomain)` 簽名加 `subdomain` 參數
- URL pattern 從 `https://${kebab}.arcrun.dev` 改為 `https://arcrun-${kebab}.${subdomain}.workers.dev`
- 兩個 callerline 128 / line 192 fallback)同步改
2. `cypher-executor/src/actions/auth-dispatcher.ts`
- `wasmWorkerUrl(...)` 呼叫同步加 `subdomain` 參數,從 `env.WORKER_SUBDOMAIN`
3. `cypher-executor/src/types.ts`
- `Bindings``WORKER_SUBDOMAIN: string`
4. `cypher-executor/wrangler.toml`
- `[vars]``WORKER_SUBDOMAIN = "uncle6-me"`
**Dashboard 一次性手動操作**5 個 P0 component workerkbdb-get / kbdb-ingest / kbdb-create-block / kbdb-patch-block / claude-api)→ Settings → Domains & Routes → workers.dev → **Enable**
**未來新增 component worker 時必須**dashboard 啟用 workers.dev URLrule 03 已加入步驟 5
## 驗證
修復後重測同樣 3 個 trigger:
| Trigger | 修復前 | 修復後 |
|---|---|---|
| cypher → kbdb-get | 522 (1064ms) | **200 (2027ms)** 拿回真實 block 內容 |
| cypher → claude-api | 522 (1014ms) | **200 (6226ms)** Claude 真實回應 |
| cypher → httpbin | 522 (1023ms) | 404 error 1042(不是 522 死鎖了;外部 fetch 限制是另一個問題,不影響 mira)|
mira `acr run wiki_synthesis` 5 節點 workflow 跑通前 3 節點(load_schema / load_skill / load_entities),後續節點失敗是 mira 業務邏輯問題(Unauthorized 在某個 block id),不是 cypher-executor 平台問題。
## 影響範圍與善後
**影響**
- 任何用 cypher-executor 跑外部 fetch 的 workflow 都壞(封測 Step 2/5/6 / mira / 自架用戶)
- arcrun 「每個零件 = 公開 URL」承諾在純 `*.arcrun.dev` 體系下無法跟「cypher-executor HTTP fetch」並存
**善後(已做)**
- ✅ rule 01-tech-stack.mdURL 慣例改為「對內 workers.dev / 對外 arcrun.dev」二元
- ✅ rule 03-component-architecture.md:第一核心概念改為「每個零件 = 兩個 URL」,部署步驟加 dashboard workers.dev enable
- ✅ arcrun.md P0 #9 標 resolved 並 reference 本 incident
- ✅ probe worker 已刪
- ⏳ mira tasks.md 7B.3c 解除阻擋(即將)
## 未來怎麼避免
1. **新 component worker 部署 checklist 強制包含 dashboard workers.dev enable**(rule 03 已加,但實務上靠 dashboard 容易忘,未來可考慮寫 deploy 後驗證 script
2. **不要再加 outbound HTTP fetch 對同 zone hostname** —— cypher-executor 對任何 `*.arcrun.dev` 的 fetch 都該走 workers.dev URL
3. **wrangler dev 是診斷神器** —— 本機跑 prod 同份 src 是區分「環境問題 vs code 問題」最快方法,未來 prod 出怪異行為先跑這個
4. **wrangler tail outcome:ok 不代表沒問題** —— 要看 component trace output 才是真相
5. **Self-hosted fork 文件要明寫**:必須改 `WORKER_SUBDOMAIN` + 所有 component worker dashboard 啟用 workers.devrule 03 已加,BETA_TEST.md 待 onboarding 章節加)
## Reference
- 對應 SDD`matrix/arcrun/.agents/specs/arcrun/arcrun.md` P0 #9
- 規範更新:`matrix/arcrun/.claude/rules/01-tech-stack.md``rules/03-component-architecture.md`
- 受影響 SDD`polaris/mira/.agents/specs/mira-app/tasks.md` 7B.3c(阻擋一整天)
@@ -0,0 +1,113 @@
# 2026-05-13 cypher-executor 巢狀 FOREACH 內層找不到 iterable
> **總耗時**:約 30 分鐘
> **根因**`getIterableFromContext()` 只看當前節點 result + 只看 top-level,巢狀 FOREACH 內層拿不到外層注入的 nested array
> **修法**fallback 找 context;掃 ctx 內 object 取 nested key
> **影響**:任何想做「FOREACH X → 每個 X 內再 FOREACH Y」結構的 workflow 都壞
---
## 症狀
mira `wiki_synthesis` 想做三層樹寫入:
```yaml
flow:
- "classify >> ON_SUCCESS >> create_wiki_page"
- "create_wiki_page >> 對每個 paragraph >> create_paragraph"
- "create_paragraph >> 對每個 triplet >> create_triplet"
```
classify 回 `{ entity, paragraphs: [{ facet, content, triplets: [...] }, ...] }`
- 外層 FOREACH(對每個 paragraph):✓ 正常跑 N 次
- 內層 FOREACH(對每個 triplet):✗ 跑 0 次
KBDB 內結果:N 個 wiki-paragraph 建好,0 個 triplet。
## 兩個獨立根因
### 根因 A`result` only, no fallback to context
外層 FOREACH 跑時:
- `create_paragraph` output: `{ data: { id, ... }, success: true }`
- FOREACH 處理 `>> 對每個 triplet >> create_triplet`iteratorKey = `triplet`
- `getIterableFromContext(result, 'triplet')``result.triplet` / `result.triplets` → 都沒,回 `[]`
`paragraph.triplets` 早就在 ctx(外層 FOREACH 注入了 `paragraph` 物件)。
→ FOREACH 該 fallback 找 context。
### 根因 B:只看 top-level,不看 nested
即使 fallback 找 context`getIterableFromContext(ctx, 'triplet')``ctx.triplet` / `ctx.triplets`**找 top-level**。
但 triplets 在 `ctx.paragraph.triplets`(外層 FOREACH 把 paragraph 整個物件注入 ctx 的 `paragraph` key)。
`getIterableFromContext` 該掃一層 nested。
## 修法
`cypher-executor/src/graph-executor.ts`
```typescript
// FOREACH case
let items = getIterableFromContext(result, iteratorKey);
if (items.length === 0) {
items = getIterableFromContext(context, iteratorKey); // ← A. fallback
}
// getIterableFromContext
function getIterableFromContext(context: unknown, key: string): unknown[] {
if (!context || typeof context !== 'object') return [];
const plural = key + 's';
const obj = context as Record<string, unknown>;
let items = obj[plural] ?? obj[key];
if (!Array.isArray(items)) {
for (const v of Object.values(obj)) { // ← B. 掃 nested
if (v !== null && typeof v === 'object' && !Array.isArray(v)) {
const nested = (v as Record<string, unknown>)[plural] ?? (v as Record<string, unknown>)[key];
if (Array.isArray(nested)) {
items = nested;
break;
}
}
}
}
return Array.isArray(items) ? items : [];
}
```
## 驗證
mira wiki_synthesis 跑「物理 AI」raw → KBDB 內出現:
```
wiki-page "物理 AI" (ef644ec3)
├─ paragraph 00d8f819
│ ├─ triplet: 物理 AI >> 對立於 >> 純數位空間的 AI
│ └─ triplet: 物理 AI >> 提出者 >> Andrej Karpathy
└─ paragraph 9a5b11b7
├─ triplet: leo >> 支持 >> 物理 AI
└─ triplet: 物理 AI >> 需要 >> 傳感器與機器人協同
```
4 個 triplet 都正確接到對應 paragraph parent_id。
## 為什麼這個沒早被踩到
cypher binding 之前的用例都是「一層 FOREACH」(commit e8fca33 wiki workflow 範例就是單層)。mira V2 wiki 結構是首個真正用巢狀 FOREACH 的,所以才剛踩到。
## 未來避免
1. **設計 FOREACH 時假設 iterable 可能在 ctx 任何深度** —— 這次的「掃一層 nested」其實還不夠通用,未來如果有三層 FOREACH(A → B → C → D),可能要遞迴掃。MVP 先一層,需要時擴
2. **驗證 yaml 應該支援巢狀 FOREACH 測試** —— CLI validator 沒擋住巢狀,但「能 parse」不等於「能跑」。未來加 e2e 測試
3. **`對每個 X` 命名建議用單數** —— iteratorKey 自動補 `s` 找 plural`paragraph` → 找 `paragraphs`)。如果 ctx 內的 array 用單數命名(如 `items`),會找不到。MVP 階段建議 yaml 內 array 用複數,FOREACH 用單數,符合慣例
## Reference
- 對應 SDD`matrix/arcrun/.agents/specs/arcrun/arcrun.md` P0 #10 補完 C 段
- 同日先解:
- [2026-05-13-cypher-outbound-522.md](./2026-05-13-cypher-outbound-522.md)
- [2026-05-13-chain-ctx-propagation.md](./2026-05-13-chain-ctx-propagation.md)
- 受影響 SDD`polaris/mira/.agents/specs/mira-app/tasks.md` 7B.3e (V2 樹狀結構)
@@ -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 步驟 2credential 注入鏈路)阻擋;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 存在 KVkv_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 上會解成垃圾 bytesAES-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 為空)。
## 附帶 bugBody 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 05runtime 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`
@@ -0,0 +1,33 @@
# Incidents
平台/架構級事件 post-mortem。每份檔案 = 一個事件。
不同於 SDD`.agents/specs/`)的「我們要怎麼設計」,incident 記錄「我們實際撞過什麼雷、怎麼診斷、怎麼修、未來怎麼避免」。
## 命名慣例
`YYYY-MM-DD-{short-slug}.md`
例:`2026-05-13-cypher-outbound-522.md`
## 寫的時機
- 花了顯著時間(> 1 小時)才查清根因的問題
- 平台層 / CF / 架構層問題(不是普通 code bug)
- 需要修架構決策或 URL 慣例的問題
- 「以為解決了結果不是」的反覆事件
普通 bug fix 不需要寫 incident。
## 檔案結構建議
每份 incident 至少含:
- **症狀**:怎麼觀察到的
- **誤判路徑**:走過哪些錯方向(含為什麼錯)
- **真相**:根因
- **修法**:實際改了什麼
- **驗證**:怎麼確認解了
- **未來避免**:下次怎麼提早識別
最重要的是**誤判路徑**——這是未來自己(或其他 dev)會踩同樣假設的最好預防。