Files
Arcrun/docs/HANDOFF-self-host-harness.md
Leo 20c7610371 refactor: 移除已廢棄的自管加密金鑰機制(credential 全面託管 CF Workers Secrets)
leo 2026-07-20 明令:「已經改用 cf 自己的 secrets,不要再說它了」
「我希望以後再也看不到這個詞再出現」

背景:credential 早已遷移至 CF Workers per-script Secrets + D1 目錄,
舊的自管金鑰(client 端 AES-GCM + KV 密文 + crypto_decrypt)是遷移期遺留。
本次連根移除,含一併作廢的死 SaaS 碼。

移除:
- 舊 KV 密文解密路徑(credential-injector.ts 整檔、dual-read fallback)
  前置驗證:leo21c / youlin 兩帳號 CREDENTIALS_KV 實測 *:cred:* 皆 0 筆
- migrate-to-workers-secrets 搬家端點(回填已完成,無可回填)
- /register 路由與 generateApiKey(HMAC 產 ak_ key 是 SaaS 遺物;
  self-hosted 走 namespace 明碼 D21,已無人使用)
- platform_crypto component(三帳號實測 404 已退役,無 workflow 引用)

保留(附理由):
- crypto_decrypt 保留為永遠回失敗的 stub——現役三個 auth .wasm 仍宣告該
  import,缺項會讓 WASM instantiate 直接失敗。待零件重編後可真正刪除。

順帶修復(原不在範圍,但會實際壞事):
- /auth/callback 有 `if (!key) redirect(server_error)` 閘,未設該 secret 的
  實例會登入直接失敗 → 已移除
- OAuth 兩處把 provider token 寫進舊加密 KV(租戶鍵與實際 api_key 在 rotate
  後必然分歧,已失效)→ 改導向 Workers Secrets,包 try/catch 不影響登入
- acr init Standard 模式呼叫已刪除的 /register → 改引導 OAuth 取 key
- .claude/rules 與 system-dev/docs 是同一規範的兩份鏡像,先前只改 rules
  導致鏡像仍在教舊做法 → 已同步(此類雙檔同步應納入檢查)

新用戶安裝從此零 secret 前置。
測試 187/188(唯一 fail 為 pre-existing,stash 驗證與本次無關);
cypher-executor 與 cli typecheck 全綠。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-21 01:32:48 +08:00

213 lines
16 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 交辦文件:完成 arcrun self-hosted harness(給接手的 CC
> 建立:2026-06-01(由前一個 CC 調查後撰寫)
> 對象:接手的外部 CC
> 目的:把 arcrun 補到「任何 CC 在自己的 CF 帳號上 self-host 後就能順暢開發、且不可能重蹈 mira 的錯」的程度。
>
> **先讀**`DECISIONS.md`(穩定決策)、`.claude/rules/06-mindset.md`mindset)、`BACKLOG.md`(流動待辦)。
> 本文件不取代它們,只是把「今天要做的三件事」連同已查證的實況整理好,讓你不用重跑調查。
---
## 0. 戰法已轉變(最重要的背景)
richblack 2026-06-01 決定:**從 SaaS 改成 self-hosted 開源策略。**
這直接改變 harness 的成功定義:
- **舊定義**:在 richblack 的 prod 帳號(`cypher.arcrun.dev`)上能跑。
- **新定義**:**任何 CC 在自己的 CF 帳號上 `acr init --self-hosted` 後就能跑通一個含 recipe 的 workflow,而且寫錯時會被程式擋住。**
richblack 會用另一個 CF 帳號實測 self-host。所以「self-hosted 一鍵起得來」從「第一期重要但非阻擋項」**升為今日第一優先**。
### arcrun 現在的核心心智(richblack 2026-06-01 校準,比 DECISIONS §1 更硬)
- 核心**零件數量少、由 richblack 維護、不接受 CC 自製**(可投稿 PR,人 merge = 閘門)。
- 其他人做的一律是 **recipe**= http_request + 一組 YAML 設定,不用 deploy)。
- arcrun 是**一套給 CC 的 harness**:事前提醒 CC 能用什麼 / 不能做什麼,事後用程式擋住讓它**無法犯錯**。
- **你不用管 mira。** mira 是錯誤做法的源頭(見 §1),它自己會修。你的目標是讓**任何** CC 都能用,且絕不會發生 mira 的錯。
---
## 1. mira 故障 = 症狀樣本(已定位,不用你修)
mira`/Users/youlinhsieh/Documents/tech_projects/InkStoneCo/polaris/mira/arcrun/*.yaml`)的 workflow 寫 `component: kbdb_get` / `claude_api` / `telegram` 等。這些是 **mira 當初自己錯做的「假零件」**DECISIONS §1 判準:打固定 endpoint 的東西是假零件,該是 recipe)。
本次整修(BACKLOG 步驟3)已把這些假零件**降級成 recipe + 刪掉零件目錄**registry/components 從 33 → 22)。所以 mira 斷了。
**這證明的事**mira 的錯,正是當時 harness 沒擋住的漏洞。零件刪了,但 harness 還缺「**事前告訴 CC 別這樣做 + 事後擋住 CC 這樣做**」的完整機制 → 下一個 CC 還會犯同樣的錯。**這就是你要補的(§3 task 2)。**
---
## 2. 已查證的實況(你不用重查,2026-06-01 實打 prod
### 2.1 降級後的 recipe 鏈路是「活的」✅
實打 `https://cypher.arcrun.dev/recipes`richblack prod)確認以下 recipe 都在 KV
| canonical_id | hash | endpoint | auth_service |
|---|---|---|---|
| `kbdb_get` | rec_4c7dcf9b | `https://kbdb.finally.click{{_path}}` | kbdb |
| `gmail_send` | rec_cd426129 | gmail.googleapis.com/.../send | google_gmail_sa |
| `google_sheets_append` | rec_9fd1b662 | sheets.googleapis.com{{_path}} | google_sheets_sa |
**「對的用法」(`component: kbdb_get` 走解析鏈 step 6 查 `recipe:kbdb_get`)本身是通的。** 不需要重建 recipe。
> 注意:這是 richblack 的 prod KV。**self-host 的新帳號 KV 是空的**,需要 seed 這些 recipe(見 §3 task 1 的 seed 步驟)。
### 2.2 component-loader 解析鏈(`cypher-executor/src/lib/component-loader.ts`
`resolveComponent` 依序嘗試 8 層(行號近似,以實際檔案為準):
```
0. 平台 orchestration 零件(trigger_workflow line ~88
1. 內建零件(純 JS) line ~96
2. 外部 URLhttp(s)://... line ~100
3. cmp_hash → WEBHOOKS KV idx → 邏輯 Worker line ~105
4. rec_hash → RECIPES KV idx → recipe 執行 line ~115
5. 邏輯零件 canonical_id → Service Binding (SVC_*) line ~122
5.5 auth recipe (auth_recipe:{service}) line ~127
6. KV recipe canonical_id → RECIPES KV → fetch 外部 API line ~130 ← kbdb_get 等降級 recipe 命中這層
7. WASM HTTP runner(白名單 WASM_HTTP_RUNNER_IDS line ~134
8. 找不到 → 報錯 line ~142
```
`WASM_HTTP_RUNNER_IDS` 白名單(line ~36)現只剩:`http_request` / `cron` / 4 個 `auth_*` primitive。
`claude_api``kbdb_upsert_block`BACKLOG 標 deferred、源碼暫留)**不在白名單也沒 recipe** → 用到它們的 workflow 會落到 step 8 報錯。這是 mira 自己的問題,不在你範圍。
### 2.3 `acr init --self-hosted` 現況:純手動問答,差很遠
`cli/src/commands/init.ts``initSelfHosted()`line 105-131**只是問 6 個問題後寫進 config**
要求 CC 自己**事先**部署好 Worker、建好 KV、再手填 Account ID / cypher URL / 兩個 KV namespace ID / WASM bucket / CF token。
BACKLOG 步驟7 要的是「**貼 CF token → 自動建 KV、部署 Worker、自動 workers.dev、寫回 config**」。**這是最大缺口,task 1 的主體。**
config 讀取端已支援 self-hosted`cli/src/lib/config.ts:52` 已能用 `cypher_executor_url`),所以你只要把「自動部署」這段補上,config/執行端不用動。
### 2.4 CI/CD 已是通用掃描式(可重用於 self-host 部署)
`.github/workflows/deploy.yml` 掃所有含 `wrangler.toml` 的目錄自動部署(見 `.claude/rules/05-deploy-convention.md`)。
self-host 自動部署可以參考同一套掃描邏輯(`find . -name wrangler.toml`),對每個目錄跑 `wrangler deploy`
---
## 3. 今天要做的三件事(按序,全在 harness 主線)
> richblack 指示:「全部要做」(含 `acr init --self-hosted`)。
> 三件都做完 = 今天可交付:外部 CC 能 self-host 起來、用對的方式開發、犯錯被擋。
### 🔴 Task 1:完成 `acr init --self-hosted` 一鍵自動化(第一優先)
> ✅ **實作狀態(2026-06-02,已大致完成)**:定稿形態為 **installer 模式**richblack 拍板)——
> 用戶只做:申請 CF 帳號 → 裝 wrangler → 裝 acr → `acr init --self-hosted`(貼 token),其餘自動。
> 已實作(typecheck 過):`cli/src/lib/api-recipe-seeds.ts`10 recipe 種子)、`cf-api.ts` 的
> `CfAccountClient`(建 KV/R2/查 subdomain/驗 token)、`deploy.ts`(常數 + downloadAndDeploy)、
> `initSelfHosted()` 改寫、`acr update`、`cypher-executor/scripts/seed-api-recipes.ts`。
> **唯一剩餘前置(13.6**:repo 沒有含預編譯 wasm 的 GitHub release.wasm 不 commitrule 05)→
> `downloadAndDeploy()` 目前**誠實回 implemented:false 不假裝部署**mindset §7)。建 KV/R2/seed/config
> 已可跑;release 產製管道補上後部署即自動化。定稿設計見 SDD `self-hosted-init.md`(含 §6 前置依賴)。
> **以下原始子步驟保留供對照**KBDB recipe 採 Supabase 模式進 seedrichblack 2026-06-02)。
**目標**CC 只需提供「CF Account ID + CF API Token」,CLI 自動完成其餘一切。
**SDD**:定稿 `docs/3-specs/arcrun/sdk-and-website/self-hosted-init.md`installer 模式,已與 richblack 對齊)。
**子步驟**
1.`cli/src/commands/init.ts``initSelfHosted()`
- 收 CF Account ID + CF API Token(要 KV Edit + Workers Scripts Edit + R2 權限)。
- 用 CF API(或 shell out `wrangler`**自動建 7 個 KV namespace**WEBHOOKS / CREDENTIALS_KV / RECIPES / USERS_KV / SESSIONS_KV / ANALYTICS_KV / EXEC_CONTEXT(清單見 `.claude/rules/01-tech-stack.md` 資料儲存表)+ R2 WASM_BUCKET。
- **自動部署所有 Worker**cypher-executor + registry + 22 個 `.component-builds/*`。可重用 §2.4 的 `wrangler.toml` 掃描。每個 worker 的 `wrangler.toml` 已含 `workers_dev = true`BACKLOG 步驟 P1#2 已做),部署後 workers.dev URL 自動啟用。
- **把 cypher-executor 的 `[vars] WORKER_SUBDOMAIN` 改成 CC 自己的帳號 subdomain**self-host 關鍵,見 P0 #9cypher-executor 走 `arcrun-{name}.{subdomain}.workers.dev` 對內 URL)。
- **seed 降級 recipe + auth recipe 進 RECIPES KV**:新帳號 KV 是空的。把 §2.1 那些 recipekbdb_get/gmail_send/...+ auth recipe seed 寫進去。auth recipe seed 已有 `cypher-executor/scripts/seed-auth-recipes.ts`API recipe 需確認有對應 seed 機制(routes/recipes.ts 是動態 push,可能要寫一份 seed 腳本或用 `acr recipe push`)。
- 寫回 config(現有欄位已足夠)。
2. **runtime secret 不進 CLI 自動化**`CF_SECRETS_API_TOKEN` 等由 CC 自己 `wrangler secret put`rule 05 禁止 secret 進自動化流程)。CLI 應在最後**印出提示**告訴 CC 要手動 put 哪些 secret 到哪些 worker。
**驗收(客觀證據,不是口頭宣布 — mindset §7)**
- richblack 用全新 CF 帳號跑 `acr init --self-hosted` → 全程無手動建 KV / 部署。
- 跑完後 `acr push` 一個含 `component: kbdb_get`(或 http_request + 自建 recipe)的 workflow → trigger → HTTP 2xx + execution trace 證明跑通。
### 🔴 Task 2(已重定義 2026-06-01):封鎖自製零件 + recipe 入庫把關
> ✅ **實作狀態(2026-06-02,第一期部分完成)**(1) 封鎖自製零件 = 靠 GitHub PR 人 merge,無需新做
> (矛盾已釐清)。(2a) 資料外流提醒 = **既有實作已涵蓋**recipe.ts `obtainExposureConsent` + exposure-warning.ts
> 非 TTY 拒絕)。(2b) 打通檢查 = **新增** `probeRecipeEndpoint`recipe.tstypecheck 過):push 後實打
> endpoint,提醒級不硬擋,含 {{模板}} 誠實說明待 run 才知,401/403 標「多半缺 credential 非 bug」。
> 公共庫 relay 檢核(--public= 第一期後。SDD `recipe-push-gatekeeping.md` + tasks.md W2。
> ⚠️ **方向修正(richblack 2026-06-01**:原 Task 2「acr validate 擋假零件名」**作廢**。
> 理由:自製/修改零件的路已封鎖(CC 造不出零件)→「擋假零件」這件事不存在;workflow 引用
> recipe`component: kbdb_get`)是合法且未來唯一的擴充方式,不該被當假零件擋。
> 把關點**從 workflow validate 移到 recipe 入庫(push)那一刻**。
> 已動的 yaml-parser.tsLEGAL_PRIMITIVES / findSuspectComponents**已回退**。
**新目標**
1. **封鎖自製零件**:靠「零件投稿走 GitHub PR + 人 merge」天然閘門(DECISIONS §8)。零件數量少、
絕大多數是 recipe → 不為零件 PR 蓋自動化把關(量少,人工檢查即可;爆量才回頭想自動化)。
2. **recipe 入庫把關**(CC 唯一能擴充的是 recipe,一律用 push,自有庫/公共庫同一套指令):
- **自有庫(self-hosted= 提醒級**:(a) 資料外流提醒——會讓資料/服務對外可見的動作需人類明示同意;
(b) 打通檢查——push 時實打 endpoint 回報 2xx 與否(誠實標原因,不假綠,不硬擋)。
- **公共庫 = 維護者 relay 檢核**(實際打通、真收到成功回傳)— 第一期後。
**SDD**:已寫 design 給 richblack review →
`docs/3-specs/component-gatekeeping/recipe-push-gatekeeping.md`+ tasks.md W2 節)。**review 通過才動 code。**
**動到的檔案(待 review**`cli/src/commands/recipe.ts`(push 加提醒 + 打通檢查)、確認 data-exfil hook 涵蓋 recipe push 路徑。
**驗收**
- `acr recipe push` 會產對外 webhook 的東西 → 印資料外流警示 + 要人類同意;非 TTY → 拒絕。
- `acr recipe push` endpoint 可達的 recipe → 回報「✓ HTTP 2xx」。
- `acr recipe push` 缺 credential → 回報「⚠️ 未打通:缺 credential」(誠實),仍允許 push。
- workflow 引用 recipe`component: kbdb_get`**不被任何 validate 步驟當假零件擋**。
### 🔴 Task 3README 重寫成單一路徑 — harness「事前提醒」
**目標**self-hosted 開源後,README 是外界 CC 唯一入口。砍掉「玩法一/二/三」三選一,講清楚單一正確路徑。
**子步驟**(改根 `README.md`):
1. 砍三選一玩法,留**一條路**`acr init --self-hosted` → 寫 workflowprimitive 串 + recipe)→ `acr push` → trigger。
2. 明示心智(呼應 mindset §1):「零件就這固定幾個由我們維護、不接受自製;要打外部 API 就寫 recipe;要編排就寫工作流。」
3. 連到 `.claude/rules/06-mindset.md` / arcrun-mindset Skill,讓 CC 一開始就有正確世界觀。
**驗收**:README 讀完,一個沒看過 arcrun 的 CC 知道:能用什麼、不能自製零件、打外部 API 要寫 recipe、怎麼 self-host 起來。
---
## 4. 今天「不要做」的(避免你走偏)
| 項目 | 為何不做 |
|---|---|
| 修 mira | richblack 明示不用管,mira 自己修 |
| 步驟2 `acr recipe test` / relay / credits | DECISIONS §3c 明確劃為服務側、非第一期 |
| 步驟6 搬家拆 matrix | 純 repo 整理,不影響 CC 能否用 |
| 砍 `injectCredentials` 舊路 / `BUILTIN_CREDENTIALS_MAP` | 獨立清理,不擋交付(DECISIONS §3b / BACKLOG「第一期之後」)|
| 新 primitive / Gherkin 真跑 / 入站認證 | richblack 已標「不要現在做」 |
---
## 5. 鐵律提醒(違反會被 hook block)
- 任何 code 變動前先讀對應 SDD + 在回覆開頭宣告(`.claude/rules/00-sdd-protocol.md`)。
- `registry/components/` 下禁止 TScypher-executor TS 禁止 credential/auth/JWT 業務邏輯(`.claude/rules/02-forbidden.md`hook 強制)。
- 每完成一個 task 立刻更新對應 tasks.md / BACKLOG.md 的 `[x]`,不批次。
- 誠實限制(mindset §7):stub / 未完成就標 unimplemented**不假綠**;完成 = 客觀證據(exit code / HTTP status + trace),不是口頭宣布。
---
## 6. 交棒:CLAUDE.block.md / README 表達優化(InkStoneCo 總管 2026-06-15
**背景**:總管與 leo 釐清 Mira 自製零件衝突時,實測一個現象——**連 Opus 讀著 `cli/harness/CLAUDE.block.md` + rules/,都把 recipe 誤解了四輪**(以為 recipe 是私有腳本、以為缺能力可用工作流拼裝補)。leo 的標準是「**Haiku 都能輕易理解**」。block 是被 `acr install-harness` 裝進各子 repo(含 mira)的**唯一入口**,細節雖在 `rules/` 但子 repo CC 預設只看 block → block 不夠清楚 = 下一個 CC 還會誤解。**這是表達缺口,不是讀者問題。**
**請 arcrun CC 做(改 `cli/harness/CLAUDE.block.md`,順帶對齊 §3 README**:把現有「不要自製零件 / 打外部 API 寫 recipe」兩句,補成「該做什麼 + 做完去哪 + 已違規的怎麼退場」。三個已核實的盲點要講清楚(論證在下,成品文案你按自己 rules 寫):
1. **recipe 是公共資產,不是私人腳本**`acr recipe push` = 投稿公共庫(`DEFAULT_PUBLIC_LIBRARY_URL`)。公私是**屬性**(UUID 身份模型,`cypher-executor/src/routes/recipes.ts` §7.5.5:身份=uuid、公私/author 是屬性、多作者版本並存),不是兩條路。**「發現缺某 stock recipe → 自己做一個 push 上去」是被鼓勵的,不必問人。** 現 block 只寫「寫 recipe」「不用部署」→ 讀者誤以為私有自用、不上傳(正好反了)。
2. **缺能力 → 補 API,不准用 recipe/工作流拼裝**。這條 rules 其實寫得很對(07-thin-shell §1 upsert 正反例、02-forbidden §5.2、01 §8),但 **block 完全沒提** → 子 repo CC 看不到。請把「缺單一 API 能力 = 去補 API endpoint,不拼裝」這條判準帶進 block(口訣:「換個介面要不要重寫?要→是能力該在 API」)。
3. **零件的退場路徑空白**。現 block 只說「不要自製零件」,沒說「**已經有自製零件的(如 mira 的 6 個)該怎麼辦**」。請補:已自製零件 → 映射到 stock(`claude_api`→自己做 AI 判斷別放工作流,mindset §2;`kbdb_*`→走已備的 `acr kbdb` 薄殼/MCP)或改寫成 recipe;無 stock 對應的單 API 缺口 → 寫 recipe 投稿。
**驗收(leo 標準)**:改完後,一個 **Haiku** 級 CC 讀 block 就能答對:recipe 要不要投稿(要)、缺 upsert 怎麼辦(補 API 不拼裝)、已有自製零件怎麼退場(映射 stock / 改 recipe)。
**連帶(非 arcrun 範圍,總管已記)**:「真正可複用的多步驟業務流程能否投稿共享」是 wishlist C6InkStoneCo 頂層),與「拼裝補 API 缺口」是兩回事(C6 紅線已劃清),arcrun 不必現在處理。
> 注:本交棒由總管寫**判準與論證**,**成品文案 + hook(若要)由 arcrun CC 按自己 rules 產出**。總管不直接改 arcrun 實作檔(職責邊界 D9)。