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
+19
View File
@@ -0,0 +1,19 @@
# 1. Vision — 設計哲學與前瞻
> 「為什麼」層:arcrun 的核心價值、設計目標、未來方向。
## 包含內容(待補充)
- arcrun 與傳統 low-code/n8n 的差異
- Haiku 就能搞定是設計目標,不是 Sonnet 妥協
- 薄殼原則:能力長在 API,介面只暴露
- 市場機制:recipe 信任靠量和星數,不靠人工審核
- 坦誠限制:不假綠、不偽造人類確認
相關詳細內容暫存於:
- `.claude/rules/06-mindset.md`(待搬來)
- `.claude/wiki/decisions-summary.md`(架構決策)
---
更新時間:2026-06-08
@@ -0,0 +1,99 @@
# SDD 協議(每次啟動必讀)
## 第零原則:沒讀 SDD 不准動 code
任何 `.go` / `.ts` / `.tsx` / `.wasm` 相關變動,**必須**按以下順序執行。**不得簡化,不得跳過**。
### 步驟 1:讀總進度
先讀 `docs/3-specs/arcrun/arcrun.md`,了解當前 Phase。
### 步驟 2:定位對應 SDD
根據任務性質找對應 SDD
| 任務類型 | 對應 SDD |
|---------|---------|
| Auth primitive WASM 零件(static_key/oauth2/service_account/mtls | `docs/3-specs/arcrun/credential-primitives-wasm/` |
| 清除 cypher-executor 裡的 TS 業務邏輯 | `docs/3-specs/arcrun/credential-primitives-wasm/` |
| WASI shim host functionskv_get / crypto_decrypt / crypto_sign_rs256 | `docs/3-specs/arcrun/credential-primitives-wasm/` |
| Auth Recipe 系統(recipe schema、KV 格式) | `docs/3-specs/arcrun/auth-recipe.md` |
| Landing Page | `docs/3-specs/arcrun/landing-page.md` |
| CLI / SDKPython/JS | `docs/3-specs/arcrun/sdk-and-website/` |
| arcrun-core-mvp 整體架構 | `docs/3-specs/arcrun-core-mvp/` |
| Platform Evolution | `docs/3-specs/arcrun-platform-evolution/` |
| Credential 長期規格(需求源) | `docs/user_requirements/credential_parts.md` |
`design.md``tasks.md` 兩份。
### 步驟 3:宣告(強制格式)
開始動手前,在回覆開頭**逐字**貼出以下宣告:
```
📋 已讀 SDD
- docs/3-specs/arcrun/arcrun.md(當前 Phase<phase 名稱>
- <對應 SDD 的 design.md 路徑>
- <對應 SDD 的 tasks.md 路徑>
🎯 本次對應 task<task 編號,例如 "Phase 1.3 實作 auth_static_key main.go">
📐 本次 task 的 SDD 規範摘要:
- <重點 1>
- <重點 2>
- <重點 3>
🚧 執行範圍:
- 會修改:<檔案清單>
- 會建立:<檔案清單>
- 會刪除:<檔案清單>
```
**不做這個宣告 = 違反 SDD 協議 = 停手等 richblack**
### 步驟 4check tasks.md 狀態
動手前:在 tasks.md 把對應 task 的 `- [ ]` 改成 `- [🔄]`(進行中標記)。
完成後:改成 `- [x]`,不批次更新,每完成一個就立刻改。
## 什麼算「任務超出 SDD 範圍」?
以下情況屬於 **change**,不是 **modify****必須停手並與 richblack 確認**
- SDD 沒寫到的新功能
- 新增頂層目錄
- 新增新的 Worker(不管是 cypher-executor / registry / 零件 worker
- 修改架構決策(例如「改用 xxx 取代 yyy」)
- 跨多個子系統的連鎖修改
**停手不是怯懦,是專業**。猜錯方向比慢一小時更糟。
## 新增 SDD 的完整程序(richblack 確認後怎麼往下走)
> 2026-06-03 補:之前協議只寫「停手等確認」,沒寫「確認後怎麼做」,導致 CC 被
> `pre-write-guard.sh` 規則 4.3 擋下後不知正路、卡死。這節補完整程序。
當「新增一個 SDD 子系統」(在 `docs/3-specs/` 下開新頂層目錄):
1. **停手,向 richblack 說明要新建哪個 SDD、為什麼**change,見上節)。
2. **取得 richblack 明確確認**後,依序:
1. **更新白名單**:在 `.claude/hooks/pre-write-guard.sh``KNOWN_SDDS` 陣列加一行
`"docs/3-specs/<新目錄名>" # YYYY-MM-DD richblack 確認新建(<一句用途>`
(這步是「執行 richblack 已授權任務的必要步驟」,不是 AI 擅自放寬 guardrail——
授權脈絡要明確,分類器才放行;沒有明確授權就改白名單 = 越界。)
2. **建目錄 + 寫 design.md / tasks.md**(白名單放行後才寫得進去)。
3. **若沒先更新白名單就 Write** → 規則 4.3 會擋你(這是對的,表示你跳了步驟 2.i)。
**為什麼白名單而非全放行**:開新 SDD 子系統 = 宣告新架構範圍,是稀有的人類決策點。
白名單讓「AI 自己無中生有開子系統」必停下(AI 改不了白名單,除非 richblack 明確授權該任務)。
已許可的目錄內寫檔零摩擦。
## 發現 SDD 本身有問題怎麼辦?
- SDD 和實作不一致 → 停手,列出矛盾點,與 richblack 確認哪一邊是對的
- SDD 規範之間互相矛盾(例如禁令 A 和設計 B 衝突)→ 停手,引用矛盾原文,與 richblack 確認
- **不可以自行猜哪個是對的**。CC 之前兩天就是這樣走錯的。
## 為什麼這個協議存在
arcrun 規範已經足夠細緻,CC 之前出錯不是因為不懂,而是因為**沒讀**或**讀了覺得「大概是這個意思」就動手**。SDD 協議強制把「先讀 → 定位 → 宣告 → 執行 → 更新」做成一條死規矩,沒有繞過去的路徑。
@@ -0,0 +1,76 @@
# 技術棧硬限制
## 三層語言對應(絕對不可混用)
| 層級 | 語言 | 位置 | 職責 |
|-----|------|------|------|
| 零件(Component | **TinyGo 或 AssemblyScript → WASM** | `registry/components/{name}/` | 所有業務邏輯 |
| 零件 Worker 包裝 | TypeScript(固定模板,不寫業務邏輯) | `.component-builds/{name}/` | WASI shimstdin/stdout JSON |
| Orchestration Worker | TypeScript + Hono | `cypher-executor/` | HTTP routing、workflow 執行排程、host functions |
| CLI | TypeScript + Node.js | `cli/` | `acr` 指令 |
| Python SDK | Python | `python-sdk/` | HTTP thin wrapper + client 端加密 |
| JS SDK | TypeScript + Web Crypto | `js-sdk/` | HTTP thin wrapper + client 端加密 |
| Frontend | React 19 + Vite + Tailwind v4 | `landing/` | Cloudflare Pages |
## 零件實作規範
### 只有兩種合法語言
- **TinyGo**`tinygo build -target=wasi -o {name}.wasm main.go`
- **AssemblyScript**`asc main.ts --target release -o {name}.wasm`
### I/O 模型
- **stdin**JSON input
- **stdout**JSON output
- 不用 HTTP server,不監聽 socketWASI preview1 沒 socket
### Host Functions(零件呼叫外部能力的唯一管道)
`u6u` namespace 下:
| Host Function | 用途 |
|---|---|
| `u6u.http_request` | 發 HTTP 請求 |
| `u6u.kv_get` | 讀 Cloudflare KVWorker 側依 key 前綴路由到正確 KV) |
| `u6u.crypto_decrypt` | AES-GCM 解密(encryption key 永不暴露給 WASM |
| `u6u.crypto_sign_rs256` | RSA-SHA256 簽章(PKCS8 bytes 傳入) |
**所有 host function 在 `cypher-executor/src/lib/wasi-shim.ts` 實作**。零件透過 WASI import 使用。
## 資料儲存
| 儲存 | 用途 | Key 格式 |
|-----|------|---------|
| Cloudflare KV `WEBHOOKS` | workflow 定義(cypher binding YAML | `webhook:{api_key}:{name}` |
| Cloudflare KV `CREDENTIALS_KV` | 加密 credential | `{api_key}:cred:{name}` |
| Cloudflare KV `RECIPES` | auth recipe / 動態 API recipe | `auth_recipe:{service}`, `rec_{hash}` |
| Cloudflare KV `USERS_KV` | 用戶資料 | `user:{api_key}` |
| Cloudflare KV `SESSIONS_KV` | session | `session:{token}` |
| Cloudflare KV `ANALYTICS_KV` | 執行分析 | `execution:{timestamp}:{id}` |
| Cloudflare KV `EXEC_CONTEXT` | workflow 執行中的 context | `ctx:{execution_id}:{node_id}` |
| Cloudflare R2 `WASM_BUCKET` | **只用於用戶自製零件**Phase 5 以後啟用) | `{api_key}:cmp:{hash}` |
**警告:R2 不存平台內建零件的 WASM**。平台零件已 bundle 進各自的 Worker binary`[[wasm_modules]]``import ... assert { type: 'webassembly' }`)。
## 加解密規範
- **演算法**AES-GCM 256-bit
- **加密位置**Client 端(CLI / Python SDK / JS SDK
- Python`cryptography` 套件
- JSWeb Crypto API`crypto.subtle`
- **解密位置**Server 端 **WASM primitive**(透過 host function `crypto_decrypt`
- cypher-executor TS **不解密**,只提供 host function
- `ENCRYPTION_KEY` 只在 Worker host function 內部讀取,**永不經 stdin / 回傳值傳給 WASM**
- **傳輸格式**`{ name, encrypted, iv }`iv base64、encrypted base64
## 網路部署
- **平台 API(對外)**`cypher.arcrun.dev`cypher-executor
- **Landing**`arcrun.dev`
- **每個零件 Worker**
- **對內(cypher-executor 用來 fetch component,避開同 zone 死鎖)**`arcrun-{kebab}.{WORKER_SUBDOMAIN}.workers.dev`
- 例:`arcrun-kbdb-get.uncle6-me.workers.dev`
- cypher-executor 從 `wrangler.toml [vars] WORKER_SUBDOMAIN` 組出此 URL
- **對外(可選,零件對全網開放被 curl 用)**:`{kebab}.arcrun.dev`
- 例:`gmail.arcrun.dev``kbdb-get.arcrun.dev`
- 仍允許保留,但**禁止 cypher-executor 透過此 URL fetch**(會撞同 zone 自循環,見 [docs/incidents/2026-05-13-cypher-outbound-522.md](../../docs/incidents/2026-05-13-cypher-outbound-522.md)
- **新增 component worker 部署清單**`name = "arcrun-{kebab}"` + `[[routes]]` 對外(可選)+ dashboard 啟用 workers.dev(必須)
- **部署工具**Wrangler
@@ -0,0 +1,153 @@
# 禁止行為清單(零容忍)
**這份清單由 `.claude/hooks/*.sh` 強制執行。違反會 block 工具呼叫(exit 2**
---
## 第一類:零件實作層級的禁令
### 1.1 禁止在 `registry/components/` 下建立 TypeScript 檔案
零件**只能**用 TinyGo`.go`)或 AssemblyScript`.ts` 但需 `asconfig.json`)實作,並編譯成 `.wasm`
cypher-executor/registry Worker 或 `.component-builds/` 內的 TS 不算零件邏輯,那是 WASI shim。
**Hook 會擋**:新增 `registry/components/*/{檔案}.ts`(除非目錄內有 `asconfig.json` 明確標記為 AssemblyScript)。
### 1.2 禁止建立新的 `auth_*` 目錄以外的 auth 實作
所有 auth 邏輯只能在:
- `registry/components/auth_static_key/`
- `registry/components/auth_oauth2/`
- `registry/components/auth_service_account/`
- `registry/components/auth_mtls/`
**不可以**出現 `cypher-executor/src/auth-primitive/``cypher-executor/src/lib/auth-*.ts``auth-worker/``credential-worker/` 等目錄。
**Hook 會擋**`mkdir``Write` 到上述違規路徑。
### 1.3 禁止用 `wrangler init/generate` 建立 auth/credential/jwt 相關的 TS Worker
Auth primitive 必須透過 `component-worker-template/` 搭配 WASM binary 部署。
**Hook 會擋**bash 指令含 `wrangler (init|generate) ... auth_``... credential_``... jwt_` 的 pattern。
---
## 第二類:cypher-executor TS 的禁令
### 2.1 禁止新增任何 credential / auth / jwt 相關的 TS 檔案
**已存在但要刪**Phase 1-3 範圍):
- `cypher-executor/src/actions/credential-injector.ts` → 刪除(走 WASM auth primitive
- `cypher-executor/src/lib/jwt-signer.ts` → 刪除(RS256 移入 auth_service_account WASM
- `cypher-executor/src/lib/component-loader.ts``BUILTIN_API_RECIPES``BUILTIN_CREDENTIALS_MAP` → 整段刪除
**Hook 會擋**:新增任何路徑含以下關鍵字的 `.ts` 檔案:
- `credential-injector``credential_injector`
- `jwt-signer``jwt_signer`
- `auth-dispatcher` 的 TS 若嘗試在裡面實作 credential 解密 / template 展開 / JWT signingblock
### 2.2 禁止在 cypher-executor 任何 TS 裡實作以下邏輯
這些邏輯全部屬於 WASM 零件職責:
- AES-GCM 解密(`crypto.subtle.decrypt`)— 只准出現在 `wasi-shim.ts``crypto_decrypt` host function
- RSA-SHA256 簽章(`crypto.subtle.sign` with RSASSA-PKCS1-v1_5)— 只准出現在 `wasi-shim.ts``crypto_sign_rs256` host function
- Template 展開(`{{secret.X}}` / `{{runtime.X}}` 替換)— 只能在 WASM 零件內
- PEM → PKCS8 解析
- JWT header/payload/signature 組裝
- Token exchange(拿 service account JWT 換 access_token
- 具體 API call 實作(例如 gmail send / telegram sendMessage / google sheets append
**Hook 會擋**
- Write/Edit 到 `cypher-executor/src/` 下的 `.ts` 時,內容含:
- `crypto\.subtle\.decrypt` 且檔名不是 `wasi-shim.ts`
- `crypto\.subtle\.sign.*RSASSA` 且檔名不是 `wasi-shim.ts`
- `interpolateTemplate``\{\{secret\.` 的模板邏輯
- `BUILTIN_API_RECIPES``BUILTIN_CREDENTIALS_MAP`(新增用)
- `gmail.googleapis.com/gmail/v1/users/me/messages/send` 類 hard-code API URL
- `api.telegram.org/bot.*sendMessage`
- `sheets.googleapis.com/v4/spreadsheets`
- `notify-api.line.me/api/notify`
### 2.3 cypher-executor TS 的合法職責(允許)
- HTTP routingHono routes
- workflow 執行排程(`graph-executor.ts`
- 呼叫 WASM 零件(透過 HTTP fetch 到對應 Worker URL,或 Service Binding fallback
- 提供 host function`wasi-shim.ts``kv_get` / `crypto_decrypt` / `crypto_sign_rs256`
- KV/R2/Service Binding 存取封裝
---
## 第三類:架構層級的禁令
### 3.1 禁止新增 Service Binding
**Cypher binding 不是 Cloudflare service binding**。它是 YAML/KV 裡的 URL 清單。
零件串接(workflow 層)一律走 HTTP URL,不走 `[[services]]`
13 個現有的 `SVC_*` 綁定(`cypher-executor/wrangler.toml`,邏輯零件)是歷史遺產(效能優化),**保留但不新增**。
> **2026-06-06 註(credential-primitives-wasm Phase 7**self-hosted 的 cypher 與 auth worker 同在 `{sub}.workers.dev` zonecypher `fetch()` 打 auth 觸發 CF **same-zone 1042**(壓測階段 11)。**未用 service binding 解**(評估後廢:service binding 靜態、加/改要重 deploy cypher)。改用 **`global_fetch_strictly_public` compatibility flag**cypher wrangler.toml)讓 same-zone fetch 走公網前門 → 同 zone 也通,**auth 維持 HTTP fetch、不加 binding**。故本禁令不變。
**Hook 會擋**bash 指令含 `wrangler tail` 以外、涉及 `[[services]]` 新增的 patternEdit wrangler.toml 新增 `[[services]]` 區塊時警告確認。
### 3.2 禁止以「從 R2 取 WASM」為設計
平台內建零件已 bundle 進各自 Worker,不從 R2 取。
R2 只在 Phase 5(用戶自製零件)啟用。
**Hook 會警告**TS 中出現 `env.WASM_BUCKET.get(` 的新增 code(除非在明確標註的 Phase 5 user-submit 路徑中)。
### 3.3 禁止複製貼上 Worker 程式碼到新目錄
要改 `gmail` 零件 → 改 `registry/components/gmail/main.go`,重新編譯、部署。
**不准**新建 `gmail-v2/``new-gmail/``gmail-worker/` 等目錄。
**Hook 會擋**`mkdir``Write``{component-name}-v2/``new-{component-name}/``{component-name}-worker/` 類路徑。
### 3.4 禁止在 SDK 內做 server 職責
- **禁止**SDK 裡做 server 端解密、credential-injector 重實作、workflow executor、auth recipe 解析
- **允許**SDK 做 HTTP thin wrapper + client 端加密(AES-GCM
---
## 第四類:流程層級的禁令
### 4.1 禁止沒讀 SDD 就動 code
`00-sdd-protocol.md`
### 4.2 禁止批次更新 tasks.md
每完成一個 task 就立刻 mark `- [x]`。不准「先全部做完再一次更新」。
### 4.3 禁止新建 SDD 而不事先與 richblack 確認
SDD 屬於架構決策,必須人確認。CC 不可以自行在 `docs/3-specs/` 底下建新目錄。
例外:在現有 SDD 目錄內新增 `requirements.md` / `design.md` / `tasks.md` 的單檔補充(需在 CLAUDE.md 已註記的 SDD 範圍內)。
---
## 第五類:薄殼原則的禁令(詳見 07-thin-shell.md
### 5.1 禁止在薄殼介面(cli/src/、arcrun-mcp/src/)實作業務邏輯
能力只實作一次,放在 APIcypher-executor 端點)。薄殼只做介面轉換 + 暴露。
**Hook 會擋**(規則 7.x,範圍 `cli/src/``arcrun-mcp/src/` 的 .ts/.js):
- 7.1 新增 `seedApiRecipes` / `seedAuthRecipes` / `seedRecipes` 編排函式(seed 是 API 行為,§4.1 反例)
- 7.2 介面層拼裝 upsert(同函式內 PATCH + POST + upsert/找則改否則建 字樣)
- 7.3 client 端「全部成功才做下一步」gate(`deployFullyOk` 類,§4.1 反例)
### 5.2 禁止用 recipe / 客製零件補 API 缺的能力
缺能力 → 去補 API endpoint,不是在 recipe 層拼湊或用零件繞過(hook 偵測不到語意,靠 code review)。
### 5.3 禁止同一 API 能力在不同介面用不同參數簽名 / 連不同帳號
`validate` 在 CLI 吃 YAML、MCP 卻要 `api_key`+`graph` = 底層分歧。薄殼差異只能來自介面慣例。
所有薄殼讀同一份身份來源(見 07 §4)。
---
## Hook Block 訊息格式
當 hook 擋住一個操作時,訊息格式統一為:
```
❌ BLOCKED by arcrun CLAUDE rules
違反項:<禁令編號,例如 2.2>
原因:<簡短說明>
正確做法:<該改去哪裡、該用什麼方式>
參考:.claude/rules/<對應檔案>
```
這樣 CC 拿到錯誤訊息後有機會自行導正,不是被擋死就愣住。
@@ -0,0 +1,165 @@
# 零件架構與部署模式(必讀,CC 最常搞錯的地方)
## 第一核心概念:每個 WASM 零件 = 一個獨立 Worker = **兩個** URL
**不是**從 R2 即時載入 WASM 執行。
**不是**用 service binding 串零件。
**不是**一個 Worker 裡跑多個零件。
**是**:每個零件都是獨立部署的 Worker,每個都有**兩個 URL**
| URL 類型 | Pattern | 用途 |
|---|---|---|
| 對內(cypher-executor 用)| `arcrun-{kebab}.{WORKER_SUBDOMAIN}.workers.dev` | cypher-executor fetch component 走這個,避開同 zone 自循環死鎖(P0 #9|
| 對外(直接 curl 用,可選)| `{kebab}.arcrun.dev` | 用戶單獨打 component 測試或 self-hosted 用法 |
例:`kbdb_get` 零件:
- 對內:`arcrun-kbdb-get.uncle6-me.workers.dev`cypher-executor 走這個)
- 對外:`kbdb-get.arcrun.dev`(用戶 / 直 curl
**為什麼這樣設計**CF Workers 「同 zone 自循環防護」會讓綁 `cypher.arcrun.dev/*` 的 cypher-executor fetch 同 zone `*.arcrun.dev` 撞 522。完整事件報告:[docs/incidents/2026-05-13-cypher-outbound-522.md](../../docs/incidents/2026-05-13-cypher-outbound-522.md)。改走 workers.dev 子域繞過。
### 零件 Worker 的結構
```
registry/components/{name}/
├── main.go ← TinyGo 原始碼(實際零件邏輯)
├── component.contract.yaml ← 輸入/輸出規格
└── {name}.wasm ← TinyGo 編譯產物
```
部署時,透過 `component-worker-template/` 把 WASM 包進一個 Hono Worker
```
.component-builds/{name}/
├── package.json
├── wrangler.toml ← name = "arcrun-{name}"route = "{name}.arcrun.dev"
├── component.wasm ← 從 registry/components/{name}/ 複製過來
└── src/index.ts ← 固定的 WASI shimPOST / → stdin → WASM → stdout → JSON
```
**src/index.ts 是通用模板**,所有零件都用同一份。這個 TS 只做 WASI runtime,不是業務邏輯。
---
## R2WASM_BUCKET)的真正用途
R2 存 WASM 只是**用戶自製零件上傳**用的。
**平台內建零件不從 R2 讀取**——它們在部署時就已 bundle 進 Worker 的 binary(透過 `[[wasm_modules]]``import` with `assert { type: 'webassembly' }`)。
Phase 5(封測後)才會啟用「用戶 push 自製零件 → 存 R2 → 動態執行」這條路徑。
**結論:當 CC 問「怎麼從 R2 取出 WASM」時,幾乎都是走錯路徑**。平台零件是獨立 Worker,走 HTTP 呼叫,不是 R2 動態載入。
---
## Cypher binding 的正確定義
**Cypher binding 不是 Cloudflare 的任何 binding 機制。**
Cypher binding 是一張 YAML 清單,內容是「一個 workflow 要呼叫哪些零件 URL」。存放在:
- 本地:`workflow.yaml`(用戶寫的 workflow
- KV`WEBHOOKS` KV(用戶 `acr push` 後存入)
Cypher executor 執行 workflow 時:
1. 從 KV 讀出 workflow YAML
2. 按 graph 順序解析每個節點的 `component`
3. 用 HTTP fetch 打對應的零件 URL
4. 把 output 當作下個節點的 input
**這就是 Cypher binding——用 HTTP URL 把零件串起來,存在 YAML/KV 裡**
### 為什麼不能用 Service Binding
Service binding 需要 `wrangler.toml` 裡寫死 `[[services]]`,且要 redeploy 才生效。arcrun 是類 n8n 服務,用戶建立新 workflow 時**絕對不可能**要他 redeploy。所以 workflow 層一定要 HTTP。
### Service Binding 的僅存合法用途
只在 `cypher-executor` 和**平台內建邏輯零件之間**保留(效能優化,避免公網往返)。看 `cypher-executor/wrangler.toml` 裡的 13 個 `[[services]]` 綁定就是這個用途。
**禁止新增任何 Service Binding**。所有新零件(含 auth primitive)都走 HTTP URL 路徑。
**same-zone 1042 的解(credential-primitives-wasm Phase 72026-06-06**self-hosted 的 cypher 與 auth worker 同在 `{sub}.workers.dev` zonecypher `fetch()` 打 auth 觸發 CF **1042**(官方 docs:「fetch from another Worker on the **same zone**」;官方 cypher 在 `cypher.arcrun.dev`、打 `*.workers.dev` 屬跨 zone 故不踩——非官方有 flag)。**解法不是 service binding**(評估後廢:靜態、加/改要重 deploy),而是 cypher wrangler.toml 加 **`global_fetch_strictly_public` flag**——讓 same-zone fetch 走公網前門 → 同 zone 也通。auth 維持 HTTP fetch、不加 binding。官方加此 flag 行為不變(本就跨 zone),self-host 被修好 → **官方與 self-host 共用同一份 toml**
**仍禁止**:為**用戶自製 / 服務專屬零件**(`gmail-worker``notion-worker` 之類)新增 binding——那些是 recipe 的事,不該有 binding。**workflow 層(用戶串零件)一律 HTTP URL 不變。**
---
## 零件之間怎麼串:實際流程
假設 workflow 是:webhook → gmail(要 auth)→ google_sheets(要 auth
```
用戶 POST https://cypher.arcrun.dev/webhooks/named/xxx/trigger
cypher-executorWorker)讀 workflow YAML
├─ 節點 1: component = gmail
│ a. 查 auth_recipe:gmail → primitive = static_key
│ b. HTTP POST https://auth-static-key.arcrun.dev
│ { action: "authenticate", api_key, service: "gmail" }
│ → 回傳 { auth_headers: { Authorization: "Bearer ..." } }
│ c. HTTP POST https://gmail.arcrun.dev
│ { to, subject, body, _auth_headers }
│ → gmail 零件 Worker 執行 WASM → 回傳 { success, data }
└─ 節點 2: component = google_sheets
... 相同模式
```
**cypher-executor 本身不做 credential 解密、不做 JWT signing、不做 auth header 組裝**。這些全在 auth primitive WASM 零件內,cypher-executor 只負責 HTTP routing 和工作流排程。
---
## 實際禁令(CC 看這裡)
### 禁止在 `registry/components/` 下建立 TypeScript 檔案
零件邏輯一律 TinyGo 或 AssemblyScript,編譯成 `.wasm`
### 禁止把 auth 邏輯寫在 `cypher-executor/src/` 裡
credential 解密、JWT signing、template 展開(`{{secret.X}}`)全部屬於 auth primitive WASM 零件的職責。cypher-executor 只呼叫它們。
### 禁止問「怎麼從 R2 取 WASM」
平台內建零件**不從 R2 取**。每個零件已部署成獨立 Worker,走 HTTP URL。用戶自製零件才用 R2(Phase 5,未啟用)。
### 禁止新增 Service Binding
13 個現有的 SVC_*(邏輯零件)是歷史遺產,不新增。新零件(含 auth primitive)一律走 HTTP URL。self-hosted 的 same-zone 1042 用 `global_fetch_strictly_public` flag 解,不靠新增 binding(見上「合法用途」段,Phase 7)。
### 禁止重建已存在的零件 Worker
要改 `gmail` 零件邏輯 → 改 `registry/components/gmail/main.go`,重新編譯 `.wasm`,重新部署對應 Worker。**不要**在 `cypher-executor/src/lib/` 或其他地方建「新的 gmail 實作」。
---
## 部署一個新零件的完整步驟(auth_static_key 為例)
1. 建立 `registry/components/auth_static_key/`
- `main.go`TinyGo 實作)
- `component.contract.yaml`IO 規格)
2. 編譯:`cd registry/components/auth_static_key && tinygo build -target=wasi -o auth_static_key.wasm main.go`
3. 建立 `.component-builds/auth_static_key/`
- 複製 `component-worker-template/src/index.ts`
- 複製 `component-worker-template/package.json`
- 新建 `wrangler.toml`
```toml
name = "arcrun-auth-static-key"
main = "src/index.ts"
compatibility_date = "2025-02-19"
[vars]
COMPONENT_ID = "auth_static_key"
[[routes]]
pattern = "auth-static-key.arcrun.dev/*"
zone_name = "arcrun.dev"
```
- 複製 `auth_static_key.wasm` 到此目錄為 `component.wasm`
4. `cd .component-builds/auth_static_key && pnpm install && pnpm deploy`
5. **Dashboard 啟用 workers.dev URL**(必須,否則 cypher-executor fetch 不到):
- Workers & Pages → `arcrun-auth-static-key` → Settings → Domains & Routes → workers.dev → Enable
- 啟用後 URL`arcrun-auth-static-key.{WORKER_SUBDOMAIN}.workers.dev`
6. 驗證對外:`curl https://auth-static-key.arcrun.dev` → 應回 `{ok: true, component: "auth_static_key"}`
7. 驗證對內:`curl https://arcrun-auth-static-key.{WORKER_SUBDOMAIN}.workers.dev` → 應同樣回 200
8. cypher-executor 透過 `wasmWorkerUrl()` 自動組對內 URL 呼叫(不用手動註冊)
**這是唯一正確的部署流程**。任何偏離這個流程的「替代方案」都要先和 richblack 確認。
**Step 5 為什麼必須**:見 arcrun.md P0 #92026-05-13)。cypher-executor 走對內 URL 避開同 zone 自循環死鎖;若 workers.dev 未啟用,cypher-executor fetch 該 component 會 404。
@@ -0,0 +1,77 @@
# 當前進度(SessionStart 會注入此檔重點)
> 更新時間:2026-04-19
> 權威來源:`docs/3-specs/arcrun/credential-primitives-wasm/tasks.md`
> 此檔僅摘要,詳細狀態以 tasks.md 為準。
---
## 封測狀態
**原定明天封測,richblack 決定推遲**,原因:cypher-executor 有三套 TS 業務邏輯違反「零件一律 WASM」架構原則(Phase 1-3 要清除的程式碼),在清除前不封測。
---
## 目前 PhaseCredential Primitives TS → WASM
**SDD 位置**`docs/3-specs/arcrun/credential-primitives-wasm/design.md` + `tasks.md`
### 已完成
- **Phase 0.10.5**:核心合併(u6u-core 併入 arcrun、21 個零件 contract 完整、刪除重複 `credentials/` 目錄、CREDENTIALS_KV binding 確認、刪除 `matrix/u6u-core/`
- `registry/components/` 下 21 個零件(邏輯 + API)都有 `main.go` + `.wasm`
### 進行中 / 未完成
| Task | 狀態 | 阻擋關係 |
|-----|------|---------|
| 0.6 wasi-shim 新增 `kv_get` / `crypto_decrypt` / `crypto_sign_rs256` host functions | ⬜ 未開始 | **Phase 1-3 的硬前置** |
| 0.7 component-loader 新增 WASM runner 路徑 | ⬜ 未開始 | **Phase 1-3 的硬前置** |
| 1.1-1.8 `auth_static_key` WASM 零件(TinyGo | ⬜ 未開始 | 涵蓋 80% 服務 |
| 2.1-2.6 `auth_service_account` WASM 零件(JWT signing | ⬜ 未開始 | Google Service Account 等 |
| 3.1-3.5 清除 `component-loader.ts``BUILTIN_API_RECIPES` | ⬜ 未開始 | 要先有 Phase 1-2 的 WASM 零件 |
| 4.1-4.4 `auth_oauth2` + `auth_mtls`(封測後) | ⬜ 未開始 | 非阻擋項 |
| 5.1-5.7 核心穩定驗證(全域搜尋確認無殘餘 TS) | ⬜ 未開始 | 封測啟動門檻 |
### Phase 1-3 要**徹底刪除**的 TS 檔案(不是搬、不是改,是刪)
| 檔案 | 違反什麼 |
|-----|---------|
| `cypher-executor/src/actions/credential-injector.ts` | AES 解密、template 展開、JWT 邏輯 —— 應在 WASM |
| `cypher-executor/src/lib/jwt-signer.ts` | RS256 JWT 簽章邏輯 —— 應在 `auth_service_account.wasm` |
| `cypher-executor/src/lib/component-loader.ts``BUILTIN_API_RECIPES`~100 行) | gmail/telegram/line/gsheets/http_request/cron 的 TS 實作 —— 應全部走對應 WASM 零件 |
---
## 下一個 session 第一件要做的事
**讀 `docs/3-specs/arcrun/credential-primitives-wasm/tasks.md`**,然後決定從 Phase 0.6 還是 0.7 開始。
0.6host functions)和 0.7WASM runner)是並列的前置工作,哪個先都可以,但都要在 Phase 1 開始之前完成。
---
## SDD 索引
| 子系統 | SDD |
|--------|-----|
| **主要(正在動)** Credential Primitives WASM 改寫 | `docs/3-specs/arcrun/credential-primitives-wasm/` |
| **LI (LLM Interface)** — AI 操盤手使用體驗(2026-05-16 新建,mira dogfood 痛點轉化) | `docs/3-specs/llm-interface/` |
| arcrun 總進度 | `docs/3-specs/arcrun/arcrun.md` |
| Auth Recipe 系統(schema、預建 20 個服務) | `docs/3-specs/arcrun/auth-recipe.md` |
| Landing Page | `docs/3-specs/arcrun/landing-page.md` |
| SDK + Website | `docs/3-specs/arcrun/sdk-and-website/design.md` |
| arcrun MVP 整體 | `docs/3-specs/arcrun-core-mvp/design.md` |
| Credential 長期規格(需求源) | `docs/user_requirements/credential_parts.md` |
| Platform Evolution | `docs/3-specs/arcrun-platform-evolution/design.md` |
| Tech Stack 詳細 | `docs/3-specs/tech.md` |
---
## 技術備註(CC 常搞錯的點)
1. **每個 WASM 零件 = 獨立 Worker = 公開 URL**(例:`gmail.arcrun.dev`)。不是從 R2 動態讀。
2. **Cypher binding = YAML 裡寫 URL 清單**。不是 Cloudflare service binding。
3. **cypher-executor 只做 routing + host functions**。業務邏輯全在 WASM 零件。
4. **TinyGo 有限制**`crypto/rsa` 支援不全 → 用 host function `crypto_sign_rs256` 讓 Worker 代簽。
5. 詳見 `.claude/rules/03-component-architecture.md`
@@ -0,0 +1,60 @@
# arcrun mindset(給 CC 的世界觀 — 走歪前先讀這個)
> 這是「為什麼」層,不是禁令層(禁令在 02-forbidden)。
> 違反禁令會被 hook 擋;違反 mindset 不會被擋,但會讓你做出「技術上對、方向上錯」的東西。
> 來源:2026-05 richblack 與 CC 的一連串判準釐清(權威記錄在 DECISIONS.md)。
>
> 註:這是步驟 7「arcrun-mindset Skill」的前身。Skill 正式建立前,本檔是 CC 的 mindset 來源。
---
## 1. 工作流是 default,零件是稀有例外
AI 開發時的預設順序:
1. **預設寫工作流**(串服務 / 自用 / 給少數人用 → 全部先工作流,用 http_request + 流程控制 primitive 串)。
2. **要打的服務不提供串接但有 API,且有必要讓全 arcrun 生態重用** → 才建零件(零件 = API 薄殼,只打一個 endpoint)。
3. **建零件前必問「你有必要嗎?」**:看到「有 API 可包成零件」≠「該包」。自用 → 工作流。
**CC 的典型走歪**:把「需要一個能力」直接翻成「做一個零件」,把「能包」當「該包」。
mira 的 claude_api / km_writer 就是這樣被錯做成零件的(其實是自用服務膠水,該是工作流)。
## 2. Arcrun 是 AI 呼叫的工具(AI → 工具),不是工具回頭呼叫 AI
需要 AI 判斷 / 轉換時,是**操盤的 CC 自己做**,再呼叫工作流做確定性的下一步。
**不要在工作流裡放零件回頭呼叫一個 LLM**。n8n 需要 AI 節點是因為它沒大腦;arcrun 的大腦就是 CC。
ai_transform_compile/run 因此被刪除。)
## 3. arcrun 不做授權判斷
「能不能打通」由發 API key 的服務裁決,不是 arcrun。401/403 是對方服務在行使授權,不是 arcrun 的 bug。
auth_recipe 只定義「怎麼認證」,不含「誰准用」清單。不要加「arcrun 替用戶擋掉某些 endpoint」的功能。
## 4. 零件投稿走 GitHub PR(人 merge = 人類閘門)
零件投稿不是 registry self-service,是 GitHub PR。人 merge = 天然人類閘門(AI 偽造不了 GitHub approve),
把關(假零件偵測 / 純WASI / Gherkin)由 CI PR check 跑(CI 能 runtime 跑 wasmCF Worker 不能)。
§8「不依賴 CI」指執行鏈路(高頻);零件投稿稀有,走 PR/CI 是例外、不違反。
## 5. 發佈安全的底氣是純 WASI 沙箱,不是 Gherkin
Gherkin 全綠 ≠ 零件安全(投稿者可寫避重就輕的 Gherkin)。真正框死破壞力的是**純 WASI 沙箱**
(零件只能 stdin→stdout、無網路 syscall、無檔案系統)。Gherkin 驗契約 + 沙箱框死 + 市場補長尾 = 風險可控,非零風險。
## 6. 暴露 / 送資料的動作 → 人類明示同意(資料外流警示)
把資料 / workflow 變成「可被外部呼叫」(部署 webhook、recipe push= 暴露面 → 需人類明示同意,不分公私庫。
**不禁止**用戶公開(他的自由),但要**確定他自己明示同意**(不是 AI 替他決定)。
警示同時是「保護措施入口」(提示可加 API Key / 權限 / 限流)。
## 7. 誠實限制(最重要的 mindset:不假裝、不假綠)
- **AI 技術上能偽造人類確認**confirmed_by_human、exposure_consent、gherkin_evidence 都能塞)。
這些機制的價值是**法律歸責 + 軌跡可審**,不是技術防偽。**絕不在文件 / 程式裡聲稱「不可能繞過」。**
- **絕不代替人類做有風險的確認**(建零件、暴露資料)。非 TTY(你直跑)就拒絕,不要自己塞 flag 假裝人類同意了 —— 那是明確越界。
- **禁假綠**DECISIONS §3c/§7):stub / 未實作就回 success:false 或明確標 unimplemented,不要回傳假資料假裝成功。
缺 credential 打不到 2xx 就誠實標「未驗收:缺 X」,不 mock 充綠燈。
- **完成 = 客觀證據**(編譯 exit code / HTTP status + trace),不是口頭宣布「我做好了」。
---
詳細判準與來龍去脈見 `DECISIONS.md`。每條都有對應的慘痛教訓,不是憑空規定。
@@ -0,0 +1,133 @@
# 薄殼原則(鐵律)— 能力長在 API,介面只暴露
> 來源:`docs/壓測報告.md` §5.4/§5.5(設計者本人於壓測中釐清)+ DECISIONS §1。
> 違反此原則的典型後果:每改一個能力要同步多份介面、介面間漂移(壓測 §5.1:CLI 改了讀
> 全域/專案/.env,MCP 沒跟上,兩者打不同帳號)、能力被某個介面綁架後別的介面用不到。
> 這條由 `.claude/hooks/pre-write-guard.sh` 規則 7.x 部分強制(見下「hook 強制範圍」)。
---
## 0. 一句話
**所有能力(business logic)只實作一次,放在 APIcypher-executor HTTP 端點)。
CLI / MCP / Python lib / JS lib 全是薄殼:只做「介面轉換 + 暴露」,不含任何商業邏輯。**
```
CLI ─┐
MCP ─┤ ← 全是薄殼:參數解析 / 格式轉換 / 暴露,不含商業邏輯
Python lib ─┤
JS lib ─┘
↓ 全部呼叫同一個
┌──────────────────────────────┐
│ API(唯一真相,能力都在這) │
└──────────────────────────────┘
```
---
## 1. 什麼是「能力下沉到 API」(正例 vs 反例)
### 正例:upsert
-**API 提供 `upsert` 端點**(內部 GET 找→有則 update 無則 insert)。CLI/MCP/lib 只呼叫它。
- ❌ 在 MCP 裡自製「先 call update API、失敗再 call insert API」的拼裝邏輯。
- ❌ 在 recipe 層拼湊 upsertrecipe/零件補 API 缺的能力 = 走歪;正解是補在 API)。
### 正例:seed recipe(壓測 §4.1 的反例修正)
-**API 在「部署/註冊完成」時保證 recipe 就緒**(seed 是 API 行為,由一個端點完成 `POST /init/seed`)。
-**種子資料(清單)放 server**`cypher-executor/src/lib/*-seeds.ts`。「裝好後預設有哪些 recipe」
是 API 的能力,種子資料是這能力的一部分。薄殼只呼叫 `/init/seed` 一次。
- ❌ 在 CLI `init.ts` 裡用迴圈 POST 11 個 recipe + 客戶端「全部成功才 seed」的 if 判斷
(這正是 §4.1 seed 永遠不被 seed 的根因:邏輯被寫進了某個介面)。
> **種子資料檔(`*-seeds.ts`)是一個普遍類別,不是某個零件的特例。**
> 它含 endpoint / `{{template}}` 字串(recipe 的資料欄位),rule 02 §2.2 hook 對**整類** `*-seeds.ts`
> 豁免 endpoint/template 檢查——因為那是「資料宣告」不是「呼叫實作」。新增任何 `xxx-seeds.ts`
> 自動適用,**不需為個別零件/recipe 改 hook**richblack 原則:不為單一零件改全域規則)。
### 判準口訣
> **「這段邏輯換一個介面(CLI→MCP)要不要重寫?」**
> 要重寫 → 它是能力,該在 API。
> 不用重寫(只是把 API 回傳值換個格式印出來)→ 它是薄殼該做的事。
---
## 2. 薄殼「允許」做的事(窮舉)
1. 解析介面慣例的輸入(CLI 吃檔案路徑 / MCP 吃 JSON 參數)→ 轉成 API 期望的 payload。
2. 呼叫 APIHTTP fetch / service binding)。
3. 把 API 回傳值轉成該介面的輸出格式(CLI 印彩色文字 / MCP 回 structured JSON)。
4. **client 端加密**(AES-GCM)——唯一例外,因 API 期望收到已加密 payload(見 rule 01 加解密)。
5. 讀取「身份設定」(哪個帳號 / 哪個 cypher URL)——但所有薄殼必須讀**同一份**身份來源
(見 §4 統一帳號來源)。
## 3. 薄殼「禁止」做的事
1. ❌ **在薄殼介面(CLI/MCP/lib)裡用多個 API 呼叫拼裝**出一個 API 沒有的能力(upsert / seed / 任何 N-step 編排)。
> **界線(2026-06-26 收窄,issue #4)**:禁的是「**把編排邏輯寫進介面層 TS**」,不是禁「用資料方式(workflow/code-node)自救」。
> 自家 API 缺能力 → 補進 API(你能改);**第三方 API 缺能力 → 走 workflow/code-node 補丁是合法的**(你改不了第三方 API,不能被規則卡死)。詳見 §3.5 自力救濟階梯。
2. ❌ **用零件**補 API 缺的能力 → 污染零件庫(缺能力 → 先看 §3.5 階梯,真需新穩定能力才走零件 PR)。
> 這條的原始精神(要保留):當初是 AI 把多步驟工作寫成**零件**污染零件庫,才訂此禁令。**禁的是「亂建零件」,不是「禁止任何補丁」。**
3. ❌ 寫死判斷來補 API 缺口(例:`deployFullyOk` 那種 client 端 gate)。
4. ❌ 同一 API 能力在不同介面用不同參數簽名(`validate` 在 CLI 吃 YAML、在 MCP 卻要 `api_key`+`graph` = 底層分歧,違反「同一 API」)。差異只能來自介面慣例(檔案路徑 vs 字串),不能來自底層實作。
5. ❌ 任一薄殼連的帳號 / 後端與別的薄殼不同(CLI 連自架、MCP 連平台 = 違反「同一 API」前提)。
---
## 3.5 缺能力時怎麼補:自力救濟階梯(普世規則,issue #4)
> **問題**:§3 舊版預設「缺能力 → 去補 API」,這**預設 API 是你能改的**。對**第三方 API**(如 Google Sheets:一次只能倒全部、輸出前無法 filter)不成立——若 Google 不開該 API、規則又禁用 workflow「倒出來自己篩」,用戶被自己的規則卡死。
> **解法**:把「補丁」分層,維持零件庫最小,但開放「用資料方式自救」的合法路徑。
**主界線(一句話)**:**「那個 API 你能不能改?」** 自家 API 缺能力 → 補 API;第三方 API 缺能力 → workflow/code-node 補丁。
| 情況 | 正解 | 為何 |
|---|---|---|
| 能打既有 API | **recipe**(沒有就建 recipe) | 單一 API 呼叫的封裝 |
| **自家** APIKBDB / cypher)缺能力 | **補進 API** + 可同時發 issue | 你能改,能力該長在 API |
| **第三方** API 缺能力(gsheets filter / 無 upsert API | **可投稿的 workflow 補丁** + 發 issue 建議原廠加 API | 你改不了第三方 API,但不能被卡死 |
| 非 call-api 的純計算(如整篇文章轉大寫) | **code-node**(空白 code 零件內寫 JS | recipe/workflow 都做不到,又不該為此建一堆專用零件 |
| 真需新穩定能力(極少數) | 自建零件 → PR | 維持零件庫最小,只有非用零件不可才建 |
**三個配套原則**
1. **補丁 workflow 可像 recipe 一樣被呼叫,但明示它是 workflow、且可投稿**(呼應 wishlist C6「工作流即零件」)。讓 AI 一遇阻就「用資料方式」自救,而非建零件。
2. **code-node**:原廠不提供某純計算時,AI 至少能用一個空白 code 零件寫 JS 自救(呼應 wishlist C1;架構決策:JS 在 CF Workers isolate 跑,不嵌 QuickJS/Rust)。
3. **補丁是過渡**:原廠出 API 後,補丁 workflow 因效能較差自然被減少使用、淘汰。
**upsert 範例**:有些服務原廠提供 upsert API(→ recipe 直接打),有些沒有(→ 做一個 upsert workflow 達成,**而非建專用零件**)。§1 把 upsert 當「該補進 API」的正例——那**只對自家 API 成立**;對改不了的第三方 API,arcrun 端永遠補不進去,正解是 workflow 補丁。
> **與 hook 的關係**:§3.1 禁的「介面層拼裝」由 `pre-write-guard.sh` 7.x 擋(範圍 `cli/src/`、`arcrun-mcp/src/` 的 TS)。**workflow/code-node 補丁是資料產物(YAML / 空白零件內的 JS),不是介面層 TS → 本就在 hook 範圍外,合法不被擋。** hook 防線不變,本次只釐清「資料方式自救」是合法路徑。
---
## 4. 統一帳號來源(薄殼共用同一身份)
所有薄殼讀**同一份**身份設定:
- self-hosted`~/.arcrun/config.yaml` / 專案層 `.arcrun.yaml` / `ARCRUN_*``CLOUDFLARE_*` env(見 `config-layering.md`)。
- standard:平台 api_key。
**MCP 目前的已知違反**(壓測 §5.2):MCP 用 Cloudflare service binding 焊死平台 `arcrun-cypher-executor`
self-hosted 用戶用 MCP 連不到自己的 cypher。修法見
`docs/3-specs/arcrun/sdk-and-website/mcp-account-source.md`SDD proposal)。
---
## 5. 出貨順序(最低出貨標)
- **CLI + MCP 兩個薄殼先到位**AI 偏好 MCP,故 MCP 不可長期落後),且兩者覆蓋**同一組 API 能力**。
- Python / JS lib 隨後補。
- 出貨順序由「介面被誰用」決定,不是由「哪個好做」決定。
- **介面進度本來就會不一致**(薄殼模型的預期狀態)——這本身不是 bug。
bug 是「**底層 API 能力不齊 / 介面含了不該含的邏輯 / 帳號來源不統一**」這三者。
---
## 6. hook 強制範圍(與「靠人判斷」的邊界)
`pre-write-guard.sh` 規則 7.x 能擋的是**語法層可偵測**的反例:
- CLI/MCP 檔案內出現「迴圈 POST 多個 recipe」「先 update 失敗再 insert」這類拼裝 pattern 的明顯特徵 → 警告/擋。
- 新增 `seedApiRecipes` / `seedAuthRecipes` 這類「seed 邏輯寫在介面層」的函式 → 擋(改去 API)。
**hook 擋不了的**(需 CC 自律 + code review):
- 把商業邏輯藏在看似無害的 helper 裡。
- recipe 層拼裝(recipe 是資料,hook 不解析語意)。
→ 故本檔是 mindset,hook 是底線;兩者都不可省。誠實限制見 mindset §7(不假裝 hook「不可能繞過」)。
+43
View File
@@ -0,0 +1,43 @@
# 2. Architecture — 系統設計 + 技術棧 + 決策
> 「是什麼」層:arcrun 的設計、技術棧、硬限制、關鍵決策。
## 快速導航
### 硬限制 & 規範
| 檔案 | 用途 |
|------|------|
| **01-tech-stack.md** | 三層語言、資料儲存、加解密、網路部署(硬規範) |
| **02-forbidden.md** | 禁止行為清單(hook 強制執行) |
| **03-component-architecture.md** | 零件架構:R2/binding/URL 正確定義 |
| **00-sdd-protocol.md** | SDD 讀取協議(流程強制) |
| **04-current-progress.md** | 當前 Phase + SDD 索引 |
### 設計原理
| 檔案 | 用途 |
|------|------|
| **06-mindset.md** | 設計哲學 7 條:為什麼做這些選擇 |
| **07-thin-shell.md** | 薄殼原則鐵律(§1-6 原則 + hook 強制範圍) |
| **05-deploy-convention.md** | 部署慣例(掃描式 workflow、lockfile、WASM 來源) |
### 決策記錄
| 檔案 | 用途 |
|------|------|
| **decisions/** | 架構決策歷史(搬來中) |
---
## 常見查詢
- **「零件怎麼部署?」** → 03-component-architecture.md 部署步驟
- **「為什麼不用 service binding?」** → 03-component-architecture.md + 決策/same-zone-1042
- **「怎樣設計新功能?」** → 06-mindset.md §1 + 07-thin-shell.md
- **「禁止清單是什麼?」** → 02-forbidden.md
- **「技術棧限制有哪些?」** → 01-tech-stack.md
---
更新時間:2026-06-08
@@ -0,0 +1,398 @@
# Arcrun 決策記錄(DECISIONS.md
> 這份檔案記錄 Arcrun 的**穩定決策**:架構定義、核心原則、為什麼這樣設計。
> 它很少改。任何 AI 或人接手 Arcrun,先讀這份。
> 流動的待辦在 `BACKLOG.md`。
>
> `DECISIONS.md` 不是寫完就死的。想法變了要回來改它,讓它跟上——
> 怕的不是改,是「想法變了但檔案沒跟著改」,那它就變成說謊的文件。
>
> 最後更新:2026-05(第一期規劃期間整理)
---
## 0. 設計哲學
**第一條:每多依賴一個不可掌控的第三方,就多一個單點故障。**
Arcrun 賣的本質是「減少你對不可控第三方的依賴」。核心開源(MIT,公司倒了還能 fork)、
支援 self-hosted(跑在你自己的 CF,甚至自己的 wazero)、workflow 是純文字你自己擁有。
但「減少依賴」不等於「零依賴」——每個依賴要問:它掛了,我有沒有退路?有退路的依賴可接受。
**第二條:解耦 / 原子化。**
什麼都要能單獨換掉、不被綁死。primitive 與 recipe 解耦、執行與發現解耦、
引擎與零件解耦。AI 時代變化快,「可改」本身就是核心價值。
**第三條:Arcrun 是 AI 用品,但設計目標是「讓 AI 不可能做歪」。**
管住 AI 的開發痛點,和要賣給用戶的產品,是同一件事。
---
## 1. 架構:primitive 與 recipe
**Arcrun 只有四種 WASM primitive**
1. 流程控制(if / switch / filter / foreach / try_catch / wait …)
2. 文字 / 資料處理(string_ops / number_ops / array_ops / date_ops / set / merge …)
3. http_request(打任意 HTTP
4. credential(四個 auth WASMauth_static_key / auth_service_account / auth_oauth2 / auth_mtls
**其他一切都是 recipe。** recipe = http_request + 一組固定設定(YAML 文字)。
| | primitive | recipe |
|---|---|---|
| 是什麼 | WASM Worker | YAML 文字 |
| 要不要 deploy | 要 | 不用 |
| 多久變一次 | 幾乎不變 | 一直長新的 |
**判準(真零件 vs 假零件):** 一個零件若滿足任一條,它是假零件,該降級成 recipe:
- contract 或原始碼出現具體外部服務的 URL / domain
- 它宣告的能力是 http_request 的子集(打某固定 endpoint
→ D1 / KV / Vectorize / Supabase / KBDB 的存取,**一律是 http_request + recipe**
絕不做 `d1_crud``kbdb_get` 這種「假零件」。
### 「recipe」有三種,不要混用這個詞
Arcrun 裡有三個都叫 "recipe" 的東西,職責不同:
| 名稱 | 是什麼 | KV key |
|---|---|---|
| **API recipe** | http_request + endpoint/method/headers/body 模板(`RecipeDefinition` | `recipe:{canonical_id}` |
| **auth recipe** | 認證設定:primitive + base_url + 注入規則(`AuthRecipeDefinition` | `auth_recipe:{service}` |
| **prompt recipe** | LLM prompt 的封裝,與 KBDB block 有關 | `prompt_recipe:{name}` |
一般講「recipe」= API recipe。假零件降級的終點是 **API recipe**`recipe:{id}`)。
降級作業不碰 auth recipe、不碰 prompt recipeprompt recipe 犍涉 KBDB block 展開,
是 BACKLOG 待決策項,不在此範圍)。
### recipe 與 primitive 的驗收標準不同(早期已定,2026-05 重新確認)
- **primitiveGherkin 通過 = 驗收通過。** primitive 是封閉的邏輯,正確性不依賴外部世界,
可以用「given / when / then」確定地驗證。
- **recipe:打得通(2xx= 驗收通過。** recipe 是「指向外部 API 的指針」,正確性一半在定義
(打不通就代表定義錯,「打通」已驗)、一半在外部服務當下的行為。
**關鍵認識:recipe 不用 Gherkin,不是偷懶,是 Gherkin 對 recipe 沒用。**
「2xx 但外部服務沒真的做事」「外部服務改了 API」——這些是 recipe 唯一的真實風險,
而 Gherkin 一樣擋不住(Gherkin 測的當下沒改就過,之後改了它早跑完了)。
能補這個風險的只有「執行 → 回報 → 修正」的市場機制,不是任何靜態驗收。
給 recipe 加 Gherkin = 花成本做一件不會多驗到任何東西的事。
recipe 的「語義正確性」(真的刪了那列嗎)交給市場:A 的 recipe 在 B 那裡失效,
B 的 AI 會因為「目的沒達成」去查、去修、提修改版。Arcrun 不監控全世界的 API 變動。
**例外——可以是 primitive 的「引擎內建能力」:** 若某能力不依賴任何外部 endpoint、
是 Arcrun 執行環境本身自帶的(如「workflow 中途暫存」),它可以是 primitive。
判準:「這段邏輯依不依賴外部服務的 endpoint?」依賴→recipe;引擎自帶→可 primitive。
### 工作流是 default,建零件要過人類閘門(2026-05 補,CC 把自用服務錯做成零件後定)
AI 開發時的預設順序:
1. **預設寫工作流**(串服務 / 自用 / 給少數人,都先工作流)。default、阻力最小。
2. **零件的正當時機**:服務不提供串接但有 API,且**有必要讓全 Arcrun 生態重用** → 才建零件
(零件 = API 薄殼,只打一個 endpoint)。
3. **建零件 = 過人類閘門**。看到「有 API 可包成零件」≠「該包」,先問「你有必要嗎?」。
AI 不可自行建零件,必須 (a) 經人類互動確認;(b) 明示舉證「為何工作流做不到」
(舉證責任在 AI,預設假設工作流能做)。把關點在「建立零件的 API」本身——
CLI / MCP / Python lib / JS lib 四路全收斂到這關。
**為什麼要人類閘門**:零件進公共庫 = 全生態都能打它。自用服務(通訊錄 / 帳本)沒設驗證就變零件
= 公開後門。安全 / 意圖機器判不了,必須人看。規範會忘、hook 不會(§7:判準寫成機械紅燈)。
**不限制自由**:別人要建零件是他的自由(開無驗證服務給人串也是),唯一硬約束「零件 = endpoint 薄殼」。
閘門不是禁止,是「要建得先說服人 + 舉證」的摩擦。
**ABC 三管齊下讓 AI 不選難路**:A 審核當場擋(§7 層二)+ B 工作流範本好寫(§7 層一)
+ C mindset 明示預設(§7 層三)。人類閘門是第四道,專擋意圖 + 安全。
原理:難路走的當下要痛、易路選的當下要爽、事先有聲音說易路是 default。
CC 把自用服務錯做成零件,正因這三者當時全缺。
---
## 2. TS 邊界規則(哪些程式碼能用 TS,哪些不能)
**判準:這段程式碼是「執行引擎 / 工具本身」,還是「一個零件該做的事」?**
- 引擎 / 工具(cypher-executor 解析 graph、CLI 讀 YAML、registry 驗收)→ **TS,第一期合法**
- 零件該做的事(打 API、處理資料、做認證)→ **必須 WASMTinyGo),用 TS = 犯規**
**靠位置判斷,不靠肉眼判斷內容:**
- `registry/components/` 底下出現 `.ts` = 犯規(該目錄只准 .wasm + contract.yaml
- `cypher-executor/src/` 底下的 `.ts` = 第一期合法(引擎程式碼,Tier 1/2 部署於 Cloudflare
本來就用 TS)。注意:這是「引擎邏輯可用 TS」,**不是**「引擎永遠是 Cloudflare 專屬」。長期見 §4。
**為什麼零件必須 TinyGo 不能 TS:** TS/JS 編不出獨立輕量的 WASI .wasm——
JS 需要一個 JS 引擎來跑,塞進 wasm 體積爆炸。TinyGo/AS/Rust 直接編譯成
自包含、只依賴 WASI 標準介面的 wasm1080KB)。這是三層 runtime 的物理前提(見 §4)。
TinyGo 為官方首選(語法與 TS 差異夠大,AI 不易把純 TS 邏輯誤搬)。
---
## 3. 開源 / 商業邊界
| 開源核心(MIT) | 服務側(付費 / 需 API Key |
|---|---|
| cypher-executor、四種 primitive WASM、CLIacr)、registry | KBDB 語義搜尋、KBDB graph 查詢、Persona / Mira 等 |
**KBDB 採 Supabase 模式:** KBDB 的 recipe 顯示在公共零件庫(能力可見=引子),
要用就申請 API Key(註冊=轉化),用爽了付費。arcrun.dev 已有「取得 API Key」入口。
**「執行」與「發現」解耦:**
- 執行:永遠本地、免費、可離線。不依賴公共庫。
- 發現(語義搜尋):連線加值。self-hosted 用 local 關鍵字搜本地下載過的;
連公共庫才能用 KBDB 語義搜尋(在大集合裡查意圖)。
**cypher-executor 裡的 KBDB = 污染(清);公共零件庫的 KBDB = 服務(留,但不在第一期)。
---
## 3b. credential 是引擎能力(不是用戶零件)
(基於 2026-05 查核 graph-executor / credential-injector / auth-dispatcher 原始碼)
credential 的注入不是一個 workflow 節點做的事。它是**執行引擎在「呼叫零件之前」
自動做的一個步驟**graph-executor.executeNode 的流程是:
組 ctx → 注入 credential → 才 runner(ctx)。零件拿到 ctx 時 credential 已被放進去,
零件自己不知道 credential 怎麼來的。
**credential 的本質 = KV 裡的加密值 + 一段「在零件執行前注入 ctx」的引擎逓輯。**
那四個 `auth_*` WASMstatic_key / service_account / oauth2 / mtls)是「注入步驟的後端」,
不是用戶會在 workflow 擺的零件。用戶永遠不會直接呼叫 `auth_static_key`——
是 auth-dispatcher 在背後呼叫它。
→ 「零件白名單」「假零件判準」不適用於 `auth_*`,它們不是用戶零件。
**credential 系統現状是「新舊兩路並存」的半成品:**
- 新路(對的方向):`auth-dispatcher` → HTTP 打 `auth_*` WASM,解密/JWT 全在 WASM 內。
已支援 static_key / service_account / oauth2mtls 尚未(Phase 4)。
- 舊路(要砸):`injectCredentials` TS 裡解密,含 `BUILTIN_CREDENTIALS_MAP`
註解自認 Phase 1.9 將刪除。砦舊路是獨立清理,**不擋降級**(見 BACKLOG)。
**注入靠 `auth_recipe:{componentId}` 觸發。** 一個服務要能被注入 credential
必須有對應的 auth recipe。KBDB 用 static_key,而 static_key 新路已支援
→ 降級 KBDB 的 credential 前置是「小」的(只需建一個 `auth_recipe:kbdb`)。
**修正一個舊裁決:** API recipe 的 `credentials_required` 欄位 **要留**
`makeRecipeRunner`(零件執行)不讀它,但 `injectCredentials`(零件執行前的注入步驟)
會讀它。credential 不是在 runner 裡處理,是在 runner 之前那一步處理。**
---
## 3c. execute vs test:意圖決定路徑(服務側,不在第一期)
一個 AI 開發 recipe,有兩種意圖,對應兩個指令:
- **只打算自己用** → 用 `execute`。直連目標 API**不經過 arcrun**self-hosted 純粋。
- **打算公佈到公共零件庫換 credits** → 從開發的第一次打就用 `test``test` 明示走 arcrun relay。
**關鍵:用 `test` 這個動作本身,就是「我打算公佈」的意思表示。**
AI 用 test 開發時,每一次打都經過 relay,arcrun 當下就看到真實打通記錄——
不需 AI 事後交一份自己寫的 log(執行者不能驗證自己,見 §7),也不需 arcrun 事後重打
(重打 delete 要自備測試環境,跟開發者工作重複、荒謬)。
### test relay 經手 credential — 誠實處理
`test` 走 relay,請求裡帶著 credential。**relay 為了轉發給目標 API,必須在內部
持有明文 credential 一瞬間——這是 proxy 的物理本質,加密絕對絍不過。**
(加密只能保護「客戶→relay」傳輸途中防監聽;relay 內部必然看得到明文。)
唯一誠實的處理方式(四道合起來才站得住):
1. **明示告知**`acr` 第一次用 test 就告訴用戶「test 會讓請求(含 credential)經過 arcrun relay
arcrun 只記錄 HTTP 回應、不記錄 credential。要完全不經過請用 execute」。
2. **「不記錄」從「承諸」升級成「可驗證」**:relay 是開源的(arcrun 核心 MIT),
用戶不需「相信」,可以讀 relay 原碼確認它沒記錄 credential。
relay 程式上:credential 欄位全程不寫日誌、不寫存儲,只在轉發那一瞬間的記憶體。
3. **傳輸層 TLS**:客戶 → relay → 目標全程加密,防線路監聽。
4. **縮小經手 credential 的價值**:用 test 開發時建議用測試帳號的 credential,不是生產環境的。
**絕不做「假加密」**——不要讓用戶以為連 relay 都看不到 credential。
兩段分開誠實講:「傳輸途中加密(防監聽)+ relay 內部短暫持有明文(開源可驗證、不記錄)」。
**範圍:** `test` / relay / credits 這整套是服務側工程,依賴公共零件庫與 credits 系統存在。
**不在第一期**(第一期是 self-hosted 能跡、`execute` 能用)。
---
## 4. cypher-binding、三層 runtime、執行核心(長期,現在不做)
> 註:credential 是引擎能力、不是第四種 primitive。詳見下方 §3b(基於 2026-05 原始碼查核)。
### cypher-binding 是什麼
workflow 不是「部署出來的東西」,是「一張可隨時改的紙」——紙上寫一排零件 + 順序 + 條件。
執行核心讀紙:跑第一個,跑完回來問紙,紙說下一個是誰,就跑下一個。
對比 service bindingCF 機制,a/b 都要 deploy):cypher-binding **不 deploy**
改 workflow 零部署成本。可能慢一點,但「寫好、按下去就跑」(像 n8n)。
### 三層 runtime 目標
零件與執行核心目標是能跑在三種環境:
- Tier 1Cloudflare Workers(現況)
- Tier 2:企業自架 workerd(不信任 CF 雲)
- Tier 3:極輕量 WASI runtimewazero,無人機 / 邊緣設備)
**現在不寫任何 wazero / workerd 程式碼。** 第一期沒有 Tier 2/3 用戶。
避債的方式不是「現在做三層」,是「現在不要做任何擋死三層的決定」。
### cypher-executor 概念上分兩層
(現在糊在一起。這**不是會累積的債**——它是一支程式、就一支,未來一次性重構即可;
不像零件會複利累積。它是「待設計的未來解法」,不需要為它焦慮。)
- **(1) 執行核心** — 讀紙、依序/依條件呼叫零件。應能編成純 WASI,跑三層任何地方。
- **(2) Cloudflare 整合層** — webhook / KV / cron / Service Binding / HTTP 路由,只服務 Tier 1/2。
Tier 3(無人機)只需要 (1)。現在不拆,但**從現在起新程式碼要有意識地把
「讀紙、呼叫零件的核心邏輯」和「Cloudflare 特有存取」分開寫,不要糊得更死**。
### KV 依賴的根源,與「紙要自包含」
現在 cypher-executor 依賴 KV,根源**不是**「紙存在 KV」,是「紙上寫的是 hash
cmp_xxx / rec_xxx),要查 KV 才能翻譯成真正的零件」。
**hash 查表不是會累積的債**——它是 component-loader 裡固定的一段邏輯,零件再多它也不變
(同一段邏輯處理更多資料 ≠ 需要更多段邏輯)。但它讓「紙」無法自包含。
Tier 3 的正解**不是**「帶一個 SQLite 上無人機來翻譯 hash」——那只是把依賴從 KV 換成
SQLite,沒有消除依賴。正解是**紙本身自包含**:紙上直接寫 URL / 內嵌 recipe YAML
不寫 hash。無人機上只有「自包含的紙 + WASM 零件」,不需要任何查表設施。
- hash = Tier 1 的**儲存格式**KV 去重 / 版本管理,Tier 1 保留無妨)
- 自包含 = **執行格式**
- 中間隔一個「展開」步驟:打包給無人機時做一次,不是執行時做(類似 `acr push` 的轉換)
**附帶好處:** 自包含的紙人類 / AI 可直讀——這正是 Arcrun 核心賣點(紙人人可讀)。
hash 其實偷偷腐蝕了這個賣點。
---
## 5. 第一期範圍鎖定
**第一期用戶 = 會開 CF 帳號的開發者,在 VSCode 用 Claude CodeCC),self-hosted。**
鎖定 Claude,因為所有「防做歪」機制都是針對 Claude 行為校準的;
Gemini / Codex 服從度不同,第一期不支援。
**第一期做:** 清污染 → 降級假零件成 recipe → 補零件庫真把關 →
白名單 hook → (搬家)→ mindset Skill → README 重寫成單一路徑 → acr init --self-hosted。
**第一期明確不做:** SaaS、API Key 多租戶、小白 onboarding、視覺化的圖、
arcrun-gui 拖拉畫布、arcrun-mcp 命名大改、新 primitive、Gemini/Codex 支援、
三層 runtime、KBDB 訂閱層。
**SaaS 解凍條件(不靠心情):** (a) self-hosted 有 ≥3 個你以外的人部署成功並貢獻
≥1 個 recipe,且 (b) 視覺化 Skill 已驗證「AI 畫的圖能讓非技術者看懂」。
---
## 6. CF 帳號與專案模型
- **CF 帳號:一個人一個,永遠就一個。** 不隨專案增加。「實驗環境」是一個 prefix,不是一個帳號。
- **Arcrun 部署:一套就夠**(像 n8n 就一套)。不是每個專案各一套。
- **「專案」是 Arcrun 的一等公民**:專案 = 一組「引用 workflow」的三元組關係(存在 KBDB / 三元組儲存,
不需要新資料表)。同一個 workflow 可被多個專案引用,改一次全部生效。
- 預設共用 workflow(引用);只有結構性差異才 fork,且 fork 要有摩擦感。
- credential 綁專案:KV key 帶專案前綴(`mira:notion_token`),邏輯隔離,非物理隔離。
---
## 7. 「讓 AI 不做歪」的三層機制 + 閉環
**閉環原則:下指令的、執行的、驗證的,必須是三個不同的角色。**
執行者驗證自己 = 沒有驗證。驗證的標準必須來自執行者碰不到、改不了的地方
(一份判準 / 一個程式 / 一個拿著判準的獨立角色)。
閉環:指令(說要什麼,不說怎麼做)→ 查閱判準 → 執行 → 獨立驗證 → 不合格退回並告知正路。
**事前防禦(擋已知的錯):**
- 層一:範本——AI 不從白紙生成,永遠在改一個正確的範本(acr new / scaffold
- 層二:會回嘴的 CLI——走歪當場 exit 2 + 指回正路(這是真護城河)
- 層三:mindset Skill——給 AI「Arcrun 很簡單、一切皆 recipe」的世界觀
**事後機制(抓漏網的錯):** 事前防禦永遠堵不滿,剩下交給事後:
- 第一層:可審計軌跡——每個動作留不可竄改記錄,事後能追
- 第二層:不變式測試——核心原則寫成自動測試,每次 commit 跑(最重要)
- 第三層:定期獨立審查——拿判準重看,審查者手上必須有判準
**關鍵心態:不要訓練自己的「辨識能力」**——不可靠、會累、無法轉移給 low-code 用戶。
要把判準寫成機械測試,讓「紅燈」代替「辨識」。同一個錯誤,以「問句」形式出現你抓不到,
以「紅燈」形式出現你一定抓到。
---
## 8. 執行鏈路不依賴 GitHub Actions(零件投稿例外,2026-05-30 釐清)
Arcrun 第一期的**執行鏈路**init / push / run / recipe)全在「用戶機器 + Cloudflare」之間,
不經過 GitHub Actions。這是常態高頻動作,用戶不該被 CI 卡住。理由見 §0 第一條。
**零件投稿是例外,走 GitHub PR + CI2026-05-30):**
- 零件投稿是**稀有低頻**事件(primitive 極少、未來絕大部分是 recipe;建零件要過人類閘門)。
- 稀有事件用 PR 治理最自然:**PR 必須有人 merge = 人類閘門**AI 偽造不了 GitHub approve);
把關(假零件偵測 / 純WASI / Gherkin / 覆蓋檢查)由 **CIPR check)跑**
- 為何非 CI 不可:**CF Workers 禁止 request-time 編譯 WASM** → registry Worker 跑不了 Gherkin / 向量;
CI 有 tinygo + 能 runtime 跑 wasm,是唯一既能跑 wasm 又「執行者碰不到」的 venue。
- **這不違反 §8 精神**:§8 防的是「高頻執行鏈路被 CI 卡住」;零件投稿稀有、且該由 PR 把關,
用 PR/CI 反而更對。兩者是不同性質的事。
- 詳見 `docs/3-specs/component-gatekeeping/`
---
## 9. 自力救濟階梯——缺能力時怎麼補(2026-06-26issue #4
§3.1 舊版「缺能力 → 去補 API」預設 **API 是你能改的**。對**第三方 API**(gsheets 一次倒全部、輸出前無法 filter;
無 upsert API 的服務)不成立——若規則又禁用 workflow「倒出來自己篩」,**用戶被自己的規則卡死**。
**結論**:不廢 §3.1,**收窄**成「禁在薄殼介面層 TS 拼裝 + 禁亂建零件污染零件庫」,並開放「用資料方式自救」的合法路徑。
**主界線(一句話)**:**「那個 API 你能不能改?」**
| 情況 | 正解 |
|---|---|
| 能打既有 API | recipe |
| **自家** APIKBDB/cypher)缺能力 | 補進 API + 可發 issue |
| **第三方** API 缺能力 | **可投稿的 workflow 補丁** + 發 issue 建議原廠加 API |
| 純計算(轉大寫等) | **code-node**(空白 code 零件寫 JS |
| 真需新穩定能力(極少) | 自建零件 → PR |
**配套**:補丁 workflow 可呼叫且明示可投稿(wishlist C6)/code-node 在 CF Workers isolate 跑 JSwishlist C1)/
補丁是過渡(原廠出 API 後自然淘汰)。**hook 不變**:7.x 擋的是「介面層 TS 拼裝」,workflow/code-node 是資料產物、
本就在 hook 範圍外、合法不被擋。**code-node 規則已定,空白零件實作屬 wishlist C1 另案。**
原始精神保留:當初訂此禁令是防 AI 把多步驟工作寫成**零件**污染零件庫——禁的是亂建零件,不是禁任何補丁。
落地:`docs/2-architecture/07-thin-shell.md §3.5` + `.claude/rules/07-thin-shell.md`(兩份同步)+ `02-forbidden.md §5.2`
---
## 10. embedding 是 base optional 模組(2026-06-26issue #7 / mira-dissolve T2.4
KBDB 查詢三模式:關鍵字(base LIKEalways)/語義(embed 模組)/圖(graph 插件,另 repo)。**語義歸 base 的 optional 模組**。
**設計鐵則**
- **binding 開/關,不拆 repo**:有 `VECTORIZE`+`AI` binding 才啟用(`embedEnabled()`);沒有 → 降級 LIKE**API 不變**。
- **不裝保持輕**(free-tier 友善):預設關;`acr init` 問、預設 N。
- **精耕非地毯式**:只 embed 被標 `metadata_json.embed:true` 的 entrywiki 段落 + node gloss),不對每個 block 灌。
- **base 對內容語意無知**:用通用 `embed:true` flag 觸發,**不寫死 entry_type 白名單**(否則 base 知道 wiki/graph 概念 = 破壞解耦)。
- **不動三表**:只標既有 `is_embedded` bookkeeping 欄(base 從不讀);向量存 Vectorize,不進 D1 表。
- **誠實降級 + 發現閉環**`?mode=semantic` 沒開 vectorize → 回 keyword + `capability_hint`「叫 CC 幫你開」。不假裝有語義。
- **CC 幫開路徑**CC 寫 config `kbdb_embed:true` + `acr update`deploy `ensureVectorizeIndex` REST 建 index 冪等 + 注入 binding)。
模型 `@cf/baai/bge-base-en-v1.5`768 維,cosine)。落地:`kbdb/src/embed.ts` + entries route + config.ts/deploy.ts/init.ts + MCP `kbdb_search mode`
詳見 kbdb-base SDD Phase 12。**與 #5.1 source 過濾協調**:語義 search 的 metadata filter 共用 source,語義/關鍵字都能按來源篩。
---
## 11. #5 KBDB 缺口分流 + 頂層覆寫(2026-06-26issue #5,普世框架視角)
mira dogfood 開的缺口,**Mira 已蒸發 → 當「未來任何 self-hosted 用戶都會撞的框架缺口」處理**,不為 Mira 特例。四點分流:
- **source 過濾 ✅做**:普世成立(按來源篩 + 語義/關鍵字都會用)。json_extract 零建表(見 mistakes #18)。
- **documents 聚合(GROUP BY page_name)❌不做**:舊河道頁特例;新架構「跨 vault 的圖」走 **graph MCP** traverse/neighbors
不靠 KBDB 出 SQL 聚合端點。**頂層 mira-dissolve R6 已否決**。
- **cypher proxy DELETE ⏸擱置**:依賴頂層「死資料自動刪除原則」(mira-dissolve T8 未定)。
且裸 `DELETE /entries/:id` **無 owner 檢查**,直接 proxy 暴露=跨租戶刪除風險 → 補時要先讀 entry 驗 owner_id 才放行
(與 #6 對 records DELETE 同理擱置)。
- **embed-on-write → 併 #7**(embed 模組職責,前端不該手動戳 process-page)。
- **能力對照文件**`docs/4-guides/kbdb-capabilities.md`**刻意不寫「documents/process-page 待移植」**(舊河道視角,新架構不移植)。
**通用教訓**:碰「舊 Mira/SaaS KBDB 開的需求」先查頂層 mira-dissolve 是否已重審覆寫,別照舊 issue 悶頭做(可能做白工)。
---
## 附:如何判斷「某個東西會不會累積成債」
通用判準——當「東西」變多時:
- **同一段邏輯處理更多資料** → 不累積。(例:hash 查表,零件再多也是同一段查表邏輯)
- **需要更多段邏輯、更多特例** → 會累積。(例:零件,每個是獨立程式、各自可能出錯)
不累積的東西不需要焦慮,它頂多是「未來一次性處理的設計點」。
會累積的東西必須在「進入點」就把關(例:零件在投稿時就驗純淨)。
@@ -0,0 +1,30 @@
# [主題] — Architecture Decision Record
> 日期:[YYYY-MM-DD]
> 狀態:[提議中 / 已採納 / 已廢棄]
> 影響範圍:[哪些子系統 / 模組]
---
## 背景
[遇到了什麼問題,需要做這個決定?]
## 決定
**[結論,一句話。]**
## 原因
[詳細說明為什麼這樣決定。]
## 放棄的選項
| 選項 | 放棄原因 |
|------|---------|
| [選項 A] | [原因] |
| [選項 B] | [原因] |
## 影響與後續
[這個決定影響哪些地方?有什麼技術債或需要注意的事?]
+50
View File
@@ -0,0 +1,50 @@
# 3. Specs — 需求 + SDD + 進度
> 各子系統的設計文檔 + 任務清單。
## 進行中(必讀)
| 目錄 | Phase | 狀態 |
|------|-------|------|
| **arcrun/credential-primitives-wasm/** | Phase 0-1 | 進行中(0.6-1 |
| **arcrun/kbdb-base/** | Phase 7 | 上線完成 + 待處理(§8) |
## 主線
| 目錄 | 內容 | 狀態 |
|------|------|------|
| **arcrun-core-mvp/** | MVP 整體架構 | 基礎完成 |
| **arcrun/auth-recipe.md** | Auth recipe 系統 | 穩定 |
| **arcrun/landing-page.md** | Landing page | 設計中 |
| **arcrun/sdk-and-website/** | SDK + MCP + 文檔 | 進行中 |
| **arcrun-platform-evolution/** | 長期演化規劃 | 路線圖 |
## 安全 & 基礎設施
| 目錄 | 用途 |
|------|------|
| **component-gatekeeping/** | 零件投稿 gatekeepingPR + CI |
| **component-registry-canon/** | registry 規範化(dead storage cleanup |
| **data-exfil-warning/** | 資料外流警示 API 層實作 |
## 未來
| 目錄 | 用途 |
|------|------|
| **llm-interface/** | AI 操盤手體驗改進(mira dogfood 痛點) |
| **recipe-system/** | recipe 長期規範(§5 onwards |
| **resumable-workflow/** | workflow 暫停恢復(Phase 6 |
| **user-cc-harness/** | CC harness 安裝 + 配置(Phase 7 |
---
## 讀 SDD 的順序
新任務開始:
1. `docs/3-specs/arcrun/arcrun.md`(全景進度)
2. 對應子系統的 `design.md`(設計背景)
3. 同目錄的 `tasks.md`(細節任務 + 依賴)
---
更新時間:2026-06-08
@@ -0,0 +1,76 @@
# [子系統名稱] — Design
> 狀態:[草稿 / 審核中 / 已採納 / 已廢棄]
> 建立:[YYYY-MM-DD] | 最後更新:[YYYY-MM-DD]
> 負責人:[名稱]
---
## 一句話說明
[這個子系統做什麼,一句話。]
---
## 背景與問題
[為什麼需要這個子系統?解決了什麼問題?]
---
## 範圍
### 包含(In Scope
- [這個 SDD 涵蓋的功能]
### 不包含(Out of Scope
- [明確排除的功能,避免 CC 自行延伸]
---
## 設計
### 架構概覽
[用文字或 ASCII 描述系統結構]
```
[元件 A] → [元件 B] → [元件 C]
```
### 關鍵決策
| 決策 | 選擇 | 原因 | 放棄的選項 |
|------|------|------|----------|
| [問題] | [選擇] | [原因] | [其他選項] |
### API / 介面定義
[端點、資料格式、輸入輸出規格]
### 資料模型
[資料結構、欄位說明]
---
## 技術限制
- [不能用什麼]
- [必須相容什麼]
- [效能要求]
---
## 驗收標準
完成的定義(CC 完成任何 task 前必須確認):
- [ ] [可客觀驗證的條件,例如:POST /api/xxx 回傳 200]
- [ ] [...]
---
## 相關文件
- [連結到相關 ADR]
- [連結到相關 SDD]
@@ -0,0 +1,50 @@
# [子系統名稱] — Tasks
> 權威來源:此檔案是進度真相,不是 CLAUDE.md 或對話。
> 規則:動手前標 [🔄],完成立刻標 [x],不批次更新。
---
## Phase 1[Phase 名稱]
### 前置條件
- [ ] [這個 Phase 開始前必須完成的事]
### Tasks
- [ ] 1.1 [task 描述]
- 驗收:[客觀可驗證的完成標準]
- 注意:[CC 容易犯的錯,可選]
- [ ] 1.2 [task 描述]
- 驗收:[...]
---
## Phase 2[Phase 名稱]
> 前置條件:Phase 1 全部完成
- [ ] 2.1 [task 描述]
- 驗收:[...]
---
## 完成定義
整個 SDD 完成 = 以下全部達成:
- [ ] 所有 tasks 標 [x]
- [ ] 驗收標準通過(有客觀證據)
- [ ] design.md 與實作一致(如有出入需更新)
---
## 狀態說明
| 標記 | 意義 |
|------|------|
| `[ ]` | 未開始 |
| `[🔄]` | 進行中(當前 session|
| `[x]` | 完成(有驗收證據)|
| `[~]` | 暫緩(說明原因)|
| `[!]` | 阻擋中(說明阻擋原因)|
@@ -0,0 +1,611 @@
# Design Document: arcrun MVP
## Overview
arcrun MVP 的核心設計原則是**最小異動、最快可用**。所有目標都能透過以下三個操作達成:
1. **Cherry-pick**:從 `matrix` 搬移指定目錄,不重寫
2. **Carve-out**:移除 cypher-executor 中與 InkStone 耦合的程式碼路徑
3. **Supplement**:補充 contract.yaml 缺少的欄位、新增 CLI
不建立新的抽象層,不改變現有零件邏輯,只做讓開源可用所需的最小改動。
---
## Architecture
### 目標 Repo 結構
```
arcrun/(新獨立開源 repo
├── README.md
├── CONTRIBUTING.md
├── cypher-executor/
│ ├── src/
│ │ ├── index.ts
│ │ ├── types.ts
│ │ ├── graph-executor.ts
│ │ ├── lib/
│ │ │ ├── component-loader.ts ← 改:只從 WASM_BUCKET 讀,移除 KBDB/REGISTRY 邏輯
│ │ │ ├── component-dispatcher.ts
│ │ │ ├── wasm-executor.ts
│ │ │ ├── wasi-shim.ts
│ │ │ └── constants.ts ← 改:移除 MINI_ME / KBDB 特殊零件 hardcode
│ │ └── actions/
│ │ ├── triplet-parser.ts
│ │ ├── graph-builder.ts
│ │ ├── execution-evaluator.ts
│ │ ├── execution-logger.ts
│ │ ├── webhook-handlers.ts
│ │ ├── webhook-graph-resolver.ts ← 改:加入 credential 注入邏輯
│ │ └── (移除 autoPublishMissing.ts)
│ └── wrangler.toml ← 改:移除 9 個 InkStone bindings,新增 CREDENTIALS_KV
├── credentials/ ← 直接搬移,無需修改
│ └── src/...
├── builtins/ ← 直接搬移,無需修改
│ └── src/...
└── registry/
└── components/ ← 搬移後補充 contract.yaml
├── gmail/
├── google_sheets/
├── telegram/
├── line_notify/
├── ... (其餘 17 個零件)
└── cli/ ← 新增:arcrun CLI
├── package.json
├── tsconfig.json
└── src/
├── index.ts
├── commands/
│ ├── init.ts
│ ├── creds.ts
│ ├── push.ts
│ ├── run.ts
│ ├── validate.ts
│ ├── parts.ts
│ ├── list.ts
│ └── logs.ts
└── lib/
├── config.ts # 讀寫 ~/.arcrun/config.yaml
├── cf-api.ts # Cloudflare KV / R2 HTTP API wrapper
└── yaml-parser.ts # workflow.yaml 解析與三元組轉換
```
---
## Component Loader 改造(關鍵變更)
### 現況(matrix 版)
```typescript
// component-loader.ts 現有四層優先序:
// 1. 特殊零件 hardcode → MINI_ME / KBDB Service Binding
// 2. 內建零件 Map → 本地純轉換
// 3. 新版:查詢 KBDB record 含 component_type → WASM 或 Service Binding
// 4. 舊版 fallback:查詢無 component_type 的 KBDB record
```
### 開源版(arcrun
```typescript
// component-loader.ts 簡化為三層:
// 1. 內建零件 Map → 本地純轉換(passthrough / counter 等,保留)
// 2. WASM_BUCKET R2 直讀 → component-name.wasm
// 3. 找不到 → 回傳結構化錯誤
async function loadComponent(componentId: string, env: Env) {
// 層 1:內建零件(無需 R2
if (BUILTIN_COMPONENTS.has(componentId)) {
return BUILTIN_COMPONENTS.get(componentId)
}
// 層 2:從 WASM_BUCKET R2 讀取
const wasmKey = `${componentId}/${componentId}.wasm`
const wasmObj = await env.WASM_BUCKET.get(wasmKey)
if (wasmObj) {
return { type: 'wasm', buffer: await wasmObj.arrayBuffer() }
}
// 層 3:找不到
throw new Error(`Component not found: ${componentId}. 請確認 ${wasmKey} 存在於 WASM_BUCKET。`)
}
```
移除:`MINI_ME``KBDB` 特殊零件的 hardcode 路徑(`comp_claude_chat``comp_kbdb_search``comp_kbdb_history`)。
---
## Credential 注入流程設計
### 執行時序
```
acr run newsletter_subscribe
cypher-executor POST /webhook/:id
webhook-graph-resolver 讀 WEBHOOKS KV → workflow 定義
graph-executor 執行節點 send_thanks
執行前:credential-injector(新增)
查 send_thanks 對應零件 canonical_id = "gmail"
讀 registry/components/gmail/component.contract.yaml
發現 credentials_required: [{key: "gmail_token", inject_as: "access_token"}]
GET CREDENTIALS_KV["gmail_token"] → AES-GCM 解密
input.access_token = decryptedToken
wasm-executor 執行 gmail.wasmstdin = 含 access_token 的完整 input
回傳結果
```
### credential-injector 實作位置
放在 `cypher-executor/src/actions/credential-injector.ts`(新增),由 `graph-executor.ts` 在每個節點執行前呼叫。
```typescript
async function injectCredentials(
componentId: string,
input: Record<string, unknown>,
env: Env
): Promise<Record<string, unknown>> {
const contract = await loadContract(componentId) // 從 WASM_BUCKET 或本地讀取
if (!contract.credentials_required) return input
const enriched = { ...input }
for (const cred of contract.credentials_required) {
const record = await env.CREDENTIALS_KV.get(cred.key)
if (!record) {
throw new Error(
`缺少 credential: ${cred.key}\n修復:在 credentials.yaml 加入 ${cred.key} 後執行 acr creds push`
)
}
const { encrypted, iv } = JSON.parse(record)
enriched[cred.inject_as] = await decrypt(encrypted, iv, env.ENCRYPTION_KEY)
}
return enriched
}
```
---
## workflow.yaml 解析設計
### CLI push 流程
```
acr push newsletter_subscribe.yaml
yaml-parser.ts 讀取 workflow.yaml
解析 flow[] 三元組 → triplets: [{subject, relation, object}]
驗證關係詞(拒絕 PIPE
POST cypher-executor /cypher/search → ExecutionGraph(節點 + 邊)
合併 config: + ExecutionGraph → WorkflowDefinition
PUT WEBHOOKS KV[workflow_name] = JSON.stringify(WorkflowDefinition)
輸出 webhook URL
```
### workflow.yaml 格式(確認版)
```yaml
name: newsletter_subscribe
description: 訂閱電子報,發感謝信並記錄到 GSheets
flow:
- "input >> 完成後 >> send_thanks"
- "input >> 完成後 >> save_to_sheet"
- "send_thanks >> 完成後 >> output"
- "send_thanks >> 失敗時 >> notify_error"
- "save_to_sheet >> 完成後 >> output"
config:
send_thanks:
to: "{{input.email}}"
subject: "感謝訂閱!"
body: "歡迎加入!"
# access_token 由 credentials.yaml 的 gmail_token 自動注入
save_to_sheet:
action: write
spreadsheet_id: "{{creds.sheet_id}}"
range: "訂閱者!A:B"
values: [["{{input.email}}", "{{input.timestamp}}"]]
notify_error:
chat_id: "{{creds.telegram_chat_id}}"
text: "發信失敗:{{input.email}}"
```
---
## contract.yaml 補充欄位格式
### credentials_requiredgmail 範例)
```yaml
credentials_required:
- key: gmail_token
type: google_oauth
description: "Google OAuth access tokengmail.send scope"
inject_as: access_token
```
### config_examplegmail 範例)
```yaml
config_example: |
send_email: # 節點名稱(可自訂)
to: "" # 收件人 Email(必填)
subject: "" # 主旨(必填)
body: "" # 內文(必填)
# access_token 由 credentials.yaml 的 gmail_token 自動注入
```
### 各零件 credentials_required 對照表
| 零件 | key | type | inject_as |
|------|-----|------|-----------|
| gmail | gmail_token | google_oauth | access_token |
| google_sheets | google_oauth | google_oauth | access_token |
| telegram | telegram_bot_token | telegram_bot_token | bot_token |
| line_notify | line_token | line_token | token |
---
## CLI 技術設計
### 依賴
```json
{
"dependencies": {
"commander": "^12.0.0",
"js-yaml": "^4.1.0",
"chalk": "^5.3.0",
"ora": "^8.0.1"
}
}
```
### config.yaml 格式(~/.arcrun/config.yaml
```yaml
cloudflare_account_id: abc123
webhooks_kv_id: xyz789
credentials_kv_id: abc456
wasm_bucket: arcrun-wasm
cypher_executor_url: https://cypher-executor.xxx.workers.dev
credentials_worker_url: https://arcrun-credentials.xxx.workers.dev
api_token: ***(加密存本機)
```
### Cloudflare API 操作
CLI 使用 Cloudflare REST API(不依賴 Wrangler CLI):
- KV 寫入:`PUT /client/v4/accounts/{id}/storage/kv/namespaces/{ns_id}/values/{key}`
- KV 讀取:`GET /client/v4/accounts/{id}/storage/kv/namespaces/{ns_id}/values/{key}`
- KV 列出:`GET /client/v4/accounts/{id}/storage/kv/namespaces/{ns_id}/keys`
---
## wrangler.toml 變更對照
### 移除(InkStone 專屬)
```toml
# 全部移除:
[[services]]
binding = "KBDB"
service = "inkstone-kbdb-api"
[[services]]
binding = "REGISTRY"
service = "inkstone-component-registry"
[[services]]
binding = "CLINIC_GDRIVE"
service = "clinic-gdrive"
# ... CLINIC_EXCEL, CLINIC_ANALYSIS, CLINIC_RENDER, CLINIC_GSHEETS
[[services]]
binding = "AICEO"
service = "inkstone-aiceo-bot"
[[services]]
binding = "MINI_ME"
service = "inkstone-mini-me"
```
### 保留
```toml
[[kv_namespaces]]
binding = "EXEC_CONTEXT"
[[kv_namespaces]]
binding = "WEBHOOKS"
[[r2_buckets]]
binding = "WASM_BUCKET"
[ai]
binding = "AI"
```
### 新增
```toml
[[kv_namespaces]]
binding = "CREDENTIALS_KV"
id = "" # 用戶自行填入
```
---
## Standard 模式架構(用戶自己的 KVarcrun.dev 的引擎)
### 儲存責任分界
```
arcrun.dev 負責:
WASM_BUCKET 公眾零件庫(.wasm 二進位)
ANALYTICS_KV 零件執行統計
ACCOUNTS_KV API Key → tenant_id + CF API Token 對應
SUBMISSIONS_KV 零件提交審核狀態
用戶自己負責(一個 CF KV,arcrun.dev 不存取明文):
USER_KV
workflow:{name} → workflow 執行圖(JSON
cred:{key} → AES-GCM 加密 credential
```
### 完整系統圖
```
┌──────────────────────────────────────────────────────────────┐
│ arcrun.dev(你的 Cloudflare 帳號) │
│ │
│ auth-workerapi.arcrun.dev
│ POST /register → { api_key, tenant_id } │
│ ACCOUNTS_KV: { tenant_id, cf_api_token, api_key_hash } │
│ ※ 不儲存用戶 credential 或 workflow 內容 │
│ │
│ cypher-executorcypher.arcrun.dev,共享) │
│ X-Arcrun-API-Key → tenant_id → cf_api_token │
│ 用 cf_api_token 呼叫 CF KV API → 讀用戶自己的 USER_KV │
│ WASM_BUCKET: gmail.wasm / telegram.wasm / ...(共享) │
│ │
│ public registryregistry.arcrun.dev
│ GET /components → 零件清單 + 統計 + author + visibility │
│ POST /submit → 接收零件,沙盒驗收後設 author_only │
│ POST /analytics/record → 執行統計(非同步) │
└──────────────────────────────────────────────────────────────┘
↕ CF KV API(用戶的 cf_api_tokenKV Edit 權限)
┌──────────────────────────────────────────────────────────────┐
│ 用戶自己的 CF 帳號 │
│ USER_KV │
│ workflow:newsletter → { triplets, config } │
│ cred:gmail_token → { encrypted, iv } │
│ cred:telegram_bot → { encrypted, iv } │
└──────────────────────────────────────────────────────────────┘
```
### acr init 互動流程
```
$ acr init
? 你的 Cloudflare Account ID: abc123
? USER_KV Namespace ID(在 CF Dashboard 建立一個 KV 後貼上): kv_xyz
? CF API Token(只需 KV Edit 權限,arcrun 用此存取你的 KV: ***
? Email(取得 arcrun.dev API Key: you@example.com
→ 呼叫 POST https://api.arcrun.dev/register { email, cf_api_token_hash }
→ 取得 api_key: ak_xxxxxxxx
✓ 設定完成 → ~/.arcrun/config.yaml
✓ 建立 credentials.yaml(已加入 .gitignore
你的 credential 與 workflow 存在你自己的 CF KVarcrun 不會儲存它們。
```
### API Key 驗證與 KV 存取 Middleware
```typescript
// cypher-executor/src/lib/tenant.ts(新增)
export async function resolveUserKv(request: Request, env: Env) {
if (env.MULTI_TENANT === 'false') {
// Self-hosted:直接用本地 KV binding
return { kv: env.LOCAL_KV, prefix: '' }
}
const apiKey = request.headers.get('X-Arcrun-API-Key')
if (!apiKey) throw new Response('Missing API Key', { status: 401 })
const hash = await sha256(apiKey)
const account = await env.ACCOUNTS_KV.get(`hash:${hash}`)
if (!account) throw new Response('Invalid API Key', { status: 401 })
const { cf_api_token, account_id, kv_namespace_id } = JSON.parse(account)
// 回傳 CF KV API wrapper,用用戶自己的 token 存取
return {
kv: new CfKvClient({ cf_api_token, account_id, kv_namespace_id }),
prefix: ''
}
}
```
### USER_KV Key Schema
```
Standard 模式(用戶自己的 KV):
workflow:{name} → WorkflowDefinition JSON
cred:{key} → { encrypted (base64), iv (base64) }
Self-hosted(本地 KV binding):
維持現有 key 格式,無 prefix
```
---
## 執行統計設計
### Analytics Record(非同步,不阻擋執行)
```typescript
// cypher-executor/src/actions/analytics.ts(新增)
export function recordExecution(
componentId: string,
version: string,
success: boolean,
durationMs: number
): void {
// fire-and-forget,不 await
fetch('https://registry.arcrun.dev/analytics/record', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ canonical_id: componentId, version, success, duration_ms: durationMs })
}).catch(() => {}) // 統計失敗不影響執行
}
```
### 統計聚合(registry Worker
```
ANALYTICS_KV 結構:
"stats:gmail:v1" → { total_runs: 140382, success_runs: 139444, total_ms: 16845840 }
每次 POST /analytics/record
原子更新(KV 樂觀鎖)→ total_runs++, success_runs += success, total_ms += duration_ms
GET /components 回傳:
success_rate = success_runs / total_runs * 100
avg_duration_ms = total_ms / total_runs
排序:total_runs × success_rateDESC
```
---
## 零件貢獻流程設計
### `acr parts publish` 流程
```
$ acr parts publish gmail-v2
1. CLI 讀取 registry/components/gmail-v2/ 目錄
- component.contract.yaml(必須有 author 欄位)
- main.go
- gmail-v2.wasm
2. POST https://registry.arcrun.dev/submit
multipart form:
contract: <yaml content>
source: <main.go content>
wasm: <binary>
Header: X-Arcrun-API-Key: ak_xxxxx
3. registry 回應:
{ submission_id: "sub_abc123", status: "pending_review" }
4. CLI 輸出:
✓ 提交成功(submission_id: sub_abc123
查詢進度:acr parts publish --status sub_abc123
```
### Registry 沙盒驗收與 visibility 狀態機
```
POST /submit 觸發(非同步執行):
[整合類零件:gmail、telegram、google_sheets、line_notify、http_request]
Step 1: 體積檢查(< 2048KB
Step 2: syscall 掃描(無 filesystem syscall;網路 syscall 允許,因需呼叫外部 API)
通過 → visibility: author_only(作者立即可用,等人工審核)
[功能類零件:所有其他零件]
Step 1: 體積檢查(< 2048KB
Step 2: 冷啟動時間(< 50ms
Step 3: syscall 掃描(無網路 / 無 filesystem
Step 4: Gherkin 測試(contract 中所有 scenario 100% 通過)
通過 → visibility: author_only(作者立即可用,等人工審核)
任一步驟失敗 → status: rejected(回傳 failed_step + reason
人工審核通過 → visibility: public
- 零件出現在所有人的 GET /components
- 開始累積公開執行統計
人工審核拒絕 → visibility 維持 author_only
- 作者仍可使用,但收到拒絕原因
- 作者修改後可重新提交
acr parts 顯示規則:
visibility: author_only → [待審核] 只有你可用(不顯示統計)
visibility: public → ★ 成功率 | N 次執行 | by @author
任一失敗 → status: rejected
- 回傳 { failed_step, reason },格式與 Requirement 2 相同
```
---
## 開發順序(Phase 對齊 requirements
```
Phase 1:搬移與清理(Requirement 1
1.1 建立 arcrun repo,搬移四個目錄
1.2 清理 cypher-executor/wrangler.toml
1.3 改寫 component-loader(移除 KBDB/REGISTRY/MINI_ME 路徑)
1.4 移除 autoPublishMissing.ts(依賴 REGISTRY binding
1.5 本機 wrangler dev 測試 /health
Phase 2:零件完整度(Requirement 2
2.1 審查 21 個零件 contract.yaml(表格回報)
2.2 補充 credentials_required4 個零件)
2.3 補充 config_example(全部 21 個)
2.4 驗證 main.go required 與 contract 一致
Phase 3credential 注入(Requirement 3
3.1 新增 credential-injector.ts
3.2 整合進 graph-executor 節點執行前
3.3 測試 gmail 零件端對端(credentials.yaml → push → run
Phase 4CLIRequirement 4
4.1 acr init--hosted / --self-hosted 分支)
4.2 acr creds pushHosted 走 APISelf-hosted 走 KV
4.3 acr push
4.4 arcrun run
4.5 acr validate
4.6 acr parts / acr parts scaffold / acr parts publish
4.7 acr list / acr logs
Phase 5:開源發布(Requirement 5
5.1 撰寫 README.md(含 --hosted 快速開始)
5.2 撰寫 CONTRIBUTING.md
5.3 確認無 InkStone 內部資訊殘留
5.4 GitHub 發布 + npm publish
Phase 6Hosted SaaSRequirement 6
6.1 建立 auth-workerapi.arcrun.dev
6.2 cypher-executor 加入 tenant middleware
6.3 CREDENTIALS_KV key schema 加 tenant prefix
6.4 部署至 arcrun.dev
Phase 7:統計與貢獻(Requirement 7 + 8
7.1 analytics.ts(執行後 fire-and-forget
7.2 registry /analytics/record 端點
7.3 ANALYTICS_KV 聚合邏輯
7.4 GET /components 加入統計排序
7.5 POST /submit 沙盒驗收 + author 寫入
7.6 acr parts publish 指令
```
@@ -0,0 +1,180 @@
# Requirements Document
## Introduction
arcrun MVP 是從 `matrix` monorepo 中 cherry-pick 出最小可獨立運作的 AI 工作流執行引擎,目標是作為**獨立開源 repo**(`arcrun`)發布。
**背景**`matrix` 因為同時承載 InkStone 內部服務(KBDB、CLINIC_*、AICEO、MINI_ME 等)與核心執行引擎,複雜度過高,難以讓外部開發者使用或貢獻。MVP 的任務是將執行引擎從內部服務中解耦,讓任何人都能自行在 Cloudflare 上部署一套完整的 AI 工作流系統。
**護城河邏輯**
- 開源:cypher-executor(執行引擎)、WASM 零件庫(21 個)、credentials Worker、CLI
- Hosted SaaS:一行指令註冊取得 API Key,直接使用公眾零件庫,無需部署任何 Worker
- 閉源(InkStone 付費):KBDB 向量搜尋、graph 查詢、Persona SDK、MatchGPT
**不在此次範圍**:KBDB 整合、前端管理介面、向量搜尋、新增 WASM 零件。
---
## Glossary
- **cypher-executor**:原 `matrix/cypher-executor`,執行 workflow 的 Cloudflare Worker。開源版移除所有 InkStone 內部 Service Binding,只保留 KV / R2 / Workers AI。
- **component(零件)**:以 TinyGo 編譯的 `.wasm` 檔案,以 WASI preview1 / stdin-stdout JSON 為 I/O 模型。
- **component.contract.yaml**:每個零件的規格宣告,含 `canonical_id``input_schema``output_schema``gherkin_tests`,開源版補充 `credentials_required``config_example`
- **credentials Worker**`arcrun/credentials`,以 AES-GCM 加密存取 API token,部署在用戶自己的 CF 帳號。
- **WASM_BUCKET**arcrun.dev 的 R2 bucket,儲存所有公眾 `.wasm` 零件二進位,由 Arcrun 負責。
- **USER_KV**:用戶自己 CF 帳號下的 KV Namespace,同時存放 workflow YAML 與加密 credential,由用戶負責,Arcrun 不經手。
- **workflow.yaml**:用戶撰寫的工作流定義,`flow:``>>` 三元組描述,`config:` 對應各節點參數,存在用戶自己的 USER_KV。
- **CLI(套件名 arcrun,指令 acr**Node.js/TypeScript CLI 工具,管理 credentials、workflow 的上傳與執行。安裝:`npm i -g arcrun`,使用:`acr <指令>`
- **credentials_required**contract.yaml 新欄位,宣告零件需要哪個 credential 以及注入到哪個 input 欄位。
- **config_example**contract.yaml 新欄位,提供 `acr parts scaffold` 指令使用的 config 範本。
- **Standard 模式(預設)**:用戶只需在自己 CF 帳號開一個 KV(存 credential + workflow),使用 arcrun.dev 的執行引擎與公眾零件庫,無需部署任何 Worker。
- **Self-hosted 模式**:用戶自行部署全套 Worker 至自己的 Cloudflare 帳號,有完全控制權,可貢獻零件至公眾庫。
- **auth-worker**arcrun.dev 上的帳號服務 Worker,處理 `POST /register` 自動發放 API Key,不儲存用戶 credential。
- **tenant_id**:每個 API Key 對應的租戶識別碼,用於讓 cypher-executor 知道要用哪個 Cloudflare API Token 去存取用戶的 USER_KV。
- **public registry**arcrun.dev 上的公眾零件庫,所有人共用,有執行統計與 author 資訊。
- **`acr parts publish`**:CLI 指令,自架用戶將自製零件提交至公眾 registry 審核。
- **execution analytics**:每次零件執行後非同步記錄的統計資料(使用次數、成功率),公開顯示於 `acr parts`
- **visibility**contract.yaml 欄位,值為 `author_only`(沙盒通過後作者立即可用)或 `public`(人工審核後所有人可用)。
---
## Requirements
### Requirement 1:搬移 cypher-executor 至獨立 repo 並移除 InkStone bindings
**User Story:** As a 開源用戶, I want 自行部署 cypher-executor 至我的 Cloudflare 帳號, so that 我不需要依賴 InkStone 的任何服務就能執行 AI 工作流。
#### Acceptance Criteria
1. THE cypher-executor `wrangler.toml` SHALL 移除以下 Service Bindings`KBDB``REGISTRY``CLINIC_GDRIVE``CLINIC_EXCEL``CLINIC_ANALYSIS``CLINIC_RENDER``CLINIC_GSHEETS``AICEO``MINI_ME`
2. THE cypher-executor `wrangler.toml` SHALL 保留以下 bindings`EXEC_CONTEXT`KV)、`WEBHOOKS`KV)、`WASM_BUCKET`R2)、`AI`Workers AI)。
3. THE cypher-executor `wrangler.toml` SHALL 新增 `CREDENTIALS_KV`KV Namespace binding),用於 credential 解密注入。
4. THE component-loader SHALL 從 `WASM_BUCKET` R2 直接讀取 `.wasm` 檔案,不透過任何 Service Binding 或外部 HTTP 查詢。
5. WHEN cypher-executor 收到執行請求,THE cypher-executor SHALL 不依賴 `KBDB``REGISTRY` 或任何 InkStone 內部 Service Binding,只使用 KV / R2 / Workers AI 完成執行。
6. THE arcrun repo SHALL 包含以下目錄:`cypher-executor/``credentials/``builtins/``registry/components/`21 個零件)。
---
### Requirement 2component.contract.yaml 完整度補充
**User Story:** As a 零件使用者, I want 每個零件的 contract.yaml 都有 `credentials_required``config_example`, so that CLI 能自動注入 credential,用戶也能快速知道如何設定節點。
#### Acceptance Criteria
1. THE `credentials_required` 欄位 SHALL 出現在以下 4 個零件的 contract.yaml 中:`gmail``google_sheets``telegram``line_notify`
2. WHEN `credentials_required` 存在,THE 欄位 SHALL 包含以下子欄位:`key`(對應 credentials.yaml 的 key 名稱)、`type`token 類型,如 `google_oauth``telegram_bot_token`)、`description`(說明)、`inject_as`(執行時注入到 input 的哪個欄位名稱)。
3. THE `config_example` 欄位 SHALL 出現在所有 21 個零件的 contract.yaml 中。
4. WHEN `config_example` 存在,THE 欄位 SHALL 為 YAML 字串,內容為可直接貼入 workflow.yaml `config:` 區塊的範本,需有人類可讀的說明註解。
5. FOR 需要 credential 的零件,THE `config_example` SHALL 包含一行註解,說明哪個 credential key 會被自動注入到哪個欄位(如 `# access_token 由 credentials.yaml 的 gmail_token 自動注入`)。
6. THE main.go 的 `required` 欄位 SHALL 與 contract 的 `input_schema.required[]` 保持一致,不得有欄位名稱不符。
---
### Requirement 3workflow YAML 格式與執行時 credential 注入
**User Story:** As a 工作流設計者, I want 用有語意的關係詞撰寫 workflow.yaml,且 credential 自動注入, so that workflow 定義中完全不出現明文 token。
#### Acceptance Criteria
1. THE workflow.yaml `flow:` 欄位 SHALL 以 `"A >> 關係詞 >> B"` 三元組陣列描述資料流。
2. THE cypher-executor SHALL 支援以下關係詞:`完成後``失敗時``對每個``條件滿足時``ON_SUCCESS``ON_FAIL``FOREACH``IF``ON_CLICK``CALLS_SUBFLOW`
3. THE cypher-executor SHALL 拒絕使用 `PIPE` 關係詞,並回傳明確錯誤訊息。
4. WHEN cypher-executor 執行一個節點,THE cypher-executor SHALL 查詢該節點對應零件的 `credentials_required`,若存在則從 `CREDENTIALS_KV` 解密對應 credential,並注入到 input 的 `inject_as` 欄位。
5. THE credential 注入 SHALL 發生在 WASM 執行前,用戶的 workflow `config:` 中不需也不應包含 token 值。
6. IF `credentials_required` 宣告的 credential key 在 `CREDENTIALS_KV` 中不存在,THE cypher-executor SHALL 回傳結構化錯誤,包含缺少的 key 名稱與修復步驟說明。
---
### Requirement 4CLIarcrun,指令 acr)核心指令
**User Story:** As a 開發者, I want 透過 `acr` CLI 管理 workflow 與 credentials, so that 不需要直接操作 Cloudflare KV / R2 API 就能完成部署與執行。
#### Acceptance Criteria
1. THE CLI SHALL 以 Node.js/TypeScript 實作,套件名 `arcrun`bin 名 `acr`,可透過 `npm i -g arcrun` 安裝,依賴只使用 `commander``js-yaml``chalk``ora`
2. THE `acr init` 指令 SHALL 以互動式問答產生 `~/.arcrun/config.yaml`,問答內容為:CF Account ID、USER_KV namespace ID、CF API Token(用於 cypher-executor 代存取用戶 KV)、email(取得 arcrun.dev API Key);並建立空白本機 `credentials.yaml`
3. THE `acr creds push [credentials.yaml]` 指令 SHALL 讀取 credentials.yaml,逐一加密上傳至用戶自己的 USER_KV,並顯示每個 key 的上傳結果。
4. THE `acr push <workflow.yaml>` 指令 SHALL 解析 `flow:` 三元組,轉換成執行圖,連同 `config:` 存入 `WEBHOOKS KV`,並輸出 webhook URL。
5. THE `acr run <workflow_name> [--input key=value...]` 指令 SHALL 觸發 cypher-executor 執行指定 workflow,顯示各節點執行結果;失敗時顯示具體節點、原因與修復步驟。
6. THE `acr validate <workflow.yaml>` 指令 SHALL 在執行前驗證:YAML 格式、關係詞合法性(無 PIPE)、所有節點在 config 中有對應、所有零件存在於 WASM_BUCKET、所有 credentials 已上傳至 CREDENTIALS_KV。
7. THE `acr parts` 指令 SHALL 列出所有可用零件(按類型分組),顯示每個零件的必填欄位與所需 credential。
8. THE `acr parts scaffold <component>` 指令 SHALL 從 contract 的 `config_example` 輸出可直接貼入 workflow.yaml 的 config 範本,以及對應的 credentials.yaml 欄位範本。
9. THE `acr list` 指令 SHALL 列出 WEBHOOKS KV 中所有已上傳的 workflow,顯示名稱與更新時間。
10. THE `acr logs <workflow_name>` 指令 SHALL 顯示最近執行記錄,包含時間、成功/失敗狀態、執行時間,失敗時顯示失敗節點與原因。
---
### Requirement 5README 與開源發布準備
**User Story:** As a 外部開發者, I want 看到清楚的 README5 分鐘內能完成部署, so that 降低試用門檻,吸引社群貢獻。
#### Acceptance Criteria
1. THE README.md SHALL 包含以下章節:專案定位(開源核心 vs 閉源付費服務說明)、快速開始(`acr init``acr creds push``acr push``acr run` 四步驟)、零件列表(21 個零件分類說明)、workflow YAML 語法說明(三元組 + 關係詞表格)、自行部署說明(Cloudflare Workers 部署步驟)。
2. THE README.md 快速開始 SHALL 以 `newsletter_subscribe` 為範例 workflow,展示 gmail + google_sheets + telegram 的完整串接。
3. THE repo SHALL 包含 `CONTRIBUTING.md`,說明如何新增零件(TinyGo 開發環境、contract.yaml 格式、本機測試指令)。
4. THE repo SHALL 確保所有 InkStone 內部資訊(Worker URL、KV namespace ID、帳號資訊)不出現在任何已提交的檔案中。
5. WHEN cypher-executor 部署後第一次被呼叫,THE cypher-executor SHALL 能正常回應 health check`GET /health` 回傳 `{ ok: true }`),不需要任何 InkStone 服務可用。
---
### Requirement 6Standard 模式 — API Key 註冊與用戶 KV 存取
**User Story:** As a 新用戶, I want 只需開一個 CF KV 就能開始使用 Arcrun,不需要部署任何 Worker, so that 最低門檻試用整個平台,且我的 credential 永遠在我自己的環境。
#### Acceptance Criteria
1. THE auth-worker SHALL 提供 `POST /register` 端點,接受 `{ email }` 後自動生成 API Key(格式:`ak_` 前綴 + 32 字元隨機字串),無需人工審核,立即回傳 `{ api_key, tenant_id }`
2. THE auth-worker SHALL 將 `{ tenant_id, email, created_at, api_key_hash }` 存入 `ACCOUNTS_KV`,只存 hash 不存明文 API Key。arcrun.dev 不儲存任何用戶 credential 或 workflow 內容。
3. WHEN `acr init` 執行,THE CLI SHALL 互動式詢問以下資料並寫入 `~/.arcrun/config.yaml`
- CF Account ID(用戶自己的)
- USER_KV namespace ID(用戶自己開的,存 credential + workflow
- CF API Token(供 cypher-executor 用 CF API 存取用戶 KV,只需 KV Edit 權限)
- email(呼叫 `POST https://api.arcrun.dev/register` 取得 API Key
4. THE cypher-executor SHALL 在每個 request 的 header 讀取 `X-Arcrun-API-Key`,驗證後取得該 tenant 的 CF API Token,用 Cloudflare API 存取用戶自己的 USER_KV;缺少或無效的 API Key 回傳 `401 Unauthorized`
5. THE `acr creds push` 指令 SHALL 使用用戶的 CF API Token,直接呼叫 Cloudflare KV API 將加密 credential 寫入用戶自己的 USER_KV,不經過 arcrun.dev。
6. THE `acr push <workflow.yaml>` 指令 SHALL 同樣直接寫入用戶自己的 USER_KV,不經過 arcrun.dev。
7. WHEN Self-hosted 模式,THE cypher-executor SHALL 可透過環境變數 `MULTI_TENANT=false` 停用 API Key 驗證,直接使用本地 KV binding,與現有行為相容。
---
### Requirement 7:公眾零件庫執行統計與貢獻榮譽
**User Story:** As a 零件使用者, I want 在 `acr parts` 看到每個零件的真實執行統計與作者資訊, so that 我能選擇最可靠的零件;As a 零件貢獻者, I want 我的名字和統計數字公開顯示, so that 我有動機將好零件推入公眾庫而非留在私庫。
#### Acceptance Criteria
1. THE contract.yaml SHALL 新增可選欄位 `author`GitHub username,如 `@alice`),在 `acr parts` 顯示時一起展示。
2. WHEN cypher-executor 執行完一個零件節點,THE cypher-executor SHALL 非同步 POST 以下資料至 `https://registry.arcrun.dev/analytics/record`,不阻擋主流程:
```json
{ "canonical_id": "gmail", "version": "v1", "success": true, "duration_ms": 120 }
```
不含任何用戶資料或 tenant_id。
3. THE public registry SHALL 聚合每個零件的執行統計:`total_runs`(總執行次數)、`success_rate`(成功率,百分比)、`avg_duration_ms`(平均執行時間)。
4. THE `acr parts` 指令 SHALL 顯示每個零件的統計資料,格式為:
```
• gmail Gmail 發信 by @alice
★ 99.2% 成功 | 140,382 次執行 | 平均 120ms
```
5. IF 零件存在於用戶自架的私有 WASM_BUCKET 而非公眾庫,THE `acr parts` SHALL 顯示該零件但標註 `[私有]`,不顯示統計數字與 author。
6. THE public registry SHALL 在 `GET /components` 回傳的零件清單中,依 `total_runs × success_rate` 排序,讓高品質高使用量的零件排在前面。
---
### Requirement 8:零件貢獻流程與 visibility 狀態
**User Story:** As a 零件開發者, I want 提交零件後立即能自己使用,等審核通過後公開給所有人, so that 不用等待審核就能驗證自己的零件是否有用。
#### Acceptance Criteria
1. THE contract.yaml SHALL 包含 `visibility` 欄位,值為 `author_only`(沙盒通過後作者立即可用)或 `public`(人工審核通過後所有人可用)。
2. THE `acr parts publish <component>` 指令 SHALL 打包指定零件的原始碼、`component.contract.yaml`、`.wasm`POST 至 `https://registry.arcrun.dev/submit`(帶 `X-Arcrun-API-Key` header)。原始碼語言不限,但編譯產出必須為 WASM + WASI preview1。
3. WHEN 零件提交後,THE registry SHALL 依零件類型執行不同層級的沙盒驗收:
- **整合類**(需呼叫外部 API,如 gmail、telegram):體積 / syscall 掃描通過 → `author_only`
- **功能類**(純邏輯,如 string_ops、if_control):體積 / syscall 掃描 / Gherkin 測試全通過 → `author_only`
- 任一必要步驟失敗 → `rejected`(回傳具體失敗步驟與原因)
4. WHEN 零件 visibility 為 `author_only`THE registry SHALL 讓該零件只對提交者的 API Key 可見,`acr parts` 顯示時標註 `[待審核]`,其他用戶看不到。
5. WHEN 人工審核通過,THE registry SHALL 將 visibility 改為 `public`,零件立即出現在所有人的 `acr parts` 清單,並開始累積公開執行統計。
6. WHEN 審核拒絕,THE registry SHALL 回傳具體失敗原因,零件保留 `author_only` 狀態讓作者繼續修改後重新提交。
7. THE `acr parts publish` 指令 SHALL 在提交後顯示 `submission_id`、目前 visibility 狀態,以及查詢審核進度的指令提示。
8. THE `acr parts` 指令 SHALL 對 `author_only` 零件顯示「[待審核] 只有你可用」,對 `public` 零件顯示執行統計與 author,讓貢獻者清楚知道零件的可用範圍。
@@ -0,0 +1,206 @@
# Implementation Plan: arcrun MVP
## Overview
依照 Design 的七個 Phase 實作。原則:最小異動,不重寫現有邏輯,只 cherry-pick + carve-out + supplement。
所有 Phase 13 工作在 `matrix` repo 對應目錄驗證後再搬到新 repo。
**PR #2claude/review-mvp-specs-8Bvdu)狀態:** 初始實作已提交,已修復以下問題後準備 merge:
- CF API Token 傳至 arcrun.dev 安全問題(已修復)
- 加密 fallback 格式不相容(已修復)
- submitComponent KBDB 依賴(已修復,改用 SUBMISSIONS_KV
- Webhook 路由缺 analytics(已修復)
- `require()` 在 ES module 中(已修復)
- api 類零件 `no_network_syscall: true` 錯誤(已修復)
---
## Phase 1:搬移與清理
- [x] 1. 建立 `arcrun` 獨立 repo 並初始化
- [x] 1.1 在 GitHub 建立新的 public repo(使用 matrix monorepo 的 `arcrun/` 子目錄代替,PR #2
- [x] 1.2 設定 `.gitignore`(排除 `node_modules/``.wrangler/``credentials.yaml``~/.arcrun/`
- [x] 1.3 從 `matrix` cherry-pick 四個目錄:
- `matrix/cypher-executor/``arcrun/cypher-executor/`
- `matrix/u6u-core/credentials/``arcrun/credentials/`
- `matrix/u6u-core/registry/components/``arcrun/registry/components/`
- _Requirements: 1.6_
- [x] 2. 清理 `cypher-executor/wrangler.toml`
- [x] 2.1 移除 9 個 InkStone Service BindingsKBDB、REGISTRY、CLINIC_*、AICEO、MINI_ME
- [x] 2.2 確認保留:`EXEC_CONTEXT``WEBHOOKS``WASM_BUCKET``AI`
- [x] 2.3 新增 `CREDENTIALS_KV``ANALYTICS_KV` KV namespace binding
- [x] 2.4 更新 `name``arcrun-cypher-executor`
- _Requirements: 1.1, 1.2, 1.3_
- [x] 3. 改寫 `cypher-executor/src/lib/component-loader.ts`
- [x] 3.1 移除對 MINI_ME、KBDB、InkStone bindings 的 hardcode
- [x] 3.2 實作三層邏輯:builtin Map → WASM_BUCKET R2 直讀 → 結構化錯誤
- _Requirements: 1.4, 1.5_
- [x] 4. 移除對 InkStone bindings 的依賴程式碼
- [x] 4.1 刪除 `autoPublishMissing.ts`(依賴 REGISTRY binding
- [x] 4.2 移除所有 `env.KBDB``env.REGISTRY``env.MINI_ME``env.AICEO``env.CLINIC_*` 引用
- _Requirements: 1.1, 1.5_
- [ ] 5. 本機驗證
- [ ] 5.1 `cd arcrun/cypher-executor && wrangler dev` 能啟動(無 binding 錯誤)
- [ ] 5.2 `GET /health` 回傳 `{ ok: true }`
- [ ] 5.3 上傳 `validate_json.wasm` 到 WASM_BUCKET,執行 `POST /execute` 能正常回傳結果
- _Requirements: 1.5, 5.5_
---
## Phase 2:零件完整度補充
- [x] 6. api 類零件 `no_network_syscall` 修正
- [x] 6.1 gmail、telegram、google_sheets、line_notify、http_request 改為 `no_network_syscall: false`
- _Requirements: 2.1_
- [ ] 7. 審查 21 個零件 contract.yaml 並補充 `credentials_required`
- [ ] 7.1 確認 gmail、google_sheets、telegram、line_notify 有 `credentials_required`PR #2 已加入,需驗證格式正確)
- [ ] 7.2 確認所有 21 個零件有 `config_example` 欄位
- [ ] 7.3 驗證 `main.go` required 欄位與 `contract.yaml` input_schema.required[] 一致
- _Requirements: 2.1, 2.2, 2.3_
---
## Phase 3Credential 注入整合
- [x] 10. `credential-injector.ts` 已實作(`arcrun/cypher-executor/src/actions/credential-injector.ts`
- [x] 10.1 讀取 contract.yaml from R2,解析 `credentials_required`
- [x] 10.2 從 `CREDENTIALS_KV` 讀取 AES-GCM 加密 token,注入到 input 對應欄位(inject_as
- [x] 10.3 credential 不存在時拋出結構化錯誤(含 key 名稱與修復步驟)
- _Requirements: 3.4, 3.5, 3.6_
- [ ] 11. 驗證 credential 注入整合進 graph-executor
- [ ] 11.1 確認 `graph-executor.ts` 在節點執行前正確呼叫 `injectCredentials`
- [ ] 11.2 確認注入只影響 WASM input,不修改 WEBHOOKS KV 中儲存的 workflow 定義
- _Requirements: 3.4, 3.5_
- [ ] 12. 端對端測試(手動)
- [ ] 12.1 建立 `credentials.yaml`,加入測試 token
- [ ] 12.2 執行 `acr creds push`,確認寫入 CREDENTIALS_KV 格式為 `{ encrypted, iv }`(無 `mode: 'base64'`
- [ ] 12.3 執行含 credential 的 workflow,確認 inject_as 欄位正確注入
- _Requirements: 3.4, 3.5_
---
## Phase 4CLI 開發
- [x] 13. CLI 專案骨架已建立(`arcrun/cli/`
- [x] 13.1 `package.json`name: `arcrun`bin: `acr`
- [x] 13.2 `tsconfig.json`module: NodeNext
- [x] 13.3 所有 10 個指令已實作骨架
- _Requirements: 4.1_
- [x] 14. `acr init` 已實作,修正項:
- [x] 14.1 Standard 模式不再傳送 `cf_api_token` 至 arcrun.dev(只傳 `email`
- [x] 14.2 `require()` 改用 `await import()` 修正 ES module 相容
- [ ] 14.3 **待補**`acr init` 需詢問 `ARCRUN_ENCRYPTION_KEY` 並寫入 config(目前加密 key 需手動設定)
- _Requirements: 4.2, 6.3_
- [x] 15. `acr creds push` 已實作
- [x] 15.1 讀取 `credentials.yaml`AES-GCM 加密後寫入用戶 CF KV`cred:{name}`
- [x] 15.2 加密 fallbackbase64)已移除,key 不足時直接拋錯提示生成指令
- _Requirements: 4.3, 6.5_
- [x] 16. `acr push` 已實作
- _Requirements: 4.4_
- [x] 17. `acr run` 已實作
- _Requirements: 4.5_
- [ ] 18. `acr validate` credential 檢測邏輯有誤,需修復
- [ ] 18.1 `extractCredentialRefs()` 目前掃描 `{{creds.xxx}}` 語法,但 injection 使用 `inject_as` key
- [ ] 18.2 改為讀取 contract.yaml 的 `credentials_required[].key`,與 `cred:{key}` KV 存在性比對
- _Requirements: 4.6_
- [x] 19. `acr parts``acr parts scaffold``acr parts publish` 已實作
- [ ] 19.1 `acr parts` 中 YAML 解析改用 `js-yaml`(目前用 regex,可能解析失敗)
- _Requirements: 4.7, 4.8_
- [x] 20. `acr list``acr logs` 已實作
- _Requirements: 4.9, 4.10_
---
## Phase 5:開源發布準備
- [x] 21. README.md 已撰寫(`arcrun/README.md`
- [x] 22. CONTRIBUTING.md 已撰寫(`arcrun/CONTRIBUTING.md`
- [ ] 23. 安全審查(PR merge 前執行)
- [ ] 23.1 搜尋 `.workers.dev` InkStone 網域
- [ ] 23.2 確認 wrangler.toml 所有 KV id 欄位留空
- [ ] 23.3 確認 `credentials.yaml``.gitignore`
- _Requirements: 5.4_
- [ ] 24. 發布(安全審查後)
- [ ] 24.1 `npm publish`CLI package `arcrun`
- _Requirements: 5.1_
---
## Phase 6Standard 模式 — auth-worker 與用戶 KV 代存取
- [ ] 25. 建立 `auth-worker`(新 Worker,部署至 `api.arcrun.dev`
- [ ] 25.1 建立 `auth-worker/` 目錄,初始化 Hono + wrangler.toml
- [ ] 25.2 實作 `POST /register`:接收 `{ email, account_id, kv_namespace_id }` + CF API Token 透過 header 傳入
- **不在 request body 中接收 CF API Token**Token 透過 header `CF-Api-Token` 傳入,減少 TLS 以外的洩漏面)
- 生成 `tenant_id``api_key`,存入 `ACCOUNTS_KV`
- [ ] 25.3 Bindings`ACCOUNTS_KV`
- _Requirements: 6.1, 6.2_
- [ ] 26. 改造 `cypher-executor` 支援 multi-tenant 用戶 KV 代存取
- [ ] 26.1 讀取 `MULTI_TENANT` env var(目前已宣告但未讀取),實作 tenant middleware
- [ ] 26.2 `X-Arcrun-API-Key` → 查 `ACCOUNTS_KV` → 取得用戶 cf_api_token + kv_namespace_id → 建立 `CfKvClient`
- [ ] 26.3 `CfKvClient` 已實作(`arcrun/cli/src/lib/cf-api.ts`),需移植到 `cypher-executor/src/lib/`
- [ ] 26.4 `credential-injector.ts` 改用 userKv 取得加密 credential
- [ ] 26.5 webhook 路由注入 userKv
- _Requirements: 6.4, 6.5, 6.6_
- [ ] 27. 端對端測試(用戶 KV 隔離)
- _Requirements: 6.4, 6.5_
---
## Phase 7:公眾零件統計與貢獻審核
- [x] 28. Analytics 基礎設施已建立
- [x] 28.1 `execution-logger.ts` 建立,`writeExecutionVerdict` 寫入 `ANALYTICS_KV`fire-and-forget
- [x] 28.2 `/execute` 路由已整合 `waitUntil(writeExecutionVerdict(...))`
- [x] 28.3 `/webhooks/:token/trigger` 路由已補上 `waitUntil(writeExecutionVerdict(...))`
- _Requirements: 7.2_
- [ ] 29. registry Worker analytics 端點
- [ ] 29.1 新增 `POST /analytics/record` 路由,原子更新 `ANALYTICS_KV`
- [ ] 29.2 `GET /components` 回傳加入 `total_runs``success_rate``avg_duration_ms`
- _Requirements: 7.3, 7.6_
- [x] 30. `author` 欄位已加入 contract.yaml 規格
- _Requirements: 7.1_
- [x] 31. 零件提交審核流程已實作(`arcrun/registry/src/actions/submitComponent.ts`
- [x] 31.1 沙盒驗收流程(sandboxAcceptance.ts):size_check + syscall_scan 已實作;cold_start + gherkin_tests 為 Phase 0 mock
- [x] 31.2 `SUBMISSIONS_KV` 儲存元數據,預設 `visibility: author_only`
- [ ] 31.3 `PATCH /submit/:id/approve` → 將 visibility 改為 `public`(待實作)
- [ ] 31.4 Gherkin 測試執行(取代 mock
- _Requirements: 8.2, 8.3, 8.4, 8.5_
---
## 待辦(無相依順序,可平行處理)
- [ ] A. `builtins/` 清理:`initComponents.ts` 仍用舊的 HTTP endpoint 模式上架零件(`buildComponentDefs` 含 URL),應改為呼叫 `POST /submit` 送 WASM binary + contract,或直接移除 builtins(功能已整合到 registry
- [ ] B. `validate` 指令 credential 檢測邏輯修復(見 Phase 4 Task 18
- [ ] C. `acr init` 加入 `ARCRUN_ENCRYPTION_KEY` 設定步驟
- [ ] D. `acr parts` YAML 解析改用 `js-yaml`
---
## Notes
- 標記 `*` 的子任務為選填,可跳過以加速 MVP 交付
- Gherkin 測試執行(sandbox 步驟 d)為 Phase 0 mockPhase 7 補充
- cold-start 測量(sandbox 步驟 b)為 Phase 0 mockPhase 2 補充
- CF API Token 永遠不離開用戶本機,arcrun.dev 只收 email + account_id + kv_namespace_id
@@ -0,0 +1,822 @@
# Design Document: u6u Platform Evolution
## Overview
u6u 平台演進的核心目標是將現有的「HTTP endpoint 零件 + 單一 Cloudflare 部署」架構,演進為「WASM 零件模型 + 三層物理部署 + 雙面翻轉畫布」的完整平台。
設計的最高原則是 **Dogfooding**:每一層都是下一層的第一個用戶。底層先建立最小可運行的能力,再用自己的方式往上蓋。這確保每個設計決策都被真實使用場景驗證,而非紙上談兵。
### Bootstrap 順序(不可跳過)
```
Phase 0:最小 WASM 執行核心
→ Component Dispatcher 能在 CF Workers 執行一個 .wasmstdin/stdout JSON
→ validate_json.wasm 作為第一個真實零件(TinyGo< 50KB,驗證整個 pipeline
→ Component Registry API/guide、/validate-contract、/components
Phase 1:遷移現有零件
→ 將 u6u-builtins 的 20 個 HTTP endpoint 逐一遷移為 .wasm
→ 遷移期間 Component Dispatcher 雙模式並存(HTTP fallback
→ 每個零件附帶 component.contract.yaml
Phase 2Cypher 語意擴展 + Multi-Tier Dispatcher
→ 支援 IS_A、ON_SUCCESS、ON_FAIL、CALLS_SUBFLOW、ON_CLICK
→ Component Dispatcher 路由層(Tier 1 CF / Tier 2 workerd / Tier 3 Wazero
Phase 3:前端畫布(用自己的 Web Components 開發)
→ 先建立 Web Components 零件庫(u6u-btn、u6u-card 等)
→ 畫布本身用這些 Web Components 組裝
→ 雙面翻轉介面
```
### 關鍵設計約束
- **KBDB 不變量**:永遠只有三張表(blocks / templates / slots),不新增表
- **API-First 鐵律**:所有跨服務通訊只透過 HTTP API,禁止相對路徑引用
- **零件 I/O 不變量**:唯一合法的 I/O 模型是 `stdin_stdout_json`
- **Tier 3 約束**:無 V8、無 Node.js、無網路,所有零件必須在 Wazero 上跑
---
## Architecture
### 系統全景圖
```mermaid
graph TB
subgraph "Tier 1 — Cloudflare Workers(雲端)"
CE[Cypher Executor<br/>GraphExecutor]
CD[Component Dispatcher<br/>路由層]
CR[Component Registry<br/>KBDB HTTP API]
KBDB[(KBDB<br/>blocks/templates/slots<br/>+ Vectorize)]
R2[(R2<br/>.wasm 二進位)]
CE --> CD
CD --> CR
CR --> KBDB
CR --> R2
end
subgraph "Tier 2 — workerd self-hosted(企業地端)"
T2D[Tier 2 Dispatcher<br/>同 wasi-shim,不同部署]
T2R[本地 Registry 快取]
T2D --> T2R
end
subgraph "Tier 3 — 邊緣載具"
T3E[Go 排程引擎]
Wazero[Wazero Runtime]
SQLite[(SQLite<br/>本地 KBDB)]
DTN[DTN 佇列]
T3E --> Wazero
T3E --> SQLite
T3E --> DTN
end
subgraph "前端"
Canvas[雙面翻轉畫布<br/>React 19 + Web Components]
WC[Web Components 零件庫<br/>u6u-btn / u6u-card / ...]
Canvas --> WC
end
CD -->|WASM 執行| Tier1WASM[.wasm 執行]
CD -->|Cypher binding| ExtSvc[外部服務<br/>MCP / n8n / 任意 URL]
CD -->|HTTP| T2D
T2D -->|Wazero IPC| Wazero
DTN -->|Burst 傳輸| T2D
Canvas -->|u6u:trigger event| CE
```
### Cypher Binding 的正確定義
**Cypher binding** 是 u6u 的核心執行機制,指「用 Cypher 三元組語法把零件串接成工作流,串接關係儲存在 KBDB,不寫死在程式碼裡」。
這個概念相對於 Cloudflare Workers 原生的 **Service Binding**(需要 deploy、串接關係寫死在 wrangler.toml)。
`cypher-executor` 就是執行 Cypher binding 的引擎。
**零件本身只有兩種 component_type**
| component_type | 說明 | 需要 deploy |
|---|---|---|
| `wasm` | 所有後端零件(內建或用戶自建),本地 WASM 執行 | 否 |
| `service_binding` | 多個零件預組合成單一高頻零件的效能最佳化(如 OAuth + GSheets 常用組合) | 是 |
> **重要:`cypher_binding` 不是 component_type。** 它是整個執行引擎的名字,描述「零件如何被串接」,而不是「零件如何被執行」。所有零件(不管是內建還是用戶自建、不管是打外部 API 還是純邏輯)都是 `.wasm`,透過 Cypher 三元組串接。
> **所有後端零件都是 `.wasm`。** 需要呼叫外部 HTTP API 的零件(如 google-sheets、http-request),透過 WASI shim 注入的 **host function** 發出網路請求,不在 .wasm 內部直接呼叫網路 syscall。
### Component Dispatcher 路由決策樹
```mermaid
flowchart TD
A[Cypher Executor 呼叫零件 id] --> B{查 Component Registry<br/>取得合約}
B --> C{component_type?}
C -->|wasm| E{當前 Tier?}
C -->|service_binding| SB{有 CF Service Binding?}
SB -->|是| SBExec[CF Service Binding 執行<br/>需 deploy,效能最佳]
SB -->|否| SBErr[回傳錯誤:binding 未宣告]
E -->|Tier 1 / Tier 2| I[workerd WASM<br/>WebAssembly.instantiate<br/>+ WASI shim(兩者相同)]
E -->|Tier 3| L[Wazero IPC<br/>stdin/stdout,完全離線]
I --> RC{runtime_compat<br/>包含 cf-workers?}
RC -->|否| J[回傳 RUNTIME_INCOMPATIBLE 錯誤]
RC -->|是| Exec[執行 .wasm]
```
### KBDB 資料模型(tpl-component
```mermaid
erDiagram
TEMPLATES {
string template_id "tpl-component"
string name
string description
}
BLOCKS {
string block_id "comp-{id}-{version}"
string template_id
string user_id
string page_name
}
SLOTS {
string slot_id
string block_id
string key
string value
}
TEMPLATES ||--o{ BLOCKS : "defines"
BLOCKS ||--o{ SLOTS : "has"
```
---
## Components and Interfaces
### 1. Component Registry`u6u-core/registry/`
Component Registry 是 KBDB 的薄包裝層,透過 HTTP API 管理零件合約。
#### 零件命名機制
零件有兩個名稱,職責完全不同:
| 欄位 | 由誰決定 | 用途 | 範例 |
|---|---|---|---|
| `display_name` | 建立者自由取 | 顯示用,不影響任何邏輯 | `宇宙無敵 GSheets 超級寫入器` |
| `canonical_id` | Registry AI 正規化後確認 | 搜尋、版本控制、Cypher 引用的唯一鍵 | `gsheets_create_table` |
**canonical_id 正規化流程:**
```
提交者輸入 display_name
Registry 用 Workers AI 建議 canonical_id
(格式:{service}_{verb}_{object},全小寫底線)
同時搜尋 Vectorize,若相似度 > 0.9 的 canonical_id 已存在
→ 提示「可能與 gsheets_create_table 重複,是否作為新版本提交?」
提交者確認或修改 canonical_id
上架,canonical_id 永久不變
```
#### 零件分類機制
採用「強制 category + 自由 tags」雙層分類:
- **`category`**:強制填,有限集合,定義前後端邊界
- `logic`:後端邏輯零件(.wasm,純計算/轉換)
- `api`:後端 API 零件(.wasm + cypher_binding,呼叫外部服務)
- `ui`:前端 UI 元件(Web Component,瀏覽器執行)
- `style`:前端樣式零件(CSS tokens
- `anim`:前端動畫零件
- **`tags`**:自由增加,跨零件共享語意
- 例:`gsheets_create_table``["google", "sheets", "spreadsheet", "storage", "write"]`
- 例:`excel_write_row``["microsoft", "excel", "spreadsheet", "storage", "write"]`
- 搜尋「外部存儲」時,兩個都能透過 Vectorize 語意搜尋找到
**HTTP 端點:**
```
GET /components/guide → 機器可讀開發指引(Markdown)
POST /components/validate-contract → 驗證 component.contract.yaml 格式
POST /components → 提交零件(.wasm + contract)觸發沙盒驗收
GET /components/:id → 取得零件合約(最優版本)
GET /components/:id/versions → 取得所有版本清單(含評分)
GET /components/search?q=... → 語意搜尋零件
```
**KBDB 整合:**
- 每個零件版本 = 一個 Block`block_id = comp-{id}-{version}`
- Template = `tpl-component`(預先建立,不新增表)
- `.wasm` 二進位存 R2KBDB slot 只存 `wasm_r2_key`
- `description` + `tags` 欄位寫入 Vectorize 索引,支援語意搜尋
**Slot 欄位對應(tpl-component):**
| Slot key | 說明 | 範例值 |
|---|---|---|
| `canonical_id` | 正規化功能名稱(永久不變,搜尋/版本控制用) | `gsheets_create_table` |
| `display_name` | 建立者自取的顯示名稱 | `宇宙無敵 GSheets 超級寫入器` |
| `category` | 零件分類(有限集合) | `logic` / `api` / `ui` / `style` / `anim` |
| `version` | 實作版本 | `v1` |
| `wasi_target` | WASM 目標 | `preview1` |
| `stability` | 穩定性標籤 | `floating` |
| `runtime_compat` | 相容 runtimeJSON 陣列) | `["cf-workers","wazero"]` |
| `component_type` | 零件類型 | `wasm` / `service_binding` |
| `max_size_kb` | 體積上限 | `2048` |
| `max_cold_start_ms` | 冷啟動上限 | `50` |
| `no_network_syscall` | 禁止網路 syscall | `true` |
| `input_schema` | JSON SchemaJSON 字串) | `{"type":"object",...}` |
| `output_schema` | JSON SchemaJSON 字串) | `{"type":"object",...}` |
| `gherkin_tests` | 測試案例(JSON 字串) | `[{"scenario":"..."}]` |
| `wasm_r2_key` | R2 物件鍵(wasm 模式) | `components/validate_json/v1.wasm` |
| `service_binding_key` | CF binding keyservice_binding 模式) | `CLINIC_GSHEETS` |
| `description` | 自然語言描述(寫入 Vectorize) | `在 Google Sheets 建立新工作表` |
| `tags` | 自由標籤(JSON 陣列,跨零件共享語意) | `["google","sheets","storage","write"]` |
| `success_rate` | 成功率(0-1 | `0.98` |
| `avg_duration_ms` | 平均執行時間 | `12` |
| `call_count` | 被調用次數 | `1024` |
| `status` | 狀態 | `active` / `deprecated` / `tombstone` |
| `deprecated_at` | 棄用時間戳記 | `1700000000000` |
### 2. Component Dispatcher`cypher-executor/src/lib/component-loader.ts` 擴展)
Component Dispatcher 是 `createComponentLoader` 的升級版,新增 WASM 執行路徑。
**介面定義:**
```typescript
// 零件類型(只有兩種)
type ComponentType =
| 'wasm' // 所有後端零件,透過 Cypher binding 串接,本地 WASM 執行
| 'service_binding'; // 效能最佳化:CF Service Binding,需 deploy,用於高頻預組合零件
// 新版 ComponentDescriptor
type ComponentDescriptor = {
component_type: ComponentType;
// WASM 模式
wasm_r2_key?: string;
runtime_compat?: string[];
max_cold_start_ms?: number;
// Service Binding 模式(CF Worker 間高效呼叫,需 deploy
binding?: string; // wrangler.toml 中宣告的 binding key
path?: string;
};
```
**Tier 1 WASM 執行(CF Workers 原生):**
Cloudflare Workers 原生支援 `WebAssembly.instantiate`,但 WASI preview1 需要手動實作 WASI imports。設計採用輕量 WASI shim 方案:
```typescript
// WASI preview1 shim(只實作 stdin/stdout/stderr,其餘 syscall 回傳 ENOSYS
function createWasiImports(stdin: Uint8Array): {
imports: WebAssembly.Imports;
getStdout: () => Uint8Array;
} {
const stdoutChunks: Uint8Array[] = [];
let stdinOffset = 0;
return {
imports: {
wasi_snapshot_preview1: {
fd_write: (fd: number, iovs: number, iovs_len: number, nwritten: number) => { /* ... */ },
fd_read: (fd: number, iovs: number, iovs_len: number, nread: number) => { /* ... */ },
proc_exit: (code: number) => { throw new Error(`wasm exit: ${code}`); },
// 其餘 syscall 回傳 ENOSYS76
fd_seek: () => 76,
fd_close: () => 0,
environ_get: () => 0,
environ_sizes_get: () => 0,
args_get: () => 0,
args_sizes_get: () => 0,
clock_time_get: () => 0,
random_get: (buf: number, buf_len: number) => { /* crypto.getRandomValues */ return 0; },
},
},
getStdout: () => { /* 合併 stdoutChunks */ },
};
}
```
> **設計決策**:不使用 `@cloudflare/workers-wasi`(已停止維護)。改用自製輕量 WASI shim,只實作 `fd_read`/`fd_write`/`proc_exit`/`random_get`,其餘 syscall 回傳 `ENOSYS`。這足以支援 TinyGo/Rust/AssemblyScript 的 stdin/stdout 零件,且不引入外部依賴。
**執行流程:**
```
1. 從 R2 取得 .wasm 二進位(ArrayBuffer
2. WebAssembly.compile(buffer) → WebAssembly.Module
3. 建立 WASI imports shim(注入 stdin = JSON.stringify(input)
4. WebAssembly.instantiate(module, imports)
5. 呼叫 _start() 或 main()
6. 從 stdout buffer 讀取輸出
7. JSON.parse(stdout) → output
```
**R2 快取策略:**
- 第一次呼叫:從 R2 fetch `.wasm``WebAssembly.compile` 後快取 `WebAssembly.Module`Worker 記憶體,跨請求共享)
- 後續呼叫:直接用快取的 Module,只重新 instantiate(避免重複編譯)
### 3. Cypher Triplet Parser 擴展(`cypher-executor/src/actions/triplet-parser.ts`
現有 parser 只支援 `PIPE / IF / FOREACH / CONTINUE`。需擴展支援新語意關係。
**新增 EdgeType**
```typescript
export type EdgeType =
| 'PIPE' | 'IF' | 'FOREACH' | 'CONTINUE' // 現有
| 'IS_A' | 'ON_SUCCESS' | 'ON_FAIL' // 新增:執行語意
| 'ON_CLICK' | 'CALLS_SUBFLOW' // 新增:觸發語意
| 'CONTAINS' | 'HAS_STYLE' | 'HAS_BEHAVIOR'; // 新增:結構語意
```
**URI 協議解析:**
```typescript
// 節點 componentId 解析
function resolveComponentId(uri: string): {
type: 'component' | 'workflow' | 'ui' | 'style';
canonicalId: string;
stability: 'floating' | 'stable' | 'pinned';
pinnedVersion?: string;
} {
// component://validate_json@stable → { type: 'component', canonicalId: 'validate_json', stability: 'stable' }
// component://validate_json@pinned:v1 → { type: 'component', canonicalId: 'validate_json', stability: 'pinned', pinnedVersion: 'v1' }
// workflow://wf_save_to_db → { type: 'workflow', canonicalId: 'wf_save_to_db', stability: 'floating' }
// ui://u6u-btn → { type: 'ui', canonicalId: 'u6u-btn', stability: 'floating' }
}
```
**ON_SUCCESS / ON_FAIL 執行語意:**
GraphExecutor 需要區分「節點執行成功」vs「節點執行失敗」,而非依賴 context 欄位:
```typescript
// 在 executeNode 中,捕捉 try/catch 後分別走 ON_SUCCESS / ON_FAIL 邊
case 'ON_SUCCESS':
// 只在上游節點成功時執行
if (!nodeError) {
result = await this.executeNode(nextNode, ...);
}
break;
case 'ON_FAIL':
// 只在上游節點失敗時執行(接收 error context
if (nodeError) {
result = await this.executeNode(nextNode, graph, { ...context, error: nodeError }, ...);
}
break;
```
**CALLS_SUBFLOW 執行語意:**
```typescript
case 'CALLS_SUBFLOW': {
// 從 KBDB 載入子 Workflow 定義
const subWorkflowId = nextNode.componentId!.replace('workflow://', '');
const subGraph = await loadWorkflowFromKBDB(subWorkflowId, env);
const subExecutor = new GraphExecutor(loader);
const subResult = await subExecutor.execute(subGraph, result as Record<string, unknown>, kvNamespace);
result = { ...(result as Record<string, unknown>), ...subResult.data as Record<string, unknown> };
break;
}
```
### 4. Web Components 零件庫(`u6u-core/web-components/`
Web Components 以原生 Custom Elements API 實作,不依賴任何框架。
**`<u6u-btn>` 介面:**
```typescript
// HTML attributes
interface U6uBtnAttributes {
label: string; // 顯示文字
color?: string; // 主題色(CSS custom property
tooltip?: string; // 滑鼠懸停提示(純靜態)
workflow?: string; // workflow://id
disabled?: boolean;
}
// 發出的自訂事件
interface U6uTriggerEvent extends CustomEvent {
detail: {
workflowId: string;
payload: Record<string, unknown>;
};
}
```
**`<u6u-card>` Smart Container 邏輯:**
```typescript
// u6u-card 攔截子元件的 u6u:trigger 事件
// 收集同容器內所有 u6u-text-input / u6u-text-field 的值
// 合併至 payload 後再向上冒泡
connectedCallback() {
this.addEventListener('u6u:trigger', (e: Event) => {
const trigger = e as CustomEvent;
e.stopPropagation();
const inputs = this.querySelectorAll('u6u-text-input, u6u-text-field');
const collected: Record<string, unknown> = {};
inputs.forEach(input => {
const name = input.getAttribute('name');
const value = (input as any).value;
if (name) collected[name] = value;
});
this.dispatchEvent(new CustomEvent('u6u:trigger', {
bubbles: true,
composed: true,
detail: {
...trigger.detail,
payload: { ...trigger.detail.payload, ...collected },
},
}));
});
}
```
### 5. 雙面翻轉畫布(`inkstone-admin/frontend/web/`
畫布本身用 React 19 + Web Components 組裝,體現 dogfooding 原則。
**翻轉狀態機:**
```mermaid
stateDiagram-v2
[*] --> UIView: 初始狀態
UIView --> LogicView: 點擊翻面按鈕
LogicView --> UIView: 點擊翻面按鈕
LogicView --> Editing: 修改三元組
Editing --> Saving: 確認儲存
Saving --> LogicView: KBDB 寫入成功
Saving --> LogicView: KBDB 寫入失敗(顯示錯誤)
```
---
## Data Models
### Component Contract YAML(完整規格)
```yaml
canonical_id: "validate_json" # 正規化功能名稱(永久不變,Registry AI 正規化後確認)
display_name: "JSON 格式驗證器" # 建立者自取,顯示用
category: "logic" # logic | api | ui | style | anim
version: "v1" # 實作版本
wasi_target: "preview1" # WASM 目標格式
stability: "floating" # floating | stable | pinned
runtime_compat:
- "cf-workers"
- "workerd"
- "wazero"
constraints:
max_size_kb: 2048
max_cold_start_ms: 50
no_network_syscall: true
no_filesystem_syscall: true
io_model: "stdin_stdout_json" # 唯一合法值
input_schema:
type: object
required: ["json_string"]
properties:
json_string:
type: string
description: "待驗證的 JSON 字串"
output_schema:
type: object
properties:
valid:
type: boolean
error:
type: string
description: "驗證失敗時的錯誤訊息"
gherkin_tests:
- scenario: "合法 JSON 通過驗證"
given: '{"json_string":"{\"key\":\"value\"}"}'
then_contains: '{"valid":true}'
- scenario: "非法 JSON 回傳錯誤"
given: '{"json_string":"not-json"}'
then_contains: '{"valid":false,"error":'
tags: ["validation", "json", "utility"]
description: "驗證輸入字串是否為合法 JSON 格式"
```
### 零件開發語言決策
**內建零件使用 TinyGo**(純邏輯零件)和 TinyGo + `json.RawMessage`(需要任意 HTTP body 的零件)。不引入 Rust 作為內建零件語言。
**用戶自建零件支援三種語言,按難度分層:**
| 語言 | 目標用戶 | JSON 能力 | 備註 |
|---|---|---|---|
| **AssemblyScript** | 一般用戶(TS 背景) | 社群套件 `assemblyscript-json`,支援動態 JSON | 語法最接近 TS,門檻最低;靜默錯誤風險,沙盒驗收必須通過 |
| **TinyGo** | 技術較強用戶(Go 背景) | 靜態 struct 完整支援;`json.RawMessage` 處理任意 body | 編譯期報錯,AI 生成安全性較高 |
| **Rust** | 進階用戶 | `serde_json::Value` 完整動態 JSON | 生態最成熟,體積最小;學習曲線陡 |
**`/components/guide` 端點提供三份語言範例**,用戶根據自身背景選擇。
**內建零件 JSON 策略(TinyGo):**
```go
// 固定 schema 零件(google-sheets、gmail 等)→ 靜態 struct
type Input struct {
SpreadsheetId string `json:"spreadsheet_id"`
Range string `json:"range"`
AccessToken string `json:"access_token"`
}
// 任意 body 零件(http-request)→ json.RawMessage 傳遞 raw bytes,不解析
type Input struct {
URL string `json:"url"`
Method string `json:"method"`
Body json.RawMessage `json:"body"` // 任意 JSON,不解析
}
```
### Workflow Cypher 三元組(完整語法)
```yaml
kind: Workflow
id: wf_submit_form
triplets:
# 節點類型宣告
- "btn_submit >> IS_A >> ui://u6u-btn"
- "step_validate >> IS_A >> component://validate_json"
- "step_save >> IS_A >> component://kbdb_write"
# 前端觸發後端
- "btn_submit >> ON_CLICK >> step_validate"
# 成功/失敗分支
- "step_validate >> ON_SUCCESS >> step_save"
- "step_validate >> ON_FAIL >> step_notify_error"
# 子流程呼叫
- "step_save >> ON_SUCCESS >> CALLS_SUBFLOW >> workflow://wf_notify_user"
# 容器結構
- "card_main >> CONTAINS >> btn_submit"
- "card_main >> CONTAINS >> input_name"
```
### Evaluation BlockKBDB tpl-evaluation
每次 Workflow 執行後,Evaluator Agent 寫入一個 Evaluation Block
| Slot key | 說明 |
|---|---|
| `run_id` | 執行唯一 ID |
| `workflow_id` | Workflow ID |
| `component_id` | 被評價的零件 ID |
| `verdict` | `success` / `failed` / `timeout` |
| `duration_ms` | 執行時間 |
| `error_message` | 失敗訊息(可選) |
| `evaluated_at` | 評價時間戳記 |
### Pitfall BlockKBDB tpl-pitfall
| Slot key | 說明 |
|---|---|
| `component_id` | 問題零件 ID |
| `failure_pattern` | 失敗模式描述 |
| `first_seen_at` | 首次發現時間戳記 |
| `occurrence_count` | 發生次數 |
---
## Correctness Properties
*A property is a characteristic or behavior that should hold true across all valid executions of a system — essentially, a formal statement about what the system should do. Properties serve as the bridge between human-readable specifications and machine-verifiable correctness guarantees.*
### Property Reflection(去重分析)
在寫出最終屬性前,先做冗餘分析:
- **1.1 + 1.2**:都是合約欄位完整性驗證,合併為 Property 1「合約格式完整性」
- **2.1 + 2.2**:驗收流程執行 + 失敗回報,合併為 Property 2「沙盒驗收流程正確性」
- **2.3 + 2.4**:提交後可讀取 + 冪等提交,合併為 Property 3「零件提交冪等性與持久性」
- **3.1 + 3.5**WASM 執行 + 雙模式路由,合併為 Property 4「Component Dispatcher 路由正確性」
- **3.4 + 6.4**:不相容 Tier 回傳錯誤 + 結構化錯誤,合併為 Property 5「Dispatcher 錯誤結構完整性」
- **4.1**URI 解析 round-trip,獨立為 Property 6
- **4.2 + 4.4**:版本選擇算法(floating 最高分 + pinned 固定版本),合併為 Property 7「版本選擇策略正確性」
- **4.5**:版本保留不變量,獨立為 Property 8
- **5.7**Confluence 屬性,獨立為 Property 9
- **8.3 + 8.4 + 8.5**Web Components 事件與渲染,合併為 Property 10「Web Components 事件與渲染冪等性」
- **10.6 + 12.5**:評價冪等性 + 查詢冪等性,合併為 Property 11「系統操作冪等性」
最終保留 11 個屬性,每個提供獨立驗證價值。
---
### Property 1: 合約格式完整性
*For any* component contract object,若缺少任何必填欄位(`id``version``wasi_target``stability``runtime_compat``constraints.max_size_kb``constraints.max_cold_start_ms``constraints.no_network_syscall``constraints.io_model``input_schema``output_schema``gherkin_tests`),合約驗證器 SHALL 拒絕該合約並回傳包含缺失欄位名稱的錯誤;反之,包含所有必填欄位的合約 SHALL 通過格式驗證。
**Validates: Requirements 1.1, 1.2, 1.4**
---
### Property 2: 沙盒驗收流程正確性
*For any* 提交的零件(.wasm + contract),若零件在驗收步驟 N 失敗,Component_Registry 的回應 SHALL 包含步驟 N 的名稱與具體失敗原因,且不執行步驟 N+1 之後的步驟。
**Validates: Requirements 2.1, 2.2**
---
### Property 3: 零件提交冪等性與持久性
*For any* 通過驗收的零件(id, version),提交後從 Component_Registry 讀取該零件的合約,所有欄位值 SHALL 與提交時的合約完全一致(序列化 round-trip);對相同 (id, version) 重複提交 N 次,KBDB 中 SHALL 只存在一個對應的 Block。
**Validates: Requirements 2.3, 2.4**
---
### Property 4: Component Dispatcher 路由正確性
*For any* 零件合約,若 `io_model = "stdin_stdout_json"`Component_Dispatcher SHALL 使用 WASM 執行路徑,將 input JSON 寫入 stdin,從 stdout 讀取 output JSON;若 `io_model = "http_endpoint"`SHALL 使用 HTTP 路徑。對任意合法 JSON inputWASM 執行路徑的輸出 SHALL 與 HTTP 執行路徑的輸出語意等效。
**Validates: Requirements 3.1, 3.5, 3.6**
---
### Property 5: Dispatcher 錯誤結構完整性
*For any* (component_id, tier) 組合,若該零件的 `runtime_compat` 不包含當前 tierComponent_Dispatcher 的錯誤回應 SHALL 同時包含:零件 id、當前 tier 名稱、已嘗試的呼叫路徑清單,三個欄位缺一不可。
**Validates: Requirements 3.4, 6.4**
---
### Property 6: 零件 URI 解析 Round-Trip
*For any* 合法的零件 URI 字串(格式為 `component://id``component://id@stable``component://id@pinned:vN`),解析後再重新序列化 SHALL 產生與原始 URI 語意等效的字串;解析出的 `id``stability``pinnedVersion` 欄位 SHALL 與原始 URI 中的對應部分完全一致。
**Validates: Requirements 4.1**
---
### Property 7: 版本選擇策略正確性
*For any* 零件 id 下的版本集合(每個版本有 success_rate、avg_duration_ms、call_count 評分),當 stability = `floating` 時,Component_Dispatcher SHALL 選取「成功率 × 速度評分 × 被調用次數」最高的版本;當 stability = `pinned:vN` 時,無論版本集合中其他版本的評分如何,SHALL 永遠選取版本 vN。
**Validates: Requirements 4.2, 4.4**
---
### Property 8: 歷史版本永久保留不變量
*For any* 已上架的零件版本(id, version),無論該版本後來被標記為 `deprecated``tombstone`,其 `.wasm` 二進位 SHALL 永遠可從 R2 讀取,且 `pinned` 引用 SHALL 永遠能透過 Component_Dispatcher 執行該版本。
**Validates: Requirements 4.5, 10.5**
---
### Property 9: Cypher 三元組解析 Confluence(順序無關性)
*For any* 合法的 Cypher 三元組集合,無論三元組在輸入陣列中的排列順序如何,`parseTriplets` 產生的執行圖(節點集合、邊集合、拓撲結構)SHALL 語意等效——即相同的節點 id 集合、相同的 (from, to, type) 邊集合。
**Validates: Requirements 5.7**
---
### Property 10: Web Components 事件與渲染冪等性
*For any* workflow URI 設定於 `<u6u-btn>``workflow` attribute,使用者點擊後發出的 `u6u:trigger` 事件 detail 中的 `workflowId` SHALL 與 URI 中的 id 完全一致;*For any* 一組具名 `<u6u-text-input>` 元件置於 `<u6u-card>` 內,觸發事件後收集到的 payload SHALL 包含所有輸入元件的 name-value 對;*For any* attribute 值,對同一 Web Component 設定相同 attribute 值 N 次,渲染結果 SHALL 與設定一次相同(冪等渲染)。
**Validates: Requirements 8.3, 8.4, 8.5**
---
### Property 11: 系統操作冪等性
*For any* Workflow 執行日誌(run_id),Evaluator_Agent 對相同 run_id 處理 N 次,KBDB 中 SHALL 只存在一個對應的 Evaluation Block,不產生重複記錄;*For any* Component_Registry 讀取操作的查詢參數,在 KBDB 資料不變的前提下,對相同參數呼叫 N 次 SHALL 回傳完全相同的結果。
**Validates: Requirements 10.6, 12.5**
---
## Error Handling
### Component Dispatcher 錯誤分類
| 錯誤類型 | 觸發條件 | 回應格式 |
|---|---|---|
| `COMPONENT_NOT_FOUND` | KBDB 中找不到零件 | `{ error: "COMPONENT_NOT_FOUND", component_id, tier }` |
| `RUNTIME_INCOMPATIBLE` | runtime_compat 不含當前 Tier | `{ error: "RUNTIME_INCOMPATIBLE", component_id, tier, attempted_paths: [] }` |
| `WASM_EXECUTION_TIMEOUT` | 超過 max_cold_start_ms | `{ error: "WASM_EXECUTION_TIMEOUT", component_id, timeout_ms }` |
| `WASM_INVALID_OUTPUT` | stdout 不是合法 JSON | `{ error: "WASM_INVALID_OUTPUT", component_id, raw_output }` |
| `WASM_SYSCALL_VIOLATION` | .wasm 嘗試網路/檔案 syscall | `{ error: "WASM_SYSCALL_VIOLATION", component_id, syscall_name }` |
| `CONTRACT_VALIDATION_FAILED` | 合約格式不合規 | `{ error: "CONTRACT_VALIDATION_FAILED", missing_fields: [] }` |
### 沙盒驗收失敗回應格式
```json
{
"success": false,
"failed_step": "syscall_scan",
"reason": "發現禁止的 syscallsock_connect",
"guide_anchor": "#syscall-constraints",
"component_id": "my_component",
"version": "v1"
}
```
### Tier 3 離線錯誤處理
Tier 3 在離線環境中,所有無法執行的操作都寫入 DTN 佇列,不拋出錯誤:
```go
// Go 排程引擎的錯誤處理策略
type DTNEntry struct {
Type string // "missing_component" | "sync_log" | "request_wasm"
Payload json.RawMessage
CreatedAt time.Time
RetryCount int
}
```
### Web Components 錯誤邊界
`<u6u-btn>``workflow` attribute 未設定時,點擊不發出事件,僅在 console 輸出警告:
```
[u6u-btn] workflow attribute is not set, click event ignored
```
---
## Testing Strategy
### 雙軌測試策略
本功能採用「單元測試 + 屬性測試」雙軌策略:
- **單元測試(Vitest)**:驗證具體範例、邊界條件、錯誤情境
- **屬性測試(fast-check**:驗證上述 11 個 Correctness Properties,每個屬性最少執行 100 次迭代
### 屬性測試配置
使用 `fast-check`(已在 tech stack 中),每個屬性測試標記格式:
```typescript
// Feature: arcrun-platform-evolution, Property N: {property_text}
it.prop([fc.record({ id: fc.string(), version: fc.string(), ... })])(
'Property 1: 合約格式完整性',
(contract) => {
// ...
},
{ numRuns: 100 }
);
```
### 各 Phase 測試重點
**Phase 0WASM 執行核心):**
- Property 4WASM 執行路徑 round-trip`validate_json.wasm` 作為 ground truth
- Property 1:合約格式驗證
- 單元測試:WASI shim 的 `fd_read`/`fd_write` 正確性
**Phase 1(零件遷移):**
- Property 3:提交冪等性(20 個零件各提交兩次,驗證無重複)
- Property 2:沙盒驗收流程(各步驟失敗案例)
- Property 8:歷史版本保留(deprecate 後仍可讀取)
**Phase 2Cypher 擴展):**
- Property 9Confluence(三元組順序無關性,fast-check shuffle
- Property 6URI 解析 round-trip
- Property 7:版本選擇策略(floating 最高分、pinned 固定版本)
- Property 5:錯誤結構完整性
**Phase 3(前端畫布):**
- Property 10Web Components 事件與渲染冪等性(`@testing-library/react` + fast-check
- Property 11:評價冪等性(Evaluator Agent 重複處理)
### 整合測試
以下場景使用整合測試(1-3 個具體範例,不用 PBT):
- Tier 1 CF Workers 環境中實際執行 `validate_json.wasm`(驗證 WASM 在 Workers 環境可運行)
- KBDB tpl-component Template 建立與 Slot 讀寫(驗證 KBDB 整合)
- R2 `.wasm` 上傳與讀取(驗證 R2 整合)
- Vectorize 語意搜尋(驗證「查詢 Google Sheets 資料」能找到 `gsheets_get_entries`
### 單元測試重點(非 PBT
- WASI shim`fd_read` 正確讀取 stdin、`fd_write` 正確寫入 stdout
- `evaluateCondition`:現有條件評估函數的邊界案例
- `resolveComponentId`:URI 解析的邊界案例(空字串、特殊字元)
- `<u6u-card>` Smart Container:巢狀容器的事件冒泡行為
@@ -0,0 +1,219 @@
# Requirements Document
## Introduction
u6u 平台演進規格描述從現況(HTTP endpoint 零件、單一 Cloudflare 部署、無前端畫布)
到目標架構(WASM 零件模型、三層物理部署、雙面翻轉畫布)的完整演進路徑。
本規格涵蓋三個核心演進軸:
1. **零件模型遷移**:將 20 個內建零件從 Cloudflare Worker HTTP endpoint 遷移至 WASI preview1 `.wasm` 格式,附帶 `component.contract.yaml`,以 stdin/stdout JSON 作為唯一 I/O 模型。
2. **多 Tier 執行抽象**:讓 Cypher Executor 透過統一的 Component Dispatcher 介面,跨 Tier 1Cloudflare Workers)、Tier 2workerd 地端叢集)、Tier 3Go + Wazero 邊緣載具)呼叫零件。
3. **前端雙面畫布**:建立以 Web Components 為基礎的視覺化畫布,正面為 UI 視圖,反面為 Cypher 邏輯視圖,智慧容器自動打包表單值。
---
## Glossary
- **Component(零件)**:系統最小執行單元,一個零件只做一件事,以 `.wasm`WASI preview1)或 Web Component 形式存在。
- **Component_Contract**:每個零件附帶的 `component.contract.yaml`,定義 id、version、wasi_target、stability、runtime_compat、constraints、input_schema、output_schema、gherkin_tests。
- **Component_Registry**KBDB 中儲存所有零件合約與 `.wasm` 位置的索引,以 `tpl-component` Template Block 實作。
- **Component_Dispatcher**Cypher Executor 內部的路由層,根據零件的 `runtime_compat` 與目標 Tier 決定呼叫路徑(Service Binding / workerd HTTP / Wazero IPC)。
- **Cypher_Executor**Workflow 執行引擎,解析三元組語法,透過 GraphExecutor 執行節點,現部署於 Cloudflare Workers。
- **Cypher_Triplet**`"A >> 關係 >> B"` 格式的三元組,描述節點間的語意關係。
- **KBDB**:三位一體記憶庫(blocks / templates / slots),搭配 Cloudflare Vectorize,是平台唯一的持久化狀態來源。
- **Tier_1**:雲端層,Cloudflare Workers + D1 + Vectorize + R2,全球無伺服器部署。
- **Tier_2**:企業地端層,workerd 叢集 + Kùzu 或 PostgreSQL + AGE,高機密內網環境。
- **Tier_3**:邊緣載具層,Go 排程引擎 + 內嵌 Wazero + SQLite,無 V8、無網路的極限環境(無人機、AGV)。
- **WASI_Preview1**WebAssembly System Interface preview1 規格,零件唯一合法的 WASM 目標格式。
- **Canvas(畫布)**:前端雙面翻轉介面,正面為 UI 視圖,反面為 Cypher 邏輯視圖。
- **Smart_Container**:畫布上的排版容器(如 `<u6u-card>`),自動打包同容器內所有輸入元件的值並附加至觸發事件的 payload。
- **Forge_AI**:工匠 AI,負責在 Tier 2 地端接收零件規格、生成 TinyGo 程式碼、編譯並測試 `.wasm`
- **Evaluator_Agent**:強制評價代理,每次 Workflow 執行後自動評估成功率、效能、警告訊息。
- **Pitfall_Block**KBDB 中記錄已知問題的 Block,AI 搜尋時強制讀取以繞道。
- **Stability_Tag**:零件版本穩定性標籤,值為 `floating`AI 自動選最優)、`stable`(人工確認才換)、`pinned`(版本凍結)。
- **DTN**Delay-Tolerant NetworkingTier 3 邊緣載具在間歇性網路下的短點射傳輸協議。
---
## Requirements
### Requirement 1:零件合約規格標準化
**User Story:** As a 平台架構師, I want 每個零件都有標準化的 `component.contract.yaml` 合約, so that AI 能透過統一介面讀取零件能力,並在任何 Tier 上驗證相容性。
#### Acceptance Criteria
1. THE Component_Contract SHALL 包含以下必填欄位:`canonical_id`(功能合約名稱,永久不變)、`display_name`(人類可讀名稱,可自由撰寫)、`description`(語意搜尋用途,需精確描述零件能做什麼、適用情境,至少 20 字)、`version`(實作版本)、`wasi_target`(值為 `"preview1"`)、`stability`(值為 `floating``stable``pinned` 之一)、`runtime_compat`(陣列,值為 `cf-workers``workerd``wazero` 的子集)、`constraints``input_schema``output_schema``gherkin_tests`
1a. THE `canonical_id` SHALL 遵循以下命名規範,以確保全庫一致性:
- 格式:`{scope}_{action}``{scope}_{object}``{scope}`(單詞),全部小寫底線,不超過 4 個單詞
- **整合類**category: api):以服務名稱為 scope,可加動作;範例:`gmail``gmail_send``google_sheets``google_sheets_append``telegram``telegram_send`
- **資料處理類**category: data):以資料型別或操作為 scope;範例:`string_ops``array_ops``date_ops``json_transform`
- **控制流類**category: logic):以控制結構名稱命名;範例:`if_control``foreach_control``try_catch``switch``wait`
- **AI 類**category: ai):以 `ai_` 為前綴;範例:`ai_transform_compile``ai_summarize``ai_classify`
- 禁止:中文、空格、大寫、連字號(`-`)、版本號混入 id`gmail_v2``version: v2` 表達)
1b. THE `display_name` SHALL 為人類可讀的自由格式名稱(可中文、可含空格);此欄位供 UI 顯示用,不作為系統識別符。範例:`canonical_id: google_sheets_append``display_name: "Google Sheets — 新增一列"`
1c. THE `description` SHALL 用於 Vectorize 語意搜尋索引;撰寫時應以「能做什麼、適合什麼情境」為核心,避免只寫零件名稱的同義詞。範例:`"傳送 Gmail 電子郵件,適合 Workflow 完成時通知使用者、訂閱確認信、錯誤警報等場景。需要 Gmail OAuth token。"` 而非 `"Gmail 發信零件"`
2. THE Component_Contract SHALL 在 `constraints` 中包含以下欄位:`max_size_kb`(上限 2048)、`max_cold_start_ms`(上限 50)、`no_network_syscall`(布林值)、`io_model`(值為 `"stdin_stdout_json"`)。
3. WHEN 一個零件的 `id` 已存在於 Component_RegistryTHE Component_Registry SHALL 允許以新 `version` 值新增該零件的新實作,而不覆蓋舊版本。
4. THE Component_Contract SHALL 在 `gherkin_tests` 中至少包含一個正常情境(happy path)與一個錯誤情境(error path)的測試案例。
5. IF 一個零件的 `input_schema``output_schema` 涉及序列化或反序列化操作,THEN THE Component_Contract SHALL 包含一個 round-trip 測試案例,驗證 `parse(format(x)) == x`
---
### Requirement 2:零件沙盒驗收流程
**User Story:** As a 零件提交者(AI 或開發者), I want 提交的零件自動通過沙盒驗收, so that 只有符合品質標準的零件才能進入零件宇宙。
#### Acceptance Criteria
1. WHEN 一個零件被提交至 Component_RegistryTHE Component_Registry SHALL 依序執行以下驗收步驟:(a)體積檢查(`.wasm` 小於 `max_size_kb`)、(b)冷啟動時間測量(小於 `max_cold_start_ms`)、(c)syscall 掃描(不含網路或檔案系統 syscall)、(d)Gherkin 測試執行(所有 scenario 100% 通過)、(e)多 runtime 相容測試(`runtime_compat` 列出的所有 runtime)。
2. IF 任一驗收步驟失敗,THEN THE Component_Registry SHALL 拒絕該零件上架,並回傳包含失敗步驟名稱與具體原因的錯誤訊息。
3. WHEN 所有驗收步驟通過,THE Component_Registry SHALL 將零件合約存入 KBDB 的 `tpl-component` Template Block,並記錄上架時間戳記。
4. THE Component_Registry SHALL 以冪等方式執行驗收流程,對相同 `id``version` 的重複提交回傳相同結果而不重複執行測試。
---
### Requirement 3:現有 HTTP 零件遷移至 WASM
**User Story:** As a 平台開發者, I want 將現有 20 個 HTTP endpoint 零件遷移為 WASI preview1 `.wasm` 格式, so that 零件能在 Tier 3 邊緣載具(無 V8、無網路)上執行,消除技術債。
#### Acceptance Criteria
1. THE Component_Dispatcher SHALL 支援以 WASM 模式呼叫零件:讀取 `.wasm` 二進位、透過 WASI preview1 runtime 執行、將 input JSON 寫入 stdin、從 stdout 讀取 output JSON。
2. WHEN 一個 WASM 零件需要呼叫外部 HTTP API(如 Google Sheets),THE Component_Dispatcher SHALL 透過 host function 注入方式提供網路能力,而非允許 `.wasm` 內部直接發出網路 syscall。
3. THE Component_Dispatcher SHALL 在 Tier 1Cloudflare Workers)環境中,以 `workerd` 內建的 WASM 執行能力執行 WASI preview1 零件。
4. WHEN 一個零件的 `runtime_compat` 不包含當前執行環境的 TierTHE Component_Dispatcher SHALL 回傳錯誤,說明該零件不相容於當前 Tier,而非嘗試執行。
5. THE Component_Dispatcher SHALL 在遷移期間同時支援舊有 HTTP endpoint 模式(Service Binding 或外部 URL)與新 WASM 模式,以 Component_Contract 的 `io_model` 欄位區分呼叫路徑。
6. FOR ALL 現有 20 個內建零件,遷移後的 WASM 版本 SHALL 通過與原 HTTP 版本相同的 Gherkin 測試案例(round-trip 等效性)。
---
### Requirement 4:零件版本控制與穩定性標籤
**User Story:** As a Workflow 設計者, I want 在 Cypher 三元組中指定零件的穩定性需求, so that 關鍵業務流程不會因 AI 自動升級零件而中斷。
#### Acceptance Criteria
1. THE Cypher_Executor SHALL 支援以下三種零件引用語法:`component://id`(預設 floating)、`component://id@stable``component://id@pinned:vN`
2. WHEN 一個 Workflow 引用 `component://id`floating),THE Component_Dispatcher SHALL 從 Component_Registry 選取該 `id` 下「成功率 × 速度 × 被調用次數」評分最高的版本執行。
3. WHEN 一個 Workflow 引用 `component://id@stable`THE Component_Dispatcher SHALL 使用當前標記為 stable 的版本,並在有更優版本時記錄提示至 KBDB,但不自動切換。
4. WHEN 一個 Workflow 引用 `component://id@pinned:vN`THE Component_Dispatcher SHALL 永遠使用版本 `vN`,即使該版本已被標記為 Deprecated。
5. THE Component_Registry SHALL 保留所有歷史版本的 `.wasm` 二進位,不因版本淘汰而刪除檔案。
---
### Requirement 5Cypher 語意關係擴展
**User Story:** As a Workflow 設計者, I want Cypher 三元組支援完整的語意關係集合, so that 能描述條件分支、子流程呼叫、前端觸發等複雜業務邏輯。
#### Acceptance Criteria
1. THE Cypher_Executor SHALL 解析並執行以下語意關係:`IS_A`(節點類型宣告)、`ON_SUCCESS`(成功後繼)、`ON_FAIL`(失敗後繼)、`ON_CLICK`(前端點擊觸發)、`CALLS_SUBFLOW`(呼叫子 Workflow)、`CONTAINS`(容器包含關係)、`HAS_STYLE`(樣式關聯)、`HAS_BEHAVIOR`(行為關聯)。
2. WHEN 解析 `IS_A` 關係,THE Cypher_Executor SHALL 從 Component_Registry 載入對應的零件合約,並以合約的 `input_schema` 驗證節點的輸入 context。
3. WHEN 解析 `ON_SUCCESS``ON_FAIL` 關係,THE Cypher_Executor SHALL 根據上游節點的執行結果(成功或拋出錯誤)決定走向,而非依賴 context 中的特定欄位。
4. WHEN 解析 `CALLS_SUBFLOW` 關係,THE Cypher_Executor SHALL 以當前 context 作為子 Workflow 的 initialContext 執行,並將子 Workflow 的輸出合併回主流程 context。
5. WHEN 解析 `ON_CLICK` 關係,THE Cypher_Executor SHALL 接受來自前端 Smart_Container 打包的 payload,並以該 payload 作為 Workflow 的 initialContext。
6. THE Cypher_Executor SHALL 支援 URI 協議前綴:`component://`(零件引用)、`workflow://`Workflow 引用)、`ui://`(前端零件引用)、`style://`(樣式零件引用)。
7. FOR ALL 合法的 Cypher 三元組序列,THE Cypher_Executor SHALL 保證解析結果的冪等性:對相同輸入三元組集合,無論排列順序,產生語意等效的執行圖(Confluence 屬性)。
---
### Requirement 6Component Dispatcher 多 Tier 路由
**User Story:** As a 平台架構師, I want Cypher Executor 透過統一的 Component Dispatcher 介面呼叫跨 Tier 零件, so that Workflow 設計者不需要知道零件部署在哪個 Tier。
#### Acceptance Criteria
1. THE Component_Dispatcher SHALL 根據以下優先序決定呼叫路徑:(1Tier 1Cloudflare Service Binding(若零件部署為 Worker)或 WASM 直接執行;(2Tier 2workerd 叢集 HTTP endpoint;(3Tier 3Wazero IPCstdin/stdout)。
2. WHEN Component_Dispatcher 在 Tier 1 環境中呼叫一個 `runtime_compat` 包含 `cf-workers` 的零件,THE Component_Dispatcher SHALL 優先使用 Cloudflare Service Binding,若 binding 不存在則退回 WASM 執行模式。
3. WHEN Component_Dispatcher 在 Tier 3 環境中呼叫零件,THE Component_Dispatcher SHALL 只使用 Wazero 執行本地 `.wasm` 檔案,不發出任何網路請求。
4. IF Component_Dispatcher 無法在當前 Tier 找到可用的呼叫路徑,THEN THE Component_Dispatcher SHALL 回傳結構化錯誤,包含:零件 id、當前 Tier、嘗試的呼叫路徑清單。
5. THE Component_Dispatcher SHALL 對每次零件呼叫記錄執行時間(ms)、成功或失敗狀態,並非同步寫入 KBDB 的 Evaluation Block,不阻擋主流程。
6. WHILE Component_Dispatcher 執行零件呼叫,THE Component_Dispatcher SHALL 強制套用 Component_Contract 中的 `max_cold_start_ms` 作為逾時上限,超時後回傳逾時錯誤。
---
### Requirement 7Tier 3 邊緣離線生存能力
**User Story:** As a 邊緣載具操作者(無人機、AGV), I want 載具在完全離線環境中仍能執行預載的 Workflow, so that 業務不因網路中斷而停擺。
#### Acceptance Criteria
1. THE Tier_3 執行引擎 SHALL 在無網路連線的環境中,使用本地 SQLite 作為 KBDB 替代儲存,執行預先下載的 Cypher Workflow 與 `.wasm` 零件。
2. WHEN Tier_3 執行引擎在執行中發現缺少所需零件,THE Tier_3 執行引擎 SHALL 記錄缺失零件的 `id``input_schema` 至本地 DTN 佇列,待下次連網時以 Burst 傳輸方式送至 Tier_2 請求代工。
3. WHEN Tier_3 執行引擎收到來自 Tier_2 的新 `.wasm` 零件,THE Tier_3 執行引擎 SHALL 在執行前對該零件進行 syscall 掃描,確認不含網路或檔案系統 syscall,通過後才載入執行。
4. THE Tier_3 執行引擎 SHALL 在 Cypher 圖譜執行中途動態替換失敗零件(如感測器零件因環境變化失效),以 Component_Registry 中相同 `input_schema` 的備用零件繼續執行,不中斷整體 Workflow。
5. WHEN Tier_3 執行引擎重新連線至 Tier_2THE Tier_3 執行引擎 SHALL 將本地執行日誌(包含 trace、評價結果、Pitfall 記錄)同步至 Tier_2 的 KBDB,確保全局狀態一致。
---
### Requirement 8:前端 Web Components 零件庫
**User Story:** As a 前端開發者, I want 一套以 Web Components 標準實作的 u6u UI 零件庫, so that 畫布上的 UI 元件能在任何現代瀏覽器中獨立運作,不依賴特定前端框架。
#### Acceptance Criteria
1. THE Canvas SHALL 提供以下核心 Web Components`<u6u-btn>`(按鈕)、`<u6u-text-input>`(文字輸入)、`<u6u-text-field>`(多行文字)、`<u6u-chart>`(圖表)、`<u6u-card>`(智慧容器)。
2. THE `<u6u-btn>` SHALL 支援以下 HTML attributes`label`(顯示文字)、`color`(主題色)、`tooltip`(滑鼠懸停提示,純靜態,不觸發 Webhook)、`workflow`(綁定的 Workflow URI,格式為 `workflow://id`)。
3. WHEN `<u6u-btn>``workflow` attribute 被設定且使用者點擊按鈕,THE `<u6u-btn>` SHALL 發出 `u6u:trigger` 自訂事件,事件 detail 包含 `{ workflowId, payload }`
4. THE `<u6u-card>` SHALL 在接收到子元件的 `u6u:trigger` 事件時,自動收集同容器內所有 `<u6u-text-input>``<u6u-text-field>` 的當前值,合併至事件的 `payload` 後再向上冒泡。
5. FOR ALL Web ComponentsTHE Canvas SHALL 保證元件的 HTML attribute 變更能即時反映至視覺渲染,且渲染結果與 attribute 值之間的對應關係具有冪等性(相同 attribute 值永遠產生相同渲染結果)。
---
### Requirement 9:雙面翻轉畫布介面
**User Story:** As a 業務使用者(非工程師), I want 畫布上每個 UI 元件都能翻面查看並編輯其 Cypher 邏輯連線, so that 不需要寫程式就能理解並修改業務邏輯。
#### Acceptance Criteria
1. THE Canvas SHALL 為每個 UI 零件提供「翻面」操作,切換至邏輯視圖後,顯示該零件關聯的 Cypher 三元組(以視覺化節點連線方式呈現)。
2. WHEN 使用者在邏輯視圖中修改 Cypher 連線(新增、刪除或修改三元組),THE Canvas SHALL 即時更新對應 Workflow 的 KBDB Block,並在正面 UI 視圖中反映連線狀態變更(如按鈕顏色或 badge 提示)。
3. THE Canvas SHALL 在邏輯視圖中提供 Workflow URI 選擇器,列出 KBDB 中所有可用的 Workflow,讓使用者透過下拉選單完成 `ON_CLICK >> workflow://id` 的綁定,不需手動輸入 URI。
4. WHEN 使用者在畫布上將兩個 UI 零件拖入同一個 `<u6u-card>` 容器,THE Canvas SHALL 自動在邏輯視圖中顯示 Smart_Container 的自動打包關係,說明哪些輸入值會被自動收集。
5. THE Canvas SHALL 在使用者嘗試替換一個已綁定 Workflow 的 UI 零件時,只顯示 Component_Registry 中具備相同觸發能力(即 `u6u:trigger` 事件)的候選零件,過濾掉不相容的零件。
---
### Requirement 10:自動演化評價迴圈
**User Story:** As a 平台維運者, I want 每次 Workflow 執行後自動觸發 AI 評價, so that 系統能持續識別問題零件並累積避坑知識。
#### Acceptance Criteria
1. WHEN 一個 Workflow 執行完畢(無論成功或失敗),THE Evaluator_Agent SHALL 在執行結束後非同步評估以下維度:執行狀態(成功 / 失敗 / 逾時)、各節點執行時間、零件錯誤率趨勢。
2. WHEN Evaluator_Agent 發現某零件的錯誤率在連續 5 次執行中超過 50%THE Evaluator_Agent SHALL 在 KBDB 中為該零件建立 Pitfall_Block,記錄:零件 id、失敗模式描述、首次發現時間戳記。
3. WHEN Component_Dispatcher 在 Component_Registry 搜尋零件時,THE Component_Dispatcher SHALL 讀取目標零件的所有關聯 Pitfall_Block,並在選擇版本時降低有 Pitfall 記錄的版本的評分權重。
4. WHEN 一個零件連續 30 天無任何 Workflow 引用,THE Component_Registry SHALL 將該零件標記為 `Deprecated`,並從預設搜尋結果中移除,但保留 `.wasm` 二進位與合約。
5. WHEN 一個 `Deprecated` 零件再經過 90 天仍無引用,THE Component_Registry SHALL 將該零件移入墓地(tombstone 狀態),從所有搜尋結果中移除,但 `pinned` 版本的 `.wasm` 永遠保留且可被 Component_Dispatcher 存取。
6. THE Evaluator_Agent SHALL 以冪等方式處理重複的執行日誌,對相同 `run_id` 的重複評價請求回傳相同結果而不重複建立 Pitfall_Block。
---
### Requirement 11:零件開發指引(Component Authoring Guide
**User Story:** As a 零件開發者(使用自己的 AI 工具,如 Claude、GPT、本地模型), I want 平台提供完整的零件開發指引, so that 我的 AI 能根據指引生成符合合約規格的 `.wasm` 零件,並一次通過沙盒驗收。
#### Acceptance Criteria
1. THE Component_Registry SHALL 在 `GET /components/guide` 端點提供機器可讀的開發指引文件(Markdown 格式),內容包含:零件合約 YAML 完整範例、I/O 模型說明(stdin/stdout JSON)、各語言(TinyGo、Rust、AssemblyScript)的最小可運行範例程式碼、本地測試指令(`wasmtime` 執行方式)、常見錯誤與解法。
2. THE Component_Registry SHALL 在開發指引中明確列出所有禁止行為:網路 syscall、檔案系統 syscall、打包 runtimeQuickJS、Node.js 等)、超過 2MB、混合前後端邏輯於同一零件。
3. THE Component_Registry SHALL 在開發指引中提供 `component.contract.yaml` 的 JSON Schema 定義,讓開發者的 AI 能在提交前自行驗證合約格式正確性。
4. WHEN 一個零件提交驗收失敗,THE Component_Registry SHALL 在錯誤回應中附上指向開發指引對應章節的錨點連結(如 `#syscall-constraints`),讓開發者的 AI 能直接定位修復方向。
5. THE Component_Registry SHALL 提供 `POST /components/validate-contract` 端點,接受 `component.contract.yaml` 內容,回傳格式驗證結果(欄位完整性、schema 合法性、gherkin_tests 最低數量),讓開發者在提交 `.wasm` 前先驗證合約。
6. FOR ALL 開發指引中的程式碼範例,THE Component_Registry SHALL 保證範例能通過 Requirement 2 定義的沙盒驗收流程(指引本身是可執行的 ground truth)。
---
### Requirement 12KBDB Component Registry 整合
**User Story:** As a 系統開發者, I want Component Registry 完全以 KBDB 的 Template/Block/Slot 機制實作, so that 零件狀態與平台其他知識共享同一個持久化層,不引入新的資料庫。
#### Acceptance Criteria
1. THE Component_Registry SHALL 以 KBDB 的 `tpl-component` Template 儲存零件合約,每個零件版本對應一個 Block,Block 的 slots 對應合約的各欄位(id、version、wasi_target、stability、runtime_compat、constraints 等)。
2. THE Component_Registry SHALL 以 KBDB 的 Vectorize 索引零件的 `description``tags` 欄位,支援語意搜尋(如「查詢 Google Sheets 資料」能找到 `gsheets_get_entries`)。
3. WHEN Component_Dispatcher 搜尋零件時,THE Component_Registry SHALL 回傳按「成功率 × 速度評分 × 被調用次數」排序的版本清單,最多回傳 10 個候選版本。
4. THE Component_Registry SHALL 透過 KBDB 的 HTTP API 存取所有資料,不直接操作 D1 SQL,符合平台的 API-First 通訊鐵律。
5. FOR ALL Component_Registry 的讀取操作,THE Component_Registry SHALL 保證在 KBDB 資料不變的情況下,對相同查詢參數回傳相同結果(查詢冪等性)。
@@ -0,0 +1,411 @@
# Implementation Plan: u6u Platform Evolution
## Overview
依照 Bootstrap 順序分四個 Phase 實作,每個 Phase 都是下一個 Phase 的基礎。
技術棧:TypeScript、Hono、Zod、Vitest、fast-check,部署於 Cloudflare Workers。
---
## Phase 0:最小 WASM 執行核心
- [x] 1. 建立 Component Registry 基礎架構(`u6u-core/registry/`
- [x] 1.1 建立 `tpl-component` Template Block(透過 KBDB HTTP API
- 呼叫 KBDB `/templates` 建立 `tpl-component` template(若不存在)
- 定義所有 slot keyscanonical_id、display_name、category、version、wasi_target、stability、runtime_compat、constraints、input_schema、output_schema、gherkin_tests、wasm_r2_key、cypher_binding_url、service_binding_key、description、tags、success_rate、avg_duration_ms、call_count、status、deprecated_at
- _Requirements: 12.1_
- [x] 1.2 實作 `POST /components/validate-contract` 端點
- 以 Zod schema 驗證 component.contract.yaml 所有必填欄位
- 回傳缺失欄位清單(`missing_fields: string[]`
- _Requirements: 1.1, 1.2, 11.5_
- [ ]* 1.3 寫 property test for 合約格式完整性
- **Property 1: 合約格式完整性**
- **Validates: Requirements 1.1, 1.2, 1.4**
- 用 fast-check 生成隨機缺少任意必填欄位的合約物件,驗證 validator 必定拒絕並回傳該欄位名稱
- 用 fast-check 生成包含所有必填欄位的合約物件,驗證 validator 必定通過
- [x] 1.4 實作 `GET /components/guide` 端點
- 回傳 Markdown 格式開發指引(TinyGo 白名單、禁止行為、contract YAML 範例、wasmtime 測試指令)
- _Requirements: 11.1, 11.2, 11.3_
- [x] 1.5 實作 `POST /components` 零件提交端點(沙盒驗收流程)
- 依序執行五個驗收步驟:(a) 體積檢查、(b) 冷啟動時間測量、(c) syscall 掃描、(d) Gherkin 測試執行、(e) runtime 相容測試
- 任一步驟失敗立即停止,回傳 `{ success: false, failed_step, reason, guide_anchor, component_id, version }`
- 通過後以 KBDB HTTP API 寫入 Block`block_id = comp-{id}-{version}`
- 同時上傳 `.wasm` 至 R2slot `wasm_r2_key` 記錄 R2 key
- _Requirements: 2.1, 2.2, 2.3_
- [ ]* 1.6 寫 property test for 沙盒驗收流程正確性
- **Property 2: 沙盒驗收流程正確性**
- **Validates: Requirements 2.1, 2.2**
- 用 fast-check 生成在步驟 N 失敗的零件,驗證回應包含步驟 N 名稱與原因,且不執行步驟 N+1
- [ ]* 1.7 寫 property test for 零件提交冪等性與持久性
- **Property 3: 零件提交冪等性與持久性**
- **Validates: Requirements 2.3, 2.4**
- 用 fast-check 生成通過驗收的零件,提交後讀取合約驗證所有欄位 round-trip 一致
- 對相同 (id, version) 重複提交 N 次,驗證 KBDB 只存在一個 Block
- [x] 2. 實作 WASI preview1 shim 與 WASM 執行核心(`cypher-executor/src/lib/`
- [x] 2.1 實作輕量 WASI preview1 shim`wasi-shim.ts`
- 實作 `fd_read`(從 stdin buffer 讀取)、`fd_write`(寫入 stdout/stderr buffer)、`proc_exit`(拋出 Error)、`random_get``crypto.getRandomValues`
- 其餘 syscall 一律回傳 ENOSYS76
- 不引入任何外部依賴(不使用 `@cloudflare/workers-wasi`
- _Requirements: 3.1, 3.3_
- [x]* 2.2 寫單元測試 for WASI shim
- 測試 `fd_read` 正確讀取 stdin buffer(含多次讀取、邊界條件)
- 測試 `fd_write` 正確寫入 stdout bufferfd=1)與 stderr bufferfd=2
- 測試 `proc_exit` 拋出 Error
- [x] 2.3 實作 Tier 1 WASM 執行器(`wasm-executor.ts`
- 從 R2 fetch `.wasm` ArrayBuffer
- `WebAssembly.compile` 後快取 `WebAssembly.Module`Worker 記憶體,跨請求共享)
- 建立 WASI shim,注入 stdin = `JSON.stringify(input)`
- `WebAssembly.instantiate(module, imports)` → 呼叫 `_start()``main()`
- 從 stdout buffer 讀取輸出,`JSON.parse` 後回傳
- 套用 `max_cold_start_ms` 逾時(`Promise.race`
- _Requirements: 3.1, 3.3, 6.6_
- [ ]* 2.4 寫 property test for Component Dispatcher 路由正確性
- **Property 4: Component Dispatcher 路由正確性**
- **Validates: Requirements 3.1, 3.5, 3.6**
- 用 fast-check 生成合法 JSON input,驗證 WASM 執行路徑輸出與預期語意等效
- [x] 3. 建立 `validate_json.wasm` 第一個真實零件(TinyGo
- [x] 3.1 撰寫 `validate_json` TinyGo 原始碼(`u6u-core/registry/components/validate_json/main.go`
- 只使用白名單 import`os``io``encoding/json`
- 讀取 stdin JSON,解析 `json_string` 欄位,嘗試 `json.Unmarshal`
- 成功輸出 `{"valid":true}`,失敗輸出 `{"valid":false,"error":"..."}`
- _Requirements: 3.1, 11.6_
- [x] 3.2 撰寫 `validate_json` component.contract.yaml
- 包含所有必填欄位、gherkin_testshappy path + error path
- `runtime_compat: ["cf-workers","workerd","wazero"]`
- _Requirements: 1.1, 1.2, 1.4_
- [ ]* 3.3 寫單元測試 for validate_jsonGherkin 場景驗證)
- 測試合法 JSON 輸入回傳 `{"valid":true}`
- 測試非法 JSON 輸入回傳 `{"valid":false,"error":...}`
- [x] 4. Checkpoint — Phase 0 驗收
- 確認 `validate_json.wasm` 能在 CF Workers 環境中透過 WASM 執行器執行
- 確認 Component Registry `/guide``/validate-contract``/components` 端點可用
- 確認所有 Phase 0 測試通過,向使用者確認是否繼續 Phase 1
---
## Phase 1:遷移現有零件(20 個 HTTP → WASM
- [x] 5. 升級 Component Dispatcher 支援雙模式(`cypher-executor/src/lib/component-loader.ts`
- [x] 5.1 重構 `ComponentDescriptor` 型別(移除舊 `http_endpoint`,新增 `component_type`
- 定義 `ComponentType = 'wasm' | 'cypher_binding' | 'service_binding'`
- 新版 `ComponentDescriptor` 欄位:`component_type``wasm_r2_key``runtime_compat``max_cold_start_ms``url`cypher_binding)、`method``binding`service_binding)、`path`
- _Requirements: 3.5_
- [x] 5.2 實作路由決策邏輯(`component-dispatcher.ts`
- 查 Component Registry 取得合約
-`component_type` 分流:`wasm` → WASM 執行器;`cypher_binding` → HTTP POST 到外部 URL`service_binding` → CF Service Binding
- 檢查 `runtime_compat` 是否包含當前 Tier,不包含則回傳 `RUNTIME_INCOMPATIBLE` 錯誤
- _Requirements: 3.4, 6.1, 6.2_
- [ ]* 5.3 寫 property test for Dispatcher 錯誤結構完整性
- **Property 5: Dispatcher 錯誤結構完整性**
- **Validates: Requirements 3.4, 6.4**
- 用 fast-check 生成 (component_id, tier) 組合,當 runtime_compat 不含當前 tier,驗證錯誤回應同時包含 component_id、tier、attempted_paths 三個欄位
- [x] 6. 遷移 20 個內建零件(`u6u-core/builtins/``u6u-core/registry/components/`
- [x] 6.1 為每個零件撰寫 TinyGo 原始碼與 component.contract.yaml(批次作業)
- 每個零件:只用白名單 import、stdin/stdout JSON I/O、附帶 gherkin_tests
- 需要外部 API 的零件(如 gsheets):改用 `cypher_binding` 模式,contract 中記錄 `cypher_binding_url`
- _Requirements: 3.6, 11.6_
- [x] 6.2 透過 `POST /components` 批次提交 20 個零件至 Component Registry
- 每個零件通過沙盒驗收後自動寫入 KBDB
- 驗證 20 個零件的 Gherkin 測試全部通過
- _Requirements: 2.1, 3.6_
- [ ]* 6.3 寫 property test for 歷史版本永久保留不變量
- **Property 8: 歷史版本永久保留不變量**
- **Validates: Requirements 4.5, 10.5**
- 用 fast-check 生成已上架零件,標記為 deprecated 後,驗證 `.wasm` 仍可從 R2 讀取,pinned 引用仍可執行
- [x] 7. 實作 Component Registry 查詢端點
- [x] 7.1 實作 `GET /components/:id``GET /components/:id/versions`
- 透過 KBDB HTTP API 查詢 `tpl-component` blocks
- `/versions` 回傳按「成功率 × 速度評分 × 被調用次數」排序的版本清單(最多 10 個)
- _Requirements: 12.3_
- [x] 7.2 實作 `GET /components/search?q=...` 語意搜尋
- 呼叫 KBDB Vectorize API,以 `description` + `tags` 欄位做語意搜尋
- _Requirements: 12.2_
- [ ]* 7.3 寫單元測試 for 查詢冪等性
- 驗證相同查詢參數在 KBDB 資料不變時回傳相同結果
- [x] 8. Checkpoint — Phase 1 驗收
- 確認 20 個零件全部通過沙盒驗收並存入 KBDB
- 確認 Component Dispatcher 雙模式路由正確(WASM + cypher_binding
- 確認所有 Phase 1 測試通過,向使用者確認是否繼續 Phase 2
---
## Phase 2Cypher 語意擴展 + Multi-Tier Dispatcher
- [x] 9. 擴展 Cypher Triplet Parser`cypher-executor/src/actions/triplet-parser.ts`
- [x] 9.1 新增 EdgeType 定義
- 在現有 `PIPE | IF | FOREACH | CONTINUE` 基礎上新增:`IS_A | ON_SUCCESS | ON_FAIL | ON_CLICK | CALLS_SUBFLOW | CONTAINS | HAS_STYLE | HAS_BEHAVIOR`
- _Requirements: 5.1_
- [x] 9.2 實作 URI 協議解析函數(`resolveComponentId`
- 解析 `component://id``component://id@stable``component://id@pinned:vN``workflow://id``ui://id``style://id`
- 回傳 `{ type, canonicalId, stability, pinnedVersion? }`
- _Requirements: 4.1, 5.6_
- [ ]* 9.3 寫 property test for 零件 URI 解析 Round-Trip
- **Property 6: 零件 URI 解析 Round-Trip**
- **Validates: Requirements 4.1**
- 用 fast-check 生成合法 URI 字串,解析後再序列化,驗證語意等效;驗證解析出的 id、stability、pinnedVersion 與原始 URI 完全一致
- [ ]* 9.4 寫 property test for Cypher 三元組解析 Confluence
- **Property 9: Cypher 三元組解析 Confluence(順序無關性)**
- **Validates: Requirements 5.7**
- 用 fast-check 生成合法三元組集合,用 `fc.shuffledSubarray` 打亂順序,驗證 `parseTriplets` 產生相同節點集合與邊集合
- [x] 10. 擴展 GraphExecutor 執行語意(`cypher-executor/src/graph-executor.ts`
- [x] 10.1 實作 `IS_A` 關係處理
- 從 Component Registry 載入零件合約,以 `input_schema` 驗證節點輸入 context
- _Requirements: 5.2_
- [x] 10.2 實作 `ON_SUCCESS` / `ON_FAIL` 分支執行
-`executeNode` 的 try/catch 中,成功走 `ON_SUCCESS` 邊,失敗走 `ON_FAIL` 邊(傳遞 error context
- _Requirements: 5.3_
- [x] 10.3 實作 `CALLS_SUBFLOW` 子流程呼叫
- 從 KBDB 載入子 Workflow 定義,建立子 GraphExecutor 執行,將輸出合併回主流程 context
- _Requirements: 5.4_
- [x] 10.4 實作 `ON_CLICK` 前端觸發處理
- 接受來自前端 Smart Container 打包的 payload,作為 Workflow initialContext
- _Requirements: 5.5_
- [x] 10.5 實作 `CONTAINS` / `HAS_STYLE` / `HAS_BEHAVIOR` 結構語意解析(不執行,僅記錄圖結構)
- _Requirements: 5.1_
- [x] 11. 實作版本選擇策略(Component Dispatcher 升級)
- [x] 11.1 實作 floating 版本選擇算法
- 從 KBDB 查詢該 id 下所有版本,計算「成功率 × 速度評分 × 被調用次數」,選取最高分版本
- _Requirements: 4.2_
- [x] 11.2 實作 stable / pinned 版本選擇
- `stable`:使用當前標記為 stable 的版本,有更優版本時記錄提示至 KBDB 但不切換
- `pinned:vN`:永遠使用版本 vN,即使已 deprecated
- _Requirements: 4.3, 4.4_
- [ ]* 11.3 寫 property test for 版本選擇策略正確性
- **Property 7: 版本選擇策略正確性**
- **Validates: Requirements 4.2, 4.4**
- 用 fast-check 生成版本集合(各有不同 success_rate、avg_duration_ms、call_count),驗證 floating 選最高分;驗證 pinned:vN 無論其他版本評分如何永遠選 vN
- [x] 12. 實作 Evaluator Agent 與評價迴圈(`cypher-executor/src/actions/`
- [x] 12.1 實作 `execution-evaluator.ts`(擴展現有 `execution-logger.ts`
- Workflow 執行完畢後非同步寫入 KBDB Evaluation Block`tpl-evaluation`
- 記錄:run_id、workflow_id、component_id、verdict、duration_ms、error_message、evaluated_at
- 冪等處理:相同 run_id 不重複建立 Block
- _Requirements: 10.1, 10.6_
- [x] 12.2 實作 Pitfall Block 建立邏輯
- 偵測某零件連續 5 次執行錯誤率 > 50%,建立 `tpl-pitfall` Block
- 版本選擇時降低有 Pitfall 記錄的版本評分權重
- _Requirements: 10.2, 10.3_
- [x] 12.3 實作零件自動 Deprecated / Tombstone 狀態轉換
- 連續 30 天無引用 → 標記 `deprecated`,從預設搜尋移除
- 再 90 天無引用 → 標記 `tombstone`,從所有搜尋移除(pinned `.wasm` 永久保留)
- _Requirements: 10.4, 10.5_
- [ ]* 12.4 寫 property test for 系統操作冪等性
- **Property 11: 系統操作冪等性**
- **Validates: Requirements 10.6, 12.5**
- 用 fast-check 生成 run_id,對相同 run_id 呼叫 Evaluator N 次,驗證 KBDB 只存在一個 Evaluation Block
- 驗證相同查詢參數在資料不變時回傳相同結果
- [x] 13. Checkpoint — Phase 2 驗收
- 確認新 EdgeType 全部可解析執行(IS_A、ON_SUCCESS、ON_FAIL、CALLS_SUBFLOW、ON_CLICK
- 確認版本選擇策略(floating / stable / pinned)行為正確
- 確認 Evaluator Agent 冪等寫入 KBDB
- 確認所有 Phase 2 測試通過,向使用者確認是否繼續 Phase 3
---
## Phase 3:前端畫布(Web Components + 雙面翻轉)
- [x] 14. 建立 Web Components 零件庫(`u6u-core/web-components/`
- [x] 14.1 實作 `<u6u-btn>` Custom Element
- 支援 attributes`label``color``tooltip``workflow``disabled`
- `workflow` 設定且點擊時發出 `u6u:trigger` CustomEvent`{ workflowId, payload }`
- `workflow` 未設定時點擊不發出事件,console 輸出警告
- _Requirements: 8.2, 8.3_
- [x] 14.2 實作 `<u6u-text-input>``<u6u-text-field>` Custom Elements
- 支援 `name``placeholder``value` attributes
- `value` property 可被 `<u6u-card>` 讀取
- _Requirements: 8.1_
- [x] 14.3 實作 `<u6u-card>` Smart Container
- 攔截子元件的 `u6u:trigger` 事件(`stopPropagation`
- 收集同容器內所有 `<u6u-text-input>` / `<u6u-text-field>` 的 name-value 對
- 合併至 payload 後重新發出 `u6u:trigger``bubbles: true, composed: true`
- _Requirements: 8.4_
- [x] 14.4 實作 `<u6u-chart>` Custom Element(基礎版)
- 支援 `data` attributeJSON 字串)、基本折線圖渲染
- _Requirements: 8.1_
- [ ]* 14.5 寫 property test for Web Components 事件與渲染冪等性
- **Property 10: Web Components 事件與渲染冪等性**
- **Validates: Requirements 8.3, 8.4, 8.5**
- 用 fast-check 生成 workflow URI,驗證點擊後 `u6u:trigger` detail.workflowId 與 URI id 完全一致
- 用 fast-check 生成具名 input 集合置於 u6u-card,驗證收集到的 payload 包含所有 name-value 對
- 用 fast-check 生成 attribute 值,對同一元件設定相同值 N 次,驗證渲染結果冪等
- [x] 15. 建立雙面翻轉畫布(`inkstone-admin/frontend/web/`
- [x] 15.1 實作翻轉狀態機(`Canvas.tsx`
- 狀態:`UIView``LogicView`(點擊翻面按鈕切換)
- `LogicView``Editing`(修改三元組)→ `Saving`(確認儲存)→ `LogicView`
- _Requirements: 9.1_
- [x] 15.2 實作邏輯視圖(Cypher 三元組視覺化)
- 顯示零件關聯的 Cypher 三元組(節點連線方式)
- 修改三元組後即時更新 KBDB Workflow Block(透過 KBDB HTTP API
- _Requirements: 9.2_
- [x] 15.3 實作 Workflow URI 選擇器
- 列出 KBDB 中所有可用 Workflow,下拉選單完成 `ON_CLICK >> workflow://id` 綁定
- _Requirements: 9.3_
- [x] 15.4 實作 Smart Container 拖放與自動打包關係顯示
- 拖入同一 `<u6u-card>` 時,邏輯視圖自動顯示 CONTAINS 關係與自動打包說明
- _Requirements: 9.4_
- [x] 15.5 實作零件替換過濾器
- 替換已綁定 Workflow 的 UI 零件時,只顯示具備 `u6u:trigger` 能力的候選零件
- _Requirements: 9.5_
- [x] 15.6 在畫布中整合 u6u Web ComponentsDogfooding
- 畫布 UI 本身使用 `<u6u-btn>``<u6u-card>``<u6u-text-input>` 組裝
- 驗證 Web Components 在 React 19 環境中正確運作
- _Requirements: 8.1, 9.1_
- [ ]* 15.7 寫整合測試 for 畫布翻轉流程
- 測試 UIView → LogicView → Editing → Saving → LogicView 完整狀態轉換
- 測試 KBDB 寫入成功與失敗兩種情境
- [x] 16. Final Checkpoint — 全平台驗收
- 確認四個 Phase 的所有測試通過(`pnpm test` in each service
- 確認 Dogfooding:畫布本身用 u6u Web Components 組裝,每一層都是下一層的第一個用戶
- 確認 KBDB 不變量:仍只有三張表(blocks / templates / slots
- 確認 API-First 鐵律:所有跨服務通訊只透過 HTTP API
- 向使用者確認所有任務完成
---
## Phase 4:邊緣基礎設施(Tier 3 支援)
> 這個 Phase 不在零件遷移範圍內,是獨立的基礎設施工作。
- [ ] 17. Credentials 邊緣支援評估與改寫(`u6u-core/credentials/`
- [ ] 17.1 評估 Tier 3 是否需要 Credentials
- 場景:無人機在有網路時呼叫外部 API(如取得感測器資料),需要 access_token
- 結論:Tier 3 需要在連網時從 Tier 2 取得 Credential,離線時使用本地快取的加密 token
- _Requirements: 7.1, 7.2_
- [ ] 17.2 實作 Credential 本地快取機制(Tier 3 用)
- Tier 3 Go 排程引擎在連網時從 Tier 2 Credentials Worker 取得加密 token
- 存入本地 SQLiteAES-GCM 加密,key 存於設備安全儲存)
- 離線時從本地快取讀取,過期時加入 DTN 佇列等待更新
- _Requirements: 7.1, 7.3_
- [ ] 18. Go Cypher ExecutorTier 3 邊緣執行引擎)(`u6u-core/executor/`
- [ ] 18.1 用 Go 實作 Cypher 三元組解析器
- 解析 `"A >> 關係 >> B"` 格式,建立執行圖(nodes + edges
- 支援 IS_A、ON_SUCCESS、ON_FAIL、CALLS_SUBFLOW、ON_CLICK 語意關係
- 對應 `cypher-executor/src/actions/triplet-parser.ts` 的 Go 版本
- _Requirements: 5.1, 5.7_
- [ ] 18.2 用 Go + Wazero 實作 WASM 零件執行器
- 載入本地 `.wasm` 檔案,透過 Wazero 原生 WASI preview1 執行
- 注入 `u6u` host module`http_request` host function,透過 DTN 或直接 HTTP
- stdin/stdout JSON I/O,與 Tier 1/2 的 `wasm-executor.ts` 語意等效
- _Requirements: 7.1, 7.4_
- [ ] 18.3 實作 DTN 佇列(離線請求緩衝)
- 零件需要網路但當前離線時,寫入本地 SQLite DTN 佇列
- 連網時 Burst 傳輸:批次送出佇列中的請求,接收回應後繼續執行
- _Requirements: 7.2, 7.5_
- [ ]* 18.4 整合測試:validate_json.wasm 在 Wazero 執行
- 確認同一個 `.wasm` 在 Tier 1wasi-shim.ts)和 Tier 3Wazero)執行結果一致
---
## Notes
- 標記 `*` 的子任務為選填,可跳過以加速 MVP 交付
- 每個任務都引用具體的 Requirements 條款以確保可追溯性
- Checkpoint 任務確保每個 Phase 完成後有明確的驗收點
- Property tests 使用 fast-check,每個屬性最少執行 100 次迭代
- 所有跨服務呼叫只透過 KBDB HTTP API,不直接操作 D1 SQL
- TinyGo 零件只使用白名單 import`os``io``encoding/json`
## Phase 5u6u-mcp 對齊新 Registry + u6u-gui 前端
> 壓測前必須完成,讓 AIu6u-mcp)和人類(u6u-gui)都能操作新的 WASM 零件架構。
- [x] 19. 更新 u6u-mcp 對齊新 Component Registry`u6u-mcp/src/tools/`
- [x] 19.1 更新 `u6u_publish_component`
- 舊:呼叫 `/components/publish`payload 為 `{ component_id, gherkin, api_config }`
- 新:呼叫 `POST /components`payload 為 `{ contract: ComponentContract, wasm_base64: string }`
- 新增 `contract``wasm_base64` 參數,更新工具描述說明 TinyGo 零件提交流程
- [x] 19.2 更新 `u6u_search_components`
- 舊:呼叫 `/components/match`(不存在的端點)
- 新:呼叫 `GET /components/search?q={query}`(新 Registry 語意搜尋端點)
- 更新工具描述:AI 可用自然語言搜尋零件(如「查詢 Google Sheets 資料」)
- [x] 19.3 更新 `u6u_get_component`
- 舊:讀舊格式 slots`component_id``name``published_at`
- 新:對齊 `tpl-component` slot 欄位(`canonical_id``display_name``category``version``stability``wasm_r2_key` 等)
- 呼叫新 Registry `GET /components/:id` 端點
- [x] 19.4 新增 `u6u_get_component_guide` 工具
- 呼叫 `GET /components/guide`,回傳開發指引給 AI
- AI 在開發新零件前可先讀取指引,確保生成符合規範的 TinyGo 程式碼
- [x] 20. 建立 u6u-gui 前端(`u6u-gui/`
- [x] 20.1 建立 Cloudflare Pages 專案結構
- React 19 + Vite + Tailwind CSS v4
- 整合 `@u6u/web-components`alias 指向 `u6u-core/web-components/src`
- wrangler.toml 設定 Pages 部署
- [x] 20.2 建立主畫布頁面(`/canvas`
- 整合 `Canvas.tsx`(從 inkstone-admin 移植)
- 連接 Cypher Executor API`POST /cypher/execute`
- 連接 Component Registry API(搜尋、查詢零件)
- AI 操作後(透過 u6u-mcp 修改 KBDB)畫布即時反映變更
- [x] 20.3 建立零件庫頁面(`/components`
- 列出所有已上架零件(呼叫 `GET /components/search`
- 顯示零件合約、評分、版本歷史
- 提供「提交新零件」入口(連結到開發指引)
- [x] 20.4 建立 Workflow 管理頁面(`/workflows`
- 列出所有 Workflow(從 KBDB 查詢 `tpl-workflow`
- 點擊進入畫布編輯
- 顯示執行歷史(Evaluation Block
- [x] 20.5 部署至 Cloudflare Pages
- `pnpm build && npx wrangler pages deploy dist`
- 設定環境變數(KBDB_URL、CYPHER_URL、REGISTRY_URL
+381
View File
@@ -0,0 +1,381 @@
# arcrun — 進度與待辦
> 設計細節見 `arcrun/README.md`(產品說明)和 `arcrun/BETA_TEST.md`(封測指南)。
> 這份文件只記錄:目前狀態、還差什麼、封測能不能啟動。
---
## 一、封測目標場景
封測者是工程師朋友,有自己的網頁,需要後端自動化。目標是他能在 AI 協助下,一次或很少次完成以下完整流程:
1. `acr init` 取得 api_key
2. `acr parts scaffold` 查零件格式,AI 幫寫 workflow YAML
3. 若內建零件不足,`acr recipe push` 增加打外部 API 的 recipe
4. `acr creds push` 上傳 OAuth tokengmail / google_sheets 等)
5. `acr push` 部署 workflow,取得 Webhook URL
6. 網頁 POST /webhooks/named/{name}/trigger,結果存 Google Sheets
---
## 二、場景各步驟驗證狀態
### Step 1acr init → api_key
- [x] `acr init` Standard 模式完成,api_key 存入 `~/.arcrun/config.yaml`
- [x] 已驗證:`mode: standard, api_key: ak_...` 正確
### Step 2acr parts scaffold → AI 看到零件格式
- [x] `acr parts` 列出 21 個零件,完全內建,不依賴 registry.arcrun.dev
- [x] `acr parts scaffold google_sheets` 輸出 spreadsheet_id / range / operation / values 格式與 credentials.yaml 範本
- [x] 已驗證:輸出可直接貼入 YAML
### Step 3acr recipe push → 打外部 API
- [x] `acr recipe push` 上傳成功,回傳 rec_hash
- [x] workflow 使用 `component: rec_xxxxxxxx`acr push 後 trigger 能正確呼叫外部 API
- [x] 已驗證(2026-04-18):httpbin_post recipe → trigger → httpbin.org/post 回傳正確 ✅
### Step 4acr creds push → 自動注入 token
- [x] `POST /credentials` API 完成,以 `{api_key}:cred:{name}` 存入 KV
- [x] Webhook trigger 時 injectCredentials 從 KV 取得 token 自動注入
- [x] `/register` 現在回傳 `encryption_key``acr init` 自動存入 config
- [x] `acr creds push` 從 config 讀 encryption_key,不再需要手動設定環境變數
- [x] 已驗證(2026-04-18):beta@arcrun.dev 帳號完整流程:init → creds push → trigger → credential 注入成功 ✅
### Step 5acr push → Webhook URL
- [x] `acr push workflow.yaml` 部署成功,顯示 Webhook URL 和完整 curl 範例
- [x] config 中的 `component` / 參數在 push 時套入 graph 節點
- [x] 已驗證(2026-04-18):sheet-test workflow push 成功 ✅
### Step 6:網頁 POST → 執行 → 結果到 Google Sheets
- [x] `POST /webhooks/named/{name}/trigger -H 'X-Arcrun-API-Key: ...'` 觸發執行正常
- [x] google_sheets 零件有實作(append row 到 Sheets API
- [x] 已驗證(2026-04-18):trigger sheet-test → 報「缺少 credential」(符合預期,credential 未上傳)✅
- [ ] **未驗證**:真實 google_oauth token + acr creds push → trigger → Google Sheets 實際寫入
- 需要真實 OAuth token 才能完整驗證
---
## 三、封測啟動阻擋項
P0 全部清除才啟動封測。
| # | 項目 | 狀態 | 說明 |
|---|------|------|------|
| 1 | acr parts scaffold 正確輸出 | ✅ 完成 | 21 個零件內建清單 |
| 2 | acr recipe push 端對端 | ✅ 完成 | httpbin_post 驗證通過 |
| 3 | acr creds push 代碼 | ✅ 完成 | 需 ARCRUN_ENCRYPTION_KEY |
| 4 | credential 注入端對端 | ✅ 完成 | 無 token 時錯誤訊息正確 |
| 5 | acr push + webhook trigger | ✅ 完成 | 端對端驗證通過 |
| 6 | acr creds push 實測 | ✅ 完成 | /register 回傳 encryption_keyacr init 自動存入 configCLI 1.0.9|
| 7 | Google Sheets 真實寫入 | ⚠️ 部分驗證 | credential 注入已驗證;實際 Sheets 寫入需真實 OAuth token |
| 8 | 第三方服務認證 recipe | ✅ 完成 | 20 個服務(Notion/Slack/GitHub/OpenAI 等),CLI 1.1.0 |
| **9** | **cypher-executor outbound HTTP fetch 全失效** | ✅ **已解決 2026-05-13**(CF 同 zone 自循環死鎖,改走 workers.dev)| 詳見下方專段 |
| **10** | **multi-node chain context propagation 漏失** | ✅ **已解決 2026-05-13**ON_SUCCESS/ON_FAIL/IF/ON_CLICK 沒 spread baseCtx| 詳見下方專段 |
**目前狀況**P0 全部解決。
- #9 修復方式:component worker URL 從 `*.arcrun.dev`(同 cypher zone)改走 `arcrun-{name}.{WORKER_SUBDOMAIN}.workers.dev`(避開同 zone 自循環)
- #10 修復方式:4 個 edge type 補 `{...baseCtx, ...result}`,跟 PIPE/FOREACH 一致
兩個 P0 解完 mira 7 節點 workflow 端對端通(含真 Claude 16 秒呼叫)。
---
### ✅ P0 #92026-05-13 已解決):cypher-executor outbound fetch 全失效
**完整事件報告(含誤判路徑)**[docs/incidents/2026-05-13-cypher-outbound-522.md](../../../docs/incidents/2026-05-13-cypher-outbound-522.md)
**修復方式**cypher-executor fetch component worker 從 `*.arcrun.dev`(同 zone)改走 `arcrun-{name}.{WORKER_SUBDOMAIN}.workers.dev`。對外 `cypher.arcrun.dev` 不變,用戶 0 感知。
**改動檔案**2026-05-13):
- `cypher-executor/src/lib/component-loader.ts``wasmWorkerUrl(canonicalId, subdomain)` 簽名加 subdomain 參數 + URL pattern 改 workers.dev
- `cypher-executor/src/actions/auth-dispatcher.ts`:同步新簽名
- `cypher-executor/src/types.ts``Bindings``WORKER_SUBDOMAIN: string`
- `cypher-executor/wrangler.toml``[vars]``WORKER_SUBDOMAIN = "uncle6-me"`
- 5 個 component worker 在 dashboard 啟用 workers.dev URLkbdb-get / kbdb-ingest / kbdb-create-block / kbdb-patch-block / claude-api**未來新 component 也都要開**
**驗證**cypher-executor → kbdb-get / claude-api 從 522 → 200。mira `acr run wiki_synthesis` 5 節點 workflow 跑通前 3 節點(kbdb_get chain)。
**Self-hosted fork 注意**:必須改 `wrangler.toml [vars] WORKER_SUBDOMAIN` 為自己的 CF 帳號 subdomain,並把所有 component worker 在 dashboard 啟用 workers.dev URL。
---
### ✅ P0 #102026-05-13 已解決):multi-node chain context propagation 漏失
**現象**cypher binding workflow 從第 2 個節點開始,原始 input contexttop-level `api_key` / `mira_token` 等)丟失,下游節點 `{{api_key}}` 模板原文未替換傳給零件 → 401 Unauthorized 或類似錯。
**測試重現**
```yaml
flow:
- "input >> ON_SUCCESS >> n1"
- "n1 >> ON_SUCCESS >> n2"
config:
n1: { component: kbdb_get, api_key: "{{api_key}}", block_id: "{{b1}}" }
n2: { component: kbdb_get, api_key: "{{api_key}}", block_id: "{{b2}}" }
context: { api_key: "ak_xxx", b1: "...", b2: "..." }
```
n1 收到 ctx 含 `api_key / b1 / b2` ✓ → 跑通。
n2 收到的 ctx 只有 `n1.output spread`blocks/count/success/block_id),**`api_key / b1 / b2` 不見**`{{api_key}}` 原文傳到零件回 401。
**根因**`graph-executor.ts` 在 PIPE / FOREACH 邊類型已修「baseCtx result」,但 **ON_SUCCESS / ON_FAIL / IF / ON_CLICK 四個 edge type 沒套同模式**,直接把 `result` 當下游 ctx 傳,丟掉原始 context。
**修法**`cypher-executor/src/graph-executor.ts` line 407 / 415 / 423 / 472):
```typescript
// 改前
result = await this.executeNode(nextNode, graph, result, ...);
// 改後(同 PIPE/FOREACH 模式)
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, ...);
```
**驗證**mira `acr run wiki_synthesis` 7 節點 workflow 端對端跑通(16 秒,含真 Claude 呼叫)。每個節點都拿到正確 `api_key` 不再 401。
**歷史脈絡**:類似問題 2026-05-07 commit e8fca33 在 FOREACH edge 已修一次("FOREACH preserves outer context"),但當時沒同步處理另外 4 個 edge type。本次補完。
---
### ✅ P0 #10 補完三個衍生問題(2026-05-13 晚 ~ 2026-05-14
P0 #10 修完後 mira 嘗試做 wiki 多段結構,又踩出三個 cypher binding 設計缺陷。**都是同一天解掉**。
#### A. interpolateData() 不遞迴 nested object
**現象**`set` / `kbdb_create_block``values: { text: "{{classify.data.text}}" }``tags_json: ["facet:{{paragraph.facet}}"]` 等 nested config 內的 `{{x}}` 不被替換,原文傳給零件。
**根因**`interpolateData()` 只 iterate top-level,對非 string 值(object / array)直接 pass-through 不下沉。
**修法**:拆 `interpolateString` + `interpolateValue`(遞迴 object / array),`interpolateData` 改 call `interpolateValue`
**測試**`set values: { text: "hello {{name}}", arr: ["item {{name}}"] }``name=world` → 全展開。
#### B. ctx 沒存上游 output 的 node id namespace
**現象**`{{classify.data.text}}` 找不到上游 classify 的 output;只能用 `{{data.text}}`(直接 spread 取),但會被下個節點覆蓋,多節點 chain 用不了。
**根因**`propagateCtx` 只把上游 result spread 進 ctx,沒額外存 `[node.id]: result`
**修法**`propagateCtx` 改回傳 `{ ...baseCtx, ...baseResult, [upstreamNodeId]: upstreamResult }`。讓下游能用 `{{node_id.data.text}}` 從 namespace 取,永不被覆蓋。
**測試**5 節點 chain 用 `{{load_schema.blocks.0.content}}` / `{{classify.data.text}}` 全展開。
#### C. FOREACH 找 iterable 只看 result,不看 ctx + 不看 nested
**現象**mira wiki_synthesis 雙重 FOREACH(外層 `對每個 paragraph`、內層 `對每個 triplet`),外層 OK,內層跑 0 次。
**根因 (C1)**`getIterableFromContext(result, key)` 只看當前節點 output。`result``create_paragraph` output`{data, success}`),不含 paragraphs。但 `paragraphs` 早就在 ctx 從 classify spread 來。
**根因 (C2)**:當外層 FOREACH 把 `paragraph` item 注入 ctx,內層 FOREACH 要找 `paragraph.triplets``getIterableFromContext` 只看 top-level,看不到 `paragraph` 物件裡的 `triplets`
**修法**
- (C1) FOREACH `result` 找不到 iterable → fallback 找 `context`
- (C2) `getIterableFromContext` 加一輪「掃 ctx 內每個 object 找 nested key」
**測試**mira wiki_synthesis 3 層樹(wiki-page → paragraphs → triplets)端對端跑通,KBDB 內驗證 `物理 AI` wiki 有 2 段 paragraph + 4 個 tripletparent_id 正確接到對應 paragraph。
#### Edge type 一致化
`propagateCtx(context, result, upstreamNodeId)` helper5 個 edge typePIPE / ON_SUCCESS / ON_FAIL / IF / ON_CLICK / FOREACH)全部用同一 function 組下游 ctx。**未來新 edge type 必須用這 helper**,避免再漏。
#### CLI validator 同步
`cli/src/lib/yaml-parser.ts` validateRelations 加 regex 支援 `對每個 X` / `FOREACH X` 迭代器命名(之前 validator 字串完全比對擋住,但 graph-builder 執行端早已支援)。
---
### 三-A、P1 待改進(不擋封測,但 mira 已踩到)
#### ✅ P1 #3cypher-executor `scheduled()` handler2026-05-14 完成)
**原痛點**cron 零件只做 expression validationcypher-executor 沒 `scheduled()` handler。寫了 cron 首節點的 workflow 不會真的跑。
**之前的 workaround**(已撤):mira 寫了個 `/mira/wiki-from-raw` route 從前端 fire-and-forget 觸發 wiki_synthesis。但這違反「一律 arcrun-native」原則,也讓 arcrun 永遠補不齊缺失。**已刪 route,回 arcrun-native 路線**。
**落地**
1. `wrangler.toml``[triggers] crons = ["* * * * *"]`(每分鐘 tick
2. `src/lib/cron-match.ts`5 欄位 cron expression matcher(支援 `*` / `N` / `*/N` / `1-5` / `5,10` 組合)
3. `src/scheduled.ts`scheduled handler 掃 KV `cron-idx:` prefix,比對 controller.scheduledTime,匹配就 `executeWebhookGraph` 背景跑
4. `routes/webhooks-named.ts`acr push 偵測首節點是 cron 零件 → 抽 `cron_expr` 存進 record + 額外寫 `cron-idx:{api_key}:{name}` 輕量 index entry。DELETE 一併清理
5. `src/index.ts`export default 改 `{ fetch, scheduled }`
6. cypher-executor 自己加 `workers_dev = true` 給未來 self-trigger 用(fork 用 path-based 子 trigger 也走 workers.dev 避同 zone
**workflow YAML 慣例**
```yaml
flow:
- "my_cron >> ON_SUCCESS >> downstream_node"
config:
my_cron:
component: cron
cron_expr: "*/5 * * * *" # 每 5 分鐘
```
acr push 就會自動建立 cron-idx 並開始定時觸發。
**測試**`tests/arcrun-test/cron_heartbeat.yaml` — 每分鐘 fire 一次 + set 節點 log。
`wrangler tail arcrun-cypher-executor` 應看 `[scheduled] trigger cron_heartbeat ...`
**對應 use case**mira `mira_feed_watcher`(7B.3h,下一輪做)/ RSS 每日抓 / voice-stt 每小時掃 / 等所有 cron-driven source。
---
#### P1 #1workflow 缺 IF/branch 能力(2026-05-14 mira 7B.3f 提出)
**現象**mira 想做「找有則 PATCH 沒則 CREATE」(index-entry upsert),arcrun 目前只有 `ON_SUCCESS` + `對每個 X`FOREACH+ 已存在但壞掉的 `if_control`(見已知限制 #1),沒有 `>> ON_TRUE >>` / `>> ON_FALSE >>` 條件路由。
**短期 workaround**(已採用,2026-05-14):建 `kbdb_upsert_block` 零件,把分支邏輯封進零件內部(GET by page_name → 找到 PATCH 沒找到 POST)。caller 看到的是單純的 upsert 介面。
**長期解**:升 `if_control` false branch 路由 / 加 `>> ON_TRUE >>` edge type,讓 workflow 層可表達分支。對未來所有「找則改否則建」/「條件分流」場景都會撞到,不只 mira。
**位置**cypher-executor/src/graph-executor.ts edge type 處理(5 個 edge type 抽出 `propagateCtx` 後新增 IF 應該不難)+ cli/src/lib/yaml-parser.ts validator。
---
#### P0 #112026-05-14 已解決):interpolateString stringify array 撐爆下游
**現象**mira_feed_watcher 用 `items: "{{list_raws.blocks}}"` 把 kbdb_get 拿到的 blocks 陣列傳給 filter 零件。watcher 跑 264ms 完成、0 raw 處理。
**根因**`interpolateString` 看到模板就用 `String.replace`,非 string 值(陣列)一律 `JSON.stringify`。filter 零件收到字串 `"[{...},{...}]"` 不是 arrayitems 被忽略 → 0 matches → FOREACH 跑 0 次。
**修法**`interpolateString` 加 single-ref pass-through 規則:若整個值是純單一 `{{x}}` 引用,回 raw value(保留 array / object 型別)。多 ref / 混合文字仍 stringify 拼接字串。
**測試**mira_feed_watcher 推到 prod 後下一個 cron tick 觀察 wiki-processed tag 是否在 raws 上出現。
---
### 三-B、新零件加入紀錄
| 日期 | 零件 | 動機 | 對應 SDD |
|---|---|---|---|
| 2026-05-14 | `kbdb_upsert_block` | mira 7B.3f index-entry per-entity upsert,繞過 workflow 缺 IF/branch 能力(P1 #1)。內部 GET by page_name → 找到 PATCH 沒找到 POST。page_name 當 idempotency key。 | polaris/mira/.agents/specs/mira-app/design.md §3.5.12.4.1 |
**新零件 checklist(避免 P1 #1 重蹈 kbdb_upsert_block 漏白名單覆轍)**
每加一個 API/data 零件(不是 logic primitive),都要:
- [ ] `registry/components/{name}/main.go` + `component.contract.yaml` + `go.mod`
- [ ] `tinygo build -target=wasi`
- [ ] `.component-builds/{name}/` 完整 4 檔(`wrangler.toml``workers_dev = true` + `pnpm-lock.yaml` + `tsconfig.json` + `src/index.ts`
- [ ] **`cypher-executor/src/lib/component-loader.ts``WASM_HTTP_RUNNER_IDS` 加 canonical_id**(漏這條 cypher-executor 永遠拋「找不到零件」,端對端會死靜悄悄)
- [ ] `acr validate workflow.yaml`
- [ ] 直接 curl `https://{kebab}.arcrun.dev` + `https://arcrun-{kebab}.{WORKER_SUBDOMAIN}.workers.dev` 都 200
---
### 三-C、P1 #2workers_dev = true 全 component 自動化(2026-05-14 已收)
**原痛點**:每新部署一個 component worker,要去 CF Dashboard 手動 Enable workers.dev URL,否則 cypher-executor fetch 該 worker 會 404。
**解**32 個 `.component-builds/*/wrangler.toml` 全部加 `workers_dev = true`。CI 每次 deploy 自動啟用對應 workers.dev URL,零手動。
**未來新 component**:模板 (`component-worker-template/`) 應該預設帶 `workers_dev = true`,新人 fork 不會踩。已列入「新零件 checklist」第 3 條。
**為何不走 `*.acr-comp.uncle6.me` 自訂 zone**CF Universal SSL 只發一層子域,sub-sub `*.acr-comp.uncle6.me` 不蓋;要 ACM ($10/月) 才能簽。違反 arcrun「fork 後 self-host 用 free tier 跑得起來」核心目標。workers_dev=true 走 CF 默認的 workers.dev certfree tier OK,更乾淨。
---
### 原 P0 #9 調查紀錄(保留作歷史參考)
**現象**cypher-executor 的 `makeHttpRunner` (`cypher-executor/src/lib/component-loader.ts:142`) 對任何 outbound URL fetch 都回 CF **522 (origin timeout, ~1000ms)**
**測試矩陣**mira repo `polaris/mira/arcrun/wiki_synthesis.yaml` 端對端壓測時發現):
| 路徑 | 結果 | 證明 |
|---|---|---|
| 本機 curl → kbdb-get.arcrun.dev | 200 (22ms) | KBDB worker 本身健康 |
| cypher-executor → kbdb-get.arcrun.dev (HTTP) | **522** (1002ms) | outbound HTTP fetch 壞 |
| cypher-executor → claude-api.arcrun.dev (HTTP) | **522** | 同 zone 也壞 |
| cypher-executor → httpbin.org (外部) | **522** | 不只是 same-account loop |
| cypher-executor → string_ops (Service Binding) | 200 ✅ | SVC_* 路徑正常 |
| acr run hello (built-in via SB) | ✅ | hello.yaml 仍跑得通 |
**衍生小 bug**`set` 零件 input schema 變了(要 `assignments` 陣列或 `values` 物件,不是 `value`)。tests/arcrun-test/hello.yaml 跑 string_ops 沒踩到。
**影響範圍(封測啟動阻擋)**
- 任何用戶 workflow 含 outbound HTTP 都壞:
- 用戶 `acr recipe push` 後 trigger → 打外部 API 全 522(推翻 P0 #2 4/18 紀錄)
- 用戶要存資料進 arcrun 內建 KBDB → 522mira 7B.3c 卡這裡)
- 任何 auth primitive 走獨立 Worker URL 路徑 → 522
- Google Sheets 寫入 → 522(推翻 P0 #7「待真實 OAuth 驗證」評估,根本還沒到 OAuth 步驟就壞)
- 只有「全內建邏輯零件 + 純 service binding」workflow 還能跑
**根因(2026-05-13 確認):5/8-5/9 9 次 manual `wrangler deploy` 把含 WIP bug 的 cypher-executor 推上 prod**
調查路徑(按時序):
1. 一開始懷疑 free tier CPU cap10ms / invocation)→ 對照測試「5 節點 SB chain」跑了 2.2 秒卻通過,**推翻**
2. 換懷疑 CF zone 規則 / Bot Fight Mode → dashboard bindings 乾淨無攔截,**推翻**
3. 用戶補繳費恢復 Workers Paid → 重測仍全 522**徹底排除付費假設**
4. 看 dashboard Version History5/8 4 次 + 5/9 5 次 = **9 次 manual `wrangler deploy by uncle6.me`**
5. 對照 GitHub Actions4/24 後完全沒 deploy(最後是 commit e222116 `fix(wasi-shim)`
6. 對照 git:本機 main **領先 origin/main 3 commits 未 push**,含:
- `497f92a feat(arcrun): recipe system + resumable workflow + component registry canon`
- `e8fca33 feat(cypher): 3-node wiki workflow end-to-end (FOREACH + nested interp + unified parsing)`
- `519423c feat(arcrun): mira wiki page with tag filter + accumulated WIP`(自描含 `cypher-executor: auth-dispatcher / wasi-shim adjustments (WIP)`
**結論**:那 9 次 manual deploy 把上面 3 個 unpushed commit 的 cypher-executor 改動推上 prod,其中至少一個改動破壞了 outbound fetch(最可能是 519423c WIP 內的 wasi-shim / auth-dispatcher 改動)。GitHub Actions 因為沒 push 沒跑,CI 沒 catch4/18-4/24 那段 SDD「驗證通過」的紀錄是 truth,現在 prod 是壞的版本。
**為何 SB 路徑沒事 / HTTP 路徑全死**SB 走 cypher-executor 內部 service binding API`env.SVC_X.fetch()`),不經過 outbound HTTP code path。HTTP 路徑走 `makeHttpRunner` (component-loader.ts:142) 的 `fetch(url, ...)`,這條路被 WIP code 弄壞。具體壞在哪要 diff 那 3 個 commit 的 cypher-executor 改動才知道。
**驗證 wrangler tail 證據**trigger 任何 outbound HTTP 的 graphcypher-executor 自己 `wallTime: 497ms, cpuTime: 2ms, outcome: ok`、無 logs、無 exceptions。代表 cypher-executor 把「fetch 失敗的 522 response」當作 component 正常輸出包回 client,自己沒撞任何錯。
**解法(三選一)**
- **A. Rollback prod 到 4/24 的 e222116** — CF dashboard → arcrun-cypher-executor → Deployments → 找 4/24 那筆 → Rollback。5 分鐘恢復 outbound fetch,丟失 wiki workflow / recipe / resumable 等 cypher 端 WIP 改動(但前端、registry components、KBDB blocks 都不丟,因為它們是別的 worker / 別的儲存)。**richblack 操作。**
- **B. Diff 3 個 unpushed commit 找出壞掉的改動修掉** — 不丟功能,但要動 src code 走 SDD 協議,30min - 數小時。
- **C. 架構切換**mira 老闆 2026-05-13 提的):sub-workflow 自殺交棒模式,cypher-executor 不再做集中 graph executor。從根本繞開「cypher-executor 一個 invocation 跑長 graph」這條脆弱路徑。一勞永逸但是大改。
**衍生小 bug 仍要修**(跟付費無關):`set` 零件 input schema 變了(要 `assignments` 陣列或 `values` 物件,不是 `value`)。要嘛 update set 零件 contract 容錯,要嘛文件化新 schema。
**為什麼這直接擋封測**
封測場景 Step 6「網頁 POST → 結果存 Google Sheets」走 google_sheets 零件 (HTTP outbound to googleapis.com)。如果 cypher-executor outbound 全壞,**封測者跑任何含外部 API 的 workflow 都會 522**,不是「Google Sheets 實際寫入未驗證」級別的小事。
**也直接擋 mira**[polaris/mira/.agents/specs/mira-app/tasks.md] 7B.3c-fwiki 合成 workflow)卡這裡。
---
## 四、封測前 P3(啟動當天)
- [ ] 用封測者 email 呼叫 `/register`,取得 api_key
- [ ] 將 ARCRUN_ENCRYPTION_KEY 以安全方式提供給封測者
- [ ] 確認聯絡管道
---
## 五、已知限制(封測期間不修)
1. `if_control` false branch 不路由(條件 false 時後續節點不執行)→ 升級計畫見 P1 #12026-05-14 mira 用 `kbdb_upsert_block` workaround
2. 多節點 context 不自動解包(上游輸出 flat merge,下游需從 `data.result` 取值)
3. 用戶自製邏輯零件(Phase 5)封測後才實作
---
## 六、實作進度
| Phase | 內容 | 狀態 |
|-------|------|------|
| 0 | Workers 部署、CI/CD、DNS | ✅ |
| 1 | CLI 基礎(init / validate / run / parts | ✅ |
| 2 | /register、/cypher/execute、21 個零件 | ✅ |
| 3 | Service Binding 架構、{{variable}} 插值、ON_FAIL 修正 | ✅ |
| 4 | 動態 Recipe KVCRUD)、acr recipe 指令 | ✅ |
| 5 | 用戶自製邏輯零件(WASM push) | ⏸ 封測後 |
| 6 | Credential 多租戶({api_key}:cred:{name})、acr creds push | ✅ |
| 7 | acr parts 內建清單、acr parts scaffold | ✅ |
| 8 | /webhooks/named、acr push 改版、config 套入 graph | ✅ |
### CLI 版本
| 版本 | 變更 |
|------|------|
| 1.1.0 | auth recipe 系統:20 個服務預建(Notion/Slack/GitHub/OpenAI/Google SA 等);acr auth-recipe 指令 |
| 1.0.9 | /register 回傳 encryption_keyacr init 自動儲存;creds push 不需手動設環境變數 |
| 1.0.8 | acr push → webhooks/namedconfig 套入 graphacr parts 內建清單 |
| 1.0.7 | acr creds push → POST /credentials |
| 1.0.6 | acr recipe push / list / delete |
| 1.0.5 | hello.yaml 改 string_ops--version 修正 |
| 1.0.4 | config/context 分離 |
| 1.0.3 | 初始發布 |
@@ -0,0 +1,295 @@
# Auth Recipe System — SDD
> 文件類型:SDDSoftware Design Document
> 建立:2026-04-19
> 狀態:實作中
---
## 一、目標
封測前完成,讓封測者碰到「我要連 X 服務」都有辦法,而不是「還沒做」。
**精神**`http_request` 是容器零件,auth recipe 是「如何對這個服務認證」的設定層,兩者分離。新增一個服務 = 寫一份 YAML,不需要改程式碼、不需要重新部署 Worker。
---
## 二、三層模型
```
Layer 3: Auth Recipe (YAML/JSON in RECIPES KV)
公共,描述「如何對某服務認證」
key: auth_recipe:{service}
例: auth_recipe:notion, auth_recipe:slack
↓ 引用
Layer 2: Auth Primitive (TypeScript in Worker)
四個通用認證邏輯:static_key | oauth2 | service_account | mtls
封測只做 static_key 和 service_account (Google JWT)
↑ 使用
Layer 1: Tenant Secret (CREDENTIALS_KV)
每個 tenant 自己的加密 credential
key: {api_key}:cred:{name}
```
---
## 三、Auth Recipe Schema
```typescript
interface AuthRecipeDefinition {
kind: 'auth_recipe'; // 區別 RecipeDefinition 用
service: string; // canonical_id, e.g. "notion"
version: number;
primitive: 'static_key' | 'oauth2' | 'service_account' | 'mtls';
base_url: string;
display_name?: string;
description?: string;
// service_account 用
service_account_kind?: 'google_jwt';
token_exchange?: {
endpoint: string; // e.g. https://oauth2.googleapis.com/token
scopes: string[];
};
required_secrets: Array<{
key: string; // CREDENTIALS_KV 的名稱
label: string; // UI/CLI 顯示
type?: 'string' | 'json_blob'; // default: string
help?: string;
help_url: string; // **必填**(官方文件連結,http(s)://)。見下「help_url 一律必填」。
}>;
inject: {
header?: Record<string, string>; // "Authorization": "Bearer {{secret.token}}"
query?: Record<string, string>;
body?: Record<string, string>;
// path:注入 endpoint URL path 的 secret2026-05-29 加)。
// 解 telegram 類「token 在 URL path」(/bot{token}/)—— header/query/body 都不適用。
// key = 模板變數名,API recipe 的 endpoint 用 {{auth.K}} 引用。
// 例:auth_recipe:telegram inject.path = { bot_token: "{{secret.telegram_bot_token}}" }
// recipe:telegram_send endpoint = "https://api.telegram.org/bot{{auth.bot_token}}/sendMessage"
path?: Record<string, string>;
};
created_at: number;
updated_at: number;
}
```
**Template 語法**
- `{{secret.KEY}}` → 從 tenant 的 CREDENTIALS_KV 解密取值
- `{{runtime.access_token}}` → service_account JWT exchange 後取得的短期 token
---
## 四、KV 儲存
沿用現有 `RECIPES` KV namespace,不新增 binding。
```
auth_recipe:{service} → AuthRecipeDefinition JSON
```
與現有 `recipe:{id}` / `idx:{hash}` 的 key 不衝突。
---
## 五、執行流程
### 5.1 static_key(涵蓋 ~80% 服務)
```
trigger → graph-executor
→ injectCredentials(componentId, input, env, apiKey)
→ resolveAuthRecipe("notion", RECIPES KV)
→ 取得 required_secrets: [{key: "notion_token", ...}]
→ 從 CREDENTIALS_KV 讀 "{api_key}:cred:notion_token"
→ AES-GCM 解密
→ 展開 inject.header templates ({{secret.notion_token}} → 實際值)
→ 注入 _auth_headers, _auth_query, _auth_body 到 input
→ makeAuthRecipeRunner(recipe)
→ 合併 _auth_headers 到 fetch headers
→ 呼叫 recipe.base_url + input._path
→ 回傳結果
```
### 5.2 service_accountGoogle 家族)
```
injectCredentials
→ resolveAuthRecipe("google_sheets_sa", RECIPES KV)
→ 解密 service_account_json (JSON blob)
→ signGoogleJwt(serviceAccountJson, scopes) via crypto.subtle (RSASSA-PKCS1-v1_5 + SHA-256)
→ POST token_exchange.endpoint → 取得 access_token
→ 展開 inject.header: { Authorization: "Bearer {{runtime.access_token}}" }
→ 注入 _auth_headers
```
---
## 六、Context key 慣例
注入後的認證資訊以 `_auth_` 前綴攜帶,不污染業務欄位:
| Key | 說明 |
|---|---|
| `_auth_headers` | `Record<string, string>` — 要合併進 fetch headers |
| `_auth_query` | `Record<string, string>` — 要附加到 URL query string |
| `_auth_body` | `Record<string, string>` — 要合併進 request body |
| `_auth_path` | `Record<string, string>` — endpoint URL path 用(2026-05-29 加)。`makeRecipeRunner` 的 endpoint interpolate 用 `{{auth.K}}` 從這裡取值 |
`makeAuthRecipeRunner` / `makeRecipeRunner` 在發出 fetch 前讀取這些 `_auth_*` 欄位,
之後從 auto-body 中剔除所有 `_` 前綴欄位(不洩漏給下游)。
## 七、API recipe 的 auth_service(多 recipe 共用一把 auth2026-05-29 加)
`RecipeDefinition``auth_service?: string` 欄位:API recipe **自報它屬於哪個服務**
auth-dispatcher 用它查 `auth_recipe:{auth_service}`,而非假設 componentId == service name。
- 讓多個 recipe 共用同一把 auth`recipe:kbdb_get` / `kbdb_create_block` 都設 `auth_service: "kbdb"`
→ 共用唯一的 `auth_recipe:kbdb`,加新 action 不必複製 auth recipe。
- auth-dispatcher 解析順序:先查 `recipe:{componentId}``auth_service`,有就用它;
沒有則 fallback 把 componentId 當 service name(向後相容舊行為)。
- 這是「服務身分標籤」非「許可清單」:auth_recipe 只定義「怎麼認證」,不含「誰准用」。
授權由發 API key 的服務裁決,arcrun 不做內部授權判斷(見 DECISIONS.md「arcrun 不做授權判斷」)。
---
## 七、向後相容
- 現有 `BUILTIN_API_RECIPES`gmail, google_sheets, telegram, line_notify**不動**
- 現有 `BUILTIN_CREDENTIALS_MAP` **不動**
- auth recipe 解析在 component-loader step 5.5(新增),在 step 6 KV recipe 和 step 7 builtin 之前
-`auth_recipe:{service}` 不存在 → 繼續往下走,行為與現在完全相同
---
## 八、新增/修改的檔案
| 檔案 | 類型 | 說明 |
|---|---|---|
| `cypher-executor/src/routes/recipes.ts` | 修改 | 加 `AuthRecipeDefinition` 型別、`resolveAuthRecipe``/auth-recipes` CRUD routes |
| `cypher-executor/src/actions/credential-injector.ts` | 修改 | 加 auth recipe 分支:static_key + service_account |
| `cypher-executor/src/lib/jwt-signer.ts` | 新增 | Google JWT signing via crypto.subtle |
| `cypher-executor/src/lib/component-loader.ts` | 修改 | step 5.5 auth recipe lookup + `makeAuthRecipeRunner` |
| `cypher-executor/src/lib/auth-recipe-seeds.ts` | 新增 | 20 個常用服務的 auth recipe 定義 |
| `cli/src/commands/auth-recipe.ts` | 新增 | `acr auth-recipe list/info/scaffold` |
| `cli/src/commands/parts.ts` | 修改 | `cmdPartsScaffold` fallback 到 auth recipe |
| `cli/src/index.ts` | 修改 | 註冊 auth-recipe 指令 |
---
## 九、封測前預計的 Auth Recipe 清單(20 個)
### static_key 類(~80% 服務)
| service | 認證方式 | credential key |
|---|---|---|
| `notion` | Bearer token (header) | `notion_token` |
| `slack` | Bot Token (Bearer) | `slack_bot_token` |
| `github` | PAT (Bearer) | `github_token` |
| `openai` | API key (Bearer) | `openai_api_key` |
| `anthropic` | API key (x-api-key) | `anthropic_api_key` |
| `airtable` | PAT (Bearer) | `airtable_token` |
| `discord` | Bot token ("Bot TOKEN") | `discord_bot_token` |
| `stripe` | Secret key (Bearer) | `stripe_secret_key` |
| `twilio` | AccountSid + AuthToken (Basic Auth) | `twilio_account_sid`, `twilio_auth_token` |
| `sendgrid` | API key (Bearer) | `sendgrid_api_key` |
| `hubspot` | Private App token (Bearer) | `hubspot_token` |
| `linear` | API key (Bearer) | `linear_api_key` |
| `shopify` | Admin API token (X-Shopify-Access-Token) | `shopify_access_token` |
| `resend` | API key (Bearer) | `resend_api_key` |
| `supabase` | Service role key (Bearer + apikey) | `supabase_service_key` |
| `typeform` | PAT (Bearer) | `typeform_token` |
| `jira` | API token + email (Basic Auth) | `jira_api_token`, `jira_email` |
### service_account 類(Google 家族,JWT signing
| service | scopes | credential key |
|---|---|---|
| `google_sheets_sa` | spreadsheets | `google_service_account` |
| `google_gmail_sa` | gmail.send | `google_service_account` |
| `google_drive_sa` | drive | `google_service_account` |
> 注意:三個 Google 服務可共用同一個 `google_service_account` credential,只是 scope 不同。
---
## 十、實作進度
### Server (cypher-executor)
- [x] `AuthRecipeDefinition` 型別 + `resolveAuthRecipe`
- [x] `/auth-recipes` CRUD routes
- [x] `injectFromAuthRecipe` — static_key primitive
- [x] `lib/jwt-signer.ts` — Google JWT via crypto.subtle
- [x] `injectFromAuthRecipe` — service_account primitive
- [x] `makeAuthRecipeRunner` in component-loader
- [x] step 5.5 in createComponentLoader
- [x] auth-recipe-seeds.ts (20 services)
- [x] seed script / deploy seeds to KV2026-04-19 全部 ✅)
### CLI (arcrun)
- [x] `commands/auth-recipe.ts` — list / info / scaffold
- [x] 更新 `commands/parts.ts` — scaffold fallback
- [x] 更新 `index.ts` — 註冊指令
- [x] 版本升 1.1.0
- [x] npm publisharcrun@1.1.0
### 驗證
- [ ] notion (static_key) 端對端
- [ ] google_sheets_sa (service_account) 端對端
- [ ] 舊有 google_sheets builtin 向後相容確認
---
## 十一、長期演進:TinyGo WASM Primitive(封測後)
> 參考:`docs/user_requirements/arcrun/credential_parts.md`
**目前封測版**Layer 2 primitive 邏輯在 `cypher-executor` TypeScript 中實作(`credential-injector.ts`)。
**長期目標**:四個 primitive 各自編譯為獨立 TinyGo WASM,取代現有 TS 實作:
```
arcrun/registry/components/auth_static_key/ ← TinyGo WASM
arcrun/registry/components/auth_oauth2/ ← TinyGo WASM
arcrun/registry/components/auth_service_account/ ← TinyGo WASM
arcrun/registry/components/auth_mtls/ ← TinyGo WASM
```
每個 primitive 實作統一 interface`Authenticate` / `NeedsRefresh` / `Refresh` / `Test`)。
切換時 `cypher-executor``injectFromAuthRecipe` 改為呼叫對應 WASM,邏輯不變。
**何時做**:封測驗證完成、TinyGo crypto 支援確認後(特別是 RS256/ES256 JWT signing)。
在此之前,**不建立任何 TypeScript SDK 或 Python SDK 來包裝 credential 邏輯**。
### 禁止的做法
- ❌ 建立 `js-sdk/``python-sdk/` 包裝 credential 加解密
- ❌ 在 client 端重實作 AES-GCM encrypt/decrypt
- ❌ 用 TypeScript 重寫已計劃用 TinyGo 實作的 primitive 邏輯
---
## help_url 一律必填(2026-06-06richblack
每個 `required_secrets[].help_url` **必填**(官方文件連結,`http(s)://`),**不分冷門/熱門服務**。
**為什麼**
- 「冷門/熱門」沒有客觀界線——不要讓系統判斷。統一「都要填」最簡單、可執行。
- AI/使用者設定 credential 時要去 provider 設定(建 token / Service Account / 分享資源給 client_email…)。
有官方連結 → AI 直接讀權威來源,不必花時間搜、也不會搜到過時/釣魚/非官方頁。
- 投稿者不一定會寫說明(mindset §7:不靠自律)→ 用機制硬性要求。
**enforcement(薄殼原則 rule 07:能力/驗證在 API,介面繼承)**
- `POST /auth-recipes``cypher-executor/src/routes/recipes.ts`)驗證:每個 secret 缺 `help_url`
或非 `http(s)://` → 400 拒絕。CLI/MCP/任何介面 push auth recipe 都經此 API → 自動受約束,不需各介面各自檢查。
- 種子(`auth-recipe-seeds.ts`)全部已含 help_url23/23 實測通過)。
- API recipe`POST /recipes`)不帶 secretssecrets 在 auth recipe),故 help_url 要求在 auth recipe 這層。
**誠實限制**:help_url 只保證「有指向官方文件」,不保證連結永遠有效(provider 可能改網址)。
這是「降低 AI 搜錯」的保險,非絕對正確;連結失效時 AI 仍可 fallback 去搜(但有起點)。
@@ -0,0 +1,184 @@
# Credential Store 遷移 SDD — KV → D1(目錄)+ Cloudflare Secrets Store(密文)
> 建立:2026-06-29 by arcrun CC|對應 issueArcrun#13(優先序 3)|決策:leo 2026-06-29 拍板(D19
> 範圍宣告:本檔是既有 SDD `credential-primitives-wasm/` 的補充設計(rule 02 §4.3 例外:現有 SDD 目錄內新增單檔)。**不施工,先 SDD,總管審對齊後放行。**
> 取代關係:本檔的儲存決策**取代** `credential-store-redesign.md` §4.5 的「維持 KV」推薦——leo 看完該推薦後**拍板走 D1+Secrets Store**D19),故 Phase B 翻回「搬」。redesign.md 的 Atelegram 一致性,已做)/ C(友善前門)/ Q1(acr parts)不受影響。
> 詞彙:component=TinyGo WASMrecipe=http_request+固定設定|workflow=多步 YAMLauth-recipe=credential 怎麼注入。
---
## 0. leo 的最終決策(D19,一句話)
**「擁有目錄,不擁有內容物」**arcrun 持有 credential 的**目錄**(看得到存了哪些、最後何時用),但**不持有、也讀不回密文明文**——連 owner 自己的界面都讀不到值。
動機(leo 親述):用戶 API key 外泄被盜刷的經濟損失 + 商譽風險(「就算不是我們過失也會怪在我們身上」)。**我們不持有明文、不持有金鑰 = 不背這個鍋。**
```
密文值(token / SA JSON / private key → Cloudflare Secrets StoreCF 託管金鑰,arcrun 拿不到明文)
credential 目錄(name/service/metadata → 使用者自己的 D1(不含密文,只含指向 Secrets Store 的 secret_ref
舊自管 ENCRYPTION_KEY + 密文存 KV → 廢掉(回填完成後);KV 降為暫存/熱讀快取,非真相源
界面 acr creds list → 讀 D1(顯示清單 + last_used),看得到但讀不回值
```
### n8n credentials UXD19 定案)
- 界面顯示**很多 credentials**(名字 / 服務 / metadata / last_used)。
- **不能 read 既有值、不能 edit 既有值**(連 owner 都不行)。只能:
- **整筆 replace**(重貼新值覆蓋舊的)
- **delete**
- read 不回值「不是限制、是設計」——值在 Secrets Storearcrun 拿不到。
---
## 1. 現狀(已核實 code2026-06-29
| 項目 | 現狀 | 檔案 |
|---|---|---|
| 密文儲存 | `CREDENTIALS_KV`key=`{api_key}:cred:{name}`,值=`{encrypted, iv}`AES-GCM | `cypher-executor/src/routes/credentials.ts:48` |
| 加密金鑰 | 自管全域 `ENCRYPTION_KEY`client 端加密、WASM `crypto_decrypt` 解密) | rule 01 加解密;`wasi-shim.ts` |
| 寫 | `POST /credentials` { name, encrypted, iv } | `credentials.ts:24-53` |
| 讀+解密 | `auth_static_key` WASM `kv_get``crypto_decrypt` | `registry/components/auth_static_key/main.go:146` |
| 列表 | `GET /credentials``KV.list({prefix})` 列名(不含值) | `credentials.ts:68-78` |
| D1 | 全 repo 無 credential D1D1 只有 KBDB graph | `kbdb/wrangler.toml:10` |
**風險點(leo 要廢的)**:自管單一 `ENCRYPTION_KEY` → 漏一把,全部 credential 裸。
---
## 2. 目標架構(D19
### 2.1 三方職責
| 元件 | 存什麼 | 誰拿得到明文 |
|---|---|---|
| **Cloudflare Secrets Store** | 密文值本體(token/SA JSON/private key | **只有 CF runtime 在執行期注入時**。arcrun TS/WASM/owner 界面**都拿不回** |
| **使用者的 D1**(與 KBDB 共用那顆 `arcrun-kbdb` | credential **目錄**`api_key / name / service / sensitivity / created_at / last_used_at / secret_ref`,**無密文** | 全員可讀目錄(非機密),**無人從這拿到值** |
| **CREDENTIALS_KV** | 過渡期:舊自管密文;遷移後:降為可選熱讀快取或廢用 | (遷移後不再是真相源) |
### 2.2 D1 schema(明確不含密文欄)
```sql
-- migrations/0002_credentials.sqlkbdb 同一顆 D1IF NOT EXISTS 冪等)
CREATE TABLE IF NOT EXISTS credentials (
api_key TEXT NOT NULL, -- 租戶
name TEXT NOT NULL, -- credential 名(= auth-recipe required_secrets[].key,如 telegram_bot_token
service TEXT, -- 對應 servicetelegram / notion …),可空
sensitivity TEXT NOT NULL DEFAULT 'standard', -- 'standard' | 'high'high = SA JSON/private key
secret_ref TEXT NOT NULL, -- 指向 Secrets Store 的 secret 名(不是密文本體)
created_at INTEGER NOT NULL,
last_used_at INTEGER, -- 執行注入時更新(治理面 last_used 顯示用)
PRIMARY KEY (api_key, name)
);
CREATE INDEX IF NOT EXISTS idx_cred_apikey ON credentials(api_key);
```
> ⚠️ **「name 是名字標籤不是密文」**leo 防誤解):`name`/`secret_ref` 都是字串標籤,**密文值永不進 D1**。
### 2.3 Secrets Store 整合(密文存 store、D1 存 ref
**已核實 CF Secrets Store 形態(wrangler 文件):**
- 管理 CLI`wrangler secrets-store store create/list/delete``wrangler secrets-store secret put/get/list/delete <STORE_ID> <name>`
- Worker bindingwrangler.jsonc):
```jsonc
"secrets_store_secrets": [
{ "binding": "CRED_STORE", "store_id": "<STORE_ID>", "secret_name": "<name>" }
]
```
- 執行期:`await env.CRED_STORE.get()` 回密文值。
**⚠️ 設計約束(必須在施工前對齊,本 SDD 標為待決)**:
上述 binding 形態要求 `secret_name` **在 wrangler config 部署時靜態宣告**——這不適合「每個用戶任意數量的動態 credential」(不可能為每個 token 改 toml 重部署)。三條候選路徑,**請 leo/總管拍板選一**:
1. **Secrets Store Account REST API(動態 by ref,推薦)**worker 執行期用 CF API`accounts/{id}/secrets_store/stores/{store_id}/secrets/{secret_ref}/value`,或官方提供的等價端點)依 `secret_ref` 動態取值。需一把「能讀 Secrets Store 的 CF API token」給 cypher worker(本身是高敏感、走 `wrangler secret put` 注入該 worker,非進 git)。**好處**:完全動態、不用為每 cred 改 toml。**代價**worker 持有一把能讀 store 的 token(但仍比「自管全域 ENCRYPTION_KEY」好——CF 託管、可輪替、範圍限 store)。
2. **靜態 binding + 預宣告**:只適合固定少量 credential,動態場景不可行 → 排除。
3. **過渡折衷**:高敏感(SA JSON/private key)走 Secrets Store;低敏感 token 暫留 KV(AES-GCM) → 但這不滿足 D19「廢自管 ENCRYPTION_KEY、owner 也讀不回」→ 僅作為回填未完成前的中繼。
> **本 SDD 預設走路徑 1REST by ref**,並標「待 leo 確認 self-hosted leo21c CF 帳號的 Secrets Store API 路徑與 token 範圍」。施工前需一次 spike 驗證 leo21c 帳號可建 store + worker 可 by-ref 取值。
**self-hostedleo21c**Secrets Store 是 CF 原生能力,leo21c 自己的 CF 帳號支援。`acr init/update` 需新增「ensure Secrets Store store 存在」步驟(類比現有 `ensureD1Database` / `ensureKvNamespace`),把 `store_id` 注入 worker。
> 註:這與「Anthropic Routine env-var 型 self-hosted 不支援」是**不同層**(那是 Anthropic 雲端;這是 CF 帳號能力)——別混(leo 提醒)。
### 2.4 寫入流程(client 加密 → Secrets Store
> ⚠️ 鐵律對齊:現行是 **client 端加密 + 自管 ENCRYPTION_KEY**。走 Secrets Store 後,**密文交給 CF 託管**arcrun 不再自管金鑰。client 端是否仍加密一層 = 待決(見 §6 開放問題 Q-b):
> - 選項甲:client 不再加密,明文值經 TLS 送到 cypher → cypher 用「能寫 store 的 token」寫進 Secrets StoreCF 端加密託管)。**arcrun TS 短暫經手明文**(記憶體中,不落地)。
> - 選項乙:保留 client 加密當「傳輸期保護」,但這需 arcrun 持有解密金鑰才能寫進 store 明文槽 → 又回到自管金鑰,違 D19。
> → 傾向**選項甲**(符合「不持有金鑰」),但「TS 短暫經手明文」需 leo 接受(mindset §7 誠實標:不是零接觸,是不落地、不持久、不持金鑰)。
### 2.5 讀取/注入流程(執行期 by ref)
1. cypher 執行 workflow,遇 recipe `auth_service='telegram'`。
2. 查 D1 `credentials` where api_key+name=`telegram_bot_token` → 取 `secret_ref` + `sensitivity`。
3. 用 `secret_ref` 向 Secrets Store 取密文值(路徑 1REST by ref)。
4. 依 `auth_recipe:telegram` 的 `inject.path/header` 注入(與現行 auth-dispatcher 同型,只是值來源從「KV 密文+WASM 解密」換成「Secrets Store 取值」)。
5. 更新 D1 `last_used_at`。
> ⚠️ rule 02 §2.2/§2.3 對齊:解密/注入仍不在 cypher TS 實作業務邏輯。Secrets Store 取值是「host 能力」(類比 `kv_get`/`crypto_decrypt`),應落在 wasi-shim host function(新增 `secret_get(ref)`)或等價的 host 邊界,**WASM 零件仍是注入邏輯的所在**。施工時須守此界線(見 §5)。
---
## 3. 治理 / UX 端點(n8n 模式)
| 端點 | 行為 | 對齊 D19 |
|---|---|---|
| `GET /credentials` | 讀 **D1**,回 `[{name, service, sensitivity, created_at, last_used_at}]`**不含 secret_ref 對外、不含值** | 顯示清單 + last_used |
| `PUT /credentials/:name`(replace | 整筆覆寫:寫新值進 Secrets Store(同 ref 覆蓋或新 ref+ 更新 D1 metadata | 只能 replace |
| `DELETE /credentials/:name` | 刪 D1 row + 刪 Secrets Store secret | 可 delete |
| ~~`GET /credentials/:name/value`~~ | **不存在 / 移除任何讀回值的路徑** | 讀不回值 = 設計 |
CLI 薄殼(rule 07):`acr creds list`(讀 D1 顯示)、`acr creds replace <name>`(覆寫)、`acr creds delete <name>`。**移除任何「印出 credential 值」的路徑**。
---
## 4. 遷移:雙讀過渡 + 回填 + 回滾
### 4.1 雙讀過渡(不停機)
執行期取 credential 值的順序:
1. 先查 D1 有無 `secret_ref` → 有 → 走 Secrets Store 取值(新家)。
2. 找不到 → fallback 舊路徑:`CREDENTIALS_KV {api_key}:cred:{name}` + WASM `crypto_decrypt`(舊自管)。
→ 新寫入一律走新家;舊資料未回填前仍可讀。
### 4.2 回填(一次性、冪等、可審)
`POST /credentials/migrate-to-secrets-store`(類比現有 `migrate-cron-index` 冪等端點):
- 對每個 `{api_key}:cred:{name}` KV rowWASM 解密取明文 → 寫進 Secrets Store(領 `secret_ref`)→ 在 D1 建 row(含 ref + metadata)。
- 冪等:D1 已有該 (api_key,name) row 且 ref 可解析 → 跳過。
- **誠實回報**逐筆 ok/fail(不假綠,mindset §7)。
- **回填完成且驗證通過後**,才執行 §4.4 廢 ENCRYPTION_KEY。
### 4.3 回滾錨點
- 回填**不刪 KV 舊密文**(保留為回滾錨點),只新增 D1+Secrets Store。
- 出問題 → 雙讀 fallback 自動回到 KV 路徑;D1 row 可刪、Secrets Store secret 可刪 → 回到純 KV 狀態。
- 只有在「全租戶回填驗證綠 + 觀察期無 fallback 命中」後,才進 §4.4。
### 4.4 廢自管 ENCRYPTION_KEY(最後一步)
- 移除 `crypto_decrypt` 對 credential 的依賴路徑(KV 密文路徑停用)。
- `wrangler secret delete ENCRYPTION_KEY`cypher / auth_static_key / auth_service_account)。
- 清理 KV 舊密文(確認 Secrets Store 是唯一真相源後)。
- ⚠️ **此步不可逆**,須 leo 明示放行 + 回填觀察期通過。
---
## 5. 與鐵律 / 既有架構對齊(施工硬約束)
- **rule 02 §2.2/§2.3**cypher TS 不實作 credential 解密/注入業務邏輯。Secrets Store 取值走 host function 邊界(新增 `secret_get(ref)` 於 wasi-shim),注入仍在 WASM auth primitive。
- **rule 01 加解密**:自管 AES-GCM 路徑在回填後廢除;新家由 CF Secrets Store 託管金鑰(arcrun 不持金鑰)。
- **不新增 component**mindset §1);不新增 service bindingrule 03 §3.1)。
- **薄殼(rule 07**CLI/MCP 只做「list 顯示 / replace 覆寫 / delete」介面轉換,store 能力在 API。
- **部署繞開 GitHub Actions**D1 migration + Secrets Store ensure 走 `acr init/update`wrangler 直推),不掛 Actions。
- **D1 同源**:用使用者既有 `arcrun-kbdb` D1(不新建第二顆),migration `0002_credentials.sql` 走現有 `deploy.ts` migration 注入路徑(與 `0001_base.sql` 同模式)。
---
## 6. 開放問題(施工前須 leo / 總管拍板)
- **Q-a(核心)**Secrets Store 動態 by-ref 取值路徑(§2.3 路徑 1 REST API)—— leo21c 自己的 CF 帳號是否支援、API 路徑與 token 範圍?需一次 spike 驗證。**這是整個遷移的硬前置**(取值不通則 D19 走不了)。
- **Q-b**:寫入時 client 是否仍加密一層(§2.4 甲/乙)。傾向甲(不持金鑰,TS 短暫經手明文不落地)——需 leo 接受此誠實 trade-off。
- **Q-c**`sensitivity` 分級的判定(誰標 high/standard)——auth-recipe 可加 `sensitivity` 欄宣告該 service 的 credential 等級,或一律 high。
- **Q-d**:與 redesign.md C(友善前門:.env → 一次填)整合——C 的「值來源 .env」在新架構下是「.env 明文 → cypher 寫進 Secrets Store」,與 §2.4 甲一致。確認 C 依附本 SDDstore 是 Secrets Store 不是 D1)。
---
## 7. 任務分解(待放行後施工;本批僅 SDD)
- [ ] T1 spikeleo21c CF 帳號 Secrets Store create store + worker by-ref 取值驗證(解 Q-a)。**硬前置。**
- [ ] T2 D1 migration `0002_credentials.sql`(§2.2+ deploy.ts 注入。
- [ ] T3 `acr init/update` 新增 ensure Secrets Store store + 注入 store_id(類比 ensureD1Database)。
- [ ] T4 wasi-shim 新增 `secret_get(ref)` host function(守 rule 02 邊界)。
- [ ] T5 寫入路徑:`POST/PUT /credentials` 改寫 Secrets Store + D1 ref(§2.4)。
- [ ] T6 讀取/注入路徑:auth-dispatcher 改 D1 ref → Secrets Store 取值(§2.5+ 更新 last_used。
- [ ] T7 雙讀 fallback(§4.1)。
- [ ] T8 回填端點 `POST /credentials/migrate-to-secrets-store`(§4.2,冪等可審)。
- [ ] T9 治理端點 list/replace/delete + 移除任何 read-value 路徑(§3+ CLI 薄殼。
- [ ] T10 回填驗證 + 觀察期 → 廢 ENCRYPTION_KEY(§4.4leo 明示放行)。
> **每個 cred 操作跨 TS / WASM / host-function / 兩個 store,必端到端實測**(防再假綠,mindset §7)。T1 不通則整案停,先解 Q-a。
@@ -0,0 +1,211 @@
# Credential Store 重設計提案(A telegram 一致性 + B KV→D1 + C 友善前門)
> 建立:2026-06-29 by arcrun CC|更新:2026-06-29(依 leo 最終精確 spec 改寫)|對應 issueArcrun#13
> 範圍宣告:本檔是既有 SDD `credential-primitives-wasm/` 的補充設計筆記(不是新 SDD 子系統,rule 02 §4.3 例外)。
> 詞彙(leo 堅持精確):**零件/component=TinyGo WASM**、**recipe=http_request+固定設定(打最終 API**、
> **workflow=多步 YAML(在這層「按名指定要哪個 credential」)**、**auth-recipe=credential 怎麼注入**。
---
## 0. leo 的最終 spec(一句話):**不是拆除,是把前門做友善 + 把 D1 收尾**
**保留全部既有零件,只改「credential 怎麼餵進去」,不改 recipe 本身。**
```
人填 .env / 環境變數 ← 友善前門,任何人都會(本機=.env;雲端=code-on-web 的 Environment 設定,對稱)
↓ acr creds push 保留,但「值的來源」= .env/env-var(不再強迫手寫 credentials.yaml
長久存「我的 D1」 ← 存一次,存進使用者自己的 D1;之後自動,永不重推
↓ workflow YAML「按名」指定要哪個 credential"我要 google 那把")→ 自動從 D1 取
recipe 打最終 API ← telegram_send / notion / gsheets,保留,不刪
```
**對稱性**:本機讀 `.env`;雲端讀 code-on-web 的 Environment 設定。**同機制,只是來源位置不同。**
### 明確「保留、不可刪」
1. **`acr creds push` 保留** —— 手動推單一 credential 仍合法(只是不再是唯一/被迫的路徑)。
2. **最終 API recipe 保留** —— telegram_send / notion / gsheetshttp_request + 固定設定)是打 API 的端點,**不刪**。改的是「credential 怎麼餵」,不是 recipe。
---
## 0.5 Q1 — list 是動態讀 store 還是 hardcoded?(已核實 code2026-06-29
**結論:recipe/auth-recipe 的「正規清單」是動態的;但 `acr parts` 是 hardcoded 且把 5 個 recipe 混進去 → 那 5 個之外的 pushed recipe 在 `acr parts` 看不到。**
| 指令 | 讀哪裡 | 動態? | pushed recipe 會出現? |
|---|---|---|---|
| `acr recipe push` | `POST /recipes` → KV storerecipe.ts:87 | — | 寫入 store |
| `acr recipe list` | `GET /recipes``RECIPES.list({prefix:'recipe:'})`recipe.ts:182 / recipes.ts:260 | ✅ 動態 | ✅ 會 |
| `acr auth-recipe list` | `GET /auth-recipes``RECIPES.list({prefix:'auth_recipe:'})`auth-recipe.ts:42 / recipes.ts:547 | ✅ 動態 | ✅ 會 |
| **`acr parts`** | **hardcoded `BUILTIN_COMPONENTS` 陣列(parts.ts:28-262),完全不 fetch store** | **❌ 靜態** | **❌ 不會** |
- **component(零件=WASMhardcoded 是對的**:零件只能 PR merge 新增(mindset §4 人類閘門),固定、慢增,靜態清單反映真實。
- **但 `acr parts` 把 5 個 recipe 也 hardcode 進去**parts.ts:197-261gmail_send / google_sheets_append / telegram_send / line_notify_send / notion)——**這是 component 與 recipe 的 conflation**。recipe 是動態投稿的(任何人 `acr recipe push`),卻被釘進 `acr parts` 的靜態清單。
- **後果(leo 的擔憂成立,但範圍精確)**:用戶 `acr recipe push my_recipe` → 進 store → `acr recipe list` / `acr recipe search` **看得到**;但 `acr parts` **永遠看不到**(除非改 source 重發 CLI)。「submitted = invisible」**只發生在 `acr parts` 這個面**,不是整個生態壞掉。
- **與 telegram bug 的關係**telegram bug 是另一條軸(seed source vs live KV 漂移,A 已修),但與此**同病**=「hardcoded 清單 ≠ live store」。`acr parts` 列退役/釘選 recipe#13)也是這病的症狀。
### Q1 修法 scope(建議,未在本批改 code)
1. **`acr parts` 移除 5 個 recipe 的 hardcode**parts.ts:197-261),只留**真零件**logic/data/ai/http_request/cron…=WASMPR-only,靜態正確)。
2. **`acr parts` 末尾改成「指路」**:列完真零件後,動態提示「整合類服務 → `acr auth-recipe list`(即時讀 store);API recipe → `acr recipe list`(即時讀 store)」。讓 AI/人從 `acr parts` 被導到動態清單,不在靜態清單找 recipe。
- 進階(可選):`acr parts` 末段直接 fetch `GET /recipes` + `GET /auth-recipes` 把 store 的 recipe 也列出來(標明「動態,來自 store」),徹底消除「submitted = invisible」。
3. **明確分流原則寫進 parts.ts 註解**:零件=靜態(PR-only);recipe/auth-recipe=動態(從 store 讀)。不再把 recipe 釘進零件清單。
4. 這 scope #13「acr parts 殘留」的根治版(#13 只把 recipe 描述改對,沒解決「recipe 不該 hardcode 在 parts」的結構問題)。**屬薄殼修正(rule 07)=把「列 recipe」這能力從介面靜態資料改成讀 API store,不違禁令。**
---
## 1. 現狀(已核實 code + 實測 live2026-06-29
### 1.1 儲存:**KV,不是 D1**(B 的根因=未完成的遷移,非「設計即如此」)
- credential 只存 `CREDENTIALS_KV`key=`{api_key}:cred:{name}`AES-GCM。
- 寫:`acr creds push``POST /credentials``cypher-executor/src/routes/credentials.ts:48`)。
- 讀+解密:`auth_static_key` WASM `kv_get``registry/components/auth_static_key/main.go:146`)。
- binding`cypher-executor/wrangler.toml:23`;型別 `src/types.ts:31`
- **全 repo 無任何 credential 的 D1**D1 引用全是 KBDB graph)。
- leo 確認:採用 KBDB 後使用者已有自己的 D1,當時就要求 credential 也搬進「我的 D1」長久存 → **此遷移從未完成**
→ BKV→D1)**確定要做**,是 leo 明確要的「長久存我的 D1」層。
### 1.2 .env → credential**值不會自動進**(C 的痛點,但前門不友善 ≠ 拆掉)
- `.env` 目前只供「設定」:`ENCRYPTION_KEY`/`NAMESPACE`/`ARCRUN_*`/`CLOUDFLARE_*``cli/src/lib/config.ts:125`)。
- credential 的**值**只來自手寫 `credentials.yaml``acr creds push``cli/src/commands/creds.ts`)。
- leo 記憶「notion/gsheets 只靠 .env 就通」與 code 不符——實際是早先 push 過、值已在 KV。
- 痛點(北極星「拿 credential ≠ 攻打用戶」):每次要手寫 yaml;不像 n8n Credentials tab 填一次長存。
**C 不是發明「自動偵測魔法」**,而是把 `acr creds push` 的**值來源**從「手寫 yaml」改成「讀 .env/env-var」,存進 D1,之後 workflow 按名取。
### 1.3 telegram 漂移(A)——已實測 smoking gun,已修 source
- recipe `telegram_send``api-recipe-seeds.ts:108``auth_service:'telegram'`endpoint `.../bot{{auth.bot_token}}/sendMessage`
-`auth-recipe-seeds.ts` source 只有 **23** 個 service,缺 `telegram`/`line_notify`/`kbdb`/`google_user`
- 實測:prod = 27(手動 seed 過 telegram);self-host **leo21c = 23,無 telegram** → telegram_send 找不到 auth recipe → `{{auth.bot_token}}` 注入空 → URL 變 `.../bot/sendMessage` → 壞。notion/gsheets 通是因它們在那 23 裡。
- 這是「source vs live 漂移=假綠」。telegram **不是**走 legacy `BUILTIN_CREDENTIALS_MAP`,正確形態一直是 recipe+auth-recipe,只是 auth-recipe 沒進 source 種子。
---
## 2. 目標狀態(target,=leo 的 3 層流,全部保留只連得更友善)
1. **唯一打外部 API 的方式** recipe(保留)。telegram 與 notion/gsheets **同一條**「workflow 按名指定 → 從 D1 取 → auth-recipe 注入 → recipe 打 API」鏈,無例外(A,本次已修 source)。
2. **credential 長久存使用者自己的 D1**(如 n8n Credentials tab):填一次、長存、workflow 按名引用、永不重推(B)。
3. **友善前門**:使用者把 token 填進 `.env`(本機)/ code-on-web Environment(雲端)**一次**`acr creds push`(保留)以此為值來源,client 端加密後存進 D1。不再被迫手寫 `credentials.yaml`C)。
---
## 3. 遷移步驟(A 已做;B/C 是設計變更,誠實標工作量)
### Phase A — telegram/line_notify/kbdb 一致性(本次已做,commit 90777e3,未 push/merge
- [x] `auth-recipe-seeds.ts` 補 telegraminject.path bot_token/ line_notifyheader Bearer/ kbdbheader Bearer),形態取自 prod。
- [x] `recipes.ts` `AuthInjectSpec``path?`WASM/SDD §六早有、source 介面漏 → tsc 擋)。
- [ ] google_useroauth2)暫不回灌:內嵌 client_secret 不可進 git + 介面無 oauth2 欄 → 留 Phase D。
- 部署後 leo21c 跑 `acr update`(/init/seed) → 23→26telegram 即與 notion 同鏈可用。
- **不刪任何 recipe;不碰 `acr creds push`。** 純補種子 + 補型別欄位。
### Phase B —(已翻案,見 §4.5)儲存:**KV 維持,不搬 D1**;高敏感可選補 Secrets Store
> 早先版本寫「KV→D1 遷移」。§4.5 核實史實 + leo 自己 2026-06-07 拍板(credential 留 KV)後**翻案**
> credentialRedis-class(按 key 直接點查、密文不可 SQL 查),搬 D1 是淨負債。**B 改為:**
1. **低敏感 credential 維持 KV**(現狀 CREDENTIALS_KVAES-GCM)。`acr creds list` 已可用(`GET /credentials` KV.list)。
2. **(可選、後續)高敏感(service account JSON/private key)→ Cloudflare Secrets Store**credential_parts.md §8.2KV 只存 ref)。唯一值得的儲存升級,與拍板不衝突。
3. **不新增 D1 credentials 表、不新增 `cred_get` host function、不做 KV→D1 migration**——這些原計畫取消(好處 KV 已有)。
4. 若 leo 仍要 D1:屬新需求(須更新 2026-06-07 拍板)。即便如此,密文本體仍留 KV/Secrets StoreD1 只放非機密 metadata。
5. 風險:跨 TS Worker / WASM / host function 三層,**必端到端實測**(防再假綠,mindset §7)。**非小工程。**
### Phase C — 友善前門:`acr creds push` 值來源=.env/env-var → D1(小~中工程,依附 B)
> **精確(leo 修正)**:不是「自動 pickup 魔法」,是「`acr creds push` 保留,但值的來源改成 .env/env-var,存進 D1,之後 workflow 按名取」。
1. `acr creds push`(保留)新增「從 .env/env-var 讀值」模式:掃 .env 中「已知 credential 名」
(由 auth-recipe 的 `required_secrets[].key` 反推,例 `notion_token`/`telegram_bot_token`)→ client 端加密 → 存進 D1Phase B 的 store)。
- 仍保留現有「從 credentials.yaml 讀值」模式(向後相容,不刪)。yaml 變成可選輸入源之一,不再是被迫。
2. **對稱**:本機讀 `.env``config.ts loadDotEnvOnce` 已會載入 .env 進 `process.env`);
雲端=code-on-web 的 Environment 設定(同樣落到 env-var)。同一套讀法、不同來源位置。
3. 名稱規約:.env 用大寫(`NOTION_TOKEN`)→ normalize 成 credential 名 `notion_token`(=auth-recipe required_secrets.key)。
4. **薄殼界線**rule 07):這是「擴 `acr creds push` 的輸入源」=介面慣例的輸入解析(§2 允許第 1 項),
**不是**在 CLI 拼裝 API 缺的能力(值來源是 .env/yaml 都只是「轉成 API 期望的加密 payload」)。加密仍 client 端(§2 第 4 項唯一例外)。store 仍是 API(D1)。
5. 結果:填一次 .env → `acr creds push`(或 deploy 末尾自動觸發一次)→ 長存 D1 → workflow 按名引用、零重複推。
### Phase D(後續,非本批)— google_user(oauth2) 回灌 + oauth2 secret 部署期注入
- AuthRecipeDefinition 介面補 oauth2 欄位;client_secret 走 `wrangler secret`/env 注入(不進 git),再回灌 source。
---
## 4. 正解:telegram 的 workflow / recipe yaml(給 mira 抄)
telegram 發訊=**recipe `telegram_send`**(不是 component、不是自建零件);**credential 在 workflow 層按名指定**
```yaml
name: notify_done
flow:
- "notify >> ON_SUCCESS >> end" # 單節點時 flow 可省
config:
notify:
component: telegram_send # ← 內建 recipe canonical_id(不是 component:telegram
chat_id: "123456789"
text: "✅ 任務完成"
# bot_token 不寫這裡。recipe 的 auth_service='telegram' → 按名從 D1 取 telegram_bot_token
# → auth-recipe:telegram 的 inject.path 注入進 URL path。一致鏈,與 notion/gsheets 同型。
```
**credential 解析(target 設計):**
1. 使用者把 `TELEGRAM_BOT_TOKEN=123456:ABC...` 填進 `.env`(本機)/ code-on-web Environment(雲端)**一次**。
2. `acr creds push`(保留;值來源=.env)→ client 加密 → 存進「我的 D1」credential store。**之後永不重推。**
3. 執行時:cypher-executor 見 `telegram_send.auth_service='telegram'``auth_static_key` WASM 用 `cred_get` 從 D1 取
`telegram_bot_token` 密文 → `crypto_decrypt` → 依 `auth_recipe:telegram``inject.path.bot_token` 輸出 `auth_path`
`makeRecipeRunner` 把 endpoint `{{auth.bot_token}}` 換成真 token → POST `https://api.telegram.org/bot<token>/sendMessage`body=`{chat_id,text}`
這條鏈與 notionheader 注入)/gsheetsservice_account runtime token 注入)**完全同型**,只注入位置不同(path vs header)。
> **過渡期(B/C 未完成前,今天就能用的方式)**:在 `credentials.yaml` 放 `telegram_bot_token` + `acr creds push`(值進 KV)。
> 上面 yaml 的 workflow 寫法不變——只是 store 暫時是 KV、值來源暫時是手寫 yaml。B/C 完成後 store→D1、來源→.env**workflow yaml 一字不改**(按名引用穩定)。
---
## 4.5 Q2 — KV vs D1:史實核實 + 三方調和 + CC 推薦(leo 授權我決定)
### 史實(git 核實,2026-06-29):**本 repo 的 credential 從來沒在 D1**
- 最早 MVP commit `2707fca`credential 即「AES-GCM decrypt from **CREDENTIALS_KV**」。
- 更早的 u6u-credentials ancestor worker`credentials/` 已刪):`CREDENTIALS_KV.put('cred:${id}')`——**也是 KV**。
(它的 `CredentialRecord` 已有 name/type/created_at 結構欄位,但以 JSON 存進 KV。)
- 全 history 的 `CREATE TABLE` 只有 KBDBentries/templates/entry_values),**從無 credentials 表**。
-**找不到任何「credential D1→KV 撤退」的 commit**。leo 記憶的「原本 D1 有 credentials 表、後來退 KV」
**在本 repo 史實裡不存在**(可能是 pre-repo 設計或別處,沒落地過 code)。
### 三方說法(全部攤開,none 寫 D1 存 credential
| 來源 | 對 credential 儲存的說法 |
|---|---|
| leo 口頭(現在) | 存 **D1**(「長久存我的 D1」) |
| 現行 code | **KV**CREDENTIALS_KVMVP 至今未變) |
| **kbdb-base SDD 拍板 2026-06-07leo 親簽「Leo:同意」)** | **credential 留 KV**(「session、verdict、credential 留 KV=短期高頻純取用,類比 Redisworkflow/recipe/成功記錄才進 D1=要層級/列舉/排序」)。`kbdb-base/design.md:318``tasks.md:11``tasks.md:83` 三處一致。 |
| credential_parts.md §8.2(長期需求源) | **tenant KV + Cloudflare Secrets Store**(低敏感→KV AES-GCM;高敏感如 service account JSON/private key→Secrets StoreKV 只存 ref)。**不是 D1。** |
**leo 口頭「D1」與他自己 2026-06-07 的書面拍板(credential 留 KV)直接衝突**,且兩份書面需求源都沒說 D1。
SDD 協議(00-sdd-protocol「規範互相矛盾→引原文、不自行猜」)要求攤開矛盾;但 leo 此次**明確授權「你決定並推薦」**,故下方給推薦(grounded in 上述證據),同時把矛盾標清楚讓 leo 有機會推翻。
### 「退 D1→KV 的理由還成立嗎?」(leo 的核心提問)
- 本 repo 沒發生過該退守,故無「當初理由」可考。但 leo 假設的理由(「self-hosted 少依賴、免 D1」)**今天確實已 moot**——
self-hosted 用戶現在本來就有一顆 KBDB D1(`kbdb/wrangler.toml:10` `[[d1_databases]]`deploy.ts 注入用戶自己的 id)。
- **但「能用 D1」不等於「credential 該用 D1」。** 2026-06-07 拍板把 KV 留給 credential 的理由不是「沒有 D1」,
而是**「credential =短期高頻純取用(by api_key+name 直接點查),是 Redis-class 工作負載,不需要 D1 的層級/列舉/排序」**。
這個理由**與有沒有 D1 無關,今天仍成立**。
### ✅ CC 推薦(leo 授權我決定):**credential 主存維持 KV;不為「列表/按名引用」而搬 D1**
理由(誠實 trade-offmindset §7):
1. **守 leo 自己的拍板**2026-06-07「credential 留 KV」是有理由的設計(Redis vs PostgreSQL 分工),非偷懶。推翻它要有新理由,目前沒有。
2. **D1 宣稱的好處在 KV 上已可達或不需要**
-`acr creds list`」KV 已能做(`GET /credentials``KV.list({prefix})` 列名,credentials.ts:68-78 已實作,**今天就有**)。
- 「按名引用(workflow 指定要哪把 cred)」是 **key 設計**`{api_key}:cred:{name}`)就給的,與 KV/D1 無關——telegram_send 的 `auth_service``required_secrets.key` 已是按名取。**A 修好後就具備,不需 D1。**
- 「結構化欄位/sensitivity 分級」credential 是不可查詢的密文 blob(查詢明文=洩漏),D1 的 SQL 查詢優勢對 credential **用不上**;分級需求由 credential_parts.md 的 **Secrets Store**(高敏感)解,不是 D1。
3. **搬 D1 成本高且踩鐵律邊界**:要新 host function `cred_get`WASM 改)、一次性 migration、跨 TS/WASM/host-function 三層 e2e——換來的好處 KV 已有。**淨負債。**
4. **真正缺的不是 D1,是「友善前門 C」**:leo 真痛點是「每 cred 手寫 yaml + 重推」,那是 **C(.env 當值來源 + 一次存)** 解的,**與 store 是 KV 還 D1 無關**。
**推薦的目標儲存(取代本檔早先「搬 D1」設計):**
- **低敏感 credential(多數 token/api_key)→ 維持 KV**(現狀,AES-GCM)。
- **高敏感(service account JSON / private key)→ Cloudflare Secrets Store**(依 credential_parts.md §8.2KV 只存 ref)——這是**唯一值得做的儲存升級**,且與 leo 拍板不衝突(拍板說 credential 留 KV,未禁高敏感走 Secrets Store;兩者互補)。
- **C(友善前門)照做**`acr creds push` 值來源=.env/env-var、存一次、workflow 按名取。**store 仍 KV(+Secrets Store)**。
-**B 從「KV→D1 遷移」改為「KV 維持 + 高敏感補 Secrets Store(可選、後續)」**。telegram/notion/gsheets 的一致鏈(A)與一次填(C)**完全不依賴 D1**。
> 若 leo 看完仍要 D1(例如想未來在同一顆 DB 做 cred 審計/輪替記錄):那是**新需求**,需更新 2026-06-07 拍板。
> 屆時 credential **本體**仍建議 KV/Secrets Store(密文不需 SQL),D1 只放**非機密的 metadata**cred 名稱/服務/建立時間/last_verified),與 workflow/recipe 進 D1 同模式。**不要把密文搬進 D1 當主存。**
---
## 5. 與既有鐵律/SDD 的對齊
- **不刪 recipe、不禁 `acr creds push`**leo spec)。A 純補種子 + 型別欄位;C 是擴 `acr creds push` 輸入源,非拆除。
- 不新建 componentmindset §1 / rule 02)。
- credential 解密仍在 WASMrule 02 §2.2);KV→D1 只是密文來源從 `kv_get` 換成新 host function `cred_get`(屬 wasi-shimrule 02 §2.3 合法)。
- 薄殼(rule 07):`acr creds push` 讀 .env 仍只做「輸入解析 + client 加密 + 呼叫 API」,能力(store)在 API/D1,不在介面層。
- 設計權威:`auth-recipe.md` §六(telegram path 注入 line 70-71)、§七(kbdb 共用 line 150-151);
需求源 `docs/user_requirements/credential_parts.md`credential 長期規格)。
@@ -0,0 +1,245 @@
# 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 primitivecrypto.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
```
stdinWorker → 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_accountcrypto_sign_rs256(jwt, pkcs8) + http_request 換 token
5. 展開 recipe.inject 的 {{secret.X}} / {{runtime.X}} 模板
stdoutWASM → 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 signingRS256PEM→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 Worker(見下「呼叫方式」)
3.`{ action, api_key, service, request }`**不送 secrets、不送 recipe plaintext**
4. WASM 透過 host function 自行 `kv_get` 讀 recipe + 加密 secret`crypto_decrypt` 解密
5. 讀回傳 → 合併 `_auth_headers` / `_auth_query` / `_auth_body` 進 ctx
**呼叫方式(Phase 72026-06-06 演化)**:原設計走 `fetch(wasmWorkerUrl(auth_{primitive}, WORKER_SUBDOMAIN))`workers.dev 公網 URL)。但壓測階段 11 證實:**self-hosted** cypher`arcrun-cypher-executor.{sub}.workers.dev`)用 `fetch()` 打**同 `{sub}.workers.dev` zone** 的 auth workerCF 回 **1042**(官方 docs:「fetch from another Worker on the **same zone**」)。官方不踩是因官方 cypher 在自訂域 `cypher.arcrun.dev`、打 `*.workers.dev` 屬**跨 zone**(非官方有 flag)。token/解密鏈本身正常,唯一卡點是同 zone fetch。
- **解法 = `global_fetch_strictly_public` compatibility flag**cypher wrangler.toml),auth-dispatcher **維持原本 `fetch(workers.dev)` 不改**。此 flag 讓 `fetch()` 走公網「前門」→ same-zone fetch 也通(官方 docs:「Worker-to-Worker fetch 可用 service binding **或** `global_fetch_strictly_public` flag」)。
- **為何選 flag 不選 service binding**service binding 靜態、加/改要重 deploy cypher(官方 docs);flag 一行解、用戶無感、不動 service binding 禁令。先做了 service bindingA)後 richblack 拍板廢、改 flagB)。
- **flag 安全(查證官方 docs**:唯一副作用「Worker fetch 自己 hostname → self-loop」;cypher 只打外部 API + sibling auth worker(皆非自己 hostname)→ 不 self-loop。
- **官方/self-host 共用同一份 toml**:官方 cypher 本就跨 zonecypher.arcrun.dev → workers.dev),加 flag 行為不變;self-host 同 zone 被修好。`stripOfficialOnlyBindings()` 不碰 `compatibility_flags`self-hosted 部署自動帶上。
- `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 FunctionsWASM ↔ 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 不得實作零件邏輯」。)
---
## §8 `{{credential.X}}` 用戶面注入語法(2026-06-10richblack 確認 change
### 問題(壓測實證的 401 根因)
Haiku 自主壓測時自然地在 workflow node.data 寫 `{{credential.openai_key}}` 引用已存的 credential
結果打目標 API 回 **401**;只有把 token **明文硬編碼**進 workflow 才 200。
**根因(系統客觀證據,三條 template 展開路徑都不認 `credential.` namespace**
1. `graph-executor.ts` `interpolateString()`node.data 展開)只認「context 內已存在的 key path」
`{{input.X}}` / `{{data.Y}}` / 任意 dot path),**無 `credential` namespace** → `{{credential.X}}`
展不開 → header 帶字面值 `Bearer {{credential.X}}` 或空 → 401。
2. auth recipe `inject` + auth primitive WASM 只認 `{{secret.X}}` / `{{runtime.X}}`,那條是 auth recipe
**自動注入**路徑(recipe 宣告 `auth_service` 才觸發),不是用戶在 node.data 直接引用的入口。
**正解(auth recipe 自動注入)其實存在,但用戶不知道、直覺會寫 `{{credential.X}}`** → 這是
**設計缺口**(缺一個直覺的用戶面 credential 引用入口),不是單純 bug。
### 決策:讓 `{{credential.X}}` 真的能用(richblack 2026-06-10
新增「用戶在 workflow node.data 直接寫 `{{credential.NAME}}` → 自動解密回填」這條入口。
**硬約束(rule 02 §2.2**:解密**絕不能**在 graph-executor TS 做(hook 擋 `crypto.subtle.decrypt`
且違反「TS 不實作 credential 邏輯」)。故 `{{credential.X}}` 的解密**復用既有 auth_static_key WASM**。
### 設計:auth_static_key 新增 `resolve_credentials` action
| 角色 | 職責 |
|------|------|
| graph-executor.ts(薄 routing | 偵測 node.data 內 `{{credential.NAME}}` → 收集 names → 呼叫 auth_static_key WASM `resolve_credentials` → 拿回明文 → 回填 node.data。**不解密、不碰 ENCRYPTION_KEY**。 |
| auth_static_key WASM(新 action | 收 `{action:"resolve_credentials", api_key, names:[...]}` → 對每個 name `kv_get("{api_key}:cred:{name}")` + `crypto_decrypt` → 回 `{success, credentials:{name: plaintext}}`。**不查 recipe**(與 authenticate 分流)。 |
**stdin/stdoutresolve_credentials**
```
stdin: { "action": "resolve_credentials", "api_key": "ak_xxx", "names": ["openai_key", "..."] }
stdout: { "success": true, "credentials": { "openai_key": "sk-...", ... } }
缺某 name → success:false + error 指明缺哪個(誠實,不假綠)
```
**展開時機(graph-executor**:在 node.data interpolate(現有 `interpolateData`**之後**、
node 執行**之前**插一步:掃描展開結果裡殘留的 `{{credential.NAME}}` → 走 WASM resolve → 回填。
放在 auth recipe 自動注入(`tryAuthDispatch`)**之外**,兩條入口並存但不重疊(一個是用戶顯式引用、
一個是 recipe 宣告 `auth_service` 隱式注入)。
**安全邊界(同 authenticate**
- `crypto_decrypt` host function 內部用 `ENCRYPTION_KEY`,**永不暴露給 WASM / 不經 graph-executor**。
- `kv_get` host 側檢查 `{api_key}:cred:*` 的 api_key 必須等於 stdin api_key(防越權,現有機制)。
- graph-executor 只傳 `names`(明文 credential 名)+ api_key**不傳、不見** ciphertext / plaintext key。
### 為何不選「新建 credential_resolve 零件」
auth_static_key 已具備 `kvGet` + `cryptoDecrypt` 全部能力,新建零件 = 功能重疊 + 多一個 Worker
部署成本。加一個 action 最貼近現有架構(richblack 2026-06-10 拍板)。
### 不做
- ❌ 不在 graph-executor 解密(rule 02 §2.2
- ❌ 不改 credential KV key 格式(仍 `{api_key}:cred:{name}`
- ❌ 不碰 auth recipe 自動注入路徑(`tryAuthDispatch` / `{{secret.X}}` 不變)
---
## 不做的事
- ❌ 不改 recipe YAML schema — 沿用現有格式
- ❌ 不改 KV 儲存結構 — `auth_recipe:{service}` / `{api_key}:cred:{name}` 不變
- ❌ 不改 SDK API — SDK 仍是 HTTP thin wrapper
- ❌ 不建新的 Worker — 在 cypher-executor 內完成(§8 復用 auth_static_key,不新建零件)
@@ -0,0 +1,237 @@
# 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_id6 個 API 零件 + 4 個 auth primitive)+ `wasmWorkerUrl()` URL 慣例輔助函數;解析鏈新增為第 8 層(放在 `BUILTIN_API_RECIPES` fallback 之後,避免 Phase 3 尚未完成時 API 零件 Worker 未部署造成 404Phase 3 刪除 `BUILTIN_API_RECIPES` 後,API 零件會自然落到此層)。auth primitive 從此層進入。`tsc --noEmit` 通過。
---
## Phase 1auth_static_key WASM(優先,涵蓋 80% 服務)
方案 BWASM 自行讀 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 recipeBasic Auth)→ 成功注入
- [ ] 1.9 **刪除 `credential-injector.ts` 整檔**`decryptCredential` / `decryptSecrets` / `interpolateTemplate` / `BUILTIN_CREDENTIALS_MAP` 全刪)
---
## Phase 2auth_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(純 Gobase64 decode + 去 header/footer
- 組 JWT header + payloadbase64url),呼叫 `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 runnerHTTP 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 4auth_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」)
---
## Phase 7auth primitive 改走 service binding(解 self-hosted CF 1042
> 2026-06-06 richblack 拍板。來源:壓測報告階段 11`test_arcrun/docs/壓測報告.md §11`)。
**背景(架構演化)**
- 當初設想「用戶對每個服務建**零件**」→ 零件用 service binding 要 redeploy → 用戶建 workflow 要 redeploy(不可接受)→ 故禁 service binding、改 cypher bindingHTTP URL)。
- 後來架構演化:用戶要的是 **recipe(資料,存 KV+ 固定 primitive**,不是新零件。`acr recipe push` 進 KV **不 deploy**。用戶用「http_request primitive + 不同 recipe」打各種服務,永不新增 primitive。
- 結論:「用戶建 workflow 要 deploy」這個禁令前提**在 recipe 模型下不成立**。primitivehttp_request + 4 個 auth)是**平台固定基礎設施**,和 13 個邏輯零件同類,給它 service binding 的 deploy 成本只在平台部署時付一次,**不落用戶身上**。
**根因(壓測階段 11,唯讀證據)**
- `auth-dispatcher.ts:67-68``fetch(wasmWorkerUrl('auth_service_account', WORKER_SUBDOMAIN))` 打 workers.dev → CF 回 **404 + "error code: 1042"**CF 邊緣頁,非 auth worker 回應)。
- **1042 精確定義(官方 docs2026-06-06 查證)**:「Worker tried to fetch from another Worker **on the same zone**, only supported when `global_fetch_strictly_public` flag is used」。關鍵是 **same zone**
- **self-hosted 踩**cypher`arcrun-cypher-executor.{sub}.workers.dev`)與 auth worker`arcrun-auth-*.{sub}.workers.dev`**同在 `{sub}.workers.dev` zone** → 同 zone fetch → 1042。
- **官方不踩**:官方 cypher 在自訂域 `cypher.arcrun.dev`,打 auth 的 `*.uncle6-me.workers.dev` 是**跨 zone**,不觸發 same-zone 限制(非「官方有 flag」——官方 wrangler.toml 只有 `nodejs_compat`,無 `global_fetch_strictly_public`)。
- 這是 P0#9「同 zone 522」的**另一形態**:之前解的是 `*.arcrun.dev` 同 zone,沒解到 `*.workers.dev` 同 zone。service binding 是內部 RPC 不經 zone → 徹底免疫(13 邏輯零件正是走 binding 才不踩)。
- 直接從外部打 `arcrun-auth-service-account.leo21c.workers.dev` → 200 + 真 `ya29...` token → **JWT/解密/token 換取鏈本身全正常**,唯一卡點是 cypher 打不到自己帳號的 auth worker。
- service binding 是 CF 內部 RPC,不經公網 → 同時繞開**同 zone 522P0#9**與**同帳號 workers.dev 子請求 1042**,比 workers.dev fetch 乾淨(13 邏輯零件正是用 binding 才不踩)。
**技術前提(已驗證 ✅)**
- CF API 查 leo21c(全新自架帳號)的 `arcrun-cypher-executor` settings → **實綁 13 個 service binding**SVC_SET→arcrun-set …)。證明 `deploy.ts` 的 binding 注入在 self-hosted 生效,auth binding 走同路綁得上。
### 修法演進:Aservice binding)→ 廢 → Bglobal_fetch_strictly_public flag
**先做了 Aservice binding)後評估廢棄,改用 Bflag)**——richblack 2026-06-06 拍板。
- **Aservice binding)為何廢**service binding 靜態,加/改要重 deploy cypher(官方 docs 證實)。richblack 判定不夠乾淨,且有更簡單的 B。
- **B`global_fetch_strictly_public` flag)為何對**:官方 docs——此 flag 讓 `fetch()` 走公網「前門」,**same-zone fetch 也能通**。cypher wrangler.toml 加一行即解,**用戶無感、不用域名、不用重 deploy、不動 service binding 禁令**。
- **B 安全(查證官方 docs**:唯一副作用是「Worker fetch 自己 hostname 會 self-loop」;cypher 只打外部 API + sibling auth worker(皆非自己 hostname)→ 不 self-loop。
- **B 官方/self-host 共用**:官方 cypher 本就跨 zone,加 flag 行為不變;self-host 同 zone 被修好。同一份 toml 兩邊通用。
- **評估但廢的他案**arcrun.dev 子域給 self-host cypher——查證後子域同 zonezone=註冊域名)照踩 1042,且跨帳號 route 落地等於 PaaS 轉向,廢。
### Tasks
- [x] 7.1 規範:`02-forbidden §3.1` / `03` / `CLAUDE.md` 鐵律4 / `pre-bash-guard 3.1`——**禁令維持原狀(不解禁 service binding**,僅加註「same-zone 1042 用 flag 解」。(先前一度改成「平台 primitive 例外允許 binding」,因改用 B 已**全部還原**)
- [x] 7.2 ~~hook 解禁~~**還原**`pre-bash-guard.sh` 規則 3.1 回原本「禁止新增 service binding」(B 不需解禁)
- [x] 7.3 `cypher-executor/wrangler.toml``compatibility_flags``global_fetch_strictly_public`(+ 註解病因/安全)。**移除先前加的 SVC_AUTH_***grep SVC_AUTH=0
- [x] 7.4 `auth-dispatcher.ts` / `types.ts`:**還原**到 A 之前(移除 binding 優先邏輯與 SVC_AUTH_* 型別,回單純 `fetch(workers.dev)`——flag 讓它同 zone 也通);tsc exit 0
- [x] 7.5 `cli/src/lib/deploy.ts`**無需改**——flag 在 cypher wrangler.toml`stripOfficialOnlyBindings()` 不碰 `compatibility_flags`self-hosted 部署自動帶上
- [ ] 7.6 驗收(客觀證據):全新自架帳號 `POST /webhooks/named/{ns}/{wf}/trigger``append_row` 的 auth 注入回 200、真的寫進 Google Sheets(第一個端到端驗 self-hosted auth 鏈的測試);`tsc --noEmit` exit 0
- [x] 7.7 design.md 同步(§修改 auth-dispatcher 補「binding 優先、fetch fallback」+ 架構演化 + 1042 根因):§修改 的 auth-dispatcher 補「binding 優先、fetch fallback」;記架構演化(recipe 取代零件 → primitive 可 binding
---
## Phase 8`{{credential.X}}` 用戶面注入語法(2026-06-10 richblack 確認 changedesign §8
> 根因:壓測 401 = workflow 寫 `{{credential.X}}` 但三條 template 展開路徑都不認此 namespace。
> 修法:auth_static_key 加 `resolve_credentials` actionWASM 解密),graph-executor 偵測+回填(不解密)。
- [x] 8.1 `registry/components/auth_static_key/main.go`input 加 `Names []string`main 在 `service`
必填檢查**之前**分流 `action == "resolve_credentials"` → 走新 `handleResolveCredentials`(不查 recipe、
不要求 service),每個 name `kvGet("{api_key}:cred:{name}")` + `cryptoDecrypt` → 回
`{success, credentials:{name: plaintext}}`;缺 name → success:false + error 指明(不假綠)
- [x] 8.2 `tinygo build -target=wasi -o auth_static_key.wasm main.go` 編譯通過(1.1MB+ copy 到
`.component-builds/auth_static_key/component.wasm`
- [x] 8.3 `cypher-executor/src/graph-executor.ts`node.data interpolate 後、node 執行前,掃描殘留
`{{credential.NAME}}` → 有則 POST `wasmWorkerUrl('auth_static_key')`
`{action:"resolve_credentials", api_key, names}` → 回填 node.data。**不解密、不碰 ENCRYPTION_KEY**rule 02 §2.2
實作放 `auth-dispatcher.ts``resolveCredentialRefs`(遞迴掃描+回填,無 {{credential.}} 則零開銷不打 WASM),graph-executor 只呼叫
- [x] 8.4 `tsc --noEmit` exit 0 + grep 確認 graph-executor / auth-dispatcher 無 crypto.subtle / {{secret. / ENCRYPTION_KEY 業務邏輯
- [x] 8.5 端對端驗收:機制已實證打通(2026-06-13 leo21c 壓測)。實際走 **Notion** 而非 OpenAI 路徑——
node.data `{{credential.notion_token}}` → 注入解密後 token → 真讀到 Notion Recipes 資料
(「蕃茄蘑菇燉雞」+ iCook 連結),同時 401 假綠根治全鏈驗證(host fn error envelope →
零件 parsed["error"] → cypher isFailure()401 回 success:false)。客觀證據:真服務 2xx + 真 body。
8.5 原訂 OpenAI 為驗收服務,實際以 Notion 達成同等證據;機制與服務無關,故視為驗收完成)
- [x] 8.6 design.md / status.md / BACKLOG.md 同步(§8 設計 + 8.1-8.4 done 標記)
---
## Notes
- 方案 B 是唯一方案(方案 A 已廢棄,違反 CLAUDE.md §禁止行為)
- Phase 8 的 `resolve_credentials` 是用戶面入口,與 authenticaterecipe 自動注入)並存不重疊
- Phase 0.6host functions+ 0.7WASM 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
@@ -0,0 +1,25 @@
# CODING AGENTS: READ THIS FIRST
This is a **handoff bundle** from Claude Design (claude.ai/design).
A user mocked up designs in HTML/CSS/JS using an AI design tool, then exported this bundle so a coding agent can implement the designs for real.
## What you should do — IMPORTANT
**Read the chat transcripts first.** There are 1 chat transcript(s) in `arcrun/chats/`. The transcripts show the full back-and-forth between the user and the design assistant — they tell you **what the user actually wants** and **where they landed** after iterating. Don't skip them. The final HTML files are the output, but the chat is where the intent lives.
**Find the primary design file under `arcrun/project/` and read it top to bottom.** The chat transcripts will tell you which file the user was last iterating on. Then **follow its imports**: open every file it pulls in (shared components, CSS, scripts) so you understand how the pieces fit together before you start implementing.
**If anything is ambiguous, ask the user to confirm before you start implementing.** It's much cheaper to clarify scope up front than to build the wrong thing.
## About the design files
The design medium is **HTML/CSS/JS** — these are prototypes, not production code. Your job is to **recreate them pixel-perfectly** in whatever technology makes sense for the target codebase (React, Vue, native, whatever fits). Match the visual output; don't copy the prototype's internal structure unless it happens to fit.
**Don't render these files in a browser or take screenshots unless the user asks you to.** Everything you need — dimensions, colors, layout rules — is spelled out in the source. Read the HTML and CSS directly; a screenshot won't tell you anything they don't.
## Bundle contents
- `arcrun/README.md` — this file
- `arcrun/chats/` — conversation transcripts (read these!)
- `arcrun/project/` — the `arcrun` project files (HTML prototypes, assets, components)
@@ -0,0 +1,56 @@
// App root — screen switcher with persistent route
const { useState, useEffect } = React;
const SCREENS = [
{ id: 'landing', label: 'Landing' },
{ id: 'auth', label: 'Auth' },
{ id: 'dashboard', label: 'Dashboard' },
{ id: 'keys', label: 'API Keys' },
{ id: 'workflow', label: 'Workflow' },
];
// Synonyms from sidebar ids
const aliases = { apps: 'dashboard', workflows: 'dashboard', docs: 'landing', settings: 'keys' };
function App() {
const [screen, setScreen] = useState(() => {
const saved = localStorage.getItem('arcrun:screen');
return saved && SCREENS.some(s => s.id === saved) ? saved : 'landing';
});
useEffect(() => {
localStorage.setItem('arcrun:screen', screen);
window.scrollTo(0, 0);
}, [screen]);
const nav = (id) => {
const resolved = aliases[id] || id;
if (SCREENS.some(s => s.id === resolved)) setScreen(resolved);
};
const Current = {
landing: Landing,
auth: Auth,
dashboard: Dashboard,
keys: ApiKeys,
workflow: WorkflowViewer,
}[screen];
return (
<div className="app">
<Current onNav={nav} />
<div className="proto-switch" role="tablist" aria-label="Screen switcher">
{SCREENS.map(s => (
<button key={s.id}
className={screen === s.id ? 'active' : ''}
onClick={() => nav(s.id)}>
{s.label}
</button>
))}
</div>
</div>
);
}
ReactDOM.createRoot(document.getElementById('root')).render(<App />);
@@ -0,0 +1,92 @@
// Top nav and sidebar
const TopNav = ({ onNav, current }) => {
const [scrolled, setScrolled] = React.useState(false);
React.useEffect(() => {
const onScroll = () => setScrolled(window.scrollY > 8);
window.addEventListener('scroll', onScroll);
return () => window.removeEventListener('scroll', onScroll);
}, []);
return (
<nav className={`topnav ${scrolled ? 'scrolled' : ''}`}>
<div className="flex gap-12" style={{alignItems: 'center'}}>
<Logo onClick={() => onNav('landing')} />
<div className="nav-links" style={{marginLeft: 20}}>
<a>Product</a>
<a>Docs</a>
<a>Pricing</a>
<a>Changelog</a>
</div>
</div>
<div className="flex gap-8" style={{alignItems: 'center'}}>
<button className="btn btn-ghost" onClick={() => onNav('auth')}>Log in</button>
<button className="btn btn-primary" onClick={() => onNav('auth')}>
Get started <Icon name="arrow_right" size={14} />
</button>
</div>
</nav>
);
};
const Footer = ({ onNav }) => (
<footer className="footer">
<div className="flex gap-12" style={{alignItems: 'center'}}>
<Logo size="sm" />
<span>© 2026 Arcrun Labs</span>
</div>
<div className="footer-links">
<a>Docs</a>
<a>Pricing</a>
<a>Changelog</a>
<a>Status</a>
<a>Privacy</a>
</div>
</footer>
);
// App shell with sidebar for logged-in screens
const Sidebar = ({ current, onNav }) => {
const items = [
{ id: 'dashboard', label: 'Dashboard', icon: 'home' },
{ id: 'apps', label: 'Apps', icon: 'grid', count: 6 },
{ id: 'workflows', label: 'Workflows', icon: 'workflow', count: 12 },
{ id: 'keys', label: 'API Keys', icon: 'key' },
{ id: 'docs', label: 'Docs', icon: 'book' },
];
const bottom = [
{ id: 'settings', label: 'Settings', icon: 'settings' },
];
return (
<aside className="sidebar">
<div className="sidebar-head">
<Logo size="md" onClick={() => onNav('landing')} />
</div>
<div className="sidebar-section">Workspace</div>
{items.map(it => (
<div key={it.id}
className={`sidebar-item ${current === it.id ? 'active' : ''}`}
onClick={() => onNav(it.id)}>
<span className="sb-ico"><Icon name={it.icon} size={15} /></span>
<span>{it.label}</span>
{it.count != null && <span className="sb-count">{it.count}</span>}
</div>
))}
<div style={{flex: 1}} />
{bottom.map(it => (
<div key={it.id} className="sidebar-item" onClick={() => onNav(it.id)}>
<span className="sb-ico"><Icon name={it.icon} size={15} /></span>
<span>{it.label}</span>
</div>
))}
<div className="sidebar-foot">
<div className="avatar-circ">MR</div>
<div className="meta">
<div className="name">Maya Rivera</div>
<div className="email">maya@northwind.co</div>
</div>
</div>
</aside>
);
};
Object.assign(window, { TopNav, Footer, Sidebar });
@@ -0,0 +1,86 @@
// Shared primitives: icons, logo, etc.
const Icon = ({ name, size = 16, stroke = 1.7 }) => {
const paths = {
arrow_right: <path d="M5 12h14M13 6l6 6-6 6" />,
arrow_left: <path d="M19 12H5M11 6l-6 6 6 6" />,
plus: <path d="M12 5v14M5 12h14" />,
copy: <><rect x="9" y="9" width="13" height="13" rx="2" /><path d="M5 15V5a2 2 0 0 1 2-2h10" /></>,
check: <path d="M20 6L9 17l-5-5" />,
close: <path d="M18 6L6 18M6 6l12 12" />,
eye: <><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z" /><circle cx="12" cy="12" r="3" /></>,
search: <><circle cx="11" cy="11" r="7" /><path d="M21 21l-4.35-4.35" /></>,
warn: <><path d="M10.3 3.86L1.82 18a2 2 0 001.72 3h16.92a2 2 0 001.72-3L13.7 3.86a2 2 0 00-3.4 0z" /><line x1="12" y1="9" x2="12" y2="13" /><circle cx="12" cy="17" r="0.5" fill="currentColor" /></>,
home: <><path d="M3 10l9-7 9 7v10a2 2 0 01-2 2h-4a2 2 0 01-2-2v-5h-2v5a2 2 0 01-2 2H5a2 2 0 01-2-2V10z" /></>,
grid: <><rect x="3" y="3" width="7" height="7" rx="1" /><rect x="14" y="3" width="7" height="7" rx="1" /><rect x="3" y="14" width="7" height="7" rx="1" /><rect x="14" y="14" width="7" height="7" rx="1" /></>,
workflow: <><circle cx="5" cy="6" r="2" /><circle cx="19" cy="12" r="2" /><circle cx="5" cy="18" r="2" /><path d="M7 6h4a4 4 0 014 4v0m0 4a4 4 0 01-4 4H7" /></>,
key: <><circle cx="7.5" cy="15.5" r="4.5" /><path d="M10.68 12.32L21 2M17 6l3 3M15 8l3 3" /></>,
book: <><path d="M2 3h6a4 4 0 014 4v14a3 3 0 00-3-3H2zM22 3h-6a4 4 0 00-4 4v14a3 3 0 013-3h7z" /></>,
settings: <><circle cx="12" cy="12" r="3" /><path d="M19.4 15a1.65 1.65 0 00.33 1.82l.06.06a2 2 0 01-2.83 2.83l-.06-.06a1.65 1.65 0 00-1.82-.33 1.65 1.65 0 00-1 1.51V21a2 2 0 01-4 0v-.09A1.65 1.65 0 009 19.4a1.65 1.65 0 00-1.82.33l-.06.06a2 2 0 01-2.83-2.83l.06-.06a1.65 1.65 0 00.33-1.82 1.65 1.65 0 00-1.51-1H3a2 2 0 010-4h.09A1.65 1.65 0 004.6 9a1.65 1.65 0 00-.33-1.82l-.06-.06a2 2 0 012.83-2.83l.06.06A1.65 1.65 0 009 4.6a1.65 1.65 0 001-1.51V3a2 2 0 014 0v.09A1.65 1.65 0 0015 4.6a1.65 1.65 0 001.82-.33l.06-.06a2 2 0 012.83 2.83l-.06.06A1.65 1.65 0 0019.4 9v0a1.65 1.65 0 001.51 1H21a2 2 0 010 4h-.09a1.65 1.65 0 00-1.51 1z" /></>,
chevron_right: <path d="M9 6l6 6-6 6" />,
chevron_down: <path d="M6 9l6 6 6-6" />,
external: <><path d="M18 13v6a2 2 0 01-2 2H5a2 2 0 01-2-2V8a2 2 0 012-2h6" /><path d="M15 3h6v6M10 14L21 3" /></>,
trash: <><polyline points="3 6 5 6 21 6" /><path d="M19 6l-1 14a2 2 0 01-2 2H8a2 2 0 01-2-2L5 6M10 11v6M14 11v6" /></>,
spark: <path d="M12 3l2.5 6.5L21 12l-6.5 2.5L12 21l-2.5-6.5L3 12l6.5-2.5L12 3z" />,
bolt: <path d="M13 2L3 14h9l-1 8 10-12h-9l1-8z" />,
github: <path d="M12 2C6.48 2 2 6.48 2 12c0 4.42 2.87 8.17 6.84 9.5.5.08.66-.22.66-.48v-1.7c-2.78.6-3.36-1.34-3.36-1.34-.46-1.15-1.12-1.46-1.12-1.46-.92-.62.07-.6.07-.6 1.01.07 1.55 1.04 1.55 1.04.9 1.54 2.36 1.1 2.94.84.09-.65.35-1.1.64-1.35-2.22-.25-4.55-1.11-4.55-4.94 0-1.09.39-1.98 1.03-2.68-.1-.25-.45-1.27.1-2.65 0 0 .84-.27 2.75 1.02A9.5 9.5 0 0112 6.8c.85 0 1.7.11 2.5.33 1.9-1.3 2.75-1.02 2.75-1.02.55 1.38.2 2.4.1 2.65.64.7 1.03 1.6 1.03 2.68 0 3.84-2.34 4.69-4.57 4.93.36.31.68.92.68 1.85V21c0 .27.16.57.67.48A10 10 0 0022 12c0-5.52-4.48-10-10-10z" fill="currentColor" stroke="none" />,
google: <><path d="M21.35 11.1h-9.17v2.73h5.24c-.23 1.41-1.69 4.13-5.24 4.13-3.15 0-5.73-2.62-5.73-5.86 0-3.24 2.58-5.86 5.73-5.86 1.8 0 3 .77 3.69 1.43l2.5-2.4C16.95 3.74 14.8 2.8 12.18 2.8c-5.26 0-9.53 4.25-9.53 9.5s4.27 9.5 9.53 9.5c5.51 0 9.15-3.87 9.15-9.32 0-.63-.07-1.1-.15-1.38z" fill="currentColor" stroke="none" /></>,
share: <><circle cx="18" cy="5" r="3" /><circle cx="6" cy="12" r="3" /><circle cx="18" cy="19" r="3" /><path d="M8.59 13.51l6.83 3.98M15.41 6.51l-6.82 3.98" /></>,
download: <><path d="M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4" /><polyline points="7 10 12 15 17 10" /><line x1="12" y1="15" x2="12" y2="3" /></>,
zoom_in: <><circle cx="11" cy="11" r="7" /><line x1="21" y1="21" x2="16.65" y2="16.65" /><line x1="11" y1="8" x2="11" y2="14" /><line x1="8" y1="11" x2="14" y2="11" /></>,
zoom_out: <><circle cx="11" cy="11" r="7" /><line x1="21" y1="21" x2="16.65" y2="16.65" /><line x1="8" y1="11" x2="14" y2="11" /></>,
maximize: <><path d="M8 3H5a2 2 0 00-2 2v3M21 8V5a2 2 0 00-2-2h-3M3 16v3a2 2 0 002 2h3M16 21h3a2 2 0 002-2v-3" /></>,
slack: <><rect x="13" y="2" width="3" height="8" rx="1.5" /><rect x="2" y="13" width="8" height="3" rx="1.5" /><rect x="14" y="14" width="8" height="3" rx="1.5" /><rect x="8" y="8" width="3" height="8" rx="1.5" /></>,
database: <><ellipse cx="12" cy="5" rx="9" ry="3" /><path d="M3 5v7c0 1.66 4.03 3 9 3s9-1.34 9-3V5M3 12v7c0 1.66 4.03 3 9 3s9-1.34 9-3v-7" /></>,
mail: <><rect x="2" y="4" width="20" height="16" rx="2" /><path d="M2 6l10 7 10-7" /></>,
filter: <path d="M3 4h18l-7 9v6l-4-2v-4L3 4z" />,
star: <polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2" />,
linear: <><rect x="3" y="3" width="18" height="18" rx="4" /><path d="M7 11l5 5M7 15l3 3M7 7l10 10M11 7l6 6M15 7l2 2" /></>,
clock: <><circle cx="12" cy="12" r="9" /><polyline points="12 7 12 12 16 14" /></>,
send: <><path d="M22 2L11 13M22 2l-7 20-4-9-9-4 20-7z" /></>,
terminal: <><path d="M4 17l6-6-6-6M12 19h8" /></>,
logout: <><path d="M9 21H5a2 2 0 01-2-2V5a2 2 0 012-2h4M16 17l5-5-5-5M21 12H9" /></>,
};
return (
<svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={stroke} strokeLinecap="round" strokeLinejoin="round" style={{display: 'block', flexShrink: 0}}>
{paths[name]}
</svg>
);
};
// Arcrun wordmark — custom "arc" glyph made of an arc stroke + ascending dot/node
const Logo = ({ size = 'md', onClick }) => {
const dims = size === 'sm' ? { w: 18, h: 18, f: 10 } : size === 'lg' ? { w: 28, h: 28, f: 14 } : { w: 22, h: 22, f: 12 };
return (
<div className="logo" onClick={onClick}>
<span className="logo-mark" style={{width: dims.w, height: dims.h}}>
<svg width={dims.w} height={dims.h} viewBox="0 0 24 24" fill="none">
<path d="M5 17 Q 12 4, 19 17" stroke="white" strokeWidth="2.4" strokeLinecap="round" fill="none" opacity="0.95" />
<circle cx="19" cy="17" r="2.2" fill="white" />
</svg>
</span>
<span>Arcrun</span>
</div>
);
};
// App icon with gradient background
const AppIcon = ({ tone = 'indigo', children, size = 38 }) => {
const tones = {
indigo: 'linear-gradient(135deg, #6366F1, #8B5CF6)',
orange: 'linear-gradient(135deg, #F59E0B, #EF4444)',
green: 'linear-gradient(135deg, #10B981, #22C55E)',
pink: 'linear-gradient(135deg, #EC4899, #8B5CF6)',
blue: 'linear-gradient(135deg, #3B82F6, #06B6D4)',
slate: 'linear-gradient(135deg, #475569, #334155)',
amber: 'linear-gradient(135deg, #F59E0B, #D97706)',
};
return (
<div className="app-icon" style={{ background: tones[tone], width: size, height: size, color: 'white' }}>
{children}
</div>
);
};
Object.assign(window, { Icon, Logo, AppIcon });
@@ -0,0 +1,95 @@
# Chat
_Started 2026-04-23 04:37 UTC_
---
## User
## Assistant
I'll design Arcrun as a cohesive 5-screen system. Let me plan and build this as an interactive prototype with all screens navigable.
_[tool: update_todos]_
Let me plan the architecture. I'll build a single HTML file with React, using multiple JSX component files, with screen navigation via state.
Design system note: I'll use Inter, electric indigo #6366F1 primary, #0F0F0F bg, #1A1A1A cards, with subtle indigo→violet gradients on CTAs. Dark, minimal, developer-focused. "Arcrun" wordmark will be original — a stylized "↯" or arc mark paired with the name.
Let me start building.
_[tool: write_file]_
_[tool: write_file]_
Now the primitives and chrome components:
_[tool: write_file]_
_[tool: write_file]_
_[tool: snip]_
Now the 5 screens. Let me write them concisely.
_[tool: write_file]_
_[tool: write_file]_
_[tool: snip]_
_[tool: write_file]_
_[tool: write_file]_
_[tool: snip]_
_[tool: write_file]_
_[tool: write_file]_
_[tool: snip]_
_[tool: update_todos]_
_[tool: register_assets]_
_[tool: done]_
_[tool: snip]_
_[tool: list_files]_
The file exists. The error may have been transient. Let me verify by checking the page loads.
_[tool: show_html]_
_[tool: snip]_
_[tool: done]_
The CSS exists but isn't loading through the preview path. Let me inline it to eliminate the dependency.
_[tool: read_file]_
_[tool: run_script]_
_[tool: snip]_
_[tool: done]_
_[tool: fork_verifier_agent]_
Built **Arcrun** as a cohesive 5-screen dark-mode prototype — Landing, Auth, API Keys, Dashboard, and Workflow Viewer — all navigable via the floating screen switcher at the bottom.
**Highlights**
- **Landing** — hero with grid/glow bg, two-path split (developer terminal w/ npm·pip·bun tabs + Claude-style chat preview showing an Arcrun tool call)
- **Auth** — centered card, Sign in / Sign up tabs, GitHub + Google OAuth, email + password
- **API Keys** — first-time key callout with copy + hide-again warning, full keys table with live status toggles
- **Dashboard** — sidebar shell, 3-col apps grid (+ empty-state card), workflows table below
- **Workflow Viewer** — full-bleed dark canvas, 8 connected nodes with left-to-right bezier arrows, selectable with detail panel (I/O schema, editable triplet config for the AI node), minimap + zoom controls, prominent "Edit in Claude" CTA
Screen state persists in localStorage so reloads land you where you were.
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,128 @@
const ApiKeys = ({ onNav }) => {
const [newKeyCopied, setNewKeyCopied] = React.useState(false);
const [keys, setKeys] = React.useState([
{ id: 'k_dev', name: 'Local Development', prefix: 'ar_dev_', created: 'Mar 12, 2026', lastUsed: '2 min ago', active: true },
{ id: 'k_prod', name: 'Production — Northwind API', prefix: 'ar_live_', created: 'Feb 3, 2026', lastUsed: '12 sec ago', active: true },
{ id: 'k_staging', name: 'Staging — Vercel', prefix: 'ar_test_', created: 'Jan 28, 2026', lastUsed: '4 hours ago', active: true },
{ id: 'k_ci', name: 'CI/CD (GitHub Actions)', prefix: 'ar_live_', created: 'Jan 10, 2026', lastUsed: 'Yesterday', active: false },
{ id: 'k_old', name: 'Legacy — Zapier import', prefix: 'ar_live_', created: 'Nov 4, 2025', lastUsed: '3 weeks ago', active: false, revoked: true },
]);
const newKey = 'ar_live_sk_7x9Qf2vLm8nR4TpW6ZjKc3bEhN1aSyU5oP0dI';
const copyKey = () => {
setNewKeyCopied(true);
setTimeout(() => setNewKeyCopied(false), 1800);
};
const toggleKey = (id) => {
setKeys(keys.map(k => k.id === id ? { ...k, active: !k.active } : k));
};
return (
<div className="shell">
<Sidebar current="keys" onNav={onNav} />
<div className="main">
<div className="main-head">
<div>
<div className="crumb">
<span>Workspace</span>
<span className="sep"><Icon name="chevron_right" size={11} /></span>
<span>Settings</span>
</div>
<h1>API Keys</h1>
<div className="sub">Scoped credentials for calling the Arcrun API from your code and CI.</div>
</div>
<div className="flex gap-8">
<button className="btn btn-secondary"><Icon name="book" size={14} /> API docs</button>
<button className="btn btn-primary"><Icon name="plus" size={14} /> Create new key</button>
</div>
</div>
<div className="main-body" style={{maxWidth: 1080}}>
<div className="new-key-box">
<div className="warn-row">
<span className="warn-icon"><Icon name="warn" size={12} /></span>
<span><strong style={{color: '#FBBF24'}}>Save this key now.</strong> For security, we won't show it again — if you lose it, you'll need to create a new one.</span>
</div>
<h3>Your new API key</h3>
<p className="desc">Key named <strong style={{color: 'var(--text)'}}>"Production — Northwind API"</strong> · created just now · all scopes</p>
<div className="key-display">
<span className="key-val">{newKey}</span>
<button className={`copy-btn ${newKeyCopied ? 'copied' : ''}`} onClick={copyKey}>
<Icon name={newKeyCopied ? 'check' : 'copy'} size={12} />
{newKeyCopied ? 'Copied' : 'Copy'}
</button>
</div>
<div style={{marginTop: 14, display: 'flex', gap: 16, fontSize: 12, color: 'var(--text-mute)', alignItems: 'center'}}>
<span className="flex gap-6" style={{alignItems: 'center'}}><Icon name="check" size={12} /> Full workspace access</span>
<span className="flex gap-6" style={{alignItems: 'center'}}><Icon name="clock" size={12} /> Never expires</span>
<span style={{marginLeft: 'auto'}}><span className="link">Add expiry or restrict scopes </span></span>
</div>
</div>
<div className="section-head">
<div>
<h2>All keys</h2>
<div className="subtle" style={{marginTop: 2}}>{keys.filter(k => !k.revoked).length} active · {keys.filter(k => k.revoked).length} revoked</div>
</div>
<div className="flex gap-8">
<button className="btn btn-secondary btn-sm"><Icon name="filter" size={12} /> Filter</button>
</div>
</div>
<div className="table-wrap">
<table className="table">
<thead>
<tr>
<th style={{width: '32%'}}>Name</th>
<th>Key</th>
<th>Created</th>
<th>Last used</th>
<th>Status</th>
<th style={{width: 60, textAlign: 'right'}}></th>
</tr>
</thead>
<tbody>
{keys.map(k => (
<tr key={k.id}>
<td>
<div style={{fontWeight: 500, fontSize: 13.5}}>{k.name}</div>
</td>
<td className="mono">{k.prefix}{k.id.slice(-4)}</td>
<td className="dim" style={{fontSize: 12.5}}>{k.created}</td>
<td className="dim" style={{fontSize: 12.5}}>{k.lastUsed}</td>
<td>
{k.revoked ? (
<span className="pill revoked"><span className="pdot" /> Revoked</span>
) : (
<div className="flex gap-8" style={{alignItems: 'center'}}>
<span className={`toggle ${k.active ? 'on' : ''}`} onClick={() => toggleKey(k.id)} />
<span className={`pill ${k.active ? 'active' : 'idle'}`}>
<span className="pdot" /> {k.active ? 'Active' : 'Paused'}
</span>
</div>
)}
</td>
<td style={{textAlign: 'right'}}>
{!k.revoked && (
<button className="btn btn-danger-ghost btn-sm"><Icon name="trash" size={12} /></button>
)}
</td>
</tr>
))}
</tbody>
</table>
</div>
<div style={{marginTop: 18, fontSize: 12, color: 'var(--text-mute)', display: 'flex', alignItems: 'center', gap: 8}}>
<Icon name="warn" size={12} />
<span>Revoking a key stops all in-flight requests within 60 seconds. This cannot be undone.</span>
</div>
</div>
</div>
</div>
);
};
window.ApiKeys = ApiKeys;
@@ -0,0 +1,90 @@
const Auth = ({ onNav }) => {
const [mode, setMode] = React.useState('signin');
const [email, setEmail] = React.useState('');
const [pw, setPw] = React.useState('');
const [remember, setRemember] = React.useState(true);
const submit = (e) => { e.preventDefault(); onNav('dashboard'); };
return (
<div className="auth-wrap">
<div className="hero-bg" />
<div className="hero-bg-grid" />
<div style={{position: 'absolute', top: 24, left: 24, zIndex: 2}}>
<Logo onClick={() => onNav('landing')} />
</div>
<div className="auth-card">
<h2 className="auth-h1">{mode === 'signin' ? 'Welcome back' : 'Create your account'}</h2>
<p className="auth-sub">{mode === 'signin' ? 'Sign in to your Arcrun workspace.' : 'Start building AI workflows in minutes.'}</p>
<div className="tabs">
<button className={mode === 'signin' ? 'active' : ''} onClick={() => setMode('signin')}>Sign in</button>
<button className={mode === 'signup' ? 'active' : ''} onClick={() => setMode('signup')}>Sign up</button>
</div>
<div className="oauth-row">
<button className="oauth-btn github" onClick={() => onNav('dashboard')}>
<Icon name="github" size={17} stroke={0} /> Continue with GitHub
</button>
<button className="oauth-btn google" onClick={() => onNav('dashboard')}>
<Icon name="google" size={15} stroke={0} /> Continue with Google
</button>
</div>
<div className="divider">or continue with email</div>
<form onSubmit={submit}>
{mode === 'signup' && (
<div className="field">
<label>Full name</label>
<input className="input" type="text" placeholder="Maya Rivera" />
</div>
)}
<div className="field">
<label>Work email</label>
<input className="input" type="email" placeholder="you@company.com" value={email} onChange={e => setEmail(e.target.value)} />
</div>
<div className="field">
<div className="field-row">
<label>Password</label>
{mode === 'signin' && <span className="link">Forgot password?</span>}
</div>
<input className="input" type="password" placeholder="••••••••••" value={pw} onChange={e => setPw(e.target.value)} />
</div>
{mode === 'signin' && (
<div style={{display: 'flex', alignItems: 'center', gap: 8, fontSize: 12.5, color: 'var(--text-dim)', marginBottom: 14}}>
<div onClick={() => setRemember(!remember)}
style={{width: 15, height: 15, borderRadius: 4, border: '1px solid var(--line-2)',
background: remember ? 'var(--primary)' : 'transparent',
display: 'flex', alignItems: 'center', justifyContent: 'center', cursor: 'pointer'}}>
{remember && <Icon name="check" size={11} />}
</div>
<span onClick={() => setRemember(!remember)} style={{cursor: 'pointer'}}>Keep me signed in for 30 days</span>
</div>
)}
<button className="btn btn-primary auth-submit btn-lg" type="submit">
{mode === 'signin' ? 'Sign in' : 'Create account'} <Icon name="arrow_right" size={14} />
</button>
</form>
{mode === 'signup' && (
<p style={{fontSize: 11.5, color: 'var(--text-mute)', textAlign: 'center', marginTop: 14, lineHeight: 1.5}}>
By signing up, you agree to our <span className="link" style={{fontSize: 11.5}}>Terms</span> and <span className="link" style={{fontSize: 11.5}}>Privacy Policy</span>.
</p>
)}
<div className="auth-foot">
{mode === 'signin'
? <>New to Arcrun? <span className="link" onClick={() => setMode('signup')}>Create an account</span></>
: <>Already have an account? <span className="link" onClick={() => setMode('signin')}>Sign in</span></>}
</div>
</div>
</div>
);
};
window.Auth = Auth;
@@ -0,0 +1,126 @@
const Dashboard = ({ onNav }) => {
const apps = [
{ id: 'digest', name: 'Weekly Digest', desc: 'Summarize customer activity into a Monday email for the revenue team.', icon: 'mail', tone: 'indigo' },
{ id: 'triage', name: 'Support Triage', desc: 'Classify inbound tickets, attach context from the CRM, and route.', icon: 'filter', tone: 'orange' },
{ id: 'seo', name: 'SEO Brief Generator', desc: 'Turn a keyword into a draft brief with outline, FAQs, and SERP notes.', icon: 'search', tone: 'green' },
{ id: 'slack', name: 'Standup Bot', desc: 'Collect Linear updates and post a tidy engineering standup to Slack.', icon: 'slack', tone: 'pink' },
{ id: 'doc', name: 'Docs Sync', desc: 'Keep Notion runbooks in sync with the production API surface.', icon: 'book', tone: 'blue' },
];
const workflows = [
{ id: 'digest_weekly', name: 'digest/weekly', nodes: 9, modified: '2 hours ago', runs: '147 runs', status: 'healthy' },
{ id: 'triage_inbound', name: 'triage/inbound-email', nodes: 14, modified: 'Yesterday', runs: '2,318 runs', status: 'healthy' },
{ id: 'seo_brief', name: 'seo/brief-from-keyword', nodes: 7, modified: '3 days ago', runs: '42 runs', status: 'healthy' },
{ id: 'standup', name: 'slack/standup-collector', nodes: 6, modified: '1 week ago', runs: '24 runs', status: 'idle' },
{ id: 'docs_sync', name: 'docs/sync-notion', nodes: 11, modified: '2 weeks ago', runs: '8 runs', status: 'failed' },
];
return (
<div className="shell">
<Sidebar current="dashboard" onNav={onNav} />
<div className="main">
<div className="main-head">
<div>
<div className="crumb">
<span>Northwind</span>
<span className="sep"><Icon name="chevron_right" size={11} /></span>
<span>Dashboard</span>
</div>
<h1>Welcome back, Maya</h1>
<div className="sub">5 apps running · 12 workflows · 2,538 runs this week</div>
</div>
<div className="flex gap-8">
<button className="btn btn-secondary"><Icon name="book" size={14} /> Templates</button>
<button className="btn btn-primary"><Icon name="plus" size={14} /> New app</button>
</div>
</div>
<div className="main-body">
{/* Apps grid */}
<div className="section-head">
<div>
<h2>My Apps</h2>
<div className="subtle" style={{marginTop: 2}}>Packaged workflows your team can run from chat or code</div>
</div>
<span className="subtle">{apps.length} apps</span>
</div>
<div className="apps-grid">
{apps.map(a => (
<div key={a.id} className="app-card">
<AppIcon tone={a.tone}><Icon name={a.icon} size={17} /></AppIcon>
<h4>{a.name}</h4>
<p className="dsc">{a.desc}</p>
<div className="row">
<a className="open" onClick={() => onNav('workflow')}>Open app <Icon name="arrow_right" size={12} /></a>
<button className="chip-btn">
<Icon name="spark" size={11} /> Edit in Claude
</button>
</div>
</div>
))}
<div className="app-card app-empty">
<div className="plus"><Icon name="plus" size={16} /></div>
<div style={{fontSize: 13, fontWeight: 500}}>Create new app</div>
<div style={{fontSize: 12, opacity: 0.75}}>Start from scratch or template</div>
</div>
</div>
{/* Workflows */}
<div className="wf-table">
<div className="section-head">
<div>
<h2>My Workflows</h2>
<div className="subtle" style={{marginTop: 2}}>The graphs that power your apps</div>
</div>
<div className="flex gap-8">
<button className="btn btn-secondary btn-sm"><Icon name="filter" size={12} /> All workflows</button>
<button className="btn btn-secondary btn-sm" onClick={() => onNav('workflow')}><Icon name="plus" size={12} /> New</button>
</div>
</div>
<div className="table-wrap">
<table className="table">
<thead>
<tr>
<th style={{width: '34%'}}>Workflow</th>
<th>Nodes</th>
<th>Last modified</th>
<th>Activity</th>
<th>Status</th>
<th style={{width: 100, textAlign: 'right'}}></th>
</tr>
</thead>
<tbody>
{workflows.map(w => (
<tr key={w.id}>
<td>
<div className="wf-row-name">
<span className="dot" />
<span className="mono" style={{fontSize: 13}}>{w.name}</span>
</div>
</td>
<td className="dim">{w.nodes}</td>
<td className="dim" style={{fontSize: 12.5}}>{w.modified}</td>
<td className="dim" style={{fontSize: 12.5}}>{w.runs}</td>
<td>
<span className={`pill ${w.status === 'healthy' ? 'active' : w.status === 'failed' ? 'revoked' : 'idle'}`}>
<span className="pdot" /> {w.status}
</span>
</td>
<td style={{textAlign: 'right'}}>
<button className="btn btn-secondary btn-sm" onClick={() => onNav('workflow')}>View</button>
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
);
};
window.Dashboard = Dashboard;
@@ -0,0 +1,168 @@
const Landing = ({ onNav }) => {
const [installer, setInstaller] = React.useState('npm');
const installCmds = {
npm: '$ npm install arcrun',
pip: '$ pip install arcrun',
bun: '$ bun add arcrun',
};
return (
<div>
<TopNav onNav={onNav} current="landing" />
<div className="container">
<section className="hero">
<div className="hero-bg" />
<div className="hero-bg-grid" />
<div className="hero-eyebrow">
<span className="dot" />
<span>Now in public beta MCP-native</span>
</div>
<h1>Build AI workflows<br/><span className="grad">without the glue code.</span></h1>
<p className="sub">Connect your tools, automate your work. Orchestrate workflows from Claude.ai, your IDE, or a few lines of code Arcrun handles auth, retries, and state.</p>
<div className="hero-ctas">
<button className="btn btn-primary btn-lg" onClick={() => onNav('auth')}>
Start free <Icon name="arrow_right" size={15} />
</button>
<button className="btn btn-secondary btn-lg">
<Icon name="book" size={14} /> Read the docs
</button>
</div>
</section>
<section className="paths">
{/* Developer path */}
<div className="path-card">
<div className="path-label">
<Icon name="terminal" size={13} /> For Developers
</div>
<h3>Three lines, any runtime.</h3>
<p className="lede">Install once, call Arcrun from Node, Python, or your edge runtime. OAuth, rate limits, and retries are handled.</p>
<div className="install-tabs">
{Object.keys(installCmds).map(k => (
<button key={k} className={installer === k ? 'active' : ''} onClick={() => setInstaller(k)}>{k}</button>
))}
</div>
<div className="terminal" style={{marginBottom: 12}}>
<div className="terminal-head">
<div className="dots"><span/><span/><span/></div>
<div className="title">terminal</div>
</div>
<div className="terminal-body">
<div><span className="dim">{installCmds[installer]}</span></div>
</div>
</div>
<div className="terminal">
<div className="terminal-head">
<div className="dots"><span/><span/><span/></div>
<div className="title">{installer === 'pip' ? 'app.py' : 'app.ts'}</div>
</div>
<div className="terminal-body">
{installer === 'pip' ? (
<>
<div><span className="c1">from</span> <span className="c2">arcrun</span> <span className="c1">import</span> <span className="c2">Arcrun</span></div>
<div className="sp-4"/>
<div><span className="c2">client</span> = <span className="c4">Arcrun</span>(<span className="c2">token</span>=<span className="c2">os</span>.<span className="c4">getenv</span>(<span className="c3">"ARCRUN_KEY"</span>))</div>
<div><span className="c2">run</span> = <span className="c2">client</span>.<span className="c4">run</span>(<span className="c3">"digest/weekly"</span>, <span className="c2">inputs</span>={'{'}<span className="c3">"user"</span>: <span className="c3">"u_219"</span>{'}'})</div>
</>
) : (
<>
<div><span className="c1">import</span> {'{'} <span className="c2">Arcrun</span> {'}'} <span className="c1">from</span> <span className="c3">"arcrun"</span>;</div>
<div className="sp-4"/>
<div><span className="c1">const</span> <span className="c2">client</span> = <span className="c1">new</span> <span className="c4">Arcrun</span>({'{'} <span className="c2">token</span>: <span className="c2">process</span>.<span className="c2">env</span>.<span className="c2">ARCRUN_KEY</span> {'}'});</div>
<div><span className="c1">const</span> <span className="c2">run</span> = <span className="c1">await</span> <span className="c2">client</span>.<span className="c4">run</span>(<span className="c3">"digest/weekly"</span>, {'{'} <span className="c2">user</span>: <span className="c3">"u_219"</span> {'}'});</div>
</>
)}
</div>
</div>
<div className="sp-16" />
<div className="flex gap-12" style={{fontSize: 12.5, color: 'var(--text-mute)'}}>
<span className="flex gap-6" style={{alignItems: 'center'}}><Icon name="check" size={12} /> Typed SDKs</span>
<span className="flex gap-6" style={{alignItems: 'center'}}><Icon name="check" size={12} /> Idempotent runs</span>
<span className="flex gap-6" style={{alignItems: 'center'}}><Icon name="check" size={12} /> Self-host ready</span>
</div>
</div>
{/* Everyone path */}
<div className="path-card">
<div className="path-label">
<Icon name="spark" size={13} /> For Everyone
</div>
<h3>Talk to your workflows.</h3>
<p className="lede">Install Arcrun inside your AI assistant and run your apps by asking. Trigger workflows, fetch data, or draft messages in plain English.</p>
<div className="chat-preview">
<div className="chat-head">
<span className="brand-dot">AI</span>
<span>Your assistant Arcrun connected</span>
<span style={{marginLeft: 'auto'}} className="pill active"><span className="pdot" />2 apps</span>
</div>
<div className="chat-body">
<div className="chat-msg user">
<div className="avatar">M</div>
<div className="bubble">Send this week's customer digest to the revenue team.</div>
</div>
<div className="chat-msg ai">
<div className="avatar">A</div>
<div className="bubble">
Running <span style={{color: 'var(--primary)', fontWeight: 500}}>digest/weekly</span> for 147 accounts, then posting to #revenue.
<div className="tool-card">
<div className="tool-icon">AR</div>
<div className="tool-meta">
<div className="tool-name">arcrun · digest/weekly</div>
<div className="tool-sub">4 of 5 steps complete · 00:12 elapsed</div>
</div>
<span className="pill active"><span className="pdot" />running</span>
</div>
</div>
</div>
</div>
<div className="chat-input">
<span>Reply to your assistant</span>
<span className="caret" />
</div>
</div>
<div className="sp-16" />
<div className="flex gap-12" style={{fontSize: 12.5, color: 'var(--text-mute)'}}>
<span className="flex gap-6" style={{alignItems: 'center'}}><Icon name="check" size={12} /> One-click connect</span>
<span className="flex gap-6" style={{alignItems: 'center'}}><Icon name="check" size={12} /> Works in your IDE</span>
<span className="flex gap-6" style={{alignItems: 'center'}}><Icon name="check" size={12} /> Audit trail</span>
</div>
</div>
</section>
<section className="strip">
<div className="cell">
<div className="ico"><Icon name="bolt" size={15} /></div>
<h4>Run anywhere</h4>
<p>Node, Python, Deno, Bun, Cloudflare Workers. One API, same semantics.</p>
</div>
<div className="cell">
<div className="ico"><Icon name="workflow" size={15} /></div>
<h4>Composable steps</h4>
<p>Model calls, HTTP, database, branching wire them visually or in code.</p>
</div>
<div className="cell">
<div className="ico"><Icon name="key" size={15} /></div>
<h4>Scoped keys</h4>
<p>Per-workflow API keys with fine-grained scopes and live revocation.</p>
</div>
<div className="cell">
<div className="ico"><Icon name="eye" size={15} /></div>
<h4>Observable</h4>
<p>Every run is replayable. Inspect inputs, outputs, and token usage.</p>
</div>
</section>
</div>
<Footer onNav={onNav} />
</div>
);
};
window.Landing = Landing;
@@ -0,0 +1,255 @@
const WorkflowViewer = ({ onNav }) => {
const nodes = [
{ id: 'trigger', x: 60, y: 260, title: 'Weekly Schedule', type: 'trigger', badge: 'CRON', icon: 'clock', tone: '#22C55E',
inputs: [], outputs: [{k: 'timestamp', t: 'ISO8601'}, {k: 'runId', t: 'string'}] },
{ id: 'fetch', x: 320, y: 140, title: 'Fetch Accounts', type: 'database.query', badge: 'DB', icon: 'database', tone: '#3B82F6',
inputs: [{k: 'segment', t: 'string'}], outputs: [{k: 'accounts', t: 'Account[]'}, {k: 'count', t: 'number'}] },
{ id: 'events', x: 320, y: 380, title: 'Pull Events', type: 'segment.events', badge: 'API', icon: 'bolt', tone: '#F59E0B',
inputs: [{k: 'since', t: 'ISO8601'}], outputs: [{k: 'events', t: 'Event[]'}] },
{ id: 'summarize', x: 600, y: 260, title: 'Summarize with Claude', type: 'ai.completion', badge: 'AI', icon: 'spark', tone: '#8B5CF6',
inputs: [{k: 'accounts', t: 'Account[]'}, {k: 'events', t: 'Event[]'}, {k: 'prompt', t: 'string'}],
outputs: [{k: 'digest', t: 'Digest'}, {k: 'tokens', t: 'number'}] },
{ id: 'filter', x: 880, y: 160, title: 'Filter — priority ≥ 2', type: 'logic.filter', badge: 'IF', icon: 'filter', tone: '#64748B',
inputs: [{k: 'digest', t: 'Digest'}], outputs: [{k: 'items', t: 'Item[]'}] },
{ id: 'slack', x: 1140, y: 100, title: 'Post to #revenue', type: 'slack.message', badge: 'OUT', icon: 'slack', tone: '#EC4899',
inputs: [{k: 'channel', t: 'string'}, {k: 'blocks', t: 'Block[]'}], outputs: [{k: 'ts', t: 'string'}] },
{ id: 'mail', x: 1140, y: 260, title: 'Email Digest', type: 'mail.send', badge: 'OUT', icon: 'mail', tone: '#6366F1',
inputs: [{k: 'to', t: 'string[]'}, {k: 'subject', t: 'string'}, {k: 'html', t: 'string'}], outputs: [{k: 'messageId', t: 'string'}] },
{ id: 'log', x: 880, y: 400, title: 'Log run metadata', type: 'arcrun.log', badge: 'LOG', icon: 'terminal', tone: '#475569',
inputs: [{k: 'runId', t: 'string'}, {k: 'stats', t: 'Stats'}], outputs: [] },
];
const edges = [
['trigger', 'fetch'],
['trigger', 'events'],
['fetch', 'summarize'],
['events', 'summarize'],
['summarize', 'filter'],
['summarize', 'log'],
['filter', 'slack'],
['filter', 'mail'],
];
const [selectedId, setSelectedId] = React.useState('summarize');
const [title, setTitle] = React.useState('digest/weekly');
const [zoom, setZoom] = React.useState(100);
const selected = nodes.find(n => n.id === selectedId);
// Edit triplet inline (for the summarize node's prompt config)
const [triplet, setTriplet] = React.useState({
model: 'claude-haiku-4-5',
temperature: '0.3',
prompt: 'Summarize this week\'s account activity for the revenue team.',
});
// Measure node widths for edge endpoint accuracy
const nodeRefs = React.useRef({});
const [sizes, setSizes] = React.useState({});
React.useEffect(() => {
const ns = {};
for (const n of nodes) {
const el = nodeRefs.current[n.id];
if (el) ns[n.id] = { w: el.offsetWidth, h: el.offsetHeight };
}
setSizes(ns);
}, []);
const getPort = (id, side) => {
const n = nodes.find(x => x.id === id);
const sz = sizes[id] || { w: 200, h: 60 };
return {
x: side === 'out' ? n.x + sz.w : n.x,
y: n.y + sz.h / 2,
};
};
return (
<div className="wf-viewer">
<div className="wf-topbar">
<div className="back" onClick={() => onNav('dashboard')} title="Back to dashboard">
<Icon name="arrow_left" size={16} />
</div>
<Logo size="sm" onClick={() => onNav('landing')} />
<div className="sep" />
<div className="wf-breadcrumb">
<span className="cr" onClick={() => onNav('dashboard')}>Workflows</span>
<Icon name="chevron_right" size={11} />
<input
className="wf-title mono"
value={title}
onChange={e => setTitle(e.target.value)}
/>
</div>
<span className="wf-saved">
<span style={{width: 6, height: 6, borderRadius: '50%', background: '#22C55E', boxShadow: '0 0 0 3px rgba(34,197,94,0.18)'}} />
Saved · 2m ago
</span>
<div className="spacer" />
<button className="btn btn-ghost btn-sm"><Icon name="share" size={13} /> Share</button>
<button className="btn btn-secondary btn-sm"><Icon name="download" size={13} /> Export YAML</button>
<button className="wf-edit-in-claude">
<Icon name="spark" size={13} /> Edit in Claude <Icon name="external" size={12} />
</button>
</div>
<div className="wf-canvas">
<svg className="wf-edges" width="100%" height="100%">
<defs>
<marker id="arrow" viewBox="0 0 10 10" refX="8" refY="5" markerWidth="6" markerHeight="6" orient="auto-start-reverse">
<path d="M0,0 L10,5 L0,10 z" fill="#6366F1" />
</marker>
<marker id="arrow-dim" viewBox="0 0 10 10" refX="8" refY="5" markerWidth="6" markerHeight="6" orient="auto-start-reverse">
<path d="M0,0 L10,5 L0,10 z" fill="#3a3a3a" />
</marker>
</defs>
{edges.map(([a, b], i) => {
const p1 = getPort(a, 'out');
const p2 = getPort(b, 'in');
const dx = Math.max(40, (p2.x - p1.x) * 0.5);
const d = `M ${p1.x} ${p1.y} C ${p1.x + dx} ${p1.y}, ${p2.x - dx} ${p2.y}, ${p2.x - 2} ${p2.y}`;
const highlight = a === selectedId || b === selectedId;
return (
<path key={i} d={d}
stroke={highlight ? '#6366F1' : '#3a3a3a'}
strokeWidth={highlight ? 2 : 1.5}
fill="none"
markerEnd={`url(#${highlight ? 'arrow' : 'arrow-dim'})`}
opacity={highlight ? 0.95 : 0.6} />
);
})}
</svg>
<div className="wf-nodes">
{nodes.map(n => (
<div key={n.id}
ref={el => (nodeRefs.current[n.id] = el)}
className={`wf-node ${selectedId === n.id ? 'selected' : ''}`}
style={{left: n.x, top: n.y}}
onClick={() => setSelectedId(n.id)}>
{n.inputs.length > 0 && <span className="port in" />}
{n.outputs.length > 0 && <span className="port out" />}
<div className="node-row-top">
<span className="node-icon" style={{background: n.tone}}>
<Icon name={n.icon} size={12} />
</span>
<span className="node-title">{n.title}</span>
<span className="node-badge">{n.badge}</span>
</div>
<div className="node-sub">{n.type}</div>
</div>
))}
</div>
{/* Detail panel */}
{selected && (
<div className="wf-detail">
<div className="dt-head">
<span className="dt-icon" style={{background: selected.tone}}>
<Icon name={selected.icon} size={15} />
</span>
<div className="dt-meta">
<h3>{selected.title}</h3>
<div className="dt-type">{selected.type}</div>
</div>
<button className="close-btn" onClick={() => setSelectedId(null)}>
<Icon name="close" size={14} />
</button>
</div>
<div className="dt-body">
<div className="dt-section">
<h4>Input schema</h4>
{selected.inputs.length === 0 ? (
<div style={{fontSize: 12, color: 'var(--text-mute)', fontStyle: 'italic'}}>No inputs this is a trigger.</div>
) : selected.inputs.map(f => (
<div key={f.k} className="schema-field">
<span className="k">{f.k}</span>
<span className="t">{f.t}</span>
</div>
))}
</div>
<div className="dt-section">
<h4>Output schema</h4>
{selected.outputs.length === 0 ? (
<div style={{fontSize: 12, color: 'var(--text-mute)', fontStyle: 'italic'}}>No outputs terminal node.</div>
) : selected.outputs.map(f => (
<div key={f.k} className="schema-field">
<span className="k">{f.k}</span>
<span className="t">{f.t}</span>
</div>
))}
</div>
{selected.id === 'summarize' && (
<div className="dt-section">
<h4>Configuration</h4>
<div className="triplet">
<div className="trow">
<div className="tkey">model</div>
<input className="tval" value={triplet.model} onChange={e => setTriplet({...triplet, model: e.target.value})} />
</div>
<div className="trow">
<div className="tkey">temp</div>
<input className="tval" value={triplet.temperature} onChange={e => setTriplet({...triplet, temperature: e.target.value})} />
</div>
<div className="trow">
<div className="tkey">prompt</div>
<input className="tval" value={triplet.prompt} onChange={e => setTriplet({...triplet, prompt: e.target.value})} />
</div>
</div>
</div>
)}
<div className="dt-section">
<h4>Last run</h4>
<div style={{display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 8, fontSize: 12}}>
<div style={{background: 'rgba(255,255,255,0.02)', border: '1px solid var(--line)', borderRadius: 7, padding: '8px 10px'}}>
<div style={{color: 'var(--text-mute)', fontSize: 10.5, textTransform: 'uppercase', letterSpacing: '0.06em'}}>Duration</div>
<div style={{fontFamily: 'JetBrains Mono, monospace', marginTop: 3}}>2.4s</div>
</div>
<div style={{background: 'rgba(255,255,255,0.02)', border: '1px solid var(--line)', borderRadius: 7, padding: '8px 10px'}}>
<div style={{color: 'var(--text-mute)', fontSize: 10.5, textTransform: 'uppercase', letterSpacing: '0.06em'}}>Status</div>
<div style={{marginTop: 2}}><span className="pill active"><span className="pdot" />success</span></div>
</div>
</div>
</div>
<button className="btn btn-primary" style={{width: '100%', marginTop: 4}}>
<Icon name="spark" size={13} /> Edit this node in Claude
</button>
</div>
</div>
)}
{/* Minimap */}
<div className="wf-minimap">
<div className="mini-label">Overview</div>
{nodes.map(n => {
const sz = sizes[n.id] || {w: 180, h: 60};
return (
<div key={n.id} className="mini-box" style={{
left: 8 + (n.x / 1400) * 164,
top: 18 + (n.y / 500) * 80,
width: Math.max(6, (sz.w / 1400) * 164),
height: Math.max(4, (sz.h / 500) * 80),
opacity: selectedId === n.id ? 1 : 0.5,
background: selectedId === n.id ? 'var(--primary)' : 'var(--primary-soft)',
}} />
);
})}
</div>
{/* Zoom controls */}
<div className="wf-controls">
<button onClick={() => setZoom(Math.max(40, zoom - 10))}><Icon name="zoom_out" size={13} /></button>
<div className="zoom-val">{zoom}%</div>
<button onClick={() => setZoom(Math.min(200, zoom + 10))}><Icon name="zoom_in" size={13} /></button>
<button><Icon name="maximize" size={13} /></button>
</div>
</div>
</div>
);
};
window.WorkflowViewer = WorkflowViewer;
@@ -0,0 +1,300 @@
# Frontend Redesign — Design
> 讀此檔前請先讀 `requirements.md``design-source/index.html`
> 視覺 spec 的 single source of truth 是 `design-source/`Claude Design 匯出的 HTML/JSX prototype)。
---
## 1. 架構總覽
```
landing/ (Next.js 15 App Router)
├── app/
│ ├── layout.tsx ← 全站 layoutnext/font + design tokens + 全域 CSS 匯入
│ ├── globals.css ← 匯入 design-tokens.cssTailwind @import
│ ├── design-tokens.css ← 新增:從 design-source 抽出的 CSS variables:root {...}
│ ├── page.tsx ← LandingRSC
│ ├── auth/
│ │ └── page.tsx ← Auth"use client"
│ ├── dashboard/
│ │ └── page.tsx ← Dashboard"use client",仍靠 middleware 保護)
│ ├── keys/
│ │ └── page.tsx ← API Keys"use client"
│ ├── workflows/
│ │ ├── page.tsx ← Workflows 清單(redirect 到 dashboard 的 table,本身極簡)
│ │ └── [name]/page.tsx ← Workflow Viewer"use client"
│ ├── integrations/page.tsx ← 保留現有
│ ├── api-docs/page.tsx ← 保留現有
│ └── login/page.tsx ← 保留現有(redirect /auth 同義;見 §9 遷移策略)
├── components/
│ ├── shell/
│ │ ├── Logo.tsx
│ │ ├── Icon.tsx
│ │ ├── TopNav.tsx
│ │ ├── Footer.tsx
│ │ └── Sidebar.tsx
│ ├── primitives/
│ │ ├── Button.tsx ← btn / btn-primary / btn-secondary / btn-ghost 對應 class
│ │ ├── Pill.tsx
│ │ ├── Toggle.tsx
│ │ ├── Terminal.tsx ← landing hero 右卡用
│ │ └── ChatPreview.tsx ← landing hero 右卡用
│ └── workflow/
│ ├── Canvas.tsx ← wf-viewer 本體(節點 + SVG edges
│ ├── NodeCard.tsx
│ ├── DetailPanel.tsx
│ ├── Minimap.tsx
│ └── ZoomControls.tsx
├── lib/
│ ├── api.ts ← typed fetch wrapperfetch ${API_BASE}${path}, credentials: 'include'
│ ├── workflows.ts ← listWorkflows / getWorkflow / getWorkflowYaml
│ ├── apiKeys.ts ← listKeys / createKey / patchKey / deleteKey
│ └── me.ts ← 已存在邏輯,集中到此
├── middleware.ts ← 擴展 matcher(加 /keys, /workflows/*
└── ...(既有 package.json / wrangler.toml 不變)
```
**路由對照設計稿的 5 screen**
| Screen | Route |
|---|---|
| Landing | `/` |
| Auth | `/auth`(新增;`/login` 保留並內部 `redirect('/auth')` |
| Dashboard | `/dashboard` |
| API Keys | `/keys` |
| Workflow Viewer | `/workflows/[name]` |
---
## 2. Design tokens 對應
設計稿所有 CSS 變數抄進 `app/design-tokens.css`**不解析、不改名**
```css
:root {
--bg: #0F0F0F;
--bg-1: #141414;
--card: #1A1A1A;
--card-2: #222222;
--line: #262626;
--line-2: #303030;
--text: #EDEDED;
--text-dim: #A0A0A0;
--text-mute: #6B6B6B;
--primary: #6366F1;
--primary-2: #8B5CF6;
--primary-soft: rgba(99, 102, 241, 0.12);
--primary-ring: rgba(99, 102, 241, 0.32);
--success: #22C55E;
--warn: #F59E0B;
--danger: #EF4444;
--gradient: linear-gradient(135deg, #6366F1 0%, #8B5CF6 100%);
--gradient-soft: linear-gradient(135deg, rgba(99,102,241,0.16) 0%, rgba(139,92,246,0.16) 100%);
}
```
並在 Tailwind v4 的 `@theme inline` block 內對應出:
```css
@theme inline {
--color-bg: var(--bg);
--color-card: var(--card);
--color-card-2: var(--card-2);
--color-line: var(--line);
--color-line-2: var(--line-2);
--color-text: var(--text);
--color-text-dim: var(--text-dim);
--color-text-mute: var(--text-mute);
--color-primary: var(--primary);
--color-primary-2: var(--primary-2);
}
```
這樣 JSX 裡可用 `bg-bg / text-text-dim / border-line`,又保留 CSS 變數語義。
**現有的 `--background: #0a0a0a` 要換成 `#0F0F0F`**(視覺 breaking change;受影響:所有沿用 `bg-[#0a0a0a]` 的 inline 值)。
---
## 3. 字型
```tsx
// app/layout.tsx
import { Inter, JetBrains_Mono } from 'next/font/google';
const inter = Inter({
subsets: ['latin'],
variable: '--font-inter',
weight: ['300', '400', '500', '600', '700', '800'],
});
const mono = JetBrains_Mono({
subsets: ['latin'],
variable: '--font-mono',
weight: ['400', '500', '600'],
});
// body class = `${inter.variable} ${mono.variable}`
```
`globals.css` 中的 `body { font-family: var(--font-inter), -apple-system, sans-serif; }``.mono` class 用 `font-family: var(--font-mono)`
**移除**
- `design-source/index.html` 第 7-9 行的 `<link rel="preconnect"> / <link href="fonts.googleapis.com">`(不寫入 production)。
- React / Babel standalone script 標籤(prototype 專用,不進 production)。
---
## 4. 元件 porting 規則
Claude Design 用了 `window.Icon / window.Logo / window.AppIcon / window.TopNav ...` 的 globals 風格 — 那是 prototype 專用。Port 到 Next.js 時:
1. 每個元件拆單檔、具名 export。
2. 用 Tailwind + `className` 模板字串;共用 variant(如 btn)用 `cva`-style helper 即可(自己寫 5 行的 `clsx`-alike 函式),**不引入 class-variance-authority / clsx 套件**(避免新依賴)。
3. Icon 的 `paths` 直接搬,但每個 icon 拆成自己的 functional component 或集中在一個 `<Icon name="..." />`(沿用 design source 的 pattern)。
4. SVG arc wordmark 的 logo 直接 port。
---
## 5. 各 screen 實作細節
### 5.1 Landing — `app/page.tsx`
- 結構:`<TopNav />` + `<Hero />` + `<Paths />` + `<Strip />` + `<Footer />`
- Heroheading、eyebrow、CTA、radial grid bg(純 CSS)。
- Paths 左卡(Developer):install tabs (`npm` / `pip` / `bun`) + 兩個 terminal block**code 範例用 dogfooding 範例**`acr` CLI),不留 `Arcrun` SDK 假 API。
- Paths 右卡(Everyone):chat preview 結構保留;assistant 對話中的 tool call 用「arcrun · digest/weekly」不動。
- Strip4 cell。
- `LandingClientTabs` 因為有 tabs state,需標 `"use client"`;外層保持 RSC。
### 5.2 Auth — `app/auth/page.tsx`
- `"use client"`。state`mode: 'signin' | 'signup'`, `email`, `pw`, `remember`
- Submit`fetch(${API_BASE}/auth/password-login, { method: 'POST', credentials: 'include' })`**若 cypher-executor 尚未支援 password auth,先顯示「Password 登入尚未開放,請用 OAuth」警示,不偽造成功流程**)。
- OAuth 按鈕:直接 `<a href={API_BASE}/auth/google/start?redirect=/dashboard>`,和現行 `/login` 同樣機制。
- 下標提示「By signing up, you agree to our Terms ...」保留 static 字串。
- 保留 `/login` 路由向後相容(RSC 裡 `redirect('/auth')`)。
### 5.3 Dashboard — `app/dashboard/page.tsx`
- `"use client"` 或 split(外層 RSC 抓 /me,內層 Client)。
- 由 `<Sidebar current="dashboard" />` + main。
- 主要區塊:
- Main headbreadcrumb「{email 的 domain} Dashboard」、heading「Welcome back, {display_name}」、subtitle 顯示 app/workflow 總數(從 `/apps` + `/workflows` 計算;若 endpoint 404 → 顯示 `—`)。
- Apps Grid`/apps` 的結果渲染;每列永遠有一個 `app-empty` 卡(新建 CTA)。
- Workflows Table`/workflows` 的結果渲染;空時改為全寬「No workflows yet. Run `acr push` to add one.」內嵌指令框。
- 「Open app」「View」按鈕導向 `/workflows/[name]`
- 「Edit in Claude」按鈕本次不做動作,僅保 UIdisabled + tooltip「Coming soon」)。
### 5.4 API Keys — `app/keys/page.tsx`
- `"use client"`
- Fetch `/api-keys`:若回傳為空陣列但 `/me` 有 api_keyfallback 顯示 `/me.api_key` 為唯一一列(單 key 相容模式)。
- 頂部 new-key-box:只在「剛剛建立新 key」的一次性狀態顯示(`useState` + `sessionStorage` flagreload 後消失)。
- 表格、toggle、trash:對應 PATCH / DELETE。
- 「Create new key」按鈕:呼叫 `POST /api-keys`,拿到後打 highlight box。
- Revoke 警告文字維持設計稿「within 60 seconds」。
### 5.5 Workflow Viewer — `app/workflows/[name]/page.tsx`
- `"use client"`param `name` 來自動態路由。
- Mount 後呼叫 `GET /workflows/:name`:後端回傳 `{ name, nodes: Node[], edges: Edge[], yaml, last_run: {...} }`(若 endpoint 未實作 → 顯示「Workflow viewer 尚未啟用」empty state,不用假資料)。
- `<Canvas>` 內:
- SVG 的 `<marker>`, `<path>` 定義抄設計稿。
- Node 用絕對定位(x/y 直接用 API 資料;資料沒有 coord 時做自動 layout — 階段性做簡單 dagre-free 的「column by depth」排版,避免新依賴)。
- 點選節點 → 右側 detail panel 顯示 input/output schema;若 type 含 `ai.*`,顯示 triplet 編輯器(model / temp / prompt)— 編輯本次 **read-only**disabled input + 「Edit via acr CLI」提示)。
- 「Export YAML」按 `GET /workflows/:name/yaml``download` blob。
- 「Edit in Claude」:本次只開新 tab 到 `https://claude.ai/new?q=...`(文案「coming soon」按鈕),避免偽裝已整合。
- Zoom controls、minimap:純 UI`zoom` state 實際不套 transform(或簡單 `style={{ transform: scale(zoom/100) }}` 套在 `.wf-nodes` + svg)。
---
## 6. API wrapper`lib/api.ts`
```ts
export const API_BASE = process.env.NEXT_PUBLIC_API_BASE ?? 'https://cypher.arcrun.dev';
export async function arcrunFetch<T>(path: string, init: RequestInit = {}): Promise<T> {
const res = await fetch(`${API_BASE}${path}`, {
credentials: 'include',
headers: { 'Accept': 'application/json', ...(init.headers ?? {}) },
...init,
});
if (res.status === 401 && typeof window !== 'undefined') {
window.location.href = `/auth?redirect=${encodeURIComponent(location.pathname)}`;
throw new Error('unauthenticated');
}
if (!res.ok) throw new Error(`arcrun ${path}: ${res.status}`);
return res.json() as Promise<T>;
}
```
所有頁面透過這個 wrapper。**禁止在 page.tsx 裡 hard-code `fetch('https://...')`**(測試可以 grep)。
---
## 7. Middleware
```ts
export const config = {
matcher: ['/dashboard/:path*', '/keys/:path*', '/workflows/:path*'],
};
```
現有邏輯(讀 `arcrun_session` cookie,沒有就 redirect `/login?redirect=...`)保留,`/login` 改為內部 redirect `/auth`
---
## 8. 不做的設計稿功能
| 設計元素 | 取捨 |
|---|---|
| 底部的 `proto-switch`5 個 screen 切換 pill | **刪**。那是 prototype 用的 demo 切換器,不進 production。 |
| Sidebar 的 `count` badge | 先保留;數字從 `/workflows` / `/apps` 的長度派生;無資料時藏起來。 |
| Sidebar bottom 的 avatar + "Maya Rivera / maya@northwind.co" | 換成 `{display_name} / {email}`(真資料)。 |
| Workflow Viewer 的 triplet 可編輯 | 本次 disabled,僅顯示。 |
| 「Edit in Claude」整合 | 按鈕保留,點擊開新 tab 到 claude.ai,不串 MCP/API。 |
| 多 workspace breadcrumb | 固定顯示用戶 email domain 或「Personal」。 |
---
## 9. 既有頁面遷移
| 既有 | 處理 |
|---|---|
| `/page.tsx` | **rewrite**:沿用設計稿結構,code demo 字串改為 `acr` 實際指令(現有的 `auth.bind(...)` 寫法可保留在 Python tab |
| `/login` | 改為 `redirect('/auth')`Next.js RSC redirect),保留舊連結相容 |
| `/dashboard` | **rewrite**:舊 dashboard 變成 API Keys 獨立頁 + 新 Dashboard 總覽。原本 dashboard 裡的 Key 卡片搬到 `/keys`。 |
| `/api-docs` | 不動 |
| `/integrations` | 不動;在 Dashboard Apps Grid 旁提供 link |
---
## 10. 開發順序(高度相依)
`tasks.md`。總則:
1. 先做 design tokens + shellLogo / Icon / Button / Sidebar / TopNav / Footer — 其他頁面都吃這些。
2. 然後 Landing(可直接驗證視覺基準)。
3. 然後 Auth(獨立)。
4. 然後 API Keys(後端依賴少)。
5. 然後 Dashboard(依賴 `/workflows` + `/apps`,若未實作先 empty state)。
6. 最後 Workflow Viewer(依賴最重,多 endpoint)。
---
## 11. 風險與未解
| 風險 | 緩解 |
|---|---|
| cypher-executor 尚未有 `/workflows`, `/apps`, `/api-keys` CRUD | 前端先做,統一走 404 → empty state;另開 task 去 cypher-executor SDD 增補。本次 SDD 不負責後端實作。 |
| Password auth 沒實作 | Auth 頁 email/password form 在 submit 時顯示「OAuth only」提示 |
| `acr push` 未記錄 node 座標 | Canvas 自動排版(by topological depth),不強制 YAML 載入 layout |
| `next-on-pages``"use client"` 大量頁面的 edge runtime 支援 | 本來就用 `next-on-pages`,問題不大;必要時 per-page `export const runtime = 'edge'` |
| 舊 `/dashboard` 的 bookmark 使用者 | 現行 `/dashboard` 的 Key 管理被搬走;保留 Key 區塊 + 顯示提示「New page: /keys」引導 |
---
## 12. 與封測的關係
此 SDD 的實作**不解除封測阻擋**(封測阻擋在 Credential Primitives WASM)。此重設計與 Phase 0.6 / 0.7 / 1-3 是並行軌道。richblack 可決定先後順序,但本 SDD 獨立可 ship。
@@ -0,0 +1,159 @@
# Frontend Redesign — Requirements
> 來源:Claude Design bundle `JAdpACs3cSyw_vN6Ketj1Q`(已歸檔於 `design-source/`)。
> 此 SDD 擴展 `../landing-page.md` 的範圍:landing 從「單頁 + OAuth + Dashboard」升級為「5 screen app shell」。
---
## 1. 背景
`arcrun/landing/` 目前只提供:
- `/` — Landing hero + code demo
- `/login` — Google / GitHub OAuth
- `/dashboard` — API Key 管理
- `/api-docs` — Swagger UI(外部)
- `/integrations` — 20 個 recipe 靜態清單
Claude Design 交付一套完整 5 screen 設計(Landing / Auth / API Keys / Dashboard / Workflow Viewer),與現有前端相比多了:
- **Dashboard 的 Apps Grid 與 Workflows Table**(現在沒有應用/workflow 清單 UI
- **API Keys 獨立頁**(現在和 dashboard 混在一起)
- **Workflow Viewer**node-based canvas,對應 YAML workflow 的視覺化(目前 acr push YAML 後只有 CLI 輸出)
---
## 2. User Stories
### US-1:新訪客認識 arcrun
- 作為沒用過 arcrun 的工程師,我要在 landing 看到兩條路(For Developers / For Everyone),5 秒內判斷這是否符合我要的用法。
- 驗收:hero + 雙 path card + feature strip 在首屏呈現;CTA「Start free」可點擊到 `/auth`
### US-2:取得 / 登入會員
- 作為訪客,我要用 GitHub / Google / Email 登入建立帳號。
- 驗收:`/auth` 支援 Sign in / Sign up tabs、GitHub + Google OAuth、Email + Password 表單。OAuth 成功後導向 `/dashboard`
### US-3:看見應用全貌
- 作為已登入用戶,我要在 Dashboard 看到我已部署的 Apppackaged workflow)和 Workflow 清單。
- 驗收:
- Apps Grid:每個 app 卡片顯示名稱、描述、「Open app」「Edit in Claude」。
- Workflows Table:顯示 workflow 名稱、節點數、最後修改、執行次數、狀態。
- 若 app / workflow 為空,顯示新建 CTA(非硬編 mock)。
### US-4:管理 API Keys
- 作為已登入用戶,我要建立、檢視、停用、刪除 API Key。
- 驗收:
- 剛產生的 Key 顯示在頂部高亮卡片(含警示文字「不會再顯示」)。
- 全部 Keys 在下方表格顯示(名稱、遮蔽後 key、建立時間、最後使用、狀態、toggle、trash)。
- Rotate / Revoke 立即生效(呼叫 cypher-executor)。
### US-5:檢視 Workflow graph
- 作為已登入用戶,我要點 workflow 進到 viewer,看到節點 DAG 與每個節點的 I/O schema 與參數。
- 驗收:
- Canvas 顯示節點(含 icon / type badge),節點以 bezier 曲線相連。
- 選中節點後右側面板顯示 input / output schema、configuration(針對 ai.completion 等節點顯示 triplet 編輯器)。
- 工具列含 Share / Export YAML / Edit in Claude。
- Minimap + zoom 控制顯示可用。
- 「Export YAML」呼叫 cypher-executor 取得該 workflow 的原始 YAML。
### US-6Dogfooding
- 作為 arcrun 核心維護者,我要前端所有與服務打交道的動作都透過 arcrun 自家 APIcypher-executor)完成,不依賴第三方 OAuth / workflow / backend 服務。
- 驗收(見 §6)。
---
## 3. 非功能需求
| 項目 | 規範 |
|---|---|
| 技術棧 | Next.js 15 App Router、React 19、Tailwind v4、TypeScript(沿用 `landing/` 現有堆疊) |
| 部署 | Cloudflare Pages`@cloudflare/next-on-pages`),沿用 `landing/wrangler.toml` |
| 字型 | Inter、JetBrains Mono — 用 `next/font/google`,不拉 unpkg / fonts.googleapis.com `<link>` |
| 依賴 | 僅 Next / React / Tailwind;禁止 tRPC、React Query、SWR、Auth.js/Clerk、ReactFlow/XYFlow、Radix、shadcn CLI install、animation libraryframer-motion)等第三方 |
| 狀態管理 | React 內建(useState / useReducer / Context);持久化用 `localStorage` 或 server session cookie |
| 國際化 | 延後;本次一律英文(與設計稿一致)。現有 `?lang=zh` 不擴展 |
| 無障礙 | 按鈕 `aria-label`、Form 控件有 `<label>`;鍵盤可完成登入 / 複製 key 流程 |
| 效能 | Landing 首屏無阻塞 JSRSC);Dashboard / Workflow Viewer 可為 Client Component |
---
## 4. 視覺基準
| 項目 | 值(source of truth`design-source/index.html` 的 CSS 變數) |
|---|---|
| 主背景 | `#0F0F0F`(現有是 `#0a0a0a` — 本次改為 `#0F0F0F` |
| Card | `#1A1A1A` |
| Line | `#262626` / `#303030` |
| Primary | `#6366F1`indigo`#8B5CF6`violet)漸層 |
| Text | `#EDEDED` / dim `#A0A0A0` / mute `#6B6B6B` |
| 字型 | Inter 300/400/500/600/700/800Mono 400/500/600 |
---
## 5. 範圍界線
### 納入
- 5 screenLanding、Auth、Dashboard、API Keys、Workflow Viewer
- LogoSVG arc wordmark)、Icon setinline SVG,從 primitives.jsx port
- TopNav / Footer / Sidebar 三個 shell 元件
### 不納入(本次 SDD
- **Multi-tenant workspace 切換**(設計稿有 "Northwind" breadcrumb,本次純顯示用戶 email
- **Multi-API-key CRUD 後端**cypher-executor 現只支援每帳號一把 key,多 key table 先以「目前只支援一把」狀態呈現 — 見 §6.2)
- **Workflow 編輯**(只做 read-only viewer;編輯仍走 acr CLI / YAML
- **即時執行狀態 stream**minimap / zoom 僅 UI,不做真實 pan-zoom transform
- **i18n 中英切換**、**Hall of Fame**、**Donate**
- **Swagger UI 頁(/api-docs** — 保留現狀
---
## 6. API 依賴(全部打 `cypher.arcrun.dev`
### 6.1 既有(已實作於 cypher-executor
- `GET /me` — 取得登入用戶 + api_keydriverdashboard 側欄 avatar / API Keys 頁的單把 key
- `PUT /me/api-key/rotate``DELETE /me/api-key` — Rotate / Revoke
- `GET /auth/google/start``GET /auth/github/start` — OAuth 起點
- `POST /auth/logout`
- `POST /webhooks/named/{name}/trigger` — 觸發(給 Landing 文案展示)
### 6.2 需新增的 endpoint(阻擋項;**本 SDD 只定義契約,cypher-executor 實作歸屬於另一個 task**
| Method | Path | 說明 | 用途 |
|---|---|---|---|
| GET | `/workflows` | 列出當前 api_key 名下的 workflow(名稱、nodes、modified、run_count、status | Dashboard / Workflows Table |
| GET | `/workflows/:name` | 取得 workflow 詳細(含 graph 節點 + edges + YAML | Workflow Viewer |
| GET | `/workflows/:name/yaml` | 下載 raw YAML | Workflow Viewer Export |
| GET | `/apps` | 列出 app= workflow 標上 icon/tone/description metadata | Dashboard / Apps Grid |
| GET | `/api-keys` | 列出多把 key(若後端仍是單把,回傳單元素 array) | API Keys 頁 |
| POST | `/api-keys` | 建立新 key | API Keys 頁「Create new key」 |
| PATCH | `/api-keys/:id` | `{ active: boolean }` 切換 | API Keys 頁 toggle |
| DELETE | `/api-keys/:id` | 刪除 | API Keys 頁 trash |
**在後端尚未實作前**:前端用型別化 fetch wrapper 封裝呼叫;遇到 404 顯示 empty state(而非假資料)。本 SDD 明確禁止 hard-code mock fixture。
### 6.3 登入前後可用的公開資訊
- Integrations 清單(20 個 recipe):現有 `/integrations` 頁已有靜態清單,繼續複用。
---
## 7. Dogfooding 紅線
| 禁止 | 用哪個 arcrun 內部替代 |
|---|---|
| Auth0 / Clerk / Supabase Auth | cypher-executor `/auth/*` + session cookie(現行 `arcrun_session` |
| Segment / PostHog | 不加,或用後續 arcrun `analytics` 零件 |
| Vercel KV / Upstash | Cloudflare KV(經 cypher-executor |
| 直接寫第三方 APISlack / Notion 等)作 dashboard demo | 透過 arcrun workflow + trigger 模擬 |
| ReactFlow / XYFlow | 純 SVG 手刻(設計稿本來就是手刻) |
---
## 8. 驗收總清單
- [ ] 5 個 screen 全部在 `/`, `/auth`, `/dashboard`, `/keys`, `/workflows/[id]` 可達。
- [ ] 設計稿的 spacing / color / border-radius 100% 對得上(以 `design-source/index.html` CSS 變數為準)。
- [ ] middleware 保護 `/dashboard`, `/keys`, `/workflows/*`(未登入 → `/auth?redirect=...`)。
- [ ] 只打 `cypher.arcrun.dev`(可透過 `NEXT_PUBLIC_API_BASE` override),grep 結果不含其他外部 API host。
- [ ] `package.json` 新增依賴 = 0(本次不引入新 npm 套件)。
- [ ] 無 mock 資料:若後端未實作,顯示 loading 或 empty state,不編假陣列給 UI。
- [ ] `pnpm build` 通過,`next-on-pages` 輸出無 edge-runtime 錯誤。
@@ -0,0 +1,140 @@
# Frontend Redesign — Tasks
> 進度來源:本檔。完成一項立刻 `[x]`,不批次。
> 本 SDD 建立於 2026-04-23richblack 尚未下令動工,**所有 task 預設 `[ ]`**。
---
## Phase 0 — SDD 建立(本次)
- [x] 取得 Claude Design bundle,歸檔至 `design-source/`
- [x] 撰寫 `requirements.md`
- [x] 撰寫 `design.md`
- [x] 撰寫 `tasks.md`(本檔)
- [ ] richblack review + 認可 → 開 Phase 1
**等 richblack 明確說「開始動工」之前,不觸 `landing/` 任何檔案。**
---
## Phase 1 — Tokens + Shell(無外部 API 依賴)
- [ ] 1.1 建 `landing/app/design-tokens.css`,抄 design-source CSS :root 變數
- [ ] 1.2 更新 `landing/app/globals.css``@import './design-tokens.css'``@theme inline` 對應 token → Tailwind color
- [ ] 1.3 `layout.tsx` 引入 `next/font/google` 的 Inter + JetBrains Monobody bg 改 `var(--bg)`
- [ ] 1.4 建 `landing/components/shell/Icon.tsx`(港設計稿 primitives.jsx 的所有 icon
- [ ] 1.5 建 `landing/components/shell/Logo.tsx`SVG arc wordmark
- [ ] 1.6 建 `landing/components/shell/TopNav.tsx`
- [ ] 1.7 建 `landing/components/shell/Footer.tsx`
- [ ] 1.8 建 `landing/components/shell/Sidebar.tsx`(含頭像、項目清單、登出按鈕)
- [ ] 1.9 建 `landing/components/primitives/Button.tsx`variants: primary / secondary / ghost / danger-ghost / sm / lg
- [ ] 1.10 建 `landing/components/primitives/Pill.tsx``Toggle.tsx`
- [ ] 1.11 Lint + build pass
---
## Phase 2 — Landing`/`
- [ ] 2.1 Rewrite `app/page.tsx`,結構照 design-source/screens/Landing.jsx
- [ ] 2.2 Heroheading / eyebrow / CTA / radial-grid bgCSS only
- [ ] 2.3 Paths 左卡:install tabs 元件(Client Component+ terminal code blocks(字串改 `acr` 實際指令)
- [ ] 2.4 Paths 右卡:chat preview(靜態)
- [ ] 2.5 Feature strip4 cell
- [ ] 2.6 TopNav / Footer 接上
- [ ] 2.7 Responsive(≤ 768px: paths 單欄、hero h1 縮 1 級)
- [ ] 2.8 視覺比對 design-source/index.html(截圖對比 / DOM spec 檢查)
---
## Phase 3 — Auth`/auth``/login` redirect
- [ ] 3.1 建 `app/auth/page.tsx`Client
- [ ] 3.2 Sign in / Sign up tabs + OAuth buttonsGoogle / GitHub)接既有 `/auth/google/start` / `/auth/github/start`
- [ ] 3.3 Email / Password form — submit 顯示「OAuth only」提示(待後端支援)
- [ ] 3.4 `/login` 頁改為 `redirect('/auth')`
- [ ] 3.5 視覺比對 design-source/screens/Auth.jsx
---
## Phase 4 — API Keys`/keys`
- [ ] 4.1 建 `lib/api.ts` fetch wrapper
- [ ] 4.2 建 `lib/apiKeys.ts`listKeys / createKey / patchKey / deleteKey)— 後端未實作時回 `[{ ...from /me }]` 的 fallback
- [ ] 4.3 建 `app/keys/page.tsx`:頂部 new-key-boxsessionStorage flag)、全表格
- [ ] 4.4 「Create new key」呼叫 `POST /api-keys`(後端未實作 → 顯示「coming soon」toast
- [ ] 4.5 Toggle active / trash 接 PATCH / DELETE
- [ ] 4.6 middleware.ts matcher 加 `/keys/:path*`
- [ ] 4.7 原 `/dashboard` 頁的 Key 卡區塊移除,放提示「API Keys 已搬到 /keys」
---
## Phase 5 — Dashboard`/dashboard`
- [ ] 5.1 建 `lib/workflows.ts``lib/apps.ts`
- [ ] 5.2 Rewrite `app/dashboard/page.tsx`(保留 /me session 檢查)
- [ ] 5.3 Main headbreadcrumb / greeting / summary counters
- [ ] 5.4 Apps Grid + empty-state 卡(`/apps` 404 → 只顯示 empty-state
- [ ] 5.5 Workflows Table`/workflows` 404 → 顯示「no workflows — acr push」CTA
- [ ] 5.6 Sidebar 接真實 useremail / display_name)、登出按鈕接 `/auth/logout`
- [ ] 5.7 視覺比對 design-source/screens/Dashboard.jsx
---
## Phase 6 — Workflow Viewer`/workflows/[name]`
- [ ] 6.1 建 `app/workflows/[name]/page.tsx`Client
- [ ] 6.2 Topbarback / logo / breadcrumb / title / saved indicator / share / export / edit-in-claude
- [ ] 6.3 `<Canvas>`SVG defs + 節點定位 + bezier edges;資料從 `GET /workflows/:name`
- [ ] 6.4 Auto-layouttopological depth → columns,同 depth 平均分配 y
- [ ] 6.5 NodeCard 點擊 → DetailPanel
- [ ] 6.6 DetailPanelinput / output schema、tripletdisabled)、last run stats(可選)
- [ ] 6.7 Export YAML`GET /workflows/:name/yaml` → blob download
- [ ] 6.8 Minimap(純顯示)、ZoomControls(簡單 scale
- [ ] 6.9 middleware.ts matcher 加 `/workflows/:path*`
---
## Phase 7 — 清理 + 收尾
- [ ] 7.1 刪除舊 `/dashboard` 不再用的 coderotate/revoke 若全搬到 /keys
- [ ] 7.2 Grep 檢查:除 `cypher.arcrun.dev` 外無任何第三方 API host
- [ ] 7.3 Grep 檢查:無 `mock` / `fixture` / 硬編的假資料陣列(`app-empty` 的字串常數除外)
- [ ] 7.4 Grep 檢查:無新增 npm 依賴(`git diff landing/package.json` 應只改版本,不加條目)
- [ ] 7.5 `pnpm build` 通過;`next-on-pages` 通過
- [ ] 7.6 local `pnpm dev` 手工巡覽 5 screen,每個截圖比對 design-source
- [ ] 7.7 更新 `.agents/specs/arcrun/arcrun.md`,加一段「CLI 1.2.0 搭配新 landing」之類的進度註記
- [ ] 7.8 richblack 認可 → 合併 / deploy
---
## 需要 cypher-executor 搭配的 endpoint(不屬於本 SDD
若 richblack 決定新 endpoint 要和前端同 PR 做:
- [ ] 後端:`GET /workflows`
- [ ] 後端:`GET /workflows/:name` + `/yaml`
- [ ] 後端:`GET /apps`
- [ ] 後端:`GET /api-keys``POST /api-keys``PATCH /api-keys/:id``DELETE /api-keys/:id`
否則:以 empty state 呈現,封測也能運作。
---
## KBDB 整合(配合 matrix/kbdb/.agents/specs/arcrun-key-auth/
Arcrun 用戶的 `ak_xxx` Key 同時可用於 KBDB(捆綁服務,自動開通)。
cypher-executor 需在以下時機呼叫 KBDB
- [ ] 後端:OAuth callback 成功 → `POST /partners`(建立 KBDB partner 記錄)
- [ ] 後端:`PUT /me/api-key/rotate` → 舊 partner revoke + 建新 partner 記錄
- [ ] 後端:`DELETE /me/api-key` → KBDB partner revoke
詳細設計見 `matrix/kbdb/.agents/specs/arcrun-key-auth/design.md`
---
## 目前狀態
- **進度**:Phase 0 已完成(4/5;最後一項等 richblack 認可)。
- **阻擋**richblack 認可 + 「開始動工」指令。
- **未啟動**Phase 1-7 全部 `[ ]`
@@ -0,0 +1,323 @@
# Design: KBDB Base —— 原子化萬用表(self-hosted 資料底座 + 官方核心)
> 2026-06-07 richblack 拍板。來源:壓測報告(`test_arcrun/docs/壓測報告.md`)暴露 self-hosted 無資料層 →
> richblack 決定把 KBDB 的「基礎儲存」開源進 arcrun,向量/三元組走插件模式(如 PostgreSQL 的 PGVector / Apache AGE)。
> **架構同時影響官方版**:官方也改成「基礎核心 + 可選模組」,故開源與官方共用同一基礎,不維護兩套。
---
## 0. 為什麼(壓測暴露的缺口)
壓測(第一次完整壓測,6 輪回歸)跑通了「表單 → workflow → Google Sheets」,但暴露 **self-hosted 沒有資料層**
| 缺口 | 現況 | KBDB Base 如何解 |
|---|---|---|
| **無「專案」維度** | KV key 只有 `{namespace}:{type}:{name}`,無 projectrecipe 甚至全局共享 | entries 樹狀(entry_type=project/workflowparent_id)→ 可列「某專案的所有工作流」 |
| **D1 完全空置** | self-hosted init 只建 8 KV,無 D1`grep d1_databases` 全 repo 零結果 | KBDB Base 用 D1 → 填上閒置的 D1 |
| **recipe 成功無記錄** | push 時 probe 2xx 但不持久化 | 成功記錄存 entries/records → 可監控、可當投稿依據 |
| **資料碎(像 n8n** | 散落 KV,無結構 | templates 做虛擬表 → 結構化資料形態 |
> recipe 成功記錄 + 投稿(帶記錄免驗證)本次一起做(見 7)。recipe 信譽/市場/排序是後續,不在本次。
---
## 1. 核心設計:原子化萬用表(萬年不動)
> richblack 設計原則:「基礎表是 truth,萬年不動。新技術(embed/triplet/未來)一直增加,
> 它們把衍生結果記在**各自的地方**,不回頭改基礎表。」這正是 PostgreSQL 核心 + PGVector/AGE 插件的模型。
### 三張表(沿用 KBDB v3 `0005_universal_table`,已驗證可用)
| 表 | 作用 |
|---|---|
| `entries` | 原子資料。`entry_type``block`/`value`/`template`/`slot`+ 本 SDD 擴充 `project`/`workflow`)。樹狀 `parent_id`、多租戶 `owner_id` |
| `templates` | 定義虛擬表的 slots`slots_json`,如 `["display_name","gender"]`)→ 同一份 entries 用不同 template 呈現不同資料形態 |
| `entry_values` | 把多個 entries 按 template 組成一筆結構化記錄(`record_id` + `slot_name` + `entry_id` |
### 萬年不動原則(本 SDD 要修正 KBDB 現況的地方)
KBDB 現況**大致已符合**,但有違反處要修:
- ✅ embed`block-embed.ts` 把向量 upsert 到 **Vectorize**(獨立),不動 blocks 表。
- ✅ triplet 主體:`triplet-crud.ts` 主要 `INSERT INTO entry_values`(獨立記錄)。
- 🔴 **違反處**`triplet-crud.ts:106/110` `UPDATE blocks SET entity_type = ?` —— 抽 triplet 後回頭改基礎表。
**修法**:衍生屬性(entity_type 等)移出基礎表,記到衍生層(entry_values 或獨立 template)。基礎表只存 truth。
---
## 2. 插件模式:一套 code,三層 binding 開關(不維護兩套)
> richblack:「像 PGVector/AGE 是插件。基礎模組提供原子化萬用表(這也是賣點);要向量、要圖資料庫另外裝。
> 我還是希望只維持一套。」
| 層 | 能力 | 依賴 | 開源 / 費用 |
|---|---|---|---|
| **基礎 KBDB Base** | entries/templates/entry_values CRUD + **D1 LIKE 關鍵字搜尋** | D1 only | ✅ 開源,免費,**不綁卡** |
| **+ embed 模組** | 語義搜尋(search 升級) | CF **Vectorize** binding + AI | embed 是 **CF 內建**(程式薄)→ **不拆 repo,給「開/關」**:有 Vectorize binding 就啟用,沒有就降級 LIKE。Vectorize 需用戶自開(自付費) |
| **+ triplet 模組** | 三元組抽取 / 知識圖譜 / 圖查詢 | AI + triplet-*.ts8 個 action,較重) | triplet 是 **richblack 的 IP、程式量大****獨立 repo / 獨立 worker**,基礎不依賴它,要才裝 |
### Q1 拍板(richblack 2026-06-07):embed 不拆、triplet 拆
- **embed → 不拆 repobinding 開/關**:因為 embed 靠 CF 內建 Vectorize,程式薄(呼叫 `env.VECTORIZE.upsert`),沒 binding 就降級。一套 code、開關即可。
- **triplet → 獨立 repo/worker**:非 CF 內建、是 richblack IP、action 多(triplet-extract/normalize/embed/crud/stats/syntax/entities/update)。基礎 KBDB **不 import triplet**,要才裝。
### search 分層(壓測者問「search 還是要有,只是沒語義」→ 對)
KBDB `search.ts` 已有兩層骨架:
- 基礎版:D1 `LIKE` 全文搜尋(KBDB 現有 fallback 邏輯,不需 Vectorize)。
- + embed:語義搜尋(Vectorize + AI)。
**API 不變,能力分層**:基礎用 LIKE,裝 embed 升級語義。
---
## 3. 解耦工作(把寫死的觸發改成可選)
KBDB 現況:`blocks.ts``ingestText`/`createTriplet`/`deleteBlockVector` **寫死 import 進寫入流程**(寫 block 順手 embed+抽 triplet)。
解耦目標:
- 寫 block = 只寫 block(純基礎)。
- embed = 可選 hookVectorize binding 在才掛)。
- triplet = 可選(triplet 模組裝了才掛)。
- 基礎 `blocks.ts` / `templates.ts` / `records.ts` **不 import** vectorize/triplet。
> ✅ 已查證 `templates.ts` / `records.ts` 本就乾淨(只 import record-crud);主要工作在 `blocks.ts`
---
## 4. 落地形態:import 進 arcrun(如 MCP 的 pattern
> richblack:「像先前把 MCP import 進本專案一樣,也把 KBDB import 進來。」對,同一 pattern。
- **基礎 KBDB Base 搬進 `arcrun/kbdb/`**(如 `arcrun/mcp/`)。形態:獨立 Worker + D1。納入 deploy 掃描(rule 05:新目錄 + wrangler.toml 自動部署)。
- **init 建 D1**`cli/src/lib/cf-api.ts` 加 D1 建立(如現有 KV 建立),`deploy.ts` 注入 D1 id。**self-hosted 從只建 KV → 建 KV + D1**。
- **cypher 改用 KBDB Base 存專案/工作流**webhook/credential/recipe 的「專案歸屬」走 KBDB entries(漸進,不一次搬完)。
- **embed**:隨基礎進 arcrunVectorize binding 開關。
- **triplet**:獨立 repo,self-host 用戶要才另裝(不進 arcrun 基礎)。
### 共用同一基礎(官方也改)
richblack 決定官方版也改成「基礎核心 + 可選模組」→ 官方與 self-hosted **共用同一份基礎 KBDB code**,差別只在 binding(官方全開、self-host 基礎免費可選開)。**不維護兩套。**
---
## 5. 邊界(本 SDD 不做)
- **recipe 投稿入口 + 帶成功記錄免驗證**:✅ **本次要做**,見新增 §7(與 KBDB 成功記錄嵌在一起)。
- **recipe 信任 = 市場機制(靠量/星數模型)**:拍板用市場優先、暫不做強防偽(見 §7.3)。本次(5.2)做投稿端點(submit 私庫 / submit-p 公共庫覆蓋同 canonical_id+ stat 存證;**跨環境「靠量同步回市場」聚合層另排 task**;強防偽(官方重跑/多方回報)只在市場失靈才做。
- **triplet 模組本身**:獨立 repo,不在本 SDD(本 SDD 只確保基礎不依賴它、可選掛上)。
- **vectorize 的進階用法**suggest/entities-graph-embed):隨 triplet/進階模組,不在基礎。
- **官方版的搬遷細節**:本 SDD 聚焦「基礎 KBDB 進 arcrun self-hosted」,官方共用基礎是方向,搬遷另排。
## 7. recipe 公庫/私庫機制 + 帶 KBDB 成功記錄(市場信任)—— 本次要做
> §7.1-7.4 原只想「投稿」單向(本次最小已做:成功記錄 5.1 + submit-p 雛形 5.2)。
> §7.5 是 richblack 2026-06-07 指示補的 **CHANGE**:把公庫/私庫的所有互動情境想全(pull/搜尋/投稿/市場同步),
> 待 review。下方 7.1-7.4 是已成立的基礎,7.5 是擴充全貌。
> richblack 2026-06-07:本次只做「可投稿 + 帶成功記錄免驗證」,這兩件嵌在一起。
> 背景:官方初期那批 recipe 是一次性建立、未驗證(只為「Arcrun 推出不能空空的」)。
> 壓測者打通的修正版(如 google_sheets_append method/body)回不了官方 → 官方仍錯,下個用戶再撞。
> 不該由 AI 手改種子 recipe(治標);正解是投稿機制 + 用真實成功記錄當免驗證依據(治本)。
### 7.1 兩件嵌在一起
1. 成功記錄(KBDB):判定單位是「**工作流執行**」(對標 n8n execution,非逐節點)。一條 workflow 跑完,
整體成功(到 Output、無 error)→ 把這次**用到的每個 recipe 節點**各記成功 +1;整體失敗 → 已跑到的 recipe 節點各記失敗 +1。
歸屬對象是 recipe(因為投稿的是 recipe),**canonical_id 取 auth recipe 的 `service` 欄位**(與投稿身份一致)。
單節點工作流(cron→gdrive,一個節點一個 credential)是最簡情形。存 D1entry/record),可累積、可查。
> 註(術語別混):**cypher-executor = 工作流引擎**,用 cypher **語法**(節點+邊的圖遍歷語意)把零件串成工作流,
> 本就在開源核心內,**不是** graph DB。「可選/不在核心」的是兩個插件:① **triplet**(知識三元組的圖**儲存**
> 對標 Apache AGE)② **vectorize**(CF 內建但可能多花錢)。recipe 成功記錄是純 D1 KBDB 核心能力,不依賴這兩個插件。
2. 投稿(registry/官方):投稿 recipe 時帶上它在投稿者環境的成功記錄。官方端看記錄即信,免人工驗證——真實打通比投稿者自寫測試可信(呼應 gherkin-not-safety-sandbox-is)。
### 7.2 本次範圍(最小)
- recipe 成功/失敗計數落地 D1(KBDB 成功記錄)。
- 一個投稿端點:收 recipe + 其成功記錄 → 進官方 recipe 池,**新增一個作者版本**app-store 模型 §7.5.5**非**覆蓋同 canonical_id;同 canonical 多作者並存)。
- 投稿者目前就是 richblack,不需誘因;重點是有入口可投 + 帶記錄免驗證。
### 7.3 信任機制:市場優先(靠量,非人力/強防偽)—— richblack 2026-06-07 拍板
> 這個決策之前討論過、那時選「先不做」,導致今天又重講一次 → 故此次**寫進 SDD**,不再丟。
**核心:recipe 信任靠「量」決定,像 GitHub 星數,不靠人力檢核、也不先做強防偽。**
不可能全世界這麼多 recipe 都靠人力檢核。改用市場機制:
- recipe 的成功/失敗由**全體使用者的真實執行**累積(投稿者 A 的 recipe 通過 2000 次;投稿者 B 的失敗 500 次 = 像被按爛)。
- 上傳爛 recipe → 別人用會失敗 → 累積失敗數 → 投稿者信用下降 → 有動機更新自己發佈的 recipe,避免持續被按爛。
- **數據來源 = §7.1 的成功記錄**(每次工作流執行把用到的 recipe 成功/失敗記到 KBDB)。市場機制 = 把這些**跨使用者**的記錄**同步回公共市場**匯總。
**為什麼先市場、暫不做強防偽(誠實,mindset §7)**:
self-hosted 投稿者 = 自己環境 root,能寫任意數字進自己 KBDB → 自報 stat 可造假。
但**先信市場**:實驗一段時間,若市場反應爛(造假猖獗)再考慮強防偽(官方重跑 / 多方獨立回報——證據由投稿者控制不了的一方產生)。
故**本次不拿投稿者自報的數字當自動放行門檻**(那會造債:未來門檻是空的)。stat 先當存證 + 法律歸責軌跡。
**分期**
- **本次(5.2**:投稿端點 `POST /recipes/submit`。submit(私庫)vs submit-p(公共庫,需 exposure_consent = 暴露同意,mindset §6)。
公共庫**新增作者版本**app-store 模型 §7.5.5,非覆蓋)。投稿者帶的 stat 寫進官方 KBDB 當存證(不當門檻)。
- **後續(市場同步,§7.3 主體)**:跨環境「靠量同步回市場」——self-hosted 各用戶的真實成功/失敗匯總回公共市場,
形成 recipe 的星數/信用。是 §7.1 數據的跨環境聚合層。本次先留接口(5.1 已在各環境記錄,市場聚合另排 task)。
- **更後續(強防偽,只在市場失靈才做)**:官方 in-process 重跑投稿者工作流拿官方自己的 2xx / 第二投稿者獨立回報。
(註:app-store 模型下「版本並存」是常態、不刪舊版 §7.5.5;舊版靠市場淘汰,不做下架引導。)
### 7.4 與零件投稿的差異
零件投稿走 GitHub PR(人 merge + CI runtime 驗 wasm,記憶 component-submission-via-pr)。
recipe 是純文字資料(非 wasm),不需 CI 跑沙箱;它的驗證改用真實成功記錄。故 recipe 投稿機制與零件不同,不混用 component-registry-canon SDD。
### 7.5 公庫 / 私庫雙向機制(CHANGErichblack 2026-06-07 指示「把兩庫所有情境想好」)—— 待 review
> 背景修正:原 §7.1-7.4 只想了「投稿」單向,太窄。richblack 指出:
> **SaaS 版(standard mode)本來就只有公庫;self-hosted 版本來就只有私庫**
> 兩庫的互動是 overall 架構(不只為投稿):私庫是公庫的**按需子集**——self-hosted 也許只用 10 個,
> 公庫也許有 10000 個,**不需每次全部拉下來,用到的再複製**。投稿(submit-p)只是其中一條反向流。
#### 7.5.1 兩個庫的物理本質(已確認現況)
| 庫 | 是誰 | 物理 | 規模 | 角色 |
|----|------|------|------|------|
| **公庫** | 官方 SaaS`cypher.arcrun.dev`)的 `RECIPES` KV | 官方 CF 帳號 | 大(~10000,含社群投稿) | 唯一公共真相、可瀏覽/搜尋/pull |
| **私庫** | 每個 self-hosted(用戶自己帳號)的 `RECIPES` KV | 用戶 CF 帳號 | 小(~10,按需子集) | 自己實際用到的 + 自己改的 |
- recipe key 都是 `recipe:{canonical_id}`(無 owner 前綴)。同一套部署的 KV = 那套的庫。**物理隔離**(不同帳號不同 KV)。
- `mode`cli config):`standard` = SaaS 用戶(只連公庫,無私庫);`self-hosted` = 自己一套(有私庫,可選連公庫 pull/submit);`local` = 純本地。
#### 7.5.2 三種使用者 × 互動矩陣
| 使用者 | 有哪些庫 | 取 recipe | 改 / 投稿 | 成功記錄 |
|--------|----------|-----------|-----------|----------|
| **SaaS 用戶**standard) | 只公庫 | 直接用公庫(公庫即他的庫) | 改了直接寫公庫(=投稿,需 consent) | 記在官方 KBDB(即市場數據本身) |
| **self-hosted 用戶** | 私庫(主)+ 公庫(唯讀來源) | **公→私 pull**:搜公庫→複製到私庫 | 私庫改 →(可選)**submit-p** 推回公庫 | 記在自己 KBDB;市場同步另排(§7.3 5.5 |
| **local** | 無遠端庫 | 本地 recipe 檔 | 無 | 本地 |
#### 7.5.3 四條互動流(窮舉)
1. **公→私 pull(核心,之前漏)**self-hosted `acr recipe pull <canonical_id>` → 從公庫只讀端點抓該 recipe → 寫進自己私庫 KV。
按需,**不全量同步**(10000 不必拉)。pull 也可帶公庫的成功記錄(市場星數)供用戶判斷要不要用。
2. **公庫瀏覽/搜尋**`acr recipe search <q>` / `list` → 打公庫只讀端點(`GET /public-recipes?q=`)。
self-hosted 沒搜尋能力就不知道公庫有什麼可 pull。SaaS 用戶同一端點即用。
3. **私→公 submit-p(已做雛形)**self-hosted 把私庫某 recipe 推回公庫(`POST /recipes/submit`,需 exposure_consent)。**新增作者版本**app-store 模型 §7.5.5,非覆蓋;同 canonical 多作者並存)。
4. **成功記錄 → 市場同步(§7.3 5.5,另排)**self-hosted 各環境真實成功/失敗匯總回公庫的市場星數。pull(流1) 帶的星數就來自這裡。
#### 7.5.4 公庫只讀端點(公→私 + 瀏覽的基礎,待實作)
官方 cypher 開**公開只讀**端點(無需 api_key,因公庫本就公共):
- `GET /public-recipes?q=&limit=&offset=` → 搜尋/列出公庫 recipe。**同 canonical_id 回多筆(多作者)**,各附作者 + 市場星數 success/failure,供 CC/AI 依數據選(§7.5.5)。
- `GET /public-recipes/:canonical_id?author=` → 取單一 recipe 全文(pull 用)。不指定 author → 回市場最佳版本(成功率最高)。
- **搜尋/取用落空 → 回 `{ found: false, canonical_id, hint }` 創作引導**(§7.5.6),不回空陣列乾等。讓 CC 知道下一步是「自己做一個成為作者」。
> 與現有 `GET /recipes`(已存在,列當前部署 KV)的差別:`/public-recipes` 語意是「**這是公庫**,給外部 self-hosted pull/瀏覽」,且**含作者維度 + 市場數據**。
> 在官方部署上讀同一 KV;命名分開是為了語意清楚 + 公庫的多作者/市場排序不污染內部 `/recipes`
> **市場星數 per-uuid(§7.5.h 已實作)**5.1 改收集 API recipe 的 **uuid**(非 auth service),KBDB 市場星數記 per-uuid。
> 故 `GET /public-recipes/:canonical_id` 的「選市場最佳作者版本」**真正能區分 Leo 版/John 版**(各自 uuid 各自累積)。
> 舊資料無 uuid → fallback canonical_idmigration 後自然帶 uuid)。
#### 7.5.5 recipe = 具名作者作品(app-store 模型)+ UUID 身份 —— richblack 2026-06-07 拍板
> 這條推翻了我原本「覆蓋同 canonical_id」的錯誤前提。正確模型如下。
**每個投稿的 recipe 是一個「具名作者作品」,像 app store 上的一個 app,只有作者(+admin)能改。**
- Leo 投稿 `gsheets_upsert` = 在 app store 放了一個 app。**只有 Leo + admin 能動它**。
- John 覺得 Leo 的很爛 → John **另推一個** `gsheets_upsert`John 的作品)。**只有 John + admin 能動**。
- 公庫裡因此**同 canonical_id 並存多份**Leo 版、John 版),各帶作者、各帶獨立市場數據。
**所以:**
1. **submit-p ≠ 覆蓋**。投稿是**新增一個作者版本**,不是覆蓋同 canonical_id。(修正原 §7.2/§7.3 的「覆蓋」字眼。)
2. **沒有「公庫改了 Leo 要不要跟」的問題**:Leo 的作品只有 Leo 改;別人不滿意是自己推新作品,不是改 Leo 的。版本關係問題消失。
3. **向後相容、不刪舊 recipe**:Leo 的 5/10 不被刪,只是市場上沒人選 → 自然淘汰(長尾留著,無破壞性刪除)。
4. **選擇由市場數據驅動(CC/AI 操盤手選)**CC 搜 `gsheets_upsert` → 找到 2 個 → 看數據(Leo 5成功/10失敗、John 100成功/0失敗)→ **選 John 的**。Leo 的逐漸沒人用。
**身份模型(落地)= UUIDrichblack 2026-06-07 拍板,比 canonical_id+author 複合鍵更乾淨)**
**每個 recipe 一誕生就領一個 UUID = 唯一身份。`canonical_id` / `author` / `公私` 全是屬性,不是身份。**
- 身份(UUID) 與 歸屬(author 屬性) 分離 → 沒有「同 canonical_id 撞 key」問題(key 是 UUID,本就唯一)。
- **KV key 設計**`recipe:{uuid}` 存 recipe 本體;`idx:canonical:{canonical_id}` → UUID 清單(同 canonical 多 UUID);
私庫執行:`idx:installed:{canonical_id}` → 該 canonical 在本庫安裝的**唯一** UUIDpull/author 時定一個)→ 執行查找仍由 canonical_id 解析到唯一 recipe**不破執行**。
- **Leo 改 John 的**Leo 拿 John 版(UUID-J)改 → 發出去領**新 UUID-L** = Leo 作品。UUID-J/UUID-L 是兩個東西,不冒名、天然不衝突。
- **author 屬性 = 該 UUID 誕生時的投稿者**(誰投誰負責那版市場數據);可選帶 `derived_from: {uuid}` 溯源(致謝/fork upstream)。
**「Leo 跟 John 一模一樣」怎麼辦**(你問的):兩個不同 UUID 都通過、各跑各的市場數據。
**重複 = 市場冗餘,靠數據淘汰不靠技術阻止**(先有數據的勝,複製品沒人選自然沉底,§7.3 靠量不靠人力)。
**檢舉功能**:本次**不做**。檢舉 = 人力檢核,與「市場優先、暫不強防偽」(§7.3)矛盾,提前做還用不到 = 造債。
市場失靈才考慮(§7.3 更後續)。**CC 不檢舉**mindset §7:CC 是操盤手不是審查者、不代人裁決;CC 對爛 recipe 的「投票」= 不選它 = 市場數據)。
> 影響現有 code5.2 的 `POST /recipes/submit` 改「覆蓋」→「領新 UUID 新增」;recipe key 從 `recipe:{canonical_id}`
> `recipe:{uuid}` + canonical 索引;執行查找改經 `idx:installed:{canonical_id}` 拿唯一 UUID(保證不破 component-loader/auth-dispatcher/credential-injector)。列入 7.5.f task。
#### 7.5.6 搜尋落空 = 創作入口(richblack 2026-06-07 拍板)
> app-store 模型的閉環關鍵:公庫沒有的 recipe,由 CC 現做成為作者,再投回公庫補上。
**CC 搜「gsheets_delete_sheet」公庫沒有 → 端點不能只回空陣列乾等,要回「可創作」訊號 → CC 說「那我做一個」→ CC 成為作者。**
- 公庫只讀端點(§7.5.4)搜尋**落空時的回應 = 創作引導**,不是空結果:
明確回 `{ found: false, canonical_id, hint: "公庫無此 recipe,可自行建立並 submit-p 投稿成為作者" }`
讓 CC(AI 操盤手)知道下一步是「自己做一個」而非卡住。
- CC 現做 → 跑成功累積市場數據(§7.1)→ submit-p 推回公庫 → 成為公庫**第一個** `gsheets_delete_sheet`CC 是作者)。
- **閉環**
- 公庫有 → pull 用(市場數據選最佳作者版本,§7.5.5)。
- 公庫沒有 → CC 現做成為作者 → 用 → 投稿 → 公庫從此有了。
- 呼應 mindset**CC 是大腦**arcrun 是 AI 呼叫的工具),缺能力時 CC 自己補(做 recipe / 工作流),
不停在「找不到」。工作流是 default、零件是例外(缺能力先想工作流/recipe,不是停手)。
---
## 8. KV list 上限威脅免費承諾 → 高頻 list 遷 D1CHANGE2026-06-08 richblack CF 收到 list 超標信)
> 事件:richblack 的 CF 帳號收到「Workers KV 免費等級每日 1000 次 list 上限已超」信,
> list API 回 429 直到隔日重置。**這直接威脅 arcrun「免費可用」核心承諾**[[deploy-via-local-deploy-not-ci]] 免費優先精神)。
> 方向拍板(richblack 2026-06-08):**高頻 list 改走 D1**(對齊 §6 Q2 的 KV vs D1 分工——要列舉/搜尋的進 D1)。
### 8.1 根因:哪些 KV list 是高頻(grep cypher-executor/src
| 位置 | list 操作 | 頻率 | 嚴重度 |
|------|----------|------|--------|
| `scheduled.ts:34` | `WEBHOOKS.list({prefix:'cron-idx:'})` | **每分鐘一次 = 1440/日** | 🔴 **常駐單獨就爆**>1000 上限) |
| `recipes.ts` public-recipes(§7.5 新加) | `RECIPES.list({prefix:'recipe:'})` | 每次 recipe 搜尋 | 🟠 搜尋頻繁就疊加爆 |
| `recipes.ts` GET /recipes / listAllRecipes | `RECIPES.list` | 每次列 recipe | 🟠 |
| `webhooks-list.ts` / `webhooks-named.ts` | `WEBHOOKS.list` | 每次列 workflow | 🟠 |
> **D1 額度遠寬**:免費 5M 讀/日、100K 寫/日(§6 Q4),對比 KV list 僅 1000/日。要「列舉/搜尋/排序」的天生該 D1。
### 8.2 緊急止血(cron,今天在爆)
`scheduled.ts` 每分鐘 list `cron-idx:` → 改成**不靠 list**
- 方案:維護**單一固定 key**(如 `cron-idx:_all` 存所有 cron workflow 的 {apiKey,name,cron_expr} JSON 陣列),
scheduled 只 `get` 一次(取代 list)。acr push 時 upsert 進這個 key。
- 1440 次/日 list → **0 次 list**(改 1 次 get/分鐘 = 1440 get/日,KV get 免費額度 100K/日,遠夠)。
### 8.3 治本(recipe / workflow 查詢遷 D1,分期)
對齊 §6 Q2recipe + workflow 進 D1):
- **recipe**recipe 本體從 RECIPES KV 遷進 KBDB D1entry_type='recipe' 或專表)。
public-recipes 搜尋、list 改 D1 queryLIKE/WHERE,含 §7.5 多作者/市場排序天生適合 SQL)。
執行時 resolveRecipe 也改 D1(但執行是 get-by-key 級,KV get 不爆——可漸進)。
- **workflow**workflow record 從 WEBHOOKS KV 遷 D1entry_type='workflow' 掛 project parent,§6 Q1)。
list workflow 改 D1 query。
- **保留 KV 的**session / credential / exec context(§6 Q2:短期高頻純 get,不 list)。
### 8.4 分期建議(待 richblack 拍板細節)
1. **P0 止血**(§8.2):cron 改單 key get。最小改動、今天可止血。
2. **P1**public-recipes / recipe list 改 D1(我 §7.5 新加的 list 是疊加元兇)。
3. **P2**workflow list 改 D1。
4. **P3**recipe/workflow 本體完整遷 D1resolveRecipe / KV→D1 讀寫)。
> **誠實 trade-off**:遷 D1 要寫 migration + 雙寫過渡(KV 舊資料相容),工程量不小。
> 但不遷 = 免費承諾破功(用戶用一用就 429)。**這是核心承諾必修項,不是優化。**
> 待 review §8 + 拍板分期顆粒度後動 code。**勿在 review 前動。**
---
## 6. 開放問題(待 review
1. **專案維度怎麼進 entries**`entry_type='project'` + workflow 用 `parent_id` 掛專案?還是 templates 定義「專案」虛擬表?
1. Leo:由你決定,我記得先前 KBDB 已經有實作,參考原有做法。
2. [DECIDED, CC 2026-06-07] use entry_type=project + parent_id tree (NOT templates virtual table). Matches existing KBDB: entry_type is an extensible string, entry-crud.ts already supports WHERE parent_id; no schema change (honors table-never-changes). "list workflows under project" = WHERE parent_id=PROJECT_ID AND entry_type=workflow. templates/slot reserved for structured records like recipe success stats.
2. KV vs D1 分工[拍板 2026-06-07]KV 不全廢,按特性分。workflow YAML + recipe + 成功記錄進 D1(要層級/列舉/排序);session、verdict、credential 留 KV(短期高頻純取用)。類比 PostgreSQL + Redis。workflow 變 entryentry_type=workflow 掛 project parent)。
1. Leo:同意
3. **triplet 獨立 repo 的接點**:基礎 KBDB 怎麼讓 triplet 模組「掛上」——HTTP hook?還是 triplet worker 自己讀同一個 D1
1. Leotriplet 其實也就是 template 加上一些函式,最簡單就是你給他一段文字抽取出三元組,但如果要自動化,就會提供 API,背後是工作流,例如 block 存入時就叫起 triplet 功能,這個功能如果自動化就是在安裝時同意,但應該也可以有 mcp 讓 AI 易於操作,如同前述,交貨時有 MCP,因為是 AI Friendly 的系統。
4. **init 建 D1 的冪等 + self-hosted 免綁卡確認**:D1 是否如 KV 一樣免費不綁卡(需查證 CF D1 免費額度條件)。
1. Leo:一樣不綁卡,除非他用量很大,屆時不是我來提醒而是 CF 會提醒他
@@ -0,0 +1,221 @@
# Tasks: KBDB Base — atomic universal table
> 對應 design.md。design 待 richblack review 後才動 code。每完成一個 task 立刻標 [x],不批次。
> 來源:壓測報告暴露 self-hosted 無資料層;richblack 2026-06-07 拍板 KBDB 基礎開源 + 插件模式。
---
## 狀態:SDD 草稿,待 review design + 拍板開放問題後才動 code
- [x] Q1 專案維度[拍板 2026-06-07]richblack 授權 CC 決定,參考 KBDB 既有實作(已有 project/parent 做法)
- [x] Q2 KV vs D1 分工[拍板 2026-06-07]KV 不全廢,按特性分——workflow YAML+recipe+成功記錄進 D1session/verdict/credential 留 KV
- [x] Q3 triplet 接點[拍板 2026-06-07]triplet = template + 函式。手動(給文字回三元組)或自動(block 存入時叫起,安裝時同意);提供 MCP(AI-friendly,交貨帶 MCP
- [x] Q4 D1 免費不綁卡[查證 2026-06-07 CF docs]Workers Free 可用、不需信用卡(不像 R2)。額度 5M 讀/日、100K 寫/日、5GB。用量大時 CF 自會提醒用戶。
## Phase 0:基礎抽取與解耦
- [x] 0.1 三表 migration 抽出 arcrun/kbdb/migrations/0001_base.sqlentries/templates/entry_values + recipe_stat template seed),無 triplet/entity/vectorize 表
- [x] 0.2 萬年不動:新基礎 schema 本就不含 entity_type(那是 triplet 衍生欄位)→ 基礎 entries 無此欄,衍生屬性天然在基礎表外。原 kbdb 的 UPDATE blocks SET entity_type 屬 triplet 模組(獨立 repo),不影響 arcrun 基礎
- [x] 0.3 解耦:arcrun/kbdb 從零寫乾淨基礎(entry/template/record CRUD),不 import 任何 embed/tripletgrep 證);不去動原 kbdb repo(官方版仍用,動它有風險)
- [x] 0.4 templates/records route 純淨:只依賴 record-crudD1),無 vectorize/triplet
- [x] 0.5 基礎 search = D1 LIKEGET /entries/search?q=mode:keyword);語義層留給 embed 模組(未在基礎)
## Phase 1embed 模組(CF 內建,binding 開/關,不拆 repo
> 狀態:基礎已與 embed 完全解耦(不 import)。embed 模組本身(Vectorize upsert + 語義 search)尚未實作——基礎不依賴它即可,embed 待要用時再加。以下 1.x 未做。
- [ ] 1.1 embed 改可選 hook:有 env.VECTORIZE 才掛
- [ ] 1.2 search 語義層:有 Vectorize 啟用,否則降級 LIKEAPI 不變
- [ ] 1.3 wrangler.tomlVectorize/AI binding 可選
## Phase 2import 進 arcrun(如 MCP pattern
- [x] 2.1 基礎 KBDB Base 搬進 arcrun/kbdb/Worker + D1):kbdb/ 已建(三表 CRUD + recipe-stats + migration),納入 find wrangler.toml deploy 掃描
- [x] 2.2 cf-api.ts 加 D1 建立(listD1Databases/ensureD1Database);deploy.ts 注入 d1DatabaseId 到 kbdb wrangler.toml
- [x] 2.3 self-hosted init 建 KV + D1Q4 已確認免綁卡):init ensureD1Database 建空 D1 → deploy 部署完對 D1 套 migrations/0001_base.sqlCF /d1/query APIidempotent)建三表 + recipe_stat seed + 注入 wrangler.toml database_id
- [ ] 2.4 cypher 改用 KBDB 存專案/工作流歸屬(依 Q1/Q2,漸進)— 未做(後續)
## Phase 3triplet 模組(獨立 repo,本 SDD 只定接點)
- [ ] 3.1 確認基礎不依賴 tripletPhase 0.3 後驗證)
- [ ] 3.2 定義 triplet 掛上基礎的接點(依 Q3)
- [ ] 3.3 triplet 本身 → 獨立 repo,不在本 SDD
## Phase 4:官方共用基礎(方向,搬遷另排)
- [ ] 4.1 官方版改用同一份基礎 KBDB(binding 全開)→ 不維護兩套
## Phase 5recipe 投稿 + 帶 KBDB 成功記錄(免官方驗證)—— 本次要做(design 7)
- [x] 5.1 recipe 成功記錄落地 D1:判定單位=工作流執行(n8n execution)。GraphExecutor 收集本次用到的 recipe keyusedRecipeKeys=API recipe uuid,§7.5.h 改 per-uuid;舊資料 fallback canonical_id);executeWebhookGraph 執行結束後一次性 POST KBDB /recipe-stats/record(整體成功→各+1成功、真錯非paused→各+1失敗,fire-and-forget via waitUntil)。cypher tsc exit 0
- [x] 5.2 投稿端點(cypher recipes.ts):POST /recipes/submit(需 exposure_consent、stat 存證不當門檻)。**已修正**(7.5.f):原「覆蓋同 canonical_id」→ app-store/UUID 模型「領新 uuid 新增作者版本」,同 canonical 多作者並存。cypher tsc exit 0
- [x] 5.3 CLI 接點(= 7.5.b/c/e):acr recipe push(私庫)/ pull(公→私)/ submit-p(公共庫帶暴露同意)。cli tsc exit 0
- [x] 5.4 市場機制(靠量/星數模型,design §7.3):stat 存證已做(5.2 recipe_submission entry+ per-uuid 市場數據(5.1+7.5.h)。先市場優先、暫不強防偽(5.6 後續)
- [ ] 5.5 市場同步(跨環境聚合層,§7.3 主體):self-hosted 各用戶真實成功/失敗匯總回公共市場 → recipe 星數/信用。5.1 已在各環境記錄,聚合另排
- [ ] 5.6 強防偽(只在市場失靈才做):官方 in-process 重跑投稿者工作流拿官方自己 2xx / 第二投稿者獨立回報;版本並存/舊版下架
## Phase 7.5:公庫/私庫雙向機制(CHANGEdesign §7.5)—— richblack 2026-06-07 review 通過,可執行
> richblack 指示「把兩庫所有情境想好」+「這些細節想好是可以執行」→ §7.5 review 通過,開始動 code。
> **7.5.5 拍板:recipe = 具名作者作品(app-store 模型)**——同 canonical_id 多作者並存、只有作者+admin 能改、
> 投稿=新增作者版本(非覆蓋)、向後相容不刪舊、選擇由市場數據驅動、無「公庫改私庫要不要跟」問題。
> **7.5.6 拍板:搜尋落空=創作入口**——公庫沒有→回 found:false 創作引導→CC 現做成為作者→投稿補上(閉環)。
- [x] 7.5.g 落空創作引導:GET /public-recipes(?q=) 與 GET /public-recipes/:canonical_id 落空回 { found:false, hint } 創作引導,不回空陣列。cypher tsc exit 0
- [x] 7.5.a 公庫只讀端點(cypher recipes.ts):GET /public-recipes?q=&limit=&offset=list/search 多作者+per-uuid 市場星數)、GET /public-recipes/:canonical_id?author=pull 取全文,多作者選市場最佳)。cypher tsc exit 0
- [x] 7.5.h **市場星數 per-uuid(§7.5.h**5.1 改收集 API recipe uuidresolveRecipe,非 resolveAuthRecipe);usedRecipeServices→usedRecipeKeysKBDB 星數記 per-uuidpublic-recipes 選最佳 per-uuid 查 → 真正區分 Leo/John 版。cypher tsc exit 0
- [x] 7.5.f **recipe UUID 身份模型(app-store 核心,§7.5.5 拍板)**recipes.ts RecipeDefinition 加 uuid/author/derived_frominstallRecipeRecord helperrecipe:{uuid} + idx:canonical 清單 + idx:installed + idx:hash);POST /recipes=私庫沿用 installed uuid 就地更新;POST /recipes/submit=領新 uuid 新增作者版本(非覆蓋);resolveRecipe 向後相容(uuid→installed→fallback 舊 key,不破執行鏈);DELETE 清 uuid+索引;GET dedupinit-seed 用 UUIDauthor=system);POST /recipes/migrate-uuid 一次性轉舊 key(增量寫不刪舊、冪等)。cypher tsc exit 0。重複靠市場淘汰、不做檢舉、CC 不檢舉(§7.3+mindset §7
- [x] 7.5.b 公→私 pullCLI 薄殼):acr recipe pull <canonical_id> [--author] → GET 公庫 /public-recipes/:id → POST 自己私庫 /recipes(帶 derived_from 溯源)。cli tsc exit 0
- [x] 7.5.c 公庫搜尋(CLI 薄殼):acr recipe search <q> → GET 公庫 /public-recipes?q=,印多作者+市場數據,落空印創作引導。cli tsc exit 0
- [x] 7.5.e submit-pCLI 薄殼):acr recipe submit-p <canonical_id> [--author] → GET 私庫取全文 → 暴露同意 → POST 公庫 /recipes/submit(新增作者版本)。config 加 DEFAULT_PUBLIC_LIBRARY_URL(公庫=官方 cypherARCRUN_PUBLIC_LIBRARY_URL 可覆蓋)。cli tsc exit 0
- [x] 7.5.i **MCP 薄殼補齊 recipe 工具(rule 07 §5**:新增 mcp/src/tools/arcrun_recipe.ts 六工具 arcrun_recipe_search/pull/submit_p/push/list/deleteregisterAllRecipeTools 註冊進 registry.ts),全用 cypherFetch 薄殼模式(無業務邏輯)。與 CLI 六能力對齊,MCP 不再落後。submit_p 帶 exposure_consent 把關。mcp tsc exit 0。註:MCP 連平台 cypher(§5.2 account-source 已知違反 pre-existing,沿用既有模式不一併修)
## Phase 8KV list 上限威脅免費承諾 → 高頻 list 遷 D1CHANGEdesign §8)—— 待 richblack review 分期顆粒度,勿動 code
> 2026-06-08 richblack CF 帳號收到 KV list 超標信(每日 1000 上限,429)。威脅免費核心承諾。
> 方向拍板:高頻 list 遷 D1(對齊 §6 Q2)。根因①cron 每分鐘 list(1440/日單獨就爆)②§7.5 新加的 recipe 搜尋 list。
> 待 review §8 分期顆粒度後才動 code。
- [x] 8.P0 **緊急止血 cron**(§8.2):scheduled.ts 每分鐘 WEBHOOKS.list('cron-idx:') → 改單一固定 keycron-idx:_all 存 {apiKey}:{name}→cron_expr map)只 get 一次。新增 lib/cron-index.tsreadCronIndex/updateCronIndexEntry,單 key read-modify-write);webhooks-named POST/DELETE 改維護單 key;新增一次性 POST /webhooks/named/migrate-cron-index 把舊 per-key 折進集中 key(冪等、不刪舊);acr update 部署後自動呼叫 migrate(接在 seed 後,冪等、失敗不致命)→ 既有 cron 不必手動重 push。1440 list/日 → 0 list。cypher+cli tsc exit 0。**已完成 2026-06-09**
- [ ] 8.P1 recipe 查詢遷 D1(§8.3):public-recipes 搜尋 + GET /recipes list(§7.5 新加的元兇)改 D1 queryLIKE/WHERE,多作者/市場排序天生適合 SQL)
- [ ] 8.P2 workflow list 遷 D1webhooks-list/webhooks-named 的 list 改 D1 queryentry_type=workflow
- [ ] 8.P3 recipe/workflow 本體完整遷 D1resolveRecipe / KV→D1 讀寫 + migration + 雙寫過渡(KV 舊資料相容)。session/credential/exec-context 留 KV(§6 Q2
## Phase 9KBDB 資料層薄殼補 MCP/CLIHANDOFF §2design §4「交貨帶 MCP」)—— 本次做
> 核實:CLI/MCP 現在完全沒 KBDB 資料層能力(既有 arcrun_skills_examples 打的是舊 /blocks /search
> v3 schema,非本基本盤三表)。基本盤 API 已完整(templates/records/entries/search)。
> 本 Phase = **薄殼暴露**,不重寫能力(rule 07)。
> **KBDB 鐵律(leo 2026-06-14**:不提供建表/SQL toolAI 只有「建 template(name+slots) + 填 record(slot→content)」
> 可用(類 Supabase 萬用表);薄殼只經 KBDB service binding 調基本盤 HTTP API,不直連 D1、不寫 SQL。
- [x] 9.1 **MCP 薄殼**(AI 用,插件也走這條):mcp/src/tools/kbdb_data.ts6 工具
`kbdb_create_template`(name+slots)、`kbdb_list_templates``kbdb_create_record`(template+values)、
`kbdb_get_record`(record_id)、`kbdb_query`(by-template 列 records)、`kbdb_search`(entries LIKE q)。
全走既有 kbdbFetchKBDB binding)薄殼模式,無業務邏輯。registerAllKbdbDataTools 註冊進 registry.ts。
**不含建表/SQL tool**(鐵律,grep 證 code 無 CREATE TABLE/.prepare/env.DB/SQL)。**mcp tsc exit 0**2026-06-14)。
- [x] 9.5 **cypher KBDB proxy9.2 的前置,2026-06-14**CLI 是 client 只認證到 cypher,達不到獨立
KBDB workerMCP 走內部 service binding 可達,CLI 不行)。故在 cypher 開 `cypher-executor/src/routes/kbdb-proxy.ts`
純轉發 `/kbdb/templates|records|search` → KBDB 基本盤(沿用 KBDB_BASE_URL HTTP fetch + KBDB_INTERNAL_TOKEN
**不新增 service binding** rule02 §3.1)。**租戶隔離(leo 拍板選項①)**X-Arcrun-API-Key 自動當 owner_id 注入
records/entries(強制覆寫 caller 自帶 owner_id 防跨租戶寫);**templates 全域共享**(虛擬表定義是 schema 非資料)。
無 SQL/建表/業務邏輯(純 proxy)。掛進 index.ts。cypher tsc exit 0。
- [x] 9.2 **CLI 薄殼**(人用,2026-06-14):`cli/src/commands/kbdb.ts` — acr kbdb template create/list、
record create/get、query、search,透過 9.5 的 cypher proxy 打基本盤(與 MCP kbdb_* 同能力,差異只來自介面慣例
rule07 §3.4)。註冊進 index.ts`acr kbdb`)。無業務邏輯(薄殼)。cli tsc exit 0。
**未驗收**:端到端需 cypher 部署 + KBDB_BASE_URL 可達後實測(acr kbdb template create → query 回得到)。
- [x] 9.3 **基本盤 entries 加 page_name 讀過濾**2026-06-14):listEntries 加 `page_name` 過濾 +
GET /entries 接 `?page_name=`(既有欄位的便利查詢,不動表結構、不違反「表不變」鐵律)。
用途:skills/examples 用 page_name 當 idempotency key 做 get-by-key。kbdb tsc exit 0。
- [x] 9.4 **修復 LI M3 斷鏈**2026-06-14,連動 llm-interface M3.2/M3.4):skills/examples 整條從
舊 v3 `/blocks` `/search` 改打基本盤 `/entries`entry_type 對應)。5 個已上線的 MCP 工具原本
對死 route 回 404(假綠),現修正;sync-registry-to-kbdb.py 改打 /entries idempotent upsert。
誠實降級:基本盤無語義 search → search_examples 改 LIKE 關鍵字(embed 模組 Phase 1 上線再換回語義)。
mcp + kbdb tsc exit 0。
- [x] 9.6 **cypher proxy 補 `/kbdb/entries` CRUDHANDOFF §2 缺口①,2026-06-15**9.5 proxy 只轉發
templates/records/search**漏了基本盤的 `/entries` CRUD**——這正是 mira `_kbdb_client.py` 主線遷移
ingest/create_block/get_by_id/get_by_page_name/patch_block)要打的端點。補 POST/GET(list)/GET(:id)/PATCH(:id)
`/kbdb/entries` 純轉發到 KBDB 基本盤 `/entries`。**租戶隔離同 9.5 選項①**:寫入強制注入 owner_id、list 強制
以本租戶 owner_id 過濾(防跨租戶讀)、PATCH 剝除 caller 自帶 owner_id(防認領/踢走);by-id GET 沿用既有
records by-id 慣例(require-key)。**刻意不開 DELETE**(基本盤 delete-by-id 無 owner 檢查,經 proxy 暴露 =
跨租戶刪除風險;mira 也不需要)。無 SQL/業務邏輯(純 proxy)。**cypher tsc exit 0** + **端到端 prod 驗收綠**2026-06-15):
無 key→401A POST→owner_id 自動=AGET by id 回得到;A list count=1、B list 同 type **count=0**(跨租戶隔離);
PATCH 改 content 成功且 owner_id hijack→B 被剝除(仍=A、B 看不到);page_name lookup count=1mira idempotency 路徑);
POST 帶 caller owner_id=B→覆寫成 A。已部署 arcrun-cypher-executor(官方 58309bb9)。smoke 資料已清。
- [x] 9.7 **修 `arcrun_report_feedback` 死 routeHANDOFF §3b 連帶,9.4 漏網,2026-06-15**9.4 把
skills/examples 從舊 v3 `/blocks` 改打基本盤 `/entries`,但 `arcrun_report_feedback` 仍 POST 死掉的 `/blocks`
KBDB 基本盤只 mount entries/templates/records/recipe-stats,無 /blocks → 404 假紅)。改打 `/entries`
entry_type=agent-feedback、owner_id=用戶 namespace、source/api_key 併入 metadata_json、tags_json 不變)。
回傳 id 從基本盤 `{entry:{id}}` 取(兼容舊 `{id}`)。薄殼模式不變(kbdbFetch)。**mcp tsc exit 0** +
**端到端 prod 契約驗收綠**2026-06-15):確認舊 `/blocks`→**404**(正是原 bug、report_feedback 假紅根因);
用 9.7 實際送的 payloadentry_type=agent-feedback + metadata_json/tags_jsonPOST `/entries`→success、欄位保留;
經 cypher proxy 讀回 count=1。已部署 arcrun-mcp(官方 58309bb9)。注:MCP service-binding hop 由既有 kbdb_* 工具
9.1,同 kbdbFetch 路徑,2026-06-14 已驗)佐證 binding 活;本次只修死 URL。smoke 資料已清。
## Phase 10base 補 record PATCHmira-dissolve T2 `[→arcrun]`issue #6)—— 本次做
> 來源:頂層 SDD `docs/3-specs/mira-dissolve/`T2.1/T2.2),總管經本 repo GitHub issue #6 交辦。
> 用途:graph 插件的精耕「取代」語意(同來源檔重萃 → 舊版 deprecate)需要「改既有 record 的 slot 值」。
> **三表 append-only 不破**deprecate = 翻 record 的 slot 值(改底層 `entries.content`),不動表結構、不加欄、不刪 row。
> **base 不知 triplet(解耦鐵律 0.3grep-proven**:故 base 端 **不建 `/templates/triplet` 專屬端點**
> issue 描述的「PUT /templates/triplet 加 status/superseded_by」拆兩半(與頂層 tasks T2.2/T3.2 一致):
> base 只提供**通用** template 增改 slots 能力(既有 `PATCH /templates/:id` 已替換 slots_json,本就支援);
> triplet 專屬的 `status`/`superseded_by` 兩 slot 由 **T3.2 在 kbdb-graph-plugin 的 `TRIPLET_SLOTS` 補**(另一 repo,非本次)。
- [x] 10.1 **`PATCH /records/:id`(T2.1,前置硬依賴)**:改既有 record 的 slot→content 值。底層更新對應
`entries.content`(既有 slot 找 entry_value→entry 改 content);slot 不存在則補建 entry + entry_value(用 record 的
template 推算 slot 是否合法)。**三表結構一個欄位都不加**。`updateRecord` 在 record-crud.ts、route 在 records.ts。
- [x] 10.2 **template 增改 slots = 既有 `PATCH /templates/:id`T2.2base 端)**:核實 `updateTemplate` 已支援
`slots` 替換(record-crud.ts:44 既有)→ base 端通用能力已就緒,**無需新 code、不建 triplet 端點**。triplet 兩 slot
歸 T3.2plugin repo)。本條=核實 + 文檔釐清,非改 code。
- [ ] 10.3 **(選)`DELETE /records/:id`(T2.3)**:暫不做。依賴頂層 T8.3「死資料自動刪除原則」(design 待觀察未定);
且 deprecated record 查詢 `where status=active` 本就濾掉 → 無此端點不阻擋 deprecate 落地。待頂層拍板再補。
## Phase 11self-hosted KBDB 查詢能力補缺(issue #5,普世框架視角)—— 本次做
> 來源:issue #5(原 Mira dogfood 開,Mira 已蒸發 → 當「未來任何 self-hosted 用戶都會撞的框架缺口」處理)。
> 四點分流依 leo 2026-06-26 修正指示 + 頂層 mira-dissolve 重審:
> 規則判準見 07-thin-shell §3.5issue #4 自力救濟階梯):source/DELETE 屬**自家 API 缺能力 → 補 API**。
- [x] 11.1 **source 過濾(#5 第1點,✅ 做,普世成立)**`listEntries``source` filter
GET /entries 接 `?source=`。**零建表/零 migration**:用 SQLite `json_extract(metadata_json,'$.source')`
查既有 metadata_json TEXT 欄(**不加欄、表不變鐵律**)。cypher proxy `GET /kbdb/entries` 白名單加
`source`(隨租戶 owner_id 一起篩)。按來源篩 + 語義/關鍵字查詢都會用,ingest envelope 帶 source.uri。
kbdb+cypher tsc exit 0。**端到端待 leo21c 部署驗**(需寫一筆帶 metadata.source 的 entry → ?source= 篩回)。
- [ ] 11.2 ~~documents 聚合 GROUP BY page_name#5 第2點)~~ **不做**(舊河道頁特例):新架構「跨 vault 的圖」
走 graph MCP traverse/neighbors,不靠 KBDB 出 SQL 聚合端點。普世用戶也不需要。頂層 R6 已否決。
- [ ] 11.3 **cypher proxy DELETE#5 第3點)⏸ 暫擱置**:依賴頂層「死資料自動刪除原則」(mira-dissolve T8
待觀察未定)。等 T8 拍板再補。**註**:#6 已對 records proxy 同理擱置 DELETE(裸 delete-by-id 無 owner 檢查,
經 proxy 暴露=跨租戶刪除風險,補時要先讀 entry 驗 owner_id==本租戶才放行)。
- [→#7] 11.4 **embed-on-write#5 第4點)併入 #7**:開 embed module + `kbdb_embed:true` 後寫入是否自動
embed,由 #7 embed 模組定義(module 職責,前端不該手動戳 process-page)。不在 #5 單獨處理。
- [x] 11.5 **能力對照文件(#5 第二部分)**`docs/4-guides/kbdb-capabilities.md`
self-hosted arcrun-kbdb 現有查詢能力清單 + 端點對照。**不寫「documents/process-page 待移植」**
(舊河道視角,新架構不移植)。
## Phase 12optional embed 模組 + vectorize 開關 + 語義查詢(issue #7 / mira-dissolve T2.4)—— 本次做
> 來源:issue #7(總管,全包 4 件)+ 頂層 mira-dissolve T2.4 系列。普世框架能力(任何 self-host 用戶可選開語義查詢)。
> 鐵律:embedding 屬 **base optional 模組**(非 graph/ingest);CF Vectorize+AI binding 開/關,不拆 repo
> **不裝保持輕**(free-tier 友善);不對每個 block 地毯式 embed(精耕:只 embed 標 embed:true 的 entry)。
> 解鎖下游:ingest embed(第二階段)+ graph 詞+gloss 語義 normalizeT3.2c)等此。
- [x] 12.1 **base embed 模組(T2.4,從零做)**`kbdb/src/embed.ts`——`embedEnabled()`VECTORIZE+AI binding 都在才算開)、
`embedOnWrite()`(寫入時對標 `metadata_json.embed:true` 的 entry 做 Workers AI `@cf/baai/bge-base-en-v1.5`
768 維 → `VECTORIZE.upsert`,標 is_embedded=1**不動表結構**)、`semanticSearch()`query 向量 + metadata filter
owner_id/source)。**base 對內容語意無知**:用通用 `embed:true` flag 而非寫死 entry_type 白名單(不破解耦)。
wrangler.toml 加註解版 `[[vectorize]]+[ai]`deploy 開時取消註解)。
- [x] 12.2 **entries route 接 embed(含 #5 第4點 embed-on-write**POST/PATCH 用 `executionCtx.waitUntil`
fire-and-forget embed(模組開 + entry embeddable 才做,失敗不致命);DELETE 連帶刪向量(避孤兒);
**GET /entries/search 加 `mode=keyword|semantic`**semantic 需模組開,未開→**誠實降級 keyword + `capability_hint`
告知「叫 CC 幫開 vectorize」**(發現閉環,#7 第4點 + 不假綠)。kbdb tsc exit 0。
- [x] 12.3 **vectorize 開關從零做(T2.4c**`.arcrun.yaml`/config 加 `kbdb_embed`config.ts interface + env override
ARCRUN_KBDB_EMBED 布林);`deploy.ts``DeployContext.kbdbEmbed`:開時 `ensureVectorizeIndex`REST
`POST /vectorize/v2/indexes` dims=768/cosine,冪等)+ `injectWranglerConfig` 取消 kbdb toml 的 vectorize/ai 註解
**置於 stripOfficialOnlyBindings 之後**,否則 [ai] 被 strip 清掉——已驗);`acr init` 互動加問「要不要開語義查詢」
(預設關);存進 config 讓 acr update 維持一致。cli tsc exit 0。
- [x] 12.4 **KBDB MCP 加語義查詢(T2.4b**`kbdb_search``mode`/`source` 參數透傳 + 把 base 的 `capability_hint`
當 next-step 回給 AI(語義/關鍵字同一 KBDB MCP,D17 邊界)。薄殼模式不變(kbdbFetch)。mcp tsc exit 0。
- [x] 12.5 **CC 幫開 vectorizeT2.4d,第一版)**:路徑=CC 寫 config `kbdb_embed:true` + `acr update`(已接 kbdbEmbed
→ 建 index + 注入 binding redeploy)。base 查詢回應的 `capability_hint` 是發現入口。Pages 設定頁不做(leo 排未來)。
- [ ] 12.V **端到端驗收 ⏳ 待 leo21c 部署驗**(需官方/leo21c 帳號開 Vectorize index):開 kbdb_embed → acr update →
寫一筆帶 `metadata.embed:true` 的 entry → `?mode=semantic` 搜回;未開時 `?mode=semantic` 回 keyword+capability_hint。
本次只到 **tsc exit 0kbdb/cypher/cli/mcp 全綠)+ toml 注入 dry-run 驗證**,不假裝端到端綠(mindset §7)。
## 驗收
- [ ] V1 純 D1(無 Vectorize/AI)能 CRUD entries/templates/records + LIKE search
- [ ] V2 開 Vectorize 語義啟用、關掉降級 LIKE,API 不變
- [ ] V3 基礎 blocks/templates/records grep 無 vectorize/triplet import
- [ ] V4 self-hosted init 建 D1 成功、不綁卡
- [ ] V5 列出某專案下所有工作流可行
- [ ] V6 各 worker tsc exit 0
## Notes
- 插件模型對標 PostgreSQL:基礎核心=psql coreembed=PGVectortriplet=Apache AGE。
- embed 不拆 repoCF 內建、binding 開關);triplet 拆獨立 repoIP、action 多)。
- 官方與 self-hosted 共用同一基礎,差別只在 binding → 只維護一套。
- recipe 投稿入口是另一條線(registry),不在本 SDD。
@@ -0,0 +1,325 @@
# arcrun.dev Landing Page — SDD
> **目標**:給工程師一個門面,可以取得 API Key、管理 Key、探索 APISwagger),同時藉此獲得會員 Email。
> **原則**:先快速可用,不追求功能完整。榮譽牆、Python Lib 是後期。
---
## 0. 範圍(這份 SDD 涵蓋)
| 功能 | 說明 |
|---|---|
| 首頁 Hero | 說明 arcrun 是什麼,CTA 取得 API Key |
| OAuth 登入 | Google / GitHub(用自己的 auth recipe — dogfooding |
| API Key 管理 | 查看、Rotate、Revoke |
| Swagger UI | 嵌入 `/api`,讓工程師直接試打 |
| 榮譽牆 `/integrations` | 靜態骨架,先列 20 個 recipe,無動態數字 |
| 中英切換 | `?lang=zh` |
**不在本次範圍**Python lib、Donate 整合、Social Proof 即時數字、貢獻者排行。
---
## 1. 技術選型
### 1.1 框架:Next.jsApp Router
**選 Next.js 而非 Astro 的原因**
- `finally-click` 已有完整 Next.js + OAuth 回調實作,可直接複用模式
- API Key 管理頁有登入態保護需求,Next.js 的 middleware 最直接
- Cloudflare Pages 支援 Next.js`@cloudflare/next-on-pages`
- Astro 在動態路由保護上摩擦較多
### 1.2 部署:Cloudflare Pages
```
arcrun.dev → Cloudflare PagesNext.js
API calls → cypher.arcrun.dev(現有 Worker
```
### 1.3 儲存:現有 cypher-executor CREDENTIALS_KV + 新增 USERS_KV
現有 cypher-executor Worker 已有:
- `CREDENTIALS_KV``{api_key}:cred:{name}` 存 tenant credentials
- `RECIPES`auth recipes
新增需求:
- **USERS_KV**:存 user 帳號,key = `user:{provider}:{provider_user_id}`
- value: `{ email, display_name, api_key, created_at, provider }`
- **SESSIONS_KV**:存 login sessionkey = `sess:{session_id}`
- value: `{ api_key, email, expires_at }`
- TTL = 7 天
兩個 KV 都加到 cypher-executor `wrangler.toml`
### 1.4 OAuth — Dogfooding 自己的 Auth Recipe
登入用 arcrun 自己的 auth recipe
- 不是用 arcrun auth recipe 的 `http_request` runner 去打第三方
- 而是 **直接複用 recipe YAML 裡定義的 OAuth App 設定(client_id/secret**
- Worker 端實作 standard OAuth2 authorization_code flow
**支援提供商(MVP**
- Googlegoogle_drive recipe 的 OAuth App,或另建 arcrun-login Google App
- GitHubgithub recipe 的 OAuth App
登入 OAuth App 與 auth recipe 的 OAuth App **可以是同一個**(只要 scopes 包含 `openid profile email`),但更乾淨的做法是登入用獨立的 Google/GitHub App(只要 email scope),auth recipe 用的是資源存取 App。
**決策:登入用獨立 OAuth App**
- `GOOGLE_CLIENT_ID``GOOGLE_CLIENT_SECRET` — 只申請 `openid profile email`
- `GITHUB_CLIENT_ID``GITHUB_CLIENT_SECRET` — 只申請 `read:user` + `user:email`
- 以 Worker Secret 方式存入 cypher-executor
---
## 2. 頁面結構
```
arcrun.dev/
├── / 首頁(Hero + Code snippet + CTA
├── /login 登入頁(Google / GitHub 按鈕)
├── /auth/callback OAuth callbackPages Function
├── /dashboard API Key 管理(需登入)
├── /api Swagger UI(嵌入 swagger.json
└── /integrations 服務目錄(靜態,20 個 recipe)
```
---
## 3. 登入 / OAuth 流程
### 3.1 流程圖
```
用戶點「Google 登入」
→ GET /auth/google/startWorker 端)
→ redirect 到 Google OAuthstate = random, 存 SESSIONS_KV sess:state:{state} = {provider, redirect_back}
→ 用戶同意
→ GET /auth/callback?code=...&state=...Worker 端)
→ 驗 state
→ 用 code 換 access_tokenPOST google token endpoint
→ 用 token 取 userinfoGET google userinfo
→ upsert USERS_KV user:{provider}:{provider_id} = {email, display_name, api_key, ...}
→ 若新用戶:呼叫現有 /register?email=... 取得 arcrun API Key
→ 建立 sessionSESSIONS_KV sess:{session_id} = {api_key, email, ...}TTL=7d
→ Set-Cookie: arcrun_session={session_id}; HttpOnly; Secure; SameSite=Lax
→ redirect 到 /dashboard
```
### 3.2 「若新用戶取得 API Key」的邏輯
現有 `/register` endpoint 接受 `email` 回傳 `api_key`HMAC 確定性)。
但 landing page 需要的是**真正綁定到用戶帳號的 key**,且用戶可以 rotate/revoke。
**方案:延伸現有 register endpoint**
`/register` 目前:`HMAC(secret, email)` → 確定性 api_key,存 CREDENTIALS_KV
新增邏輯:
1. 若 USERS_KV 已有此 user → 直接用記錄裡的 api_key
2. 若新 user → 呼叫現有 `/register`(保持 HMAC 確定性邏輯)→ 拿到 api_key → 存入 USERS_KV
**好處**:不破壞現有 register 邏輯;登入後的 dashboard 顯示的 key = 現有 key = 封測用的 key。
### 3.3 Rotate / Revoke
- **Rotate**:產生新 UUID v4 key → 更新 USERS_KV 記錄 → 舊 key 失效(透過把新 key 加到 CREDENTIALS_KV,舊 key 的資料都跟著 API Key 命名空間走,所以 credentials 會留在舊 namespace
- 簡化版:Rotate 後顯示提示「您的 workflow credentials 已和舊 Key 分離,請重新設定」
- **Revoke**USERS_KV 記錄 `revoked: true` → Worker middleware 拒絕此 key
---
## 4. API 端點(新增到 cypher-executor
```
GET /auth/google/start → redirect 到 Google OAuth
GET /auth/github/start → redirect 到 GitHub OAuth
GET /auth/callback?code=&state= → 換 token、建立 session
POST /auth/logout → 清 session cookie
GET /me → 回傳當前登入用戶資訊(需 session cookie 或 API Key
PUT /me/api-key/rotate → 產生新 key
DELETE /me/api-key → Revoke(標記撤銷)
```
---
## 5. 前端頁面設計
### 5.1 首頁(/
```
Hero:
Stop fighting OAuth.
One API key. Every service. Works anywhere.
[Get API Key — Free] [View on GitHub]
Before/After:
40 行 OAuth 程式碼 → auth.bind("google_drive")
Code Demo(三個 tab):
Python / JavaScript / HTTPn8n 用戶)
[Get Free API Key] 按鈕
```
### 5.2 登入頁(/login
```
arcrun
登入或建立帳號
[Continue with Google]
[Continue with GitHub]
不需要信用卡。API Key 立即可用。
```
### 5.3 Dashboard/dashboard
```
歡迎,{display_name}
您的 API Key
┌────────────────────────────────┐
│ ak_xxxxxxxxxxxxxxxxxxxx [複製] │
└────────────────────────────────┘
[Rotate Key] [Revoke Key]
使用說明:
Authorization: Bearer {key}
或 X-Arcrun-API-Key: {key}
[登出]
```
### 5.4 Swagger UI/api
- 嵌入 `<SwaggerUIBundle>` JSCDN
- `url: 'https://cypher.arcrun.dev/swagger.json'`(現有 Worker 已有 `/docs` openapi endpoint
- 頂部說明:「這是 arcrun 的原始 API。Python / JS lib 是它的包裝,任何能發 HTTP request 的工具都能直接用。」
### 5.5 服務目錄(/integrations
- 靜態列出 20 個 auth recipe(從 seed data 產生)
- 每個 recipe:名稱、認證方式(static_key / service_account)、所需 credentials
- 「找不到你要的服務?開 PR 貢獻 Recipe」CTA
---
## 6. 檔案結構
```
arcrun/landing/ ← 新 Next.js 專案
├── app/
│ ├── layout.tsx
│ ├── page.tsx 首頁
│ ├── login/
│ │ └── page.tsx
│ ├── dashboard/
│ │ ├── page.tsx
│ │ └── middleware.ts (或 root middleware
│ ├── api-docs/
│ │ └── page.tsx Swagger UI
│ └── integrations/
│ └── page.tsx
├── middleware.ts 保護 /dashboard(讀 cookie
├── lib/
│ └── auth.ts session helpers
├── public/
├── next.config.ts
├── package.json
└── wrangler.toml CF Pages 設定
```
cypher-executor 新增:
```
arcrun/cypher-executor/src/routes/
├── auth.ts ← 新增(OAuth start/callback/logout/me
```
cypher-executor wrangler.toml 新增:
```toml
[[kv_namespaces]]
binding = "USERS_KV"
id = "<to be created>"
[[kv_namespaces]]
binding = "SESSIONS_KV"
id = "455d0505c7534883a4d4985ab8295857"
```
---
## 7. 環境變數 / Secrets
### cypher-executorWorker Secrets
```
GOOGLE_CLIENT_ID Google OAuth App client_id(僅 openid profile email scope
GOOGLE_CLIENT_SECRET Google OAuth App client_secret
GITHUB_CLIENT_ID GitHub OAuth App client_idread:user + user:email scope
GITHUB_CLIENT_SECRET GitHub OAuth App client_secret
SESSION_SECRET 隨機 32 bytes,用於 HMAC session ID(或直接用 UUID
```
### landingPages Environment Variables
```
NEXT_PUBLIC_API_BASE https://cypher.arcrun.dev
```
---
## 8. 實作步驟(Checklist
### Phase 1cypher-executor 後端擴充
- [x] `wrangler kv:namespace create USERS_KV` → 填入 wrangler.toml (id: 25bef01d079148919578894434d58c4d)
- [x] `wrangler kv:namespace create SESSIONS_KV` → 填入 wrangler.toml (id: 455d0505c7534883a4d4985ab8295857)
- [x] 建立 `arcrun/cypher-executor/src/routes/auth.ts`
- [x] GET `/auth/google/start`
- [x] GET `/auth/github/start`
- [x] GET `/auth/callback`(換 token → userinfo → upsert USERS_KV → 建 session → Set-Cookie → redirect
- [x] POST `/auth/logout`
- [x] GET `/me`(讀 session cookie 或 API Key header
- [x] PUT `/me/api-key/rotate`
- [x] DELETE `/me/api-key`revoke
- [x] 在 `src/index.ts` 掛載 `authRouter`
- [ ] `wrangler secret put GOOGLE_CLIENT_ID` 等 4 個 secrets ← **用戶需自建 Google/GitHub OAuth App**
- [x] `wrangler deploy` ← 已部署(Worker version 7877857b
### Phase 2Next.js Landing 專案
- [x] `npx create-next-app@latest arcrun/landing --typescript --tailwind --app`
- [x] 設定 `@cloudflare/next-on-pages`Next.js 15 + .npmrc legacy-peer-deps
- [ ] 建立 `middleware.ts`(保護 /dashboard,讀 `arcrun_session` cookie)← 待做
- [x] 首頁(`app/page.tsx`):Hero + Code Demo tab + CTA
- [x] 登入頁(`app/login/page.tsx`):Google / GitHub 按鈕(href 到 cypher.arcrun.dev/auth/google/start
- [x] Dashboard`app/dashboard/page.tsx`):顯示 API KeyRotate / Revoke 按鈕
- [x] Swagger UI`app/api-docs/page.tsx`):client component,動態 import swagger-ui CDN
- [x] 服務目錄(`app/integrations/page.tsx`):靜態,列 20 個 recipe
- [ ] 中英切換 ← 低優先,可延後
- [x] `wrangler pages deploy` → https://42a8d302.arcrun-landing.pages.dev
- [ ] Cloudflare dashboard 設定 arcrun.dev custom domain → arcrun-landing Pages project
### Phase 3:驗收(待 OAuth Secrets 填入後)
- [ ] Google / GitHub OAuth 完整流程(登入 → dashboard → 看到 key
- [ ] Rotate:新 key 出現
- [ ] Revoke:舊 key 的 API 呼叫回傳 401
- [ ] Swagger UI 正常載入,可試打 `/health`
- [x] `/integrations` 正確列出 20 個服務
---
## 9. 待決事項(開始實作前確認)
| 問題 | 預設決策 |
|---|---|
| Google OAuth App 是否要另建(只有 email scope? | 是,另建;auth recipe 的 App 不動 |
| Rotate 後舊 credentials 是否遷移? | 不遷移,顯示提示 |
| Domain arcrun.dev 是否已購入且在 Cloudflare | 假設是(wrangler.toml 有設 zone_name |
| 登入後 redirect 預設到 /dashboard | 是,可從 `?redirect=` 覆寫 |
@@ -0,0 +1,109 @@
# Design 補充:設定分層(env > 專案層 > 全域)+ init 非互動
> 2026-06-04 建立。`sdk-and-website/design.md` 的單檔補充(規則 02 §4.3 允許)。
> 來源:`docs/壓測報告.md` §1.2(方案 C+ §2.2(init 非互動)= 阻斷項 #7#8
> richblack 2026-06-04 授權:「#7#8 是同一問題,明顯發現的問題當然要解決,完成後就要推。」
---
## 1. 問題(壓測實測)
`acr` v1.1.0 把設定**寫死在全域唯一一份** `~/.arcrun/config.yaml``config.ts`
`CONFIG_DIR = join(homedir(), '.arcrun')`),且 `init` 只能 readline 互動。後果:
- **#8 多帳號**:同一台電腦只能一個 arcrun 身份 → 接案者 / 多公司 / 個人+公司混用做不到。
壓測者克難法是覆寫 `HOME`(需讀原始碼才找得到、不直覺)。
- **#7 非互動**init 只能 TTY 問答 → AI / CI 無法用 flag / env 完成。
兩者本質同一:**設定來源不該只有「全域單檔 + 強制 TTY」一條路**。
## 2. 設計(richblack 2026-06-04 拍板:三層全上 + init flag/env
### 2.1 設定分層優先序(仿 git config / Claude Code MCP
```
1. 環境變數 ARCRUN_* / CLOUDFLARE_* ← 最高,解 #7AI/CI 非互動)
2. 專案層設定 <就近往上找>/.arcrun.yaml ← 解 #8(接案多帳號),有 fallback
3. 全域設定 ~/.arcrun/config.yaml ← 平常自動用(自己的帳號)
```
- **就近往上找**:從 `process.cwd()` 往上層目錄逐層找 `.arcrun.yaml`,找到第一個即用(停在檔案系統根)。
→ 自己的專案不放檔 → 自動 fallback 全域;客戶資料夾放檔 → 只在該樹生效,離開自動切回。零心智負擔、不會忘記切換。
- **覆蓋是「欄位級 merge」**:高層只覆蓋它有提供的欄位,未提供的欄位 fallback 到低層。
(例:專案層只放 `cypher_executor_url`,其餘仍用全域。)
### 2.2 env 變數對應(最高層,欄位級覆蓋)
| env | 覆蓋 config 欄位 | 用途 |
|---|---|---|
| `ARCRUN_MODE` | `mode` | local/standard/self-hosted |
| `ARCRUN_API_KEY` | `api_key` | standard |
| `ARCRUN_ENCRYPTION_KEY` | `encryption_key` | standard/self-hosted |
| `ARCRUN_CYPHER_EXECUTOR_URL` | `cypher_executor_url` | self-hosted 指向自己的 cypher |
| `CLOUDFLARE_ACCOUNT_ID` | `cloudflare_account_id` | self-hosted(沿用 wrangler 慣用名)|
| `CLOUDFLARE_API_TOKEN` | `cf_api_token` | self-hosted(沿用 wrangler 慣用名)|
> CF 兩個用 `CLOUDFLARE_*` 而非 `ARCRUN_*`:與 wrangler / deploy.ts 既有慣例一致(deploy.ts 跑 wrangler 時就是設這兩個 env),CI 設一次兩邊通用。
### 2.3 init 非互動(flag > env > 互動問答)
`acr init --self-hosted` 取得 account-id / api-token 的順序:
1. **flag**`--account-id <id>` / `--api-token <token>`
2. **env**`CLOUDFLARE_ACCOUNT_ID` / `CLOUDFLARE_API_TOKEN`
3. **互動**:前兩者缺才 readline 問(保留現有 UX
> mindset §7 判準:帳號設定**不是**「暴露資料 / 建零件」類風險確認,是單純設定值,flag/env 合法、不違反「非 TTY 拒絕代人類確認」。
> 風險確認(exposure_consent 等)仍維持需人類明示,不在本次放寬範圍。
## 3. 實作(只動 cli/
| 檔案 | 動作 |
|---|---|
| `cli/src/lib/config.ts` | `loadConfig()` 改三層解析:全域 → merge 專案層 `findProjectConfig()` → merge env`applyEnvOverrides()`)。新增 `acr config --where` 用的 `resolveConfigSources()` |
| `cli/src/commands/init.ts` | `cmdInit` / `initSelfHosted``accountId`/`apiToken` options,缺才走 env 再 fallback 互動 |
| `cli/src/index.ts` | init 加 `--account-id <id>` / `--api-token <token>` option |
**`loadConfig()` 是唯一設定入口**(12 個指令全走它)→ 改它一處,全指令自動受益分層。
### 3.1 `acr config --where`(壓測 §1.2 建議 #3,避免用錯帳號)
新增輕量 `acr config` 指令,印出「現在這個資料夾正用哪個帳號 / 設定來自哪一層」:
```
mode: self-hosted(來源:專案層 /path/客戶A/.arcrun.yaml
cloudflare_account_id: abc...(來源:env CLOUDFLARE_ACCOUNT_ID
cypher_executor_url: https://...(來源:全域 ~/.arcrun/config.yaml
```
> 安全價值:部署前一眼確認「沒用錯帳號」。本次一併做(成本低、直接回應壓測痛點)。
### 3.2 安全:專案層 .arcrun.yaml 含憑證 → 必須 gitignore
專案層 `.arcrun.yaml` 可能含 `cf_api_token``createCredentialsYamlIfMissing()` 既有 gitignore 邏輯
擴充為一併忽略 `.arcrun.yaml`(壓測 §1.2 安全附帶發現:憑證進版控 = 帳號外洩)。
## 4. 驗收標準(客觀證據,mindset §7)
1. 專案資料夾放 `.arcrun.yaml``acr config --where` 顯示來源為該專案層;離開該樹 → 顯示全域。
2. `CLOUDFLARE_ACCOUNT_ID=x CLOUDFLARE_API_TOKEN=y acr init --self-hosted` → 不問互動直接跑(#7)。
3. `acr init --self-hosted --account-id x --api-token y` → 同上(flag 優先於 env)。
4. 三者皆缺 → fallback 互動問答(既有 UX 不破壞)。
5. env > 專案層 > 全域 的欄位級覆蓋:單元測試覆蓋三層 merge。
6. `npx tsc --noEmit` 全綠。
## 4.1 實作完成記錄(2026-06-04
全部 task 完成,客觀證據如下(mindset §7):
- [x] `config.ts` 三層解析:`loadConfig()` = 全域 → 專案層 → env 欄位級 merge;新增 `findProjectConfig()`(就近往上找)/ `resolveConfigSources()` / `activeProjectConfigPath()`
- [x] `init.ts``initSelfHosted` 收 flag/env,缺才互動;gitignore 一併排除 `.arcrun.yaml`
- [x] `index.ts`init 加 `--account-id`/`--api-token`;新增 `acr config [--where]` 指令
- [x] 新增 `commands/config.ts`token 遮罩、來源層標示)
- **驗收證據**
- 端對端測試(真實 fs 臨時目錄樹)8/8 通過:深層就近找專案層、未提供欄位 fallback、env 最高層覆蓋、來源層標示、離開專案樹回全域
- `acr init --help` 顯示 flag`acr config --where` 正確標來源 + token 遮罩 `secret_t…`
- `npx tsc --noEmit` 全綠
## 5. 為何不違反鐵律
- 只動 `cli/`,不碰零件 / cypher-executor 執行路徑 / Service Binding。
- flag/env 是帳號設定值,非風險確認(mindset §7 風險確認仍須人類明示)。
- 不新增頂層 SDD 目錄(本檔是 sdk-and-website 子系統內單檔補充,rule 02 §4.3)。
@@ -0,0 +1,281 @@
# Design Document: arcrun SDK Libraries + Website
## Overview
本設計涵蓋 arcrun 的三個新增交付物:
1. Python SDK lib`pip install arcrun`
2. JS/TS SDK lib`npm install arcrun``@arcrun/sdk`
3. arcrun.dev 網站完善(零件列表、recipe 列表、登入管理)
**設計原則:修改不重建。** SDK 是 `cypher.arcrun.dev` HTTP API 的 thin wrapper。不在 client 端重新實作任何 server 端已有的邏輯(workflow 執行、credential 注入、auth recipe 解析)。唯一在 client 做的是 AES-GCM 加密(因為 server 的 POST /credentials 期望收到加密後的 payload)。
---
## Architecture
### 系統關係圖
```
使用者程式碼
├── CLIacr → cypher.arcrun.devHTTP API
├── Python SDKarcrun → cypher.arcrun.devHTTP API
└── JS SDKarcrun / @arcrun/sdk → cypher.arcrun.devHTTP API
arcrun.dev 網站(Next.js / Cloudflare Pages
├── /login → /auth/google/start, /auth/github/startcypher.arcrun.dev
├── /dashboard → /me, /me/api-key/rotatecypher.arcrun.dev
├── /integrations → /auth-recipescypher.arcrun.dev
└── /components → /recipes + 靜態零件清單(embedded
cypher.arcrun.devCloudflare Worker — cypher-executor,不改)
├── POST /credentials ← 接收 { name, encrypted, iv }
├── GET /credentials ← 列出 credential 名稱
├── DELETE /credentials/:name ← 刪除 credential
├── GET /auth-recipes ← 列出 20 個 auth recipe
├── GET /auth-recipes/:service ← 單一 recipe 詳情
├── POST /webhooks/named ← 部署 workflow
├── POST /webhooks/named/:name/trigger ← 觸發 workflow
├── GET /webhooks/named ← 列出 workflow
├── POST /register ← 註冊取得 API Key
├── GET /me ← 當前用戶資訊
└── /auth/* ← OAuth 流程
```
---
## Python SDK`arcrun/python-sdk/`
### 目錄結構
```
arcrun/python-sdk/
├── pyproject.toml ← hatchling build, name="arcrun", deps=[httpx>=0.27, cryptography>=42]
├── README.md
└── arcrun/
├── __init__.py ← from .client import Arcrun
├── client.py ← Arcrun class(主入口)
├── crypto.py ← AES-GCM 加密(client 端,用 cryptography 套件)
├── creds.py ← CredentialsClientpush/list/delete
├── auth.py ← AuthClientsetup/bind/get_token/list_services
└── workflows.py ← WorkflowClientrun/push/list/delete
```
### API 設計
```python
from arcrun import Arcrun
# 建構 — api_key 從參數 > 環境變數 > ~/.arcrun/config.yaml 自動取得
client = Arcrun()
# 或明確指定
client = Arcrun(api_key="ak_xxx", encryption_key="hexstring")
# Auth:設定並綁定服務
client.auth.setup("openai", api_key="sk-xxx") # 加密 + 上傳
openai_client = client.auth.bind("openai") # 取回 pre-auth client
response = openai_client.get("/models") # httpx.Client
token = client.auth.get_token("openai") # raw token string
services = client.auth.list_services() # [{ service, display_name, ... }]
# Credentials:低階操作
client.creds.push("my_token", "value123")
names = client.creds.list()
client.creds.delete("my_token")
# Workflows
result = client.workflows.run("my-flow", {"email": "user@example.com"})
url = client.workflows.push("my-flow", graph_dict)
workflows = client.workflows.list()
```
### Credential 加密流程
```
setup("openai", api_key="sk-xxx")
1. GET /auth-recipes/openai → recipe(含 required_secrets, inject
2. 對應 required_secrets[0].key = "openai_api_key"
3. crypto.py 用 encryption_key AES-GCM 加密 "sk-xxx"
4. POST /credentials → { name: "openai_api_key", encrypted, iv }
5. 本地 _cred_cache["openai_api_key"] = "sk-xxx"(供 bind() 用)
bind("openai")
1. GET /auth-recipes/openai → recipe.inject.header = { Authorization: "Bearer {{secret.openai_api_key}}" }
2. 用 _cred_cache["openai_api_key"] 替換 template → "Bearer sk-xxx"
3. 回傳 AuthenticatedClient(base_url="https://api.openai.com/v1", headers={"Authorization": "Bearer sk-xxx"})
```
**注意**`bind()` 依賴 `setup()` 在同一 session 建立的 `_cred_cache`。跨 session 使用時(credential 已上傳但 cache 不存在),`bind()` 無法解析 template — 此時 `get_token()` 也無法返回值。**這是已知限制,封測期間先接受。** 長期解法是 server 提供 `/credentials/:name/secret` 解密端點(u6u-core/credentials 已有)。
### 關鍵差異:crypto.py 的定位
`crypto.py` 只做 **加密**encrypt),不做解密。
功能等同 `u6u-core/credentials/src/actions/crypto.ts``encrypt()` 函數。
解密只在 server 端發生(cypher-executor 的 `credential-injector.ts``u6u-core/credentials/getCredentialSecret.ts`)。
---
## JS/TS SDK`arcrun/js-sdk/`
### 目錄結構
```
arcrun/js-sdk/
├── package.json ← name TBDarcrun vs @arcrun/sdk),tsup build
├── tsconfig.json ← ES2020, NodeNext
└── src/
├── index.ts ← export class Arcrun
├── crypto.ts ← Web Crypto API AES-GCM encryptclient 端)
├── creds.ts ← CredentialsClientpush/list/delete
├── auth.ts ← AuthClientsetup/bind/getToken/listServices
└── workflows.ts ← WorkflowClientrun/push/list/delete
```
### API 與 Python SDK 對等
```typescript
import { Arcrun } from 'arcrun' // or '@arcrun/sdk'
const client = new Arcrun() // reads ARCRUN_API_KEY from env
await client.auth.setup('openai', { api_key: 'sk-xxx' })
const oai = await client.auth.bind('openai')
const models = await (await oai.get('/models')).json()
const token = await client.auth.getToken('openai')
const services = await client.auth.listServices()
await client.creds.push('my_token', 'value')
const names = await client.creds.list()
const result = await client.workflows.run('my-flow', { email: 'user@example.com' })
```
### Build 產物
```
dist/
├── index.js ← ESM
├── index.cjs ← CJS
├── index.d.ts ← TypeScript 型別
└── index.d.cts
```
### Crypto 實作
使用 Web Crypto API`crypto.subtle`),相容 Node 18+ / browsers / CF Workers / Deno
```typescript
async function encrypt(plaintext: string, hexKey: string): Promise<{ encrypted: string; iv: string }> {
const key = await crypto.subtle.importKey('raw', hexToBytes(hexKey), { name: 'AES-GCM' }, false, ['encrypt']);
const iv = crypto.getRandomValues(new Uint8Array(12));
const ciphertext = await crypto.subtle.encrypt({ name: 'AES-GCM', iv }, key, new TextEncoder().encode(plaintext));
return { encrypted: toBase64(ciphertext), iv: toBase64(iv.buffer) };
}
```
---
## arcrun.dev 網站
### 現有狀態(`arcrun/landing/`
已完成:
- [x] `/` — Hero + Code DemoPython/JS/HTTP tabs
- [x] `/login` — Google + GitHub OAuth 按鈕(前端 OK,需設 OAuth secrets
- [x] `/dashboard` — API Key 查看/Copy/Rotate/Revoke(依賴 `/me` API
- [x] `/integrations` — 20 個 recipe 靜態卡片
- [x] `/api-docs` — Swagger UI CDN 嵌入
- [x] `middleware.ts` — 保護 `/dashboard`(未登入 → `/login`
- [x] Cloudflare Pages 部署
待完成:
- [ ] OAuth secrets 設定(`GOOGLE_CLIENT_ID` / `GOOGLE_CLIENT_SECRET` / `GITHUB_CLIENT_ID` / `GITHUB_CLIENT_SECRET`
- [ ] `/components` 頁面(零件列表 — 21 個 WASM 零件的 input/output/config_example
- [ ] 首頁 code demo 更新為三種使用方式(CLI / Python / JS
- [ ] 登入流程真實驗證
### 新增頁面:`/components`
```
/components
├── 零件卡片(21 個)
│ ├── canonical_id
│ ├── display_name
│ ├── description
│ ├── input_schemarequired / optional 欄位)
│ ├── output_schema
│ ├── credentials_requiredif any
│ └── config_exampleYAML code block
└── 分類篩選(邏輯 / API / 控制流)
```
資料來源:靜態嵌入(從 `registry/components/*/component.contract.yaml` 在 build 時讀取),不依賴 runtime API。
### OAuth 設定(待 richblack 操作)
需要在 Cloudflare Worker 設定以下 secrets
```bash
wrangler secret put GOOGLE_CLIENT_ID --name arcrun-cypher-executor
wrangler secret put GOOGLE_CLIENT_SECRET --name arcrun-cypher-executor
wrangler secret put GITHUB_CLIENT_ID --name arcrun-cypher-executor
wrangler secret put GITHUB_CLIENT_SECRET --name arcrun-cypher-executor
wrangler secret put SESSION_SIGNING_SECRET --name arcrun-cypher-executor
```
---
## server 端需要的修改
### cypher-executor 修改(最小化)
目前 `POST /credentials` 端點(`routes/credentials.ts`)接收 `{ name, encrypted, iv }` 後直接存 KV。
SDK 需要的改動:
1. **`GET /auth-recipes` 回應格式**:目前 list 端點回 `{ recipes: [...] }` 但 recipe 的 `service` 欄位是 key — SDK 已在 list_services() 正確處理 ✅
2. **`GET /auth-recipes/:service` 回應格式**:目前回 `{ success: true, recipe: {...} }` — SDK 需讀 `body.recipe` 而非 body 本身 ✅
3. **`POST /credentials` 不需改動** — SDK 自己做 AES-GCM 加密後送 `{ name, encrypted, iv }`
4. **未來**:新增 `GET /credentials/:name/secret` 端點(解密返回 plaintext),讓跨 session 的 `bind()` 能工作。但此端點在 `u6u-core/credentials/src/actions/getCredentialSecret.ts` 已有實作 — 需要在 cypher-executor 整合或 Service Binding 到 u6u-credentials Worker。**封測後再做。**
---
## 不做的事(明確排除)
- ❌ 不在 SDK 裡做 workflow 解析或 YAML 處理 — 那是 CLI 的職責
- ❌ 不在 SDK 裡做 server-side 解密 — 解密只在 server 端
- ❌ 不建新的 credentials Worker — 用現有的
- ❌ 不建新的 KV namespace — 用現有的 CREDENTIALS_KV
- ❌ 不改 cypher-executor 的 credential-injector.ts — 那已經完成且測試通過
---
## 實作順序
```
Phase 1Python SDK 重建 + 測試
1.1 重建 arcrun/python-sdk/(按本 SDD 的結構)
1.2 修正上次的 bugrecipe 回應 wrapper、inject key "header" vs "headers"、secret key mapping
1.3 對 cypher.arcrun.dev live 測試全部 API
1.4 本地安裝測試(pip install -e .
Phase 2JS SDK 重建 + 測試
2.1 重建 arcrun/js-sdk/(按本 SDD 的結構)
2.2 同步修正 Python SDK 發現的所有 recipe 格式問題
2.3 buildtsup+ 本地測試
Phase 3arcrun.dev 網站補完
3.1 新增 /components 頁面
3.2 更新首頁 code demo(三種使用方式)
3.3 OAuth secrets 設定(需 richblack 操作 GCP / GitHub
3.4 登入流程驗證
Phase 4GitHub README + 發布
4.1 更新 arcrun/README.md — 三種 Quick Start
4.2 pip publisharcrun
4.3 npm publishTBD 套件名)
4.4 最終驗證:從零開始 pip install / npm install / 打 API
```
@@ -0,0 +1,132 @@
# DesignMCP 統一帳號來源 — 單一 remote MCP + .env 切 MCP URL
> 2026-06-06 richblack 拍板(推翻本檔初版的「①工具帶參數 / ②自架 worker / ③平台 routing」三方案)。
> `sdk-and-website/design.md` 的單檔補充(規則 02 §4.3 允許)。
> 來源:壓測報告 §5.1/§5.2/§5.4(薄殼原則)+ richblack 對話釐清。
> 對應鐵律:`.claude/rules/07-thin-shell.md` §4(統一帳號來源)。
---
## 1. 問題(壓測 §5.2
CLI 已能讀三層 configenv > 專案 `.arcrun.yaml` > 全域)切帳號;**MCP 不能**——MCP 用 Cloudflare
service binding 焊死平台 `arcrun-cypher-executor`self-hosted 用戶用 MCP 連不到自己的 cypher。
**這違反薄殼鐵律**:「切換帳號」這能力做在了 CLI(介面層),MCP 沒跟上 → 證明它不在 API/共用層。
## 2. 關鍵洞見(richblack):不需要兩套 transport
> 「self-hosted 用戶也有 CF,把 MCP 放上網對他也沒困難。接案時進客戶專案讀 `.env` 連的是
> 客戶專案的雲端 MCP — 這比保留 stdio + 雲端兩套更單純。」
推論:**所有人都用 remote Worker MCP,差別只在「連哪台 MCP」**。
- **我自己**:全域 config 的 `mcp_url` 指向我自己的 MCP Worker。
- **接案幫客戶**:客戶資料夾 `.arcrun.yaml` / `.env` 放客戶的 `mcp_url`(那台 MCP 綁客戶 cypher)。
進客戶資料夾 → 自動連客戶那套。
- **SaaS 用戶**:不設 `mcp_url` → fallback 平台預設 MCPAI 幫他帶 api_key。
→ **不需要 stdio 本機 MCP、不需要 transport 抽象層、不需要把 Worker 改成讀本機檔。**
現有 remote HTTP Worker MCP 形態完全保留。
## 3. 薄殼原則怎麼落地
「身份解析(讀哪個帳號/哪台 MCP)」本質在 client(帳號設定是本機檔,cypher/MCP Worker 讀不到)。
正解不是「API 去讀 .env」(做不到),而是:
> **MCP URL 與 cypher URL 一樣,由同一份 config 解析模組(env > 專案 > 全域)決定。**
> CLI 讀 `cypher_url`Claude Code 的 MCP 連線讀 `mcp_url`**同一份 `.arcrun.yaml` / env、同一個解析邏輯**。
> 「切換帳號」這能力只實作一次(config 解析),不再綁死在 CLI。
### 接線:mcp_url → Claude Code 的 MCP 設定
Claude Code 看 `.mcp.json`(專案層)決定連哪台 MCP。所以需要一個東西把
「arcrun config 解析出的 `mcp_url`」寫進專案 `.mcp.json`
- **`acr mcp-setup`**(新指令):依三層 config 解析出 `mcp_url`,在 cwd 寫 / 更新 `.mcp.json`
- `acr init` 順帶呼叫(裝好就有)。
- 「切帳號」= 在客戶資料夾跑 `acr mcp-setup`(讀該資料夾的 `.arcrun.yaml`)→ 產對的 `.mcp.json`
## 4. 動到的檔案
| 檔案 | 動作 | 狀態 |
|---|---|---|
| `cli/src/lib/config.ts` | `ArcrunConfig``mcp_url``ENV_MAP``ARCRUN_MCP_URL``resolveConfigSources` 含 mcp_url;新增 `DEFAULT_MCP_URL` + `getMcpUrl()` | ✅ 完成 |
| `cli/src/commands/mcp-setup.ts`(新增) | `acr mcp-setup`:依 `getMcpUrl()` 寫專案 `.mcp.json` | 進行中 |
| `cli/src/index.ts` | 註冊 `acr mcp-setup` | 進行中 |
| `cli/src/commands/init.ts` | init 尾端順帶 `acr mcp-setup`(裝好即有) | 進行中 |
| **`arcrun/mcp/`(新目錄)** | MCP 從 sibling repo `matrix/arcrun-mcp` **搬進主庫**(與 cli/ 並列)。形態不變(remote Worker),只是進主庫 → 同 repo、同 deploy 掃描、與 cypher API 對齊 | 進行中 |
| `mcp/wrangler.toml` | name/route 對齊 arcrun 部署慣例(`workers_dev=true`deploy.yml/local-deploy.sh 自動掃到) | 進行中 |
## 5. MCP 搬進 arcrun/mcp/(不改形態,只進主庫)
- **搬 verbatim**`src/tools/*``src/lib/*``src/types.ts``src/mcp-handler.ts``src/index.ts``tests/`
- **形態不變**:仍是 Hono + `WebStandardStreamableHTTPServerTransport` 的 remote Worker(不加 stdio)。
- **進主庫的理由**(a) 與 cypher-executor 同 repo → 改 API 時 MCP 薄殼同步可見、一起 review;
(b) 被 deploy 掃描(wrangler.toml)自動部署,納入 release.feature「推送=全部到位」;
(c) self-host 用戶 codeload 主庫即得 MCP,能部署自己的 MCP Worker。
- **sibling repo `matrix/arcrun-mcp` 去留**:搬進來後,原 sibling 為歷史/過渡;新開發在 `arcrun/mcp/`
## 5.5 self-hosted MCP 認證對齊(2026-06-14HANDOFF §3b
> 症狀(mira CC 實測 `arcrun-mcp.leo21c.workers.dev`):self-hosted 用 namespace 明碼連 MCP 一律 401
> CLL 全通。根因:MCP `middleware/partner-auth.ts` 把 Bearer 拿去 KBDB `/partners/:token/info`
> **驗證**namespace 明碼非註冊 partner → 401。而 cypher-executor 的 `X-Arcrun-API-Key`
> **不驗證、直接當分區 key**webhooks-named.ts triggerNamed)→ 這就是「CLI 通、MCP 401」的分歧。
**決策:① MCP self-hosted 繞 partner 驗證 + ② mcp-setup 寫 headers(兩者缺一不可)。**
| | 修法 | 為何必須 |
|---|---|---|
| ① | `partner-auth.ts``MULTI_TENANT === 'false'` 時 Bearer = namespace 明碼直接當 `org_namespace`,不打 KBDB partner 驗證(對齊 cypher 的 opaque-key 模型)。官方 SaaS(不設/`"true"`)行為不變 → 官方與 self-host 共用同一份程式碼 | 只做②也沒用:partner 驗證仍擋明碼 |
| ② | `mcp-setup.ts`:把 `config.api_key`self-hosted 存 namespace 明碼)寫進 `.mcp.json``headers.Authorization: Bearer …`(與 CLI 同一份身份,rule 07 §4) | 只做①也沒用:裸 `.mcp.json` 不送任何 header |
判定旗標:worker `[vars] MULTI_TENANT`(與 cypher 同名)。
### 5.5.1 部署注入修補(2026-06-15HANDOFF §3b-2
> 症狀:①②code 正確、官方帳號測綠,但 mira 推 leo21c 端到端**仍 401**。
> **根因(非 code bug,是部署注入缺口)**partner-auth.ts `if (c.env.MULTI_TENANT === 'false')`
> 邏輯對,但 worker env 裡 `MULTI_TENANT === undefined`——因為:
> - mcp/wrangler.toml 的 `MULTI_TENANT` 原本是**註解掉的**
> - `cli/src/lib/deploy.ts``injectWranglerConfig` 部署時注入了 KV id / WORKER_SUBDOMAIN / D1 id
> **但沒注入 MULTI_TENANT** → 部署後 `c.env.MULTI_TENANT===undefined ≠ 'false'` → 走 partner-key → 401。
> - config 源頭早有(init.ts `multi_tenant:false` + `mode:'self-hosted'`),只是沒被注進 worker。
>
> **只取消註解 mcp/wrangler.toml 不夠**——那只修「手動 fork」,沒修「acr update 自動部署」(mira 走後者)。
> 根因要修在 deploy.ts 注入邏輯。
**修法(方案①:注 vars 非 secret,符合 self-hosted 零填寫契約)**
| 檔案 | 動作 | 狀態 |
|---|---|---|
| `cli/src/lib/deploy.ts` | `DeployContext``selfHosted?`;新增 export `injectMultiTenant(toml)`(處理 active/註解/無行三態,加進 `[vars]`);`injectWranglerConfig``selfHosted` 時呼叫——與 WORKER_SUBDOMAIN/KV 注入同層級 | ✅ 完成 |
| `cli/src/commands/init.ts` | deployCtx 帶 `selfHosted: true`init 本就是 --self-hosted 分支) | ✅ 完成 |
| `cli/src/commands/update.ts` | ctx 帶 `selfHosted: config.mode==='self-hosted' \|\| config.multi_tenant===false`mira 走這條) | ✅ 完成 |
| `mcp/wrangler.toml` | `# [vars]`/`# MULTI_TENANT` 改 active `[vars]`(官方不含 MULTI_TENANT=多租戶;注入走 case-3 加行,結構正確在 [vars] 下) | ✅ 完成 |
**本地驗注入(dry-run,真實 export 函式)**mcp / cypher-executor 注入後各恰 1 行 active `MULTI_TENANT = "false"` 且在 active `[vars]` 之下 → ✓ PASS。cli tsc exit 0。
**端到端(交棒回 mira**mira 在 leo21c 重跑 `acr update` 重部 MCP worker(這次帶 MULTI_TENANT=false)→ `curl -H "Authorization: Bearer leo" https://<mcp>.leo21c.workers.dev/mcp`**200 非 401**。官方帳號測不到(不設 MULTI_TENANT)。
### 5.5.0 原始 code 修法(2026-06-14,①②)
| 檔案 | 動作 | 狀態 |
|---|---|---|
| `mcp/src/types.ts` | `Env``MULTI_TENANT?` | ✅ 完成 |
| `mcp/src/middleware/partner-auth.ts` | self-hosted 分支:Bearer 明碼直接當 org_namespace | ✅ 完成 |
| `cli/src/commands/mcp-setup.ts` | `.mcp.json``headers.Authorization` | ✅ 完成 |
驗收:mira 用 namespace 明碼連 self-hosted MCP(待 leo21c 部署後實測,HANDOFF §3);官方 SaaS MCP partner-key 路徑回歸不變。
## 6. 不在範圍(明確排除)
- ❌ 不加 stdio transportrichblack §2:不需要)。
- ❌ 不把 init / config 搬進 MCPinit 是本機 CF 部署動作,MCP Worker 做不到)。
- ❌ 不在 MCP 重實作 credential 加密 / workflow 執行(server 職責,rule 02 §3.4)。
## 7. 驗收(客觀證據,mindset §7)
1. `cli/` + `mcp/` 各自 `tsc --noEmit` exit 0。
2. `acr mcp-setup` 在含 `.arcrun.yaml`(mcp_url=X)的資料夾 → 產出 `.mcp.json` 指向 X;無 mcp_url → 指向 DEFAULT_MCP_URL。
3. `acr config --where` 顯示 mcp_url 來源層。
4. `mcp/` 被 deploy 掃描掃到(`find wrangler.toml` 命中)。
5. self-hosted 用戶在客戶資料夾 `acr mcp-setup` → Claude Code 連客戶 MCP(端到端待 richblack 實測)。
@@ -0,0 +1,131 @@
# Requirements: arcrun SDK Libraries + Website
## Introduction
arcrun 目前有三個使用介面:
1. **CLI**`acr` 指令)— 已完成,用 YAML 定義 workflow 並推送執行
2. **Python / JS SDK lib**(本次新增)— `pip install arcrun` / `npm install arcrun`,讓開發者在寫程式時直接用 arcrun 功能
3. **arcrun.dev 網站**(本次完成)— 登入取得 API Key、管理 Key、瀏覽零件 / recipe 列表
**核心原則**SDK lib 是 `cypher.arcrun.dev` HTTP API 的 thin wrapper。所有業務邏輯(加解密、credential 注入、workflow 執行)都在 server 端完成。Client 端不重做 server 已有的邏輯。
**現有基礎設施**(不重建,直接使用):
- `cypher.arcrun.dev`cypher-executor Workerworkflow 執行、credential 管理、auth recipe、webhook
- `u6u-core/credentials`credential WorkerAES-GCM 加解密)— arcrun/credentials 是其 cherry-pick
- `arcrun/cli`CLI 工具(已發布 npm `arcrun@1.1.0`
- `arcrun/landing`Next.js 前端(已部署 Cloudflare Pages,有 hero/login/dashboard/integrations 骨架)
---
## Glossary
- **SDK lib**Python / JS 套件,wrapping `cypher.arcrun.dev` HTTP API,安裝後可在程式碼中直接使用
- **auth.setup()**:上傳一個服務的 credential(如 Notion token、OpenAI API Key)到 arcrun
- **auth.bind()**:取回已設定服務的 pre-authenticated HTTP client
- **auth.get_token()**:取回某服務的 raw tokenescape hatch,給官方 SDK 用)
- **workflows.run()**:觸發已部署的 workflow
- **workflows.push()**:上傳 workflow 定義
- **Recipe**:描述「如何對某服務認證」的 YAML 設定,存在 RECIPES KV
---
## Requirements
### Requirement 1Python SDK`pip install arcrun`
**User Story:** As a Python 開發者, I want `pip install arcrun` 後在程式碼中使用 arcrun, so that 不用離開寫程式環境就能串接 20+ 服務。
#### Acceptance Criteria
1. THE Python SDK SHALL 以 `arcrun` 套件名發布到 PyPI,支援 Python 3.10+。
2. THE SDK SHALL 提供以下 API
- `Arcrun(api_key=, base_url=)` — 建構 clientapi_key 支援從環境變數 `ARCRUN_API_KEY``~/.arcrun/config.yaml` 自動讀取
- `client.health()` — 健康檢查
- `client.auth.list_services()` — 列出可用 auth recipe 服務
- `client.auth.setup(service, **kwargs)` — 上傳 credential
- `client.auth.bind(service)` — 取得 pre-authenticated HTTP client
- `client.auth.get_token(service)` — 取得 raw token
- `client.creds.push(name, value)` — 上傳加密 credential
- `client.creds.list()` — 列出 credential 名稱
- `client.creds.delete(name)` — 刪除 credential
- `client.workflows.run(name, input)` — 觸發 workflow
- `client.workflows.push(name, graph)` — 上傳 workflow
- `client.workflows.list()` — 列出已部署 workflow
3. THE SDK 的 credential 加密 SHALL 在 client 端完成(使用 `cryptography` 套件 AES-GCM),然後以 `POST /credentials` 上傳加密後的 `{ name, encrypted, iv }` 到 server。
4. THE `auth.bind()` SHALL 從 server 取得 auth recipe 的 inject template,在 client 端用 cache 的 plaintext 值填入,回傳 pre-configured `httpx.Client`
5. THE SDK SHALL 使用 `httpx` 做 HTTP clientasync 版使用 `httpx.AsyncClient`)。
6. THE SDK 位置 SHALL 為 `arcrun/python-sdk/`build 系統用 `hatchling``pyproject.toml`)。
---
### Requirement 2JavaScript/TypeScript SDK`npm install arcrun`
**User Story:** As a JS/TS 開發者, I want `npm install arcrun` 後在程式碼中使用 arcrun, so that 可以嵌入現有 Node.js / Deno / Cloudflare Workers 專案。
#### Acceptance Criteria
1. THE JS SDK SHALL 以 `arcrun` 套件名發布到 npm,提供 ESM + CJS 雙格式 + TypeScript 型別定義。
2. THE SDK SHALL 提供與 Python SDK 對等的 APIcamelCase 版):
- `new Arcrun({ apiKey?, baseUrl? })` — 讀 `process.env.ARCRUN_API_KEY`
- `client.health()` — 回傳 `Promise<unknown>`
- `client.auth.listServices()` / `setup()` / `bind()` / `getToken()`
- `client.creds.push()` / `list()` / `delete()`
- `client.workflows.run()` / `push()` / `list()` / `delete()`
3. THE SDK 的 credential 加密 SHALL 使用 Web Crypto API`crypto.subtle` AES-GCM),相容 Node 18+、browsers、Cloudflare Workers、Deno。
4. THE `auth.bind()` SHALL 回傳一個有 `get/post/put/delete/patch` 方法的 `AuthenticatedClient`base URL + auth headers 已配置。
5. THE SDK SHALL 使用原生 `fetch()` API,不依賴外部 HTTP client 套件。
6. THE SDK 位置 SHALL 為 `arcrun/js-sdk/`build 用 `tsup`ESM + CJS + DTS),`tsconfig.json` target ES2020 + NodeNext module。
7. THE JS SDK 套件名與 CLI 套件名衝突(都叫 `arcrun`),SHALL 使用 `@arcrun/sdk` 或由 richblack 決定套件名。
---
### Requirement 3arcrun.dev 網站完成
**User Story:** As a 潛在用戶, I want 在 arcrun.dev 上登入取得 API Key、瀏覽零件和 recipe 列表, so that 我可以評估 arcrun 是否符合需求並立即開始使用。
#### Acceptance Criteria
1. THE 網站 SHALL 在 `arcrun.dev` 提供以下頁面:
- `/` — 首頁 Hero + 三種使用方式(CLI / Python / JS
- `/login` — Google + GitHub OAuth 登入
- `/dashboard` — 登入後顯示 API Key(查看/Copy/Rotate/Revoke
- `/integrations` — 列出 20 個 auth recipe 服務,可按分類篩選
- `/components` — 列出所有零件(21 個 WASM 零件),顯示 input/output schema、config_example
- `/api-docs` — Swagger UI,可直接試打 API
2. THE 登入 SHALL 使用 Google + GitHub OAuth,流程走 `cypher.arcrun.dev``/auth/*` 端點。
3. THE 登入後 SHALL 自動對該 email 呼叫 `/register` 取得 API Key(若已有則取回現有 key)。
4. THE `/dashboard` SHALL 允許 Rotate(生成新 key)、Revoke(標記失效)、Copy to clipboard。
5. THE 網站 SHALL 部署在 Cloudflare Pages(現有 `arcrun/landing`),使用 Next.js App Router。
6. THE 首頁 code demo 區 SHALL 包含三個 tabPython、JavaScript、HTTP/curl,展示三種使用方式。
---
### Requirement 4GitHub README 更新
**User Story:** As a GitHub 訪客, I want README 清楚說明三種使用方式, so that 我能選擇最適合的方式開始用 arcrun。
#### Acceptance Criteria
1. THE `arcrun/README.md` SHALL 包含三種 Quick Start
- **CLI**`npm i -g arcrun && acr init && acr push workflow.yaml && acr run`
- **Python**`pip install arcrun && from arcrun import Arcrun && ...`
- **JavaScript**`npm install arcrun && import { Arcrun } from 'arcrun' && ...`
2. THE README SHALL 包含完整零件列表(21 個)和 auth recipe 列表(20 個服務)。
3. THE README SHALL 連結到 `arcrun.dev`(取得 API Key)和 Swagger UIAPI 文件)。
---
### Requirement 5SDK 發布
**User Story:** As a SDK 使用者, I want 公開安裝並直接使用, so that 不需要從原始碼 build。
#### Acceptance Criteria
1. THE Python SDK SHALL 發布到 PyPI`pip install arcrun` 可安裝。
2. THE JS SDK SHALL 發布到 npm`npm install arcrun`(或 `@arcrun/sdk`)可安裝。
3. THE 發布前 SHALL 完成以下測試(對 `cypher.arcrun.dev` live API):
- `health()`
- `auth.list_services()`
- `auth.setup()` + `auth.bind()` ✅(至少一個 static_key 服務如 openai
- `creds.push()` + `creds.list()`
- `workflows.list()`
@@ -0,0 +1,375 @@
# Design 補充:`acr init --self-hosted` 一鍵自動化(installer 模式)
> 2026-06-01 初稿 → 2026-06-02 定案改寫(richblack 拍板 installer 形態)。
> 本檔是 `sdk-and-website/design.md` 的單檔補充(規則 02 §4.3 允許)。
> **狀態:design 已與 richblack 對齊;實作前讀 §6 前置依賴。**
> 背景:戰法從 SaaS 轉 self-hosted 開源(docs/HANDOFF-self-host-harness.md §0)。
---
## 1. 定案形態(richblack 2026-06-02
**arcrun CLI = installer / orchestrator**(類似 rustup / nvm:工具本身小,按需從遠端拉真正內容)。
### 用戶只做 4 件事,中間什麼都不用懂:
1. 申請 CF 帳號
2. 安裝 CF CLI`wrangler`
3. 安裝 arcrun CLI`npm i -g arcrun`
4. `acr init --self-hosted`(貼 CF Account ID + API Token)→ **完成,其餘看機器跑**
### CLI 自動做(用戶無感):
- 驗 CF token 權限
- 建 7 個 KV namespace + 1 個 R2 bucket(冪等)
- **從 GitHub release 下載預編譯部署物**(含 24 個 `.wasm` + 各 Worker 的 wrangler.toml + cypher-executor/registry
- 把建好的 KV namespace id 注入各 wrangler.toml + cypher-executor 的 `WORKER_SUBDOMAIN`
- **`wrangler deploy` 部署全部 Worker**(用戶已裝 wrangler
- seed auth recipe + API recipe 進 RECIPES KV
- 寫回 `~/.arcrun/config.yaml`
- 印出「手動 `wrangler secret put ENCRYPTION_KEY` ×3」提示(secret 不自動化,rule 05
### 關鍵技術決策(richblack 2026-06-02
| 決策 | 選擇 | 理由 |
|---|---|---|
| 零件部署物 | **預編譯 `.wasm`**(不在用戶端 build)| 用戶不懂 tinygo、也不該懂。下載即用。 |
| 部署工具 | **wrangler**shell out| 用戶已裝 CF CLIself-host 本來就有上傳能力。CLI 不自己重寫 CF Script Upload API。 |
| 源碼來源 | **GitHub release tarball**(含預編譯 wasm)| 版本明確、不需用戶有 git、`acr update` 拉新 release 同一條路。 |
| 為何不是 git clone | repo **沒 commit `.wasm`**rule 05 build 產物不 commit)→ clone 拿不到 wasm | 必須走含 wasm 的 release artifact。 |
---
## 2. 為什麼是 installer 而非「repo 內掃 wrangler.toml」(推翻初稿)
初稿假設「用戶在 repo 內跑、CLI 掃 wrangler.toml」。**推翻**,因為:
- npm 全域裝的 `acr` 手上**沒有** 24 個 Worker 源碼。
- repo 沒 commit `.wasm`(已查證 `git ls-files .component-builds | grep .wasm` = 0)→ 連 clone 都拿不到可部署的 wasm。
- 用戶不該需要懂 git / tinygo / repo 結構。
→ 正解:CLI 當 installer,從 **GitHub release(含預編譯 wasm** 拉部署物到暫存目錄,在暫存目錄注入 KV id 後 `wrangler deploy`
---
## 3. 流程設計(`initSelfHosted` 改寫)
```
acr init --self-hosted
├─ 1. 問 2 輸入:CF Account ID + CF API Token
wrangler 是否已裝?which wrangler;沒裝 → 提示先裝 CF CLI 再來)
│ 驗 tokenCF API GET /accounts/{id}/tokens/verify + GET /accounts/{id}
│ 缺權限(Workers Scripts Edit / KV Edit / R2 Edit)→ exit 1 指出缺哪個 scope
├─ 2. 建資源(冪等:先 list 已存在就重用)
│ 7 KVWEBHOOKS / CREDENTIALS_KV / RECIPES / USERS_KV /
│ SESSIONS_KV / ANALYTICS_KV / EXEC_CONTEXTrule 01 資料儲存表)
│ 1 R2WASM_BUCKET
├─ 3. 下載部署物:GitHub release tarball → 解壓到暫存目錄 (~/.arcrun/.deploy-<ver>/)
│ 內含:cypher-executor/ + registry/ + .component-builds/*(每個含預編譯 component.wasm + wrangler.toml
├─ 4. 注入設定到暫存目錄的 wrangler.toml(不改用戶 repo,改暫存副本)
│ - 各 Worker 的 KV binding id ← step 2 建立的
│ - cypher-executor [vars] WORKER_SUBDOMAIN ← CF API GET /accounts/{id}/workers/subdomain
├─ 5. 部署:對暫存目錄每個含 wrangler.toml 的 dirshell out
`wrangler deploy`env CLOUDFLARE_API_TOKEN=<token>, CLOUDFLARE_ACCOUNT_ID=<id>
│ 分兩層:tier1 = .component-builds/*(先)→ tier2 = cypher-executor / registry(後)
│ 每個 wrangler.toml 已含 workers_dev = true → workers.dev URL 自動啟用
├─ 6. seed recipe 進 RECIPES KV(部署後打新 cypher URL,或直接 CF KV API 寫)
│ - auth recipe:重用 AUTH_RECIPE_SEEDScypher-executor/src/lib/auth-recipe-seeds.ts
│ - API recipe:新增 seed-api-recipes.ts(見 §5
├─ 7. 寫回 configmode: self-hosted + 所有 id + cypher_executor_url = 部署後 workers.dev URL
└─ 8. 印手動 secret 提示:
wrangler secret put ENCRYPTION_KEY --name arcrun-cypher-executor
wrangler secret put ENCRYPTION_KEY --name arcrun-auth-static-key
wrangler secret put ENCRYPTION_KEY --name arcrun-auth-service-account
(三 Worker 共用同一把 key,見 memory: encryption-key-drift-trap
```
### `acr update`(同一條路,未來新零件)
- 拉新 GitHub release → 解壓 → 注入既有 config 的 KV id → wrangler deploy 變動的 Worker。
- 第一期至少做到「重跑等效 init 的部署步驟」;diff-only 部署可後續優化。
---
## 4. 動到的檔案
| 檔案 | 動作 |
|---|---|
| `cli/src/commands/init.ts` | 改寫 `initSelfHosted()`line 105-131)為 installer 流程 |
| `cli/src/lib/cf-api.ts` | 擴充:KV namespace 建立/list、R2 bucket 建立、subdomain 查詢、token verify |
| 新增 `cli/src/lib/deploy.ts`(暫定)| 下載 release tarball + 解壓 + 注入 wrangler.toml + shell out wrangler deploy |
| 新增 `cli/src/commands/update.ts`(暫定)| `acr update`:拉新 release 重部署 |
| 新增 `cli/src/lib/api-recipe-seeds.ts` | API recipe **種子資料**installer 用;放 CLI 端,**不放 cypher-executor/src**——rule 02 §2.2 hook 擋 cypher-executor TS hard-code endpoint,且 seed 資料本就屬 installer 職責)|
| 新增 `cypher-executor/scripts/seed-api-recipes.ts` | seed **腳本**(給 prod 補灌用,import CLI 的種子資料;`scripts/` 不受 §2.2 hook 管)|
| `cli/src/index.ts` | 註冊 `acr update` 指令 |
**不動**cypher-executor 執行路徑、既有零件 wasm 源碼、config 讀取端(config.ts:52 已支援 self-hosted)。
---
## 5. API recipe seed(新增 seed-api-recipes.tsrichblack 2026-06-02 定)
codebase 只有 auth recipe seed。新增 `seed-api-recipes.ts`,把現役 API recipe hard-code 成種子。
### 現役 API recipe(從 prod KV 查得,2026-06-01
- `kbdb_get`+ create_block / patch_block / delete / ingest)→ auth_service: kbdb
- `gmail_send` → google_gmail_sa
- `google_sheets_append` / `google_sheets_read` → google_sheets_sa
- `telegram_send` → telegram
- `line_notify_send` → line_notify
### KBDB recipe 採 Supabase 模式(richblack 2026-06-02
- **KBDB 是 richblack 提供的服務**(跟 arcrun 一樣),採「基礎免費、大量收費」。
- KBDB recipe **進 seed**(展示能力 = 引子,Supabase 模式)。使用者要用 → 去 **arcrun 取統一 API Key**(已有 /register 入口),把 key 設成 credential。
- ⚠️ **FOLLOW-UP(交 KBDB 端)**:現役 endpoint 是 `kbdb.finally.click{{_path}}`。richblack:這是 KBDB 端要改的問題——KBDB 該用統一對外網址提供大家用,不是 finally.click。**seed 先照現況進;KBDB 端改網址後同步更新 seed。** 此事不擋 init 實作。
---
## 6. 部署物產製:commit wasm 進 repo + codeload tarballrichblack 2026-06-02 定案)
> 此節**取代初稿的「GitHub release artifact」構想**。richblack 拍板更輕的做法:
> 直接把預編譯 wasm commit 進 repoCLI 從 GitHub codeload tarball 拿。不需 release.yml 機制。
### 6.1 策略
- **repo 自帶可部署的 wasm**:刪 `.gitignore``*.wasm` 排除,commit 預編譯 wasm 進 repo。
→ repo 本身就是部署來源,CLI 直接拿、用戶用自己的 CF token deploy。
- **CLI 走 codeload tarball**`https://codeload.github.com/richblack/arcrun/tar.gz/{ref}`
ref = main 或 tag)。用戶不需 git、版本可控(tag)。`acr update` 拉新 ref。
- **理由**richblack):「我在我的 CF 能用 = 我已擁有 wasm;用戶指向我的 GitHub 取得 wasm
用他自己的 CF credential deploy。開源,看不看源碼不重要,體驗好最重要。」
### 6.2 ⚠️ 推翻既有鐵律(rule 05)— 需同步改規則
`.claude/rules/05-deploy-convention.md` 明文「`.component-builds/{name}/component.wasm` **不 commit 進 repo**
build 產物)」「Phase 1-3 暫時 commit 過,**之後會加 .gitignore 清理**」。
**本決策反向**commit wasm 進 repoself-host 需 repo 自帶可部署 wasm)。
**實作時必須同步改 rule 05 + .gitignore**,否則 pre-write hook / 規則與實作打架。
→ deploy.yml 的 CI rebuild 步驟仍保留(CI 部署 prod 時用最新 source rebuild,與 commit 的 wasm 不衝突;
commit 的 wasm 是給「self-host 用戶 + acr init」用的部署來源)。
### 6.3 只 commit 部署所需的 wasm(省空間)
- 實況(2026-06-02 查):`registry/components/*.wasm` 23 個(build 中間產物)+
`.component-builds/*/component.wasm` 22 個(部署物),共 **~50MB**。
- **部署只需 `.component-builds/*/component.wasm`**wrangler deploy 認這個)。
**只 commit `.component-builds/*/component.wasm`22 個),不必 commit registry 那 23 個**(省一半)。
`.gitignore` 改成:保留排除 `registry/components/**/*.wasm`(中間產物),只放行 `.component-builds/**/component.wasm`
- ⚠️ **誠實 trade-off**mindset §7):commit wasm 進 repo → 每次 wasm rebuild 都在 git 歷史累積二進位,
**repo 長期會膨脹**。可接受(self-host 體驗優先),但記錄此代價;未來若膨脹過劇,再考慮 release artifact / git-lfs。
### 6.3.1 「錯做成零件」的 3 個不 commitrichblack 2026-06-02
實際 commit 的是 **19 個正當零件**,不是 22。排除的 3 個:`claude_api` / `km_writer` / `kbdb_upsert_block`
- **原因(richblack 修正「待刪」說法)**:它們**不是 endpoint 薄殼,是把工作流硬塞進零件**(違反 DECISIONS §1)。
例:`kbdb_upsert_block` 的 upsert 邏輯應在 KBDB API 那邊(API 提供 upsert endpoint),零件只該驅動它;
現在卻把「GET 找→有則 PATCH 無則 POST」整段工作流塞進零件。本質是工作流/recipe,被錯做成零件。
- **為何「現在就不 commit」而非「先 commit 之後刪」**commit 二進位進 git 歷史後,即使日後 `git rm`
歷史裡仍永久殘留(repo 體積已被佔),除非 rewrite history(很麻煩)。**錯誤的東西不灌進永久歷史。**
- **落地**`.gitignore` 放行 22 個後**再排除這 3 個**(後出現規則勝出);`deploy.ts discoverWorkerDirs`
只部署「同時有 wrangler.toml + component.wasm」的目錄 → self-host 用戶 codeload 拿到的目錄缺這 3 個 wasm → 自然跳過。
- **後續**:這 3 個的降級(變回工作流/recipe)是 BACKLOG 既有待辦,本次不處理,但確保它們不進 self-host 部署來源。
### 6.4 CLI deploy 流程(deploy.ts downloadAndDeploy 補實作)
```
1. 下載 codeload tarballref 預設 mainacr update 可帶 tag)→ 解壓 ~/.arcrun/.deploy-<ref>/
2. 讀解壓出的 .component-builds/* + cypher-executor/ + registry/
3. 各 wrangler.toml 注入 ctx.kvNamespaceIds + cypher-executor WORKER_SUBDOMAIN
4. tier1=.component-builds/*(先)→ tier2=cypher-executor/registry(後)
每個 dirpnpm install(若有 lock)→ CLOUDFLARE_API_TOKEN=<用戶> wrangler deploy
5. 回 cypherExecutorUrl = https://arcrun-cypher-executor.<subdomain>.workers.dev
```
注意:tier2cypher-executor/registry)是 TSwrangler deploy 會在用戶端用內建 esbuild bundle
(不需額外工具,richblack 確認源碼可見不重要、體驗優先 → artifact 含 TS 源碼即可)。
### 6.5 實作順序
1. 改 `.gitignore`(放行 `.component-builds/**/component.wasm`+ commit 22 個 wasm。
2. 同步改 rule 05(記錄此決策推翻原慣例)。
3. 補實 `deploy.ts downloadAndDeploy`codeload 下載 + 注入 + wrangler deploy)。
4. **在 1-2 完成前,downloadAndDeploy 維持誠實 unimplemented,不假裝(mindset §7)。**
### 6.6 未來方向:零件按需安裝(richblack 2026-06-02,現在不做)
- 現在 `acr init --self-hosted` **全裝基礎零件**22 個一次部署)。簡單、夠用。
- **未來若零件數量真的變很多**,再思考「按需安裝」(只裝 workflow 實際用到的零件 / 用戶選裝)。
- **現在不做的理由**(DECISIONS 附錄「會不會累積成債」):零件目前少且未來絕大多數是 recipe
(不需 deploy)→ 為「零件爆量」做按需安裝基建 = 為不存在的規模做自動化 = 過度工程。
零件真的爆量再回頭做,屆時是「未來一次性處理的設計點」,現在不必焦慮。
---
## 7. 驗收標準(客觀證據,mindset §7)
1. richblack 用**全新 CF 帳號** + wrangler 已裝 + 一個 CF API Token 跑 `acr init --self-hosted`
→ 全程無手動建 KV / 無手動 clone / 無 tinygo / 無手動填 namespace id。
2. 跑完印 secret 提示,richblack 手動 `wrangler secret put ENCRYPTION_KEY` ×3。
3. `acr push` 一個含 http_request + 自建 recipe 的 workflow → trigger → **HTTP 2xx + execution trace**
4. 冪等:重跑 init 不重建已存在 KV / 不報錯。
5. `acr update` 拉新 codeload tarballtag)→ 重部署成功。
---
## 7.5 壓測修正(2026-06-04):fork 帳號裝不起來的四個阻斷項
> 來源:`docs/壓測報告.md`(壓測者:一個「完全不懂程式」的人 + AI 自架)。
> §3/§6 的 installer 流程在**非官方 CF 帳號**上實測**無法完成部署**。根因與修法如下,全部在本 SDD 範圍內(installer 補洞 + R2 dead storage 清除),不是新架構。
### 根因
各 worker 的 `wrangler.toml`repo 內)寫死了**只有 arcrun 官方帳號才有的綁定**:
- `[[routes]] zone_name = "arcrun.dev"`(每個零件 + cypher-executor)→ fork 沒有此 zone → `wrangler deploy` 找不到 zone 失敗(阻斷 #1
- `[[r2_buckets]]` + `[ai]`cypher-executor)→ fork 未必有 R2/AI(阻斷 #2
- R2 是 dead storage,卻因 CF R2 首次啟用**強制綁信用卡** → 違背「開源免費自架」(阻斷 #3
- R2 bucket 名 `WASM_BUCKET`(大寫+底線)違反 R2 命名規則 → 證明該碼從未成功跑過(阻斷 #4
### 修法(只動 `cli/`**不刪 repo 內 worker toml**
**關鍵判準**repo 內 toml 的 `[[routes]]` 是**官方 prod CI 部署**需要的(對外開放零件),直接刪會破壞官方部署。
正解是 `deploy.ts` 在**注入暫存副本**時 strip(§3 step 4 本就「不改用戶 repo,改暫存副本」),因為 deploy.ts 只在 self-hosted 路徑跑。
| 檔案 | 改動 |
|---|---|
| `cli/src/lib/deploy.ts` | 新增 `stripOfficialOnlyBindings()`:注入時移除 `[[routes]]`/`zone_name`/`[[r2_buckets]]`/`[ai]`。worker 靠 `workers_dev=true` 對外。移除 `REQUIRED_R2_BUCKET` |
| `cli/src/commands/init.ts` | 移除 `ensureR2Bucket` 呼叫 + `wasm_bucket` config + token 提示去掉「R2 Edit」+ 結果文案改「7 KV,無需綁卡」 |
| `cli/src/lib/cf-api.ts` | 移除 `ensureR2Bucket()` 方法 |
| `cli/src/lib/config.ts` | 移除 `wasm_bucket?` 欄位 |
| `cli/src/commands/validate.ts` | 錯誤文案「WASM_BUCKET 中找不到」→「registry 中找不到零件」(去命名誤導) |
→ self-hosted 改為**只需 Workers + KV**(皆免費額度、不綁卡),回歸開源免費承諾。
### 客觀證據(mindset §7
- `stripOfficialOnlyBindings()` 對真實 `auth_static_key`/`cypher-executor` toml 實測:routes/R2/AI 全 stripname/workers_dev/kv_namespaces/vars 全保留。
- `npx tsc --noEmit` CLI 全綠;`grep R2/WASM_BUCKET src/` 零殘留。
### 報告其他項(本次未動,記錄待辦)
- **#7 init 只能互動式**readline,無 flag/env)→ AI/CI 不友善。建議支援 `--account-id`/`--api-token` 或讀 env。**待 richblack 決定**。
- **#8 無多帳號/專案 scope**config 寫死全域 `~/.arcrun/config.yaml`)→ 接案者痛點。報告建議專案層 `.arcrun.yaml` 覆蓋全域(git config 模式)。**屬新功能(change),需 richblack 確認後另開 SDD**。
- **#5/#6 權限前置驗證 + 錯誤訊息**:移除 R2 後 R2 權限問題自然消失;KV 前置一次驗證可後續優化。
## 7.6 壓測修正(2026-06-06):seed 下沉成 API 行為(推翻 §5「seed 資料放 CLI」)
> 來源:壓測報告 §4.1seed 機制對所有 self-host 用戶都壞)+ §5.5(薄殼原則)。richblack 點名「薄殼原則寫入鐵律」。
> **此節推翻 §5「種子資料放 CLI 端」+ §8「只動 cli/」的範圍**——seed 改為 API 行為(rule 07)。
### 推翻什麼
§5 原寫「種子資料放 `cli/src/lib/api-recipe-seeds.ts`(installer 職責)」。壓測證明這是 §4.1 bug 的結構根因:
seed 邏輯寫在 CLi `init.ts`(迴圈 POST + `deployFullyOk` gate),registry 一個無關 worker 失敗就連坐讓 seed 永遠被跳過;且 init 從不 seed auth recipe。
### 改法(seed = API 能力,薄殼只呼叫一次)
| 檔案 | 改動 |
|---|---|
| 新增 `cypher-executor/src/routes/init-seed.ts` | `POST /init/seed`:一次灌 API recipe + auth recipe(冪等、直寫 KV、誠實計數)|
| 新增 `cypher-executor/src/lib/api-recipe-seeds.ts` | 種子資料**唯一真相移到 server**rule 02 §2.2 hook 對 `*-seeds.ts` 整類加例外,richblack 2026-06-06 授權)|
| 刪 `cli/src/lib/api-recipe-seeds.ts` | CLI 重複檔刪除(避免漂移)|
| `cypher-executor/scripts/seed-api-recipes.ts` | import 改指 server 端;備援路徑 |
| `cli/src/commands/init.ts` | `seedApiRecipes` 迴圈 → `callSeedEndpoint` 一次呼叫;移除 deployFullyOk gate |
| `cli/src/commands/update.ts` | 重部署後也呼叫 `/init/seed`;改重解析全部 KV id |
| `cli/src/lib/deploy.ts` | `REQUIRED_KV_NAMESPACES``SUBMISSIONS_KV`(修 registry 20/21,§4.1.1|
### 對 §8「為何不違反鐵律」的修正
§8 原寫「只動 cli/ + scripts/」。現在**也動 cypher-executor/src/**(新增 `/init/seed` route + 種子資料),
但**不違反 rule 02 §2.2**:seed 端點只做「讀內嵌種子資料 → 寫 KV」的 routing/資料搬運,不解密/不簽章/不展開 template;
種子的 endpoint 字串是資料宣告(`*-seeds.ts` 類豁免),真正 API 呼叫仍走零件路徑。符合薄殼原則(rule 07)。
## 7.7 壓測修正(2026-06-06):self-hosted 身份 = 明碼 namespace(不發 api_key
> 來源:壓測報告 §7.2seed 通了,但 creds push/push/runtime 全卡「缺少 api_key」)。
> richblack 拍板:self-hosted 不需要「祕密 api_key」,因為它只需要 namespace(分區標籤),不需要認證(你的 cypher 在你自己的 CF,無「別人」會冒用)。
### 根因
credential/部署/執行三條路徑全建在多租戶 `{api_key}:cred:{name}` 模型,但 self-hosted(單租戶)init 從不發 api_key →
`creds push`/`push` 硬要 api_key 而退出;runtime webhook trigger 也要 `X-Arcrun-API-Key` header(公開表單帶不了)。
### 設計(richblack 2026-06-06
**self-hosted 的「api_key」= 明碼 namespace,用戶在 `.env` 自填。工具不生成、不 hash、不外傳。**
- 兩欄(像 username/password,但都由用戶持有):
- `NAMESPACE=leo`(明碼分區標籤,當 KV 前綴;**非密碼**)
- `ENCRYPTION_KEY=<64+ hex>`(creds 加密金鑰,用戶自保管;須與 worker secret 一致)
- **誠實限制**mindset §7):namespace 明碼不提供認證。要防外部呼叫 → 對 webhook 加保護(mindset §6);arcrun 不做授權判斷(mindset §3)。
- **零分叉**SaaS 仍走 register 發的真 api_keyself-hosted 走 .env 的 NAMESPACE。**同一條 `api_key` 路徑、同一份 config 解析**,只是值來源不同。
### 改法
| 檔案 | 改動 |
|---|---|
| `cli/src/lib/config.ts` | `ENV_MAP``NAMESPACE`/`ARCRUN_NAMESPACE`/`ENCRYPTION_KEY` → 對應 `api_key`/`encryption_key`;新增 `.env` 自動載入(就近往上找,shell > .env,無 dotenv 依賴)|
| `cli/src/commands/creds.ts``push.ts` | self-hosted 缺值時,引導設 `.env` 的 NAMESPACE/ENCRYPTION_KEY(不再叫去 register|
| `cli/src/commands/init.ts` | self-hosted 結尾改提示「在 .env 設 NAMESPACE + ENCRYPTION_KEY」+「把同把 key wrangler secret put 進 worker;不想自己跑可明示同意我代設」|
| `cypher-executor/src/routes/webhooks-named.ts` | 新增 `POST /webhooks/named/:ns/:name/trigger`namespace 走 path,公開表單免 header);與 header 路徑共用 `triggerNamed`,不分叉 |
| `cli/src/commands/push.ts` | self-hosted 輸出 path-based 公開 webhook URL(免 header|
### 為何 namespace 進 URL path 安全
namespace 是明碼標籤非密碼,放 path 不洩漏任何祕密。真正的祕密是 `ENCRYPTION_KEY`(在 worker secret + 用戶 .env,永不進 URL/path)。要防外部濫用 webhook 是「webhook 保護層」職責,非 namespace。
## 7.8 壓測修正(2026-06-08):onboarding 四缺陷 → pip 式自我修復安裝(CHANGE,待 review
> 來源:Haiku 冷啟動壓測(test_arcrun/2/test_record.md+ richblack 觀察。
> **核心判準([[haiku-capable-is-design-goal]]**:Haiku 撞牆 = 介面缺陷,不是換強模型。
> 暴露的四個缺陷都是 onboarding/AI-UX,非功能 bug。**richblack 要的是系統級正解,不是暫時解法/外掛 doctor。**
### 缺陷清單(壓測實證)
| # | 缺陷 | 根因 | Haiku 實際行為(撞牆) |
|---|------|------|------|
| D1 | **安裝不偵測環境**(缺 node/wrangler/CF登入/D1 任一就卡) | init 假設前置齊備,缺了不自己補也不引導 | Haiku 遇 D1 不存在 → **跑去讀原始碼自己想辦法建**(非技術用戶災難) |
| D2 | **帳號 scopeAI 繞過 CLI 自己 curl 全域** | config 優先序**已實作**(專案>全域,config.ts:169-193),但 AI 不用 CLI 讀帳號、自己 curl 猜 → 打到全域 | Haiku curl 全域帳號 URL,非當前 project scope |
| D3 | **MCP 掛不進不提示重啟** | 安裝完 project scope MCP 顯示無法用,沒告訴用戶「MCP 要重啟 client 才載入」 | 用戶以為壞了 |
| D4 | (壓測舊版 1.3.0 無 D1 自動建;1.3.2 已修,但 D1↑ 的「偵測+冪等」更根本) | — | — |
### 正解:安裝流程內建偵測 + 自我修復 + 冪等(pip 式)—— richblack 2026-06-08 拍板
**`acr init`(安裝流程本身)= 像 pip:先偵測環境有什麼、版本對不對,才動手;缺什麼 Arcrun 自己負責裝。
重跑會檢查後說「什麼也沒動」(冪等)。不另做 doctor——偵測是正規安裝流程的一部分。**
1. **偵測先於動作**init 先檢查各前置(node / wrangler / CF 登入 / D1 / KV / migration+ 版本 →
缺的才裝/建、有的跳過、版本不對才升。**不是假設齊備直接動手**。
2. **Arcrun 負責裝它需要的**:缺什麼是 Arcrun 的工作,不丟給 AI/用戶自己想辦法(D1 不存在 → init 自己建+套 migration
非讓 AI 讀原始碼)。需人類授權的(建 CF 資源、暴露)仍停下請示(mindset §7 非 TTY 不偽造同意)。
3. **冪等可重跑**:重跑檢查後「什麼也沒動」(像 pip install 已裝),不報錯不重建。
4. **AI 無腦入口 = 一個 GitHub 連結**:AI 讀 README/連結就照著走,**不繞、不讀原始碼、不自己 curl**。
安裝指示集中在一處,AI 跟著做即可。
### 配套介面(讓 AI 不需也不該猜)
- **`acr whoami` / `acr config show`**(D2 修法):印「當前生效帳號 + scope 來源(project/global/env)」。
AI 無腦問 CLI 拿正確帳號,**不自己 curl**。同步加 **MCP `arcrun_whoami`**(薄殼一致,rule 07 §5)。
→ 治本 D2:不是 config 沒實作(已對),是 AI 該用工具讀、不該繞 CLI 猜帳號。
- **MCP 掛載後提示重啟**D3 修法):`acr init` / `acr mcp-setup` 寫完 `.mcp.json` 後,
明確印「⚠️ MCP 已設定,**請重啟 IDE/client 才會載入** project scope MCP」。掛不進時引導重啟,不讓用戶以為壞了。
### 範圍 / 分期(richblack 2026-06-09 授權 P0-P2 執行)
- [x] **P0**`acr init` 偵測 + 驗收(pip 式)。新增 `cli/src/lib/preflight.ts`
- `detectEnvironment()` 安裝前偵測 node/wrangler(缺=fatal 停下給補救指令,不假設齊備直接動手);
- `verifyInstall()` 裝完實查 CFKV/D1 listKv/listD1+ 打 cypher `/health` 確認真就緒,缺哪項報哪項 + 一鍵補裝(acr update 冪等)。
- `initSelfHosted` 開頭接 detectfatal 即 exit 1)、結尾接 verify(未就緒印補裝指引)。冪等沿用 ensureKv/ensureD1。cli tsc exit 0。**2026-06-09 完成**
- [x] **P1**`acr whoami``cli/src/commands/whoami.ts`,人讀 + `--json`,印 mode/帳號/連哪台/來源層,薄殼讀 resolveConfigSources+ MCP `arcrun_whoami``mcp/src/tools/arcrun_whoami.ts`,回報 orgNamespace + binding,與 CLI 對齊)。AI 問工具拿身份,不自己 curl 猜。cli+mcp tsc exit 0。**2026-06-09 完成**
- [x] **P2**MCP 掛載重啟提示(D3)。`cli/src/commands/mcp-setup.ts` 寫完 `.mcp.json` 後印「⚠ 請重啟 IDE/client 才會載入 project scope MCP」+ 信任工作區提醒。經 init 也會流到(init 呼叫 cmdMcpSetup)。**2026-06-09 完成**
- [~] **P3**README/GitHub 入口整理成「AI 讀了就照著裝」的單一指引(D 無腦入口)。**部分完成 2026-06-09**
- 新增 repo 根 `.env.example` 範本(CF Account/Token 基礎兩格 + NAMESPACE/ENCRYPTION_KEY + 服務 token 區,
每格上面白話說明「去哪申請、怎麼拿」,值留空。`.gitignore``!.env.example` 放行進 repo)。
- `llms.txt` step 3 改成「**你(AI)先 `cp .env.example .env`**,帶用戶填值(用戶只填「=」右邊)」——
把「建 .env 結構」從用戶身上挪到 AI,用戶只做貼值。step 4 補 init「安裝驗收」說明。
- 仍待:`arcrun.dev/llms.txt` 沒 servelanding/public 缺檔,404GitHub repo 內 llms.txt 正常)。
test/5 prompt 給 GitHub URL 故不阻擋;arcrun.dev serve 另排。
> **誠實**:偵測各前置 + 版本比對 + 冪等重跑工程量不小(跨 CLI 多步)。但「裝很久 + AI 一直繞」對非技術用戶
> 是致命體驗 → 這是 onboarding 必修不是優化。
>
> **2026-06-09 補(test_arcrun/4 壓測根因)**:D1/D2/D3 之上還有更致命的一層——壓測時 AI **整個沒跑 `acr init`**
> settings.local.json 沒 allow init),導致 D1/harness/MCP/slash-command 全沒裝(全掛在 cmdInit 內)。
> P0 的「偵測+驗收」讓「跑了 init 但環境半殘」能被看見並自癒;但「**根本沒跑 init**」要靠 P3README/入口
> 引導 AI「第一件事就是 acr init」)+ harness 的 mindset/CLAUDE.md 提醒。P3 後續補。
---
## 8. 為何不違反鐵律
- 只動 `cli/` + 新增 `cypher-executor/scripts/`(seed 腳本,非執行路徑業務邏輯)。
- 不在 `registry/components/` 寫 TS;不在 cypher-executor TS 實作 credential/auth/JWT。
- 不新增 Service Binding。
- secret 不進自動化(§3 step 8 手動)。
- 不重寫部署輪子(用 wrangler,不自寫 CF Script Upload)。
@@ -0,0 +1,170 @@
# Implementation Plan: arcrun SDK Libraries + Website
## Overview
按 Design 的四個 Phase 實作。原則:修改不重建,SDK 是 HTTP API thin wrapper,加密只在 client 做 encrypt(不做 decrypt)。
**前置依賴**:必須先完成 `credential-primitives-wasm/tasks.md` 的 Phase 0-3(核心合併 + WASM primitives),確認核心穩定後才開始建三個介面。
---
## Phase 0(前置):核心合併 + WASM 改寫
> 詳見 `.agents/specs/arcrun/credential-primitives-wasm/tasks.md`
>
> 摘要:
> - 合併 u6u-core → arcrun(搬 builtins、刪重複 credentials
> - credential-injector TS → auth_static_key / auth_service_account WASM
> - 刪除 component-loader 內建 API recipes TS
> - 驗證 20 個 auth recipe 正常運作
---
## Phase 1Python SDK
- [ ] 1. 建立 `arcrun/python-sdk/` 目錄
- [ ] 1.1 `pyproject.toml`name=arcrun, deps=[httpx>=0.27, cryptography>=42], build-system=hatchling
- [ ] 1.2 `arcrun/__init__.py``from .client import Arcrun`
- [ ] 1.3 `arcrun/crypto.py`AES-GCM encrypt only(使用 `cryptography` 套件)
- [ ] 1.4 `arcrun/creds.py`CredentialsClient — push(加密 + POST /credentials)、listGET /credentials)、delete
- [ ] 1.5 `arcrun/auth.py`AuthClient — setupfetch recipe → match secrets → encrypt → push)、bindfetch recipe → resolve headers from cache → return AuthenticatedClient)、get_token、list_services
- [ ] 1.6 `arcrun/workflows.py`WorkflowClient — runPOST /webhooks/named/{name}/trigger)、pushPOST /webhooks/named)、listGET /webhooks/named)、delete
- [ ] 1.7 `arcrun/client.py`Arcrun class — 讀 api_key / encryption_key 從 param > env > config.yaml
- [ ] 2. 修正上次已知的 bug
- [ ] 2.1 `_fetch_recipe()` 回應是 `{ success: true, recipe: {...} }`,需讀 `.recipe` 欄位
- [ ] 2.2 `inject` 下的 key 是 `header`singular),不是 `headers`
- [ ] 2.3 `required_secrets[].key` 是 prefixed(如 `openai_api_key`),setup() 的 kwargs alias 要能對應
- [ ] 2.4 `list_services()` 回應的 recipe 用 `service` 欄位(不是 `service_id`
- [ ] 3. 測試(對 cypher.arcrun.dev live API
- [ ] 3.1 `health()``{"ok": true}`
- [ ] 3.2 `auth.list_services()` → 20 個服務
- [ ] 3.3 `auth.setup("openai", api_key="sk-test-dummy")` → 成功
- [ ] 3.4 `auth.bind("openai")` → AuthenticatedClient with Authorization header
- [ ] 3.5 `auth.get_token("openai")` → "sk-test-dummy"
- [ ] 3.6 `creds.push("test_token", "value123")` → 成功
- [ ] 3.7 `creds.list()` → 含 "test_token"(注意 KV eventual consistency
- [ ] 3.8 `workflows.list()` → []
- [ ] 3.9 cleanup: `creds.delete("test_token")`
---
## Phase 2JS/TS SDK
- [ ] 4. 建立 `arcrun/js-sdk/` 目錄
- [ ] 4.1 `package.json`name TBDarcrun vs @arcrun/sdk),deps=devDeps onlytsup, typescript, @types/node
- [ ] 4.2 `tsconfig.json`ES2020, NodeNext
- [ ] 4.3 `src/crypto.ts`Web Crypto API AES-GCM encrypt only
- [ ] 4.4 `src/creds.ts`CredentialsClient — push/list/delete via fetch
- [ ] 4.5 `src/auth.ts`AuthClient — setup/bind/getToken/listServices
- [ ] 4.6 `src/workflows.ts`WorkflowClient — run/push/list/delete
- [ ] 4.7 `src/index.ts`export class Arcrun + re-exports
- [ ] 5. 同步修正(與 Python SDK 同樣的 recipe 格式問題)
- [ ] 5.1 `_fetchRecipe()``body.recipe`
- [ ] 5.2 inject key: `header` not `headers`
- [ ] 5.3 setup() secret key alias matching
- [ ] 5.4 listServices() 用 `service` 欄位
- [ ] 6. Build + 測試
- [ ] 6.1 `tsup` build → dist/index.js + dist/index.cjs + dist/index.d.ts
- [ ] 6.2 Node.js 腳本對 live API 測試(同 Python 測試項目)
---
## Phase 3arcrun.dev 網站
- [ ] 7. 新增 `/components` 頁面
- [ ] 7.1 從 `registry/components/*/component.contract.yaml` 讀取 21 個零件資料
- [ ] 7.2 卡片顯示:canonical_id, display_name, description, input required/optional, credentials_required, config_example
- [ ] 7.3 分類篩選:邏輯類 / API 類 / 控制流類
- [ ] 8. 更新首頁
- [ ] 8.1 Code demo tabs 改為 CLI / Python / JS 三個
- [ ] 8.2 CLI tab 展示 `acr init → acr push → acr run`
- [ ] 8.3 Python tab 展示 `pip install arcrun → Arcrun() → auth.setup → auth.bind`
- [ ] 8.4 JS tab 展示 `npm install arcrun → new Arcrun() → auth.setup → auth.bind`
- [ ] 9. OAuth 流程補完
- [ ] 9.1 確認 cypher-executor 的 `/auth/google/start``/auth/github/start``/auth/callback` 路由正確
- [ ] 9.2 提供 richblack OAuth secrets 設定指令清單
- [ ] 9.3 richblack 設定 secrets 後驗證登入流程
- [ ] 10. 部署
- [ ] 10.1 Cloudflare Pages build + deploy
- [ ] 10.2 驗證所有頁面可存取
---
## Phase 4README + 發布
- [ ] 11. 更新 `arcrun/README.md`
- [ ] 11.1 三種 Quick StartCLI / Python / JS
- [ ] 11.2 零件列表(21 個)
- [ ] 11.3 Auth Recipe 列表(20 個服務)
- [ ] 11.4 連結到 arcrun.dev 和 Swagger UI
- [ ] 12. 發布
- [ ] 12.1 Python SDK`pip install build && python -m build && twine upload dist/*`
- [ ] 12.2 JS SDK`npm run build && npm publish`
- [ ] 12.3 驗證:從零開始 `pip install arcrun` / `npm install arcrun` + hello world
---
## Phase 5acr init --self-hosted installer2026-06-02 新增)
> 定稿 design`self-hosted-init.md`。CLI = installer:建 KV/R2 + 拉預編譯 wasm + wrangler deploy + seed。
> 用戶只做:申請 CF 帳號 → 裝 wrangler → 裝 acr → acr init --self-hosted。其餘自動。
> 背景:戰法轉 self-hosted 開源(docs/HANDOFF-self-host-harness.md)。
- [x] 13.1 API recipe 種子 — **位置修正**:種子資料放 `cli/src/lib/api-recipe-seeds.ts`installer 用,避開 cypher §2.2 hook),seed 腳本 `cypher-executor/scripts/seed-api-recipes.ts`import 種子,給 prod 補灌)。10 個現役 recipekbdb_*/gmail_send/google_sheets_*/telegram_send/line_notify_send)。KBDB Supabase 模式進 seedfinally.click 是 KBDB 端 follow-up,已註於 api-recipe-seeds.ts
- [x] 13.2 `cli/src/lib/cf-api.ts` 新增 `CfAccountClient`verifyAccess / listKvNamespaces / ensureKvNamespace(冪等)/ ensureR2Bucket(冪等)/ getWorkersSubdomain
- [x] 13.3 `cli/src/commands/init.ts` `initSelfHosted()` 改寫:驗 token → 建 7 KV + R2 → 查 subdomain → downloadAndDeploy → 寫 config → seed(部署完成時)→ 印 secret 提示。誠實:部署未自動化時明說,不假綠
- [x] 13.4 `cli/src/lib/deploy.ts`REQUIRED_KV/R2/SECRET 常數 + wranglerAvailable() + **downloadAndDeploy 已補實**codeload tarball 下載 + 解壓 + discoverWorkerDirs 分 tier + injectWranglerConfig 注入 KV id/subdomain + runWranglerDeploy;部分失敗誠實收集回報,不假綠)
- [x] 13.5 `cli/src/commands/update.ts` + index.ts 註冊 `acr update`self-hosted 重部署,同走 downloadAndDeploy
- [x] 13.6 部署物產製:**改用 commit wasm 進 repo + codeload**(取代 release artifactrichblack 2026-06-02,§6
- `.gitignore` 否定規則放行 `.component-builds/**/component.wasm`(registry 中間產物仍排除)→ 已驗 git check-ignore
- rule 05 同步改(記錄推翻「wasm 不 commit」+ trade-off
- commit 22 個 `.component-builds/*/component.wasm` 進 repo
- [ ] 13.7 驗收:全新 CF 帳號跑 acr init --self-hosted 全自動;acr push workflow → trigger 2xx + trace**待 richblack 用第二帳號實測** + push 含 wasm 的 commit 到 GitHub 後 codeload 才拿得到)
- [x] 13.8 typecheckcli `tsc --noEmit` exit 0
## Phase 6:壓測四橫向問題修正(2026-06-06richblack 點名)
> 來源:`/Users/youlinhsieh/Documents/tech_projects/test_arcrun/docs/壓測報告.md`
> 四個結構性問題(非個別 bug):薄殼原則未成鐵律、CLI/MCP 不同步打不同帳號、deploy 未全推、表達不清。
- [x] 6.1 **薄殼鐵律成文 + hook 強制**:新增 `.claude/rules/07-thin-shell.md`(能力長在 API,介面只暴露);
`02-forbidden.md` 第五類 + `CLAUDE.md` 鐵律 8 + 索引;`pre-write-guard.sh` 規則 7.x
(擋 cli/src + arcrun-mcp/src 的 seedApiRecipes/seedAuthRecipes、upsert 拼裝、deployFullyOk gate)。
- [x] 6.2 **seed 下沉成 API 行為**(薄殼正例 + 修 §4.1 seed bug):
- 新增 `cypher-executor/src/routes/init-seed.ts``POST /init/seed` 一次灌 API recipe + auth recipe(冪等、直寫 KV、誠實計數),mount 進 index.ts
- 種子資料移到 server`cypher-executor/src/lib/api-recipe-seeds.ts`(唯一真相),刪 CLI 重複 `cli/src/lib/api-recipe-seeds.ts``seed-api-recipes.ts` 改 import server 端
- CLI `init.ts``seedApiRecipes` 迴圈 → `callSeedEndpoint` 薄殼一次呼叫;**移除 deployFullyOk gate**registry 失敗不再連坐害 seed 被跳過,§4.1 根因);auth recipe 現在一併 seed(§4.1.2 修)
- CLI `update.ts`:重部署後也呼叫 `/init/seed`(修 §4.1.3「update 不 seed 卻提示說會 seed」矛盾)+ 改為重解析「全部」KV id(修「只注入 2 個」風險)
- [x] 6.3 **hook seed 例外通用化**`*-seeds.ts` 整類豁免 endpoint/template 檢查(richblack 原則:不為單一零件改全域規則),未來新種子檔自動適用,永不再動 hook。
- [x] 6.4 **registry SUBMISSIONS_KV**(修 §2.6/#11「20/21」):加進 `REQUIRED_KV_NAMESPACES` → init/update 會建 + 注入 → registry 部署回 21/21 → 連帶 seed 不再被連坐(§4.1.1)。
- [x] 6.5 **Deploy 一致性(Gherkin + 把 npm publish 補進現行機制)**:誠實前提——GH Actions 2026-05-16 停用、公開 repo `.github/` 已移除,現行 deploy 走 `scripts/local-deploy.sh`。故:
- `tests/release.feature`(每個 publish target 場景,描述「該到位什麼」)
- `scripts/check-release.sh`(一眼看版本/部署狀態,已驗證抓出 CLI 1.2.0≠npm 1.1.0 漂移)
- **`scripts/local-deploy.sh` 第 6 段新增 CLI npm publish**cli/ 變動且 version bump → npm publish;同版跳過、未登入誠實標)。這是真正讓「推送=全部到位」的修法,根因是舊腳本只 wrangler deploy worker 從不 publish CLI。
- `.github/workflows/publish-cli.yml` 為預備檔(gitignored、不跑),日後重啟 GH Actions 可啟用。
- [x] 6.6 **表達不清 → README 同步現況**:移除 R2 殘留文案(權限表去掉 R2、init 輸出去掉 WASM_BUCKET、「無需綁卡」)、補 `--account-id/--api-token`/env 非互動、補多帳號 `.arcrun.yaml` + `acr config --where`
- [x] 6.7 客觀驗證:cypher-executor + cli 兩端 `tsc --noEmit` exit 0hook 7.x + *-seeds 例外 + 非 seed 仍擋 全部回歸測試通過。
- [x] 6.8 **MCP 帳號架構**richblack 2026-06-06 拍板「單一 remote MCP + .env 切 MCP URL」,推翻初版三方案):
- SDD `mcp-account-source.md` 重寫為實作版。**不加 stdio**self-hosted 用戶也有 CFMCP 一律 remote Worker;差別只在連哪台)。
- 共用 config 解析:`config.ts``mcp_url` 三層(env `ARCRUN_MCP_URL` > 專案 `.arcrun.yaml` > 全域)+ `getMcpUrl()` + `DEFAULT_MCP_URL`。「切換帳號」能力收斂到 config 解析(薄殼正解),不再綁 CLI。
- **MCP 搬進 `arcrun/mcp/`**(從 sibling repo verbatim 搬;wrangler name→`arcrun-mcp`+`workers_dev=true`;形態不變仍 remote Worker)。deploy 掃描已掃到、tsc 綠。
- **新增 `acr mcp-setup`**:依 `getMcpUrl()` 寫專案 `.mcp.json`remote http MCP)。init 順帶呼叫。接案進客戶資料夾跑一次 → Claude Code 連客戶 MCP。**實測 3 情境(fallback/專案覆蓋/env+ merge 保留既有 server 全通過**。
- `acr --version` 改從 package.json 動態讀(不再 hardcode,根治版本漂移)。
- [x] 6.10 **CLI 版本自動化**richblackdeploy 時自動昇版 + 留記錄,避免忘了改):`local-deploy.sh` deploy CLI 時若版本未 bump → 自動 `npm version patch` + prepend `cli/CHANGELOG.md`(含 commit subject)。已 bump 到 1.3.0 + 建 CHANGELOG.md。
- [ ] 6.9 驗收:richblack 跑 `scripts/local-deploy.sh`worker + MCP + CLI npm 一起);壓測者重跑 `acr init` → recipe/auth-recipe 不再空 + `.mcp.json` 自動產 → 表單→Google Sheets 端到端。
- [ ] 6.11 待 richblack 確認:MCP 搬進主庫後**對外正式網址**(現役 `studio.finally.click/mcp``DEFAULT_MCP_URL` 暫用此);sibling repo `matrix/arcrun-mcp` 去留(建議標歷史、新開發在 arcrun/mcp/)。
## Notes
- JS SDK 套件名需 richblack 決定(`arcrun` 已被 CLI 佔用 → 可能用 `@arcrun/sdk`
- OAuth secrets 設定需 richblack 手動操作(GCP Console + GitHub Settings
- `bind()` 跨 session 限制是已知的,封測期間先接受
- credential 加密用的 `encryption_key` 目前由 `/register` 回傳,`acr init` 自動存入 config
@@ -0,0 +1,218 @@
# Design: Component Gatekeeping(零件投稿真把關)
> 2026-05-29。實作 requirements.md 的 R1-R6。
---
## ⚠️ 方向修正(richblack 2026-05-30):投稿走 GitHub PR,廢 registry self-service
**零件投稿管道 = GitHub PR,不是 registry submit API。** 理由與影響見下;以下 §0-§9 的 registry submit 設計,
凡屬「self-service 投稿管道」者**作廢**,凡屬「把關邏輯」者**搬到 CI(PR check)跑**。
**為何改:** primitive 極少、未來絕大部分是 recipe → 新增零件是稀有低頻事件,不需 self-service 自動化管道。
PR 天然滿足每道閘門:
| 設計的閘門 | PR 怎麼天然滿足 |
|---|---|
| G0 人類閘門 | PR 必須有人 mergerichblack approve);AI 偽造不了 GitHub approve |
| 舉證「為何不是工作流」 | PR descriptionreview 時看 |
| G1 假零件 / G3 純WASI / G4 Gherkin / 覆蓋檢查 / 黃金向量 | **CIPR check)跑** —— CI 有 tinygo + 能 runtime 跑 wasm**繞開 CF Workers 不能 runtime 編譯 wasm 的 venue 牆** |
**§8 衝突釐清:** DECISIONS §8「不依賴 GitHub Actions」指**執行鏈路**init/push/run/recipe,常態高頻,
用戶機器+CF)。**零件投稿是稀有低頻、該由 PR 治理**,用 PR/CI 不違反 §8——反而更對(CI 能跑 wasm,
registry Worker 不能)。需在 DECISIONS 補這個區別(待 richblack 確認改穩定文件)。
**哪些作廢 / 哪些保留:**
- ❌ 作廢:registry submit API 當主投稿管道、四路(CLI/MCP/py/jsself-service 投稿、平台端 sandbox 重跑、`acr parts publish` 加人類閘門(投稿不走 CLI 了)。
- ✅ 保留並搬 CI:G1 假零件偵測邏輯(detectFakeComponent.ts)、G3 純WASIwasmImports.ts)、G4 Gherkin 真跑(CI 能跑 wasm)、B 覆蓋檢查、黃金向量人工核對。
- ✅ 已 commit 的 registry G0/G1/G3 程式碼**保留不刪**(無害,且 G1/G3 邏輯被 CI 複用),但 registry submit 不再是主管道。
- ✅ R5 本機 hook(擋 CC 直接造零件目錄)仍要 —— 它擋的是「繞過 PR 直接改 repo」,與 PR 管道互補。
**以下 §0-§9 為原 design,閱讀時套用上述修正。**
---
## 0. 架構總覽
### 0.0 範圍:把關跨公共庫 + self-hosted 私人庫(richblack 2026-05-29
把關**不是公共庫專屬**。每個 self-hosted 部署有自己的零件庫(自己的 registry Worker)。
**加入公共庫或任何 self-hosted 私人庫,都跑同一套把關鏈(G0-G6 + 本機 hook)。**
- 實作上天然成立:把關邏輯在 registry Worker code 裡,self-hosted 跑同一份 registry → 把關跟著走,不需公私庫分兩套。
- G0 人類閘門在 self-hosted 下,「人類」= 部署擁有者本人(防的是「他的 AI 自作主張把東西做成零件」,不是防他本人;他自己確認 + 舉證即可過)。
- 理由:self-hosted 一樣有「自用服務沒驗證就變零件」的風險,且私人庫零件之後可能貢獻回公共。
### 0.1 把關鏈
投稿零件的唯一入口是 registry Worker 的 submit。把關鏈(依序,任一失敗即退稿):
```
submit 請求(帶 wasm + contract + 人類確認憑證 + 舉證)
├─ G0 人類閘門(R4) ← 最先擋:沒人類確認 + 舉證 → 403
├─ G1 假零件偵測(R2 ← contract/原碼有外部 URL 或 http 子集 → 退稿指回正路
├─ G2 size_check(已有)
├─ G3 syscall_scan + 純WASIR3)← 擴充:只准 WASI preview1 + u6u host func 白名單
├─ G4 gherkin_testsR1 ← 真跑 WASMgiven→stdin→比對 then_contains
├─ G5 cold_startmock,標未實作)
└─ G6 runtime_compatmock,標未實作)
→ 全過 → 派 hash → 寫 KV
```
另一道獨立防線:**本機 hook**(R5),擋 CC 繞過 API 直接在 repo 造零件目錄。
---
## 1. G0 人類閘門(R4)— registry submit endpoint
### 1.1 請求格式增欄
submit 請求 body 增兩個欄位:
```ts
interface SubmitRequest {
wasm_base64: string;
contract: ComponentContract;
human_confirmation?: {
confirmed_by_human: true; // 必須為 literal true
reason_why_not_workflow: string; // 非空,AI 舉證
confirmed_at: string; // ISO timestamp
};
skip_acceptance?: boolean; // 既有:backfill 用(仍保留)
}
```
### 1.2 閘門邏輯
```
若 skip_acceptancebackfill 既有零件)→ 跳過 G0(這些是已驗、已部署的存量,不是新投稿)
否則(新投稿):
若無 human_confirmation 或 reason_why_not_workflow 空 → 403
"建零件需人類確認。請用 `acr parts publish`(會互動式問你),
並說明為何工作流做不到。預設假設工作流能做——先試工作流 / recipe。"
記錄 reason_why_not_workflow 進 KV metadata(軌跡可審)
```
### 1.3 四路收斂(CLI / MCP / Python / JS
- 它們建零件都呼叫 registry submit endpoint → G0 在 endpoint,自動四路通管。
- **CLI `acr parts publish`**:強制互動式 prompt 問人類「(1) 工作流為何做不到?(2) 確認要建零件?」,把答案組成 `human_confirmation` 送出。非互動環境(AI 直跑)`acr` 偵測 stdin 非 TTY → 拒絕並提示「需人類互動」。
- MCP / Python / JS lib:傳 `human_confirmation` 才能成功;它們的 SDK 文件註明此欄位需人類提供。
- **誠實限制**(寫進 mindset + 文件):AI 技術上能偽造 `confirmed_by_human:true`。靠 reason 留記錄 + mindset 明示「絕不代替人類確認建零件」+ 軌跡可審,讓偽造成明確越界,不聲稱不可能繞過。
## 2. G1 假零件偵測(R2
新增 `detectFakeComponent(contract, wasmBytes): string | null`
- (a) **外部 URL/domain**:掃 contract 的 description / input_schema / output_schema 文字,及 wasm binary 文字,比對 URL pattern`https?://`、常見 domain)。命中 → 退稿。
- (b) **http_request 子集**:若 contract 宣告只做「打固定 endpoint」(heuristicdescription 含「打/呼叫 ... API/endpoint」且 input 有 url-like 欄位且無實質邏輯運算),標記疑似。
- 退稿訊息:「偵測到疑似假零件(寫死 endpoint / http 子集)。這該是 API recipehttp_request + 固定設定)或工作流,不是零件。見 DECISIONS §1。」
- 排除:`auth_*` primitivecredential 後端,DECISIONS §3b 不適用假零件判準)、`http_request` 自己。
## 3. G3 純 WASI 把關(R3
擴充現有 `scanSyscalls`
- 現況:掃 `FORBIDDEN_SYSCALLS` 黑名單。
- 擴充:改為「import 白名單」——解析 wasm import section,確認所有 import module 只屬 `wasi_snapshot_preview1` + `u6u`host functions)。出現其他 module → 退稿(runtime 鎖定風險)。
- 實作:簡易 wasm import section 解析(不需完整 wasm parser,掃 import 段的 module name 字串)。
## 4. G4 Gherkin 真實作(R1)— **修訂(2026-05-29richblack review**
### 4.0 為何不能在 registry Worker 跑(原設計作廢)
原設計假設 registry Worker instantiate 投稿 wasm 跑 Gherkin。**此假設錯誤**
- **Cloudflare Workers 禁止 request-time 編譯 WASM**`new WebAssembly.Module(bytes)` / `WebAssembly.compile()` 只能 startup 用 bundle 的 moduleworkers-types 把 `Module` 標 abstract 正反映此限制)。registry 收到的是 runtime 投稿 bytes → 跑不了。
- DECISIONS §8:第一期**不依賴 GitHub Actions** → 也不能靠 CI 跑 Gherkin。
- 剩下唯一一致 venue = **投稿者本地機器**(有 tinygo + 能跑 wasm,與現有 build 流程同環境)。
### 4.1 正確設計:Gherkin 在投稿指令本地跑
零件投稿走一個**獨立 CLI 指令**(既有指令;「本地或公共都是投稿」):
1. 本地 `tinygo build`(或讀已 build 的 .wasm)。
2. **本地跑 Gherkin**:對每個 `gherkin_tests[]`,用 Node 的 WebAssembly + 同一份 wasi-shim instantiate wasmgiven→stdin→run→比對 then_contains。Node 環境能 runtime 編譯 wasm(不像 CF Workers)。
3. 任一 scenario 失敗 → 投稿指令本地就擋下,不送出。
4. 通過 → 把**測試結果隨投稿上傳**(見 4.2)。
`runGherkin.ts`(已寫,用 createWasiShim)邏輯正確,只是**執行 venue 從 registry Worker 改成 CLINode**。registry 端不再跑 Gherkin。
### 4.2 「平台看得到測試結果」(呼應 §3c:執行者不能驗證自己)
投稿 payload 帶 `gherkin_evidence`:每個 scenario 的 `{scenario, given, actual_stdout, passed}`
- registry 存進 KV metadata(軌跡可審)。
- 平台看得到**原始 stdout**,不是只看投稿者宣稱的「passed」。
- **誠實限制**(同人類閘門):本地跑 + 自報結果,AI 技術上能偽造 actual_stdout。靠軌跡可審 + mindset 明示 + 未來 §3c 的 test/relay(投稿走 relay 讓平台當下親跑,第一期後)補強。第一期是「本地跑 + 上傳證據 + 可審」,不聲稱不可繞過。
### 4.3 公私庫分流(投稿指令旗標)
- 預設投稿 → **私人庫**self-hosted 自己的 registry)。
- `-p` / `--public` → 推**公共庫**。
- 兩者都跑同一套把關(§0.0:跨公私庫同一套)。差別只在目標 registry。
### 4.4 registry 端對應
- registry submit 仍跑 G1(假零件)、G3(純WASI)——這兩個是**靜態掃描,不需執行 wasm,CF Worker 可跑**。
- G4 Gherkin 的執行移到 CLIregistry 收 `gherkin_evidence` 存證、可選做輕量一致性檢查(evidence 的 scenario 數與 contract.gherkin_tests 對得上、每個 passed=true),但**不重跑**(跑不了)。
- G5/G6cold_start/runtime_compat)維持 unimplemented_steps 標記。
## 4.5 信任模型與發佈風險(richblack 2026-05-29 定,重要——勿誤讀為零風險)
**Gherkin 全綠 ≠ 零件安全可發佈。** 把關能驗到什麼、不能驗到什麼,要說清楚:
### 把關各層各防什麼
| 層 | 防得了 | 防不了 |
|---|---|---|
| G4 Gherkin | 「零件對宣稱的 given 不 work」 | 「Gherkin 寫得避重就輕、只測會過的 case」——投稿者可寫不完整的測試,沒測到的 input 仍可能出錯/惡意 |
| G1 假零件 / G3 純WASI | 連外 / 非 WASI / 寫死 endpoint | 沙箱內的邏輯錯誤 |
| **純 WASI 沙箱(真正的發佈底氣)** | **框死零件能做的事**:只能 stdin→stdout,無網路 syscall、無檔案系統、無 host 能力(除白名單 host func)→ 就算邏輯有問題或惡意,傷害被沙箱框死 | 沙箱允許範圍內的錯誤輸出 |
| 市場機制(§1) | 長尾:別人用了發現不對 → 回報 → 修 | 投稿當下測不到的 |
**結論:發佈非零風險,是風險可控。** 主要安全保證是**純 WASI 沙箱框死能力**(不是 Gherkin)。Gherkin 驗行為契約、沙箱框死破壞力、市場補長尾,三者合起來讓「發佈陌生人的零件」風險可控到能接受。
### 信任層級(誰跑 Gherkin 決定可信度,§3c:執行者不能驗證自己)
- **第一期**:投稿者**本地跑 + evidence 上傳可審**。AI 技術上能偽造 actual_stdout(它控制本地環境)。**誠實標明:第一期 evidence 可造假**,靠純 WASI 沙箱 + 軌跡可審 + mindset 撐著。
- **未來**:平台**自己重編源碼 + 重跑 Gherkin**(投稿者無法造假「跑的結果」)。需要能 runtime 跑 wasm 的 venue——**非 CF Worker(不能 runtime 編譯)、非 CI(§8 不依賴)**——可能是公共庫專屬的 sandbox 服務。列未來,可能擋公共庫「完全可信發佈」。
- 再往後:§3c 的 test/relay(投稿走 relay 讓平台當下親跑)。
## 5. G5/G6 mock 標未實作(R3 誠實)
- cold_start / runtime_compat 保留 mock,但 **SandboxResult 增 `unimplemented_steps: string[]`**,回傳時明列 `["cold_start","runtime_compat"]`,submit 回應與文件明示「這兩步未實作、未真正驗證」。不回 `success:true` 假裝全綠——回 success 但附 unimplemented 清單。
## 5.5 黃金向量:人工核對 + B 覆蓋檢查(richblack 2026-05-30 定)
Claude.ai 建議用「黃金向量 + 把關自己重跑」自動驗收,防放水的 Gherkin。**價值保留,實作降級**,理由:
- **primitive 極少、未來絕大部分是 recipe**(人類閘門 + 工作流優先把零件擋在源頭)。現役 17 白名單 + cron/platform_crypto,未來極少新增。
- 「把關自動重跑向量」是為「零件大量增加」做的規模化基建——但零件不會大量增加 → 為不存在的規模做自動化 = 過度工程(DECISIONS 附錄「會不會累積成債」判準)。
- 且「把關自己重跑 wasm」撞 venue 牆(CF 不能 runtime 編譯 wasm,同 §4.0)→ 需平台端 sandbox(第一期沒有)。
**降級後做法:**
- **A 黃金向量 conformance → 人工核對**:黃金向量當「人類閘門時用 CC 核對 primitive 的對照表」。新增 primitive(極稀有)時,人類閘門已要你親自確認,那一刻用 CC 本地跑向量核對(本地有 tinygo + 能跑 wasm,繞開 venue 牆)、人工凍結。**不做機器自動重跑。**
- **B 覆蓋檢查 hook → 現在做(純靜態、不可造假、成本低、價值與零件數無關)**:靜態 parse contract`input_schema` 每個 required 欄位至少出現在一個 Gherkin given、`output_schema` 每個欄位至少被一個 then_contains 斷言。缺 → exit 2 指出漏哪個欄位。擋「只測 happy path、不碰宣告過的行為面」。
- **初始向量來源(信任根:寫向量≠寫實作)**:另起 session 從 contract 語義寫、不看實作原碼(primitive 語義客觀如 add(2,2)=4)。人工核對用,不急、新增 primitive 時逐個補,不必 21 個一次到位。
- 殘留(誠實):向量/覆蓋檢查擋不住「列出的 case 全對、沒列到的 case 錯」。交給「用」——出 bug 補進向量,永不 regress。是會長大的網,非設一次完美。
## 6. R5 白名單 + 本機 hook
- `registry/MVP_COMPONENTS.txt`:一行一個白名單 canonical_id(現役 22 個)。
- `pre-write-guard.sh` 增規則:寫入 `registry/components/{name}/...``{name}` 不在 MVP_COMPONENTS.txt → exit 2,訊息「新增零件需走 submit API 人類閘門,不可直接造 repo 目錄」。
- `pre-bash-guard.sh` 增規則:`mkdir .../registry/components/{白名單外}` → exit 2。
- `.ts` 偵測現有 hook 已做(rule 1.1)。
- **B 覆蓋檢查**5.5):可放 registry submit 的靜態驗收(不需跑 wasmCF 可跑)或 pre-write hook,擋宣告過的欄位沒被 Gherkin 測到。
## 7. 範圍邊界
- **動 registry TS**sandboxAcceptance / submitComponent / routes / types+ **CLI**acr parts publish,既有指令)+ **hook**
- 不動 cypher-executor 執行路徑、不動既有零件 wasm。
- backfill 路徑(skip_acceptance)保持可用,不被新閘門擋(存量零件不需人類閘門)。
- CLI/MCP/Python/JS 四路:本期至少做 CLI `acr parts publish` + registry endpoint 強制;MCP/Python/JS 補 `human_confirmation` 欄位支援(薄)。
## 8. 驗收標準
- 投一個寫死 endpoint 的假零件 → G1 退稿(終端輸出)。
- 投一個 `.ts` 進 registry/components → hook exit 2。
- 投一個白名單外的新零件目錄(本機造)→ hook exit 2。
- 無 human_confirmation 的 submit → 403。
- 帶 human_confirmation + 過 Gherkin 的真零件 → 通過、寫 KV、reason 留 metadata。
- Gherkin given/then 對真零件跑綠;故意改壞 then_contains → 退稿。
- cold_start/runtime_compat 在回應裡列入 unimplemented_steps(不假綠)。
## 9. 決議(richblack 2026-05-29 design review 定)
- **Q1 → 消解**:Gherkin 測的零件**永遠是封閉邏輯(框架),不連外**。任何要加外部 URL 的東西按定義就是 recipe,不是零件——這種「連外零件」根本不該存在(會被 G1 假零件偵測擋下、降成 recipe)。所以 G4 Gherkin 只跑不需 host function 的封閉邏輯零件,**不需要 mock host func、不需要 skip 機制**。零件用 `u6u.http_request` 連外 = G1 直接退稿。
- **Q2 → 兩者都硬擋**(a) contract/原碼有具體外部 URL/domain → 硬退稿;(b) 宣告能力是 http_request 子集 → 也硬退稿。理由:與 Q1 一致——零件不該連外,這兩個 pattern 都是「該是 recipe 的東西偽裝成零件」,硬擋無誤殺顧慮(真的要連外就去做 recipe)。
- **Q3 → submit 過閘門後自動 append**:人類閘門通過 + 驗收綠的零件,submit 成功時自動把 canonical_id append 進 `MVP_COMPONENTS.txt`。白名單反映「已正當投稿的零件」,不需手動維護。本機 hook 讀此檔擋「白名單外的直接造目錄」。
### Q1 連帶結論(強化 G1
既然「零件不連外、連外即 recipe」是硬規則,G1 假零件偵測 = G4 Gherkin 的前置守門:
G1 擋掉所有連外/http 子集的投稿 → 能進到 G4 的必然是封閉邏輯零件 → Gherkin 必然不需 host func。
兩道閘門邏輯自洽。
@@ -0,0 +1,105 @@
# Design 補充:recipe 入庫把關(push 那一刻)
> 2026-06-01。本檔是 `component-gatekeeping/design.md` 的單檔補充(規則 02 §4.3 允許)。
> **狀態:待 richblack review 才動 code(這是 change)。**
> 背景:richblack 2026-06-01 方向修正(見下「方向定調」)+ docs/HANDOFF-self-host-harness.md。
---
## 0. 方向定調(richblack 2026-06-01
把關的對象與位置整個移位了,先講清楚才不會做歪:
### 0.1 零件這條路 = 封鎖,且「不再有假零件這件事」
- 零件由維護者(richblack)管理,**CC 不能自製/修改零件**。
- 封鎖機制 = **零件投稿走 GitHub PR + 人 merge**DECISIONS §8 / design 頂部方向修正)。
AI 偽造不了 GitHub approve,這是天然人類閘門。CC 在本機產不出能進庫的零件。
- → 因此「擋假零件」(原 W1)這件事**不存在了**:CC 根本造不出零件,workflow 引用 recipe
(如 `component: kbdb_get`)是**合法且未來唯一的擴充方式**,不該被當假零件擋。
### 0.2 零件 PR 把關 = 人工,不自動化(除非未來爆量)
- richblack 2026-06-01 澄清 BACKLOG 步驟5「不做 hook/自動化」的真意:
**零件真實數量很少、絕大多數是 recipe** → 原本想做的「驗證零件 PR 的自動化機制」
CI 跑 Gherkin/沙箱/向量)**不需要**,量少 → **有 PR 進來就人工檢查**
**只有零件開發量變很大時**才回頭想自動化。
- → component-gatekeeping 的 G4/覆蓋檢查/黃金向量自動化 = **不做**(人工取代),與既有 tasks 收尾一致。
### 0.3 真正的 harness 把關 = recipe 入庫(push)那一刻
CC 唯一能擴充的是 recipe。recipe 一律用「**推(push)**」,**自有庫與公共庫同一套指令**。
把關依庫別分強度:
| 庫別 | 能做到的把關 | 機制 |
|---|---|---|
| **自有庫(self-hosted** | 只能**提醒**(無法在別人機器強制) | (1) 資料外流提醒 (2) 打通檢查 |
| **公共庫** | 維護者機制**檢核實際打通、真收到成功回傳** | PR/CI relayDECISIONS §3c,第一期後)|
---
## 1. 自有庫 push 把關(self-hosted,第一期做)
`acr recipe push` 的兩個提醒。**提醒級 = 告知 + 需人類明示同意,不硬擋**(self-hosted 是用戶自己的庫,
他同意後就是他的責任 — mindset §6 / data-exfil-warning 既有原則)。
### 1.1 資料外流提醒(W2.2
- **觸發**push 的 recipe / 或部署的 workflow 會讓「資料對外可見」——主要是產生**對外可被呼叫的 webhook**
`POST /webhooks/named/...` 對外 trigger URL),或 recipe 把本地資料 POST 到外部服務。
- **行為**:CLI 印明確警示「這個動作會讓 X 對外界可見/可呼叫,確認要繼續嗎?」→ 需人類明示同意(y/N)。
非 TTY(AI 直跑)→ 拒絕,提示「需人類確認」(mindset §7:絕不代替人類做暴露確認)。
- **與既有 data-exfil-warning 的關係**:已有 API 層 + pre-bash hookcommit 51d40ee 等)。
本項確認**涵蓋 recipe push 這條路徑**;若已涵蓋則只補文件,若沒涵蓋則補上 push 路徑的提醒。
- **誠實限制**AI 技術上能偽造 exposure_consent。價值是法律歸責 + 軌跡可審,不聲稱不可繞過(mindset §7)。
### 1.2 打通檢查(W2.3
- **目的**recipe 是「指向外部 API 的指針」,正確性一半在「打不打得通」(DECISIONS §1 recipe 驗收標準 = 2xx)。
- **行為**push 時(或 push 後)對 recipe 的 endpoint **實打一次**,回報 HTTP status。
- 2xx → 「✓ recipe 打通(HTTP 200)」
- 4xx/5xx → 「⚠️ recipe 未打通(HTTP 401/404/...)」+ 誠實標原因(如「缺 credential → 先 acr creds push」)
- 連不上 → 「⚠️ 無法連線」
- **self-hosted 是提醒級**:打不通**不硬擋 push**(用戶可能就是要先 push 再設 credential),只如實回報。
- **誠實**mindset §7):缺 credential 打不到 2xx 就誠實標「未驗收:缺 X」,不 mock 充綠燈。
### 1.3 動到的檔案(待 review 後)
| 檔案 | 動作 |
|---|---|
| `cli/src/commands/recipe.ts` | push 流程加 (1) 資料外流提醒 prompt (2) 打通檢查(實打 endpoint 回報 status |
| `.claude/hooks/*`(如需)| 確認 data-exfil pre-bash hook 涵蓋 recipe push;缺則補 |
**不動**cypher-executor 執行路徑、零件、credential 解密邏輯。
---
## 2. 公共庫 push 把關(第一期後)
- recipe 進公共庫 = 別人會用 → 需維護者機制檢核「**實際打通、真收到成功回傳**」(不是投稿者自報)。
- 機制:DECISIONS §3c 的 **test/relay**——push 公共庫走 relay,維護者當下親見真實打通記錄
(執行者不能驗證自己,§7 閉環)。
- **範圍**:依賴公共庫 + relay 基建,**第一期不做**(第一期是 self-hosted + 提醒級)。
- 本檔只記框架,第一期不實作。
---
## 3. 同一套指令、不同把關強度的切分(W2.4)
- `acr recipe push`(自有庫,預設)→ §1 提醒級。
- `acr recipe push --public`(公共庫,未來)→ §2 relay 檢核級。
- 同一指令、旗標分流(呼應 design §4.3 公私庫分流:`-p`/`--public`)。
- 第一期只實作預設(自有庫)路徑。
---
## 4. 驗收標準(客觀證據,mindset §7)
第一期(自有庫提醒級):
1. `acr recipe push` 一個會產對外 webhook 的東西 → CLI 印資料外流警示 + 要人類同意;非 TTY → 拒絕。
2. `acr recipe push` 一個 endpoint 可達的 recipe → 打通檢查回報「✓ HTTP 2xx」。
3. `acr recipe push` 一個缺 credential 的 recipe → 回報「⚠️ 未打通:缺 credential」(誠實,不假綠),但仍允許 push。
4. 確認 workflow 引用 recipe`component: kbdb_get`**不再被任何 validate 步驟當假零件擋**(W1 已作廢)。
---
## 5. 與既有 SDD 的一致性確認(無新矛盾)
- 不動「零件投稿走 PR + 人工檢查」(§0.1/0.2,與 design 頂部方向修正、DECISIONS §8 一致)。
- 不重啟「零件 PR 自動化把關」(§0.2,與 BACKLOG 步驟5 真意一致)。
- 資料外流提醒延續既有 data-exfil-warning 原則(mindset §6),只確認涵蓋 recipe push 路徑。
- 打通檢查 = recipe 驗收標準 2xx 的落地(DECISIONS §1)。
@@ -0,0 +1,53 @@
# Requirements: Component Gatekeeping(零件投稿真把關)
> 2026-05-29 建立(richblack 確認)。對應第一期 BACKLOG 步驟 4(補零件庫真把關)+ 步驟 5(人類閘門 + 白名單 hook)。
> 判準源:DECISIONS.md §1(工作流是 default / 建零件人類閘門 / ABC 三管齊下)、§7(讓 AI 不做歪三層機制 + 閉環)、§3c/§7(禁假綠)。
---
## 背景
第一期要把「零件投稿」從「無審核」變成「真的會擋」的把關。現況:
- `registry/src/actions/sandboxAcceptance.ts`size_check / syscall_scan 已實作;cold_start / gherkin_tests / runtime_compat 是 `return null` mock。
- `submitComponent.ts`:跑 sandboxAcceptance → 派 hash → 寫 KV。無假零件偵測、無人類閘門。
- 風險(DECISIONS):CC 把自用服務(通訊錄/帳本)做成零件進公共庫 → 全生態能打 → 機密外洩。
## 需求
### R1 — Gherkin 驗收真實作(取代 mock
- `runGherkinTests` 要真的跑零件 WASM:對每個 `gherkin_tests[]` 的 given → 餵 stdin → 比對 stdout 是否 `then_contains`
- 失敗 → 退稿,回明確 reason + guide anchor。
- recipe 不適用 GherkinDECISIONS §1recipe 驗收 = 2xx);本需求只針對 WASM 零件投稿。
### R2 — 假零件偵測(退稿並指回正路)
依 DECISIONS §1 判準,投稿零件若滿足任一,退稿:
- (a) contract 或(可得的)原碼出現具體外部服務 URL / domain
- (b) 宣告能力是 http_request 子集(打某固定 endpoint
退稿訊息要指回正路:「這該是 recipe / 工作流,不是零件」。
### R3 — 純 WASI preview1 / 零宿主依賴把關
- 擴充 syscall 掃描:確認零件只依賴 WASI preview1 + u6u host functions 白名單,無其他宿主特定依賴(避免 runtime 鎖定債,DECISIONS §4)。
- 冷啟動 / runtime 相容兩步可暫留 mock,但**明確標「未實作」**,不可假裝通過(§3c/§7 禁假綠)。
### R4 — 建零件的人類閘門(核心,BACKLOG 步驟 5 強化規格)
- **把關點 = 建立零件的 API 本身**registry submit endpoint),不是寫檔案。CLI / MCP / Python lib / JS lib 四路全收斂到這關。
- submit 預設**拒絕**,除非請求帶:
- (a) 人類已確認憑證(CLI `acr component create` 強制互動式問人類,AI 非互動環境生不出)
- (b) 舉證 `reason_why_not_workflow`(AI 須證明工作流為何做不到;舉證責任在 AI,預設假設工作流能做)
- 缺 → 403 + 指回正路(先試工作流)。
- 誠實限制:沒技術能 100% 防 AI 假冒人類確認;靠閘門 + 舉證留記錄 + mindset 明示「絕不代替人類確認」+ 軌跡可審計,讓假冒成明確越界。
### R5 — 白名單 + 本機 hook 兜底
- `MVP_COMPONENTS.txt` 白名單檔。
- pre-write / pre-bash hookCC 直接在 `registry/components/{白名單外新名}/` 造目錄或 mkdir → exit 2(擋繞過 API 直接改 repo 等 CI 部署的旁門)。
- pre-write hook`registry/components/` 下出現 `.ts`(非 AssemblyScript)→ exit 2(現有 hook 已部分做)。
### R6 — 不限制自由
- 不做「audience 宣告 / 禁止自用進公共庫」式的限制(DECISIONS:別人要建零件是他的自由)。
- 唯一硬約束:零件 = 只打一個 endpoint 的薄殼。閘門是「要建得先說服人 + 舉證」的摩擦,非禁止。
- arcrun 不做授權判斷(能否打通由發 key 的服務裁決)——把關針對「是否該是零件」,不針對「誰能打哪個 API」。
## 非目標
- Phase 5 用戶自製零件 R2 上傳(未啟用)。
- registry KV schema 大改(用既有結構)。
- 完整的 audit trail 系統(DECISIONS §7 事後機制第一層,另議)。
@@ -0,0 +1,114 @@
# Tasks: Component Gatekeeping
> 對應 design.md。每完成一個 task 立刻標 [x],不批次。
> Design 已 richblack 確認(2026-05-29,含 Q1-Q3 決議)。
---
## ⚠️ 收尾狀態(2026-05-30,方向修正後)
投稿改走 **GitHub PR**(廢 registry self-service,見 design 頂部「方向修正」)。本 SDD 收尾於:
- **已完成且 commit**G1detectFakeComponent)、G3wasmImports)、G5/G6unimplemented_steps)、
G0 registry 人類閘門(保留不刪)。測試 15 綠。
- **改去向**G4 Gherkin / 覆蓋檢查 → 未來接 CI PR checkCI 能跑 wasm)。G1/G3 邏輯可複用。
- **不做**CI PR checkrichblack:人工 review 就夠,primitive 極少)、R5 本機 hook
PR/merge + G1 + 沙箱已防「未經同意變公共零件」,hook 過度工程)、registry self-service、
acr parts publish 加閘門、平台 sandbox 重跑。
- **黃金向量**:人工核對(另起 session 從語義寫),不急、不機器自動化。
- **轉出範圍**:真正的裸奔風險在「資料外流」(recipe/webhook 把資料送出去),不分公私庫
→ 另開新 SDD「資料外流警示」(API 層警示 + AI 動手前 hook)。用戶 API 保護(入站認證)
+ recipe/part/function 架構釐清 → 記 BACKLOG 待決策。
---
## W3 `acr parts` 去 hardcode recipe → 動態 store 清單(2026-06-29,總管交辦,issue #13 根治)
> 根因(leo 2026-06-29):「telegram 與 google 不一致」其實是**查錯表**——讀 `acr parts` + BUILTIN_CREDENTIALS_MAP
> 推論「telegram 走不同機制」。對外部服務該查的是動態 store-backed 的 `acr recipe list` / `acr auth-recipe list`
> 在對的表裡 telegram 與 google 同一條鏈。`acr parts` 把 5 個 recipe hardcode 進零件清單(parts.ts:197-261)→
> 誤導下一個人查錯表 + pushed recipe 在 `acr parts` 永遠看不到。這是 component(靜態PR-only) 與 recipe(動態store) 的 conflation。
> 屬薄殼修正(07 §1 seed 正例:清單應反映 store 真相,不釘 hardcode);不違禁令(非新增 seed/upsert 拼裝)。
- [x] W3.1 `acr parts` 移除 5 個 recipe hardcodegmail_send/google_sheets_append/telegram_send/line_notify_send/notion),只留真零件(WASM)。實證:`acr parts` 只剩 http_request 等真零件。
- [x] W3.2 統一跨類搜尋(leo 2026-06-29 升級):**新增 `acr search <term>`**cli/src/commands/search.tsfan-out 4 來源——component(靜態 BUILTIN_COMPONENTS) + recipe(GET /recipes) + auth-recipe(GET /auth-recipes) + workflow(GET /webhooks/named),依類別回 counts+命中,各來源獨立 try(離線降級成指路,不連坐)。`acr parts` 末段改指向動態清單 + `acr search`。實證:`acr search telegram``component:0 recipe:1 auth-recipe:0 workflow:1`(一眼看出 telegram 是 recipe)。
- [x] W3.3 parts.ts 檔頭註解寫死分流原則:component=靜態(PR-only WASM)、recipe/auth-recipe/workflow=動態(store-backed);移除處留註解防再 hardcode。
## G1 假零件偵測(R2,Q2=兩者硬擋)
- [x] 1.1 `registry/src/actions/detectFakeComponent.ts`(a) 外部 URL/domain 偵測(掃 contract 文字 + wasm binary 文字)硬擋;(b) http_request 子集偵測硬擋
- [x] 1.2 排除 `auth_*` primitive 與 `http_request` 自己
- [x] 1.3 接進 sandboxAcceptance 步驟鏈(fake_component_scan 為第一步)
- [x] 1.4 退稿訊息指回正路(「這該是 recipe/工作流」)
## G3 純 WASI 把關(R3
- [x] 3.1 `wasmImports.ts` 解析 wasm import section,取出所有 import module name(已對真實零件驗證)
- [x] 3.2 白名單:只准 `wasi_snapshot_preview1` + `u6u`;其他 module → 退稿
- [x] 3.3 接進 scanSyscalls(白名單為主,黑名單為次)
## G4 Gherkin 真實作(R1)— venue 修訂:CLI 本地跑,非 registrydesign §4 修訂)
> CF Worker 不能 runtime 編譯 wasm + §8 不依賴 CI → Gherkin 在投稿指令本地(Node)跑。
> 在第一段(tinygo build→wasm 之後)測,跟 worker 無關。registry 只存 evidence 不重跑。
- [x] 4.1 `runGherkin.ts`createWasiShim 跑 wasm,邏輯正確)—— 但 venue 要從 registry 改 CLI
- [ ] 4.2 回退 sandboxAcceptanceregistry 不跑 Gherkin(移除 await runGherkin),改回靜態步驟
- [ ] 4.3 Gherkin 邏輯搬到 CLI 投稿指令(Node 環境 instantiate wasm
- [ ] 4.4 投稿 payload 帶 gherkin_evidencescenario/given/actual_stdout/passed),registry 存 metadata 可審
- [ ] 4.5 誠實標明第一期 evidence 可造假(mindset + 文件);平台重跑列未來
- [ ] 4.6 投稿指令(暫名 acr component submit+ 公私庫分流(-p 公共)— 新 CLI 工程
## G5/G6 誠實標未實作(R3 禁假綠)
- [x] 5.1 SandboxResult 增 `unimplemented_steps: string[]`
- [x] 5.2 cold_start / runtime_compat 列入 unimplemented_stepssubmit 回應明示
## G0 人類閘門(R4,核心)
- [x] 0.1 submit 請求增 `human_confirmation`SubmitOptions in submitComponent.ts+ route 解析(multipart/JSON 皆支援)
- [x] 0.2 submit 邏輯:非 skip_acceptance 的新投稿,無 human_confirmation/空 reason → gateError(指回正路)
- [x] 0.3 human_confirmation + gherkin_evidence 寫進 KV metadata(軌跡可審)
- [ ] 0.4 CLI `acr parts publish`(既有指令):互動式問人類(工作流為何做不到 + 確認),非 TTY 拒絕
- [ ] 0.5 MCP / Python lib / JS lib 補 human_confirmation 欄位支援(薄)
- [ ] 0.6 誠實限制寫進 mindset Skill(步驟 7+ SDK 文件
> 命名修正(2026-05-29):投稿走**既有** `acr parts publish`cli/src/commands/parts.ts),
> 非另建 acr component create(符合「修改現有不重建」)。G0-CLI(0.4)與 G4-CLI 合併在此指令做。
## R5 白名單 + 本機 hook
- [ ] 5.3 `registry/MVP_COMPONENTS.txt`(現役 22 個 canonical_id
- [ ] 5.4 submit 過閘門成功 → 自動 append canonical_id 進白名單(Q3
- [ ] 5.5 pre-write-guard.sh:寫 `registry/components/{白名單外}/` → exit 2
- [ ] 5.6 pre-bash-guard.shmkdir `registry/components/{白名單外}` → exit 2
## W1 ~~CLI workflow validate 擋假零件式 component 名~~2026-06-01 作廢,方向修正)
> **作廢原因(richblack 2026-06-01**:「擋假零件」這件事不再存在——因為**自製/修改零件的路
> 已被封鎖**CC 根本造不出零件),workflow 引用 recipe(如 component: kbdb_get)是**合法且
> 未來唯一的擴充方式**,不該被當「假零件」擋。把關點從「workflow validate」**移到 recipe 入庫
> push)那一刻**。已動的 yaml-parser.ts `LEGAL_PRIMITIVES`/`findSuspectComponents` 已回退。
> 取而代之 → 見 W2。
## W2 封鎖自製零件 + recipe 入庫把關(2026-06-01 新方向)
> richblack 2026-06-01 定調:
> - 零件由維護者管理,**CC 不能自製/修改零件**(hook + CLI 拒絕)→ 不再有「假零件」。
> - CC 唯一能擴充的是 **recipe**。recipe 一律用「推(push)」,**自有庫與公共庫同一套指令**。
> - 把關依庫別分強度:
> - **自有庫(self-hosted**:只能**提醒**(無法在別人機器強制)。兩個提醒:
> (1) 資料外流提醒——某動作會讓外界看到你的東西(如 workflow 產對外 webhook),同意後是他的責任;
> (2) 打通檢查——查他要打的 API 是否打得通(2xx)。
> - **公共庫**:由維護者機制檢核「實際打通、真收到成功回傳」(PR/CI relayDECISIONS §3c,第一期後)。
> 屬 change,需先寫 design 給 richblack review 才動 code。本節先記框架。
- [x] W2.1 封鎖自製零件 — **釐清完成(richblack 2026-06-02**:靠「零件投稿走 GitHub PR + 人 merge」
天然閘門(DECISIONS §8)。BACKLOG 步驟5「不做 hook」真意 = 零件少、不為零件 PR 蓋自動化把關
(量少人工檢查;爆量才回頭想),**不是**不阻止自製。無矛盾,不需新做 hook。
- [x] W2.2 `acr recipe push` 資料外流提醒 — **既有實作已涵蓋**recipe.ts:70-79 `obtainExposureConsent`
exposure-warning.ts:互動打資源名確認、非 TTY 拒絕、首次問記住)。data-exfil-warning SDD 已做,確認涵蓋 recipe push 路徑。
- [x] W2.3 `acr recipe push` 打通檢查 — **新增** `probeRecipeEndpoint`recipe.ts):push 成功後實打 endpoint
回報 2xx/⚠。提醒級不硬擋;endpoint 含 {{模板}} → 誠實說明待 run 才知;401/403 → 標「多半缺 credential,非 recipe bug」(不假綠,mindset §7
- [ ] W2.4 公共庫 push--public= 維護者 relay 檢核(DECISIONS §3c)— 第一期後,本期只做自有庫提醒級
## 驗收(design §8
- [ ] V1 投寫死 endpoint 假零件 → G1 退稿(終端輸出)
- [ ] V2 投 `.ts` 進 registry/components → hook exit 2
- [ ] V3 本機造白名單外零件目錄 → hook exit 2
- [ ] V4 無 human_confirmation 的 submit → 403
- [ ] V5 帶 human_confirmation + 過 Gherkin 真零件 → 通過 + reason 留 metadata + 白名單 append
- [ ] V6 故意改壞 then_contains → Gherkin 退稿
- [ ] V7 回應含 unimplemented_stepscold_start/runtime_compat 不假綠)
@@ -0,0 +1,162 @@
# SDD: arcrun Component Registry 正典化(Component Registry Canon
> 2026-05-07 建立。狗糧發現的根本問題:registry 活著但 index 空的,AI 找不到零件就會繞回 Python。
> 範圍:**讓 registry 成為零件 metadata 的 SSOT**,含 u6u → arcrun rebrand。
---
## 1. 問題
### 1.1 表象
- `registry.arcrun.dev/components/search?q=*` 永遠回 0 結果
- MCP `u6u_search_components` 找不到任何零件
- `acr parts list` 同樣空
### 1.2 根因
`matrix/arcrun/registry/components/` 下 30+ 個零件已經部署成獨立 Workerkbdb_ingest, claude_api, kbdb_create_block, kbdb_patch_block, http_request, string_ops, ⋯),但**它們的 contract.yaml 沒有透過 `POST /components/submit` 進 registry index**。
部署路徑:
```
registry/components/{name}/main.go ← TinyGo 寫的零件
↓ tinygo build
.component-builds/{name}/component.wasm
↓ wrangler deploy
{name}.arcrun.dev (Worker) ← 零件可被 HTTP 呼叫了
registry index? ← 這步從來沒做
```
### 1.3 影響(吃狗糧的觀察)
- 新 AIClaude / Gemini / Codex)進來不知道有什麼零件 → 自己寫 Python 直打 API
- arcrun 想推「AI-first 自服務」整個破功
- 文件寫得再好都救不了 — 因為 README 只能寫概念,零件清單必須是 API 動態查
---
## 2. 目標
**Registry 是零件 metadata 的 SSOT**
- 零件 Worker 在跑 ⇔ registry 有對應 entry(雙向綁定)
- AI 透過 MCP `search_components` 永遠找得到所有現役零件
- README 不寫死數量,動態 badge 即時反映
- 第三方裝完 MCP 30 秒內能找到第一個可用零件
---
## 3. 三層設計
### Layer 1: 一次性 backfillPhase 1
`matrix/arcrun/registry/components/*/component.contract.yaml`,把每個 contract POST 進 registry index。
工具:`matrix/arcrun/registry/scripts/backfill-index.ts`
- 讀檔 → 解析 YAML → 呼叫 registry submit endpoint
- idempotent:已存在不重複寫(registry 端要支援 upsert
- 跳過沙盒驗收(這些零件已驗過、已部署,不用重跑 gherkin tests
### Layer 2: 部署即註冊(Phase 2
`.github/workflows/deploy.yml`
- 通用掃描掃到 `.component-builds/{name}/wrangler.toml` 部署成功後
- post-deploy step 自動呼叫 registry submitcontract 從 `registry/components/{name}/component.contract.yaml` 讀)
零件 Worker 部署 ⇒ registry 自動更新。沒有「零件部署了但 registry 不知道」的可能。
### Layer 3: DiscoverabilityPhase 3
- README 移除「21 個零件」這種寫死數字,改「跑 search 看當前清單」
- 加 badge endpoint `registry.arcrun.dev/badge/components.svg` 即時顯示數量
- MCP `get_component_guide` 開頭加鐵律:「動工前必須先 search,不是猜」
- onboarding kit GitHub templateCLAUDE.md / .cursor/rules / AGENTS.md 三件套,all 強制 search 優先
### Layer 4: u6u → arcrun RebrandPhase 4
`matrix/u6u-mcp/` 跟所有 `u6u_*` tool 名搬到 arcrun 命名空間。
理由:
- u6u 是申請 arcrun.dev 之前的暫名,現在已過時
- 命名混亂阻礙推廣(「為什麼 arcrun 文件叫 u6u_*?」)
- 第三方看到 u6u 不知道是同一個產品
範圍:
1. 目錄:`matrix/u6u-mcp/``matrix/arcrun-mcp/`
2. Worker name`u6u-mcp``arcrun-mcp`
3. Tool 前綴:`u6u_search_components``arcrun_search_components`14 個 tool
4. Hostname`mcp.finally.click``mcp.arcrun.dev`finally.click 保留 redirect 到 arcrun.dev 過渡期)
5. Repo / Worker 內部 IDu6u-mcp-server → arcrun-mcp-server
6. README 全文:u6u → arcrun
7. user memoryCLAUDE.md / MEMORY.md)相關提及一併更新
8. inkstone-component-registry(舊 worker)廢止 → arcrun-registry 為唯一現役
**Rebrand 原則:**
- 用戶端 configclaude_desktop_config.json 等)給過渡期:兩個 URL 都活,舊的回 deprecation header 提示換新
- Tool 前綴 `u6u_*``arcrun_*` 沒有過渡期(一刀切,因為前綴是 AI 看的,不是用戶記憶肌肉)
- 文件 / repo 內所有 reference 立即改
---
## 4. 範圍邊界
**在本 SDD 範圍內:**
- ✅ Phase 1: backfill index
- ✅ Phase 2: 部署即註冊 hook
- ✅ Phase 3: README + badge + onboarding kit
- ✅ Phase 4: u6u → arcrun rebrand(含目錄 / worker / hostname / tool 前綴 / 文件)
**不在範圍內:**
- 新零件開發(這是 polaris 業務範圍)
- registry KV schema 改動(用既有結構)
- u6u-gui 的 rebrandu6u-mcp 同 monorepo 但獨立 SDD
- Phase 5(用戶自製零件 R2 上傳)— 等 Phase 4 完成後另開 SDD
**前置依賴(已完成):**
- ✅ u6u-mcp Zod 4 → Zod 3 修復(2026-05-07
- ✅ u6u-mcp service binding 改指 arcrun-registry2026-05-07
- ✅ arcrun-registry Worker 部署在 registry.arcrun.dev
---
## 5. 驗收標準
### Phase 1 驗收
- `u6u_search_components("kbdb")` 回非空結果,含 `kbdb_ingest` / `kbdb_create_block` / `kbdb_patch_block`
- `acr parts list` CLI 端對端能列出
- registry KV 內至少 30 entries
### Phase 2 驗收
- 部署任一既有零件後,registry 30 秒內 reflect 更新
- 部署一個全新零件,無需手動 publish,registry 自動有
- CI workflow 不會因 registry 寫入失敗就擋部署(degraded mode:寫入失敗 log warning 但不 fail
### Phase 3 驗收
- README 沒有「21 個零件」「30 個零件」這種寫死數字
- badge SVG 渲染正確、數字跟 KV 一致
- onboarding kit clone 下來,照 README 跑能 30 秒內 list 到零件
### Phase 4 驗收
- `mcp.arcrun.dev/mcp/mcp` 通,回的 tool 名都是 `arcrun_*`
- 舊 `mcp.finally.click/mcp/mcp` 仍可用但回 deprecation header
- README / docs / GUIDE 全部 u6u 字樣消除
- `matrix/u6u-mcp/` 目錄不存在,改為 `matrix/arcrun-mcp/`
- 用戶記憶(`~/.claude/.../MEMORY.md`arcrun MCP 設定範例已更新
---
## 6. 風險與緩解
| 風險 | 緩解 |
|---|---|
| backfill 把 contract 灌進去後,沙盒驗收覆蓋既有資料 | registry submit 加 `skip_acceptance=true` flag,僅 backfill 用 |
| 部署 hook 寫入失敗擋掉部署 | hook degraded mode:失敗只 warning,不 fail 部署 |
| Rebrand 把現役 client 弄壞 | 過渡期:舊 hostname 跟 worker 並存 1 個月 |
| Tool 前綴改名 AI 適應期 | 不過渡,一刀切(前綴是 system instruction 範圍,AI 一個 prompt 就學會)|
| 既有用戶 config 寫死 finally.click | 提前公告 + 過渡期 + 舊 endpoint 自動 redirect / proxy |
---
## 7. 變更紀錄
| 版本 | 日期 | 內容 |
|---|---|---|
| v1.0 | 2026-05-07 | 初版。吃狗糧發現 registry 空的,三層設計(backfill / auto-register / discoverability+ u6u → arcrun rebrand 一併納入。 |
@@ -0,0 +1,174 @@
# Tasks — Component Registry Canon
> 對應 SDD[design.md](design.md)
> 上次更新:2026-05-07
**狀態 legend**`[ ]` 待辦 / `[🔄]` 進行中 / `[x]` 完成
---
## Phase 0:前置(已完成)
- [x] 0.1 u6u-mcp Zod 4 → Zod 3 降版修 tools/list `_zod undefined` bug2026-05-07
- [x] 0.2 u6u-mcp service binding `inkstone-component-registry``arcrun-registry`2026-05-07
- [x] 0.3 確認 `mcp.finally.click/mcp/mcp` 端對端通,tools/list 回 14 個 tool2026-05-07
---
## Phase 1Backfill Index(半天,立即見效)
- [x] 1.1 探查 registry 既有 endpoint:發現
- 既有 `POST /components` 強制要 wasm bytesmultipart 或 base64),跑沙盒驗收 + 寫 R2 + 寫 KV
- cypher-executor 已不從 R2 動態載 wasmline 32 標 R2 路徑作廢,零件用獨立 Worker URL)
- 結論:R2 是 legacyregistry 真正用途是 metadata 索引給 AI 搜尋
- 決策:**加新 endpoint `POST /components/index-only`** 接 contract(無 wasm、無沙盒),專供 backfill 跟「已部署但未索引」零件用
- [x] 1.1.1 加 `src/actions/indexOnlyComponent.ts`metadata-only 寫 KV,冪等)
- [x] 1.1.2 加 `src/routes/components.ts``POST /index-only` route
- [x] 1.1.3 部署 + smoke testcontract 驗證 + 錯誤處理通過)
- [x] 1.2 寫 `matrix/arcrun/registry/scripts/backfill-index.mjs`zero-build node script,用 js-yaml
- [x] 1.3 dry-run 確認 30 個 component 全 parse 通
- [x] 1.4 跑真 backfill(過程中發現並修了兩個 schema 問題):
- schema enum `category``auth` / `ai` / `platform`types.ts
- `max_cold_start_ms` 上限放寬 50 → 500auth/ai 含 crypto 需要)
- `no_network_syscall` / `no_filesystem_syscall` 改 optional
- `max_size_kb` 上限放寬 2048 → 8192
- index-only route 對缺 gherkin/description/tags 的零件補 placeholder(不擋索引)
- [x] 1.5 驗證:MCP `u6u_search_components("kbdb")` 回 3 個零件(kbdb_ingest / kbdb_create_block / kbdb_patch_block
- [ ] 1.6 驗證:`acr parts list` CLI 端對端能列
- [x] 1.7 驗證:registry KV 30 entries30 created + 30 idx 共 60 keys
---
## Phase 1.5:砍 R2 dead storage(先於 Phase 2,清架構斷層)
> 2026-05-07 加入。R2 wasm 路徑早已 deadcypher-executor 不從 R2 讀),保留只會誤導 AI。
> SDD design.md 的「Phase 5 用戶自製零件 R2 上傳」一併廢止。
- [x] 1.5.1 改 `submitComponent.ts`:移除 R2 寫入段落,保留 KV 寫入
- [x] 1.5.2 移除 `wrangler.toml``[[r2_buckets]] WASM_BUCKET` binding
- [x] 1.5.3 移除 `types.ts` Bindings 的 `WASM_BUCKET: R2Bucket`
- [x] 1.5.4 既有 `wasm_r2_key` 欄位保留為 deprecatedqueryComponents 仍會讀 legacy record
- [ ] 1.5.5 廢止 `arcrun-wasm` R2 bucket30 天觀察期後 → 2026-06-07 之後 `wrangler r2 bucket delete`
- [x] 1.5.6 部署 + smoke testsearch 端對端通過(kbdb 找到 3 個零件)
- [x] 1.5.7 cypher-executor 補清(2026-06-04 richblack 確認):當初 1.5.2/1.5.3 只清 registrycypher-executor 的同類 R2 殘留一併移除
- `cypher-executor/wrangler.toml`:移除 `[[r2_buckets]] WASM_BUCKET`
- `cypher-executor/wrangler.test.toml`:移除 R2 mock
- `cypher-executor/src/types.ts`:移除 `WASM_BUCKET: R2Bucket`
- `cypher-executor/src/lib/constants.ts`:修正過時註解(不再說「從 R2 讀取」)
- 動機:R2 綁信用卡與 open source 零費用核心衝突;tsc --noEmit 通過
- CLI `acr init` 的 R2 → 見 1.5.8(壓測報告出來後已處理)
- [x] 1.5.8 CLI R2 全清(2026-06-04,依壓測報告 docs/壓測報告.md #3#4):R2 綁卡違背開源免費 + bucket 名非法證明從未跑通
- `cli/src/lib/deploy.ts`:移除 `REQUIRED_R2_BUCKET`;新增 `stripOfficialOnlyBindings()`(注入時 strip routes/r2/ai
- `cli/src/commands/init.ts`:移除 `ensureR2Bucket` 呼叫 + `wasm_bucket` config + token 提示去 R2 Edit
- `cli/src/lib/cf-api.ts`:移除 `ensureR2Bucket()`
- `cli/src/lib/config.ts`:移除 `wasm_bucket?` 欄位
- `cli/src/commands/validate.ts`:錯誤文案去 WASM_BUCKET 命名誤導
- 同時修阻斷 #1#2routes/AI 寫死 arcrun.dev):strip 在 deploy.ts 注入暫存副本時做,不刪 repo 內 toml(官方 prod CI 仍需 routes
- 詳見 .agents/specs/arcrun/sdk-and-website/self-hosted-init.md §7.5
## Phase 2:部署即註冊(1-2 天)
- [x] 2.1 選擇方案:CI stepgithub actions)— 在 wrangler deploy 之後 curl `/index-only`
- [x] 2.2 寫 `registry/scripts/register-component.sh`(本地 + CI 共用 SSOTpython3 + pyyaml 解 YAMLcurl POST registry
- [x] 2.3 改 `.github/workflows/deploy.yml` tier1 deploy step 後加 "Register component in registry" stepdegraded mode:失敗只 warning
- [x] 2.4 本地驗 `bash scripts/register-component.sh kbdb_ingest` → 200 + already_indexed
- [ ] 2.5 真正 push 一個新零件驗 CI hook 端對端(需要等下次新增零件時驗)
- [ ] 2.6 文件化:`docs/contributing-components.md`「新增零件的標準流程」
- [ ] 2.7 廢止 `u6u_publish_component` tool 的「需手動 publish」假設(rebrand 一起做)
---
## Phase 3Discoverability(半天)
- [ ] 3.1 改 GitHub `richblack/arcrun` README
- 移除「21 個零件」這種寫死數字
- 加「跑 `acr parts list` 或 MCP search 看當前清單」
- 加 badge`![components](https://registry.arcrun.dev/badge/components.svg)`
- [ ] 3.2 加 `matrix/arcrun/registry/src/routes/badge.ts`
- GET `/badge/components.svg` 回 shields.io 格式 SVG
- count 從 KV 即時 query
- cache 1 分鐘(`Cache-Control: max-age=60`
- [ ] 3.3 改 MCP `u6u_get_component_guide` tool(之後改名 `arcrun_*`
- 開頭加「鐵律:動工前必須先 search_components,找不到才 publish」
- [ ] 3.4 onboarding kit GitHub template repo(建議名 `arcrun-quickstart`
- 三件套:CLAUDE.md / `.cursor/rules/arcrun.mdc` / AGENTS.md
- 強制:「呼叫 Claude/任何 AI 前,先 list MCP toolsarcrun MCP 已連線時,**禁止用 Python 直打 HTTP API**」
- 內附範例 hello workflow 跟 component
- [ ] 3.5 寫 onboarding doc`docs/onboarding-third-party-engineer.md`
- 第三方工程師如何 30 秒內讓 AI 學會用 arcrun
---
## Phase 4u6u → arcrun Rebrand1 天,最後做)
> 規劃做完 Phase 1-3 驗證 OK 才動 rebrand,避免邊改邊驗。
### 4.1 Repo & Worker
- [ ] 4.1.1 `git mv matrix/u6u-mcp matrix/arcrun-mcp`(或 cp + rm,視 git history 偏好)
- [ ] 4.1.2 改 `matrix/arcrun-mcp/wrangler.toml`
- `name = "u6u-mcp"``name = "arcrun-mcp"`
- 加 route `mcp.arcrun.dev/*`,舊 `studio.finally.click/mcp*` 保留 1 個月
- [ ] 4.1.3 改 `package.json``@inkstone/u6u-mcp-worker``@arcrun/mcp-worker`
### 4.2 Tool 前綴改名
- [ ] 4.2.1 14 個 tool 檔案 rename`u6u_*.ts``arcrun_*.ts`
- [ ] 4.2.2 每個 tool 內部 `server.tool("u6u_xxx", ...)``server.tool("arcrun_xxx", ...)`
- [ ] 4.2.3 `src/tools/registry.ts` import 路徑全改
- [ ] 4.2.4 `src/index.ts` `serverInfo.name``u6u-mcp-server``arcrun-mcp-server`
### 4.3 文件
- [ ] 4.3.1 README.md 全文 u6u → arcrun
- [ ] 4.3.2 GUIDE.md 同上
- [ ] 4.3.3 GitHub `richblack/arcrun` README 補 MCP 段落(之前沒提)
- [ ] 4.3.4 任何提到 `u6u-mcp` / `mcp.finally.click` 的 docs 更新
### 4.4 用戶記憶
- [ ] 4.4.1 `~/.claude/projects/.../memory/MEMORY.md` 加 arcrun MCP entry
- URL: `https://mcp.arcrun.dev/mcp/mcp`
- tool 前綴: `arcrun_*`
- finally.click 過渡期到何時
- [ ] 4.4.2 polaris/mira/CLAUDE.md 提到 daemon / arcrun / MCP 的部分對齊新命名
### 4.5 過渡期(舊 endpoint 不立刻砍)
- [ ] 4.5.1 舊 `mcp.finally.click/mcp/mcp` 加回應 header `Deprecation: true` + `Link: <https://mcp.arcrun.dev/mcp/mcp>; rel="successor-version"`
- [ ] 4.5.2 舊 worker 繼續服務 30 天(2026-06-07 為止)
- [ ] 4.5.3 廢止排程:2026-06-07 後舊 worker 改回 410 Gone + 提示換新 URL
### 4.6 驗證
- [ ] 4.6.1 `mcp.arcrun.dev/mcp/mcp` initialize + tools/list + 一個 tool call 全通
- [ ] 4.6.2 我的 Claude Code config 切到新 URL,用 `mcp__arcrun__search_components` 端對端測
- [ ] 4.6.3 grep `u6u``matrix/arcrun-mcp/` 結果為 0(除了 changelog 紀錄)
---
## 風險追蹤
- 風險 1backfill 跑進去發現某些 contract.yaml 格式跟 registry 期望不一樣 → 緩解:dry-run 先看,必要時補 contract 欄位
- 風險 2Phase 4 rebrand 期間用戶 client 設定亂 → 緩解:過渡期 + Deprecation header
- 風險 3:自動註冊 hook 失敗導致部署被擋 → 緩解:degraded modewarning 不 fail
---
## Known Issues(吃狗糧發現的,先記錄)
### KI-1u6u-mcp README URL 寫錯
- README 寫 `mcp.finally.click/mcp`,實際是 `mcp.finally.click/mcp/mcp`basePath + route
- 影響:用戶照 README 裝完試打不通
- 解法:rebrand 時順便修
### KI-2inkstone-component-registry 跟 arcrun-registry 並存
- 兩個 worker 都活著,u6u-mcp 之前指錯
- inkstone-component-registry 是舊版(2026-03-24)、arcrun-registry 是現役(2026-04-16
- 解法:Phase 1 backfill 完成後,inkstone-component-registry worker 廢止
### KI-3:search 對自然語言不夠靈敏(吃狗糧第一個發現)
- 現象:
- `search("從 KBDB 讀取或查詢 block")` → 0 結果
- `search("kbdb")` → 3 結果(kbdb_ingest / kbdb_patch_block / kbdb_create_block
- 根因:搜尋走 embedding(bge-m3)相似度,但既有零件清單少(30 個)+ description 寫得正式,自然語言整句的 embedding 跟 description 距離太遠
- 影響:**致命** — AI 第一句永遠是自然語言整句,回 0 就會放棄 search 改寫 Python
- 解法(Phase 3 處理):
1. embedding search 之外加 keyword fallbacksplit query → 對 canonical_id / display_name / tags 做 ILIKE
2. 或 lower threshold(目前 SCORE_THRESHOLD = 0.5,可能過高)
3. MCP get_component_guide 教 AI 「找不到時拆關鍵字再 search」
- 優先級:P1(會擋推廣)
@@ -0,0 +1,117 @@
# Design: 資料外流警示(Data Exfiltration Warning
> 2026-05-30。實作 requirements.md。**本 design 需 richblack review 後才動 code。**
> 觸發策略(richblack 定):**只在「資料變成可被外部呼叫」時問**(暴露面),不管「我去打別人 API」(出站,高頻低風險)。
---
## 0. 核心定義:什麼是「資料變成可被外部呼叫」
警示的觸發點 = 一個動作**讓某份資料 / 能力變成「別人能呼叫得到」**。這是真正的裸奔動作。
**不觸發**:「我自己的 workflow 去打別人的 API」(出站)——那是我主動用別人服務,不是把我的東西開放出去。
### 哪些動作屬於「變成可被外部呼叫」(要警示)
| 動作 | 為何是暴露 |
|---|---|
| 部署 webhook trigger`acr push` workflow → 可被 POST 觸發) | workflow 變成一個對外可呼叫的 endpoint。誰打它就能跑它、拿它的輸出 |
| recipe 貢獻到公共庫(未來飛輪項) | recipe(含 endpoint 設定)變成全生態可見可用 |
| 把 workflow / recipe 的可見性改為 public(若未來有此欄位) | 同上 |
### 哪些動作**不**觸發(避免盲目按 yes)
- `acr run`(本機跑,不暴露)
- `acr recipe push`(存私人 KV,綁 api_key,只有自己 + 自己的 workflow 用——**不是**對外暴露)
- ⚠️ 待 design review 確認:recipe push 現況是私人的,**不暴露**。除非未來加「公共」旗標才觸發。
- workflow 裡「打別人 API」的 http 節點(出站,不暴露我的資料給別人呼叫)
- 查詢類、creds push(上傳加密 credential 是保護不是暴露)
### 灰色地帶(design review 要定)
- **webhook trigger 現況**:要 `X-Arcrun-API-Key`(owner 的 key)才能打 → 嚴格說「只有我能打」,不算對全世界暴露。
但:(a) key 一旦外流就全開;(b) 用戶可能不知道「部署 webhook = 開了一個 endpoint」。
**是否警示 webhook 部署?** 傾向「是」,因為用戶可能不知情它變成 endpoint,且這是「把 workflow 變 API」的那一步(richblack 最早的風險點)。
## 1. 兩道防線
### 1a. hook(防在前,AI 動手當下)
AI 在本機寫「會變成可被外部呼叫的東西」前,hook 警告 + 要人類確認。可偵測的訊號:
- 寫 workflow YAML 含 webhook trigger / 對外觸發設定。
- 跑 `acr push`(部署 webhook)、未來的 recipe 公共貢獻指令。
- pre-bash / pre-write hook 偵測這些 → 輸出警示「這會把 X 變成可被外部呼叫,需人類確認」。
- **誠實限制**:hook 偵測的是「動作形態」(部署 webhook),不是「資料是否敏感」(機器判不準)。
### 1b. API 層(防在後,真的暴露前)
暴露動作打到 serverwebhook 部署 endpoint / 未來公共貢獻 endpoint)時,server 要求帶
「人類已確認暴露」的明示旗標,沒帶 → 拒絕 + 提示。與 component-gatekeeping 的 human_confirmation 同模式:
```ts
exposure_confirmation?: {
confirmed_by_human: true;
understood_exposure: string; // 人類說明「我知道這會把什麼開放給誰」
confirmed_at: string;
}
```
缺 → 拒絕,訊息:「部署 webhook = 開一個對外可呼叫的 endpoint。確認你知道這會暴露 [workflow 名]
的能力/輸出?用 `acr push --confirm-exposure` 並說明。」
### 為何兩道都要
- 只有 hookAI 可繞過 CLI 直接打 server API → API 層補。
- 只有 API 層:AI 已經寫好暴露的東西才在送出時被擋,浪費 + 用戶較晚看到 → hook 提前。
- 同 component-gatekeeping 的雙層精神。
## 2. 警示要讓人看得懂(R1
不是「確認嗎 Y/N」,是說清楚風險:
```
⚠️ 這個動作會把 workflow "contacts_lookup" 變成可被外部呼叫的 endpoint:
https://cypher.arcrun.dev/webhooks/named/contacts_lookup/trigger
任何持有觸發憑證的人都能呼叫它、取得它的輸出。
這個 workflow 讀取:[盤出 workflow 用到的資料源,若可得]
需要保護(要求呼叫者認證)嗎?目前的觸發認證是:[現況]
確認部署?(需人類明示)
```
- 「workflow 讀取什麼資料源」:盡力從 workflow 定義盤(用了哪些 recipe / endpoint),盤不出就標「無法自動判斷,請自行確認」。誠實。
## 3. known-destinations / 不重複問(R4 避免盲目按 yes)
觸發策略已經很窄(只暴露動作),但同一個 workflow 重複部署不該每次問。
- 首次部署某 workflow 為 webhook → 問。人類確認後記住(該 workflow 標記 exposure-confirmed)。
- 之後同 workflow 更新 → 不重問(除非暴露面變大,如新增對外觸發)。
- 記在哪:workflow metadataWEBHOOKS KV 的 record 加 `exposure_confirmed_at`)。
## 4. 與既有一致(R5
- 同 component-gatekeepingAI 不可替人類決定有外洩風險的動作;誠實限制(AI 能偽造 confirmed_by_human,靠軌跡可審 + mindset)。
- 同「arcrun 不做授權判斷」:不判斷「該不該暴露」,只「攔下來讓人類明示同意」。不禁止暴露,要明示同意。
## 5. 範圍邊界
- **動**webhook 部署路徑(webhooks-named.ts)的 exposure_confirmation + CLI `acr push --confirm-exposure` 互動 + hook。
- **不動**:用戶 API 入站保護機制(發 key/權限/限流,另列 BACKLOG);recipe 私人 push(不暴露,不擋);出站 http 節點(不擋)。
- recipe 公共貢獻路徑未實作 → 本系統只要求它**未來**內建 exposure 閘門(記進那條 BACKLOG 項)。
## 6. 決議(richblack 2026-05-30 design review
- **Q1 → recipe push 也警示,公私一視同仁。** 不是因為 recipe 本身暴露,而是統一原則「凡有資料去向/暴露面的動作都警示」。用戶可選「以後不要警示」(記偏好)。理由見 §7 同意 log。
- **Q2 → webhook 部署要警示,但角度是「提醒 + 提供保護」不是「擋」。** 用不用認證是用戶決定(如美國氣象 API 本就無 key 公開)。我們警示時**順便提醒「可用 arcrun 提供的保護措施」**(接「用戶 API 保護機制」資安優勢,BACKLOG 待決策)。首次問、記住(§3)。
- **Q3 → hook 偵測 `acr push` 指令**(簡單版,pre-bash 攔指令)。
## 7. 同意 = 法律憑證(richblack 2026-05-30,重要)
每次人類同意「暴露/送資料」的動作,**留 log(誰、何時、同意了什麼)**。這不只是「軌跡可審」,是**法律保護**:
- 真發生資料外洩 / 糾紛時,有「用戶在 [時間] 明示知情同意把 [什麼] 暴露給 [誰]」的證據 → 避免訴訟風險(責任在明示同意的用戶,不在 arcrun)。
- 「以後不要警示」這個選擇**本身也要 log**(用戶在 [時間] 選擇了不再對 [X] 警示 = 他知道風險並接受)。
- 同意 log 存放:與動作關聯(webhook record / recipe record 的 `exposure_consent: { confirmed_by, understood, confirmed_at, suppress_future }`)。
- 誠實限制同前:AI 能偽造 confirmed_by_human。但**法律意義上,憑證存在 + 可審 = 用戶有機會知情**,這道防線的價值是法律歸責不是技術防偽。
→ 這把 §1b 的 `exposure_confirmation` 升級為**帶法律意義的同意憑證**,所有暴露/送資料動作(recipe push / webhook 部署)共用此機制。
## 8. 警示是「保護措施的入口」(不只是攔)
警示訊息除了說明風險,**主動提供 arcrun 的保護措施**(產品價值,非只防呆):
```
⚠️ 這個動作會把 [X] 開放/送出。
arcrun 可以幫你保護它:
- 要求呼叫者帶 API Key(你發給特定對象)
- 設定權限 / 限流
一個動作就能加上。要加保護嗎?還是確認公開(如公開資料 API)?
```
(具體保護機制是 BACKLOG「用戶 API 保護機制」待決策項——本系統先在警示處**提示它存在**,實作後接上。)
@@ -0,0 +1,56 @@
# Requirements: 資料外流警示(Data Exfiltration Warning
> 2026-05-30 建立(richblack 確認)。資安優先、及早做。
> 判準源:DECISIONS §7(讓 AI 不做歪 + 閉環)、§0(減少不可控依賴 / 風險);BACKLOG 步驟 5b。
---
## 背景與風險
arcrun 讓「產生 API / 把資料送出去」變很簡單(一堆資料 + webhook trigger = APIrecipe = 打某 endpoint)。
**這個「簡單」本身是風險**:簡單到 AI 可能在用戶不知情下,把含個資的東西變成可被呼叫的 endpoint,或把
敏感資料送到非預期對象。
**richblack 的核心情境**:用戶有個 Google Sheets 存所有朋友的個資。正確用法是「call 它查詢」(自用)。
但若 AI 把它變成一個 recipe / workflow**送資料到非預期對象**(公開 webhook、公司群、外部 endpoint),
且**沒做認證** → 所有資料裸奔。
**關鍵原則(richblack 2026-05-30):**
1. **不分公私庫都要警示**。私人庫(公司用)一樣會出事(不小心把個資 POST 到公司群)。觸發點不是「推公共」,
是「這動作會讓資料流向某處」。
2. **不禁止**用戶公開 / 送資料(他要放什麼給誰是他的自由)——**但要確定他自己明示同意了**,不是 AI 替他決定。
3. **兩道防線**(a) hook 在 AI 動手前警告(防在前);(b) API 層在資料真送出前攔(防在後)。不論哪條路都攔。
## 需求
### R1 — API 層警示(資料送出前需人類同意,不分公私庫)
任何「把資料送往某目的地」的 API 動作,在執行/儲存前需人類明示同意。涵蓋(待 design 盤準):
- `acr recipe push`recipe 定義一個 endpoint(資料去向)。
- `acr push`workflow):workflow 可能含「讀敏感源 → 送往某 endpoint」的節點。
- webhook trigger 部署:把 workflow 變成可被呼叫的 API。
- 未來「recipe 貢獻公共庫」路徑(BACKLOG 飛輪項)。
同意內容要讓人看得懂風險:**這個動作會把 [什麼資料] 送到 [哪個目的地],需不需要 credential 保護?確認?**
### R2 — hook 在 AI 動手前警告
AI 要做這類動作(寫含外部 endpoint 的 recipe/workflow、跑會送資料的指令)前,hook 先警告 + 要人類確認。
這是「防在前」——在 AI 本機動手當下就提醒,不等到 API 層才攔(省一趟、且更早讓人看到)。
### R3 — 誠實的偵測範圍(不假裝能全防)
「敏感資料送到非預期對象」機械上難完整偵測。本系統做**可機械判斷的部分**,誠實標明擋不住的:
- 能偵測:動作含「外部 endpoint」(recipe endpoint / workflow http 節點 / webhook 對外)。
- 難偵測:「這份資料是否敏感」「這個目的地是否非預期」——這需要語境,機器判不準。
- 做法:偵測到「資料 → 外部目的地」的動作就警示,由**人類**判斷該資料/目的地是否該放行(人類同意是判斷點,機器只負責「攔下來問」)。
### R4 — 不擋自用、不製造過度摩擦
- 純自用、不送資料出去的動作(acr run 本機、查詢類)不該被警示淹沒。
- 警示要精準在「資料外流」動作,不是每個動作都問(否則用戶會盲目按 yes,警示失效)。
### R5 — 與既有閘門一致
- 與「建零件人類閘門」(component-gatekeeping)同精神:AI 不可替人類決定有外洩風險的動作。
- 與「arcrun 不做授權判斷」一致:不判斷「該不該送」,只「攔下來讓人類明示同意」。
## 非目標
- 用戶自己的 API 保護機制(入站認證:發 key 給別人 / 權限 / 限流)—— 另列 BACKLOG 待決策。
- 完整 DLP(資料外洩防護)系統 / 內容掃描判斷敏感度 —— 機器判不準,不做。
- recipe 公共貢獻路徑本身 —— 未實作(飛輪項),本系統只要求它未來內建同意閘門。
@@ -0,0 +1,40 @@
# Tasks: 資料外流警示
> 對應 design.mdrichblack 已 reviewQ1-Q3 + 法律憑證 + 保護入口決議)。
> 每完成一個 task 立刻標 [x],不批次。
---
## 共用:同意憑證機制(§7 法律憑證)
- [x] C1 定義 `exposure_consent { confirmed_by, understood, confirmed_at, suppress_future }` 型別
- [x] C2 同意 log 寫入動作關聯的 recordwebhook record / recipe record),可審
- [x] C3 「以後不要警示」(suppress_future)本身也 log(用戶知風險並接受)
## API 層警示(R1,防在後)
- [x] A1 webhook 部署(webhooks-named.ts POST)要 exposure_consent,缺且未 suppress → 拒絕 + 提示
- [x] A2 recipe push/recipes POST)同上(公私一視同仁)
- [x] A3 首次暴露某資源問、記住(exposure_confirmed / suppress_future)→ 之後不重問(§3)
- [x] A4 警示訊息說明風險 + 盤資料源(盡力,盤不出標「請自行確認」)+ **提示 arcrun 保護措施**(§8
## CLI 警示(互動 + 旗標)
- [x] B1 `acr push`:部署前互動式警示(首次某 workflow),人類確認組 exposure_consent 送出;`--confirm-exposure` 跳過互動(CI/非 TTY);`--suppress-warning` 記偏好
- [x] B2 `acr recipe push`:同上
- [x] B3 非 TTYAI 直跑)無 --confirm-exposure → 拒絕並提示「需人類確認暴露」
## hook(R2,防在前,Q3=偵測指令)
- [x] H1 pre-bash-guard:偵測 `acr push` / `acr recipe push` → 警示「這會把 X 變可被外部呼叫,需人類確認」
## mindset / 文件
- [x] M1 誠實限制(AI 能偽造 confirmed_by_human,靠憑證可審 + 法律歸責)寫進 mindset Skill(步驟 7+ 文件
## 驗收
- [ ] V1 acr push 部署 webhook(首次)→ 互動警示 + 說明暴露 + 提示保護 的終端輸出
- [x] V2 非 TTY 跑 acr push 無 --confirm-exposure → 拒絕的輸出
- [x] V3 webhook 部署 API 無 exposure_consent → 拒絕的輸出
- [x] V4 同一 workflow 二次部署 → 不重問(已記住)
- [ ] V5 --suppress-warning 後 → 不再警示,但 suppress 選擇有 log
- [x] V6 同意後 → exposure_consent 寫進 record 可查(法律憑證)
## 範圍邊界
- 不動用戶 API 入站保護機制(發 key/權限/限流)—— BACKLOG 待決策,本系統只在警示處「提示它存在」。
- 不擋出站 http 節點(不暴露我的資料)、不擋 acr run(本機)。
@@ -0,0 +1,642 @@
# Design: LI (LLM Interface) for arcrun
> v0.1 — 2026-05-16
> 對應 requirements.md(同目錄)
---
## 1. 設計哲學
| 過去 (UI 時代) | 現在 (LI 時代) |
|---|---|
| 使用者是人,要學軟體 | 使用者是 AI,要被軟體 onboard |
| 操作靠視覺、滑鼠 | 操作靠 MCP tool call |
| 錯誤訊息是技術 stack trace | 錯誤訊息是「下一步該做什麼」 |
| 文件是 long-form 教學 | 文件是結構化 schema + 可程式查 |
| 用戶教學是 onboarding 課程 | 「教學」是 MCP 工具自己會回 hint |
| 回饋靠 helpdesk 工單 | 回饋是 MCP toolAI 直接 call |
**原則**
1. **discoverable** — AI 不靠 grep / 不靠人就能知道有什麼
2. **idempotent** — 同樣輸入兩次結果一樣,可預測
3. **dry-run by default** — preview 是預設、commit 是 explicit
4. **structured errors** — error 是 JSON 含 next_actions,不是字串
5. **closed loop** — AI 卡住的 data 自動回流,平台 self-improving
---
## 2. 系統架構
```
┌────────────────────────────────────────────────────────────────┐
│ AI agent (Claude / Cursor / ...) │
└──────────────────┬──────────────────────────────┬───────────────┘
│ │
│ ① Read AGENTS.md │ ② MCP tool calls
▼ ▼
┌────────────────────────┐ ┌──────────────────────────────────┐
│ AGENTS.md │ │ arcrun-mcp (擴張 arcrun-mcp) │
│ (repo + KBDB block) │ │ 25 tools, 5 categories │
└────────────────────────┘ └──────────────┬───────────────────┘
┌────────┬─────────┬────────┴────────┬──────────┐
▼ ▼ ▼ ▼ ▼
┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌──────────┐
│ cypher- │ │registry │ │ kbdb │ │ Skills │ │ Examples │
│executor │ │ │ │ │ │ (KBDB) │ │ (git+KV) │
│ 31 路由 │ │ │ │ 50 路由 │ │ │ │ │
└─────────┘ └─────────┘ └─────────┘ └─────────┘ └──────────┘
│ ③ AI 部署完 / 卡住 call feedback
┌─────────────┴──────────────┐
│ report_feedback / agent-telemetry │
│ (寫進 KBDB) │
└─────────────┬──────────────┘
│ ④ 每週聚合
┌──────────────────────────────┐
│ agent_feedback_weekly_review │
│ (arcrun workflow, dogfood) │
│ → arcrun-roadmap KBDB block │
└──────────────────────────────┘
```
---
## 3. 五層 LI 模型
### Layer 0OnboardingAGENTS.md
**位置**
- `arcrun/AGENTS.md`repo 根,git 可讀)
- KBDB `type=agent-onboarding` blockMCP `get_onboarding_doc()` 拉)
**結構(控 5-8K tokens**
1. **30 字 What** — 「arcrun 是用 YAML 把零件串成 workflow 的平台」
2. **5 個核心概念** — component / cypher binding YAML / FOREACH / paused-resume / api_key
3. **連線**:「在你的 MCP config 加 `https://mcp.arcrun.dev/mcp`bearer `ak_xxx`
4. **第一個 workflowminimal example** — 10 行 YAML,含部署 + 觸發
5. **5 個 URL 慣例與為什麼**workers.dev vs arcrun.dev / KBDB endpoints
6. **常見錯誤 5 個 + 怎麼讀**
7. **不確定的時候**:明確列「先 call list_X 再下手」
8. **回報機制**:完成任何 workflow deploy / 卡住,**必須** call `report_feedback`
**部署機制**
- repo `arcrun/AGENTS.md` 是 source of truth
- GH Actions 監聽該檔變動 → 自動 PATCH KBDB onboarding block
- MCP `get_onboarding_doc()` 從 KBDB 讀(最新)
---
### Layer 1arcrun-mcp(主戰場)
#### 1.1 命名與部署
- **Server URL**: `https://mcp.arcrun.dev/mcp`(唯一 URL
- **舊 URL**`mcp.finally.click` 直接退場(leo 2026-05-16 拍板,理由:「finally.click 是還沒申請新網址的暫用,那是一個服務,arcrun 是底層」)
- **Worker rename**`u6u-mcp``arcrun-mcp`wrangler name 改),repo 路徑 `matrix/u6u-mcp/``matrix/arcrun-mcp/`
- **Tool prefix**: 統一 `arcrun_*`(單一 rename**不保留 `u6u_*` alias**,不留 deprecation 期。用戶量還很少,一次切換可接受。切換前在 mira / telegram 公告)
#### 1.2 Tool 完整清單(25 個,5 類)
##### A. Onboarding & Discovery5 tools
| Tool | 對應 HTTP | 用途 |
|---|---|---|
| `arcrun_get_onboarding` | KBDB GET `/blocks?type=agent-onboarding` | 拉 AGENTS.md 內容 |
| `arcrun_list_components` | registry GET `/components` | 所有可用零件 + 1 行 desc |
| `arcrun_get_component_contract` | registry GET `/components/:id` | input/output schema + gherkin tests |
| `arcrun_list_recipes` | cypher GET `/recipes` + `/auth-recipes` | API recipe 一覽 |
| `arcrun_search_examples` | KBDB POST `/search`type=workflow-example| 用 use case 搜範例 |
##### B. Workflow CRUD7 tools
| Tool | 對應 HTTP | 用途 |
|---|---|---|
| `arcrun_list_workflows` | cypher GET `/webhooks/named` | 用戶現有 workflow |
| `arcrun_get_workflow` | cypher GET `/webhooks/named/:name` | 拿 YAML / graph |
| `arcrun_validate_yaml` | cypher POST `/validate` | dry-run 校驗,不部署 |
| `arcrun_push_workflow` | cypher POST `/webhooks/named` | 部署(也含 validate |
| `arcrun_delete_workflow` | cypher DELETE `/webhooks/named/:name` | 刪 |
| `arcrun_preview_workflow` | **新增** POST `/preview` | 不寫 KV、模擬 execute |
| `arcrun_diff_workflow` | **新增** POST `/webhooks/named/:name/diff` | 既有 vs 新 YAML 差異 |
##### C. Execution & Trace5 tools
| Tool | 對應 HTTP | 用途 |
|---|---|---|
| `arcrun_run_workflow` | cypher POST `/webhooks/named/:name/trigger` | 觸發 + 回 execution_id |
| `arcrun_get_execution_trace` | **新增** GET `/executions/:id` | 結構化 traceper-node status |
| `arcrun_list_recent_executions` | **新增** GET `/workflows/:name/executions?limit=10` | 最近 N 次 |
| `arcrun_list_paused_executions` | **新增** GET `/executions/paused` | 卡 callback 的 |
| `arcrun_resume_execution` | cypher POST `/workflows/resume` | 手動 resume |
##### D. Component & Recipe Management4 tools
| Tool | 對應 HTTP | 用途 |
|---|---|---|
| `arcrun_search_components` | registry POST search | 語意搜尋 |
| `arcrun_publish_component` | registry POST publish | 上 WASM |
| `arcrun_create_recipe` | cypher POST `/recipes` | 建 API recipe |
| `arcrun_create_auth_recipe` | cypher POST `/auth-recipes` | 建 auth recipe |
##### E. Feedback & Skills4 tools
| Tool | 對應 HTTP | 用途 |
|---|---|---|
| `arcrun_report_feedback` | KBDB POST `/blocks`type=agent-feedback| AI 主動回報 |
| `arcrun_list_skills` | KBDB GET `/blocks?type=agent-skill` | 列可用 playbook |
| `arcrun_get_skill` | KBDB GET `/blocks/:id` | 拿 skill 內容 |
| `arcrun_publish_skill` | KBDB POST `/blocks`type=agent-skill| AI 把學到的存回去 |
#### 1.3 Tool contract 統一規範
每個 tool 都遵守:
**Input**
- Zod schema declarative
- 每個參數有 `.describe('...')`(給 AI 看的)
- optional 標 default
**Output(成功)**
```typescript
{
ok: true,
data: T,
hints?: string[], // optional next-step suggestione.g.「你剛部署了,可 call run_workflow 測試」
}
```
**Output(失敗)**
```typescript
{
ok: false,
error_code: 'enum_value',
human_message: string,
next_actions: string[], // 可程式化的下一步
detail?: unknown, // 原始錯誤(debug
}
```
#### 1.4 error_code enumv1,可加不可刪)
| error_code | 對應狀況 |
|---|---|
| `auth_missing` | api_key 沒帶 |
| `auth_invalid` | api_key 無效 |
| `not_found` | workflow / block 不存在 |
| `validation_failed` | YAML / schema 不過 |
| `component_not_found` | 零件 ID 沒對應 |
| `component_not_in_whitelist` | 零件存在但 cypher-executor 不認 |
| `paused_awaiting_resume` | workflow 在等 callback |
| `rate_limited` | 太頻繁 |
| `internal_error` | 平台 bug(含 detail |
| `dependency_unavailable` | 下游服務(claude / mira daemon / KBDB)掛 |
---
### Layer 2Skill blocksKBDB-native playbook
#### 2.1 Schema
```typescript
type AgentSkill = {
id: string;
type: 'agent-skill';
page_name: `skill-${slug}`;
content: string; // markdown playbook
tags_json: string[]; // ['watcher', 'debug', 'rag', ...]
source: 'manual' | 'auto-extracted';
metadata_json: {
when_to_use: string;
example_use_case: string;
contributed_by?: string; // agent user_agent
success_count?: number;
};
};
```
#### 2.2 種子 skill(先建 5 個,以 mira 經驗為本)
| skill page_name | 用途 |
|---|---|
| `skill-build_watcher_workflow` | cron 掃資料 → 觸發處理(mira_feed_watcher pattern |
| `skill-debug_paused_workflow` | claude_api / 任何 async callback paused 怎麼追 |
| `skill-migrate_http_to_trigger_workflow` | self-fetch 換 trigger_workflow(剛踩過的) |
| `skill-rag_with_arcrun` | 用 KBDB + claude_api 做 RAG |
| `skill-add_new_wasm_component` | 從 TinyGo 寫到 deploy 全流程 |
#### 2.3 自動萃取機制(後期)
`agent_feedback_weekly_review` 跑出來的 Top patternsLLM 包成 skill draftleo review 後 publish。
---
### Layer 3Examples(可搜尋的範例庫)
#### 3.1 存放
- `arcrun/registry/examples/{slug}/`
- `workflow.yaml`
- `description.md`(解決什麼問題、怎麼 trigger、預期結果)
- `tags.json``["webhook", "llm", "cron", ...]`
- CI build 出 `examples-index.json` 推 KBDBtype=workflow-example,內容是 YAML + tags
#### 3.2 搜尋
- `arcrun_search_examples(use_case)` 走 KBDB `/search`(既有 semantic
- 命中 → 回 YAML + description + 「你可以基於這個改 X」hint
#### 3.3 第一批範例(10 個)
| slug | 用途 |
|---|---|
| `webhook-to-slack` | 簡單轉發 |
| `cron-watcher` | mira_feed_watcher 簡化版 |
| `llm-classify` | claude_api 分類 |
| `rag-search-answer` | 從 KBDB 找 context → 回答 |
| `email-summary` | gmail → claude → telegram |
| `pdf-to-blocks` | convert → ingest |
| `github-issue-bot` | webhook → claude → 留 comment |
| `daily-digest` | cron → 多源聚合 → 推送 |
| `parallel-fanout` | 一份輸入分發多 workflow |
| `error-retry` | try_catch + wait + retry |
---
### Layer 4Telemetry & Feedback Loop
#### 4.1 兩條線
```
explicit feedback (AI call) implicit telemetry (platform auto-log)
│ │
▼ ▼
KBDB block type= KBDB block type=
agent-feedback agent-telemetry
│ │
└──────────────┬───────────────────────┘
agent_feedback_weekly_review
(arcrun workflow, 週一 9am cron)
LLM 聚合 + 分類
KBDB block type=arcrun-roadmap
leo 收到 + 寫進 mira 河道)
```
#### 4.2 agent-feedback schema
```typescript
type AgentFeedback = {
type: 'agent-feedback';
content: string; // description 原文
source: 'mcp-tool-call';
user_id: string; // 用戶 namespace
metadata_json: {
workflow_name?: string;
issue_type: 'doc_unclear' | 'tool_missing' | 'error_unhelpful'
| 'unexpected_behavior' | 'feature_request' | 'success_story';
retry_count?: number;
blocked: boolean;
suggested_fix?: string;
agent_user_agent: string; // 'claude-code/1.x' etc
};
tags_json: ['agent-feedback', `issue:${issue_type}`];
};
```
#### 4.3 agent-telemetry schema
```typescript
type AgentTelemetry = {
type: 'agent-telemetry';
source: 'cypher-executor';
metadata_json: {
event_type: 'deploy_success' | 'deploy_fail' | 'run_success'
| 'run_fail' | 'validation_error' | 'mcp_tool_call';
workflow_name?: string;
component_id?: string;
error_code?: string;
duration_ms: number;
api_key_hash: string; // 雜湊,不存原值
agent_user_agent: string;
};
};
```
寫入點:
- `webhook-handlers.executeWebhookGraph` 完成(成功 / 失敗都記)
- `routes/webhooks-named.ts` pushdeploy 記)
- `routes/cypher.ts` validate 失敗(validation_error 記)
- arcrun-mcp 每個 tool call wrap 記(mcp_tool_call
#### 4.4 隱私
- api_key 一律 SHA-256 截前 16 字元(不可逆,可聚合)
- workflow content 不 log(只記 name
- 用戶看自己的 telemetryMCP `arcrun_my_telemetry(limit)`,只回自己 hash
#### 4.5 weekly review workflow
```yaml
name: agent_feedback_weekly_review
description: 每週一聚合 AI 回饋 + telemetry,產出 Top 痛點
flow:
- "weekly_cron >> ON_SUCCESS >> fetch_feedback"
- "fetch_feedback >> ON_SUCCESS >> fetch_telemetry"
- "fetch_telemetry >> ON_SUCCESS >> aggregate"
- "aggregate >> ON_SUCCESS >> llm_summarize"
- "llm_summarize >> ON_SUCCESS >> publish_roadmap_block"
- "publish_roadmap_block >> ON_SUCCESS >> notify_telegram"
config:
weekly_cron:
component: cron
cron_expr: "0 9 * * 1" # 週一 9 am UTC
fetch_feedback:
component: kbdb_get
type: 'agent-feedback'
created_after: "{{ now - 7d }}"
limit: 500
fetch_telemetry:
component: kbdb_get
type: 'agent-telemetry'
created_after: "{{ now - 7d }}"
limit: 5000
aggregate:
component: filter # 或寫個 `group_by` 邏輯零件
items: "{{fetch_feedback.blocks}}"
group_by: 'metadata_json.issue_type'
llm_summarize:
component: claude_api
prompt: |
你是 arcrun 平台的回饋分析師。下面是這週 AI agent 的所有回饋 +
telemetry 失敗事件。請:
1. 列 Top 5 痛點(含證據引用)
2. 為每個痛點建議具體改動(文件 / MCP tool / 錯誤訊息)
3. 評估嚴重程度(blocked AI vs 略不便)
Feedback{{aggregate}}
Telemetry 失敗事件:{{fetch_telemetry.blocks}}
publish_roadmap_block:
component: kbdb_create_block
type: 'arcrun-roadmap'
page_name: 'roadmap-week-{{date.iso_week}}'
content: "{{llm_summarize.data.text}}"
notify_telegram:
component: telegram
chat_id: "{{ leo_chat_id }}"
text: "Arcrun 週報出爐:{{publish_roadmap_block.data.id}}"
```
**這個 workflow 是 dogfood inception**arcrun 自己跑分析 arcrun 自己。
---
## 4. Coverage Matrix(人類 vs AI
> 這份表是 NFR-5「覆蓋率可量化」的具體實踐。每個 release 重新檢視。
### 4.1 人類 GUI 能做的事 × LI 對應
| 人類在 arcrun-gui 做的 | LI MCP 工具 | 對等? |
|---|---|---|
| 看現有 workflow 列表 | `arcrun_list_workflows` | ✅ |
| 點開 workflow 看 YAML | `arcrun_get_workflow` | ✅ |
| 編輯 workflow YAML | `arcrun_push_workflow`(含 update | ✅ |
| 執行 workflow | `arcrun_run_workflow` | ✅ |
| 看執行結果 | `arcrun_get_execution_trace` | ✅(新增) |
| 翻零件庫 | `arcrun_list_components` + search | ✅ |
| 看零件 contract | `arcrun_get_component_contract` | ✅ |
| 上傳 WASM 零件 | `arcrun_publish_component` | ✅ |
| 設 credential | `arcrun_create_recipe` + (手動 push credential | ⚠️ 部分(credential 提交需安全考量,AI 不全自動) |
| 翻歷史執行 | `arcrun_list_recent_executions` | ✅(新增) |
| 看 paused 工作流 | `arcrun_list_paused_executions` | ✅(新增) |
| 手動 resume | `arcrun_resume_execution` | ✅ |
| 視覺化 canvas 拖拉 | — | ❌(純視覺,LI 不複製) |
| 看排版 prototype | — | ❌(同上) |
| Flip UI/Logic view | — | ❌(同上) |
| Action log(操作軌跡) | `arcrun_my_telemetry` | ✅(含更多資料)|
**Gap**:3 個視覺類動作 LI 不需要對等;其他全等。
### 4.2 cypher-executor 31 路由 × LI 暴露
| 路由 | LI 暴露? | 工具 |
|---|---|---|
| `/health``/docs``/openapi.json` | ❌ infra | — |
| `/execute` | ✅ | `arcrun_run_workflow`(內部) |
| `/cypher/search``/cypher/execute` | ✅ | discovery 內部用 |
| `/workflows/resume` | ✅ | `arcrun_resume_execution` |
| `/webhooks/named*` | ✅ | CRUD 對應 5 個 tools |
| `/webhooks/*` (anonymous) | ❌ deprecated | — |
| `/credentials*` | ⚠️ | 只 list + deletePOST 走人類流程(安全考量) |
| `/recipes*``/auth-recipes*` | ✅ | 4 個 tools |
| `/validate` | ✅ | `arcrun_validate_yaml` |
| `/auth/*``/register``/me*` | ❌ admin | — |
**新增需建路由**`/preview``/executions/:id``/workflows/:name/executions``/executions/paused``/webhooks/named/:name/diff``/my-telemetry`(共 6 個)
### 4.3 KBDB 50 路由 × LI 暴露
LI **不直接 expose KBDB 50 個路由**。AI 透過 arcrun-mcp 的 abstracted tool`get_skill` / `list_skills` / `report_feedback` 等)間接用 KBDB。
例外(值得直接 expose 的):
- `arcrun_kbdb_search(query)` — 走 `/search` semantic
- `arcrun_kbdb_get_block(id)` — 看 block 內容(debug 用)
KBDB schema 設計 / triplets / records 這些屬於另一個 SDDKBDB MCP),不在本 LI 範圍。
---
## 5. AGENTS.md 模板
```markdown
# Arcrun for AI Agents
## What is Arcrun (30 sec)
Arcrun lets you compose Cloudflare Workers (WASM components) into workflows via YAML.
You write YAML, push to KV, trigger via webhook or cron. Each component is a
TinyGo/AssemblyScript WASM with stdin/stdout JSON I/O.
## Connect (one step)
Add to your MCP config:
\`\`\`json
{
"mcpServers": {
"arcrun": {
"url": "https://mcp.arcrun.dev/mcp",
"headers": {"Authorization": "Bearer $ARCRUN_API_KEY"}
}
}
}
\`\`\`
Get your ak_ key at https://arcrun.dev/me.
## Your first workflow (5 min)
1. `arcrun_list_components()` → see what's available
2. Write 3-line YAML in chat (assistant does this)
3. `arcrun_validate_yaml(yaml)` → confirm
4. `arcrun_push_workflow(yaml)` → deploy
5. `arcrun_run_workflow(name, input)` → test
6. `arcrun_get_execution_trace(id)` → see results
## Core concepts (5 things)
- **Component**: WASM Worker. Use existing or `arcrun_publish_component`.
- **Cypher binding YAML**: triplets `A >> relation >> B` define graph.
- **FOREACH**: `>> 對每個 X >> next_node` iterates lists.
- **Paused-resume**: claude_api etc pause workflow; resumes via callback.
- **api_key**: ak_xxx, all calls need this in MCP auth.
## URL conventions (why this matters)
- `cypher.arcrun.dev` = orchestration API (you push here)
- `arcrun-{kebab}.{user}.workers.dev` = component workers (workers.dev avoids CF self-fetch)
- `kbdb-*.arcrun.dev` = KBDB component workers (your data lives here)
## Common errors
| error_code | What to do |
|---|---|
| `component_not_found` | call `list_components`, check spelling |
| `validation_failed` | read `next_actions` field, fix YAML |
| `paused_awaiting_resume` | normal for claude_api; wait or `get_execution_trace` |
| `auth_invalid` | ak_ wrong, re-fetch from /me |
## Unsure? Do this:
- Don't know what's available → `list_components` / `list_workflows` / `search_examples`
- Don't know syntax → `get_skill('skill-build_X')` for playbooks
- Hit weird behavior → **MUST** call `report_feedback(issue_type='unexpected_behavior', ...)`
## Feedback (required)
After every workflow deploy or debug session, call:
\`\`\`
arcrun_report_feedback({
workflow_name: '...',
issue_type: 'success_story' | 'doc_unclear' | 'tool_missing' | 'error_unhelpful' | 'feature_request',
description: '...',
blocked: false,
retry_count: N,
suggested_fix: '...' // optional
})
\`\`\`
Even "success" stories help — they tell us what's working.
```
---
## 6. Migration planu6u-mcp → arcrun-mcp 一次切換)
### 6.1 切換方式
leo 2026-05-16 拍板:u6u branding 整體退場,**單一 rename,不留 alias 也不留 deprecation 期**。
- Worker 部署 name 改:`u6u-mcp``arcrun-mcp`wrangler.toml
- 路由改:`mcp.finally.click/*``mcp.arcrun.dev/*`DNS + worker route 同步切)
- 舊 URL 退場:`mcp.finally.click` 不保留(要嘛 410 Gone,要嘛 301 redirect 到 arcrun.dev landing 一個說明頁)
- Tool 名一次改:所有 `u6u_*` rename 成 `arcrun_*`**舊名直接消失**
- repo 路徑改:`matrix/u6u-mcp/``matrix/arcrun-mcp/`
### 6.2 切換前須做
- [ ] 全 monorepo `grep u6u_` 確認所有 clientmira / 自家腳本 / leo 自己的 IDE 配置)
- [ ] 公告:在 mira 河道 + telegram 通知「ak_xxx 用戶請更新 MCP 配置:URL → mcp.arcrun.dev」
- [ ] 切換當天 worker 部署兩個 name(過渡 1 天可回滾),確認流量切完才把舊 worker disable
### 6.3 為什麼不做 deprecation
- 用戶量極少(dogfood 階段,主要是 leo 自己)
- 留 alias 會讓新 AI agent 學到舊名,違背「LI 一致性」原則
- 90 天監控成本 > 一次切換 + 公告
### 6.2 新增 tools(按 phase
Phase 1gap-fill):
- `arcrun_validate_yaml`(既有 `/validate` 已存,包 MCP 即可)
- `arcrun_get_execution_trace`(需 cypher-executor 新加 `/executions/:id`
- `arcrun_list_recent_executions`(新 endpoint
- `arcrun_list_paused_executions`(新 endpoint
- `arcrun_report_feedback`(新 tool
- `arcrun_get_onboarding`KBDB read
Phase 2advanced):
- `arcrun_preview_workflow`(新 endpoint,沒 KV side-effect
- `arcrun_diff_workflow`
- `arcrun_my_telemetry`
- `arcrun_search_examples`KBDB write + search 已存)
- `arcrun_list_skills` / `arcrun_get_skill` / `arcrun_publish_skill`
Phase 3auto-loop):
- `agent_feedback_weekly_review` workflow
- `arcrun-roadmap` block 生成
- LLM extract skill 自動化
---
## 7. 開發順序與里程碑
### Milestone 1:可量測(1 週)
- 寫 AGENTS.md v1
- 加 implicit telemetry 寫入點(cypher-executor
- 加 `arcrun_report_feedback` MCP tool
- 建 `agent-feedback` / `agent-telemetry` KBDB template
- 開始收 data
### Milestone 2gap-fill1 週)
- arcrun-mcp 補上 6 個 Phase 1 tools
- 新 endpoints 在 cypher-executor 加
- 每個 tool 結構化 error contract
### Milestone 3skill + example1 週)
- 種子 5 個 skill blocks
- 種子 10 個 example workflows
- `search_examples` 跑通
### Milestone 4closed loop(半週)
- weekly_review workflow 部署
- 第一份 arcrun-roadmap block 產出
- leo 收到第一份週報
### Milestone 5rename + cleanup1 週)
- arcrun-mcp → arcrun-mcp 公開
- 舊 tool 加 deprecation warning
- AGENTS.md 同步 KBDB
---
## 8. 決策紀錄
### 8.1 已拍板(2026-05-16 leo
| 決策 | 結果 | 理由 |
|---|---|---|
| MCP server URL | `mcp.arcrun.dev` 單一 URL,舊 `mcp.finally.click` 直接退場 | finally.click 是還沒申請新網址的暫用,那是一個服務,arcrun 是底層 |
| u6u branding | 整體退場改 arcrunrepo / worker / tool 命名一次 rename | u6u 不存在了 |
| Deprecation 期 | 不留(一次切換 + 公告) | 用戶量極少,留 alias 反而讓新 AI 學到舊名 |
| 擴張 vs 建新 | 擴張既有 u6u-mcprename 成 arcrun-mcp | 不 fork,零移轉痛 |
| AGENTS.md 位置 | repo `arcrun/AGENTS.md` + 自動同步 KBDB block | 兩面都拿到(git-access 也 OK,純 MCP 也 OK |
| feedback 寫入 auth | 要(驗 ak_ 存在即可,不查餘額) | 防 spam |
### 8.2 仍可商議(小議題)
| 議題 | 建議 |
|---|---|
| LI 是否包含 KBDB MCP | 部分(abstracted),KBDB MCP 另立 SDD `kbdb-llm-interface` |
| Telemetry 保留多久 | 90 天 hot + 1 年 cold archive |
| AGENTS.md 第一版用中文還是英文 | 中文(leo 自家 + mira 一致),英文版 v2 開源時補 |
---
## 9. 跨 SDD 連動
| 其他 SDD | 連動點 |
|---|---|
| `credential-primitives-wasm` | LI 不重做 auth,只用 `auth_static_key` 等既有零件 |
| `recipe-system` | LI 暴露 recipe CRUD tool 對應 |
| `component-registry-canon` | LI `list_components` / `get_contract` 走 registry |
| `resumable-workflow` | LI `list_paused_executions` / `resume` 用 |
| `arcrun-platform-evolution` | LI 是 evolution 之一,未來分 user-tier 時要考慮 |
| **mira-app** (polaris/mira) | LI 是 mira dogfood 痛點轉化的產物,roadmap 含對 mira 的回饋 |
@@ -0,0 +1,246 @@
# Requirements: LI (LLM Interface) for arcrun
> 把 arcrun 平台對「AI 操盤手」的完整使用面,當成 first-class product 設計。
> 對比:以前做網站要 user-friendlyUI),現在 AI 是主要用戶(LI)。
---
## 背景
arcrun 是 n8n-like workflow 平台,平台本身的 end-user 有兩類:
- **人類**:透過 arcrun-gui canvas / arcrun.dev landing 操作
- **AI agent**:透過 MCP / API 操作(Claude Code、Cursor、Codex、自製 agent 等)
過去設計集中在「人」(UI / docs)。AI 對 arcrun 的「可用性」沒被當第一公民。
### 證據:3 天 mira dogfood 累積的 14 個痛點(baseline
| 編號 | 痛點 | 性質 |
|---|---|---|
| 1 | 新零件 deploy 要手動 dashboard 點 workers.dev | platform DX |
| 2 | CF 同 zone self-fetch 死鎖 | platform infra |
| 3 | `http_request` 4xx 回 success=true cascade | error semantics |
| 4 | interpolateString array stringified | dev experience |
| 5 | `kbdb_upsert_block` 沒在 `WASM_HTTP_RUNNER_IDS` 白名單 | discovery |
| 6 | `resumeFromPaused` paused_node_id 沒 namespace | implicit behavior |
| 7 | `acr validate` 不認「對每個 X」 | validator stale |
| 8 | cron 從零寫起 | feature gap |
| 9 | CF Pages 沒接 auto-deploy | infra setup |
| 10 | watcher self-fetch 死鎖(剛解) | architecture |
| 11 | skill 改要寫 python script | tooling gap |
| 12 | 不知道有什麼零件可用 | **LI gap** |
| 13 | 不知道現有 workflow 怎麼長 | **LI gap** |
| 14 | trace / log 看不到結構化的 | **LI gap** |
**14 個裡有 7 個(編號 3-7, 12-14)純粹是 LI 缺失**。如果 LI 完整,等於把 50% 的踩坑時間還給開發者。
---
## 目標用戶(personas
### P1Claude Code / Cursor 等 IDE-embedded AI(主力)
特徵:
- 跟 leo 一起 pair programming
- 有 file access、shell 能力
- 透過 MCP / curl / SDK 跟 arcrun 對話
需求:
- 知道 arcrun 全貌(不靠人類解釋)
- 知道目前用戶有什麼資產(workflow / component / template / credential
- 寫 YAML 不靠猜
- 部署前能 dry-run
- 部署後能看執行結果
- 卡住能 introspect + ask
### P2:用戶私人 agent(次要,未來)
特徵:
- 在 mira / 用戶 app 內跑
- 透過 MCP 對 arcrun
- 沒有檔案系統,只有 HTTP
需求:基本同 P1,但**完全靠 MCP**(沒 grep / file read fallback
### P3:自製腳本 / SDK 使用者
不在本 SDD 範圍。SDK 既有規格走 `.agents/specs/arcrun/sdk-and-website/`
---
## 範圍(系統涵蓋)
LI 不是「新建一個 service」,是**跨 5 個既有系統的橫向 layer**:
| 系統 | 既有狀態 | LI 涵蓋的部分 |
|---|---|---|
| **arcrun cypher-executor** | 31 HTTP 路由 | 哪些對外、哪些 AI 該看得到 |
| **arcrun registry** | component 管理 Worker | discovery layer |
| **arcrun-mcp**(目前 `matrix/u6u-mcp/`,本 SDD M5 改名) | 15 MCP toolsHTTP→MCP 薄包裝) | 主擴張面(gap-fill) |
| **arcrun-gui**(目前 `matrix/u6u-gui/`,本 SDD M5 改名) | 人類 canvas IDE | 取其 endpoint 觀念,不取其 UI 元素 |
| **kbdb** | 50 個 HTTP 路由 | KBDB 該不該直接給 AI、用什麼姿勢 |
| **arcrun CLI (acr)** | Node CLI | LI 不依賴 CLICLI 是人的工具) |
---
## 非範圍(這個 SDD 不處理)
- 不處理 GUI 設計 / 視覺
- 不處理 SDKPython/JSAPI 設計
- 不處理 user OAuth flow
- 不重新設計 KBDB schema(只決定 LI 該包什麼 API)
- 不重新設計 cypher binding 語法
---
## 功能需求(FR
### FR-1AI 一條指令就能上手 arcrun
- 提供 `AGENTS.md`onboarding doc),AI 載入就能用
- MCP server URL 寫進 doc,「直接 connect」一步搞定
- 不需讀 SDD / 不需 grep codebase / 不需問人
### FR-2:完整 CRUD + Discovery 對等
人類在 arcrun-gui 能做的 8 個動作(list_workflows / get / update / execute / search_components / get_component / list_templates / list_credentials),**MCP 至少同等覆蓋**,不能有「人類能做但 AI 不能做」的 gap。
### FR-3Dry-run 是預設行為
- `validate_workflow(yaml)` MCP tool(取既有 `/validate` 路由)
- `preview_workflow(yaml, input)` 不寫入 KV,模擬執行
- AI 養成習慣「先 dry-run 再 push」
### FR-4:可診斷的 trace
- `get_execution_trace(execution_id)` 回結構化 JSON:每個 node 的 status / input / output / error / duration
- `list_paused_executions()` 列卡住的執行(callback 沒回時 debug 用)
- 不靠 `wrangler tail` 純文字
### FR-5:錯誤訊息是「給 AI 看的下一步」
所有 MCP tool error response 必須含:
- `error_code`(穩定字串 enum,可程式化 catch)
- `human_message`(描述)
- `next_actions`(陣列,可選:「call X」/「執行 Y」/「修改 Z」)
範例:
```json
{
"error_code": "component_not_in_whitelist",
"human_message": "零件 'filter' 不在用戶可用清單",
"next_actions": [
"call list_components() 看完整可用清單",
"若該零件需平台啟用,告訴用戶執行 `acr enable filter`",
"若是自製零件,先 push 零件再 push workflow"
]
}
```
### FR-6AI 可以回報問題
- `report_feedback(workflow_name, issue_type, description, ...)` MCP tool
- 結構化 enum issue_type(防自由文字難聚合)
- 寫進 KBDB 成 `agent-feedback` block,可被定期 review
- AI 規範「順利 / 卡住 / 不確定 都該 call」(透過 AGENTS.md 強制)
### FR-7Implicit telemetry 不依賴 AI 自覺
- cypher-executor 每次 deploy / execute / fail 自動寫 `agent-telemetry` block
- 含 client_user_agent(哪個 AI 用的)、error_message、duration
- 不依賴 AI 主動 call,平台自己收
### FR-8Skill blocks 可重用
- 「pattern / playbook」存 KBDB type=`agent-skill`
- MCP `list_skills(tag)` / `get_skill(id)` 給 AI 查
- 範例:`build_watcher_workflow` / `debug_paused_workflow` / `migrate_http_to_trigger_workflow`
### FR-9:範例庫可搜尋
- `search_examples(use_case)` 給 use case → 回相似 workflow YAML
- 範例存 `registry/examples/` git + 一份 index 在 KV
- 進階:semantic search(既有 KBDB `/search/embed`
### FR-10:定期 review 機制(自動化)
- `agent_feedback_weekly_review` workflow**arcrun 自己跑**
- 每週一聚合 feedback + telemetry,產出 Top N 痛點 + 建議
- 寫進 `arcrun-roadmap` KBDB block
---
## 非功能需求(NFR
### NFR-1:向下相容
LI 在「擴張」階段(M1-M4)不破壞既有:
- M1-M4 階段:arcrun-mcp 既有 15 tool 保留,新增 13 個 toolpre-rename
- arcrun-gui 不動
- cypher-executor 既有路由不改 contract(只加新 endpoint
M5rename)階段是 breaking changeleo 拍板),需事前公告。
### NFR-2transport 不鎖死
MCP 主,但 HTTP 同等可用(讓沒 MCP 客戶端的 agent 也可用)。每個 MCP tool 都對應一個 HTTP endpoint。
### NFR-3error contract 穩定
`error_code` enum 是 public API,加新值是 minor,移除值是 major。版本化。
### NFR-4feedback 數據可外部 export
`agent-feedback` / `agent-telemetry` 走 KBDB `/blocks` 標準 API,任何 AI / 人都能拉。不鎖死在 dashboard。
### NFR-5:覆蓋率可量化
- 「人類在 arcrun-gui 能做但 MCP 不能做」清單必須能列出
- 每次 LI 改動後,這份清單往 0 收斂
---
## 成功指標
| 指標 | 量測 | 目標 |
|---|---|---|
| 新 AI agent 上手時間(從 zero 到第一個 workflow 部署) | 手動實驗 | < 10 min |
| 一次 workflow 部署需要的 MCP call 次數 | telemetry | < 5 calls |
| AI 回報「文件不清楚」週次數 | feedback aggregation | 持續下降 |
| 「MCP 缺工具」issue 類型佔比 | feedback type ratio | < 10% |
| Implicit telemetry 與 explicit feedback 比例 | 計數 | 9:1 不算問題(implicit 量大正常) |
| Coverage gap(人類能做 vs AI 能做) | 手動審查 | 0 |
---
## 風險與假設
### 假設
- MCP 是主要 AI 接觸點(不是 SDK / CLI
- AI 願意按 AGENTS.md 規範 call feedback tool
- KBDB 能承載 telemetry 量級(短期:< 1000 events / day
### 風險
| 風險 | 應對 |
|---|---|
| AI 不照 AGENTS.md 規範用 | implicit telemetry 不依賴自覺,仍能收 data |
| MCP tool 設計錯誤越改越亂 | 每個 tool 有 contract testerror_code enum 版本化 |
| feedback 雜訊太多無法 review | review workflow 用 LLM 聚合,不靠人讀原始 |
| arcrun-mcp 重構搞壞既有用戶 | 用戶量還很少(dogfood 階段),一次切換可接受;切換前公告 |
| KBDB telemetry 量爆掉 | sample rate / 老資料自動 archive |
---
## 決策(2026-05-16 leo 拍板)
1. **deployment 名稱**:✅ `mcp.arcrun.dev` 單一 URL
- `mcp.finally.click` 直接退場,不並存
- 理由(leo):「finally.click 是還沒申請新網址的暫用,那是一個服務,arcrun 是底層」
2. **u6u branding 整體退場**:✅ u6u 命名全部改 arcrun
- `u6u-mcp` repo → `arcrun-mcp`
- `u6u-gui` repo → `arcrun-gui`
- 工具命名 `u6u_*``arcrun_*`**單一 rename,不留 alias 也不留 deprecation 期**
- 理由:「u6u 不存在,改成 Arcrun」
3. **AGENTS.md 位置**:✅ repo `arcrun/AGENTS.md` + 自動同步 KBDB block(兩面都拿到)
4. **feedback 寫入需 auth**:✅ 驗 ak_ 存在即可(防 spam,tier 低不查餘額)
@@ -0,0 +1,282 @@
# Tasks: LI (LLM Interface) for arcrun
> SDD: design.md + requirements.md(同目錄)
> 進度標記:`[ ]` pending / `[🔄]` doing / `[x]` done / `[⏸]` blocked
## 進度速覽(2026-05-16
- **M1 完成**AGENTS.md / telemetry helper / report_feedback tool 全部 deploy + e2e 驗證 ✅
- **M2.1 完成**3 個 introspection endpoints + index 強 consistent 修補 ✅
- **M2.2 部分**4 個 introspection + 5 個 CRUD = 9/13 tools,剩下 preview/diff/auth-recipes
- **M3.1/M3.3 完成**5 個 skill blocks + 10 個 example workflows ✅
- **M3.4 完成**sync-registry-to-kbdb.py 跑通,15 blocks 進 KBDB ✅
- **M4 完成**weekly_review workflow 跑通,產出第一份 arcrun-roadmap block ✅
- **M5 大 rename**repo / dir / SDD 已 renameWorker name 待後段 DNS 遷移
阻擋項:GH Actions 用戶層被 disableleo 申訴中)→ 改用本機 wrangler deploy + scripts/local-deploy.sh fallback。
---
## Milestone 1:可量測(先收 data)✅
### M1.1 AGENTS.md v1
- [x] 寫 `arcrun/AGENTS.md`5697355 + 3892dc3263 行)
- [ ] CI hookrepo `AGENTS.md` 變動 → 自動同步 KBDB block
- [ ] `arcrun_get_onboarding` MCP tool(讀 KBDB block
### M1.2 Implicit telemetry 收集 ✅
- [x] 建 KBDB block type=`agent-telemetry`slots 直接 metadata_json 不走 template
- [x] `webhook-handlers.executeWebhookGraph` 末尾加 telemetry(成功 / 失敗 / paused 都記)
- [x] `routes/webhooks-named.ts` push deploy 事件(deploy_success
- [x] `routes/validate.ts` validation 失敗事件(schema_failed / edge_node_missing
- [x] `hashApiKey` SHA-256 截 16 字元 helper
- [x] 隱私:只記 workflow name 不記 content
- [x] 實測:KBDB block `68635dcb-62e5-49ca-9c67-33f4ca82b7a0` event=run_success, paused_awaiting_resume
### M1.3 Explicit feedback tool ✅
- [x] KBDB block type=`agent-feedback`
- [x] arcrun-mcp tool `arcrun_report_feedback` (commit e637c3e)
- [x] Zod enum 鎖死 issue_type
- [x] user_id 從 partnerAuth 取
- [x] tags_json auto: ['agent-feedback', 'issue:{type}', 'blocked'?, 'wf:{name}'?]
- [x] schema 實測:KBDB block `80f1d2d1-c95a-4dfe-a889-d23b2e9b247d`
### M1.4 驗收 ✅
- [x] 觸發 mira watcher → KBDB agent-telemetry 即時出現
- [x] curl + python verify 8 個 telemetry blocksevent=run_success, workflow_name 對, duration_ms 對)
- [x] feedback block 寫入測 schema 通
---
## Milestone 2gap-fill(補 MCP 工具)
目標:人類 GUI 能做的,AI 透過 MCP 都能做。
### M2.1 新增 cypher-executor 路由 ✅
- [x] `GET /executions/:task_id` — 回結構化 paused state (989fbeb)
- [x] `GET /workflows/:name/executions?limit=10` — 走 ANALYTICS_KV stats:* prefix (989fbeb)
- [x] `GET /executions/paused` — 改 per-user index 強 consistent (4e7880c)
- [ ] `POST /preview` — dry-run,不寫 KV(暫緩)
- [ ] `POST /webhooks/named/:name/diff` — 新舊 YAML diff(暫緩)
- [ ] `GET /my-telemetry?limit=N` — 用戶自己看 telemetry(暫緩)
### M2.2 MCP tools ✅ (9/13)
完成(commit faf75cd + f91b1fd):
- [x] `arcrun_validate_yaml` — wrap /validate
- [x] `arcrun_get_execution_trace`
- [x] `arcrun_list_recent_executions`
- [x] `arcrun_list_paused_executions`
- [x] `arcrun_push_workflow` — wrap /webhooks/named POST(取代壞掉的 u6u_deploy_workflow
- [x] `arcrun_list_workflows`
- [x] `arcrun_get_workflow`
- [x] `arcrun_delete_workflow` (require confirm:true literal)
- [x] `arcrun_run_workflow` (paused 視為 success)
暫緩(等 endpoint 完成):
- [ ] `arcrun_resume_execution` — 包既有 /workflows/resume
- [ ] `arcrun_preview_workflow` — 待 M2.1 /preview
- [ ] `arcrun_diff_workflow` — 待 M2.1 diff
- [ ] `arcrun_list_recipes` / `create_recipe`
- [ ] `arcrun_list_auth_recipes` / `create_auth_recipe`
- [ ] `arcrun_my_telemetry`
### M2.3 Error contract 統一 ✅
- [x] `error_code` enum v1 定義在 design.md §1.4 + cypher-executor /executions/* 路由都用
- [x] arcrun-mcp `lib/cypher-client.ts` errorResponse() / successResponse() 統一 helper
- [x] 所有新 MCP tool10 個)都用統一 contractok, data?, error_code?, human_message?, next_actions?, hints?
- [ ] cypher-executor 既有 route(非 /executions/*)改成統一格式(暫緩)
- [ ] 每個 error_code 對應 unit test(暫緩)
### M2.4 驗收(部分)
- [ ] 模擬 zero-knowledge AI 跑 hello workflow(待 leo 提供 pk_live
- [ ] 量測:from list_components 到 run_workflow 成功 MCP call < 5
- [ ] 比較人類 GUI 路徑,clickwise 對等
---
## Milestone 3skill blocks + examples ✅
目標:AI 寫第一個 workflow 不靠猜,有範本和 playbook。
### M3.1 種子 skill blocks ✅ (commit 388c193)
- [x] `skill-build_watcher_workflow` — cron + 過濾 + trigger 模式
- [x] `skill-debug_paused_workflow` — claude_api callback 流程 + 怎麼追
- [x] `skill-migrate_http_to_trigger_workflow` — 從 self-fetch 換 trigger_workflow
- [x] `skill-rag_with_arcrun` — KBDB search + claude_api 組裝
- [x] `skill-add_new_wasm_component` — TinyGo 寫 + push + 註冊白名單
### M3.2 MCP tools ✅(2026-06-14 重寫修復,原打舊 v3 schema 已失效)
- [x] `arcrun_list_skills(tag?)` — mcp/src/tools/arcrun_skills_examples.ts
- [x] `arcrun_get_skill(slug)`
- [x] `arcrun_list_examples(tag?)` / `arcrun_get_example(slug)` / `arcrun_search_examples(query)`
- [ ] `arcrun_publish_skill` — AI 把學到的回存(未做,非阻擋)
> **2026-06-14 斷鏈修復(重要)**:這 5 工具早已建好並註冊上線(registry.ts),但打的是 KBDB v3
> `/blocks``/search` 路徑。KBDB 後來降基本盤(三表 entries/templates/records,無 blocks 表、
> 無語義 search)→ **5 工具全部對死 route 回 404(假綠:AI 看得到工具、叫了卻拿 404)**
> 已重寫改打基本盤 `/entries`entry_type 取代 typebase listEntries 加 page_name 過濾)。
> mcp tsc exit 0。**誠實降級**`arcrun_search_examples` 基本盤無語義 → 改 D1 LIKE 關鍵字 +
> slug/tag 加權(回 search_mode:keyword),embed 模組(kbdb-base Phase 1)上線後只換內部、簽名不變。
### M3.3 種子 examples ✅ (commit 388c193)
10 個範例都建立(webhook-to-http / cron-watcher / llm-classify /
rag-search-answer / email-summary / pdf-to-blocks / github-issue-bot /
daily-digest / parallel-fanout / error-retry),每個含 workflow.yaml +
description.md + tags.json。
### M3.4 examples 索引 + 搜尋 ✅(2026-06-14 重寫,原走已廢的 v3 零件)
- [x] scripts/sync-registry-to-kbdb.py — 把 registry/examples + skills 同步進 KBDB
- ~~走 kbdb-upsert-block.arcrun.dev~~**改打基本盤 KBDB Worker `/entries`**v22026-06-14)。
舊零件 worker 已不存在;改用 `entry_type` + `page_name` idempotency keyGET ?page_name= →
PATCH /entries/:id 或 POST /entries,腳本自行編排)。需 `KBDB_BASE_URL` 環境變數。
- examples → entry_type=workflow-example, page_name=example-{slug}
- skills → entry_type=agent-skill, page_name=skill-{slug}
- dry-run 實測:10 examples + 5 skills 全掃到(py_compile + --dry-run 通過)
- [x] `arcrun_search_examples(use_case)` MCP tool — 已補(見 M3.2 重寫;基本盤 LIKE 關鍵字版)
---
## Milestone 4closed loop ✅
目標:data 收得到 → 平台自己消化產出 roadmap。
### M4.1 Weekly review workflow ✅ (mira commit de11625)
- [x] 寫 `polaris/mira/arcrun/agent_feedback_weekly_review.yaml`
- [x] cron `0 9 * * 1` (台灣 17:00 週一)
- [x] `acr push` 部署
- [x] 手動觸發測試一次(5/6 nodes success,唯一 fail 是 notify_leo 缺 credential
### M4.2 LLM 聚合 prompt ✅
- [x] prompt 結構化:數字 / Top 5 痛點(含證據 / 嚴重度)/ 成功 pattern / 下週優先 3 件
- [x] 一律繁體中文 + 引用 block_id 為證據
- [x] 存 KBDB type=`arcrun-roadmap`, page_name=roadmap-latest(每週覆蓋)
- [x] 實測產出真有用:抓到「paused_awaiting_resume 語意不清」「data 量太少」「自動建議包 skill」三個真實 LI 改進建議
### M4.3 通知
- [x] notify_leo 節點:telegram chat_id from secret
- [ ] leo 補 telegram_bot_token credential 後生效
- [ ] 同時寫進 mira 河道(讓 leo 在熟悉介面看)— 暫緩
### M4.4 驗收
- [x] 第一次手動觸發 → 收到第一份 roadmap (KBDB block id e924c231-cf5e-4541-89d8-da550ecae2f3)
- [ ] cron 自動跑首次(下週一驗證)
- [ ] leo review 後挑 1-2 個 issue 修補
- [ ] 跑第二週 → 確認該 issue 從 top list 消失
---
## Milestone 5rename + cleanupu6u → arcrun 一次切換)
目標:完成 LI 品牌化,u6u branding 整體退場。**leo 2026-05-16 拍板:單一 rename,不留 alias 也不留 deprecation 期**。
### M5.1 切換前準備
- [ ] 全 monorepo `grep -rn "u6u_\|mcp\.finally\.click"` 列出所有受影響檔案 + 用戶配置
- [ ] mira 河道 + telegram 公告:「ak_xxx 用戶請更新 MCP 配置 URL → mcp.arcrun.dev」(**至少切換前 24h**
- [ ] 列 leo 自己的 IDEClaude Code / Cursor)配置位置,準備同步更新
### M5.2 Repo / Worker rename
- [ ] `matrix/u6u-mcp/``matrix/arcrun-mcp/` (git mv)
- [ ] `matrix/u6u-gui/``matrix/arcrun-gui/` (git mv)
- [ ] `arcrun-mcp/wrangler.toml`: name = `arcrun-mcp`
- [ ] `arcrun-gui/wrangler.toml`: name = `arcrun-gui`
- [ ] DNS`mcp.arcrun.dev` route 接到 arcrun-mcp worker
- [ ] CIdeploy.yml)若有寫死 path 同步改
### M5.3 Tool rename(一次切換)
- [ ] 所有 `u6u_*` MCP tool 改 `arcrun_*`(不留 alias
- [ ] AGENTS.md 全用新名
- [ ] `arcrun-mcp/README.md` / `GUIDE.md` 全部用新名
### M5.4 舊 URL 退場
- [ ] `mcp.finally.click` 接 410 Gone + 訊息「請改用 mcp.arcrun.dev」
- 或 301 redirect 到 arcrun.dev landing 一個說明頁
- [ ] DNS 紀錄保留 30 天(防意外 client 還沒切)後刪除
### M5.5 文件最終化
- [ ] `arcrun/AGENTS.md` 最終版發布
- [ ] `matrix/arcrun/.agents/specs/llm-interface/design.md` 加「實際部署狀態」附錄
- [ ] 寫一篇 retrospectiveLI 做完前後 AI 使用 arcrun 的 time-to-first-workflow 對比
### M5.6 連動(不在本 SDD 範圍但要追蹤)
- [ ] `matrix/arcrun/.agents/specs/arcrun-core-mvp/` SDD 改名(另立 task,跨 SDD rename
- [ ] `matrix/arcrun/.agents/specs/arcrun-platform-evolution/` 同上
- [ ] 兩個 SDD 重命名屬「u6u 品牌退場」系列,需要單獨追蹤 task
---
## Backlog(暫不排)
### B.1 KBDB MCP 獨立 SDD
- LI 範圍只包 KBDB 的 `agent-*` template
- 完整 KBDB AI 介面(type=note/page/triplet/template/record 等)另立 SDD `kbdb-llm-interface`
- 跟 mira KM 系統互動最密
### B.2 多 agent 隔離
- 多 AI 共用同 ak_xxx 時,telemetry 區隔 agent_user_agent
- 進階:每個 AI 子 namespacemira / cursor / 自製 agent
### B.3 AGENTS.md i18n
- v1 純中文(leo + 自家用)
- v2 英文版(給開源用戶)
### B.4 自動 skill 萃取
- weekly_review 產出的 pattern 自動包成 skill draft
- leo review approve → publish
### B.5 SDK 對等(python-sdk / js-sdk
- SDK 提供和 MCP 同樣的 25 個 method
- 給「不想用 MCP 的人」也能 AI-friendly
- 走 sdk-and-website SDD 範圍
### B.6 GUI side 補 LI 看板
- arcrun-gui 加 `/li-dashboard` 顯示用戶自己的 telemetry / feedback
- 不阻擋 LI 推出(leo 先看 KBDB 原始 block 即可)
---
## 依賴關係
```
M1 (data 收集)
M2 (MCP gap-fill)
M3 (skill + examples) ← 可與 M2 並行後段
M4 (closed loop) ←─── 需 M1 data 累積 1-2 週
M5 (rename)
```
---
## 工估算
| Milestone | 工 | 阻擋項 |
|---|---|---|
| M1 | 5 個工作日 | 無 |
| M2 | 5 個工作日 | M1 完(telemetry 先就位才好驗證 M2 改動) |
| M3 | 5 個工作日 | M2 完(tool 介面定型才寫 skill |
| M4 | 3 個工作日 | M1 data 累積 1 週 |
| M5 | 5 個工作日 | M2-M4 完 |
| **總** | **23 個工作日 (~5 週)** | |
實際視 leo 排程,可邊用邊改、不必一氣呵成。**M1 是硬前置**——資料不收,改了也不知道改對沒。
@@ -0,0 +1,240 @@
# SDD: arcrun Recipe System(容器 + Recipe 模式)
> 2026-05-07 建立。吃狗糧寫 wiki 合成 workflow 時撞牆發現的平台缺口。
> 核心原則:**一個 WASM 零件 = 容器,內容(recipe)存資料庫**。
> n8n 為每種 API 寫獨立 nodearcrun 走「容器 + recipe」減少零件數量。
---
## 1. 問題
### 1.1 撞牆現場
寫 mira wiki 合成 workflow7-B)時:
- 流程:`kbdb_get(stale)` → foreach → `kbdb_get(drafts)``claude_api(合成 prompt)``kbdb_ingest`
- 第三步要組 prompt`schema 內容 + skill 模板 + drafts array + existing_entities`
- cypher binding 內建 `{{var}}` 模板太弱(只支援 top-level,不支援嵌套 / array → string
- 沒有 `string_template` 零件、沒有 `array_to_markdown` 零件
- 寫專用 `wiki_prompt_builder` 零件 = 走 n8n 老路,每個 AI workflow 都要寫一個
### 1.2 根因
**arcrun recipe 系統只覆蓋 HTTP / auth 兩層**
| Recipe 種類 | 存哪 | 容器 | 狀態 |
|---|---|---|---|
| auth_recipe | RECIPES KV (`auth_recipe:{service}`) | auth_static_key / auth_oauth2 / ... | ✅ 已有 |
| api_recipe | RECIPES KV (`rec_{hash}`) | http_request | ✅ 已有(hard-code 在 cypher-executor 待清,Phase 1-3 處理)|
| **prompt_recipe** | ❌ 不存在 | claude_api(容器) | **缺** |
`claude_api` 零件目前吃 `prompt: string`(已組好的字串),沒有「recipe 模式」可以讓 AI 用「組合配方」的方式呼叫。
### 1.3 影響
- **致命**:寫不出第一個 wiki 合成 workflow7-B 卡關)
- **推廣破功**arcrun 對外 prop 是「容器 + recipeAI 不用寫 code」,但 prompt 這層做不到
- **未來所有 AI workflow 都會撞同樣問題**rss-tech-news 評語、河道 AI 副駕、ai-comment、文章摘要⋯ 全部需要組 prompt
---
## 2. 設計
### 2.1 核心:prompt_recipe 平行於 auth_recipe / api_recipe
**儲存**`RECIPES` KVkey 格式 `prompt_recipe:{name}`
**結構**
```yaml
id: prompt_recipe:wiki_synthesis
version: v1
description: "Mira wiki 合成(抽 triplet + 寫 wiki paragraph"
model: sonnet # haiku / sonnet / opusclaude_api 沿用既有 routing
# 從 KBDB / 其他來源取的 fragment(在 prompt 組合時抓並插入)
fragments:
- var: schema
source: kbdb_block
block_id: "7a4e456e-1b0f-406a-8842-5e01d1cf1eef" # mira-wiki-schema
field: content
- var: skill_template
source: kbdb_block
block_id: "85e3b81e-dca8-4131-bcdc-990bd0d3a16f" # source-skill-wiki-synthesis
field: content
# 從 workflow context 取(input/前置節點輸出)
inputs:
- var: drafts # 草稿 array
from: "ctx.read_drafts.blocks"
transform: "json_array" # 轉成 JSON array string
- var: existing_entities
from: "ctx.read_entities.blocks"
transform: "extract_field:page_name" # 抽 array 的 page_name 欄位 join 成 list
- var: entity_name
from: "ctx.loop.item" # foreach 迴圈當前元素
# 最終 prompt 由 fragments + inputs 套進 skill_template 組成
prompt_assembly:
system: "{{schema}}" # 直接用 schema 當 system prompt
user: "{{skill_template}}" # skill template 內含 {{drafts}} {{existing_entities}} {{entity_name}} 變數
# 期待輸出
output:
format: json # claude_api 自動 parse 為 object
schema: # zod-styleparse 失敗回 success:false
type: object
required: [triplets, entities, paragraphs, source_summary]
```
### 2.2 Recipe 解析在 cypher-executor(架構選擇 B
**設計決策**2026-05-07):recipe 解析跟 prompt 組裝**在 cypher-executor TS**,不改既有 claude_api WASM。
理由:
1. recipe 解析是 cypher-executor 既有 `api_recipe / auth_recipe` 同性質工作
2. 既有 claude_api 已部署 + 已測試,不動影響面最小
3. transform 邏輯(json_array / extract_field 等)TS 寫起來比 TinyGo 簡單 10 倍
4. 不違反 §1.6 — skill 還是 KBDB blockcypher-executor 只是組合者,不寫死 prompt
**流程:**
```
workflow YAML 節點 config 出現 `recipe: prompt_recipe:xxx`
cypher-executor graph-executor.ts
在執行該節點前 → 偵測 recipe 欄位 → 走 recipe expander
recipe expander(新 module
1. 從 RECIPES KV 抓 `prompt_recipe:xxx` 定義
2. 按 fragments 規則 → 用既有 KBDB client 抓 block content
3. 按 inputs 規則 → 從 context 取值 + 跑 transform
4. 組 system prompt + user prompt
5. 把 {prompt, model, mira_token, ...} 當作節點實際 input
loader 呼叫 claude_api 容器(不知道 recipe 存在,仍吃舊介面)
claude_api 容器 → Mira daemon → 回 LLM 結果
graph-executor 取結果 → 按 recipe.output 規則 parse JSON / 驗 schema
```
**對 claude_api 容器的影響**:完全沒有。它仍吃 `{mira_token, prompt, model}`
**對 workflow 作者的體驗**
```yaml
config:
synthesize:
component: claude_api
recipe: "prompt_recipe:wiki_synthesis" # ← cypher-executor 偵測到這欄位,自動解析
mira_token: "{{secret.mira_token}}"
```
不寫 recipe 走舊路:
```yaml
config:
reply:
component: claude_api
prompt: "{{ctx.user_message}}" # ← 沒 recipecypher-executor 直接透傳
mira_token: "{{secret.mira_token}}"
```
### 2.3 Workflow YAML 體驗
```yaml
name: wiki_synthesis
flow:
- "input >> 完成後 >> read_stale"
- "read_stale >> 對每個 >> read_drafts"
- "read_drafts >> 完成後 >> synthesize"
- "synthesize >> 完成後 >> write_wiki"
config:
read_stale:
component: kbdb_get
page_name: "mira-wiki-index-stale"
read_drafts:
component: kbdb_get
page_name: "{{loop.item}}" # entity name
synthesize:
component: claude_api
recipe: "prompt_recipe:wiki_synthesis" # ← 重點:指 recipe,不寫 prompt
mira_token: "{{secret.mira_token}}"
write_wiki:
component: kbdb_ingest
text: "{{prev.paragraphs}}"
```
**AI 寫這 workflow 只需要:**
1. 知道有 `kbdb_get / claude_api / kbdb_ingest` 三個容器(MCP search 找得到)
2. 知道有 `prompt_recipe:wiki_synthesis` 這個配方(MCP search 找得到)
3. 不需要懂 prompt 怎麼組、不需要看 wiki schema 文字
### 2.4 Recipe 是 KBDB block 還是 KV
**選 KV**`RECIPES` namespace),跟既有 auth_recipe / api_recipe 一致:
- key: `prompt_recipe:{name}`
- value: YAML/JSON
- CLI 跟 MCP 用既有 `recipe push` / `recipe list` 工具管理(不需新工具)
**不選 KBDB block**
- 雖然 polaris/mira/CLAUDE.md §1.6 說「source-skill 存 KBDB block」
- 但 §1.6 講的是 mira 業務的 skill templateschema / skill 模板)
- recipe 是「組合配方」(指向哪些 block + 怎麼組),是 platform 層
- recipe **裡面** 引用 KBDB block idfragments.source: kbdb_block)— 兩層關係清楚
---
## 3. 範圍邊界
**在本 SDD 範圍內:**
- ✅ Phase 1: prompt_recipe schema + RECIPES KV 規範
- ✅ Phase 2: claude_api 改吃 recipe(向後相容舊 prompt 參數)
- ✅ Phase 3: 寫第一個 recipe `prompt_recipe:wiki_synthesis`
- ✅ Phase 4: 用此 recipe 完成 mira 7-B workflow
- ✅ Phase 5: MCP 加 recipe 管理 toollist / get / push / delete prompt_recipe
**不在範圍內:**
- HTTP api_recipe / auth_recipe 改造(已有,不動)
- 多模態 promptimage input)— 等 P2
- recipe 沙盒驗收(recipe 是資料不是 code,不需要)
**前置依賴(已完成):**
- ✅ kbdb_get 零件(5.3
- ✅ component-registry MCP backfillcomponent-registry-canon Phase 1
---
## 4. 為什麼這個設計重要
| n8n | arcrun |
|---|---|
| Gmail node、Slack node、OpenAI node、Anthropic node、各 LLM node ⋯(每種 API 一個 node| `http_request` 容器 + 各 service 的 api_recipe |
| 每個 LLM 用法新 nodechat / completion / embedding| `claude_api` 容器 + 各用途的 prompt_recipe |
| AI 要學「Gmail node 怎麼用」「Slack node 怎麼用」⋯ | AI 要學「容器 + 配方」一次學會 |
| 零件數爆炸(500+) | 容器固定(< 30),配方無限擴充 |
| 配方藏在程式碼 | 配方在 KV,AI 直接 CRUD |
**對 AI 推廣**:第三方 AI 看到「30 個容器 + 100 個配方」遠比「500 個 node」好理解,且配方是文字資料不是 code,AI 寫配方比寫 node 簡單。
---
## 5. 風險與緩解
| 風險 | 緩解 |
|---|---|
| recipe 結構過度複雜,AI 寫不出來 | Phase 3 寫第一個 recipewiki_synthesis)作為範本,未來 AI 抄 |
| 向後相容讓 claude_api 變兩條路 | 內部統一用 recipe path,舊 prompt 參數 → 自動轉成 inline recipe |
| recipe 引用 KBDB block id 寫死,block 改 id 就壞 | KBDB block 用 `page_name` 識別比 id 穩定,recipe 支援 `block_page_name` 欄位 |
| KV 寫入頻繁的 transform 邏輯(json_array, extract_field:x)→ 變 mini DSL | 限制 transform 種類(10 個內),列白名單,超過就請寫零件 |
---
## 6. 變更紀錄
| 版本 | 日期 | 內容 |
|---|---|---|
| v1.0 | 2026-05-07 | 初版。吃狗糧寫 wiki 合成 workflow 撞到「prompt 組裝缺口」,補 prompt_recipe 層平行於既有 auth_recipe / api_recipe。 |
| v1.1 | 2026-05-07 | 架構選擇 Brecipe 解析在 cypher-executor TS(不改 claude_api WASM)。減少改動面、可單元測試、跟既有 api_recipe 同層次。 |
@@ -0,0 +1,110 @@
# Tasks — Recipe System (容器 + Recipe 模式)
> 對應 SDD[design.md](design.md)
> 上次更新:2026-05-07
**狀態 legend**`[ ]` 待辦 / `[🔄]` 進行中 / `[x]` 完成
---
## Phase 1prompt_recipe Schema + KV 規範
- [x] 1.1 寫 `cypher-executor/src/lib/prompt-recipe-schema.ts`85 行 Zod schemafragments / inputs / prompt_assembly / output + transform 白名單 7 個)
- [x] 1.2 確認 cypher-executor wrangler.toml 已有 RECIPES KV binding
- [x] 1.3 寫 recipe loader (`recipe-loader.ts` 50 行) + transforms (`recipe-transforms.ts` 58 行) + expander (`recipe-expander.ts` 127 行)
- transform 7 個:json_array / to_string / join / markdown_list / extract_field / first / pluck_content
- expanderfragments(KBDB) + inputs(context+transform) → 套 {{var}} 模板 → {prompt, model, output_*}
- type-check 全通過
## Phase 2cypher-executor recipe expander(架構選擇 B,不改 claude_api
- [x] 2.1 寫 `recipe-expander.ts`127 行:load → fragments → inputs+transform → 套模板 → 回傳 prompt+model+output_*
- [x] 2.2 寫 `recipe-transforms.ts`58 行:7 個 transform
- [x] 2.3 改 `graph-executor.ts` Component case:偵測 `node.data.recipe` → 呼叫 expandPromptRecipe → merge 進 mergedContext
- [x] 2.4 output parser hook:執行完若 `_recipe_output_format === 'json'` 自動 parse + required_fields 驗證
- [x] 2.5 部署 cypher-executor v426b099e
- [x] 2.6 端對端驗證:用 curl 打 `/cypher/execute` 帶 recipetrace 顯示 recipe 展開正確 + claude_api 拿到組好 promptMira daemon 端 522 timeout 是 daemon 問題,不是 recipe 系統)
- [x] 2.7 [紅利修復] cypher-executor `WASM_HTTP_RUNNER_IDS` 加 5 個 mira 零件(claude_api / kbdb_*)— 短期解,根本修法見 KI-13
## Phase 3:第一個 recipe — wiki_synthesis
- [x] 3.1 寫 `polaris/mira/recipes/wiki_synthesis.json`4 fragments + 4 inputs + system/user template + json output
- [x] 3.2 用 `wrangler kv key put --remote` 推進 RECIPES KV (key: `prompt_recipe:wiki_synthesis`)
- [x] 3.3 確認 KV 寫入成功(wrangler kv get 驗證)
- [ ] 3.4 不適用(架構選擇 B 不改 claude_apirecipe 在 cypher-executor 解析)
- [x] 3.5 端對端測試:用 MCP `u6u_execute_workflow` 跑 wiki_synthesis 成功
- input1 句草稿(黃仁勳 GTC 2026 物理 AI
- output3 triplets + 3 entities + 1 wiki paragraph + source_summary
- 過程修了 KI-14 (service binding 指錯)、KI-15 (token 沒轉發)、KI-16 (Claude markdown fence 沒剝)
## Phase 4mira 7-B 用 recipe 完成 wiki workflow
- [🔄] 4.1 寫 `polaris/mira/workflows/wiki_synthesis.yaml`cypher binding YAML
- 用 `recipe: prompt_recipe:wiki_synthesis` 指 recipe
- 4-5 個節點:read_stale → foreach → read_drafts → synthesize → write_wiki + log
- [ ] 4.2 用 MCP `u6u_execute_workflow` sandbox 跑(試一個 entity 不真寫 KBDB
- [ ] 4.3 用 MCP `u6u_deploy_workflow` 部署到 cypher-executor
- [ ] 4.4 手動觸發 cron,驗 wiki page 真的出現
- [ ] 4.5 在 mira/wiki/ 前端看到第一張 AI 合成 wiki page
## Phase 5MCP recipe 管理 tools
- [ ] 5.1 加 MCP tool `arcrun_list_recipes(prefix?)`:列所有 prompt_recipe
- [ ] 5.2 加 MCP tool `arcrun_get_recipe(name)`:取單一 recipe 內容
- [ ] 5.3 加 MCP tool `arcrun_push_recipe(name, yaml_content)`upsert recipe
- [ ] 5.4 加 MCP tool `arcrun_delete_recipe(name)`
- [ ] 5.5 既有 auth_recipe / api_recipe 也通用同套 tool(不只 prompt_recipe
---
## 風險追蹤
- 風險 1claude_api 改造跟 mira-app 同時動,可能影響河道 AI 副駕
- 緩解:向後相容,舊 input 仍可用,mira 河道先不切 recipe
- 風險 2recipe transform 白名單漏了某種需求
- 緩解:發現缺什麼再加,第一版優先支援 wiki 用到的(json_array, extract_field, join
- 風險 3:KV 跟 KBDB 都存配置,AI 困惑「該存哪邊」
- 緩解:清楚分層 — recipe(容器組合方式) KVdataschema 文字、skill 模板) KBDB
---
## Known Issues(吃狗糧發現,記錄)
### KI-11MCP `u6u_execute_workflow` 不暴露 config 欄位 ✅ 修復(2026-05-07
- 已修:tool schema 加 optional `config: Record<string, Record<string, any>>`
- 部署:u6u-mcp v11d7e366
- 用戶要重啟 client session 才能看到新 schema
### KI-12MCP execute 路由打 `/execute` 而非 `/cypher/execute` ✅ 修復(2026-05-07
- 已修:service binding fetch URL 改成 `http://cypher-executor/cypher/execute`
- 部署:u6u-mcp v11d7e366
### KI-14u6u-mcp service binding 指向已廢棄的 inkstone-cypher-executor ✅ 修復
- 現象:MCP 路徑跑 workflow trace 顯示 synth 變 Output、config 被忽略
- 根因:`u6u-mcp/wrangler.toml` services binding 是舊 worker `inkstone-cypher-executor`,不是現役 `arcrun-cypher-executor`
- 解法:改 service name + redeploy
### KI-15u6u-mcp 沒把 partner token 轉發給 cypher-executor ✅ 修復
- 現象:recipe expander 抓 KBDB block 401(沒 auth
- 根因:partnerAuthMiddleware 驗完 token 但只 set org_namespace,沒留 tokenexecute_workflow tool fetch 沒帶 X-Arcrun-API-Key
- 解法:middleware 也 set partner_token、handleMcpRequest + registerAllTools + execute_workflow 多一個 partnerToken 參數、fetch header 加 X-Arcrun-API-Key
### KI-16Recipe JSON output 被 Claude 包在 ```json``` markdown fence ✅ 修復
- 現象:JSON.parse 失敗 "Unexpected token \`"
- 根因:Claude 預設輸出 ```json\n{...}\n``` 包裝
- 解法:cypher-executor 解析前 regex 剝 fence
### KI-13cypher-executor `WASM_HTTP_RUNNER_IDS` 寫死白名單
- 現象:每加新零件要回 cypher-executor 改白名單 + 重部署
- 影響:違反 arcrun「容器+ recipe,新零件無需改 platform」承諾
- 短期解:手動加進白名單(claude_api / kbdb_* 已加)
- 根本解:改成從 component-registry KV 動態查 canonical_id
- 優先級:P1(推廣破口),需新 SDD `cypher-executor-dynamic-component-discovery`
---
## 對外推廣(Phase 6+,本 SDD 不執行,記錄)
- README 示範「容器 + recipe = 一個 service」(Gmail / Slack / Claude)
- onboarding kit GitHub template 內含 5 個經典 recipe 當範例
- 「recipe market」想法:用戶分享 recipe 幫他人少寫 prompt
@@ -0,0 +1,318 @@
# SDD: Resumable Workflowwebhook callback 喚醒)
> 2026-05-07 建立。狗糧寫 wiki 合成 workflow 時,Mira daemon 對長草稿(>2KB)切非同步模式回 `{pending, task_id, poll_url}`cypher-executor 沒處理就直接傳下游。
> 本 SDD 解這層:**workflow 跑到一半遇到 pending 任務 → 暫停 + 持久化狀態 → 外部 callback 進來時喚醒繼續**。
> 範圍:兩家自家服務之間(Mira daemon ↔ cypher-executor)走 webhook 推。對外服務無 webhook 的場景留 wishlist 用 poll 解。
---
## 1. 問題
### 1.1 撞牆現場
wiki 合成 workflow 第一節點 `claude_api(recipe:wiki_synthesis)`
- 短草稿(< 2KB)→ daemon 同步回 `{success, data: {text}}`recipe output parser 解 JSON 成功
- 長草稿(> 2KB)→ daemon 估 75s,切非同步模式回:
```json
{
"success": true,
"pending": true,
"task_id": "task_14_1778133152480",
"poll_url": "https://mira.uncle6.me/mira/execute/task_14_1778133152480",
"estimated_seconds": 75
}
```
cypher-executor 拿到這個物件就當 result,但裡面沒 `data.text`,下游 recipe output parser 找不到要 parse 的東西,整個 workflow 算「success」但實際上 wiki 還沒生出來。
### 1.2 現有 toolkit 不夠
- `wait` 零件:固定 sleep N ms,沒 retry / 條件判斷
- `http_request` 零件:通用 HTTP,不認 daemon 的 polling 協議
- cypher-executor `visited` Set:擋住節點重訪,沒辦法做迴圈式 poll
- Worker CPU 30s 限制:同步 poll 75s 任務不可能
### 1.3 Push vs Pull 抉擇(2026-05-07 拍板)
| | Webhook 推 | Poll 拉 |
|---|---|---|
| 適用 | 雙方都自家 | 對方無 callback 能力 |
| Worker 時間消耗 | 趨近 0 | 全程占用 |
| 時長限制 | 無 | Worker CPU 30s |
| 工程位置 | runtime 能力(cypher-executor| 零件(poll_task |
**走 Webhook 推**(自家服務優先,poll_task 進 wishlist)。
---
## 2. 設計
### 2.1 三層改動
**A. Mira daemon 端(infra/cloud-cto**
- `/mira/execute` 接受新欄位 `callback_url: string`optional
- task 完成時 POST 到 `callback_url`body
```json
{
"task_id": "task_14_xxx",
"success": true,
"data": { "text": "..." }
}
```
- 失敗也要 callbackbody 含 `error` 欄位
- 重試策略:3 次 backoff1s / 5s / 30s),最後失敗就放棄(task 狀態存進 daemon 自己 KV
**B. cypher-executor 端(resumable runtime**
新概念:**workflow run 可以暫停**。
設計:
1. 新 KV namespace(或用既有 `EXEC_CONTEXT`)存暫停的 run state
- key: `paused_run:{task_id}``paused_run:{run_id}`
- value: `{ run_id, graph, paused_node_id, paused_node_pending_result, context, trace_so_far, kv_store_ref, expires_at }`
2. graph-executor 偵測節點 result 含 `pending: true` + `task_id` → 暫停 + 寫 KV + 回 `{paused: true, task_id, run_id}`
3. 新 endpoint `POST /workflows/resume`
- body: `{ task_id, result }`result 是 daemon callback 給的完整資料)
- 從 KV 拿 paused state → merge result 進 paused_node 的 output → 從下個節點繼續執行
4. claude_api 容器呼叫 daemon 時自動帶 `callback_url`
- `https://cypher.arcrun.dev/workflows/resume?task_id={預先派發的 task_id}`
- 但 task_id 是 daemon 自己派的,cypher-executor 不知道。需先 daemon 派完 task_id 才能組 URL
- 解:daemon 改成「先回 task_id,再啟動實際工作 + 完成時 callback」— 兩階段 hand-shake
實際流程(兩階段):
```
cypher-executor Mira daemon
│ │
│ POST /mira/execute │
│ { prompt, │
│ callback_url: "?run_id=R1" }
├─────────────────────────────>│
│ │ 立即回 task_id(決定走非同步)
│<─────────────────────────────┤ { pending, task_id: T9 }
│ │
├─ 看到 pending → 寫 KV │ 啟動實際 LLM 任務
│ paused_run:T9 = {run R1, │
│ paused_node, ctx, ...} │
│ │
│ 立即回 client (MCP)
│ { paused, task_id: T9 } │
│ │
⋯⋯⋯⋯⋯ 75s 後 ⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯
│ │ task done
│ POST /workflows/resume │
│ { task_id: T9, result: {...} }
│<─────────────────────────────┤
│ │
│ 從 KV 取 paused_run:T9 │
│ → merge result 進 paused 節點 │
│ → 從下個節點繼續 │
│ │
│ run 跑完 → 寫 trace │
│ → 通知 client (?) │
│ │
```
### 2.2 範圍邊界
**第一版(v1)做:**
- ✅ 單節點 pending → resume(最常見:claude_api 拿到 daemon pending
- ✅ daemon 加 callback_url 支援
- ✅ cypher-executor `/workflows/resume` endpoint
- ✅ run state 寫 EXEC_CONTEXT KV,含 24h TTL(避免 KV 累積)
- ✅ 整合測:用 wiki 合成跑長草稿,驗 callback 進來能繼續
**第一版不做:**
- ❌ 多節點都 pending 的 nested 場景(例如 claude_api → 又一個 claude_api)— v2
- ❌ foreach 內 pendingitem-level resume)— v2
- ❌ pending 期間用戶看到「進度」的前端 UI — 走 trace 有 paused 標記,前端 polling 自己做即可
- ❌ pending callback 失敗時的 retry / DLQ — v2,先記 log
**前置依賴:**
- ✅ recipe-system 已部署(cypher-executor 已會解 recipe
- ✅ Mira daemon 在 Hetzner,可改 code
### 2.3 為什麼不用 Cloudflare Queues / Durable Objects
- **CF Queues**:適合大量 fan-out,這裡是點對點 callbackKV 已夠
- **Durable Objects**long-lived state 比 KV 強,但成本高 + 複雜
- **EXEC_CONTEXT KV**:既有 binding,工程量最小
未來真撞到 KV 限制(每 partner 寫入頻率上限)再升級。
---
## 3. 詳細設計
### 3.1 daemon 端 callback 機制
`infra/cloud-cto/index.js`Mira daemon):
```js
// /mira/execute handler
{
// 既有 input + 新加:
callback_url: string // optional
}
// 處理邏輯:
// 1. 啟動 task(既有邏輯)
// 2. 預估時間 > 30s → 切非同步:
// - 立即回 { success: true, pending: true, task_id, poll_url, estimated_seconds }
// - 背景 task 完成時:
// if (callback_url) POST callback_url with { task_id, success, data, error? }
// (不論用戶有沒有 poll,callback 一定會送)
```
callback 失敗策略:
- 3 次重試(1s / 5s / 30s
- 全失敗:task 狀態維持完成,等 client 主動 pollpoll_url 仍有效)
- 超過 24h 沒被消化的 taskdaemon GC
### 3.2 cypher-executor 端 resumable runtime
#### 3.2.1 偵測 pendinggraph-executor
在 Component caserunner 回傳後:
```ts
result = await runner(mergedContext);
// 偵測 pending patterndaemon 約定的回應結構)
if (isResumablePending(result)) {
await persistPausedRun(this.env.EXEC_CONTEXT, taskIdFromResult(result), {
run_id, graph, paused_node_id: node.id, paused_context: context,
paused_result: result, trace_so_far: trace, expires_at: Date.now() + 24*60*60*1000
});
// 提早結束此 run,回 paused 狀態
return { paused: true, task_id, run_id };
}
// ... 既有的 recipe output parsing / kvSetNodeOutput / 等
```
`isResumablePending(result)` = `result?.pending === true && typeof result?.task_id === 'string'`
#### 3.2.2 callback URL 注入(claude_api 之前的 layer
問題:claude_api 容器發 daemon 請求時,要帶 `callback_url`。但 task_id 是 daemon 派的,URL 裡只能放 run_iddaemon 收到 callback 時填 task_id
`callback_url = https://cypher.arcrun.dev/workflows/resume?run_id={current_run_id}`
但 cypher-executor 端用 task_id 找 paused state(一個 run 可能多個 pending),所以 callback URL 應該是:
`callback_url = https://cypher.arcrun.dev/workflows/resume`(不帶 querytask_id 在 body
**實作位置**:在 graph-executor 呼叫 claude_api 前,自動注入 `callback_url` 到 mergedContext
```ts
if (node.componentId === 'claude_api' && this.env?.PUBLIC_BASE_URL) {
mergedContext.callback_url = `${this.env.PUBLIC_BASE_URL}/workflows/resume`;
}
```
> 暫先用「componentId 寫死匹配」是 hacky,未來 component contract 加 `supports_async_callback: true` 標記就 generic 了。
#### 3.2.3 resume endpoint
`POST /workflows/resume`
```ts
{
task_id: string, // daemon 給的
success: boolean,
data?: { text: string }, // 跟同步呼叫一樣的結構
error?: string
}
```
處理:
1. 從 EXEC_CONTEXT KV `paused_run:{task_id}` 拿 state
2. 沒拿到(過期 / 重複 callback)→ 回 200 + log
3. 把 callback 給的 result 當作 paused_node 的 output
4. 重建 GraphExecutor,從下個節點繼續執行
5. 跑完寫完整 trace
**問題:resume 後沒辦法再回給原 client。** 用戶最初打 `/cypher/execute`(同步),拿到 `{paused, task_id}` 之後就斷了;resume 跑完 result 沒地方送。
**v1 解法**resume 完寫進 `analytics_kv` 或 D1,**用戶要主動 query**。簡單但 UX 差。
**v2 想法**resume 完發另一個 webhook 給原 clientclient 在 trigger 時帶 final_callback_url)。
---
## 4. 範圍
**在本 SDD 範圍內:**
- 4.1 daemon `/mira/execute` 加 callback_url 支援
- 4.2 cypher-executor 偵測 pending + 持久化 paused state
- 4.3 cypher-executor `/workflows/resume` endpoint
- 4.4 callback_url 自動注入(claude_api 場景)
- 4.5 wiki 合成 workflow 用長草稿端對端測試
**不在本 SDD 範圍:**
- nested pendingv2
- foreach 內 pendingv2
- final_callback 給原 clientv2
- poll_task 零件(wishlist
---
## 5. 驗收標準
1. wiki 合成 workflow 餵 5KB+ 草稿,跑完後 wiki page 有寫進 KBDB(不再 trace `pending` 假成功)
2. trace 有 `paused` 紀錄,能看到 task_id
3. 從 daemon 觸發 callback 後 < 5s 內 cypher-executor 把 paused state 撿起來繼續
4. 24h 沒 callback 的 paused state KV 自動 expire(看 KV TTL 列表)
---
## 5. Trigger 端背景執行(`?async=1`)(leo 2026-05-24 拍板)
> 背景:Phase 1-4 的 resumable runtime 只在「daemon **主動切非同步**回 pending」時生效。但實測 project_detector 的 detect 節點,daemon 在 31s 內**同步回 done**(短 prompt 不切非同步),整個 webhook trigger 同步阻塞 31 秒。前端 fire-and-forget 只等 1.5s 就刷新 → 中斷未回的連線 → workflow 中止 → tag 沒寫成。這是「河道貼文後 project_detector 完全沒留 tag」的根因(2026-05-24 對照實驗:全程等 31s 成功 / 2s 斷線失敗)。
### 5.1 問題定位
`POST /webhooks/named/:name/trigger`[routes/webhooks-named.ts](../../../cypher-executor/src/routes/webhooks-named.ts)`await executeWebhookGraph(...)` 跑完整個 graph 才回 200。呼叫端連線必須存活到 workflow 結束。
resumable runtimepause/resume)解的是「**單一 claude_api 節點** pending」;解不了「整個 workflow 同步跑很久、但沒有任何節點 pending」的情況。
### 5.2 解法:trigger route 加 `?async=1`,背景執行立刻回 202
- trigger route 偵測 query `?async=1`:用 `c.executionCtx.waitUntil(executeWebhookGraph(...))` 把執行丟背景,**立刻回 `202 {accepted:true}`**,不等 workflow 結束。
- workflow 在 CF Worker 背景跑(waitUntil 保證 Worker 不在回應後立即終止),**不依賴呼叫端連線**。前端 fire-and-forget 不論等多久都不影響。
- **不帶 `?async=1` → 維持原同步行為**(向後相容,既有呼叫端、MCP、CLI 不受影響)。
- 與 resumable runtime 正交:背景 run 內若有節點 pending,照樣走 pause→KV→callback resume(背景 run 的後續由 callback 接續,更不靠連線)。
### 5.3 與 G1 拆條(mira project_detector)的關係
mira 的 project_detector 要對 `todos[]` FOREACH 各跑一次(design.md mira-app §3.7.5.7 G1)。foreach-level resume 是本 SDD v2 未做項(§v2 已記錄)。`?async=1` 正好繞開:整個 workflow(含 foreach)在背景一次跑完,trigger 早回 202,不需要 foreach 內 per-node pending。
### 5.4 套用範圍
- mira 前端 `triggerProjectDetector` / `triggerWikiSynthesis` 觸發 URL 加 `?async=1`feed/page.tsx)。
- 其餘呼叫端不變。
→ 對應 tasks.md **Phase 6**
---
## 6. 風險
| 風險 | 緩解 |
|---|---|
| `?async=1` 背景 run 失敗時呼叫端不知道(回了 202 | 失敗寫 telemetry + execution verdict(既有);mira UI 靠 refetch tag 觀察結果(同 wiki 既有模式)|
| waitUntil 有 CF 背景執行時間上限 | 與既有同步執行同限;超長 workflow 仍靠 per-node pause/resumeresumable runtime)跨越 |
| daemon callback 進來時 cypher-executor 重啟 → state 還在 KVOK | KV 持久化 |
| 同 task_id 重複 callback(網路重試)→ 重複執行下游 | resume endpoint idempotent:拿到 state 後立刻刪 KV,重複 callback 找不到 state |
| daemon callback 失敗(網路)| daemon 端 3 retry + 24h GC,超過就需手動干預(v1 接受) |
| paused state 含敏感資料(partner key| KV 有 24h TTL;不寫 plaintext secrets(既有 credential injection 在執行前才解,paused state 存的是執行前的 contextsecret 還沒解)|
---
## 7. 變更紀錄
| 版本 | 日期 | 內容 |
|---|---|---|
| v1.0 | 2026-05-07 | 初版。狗糧 wiki 合成撞 daemon 非同步 → 補 resumable workflow runtime。第一版只做單節點 pending + claude_api callback 注入。|
| v1.1 | 2026-05-24 | 補 §5 trigger 端背景執行(`?async=1`)。根因:project_detector daemon 同步回 done(不切非同步),webhook 同步阻塞 31s,前端 1.5s 斷線殺掉 workflow。解法:waitUntil 背景跑 + 立回 202,向後相容。|
@@ -0,0 +1,71 @@
# Tasks — Resumable Workflow
> 對應 SDD[design.md](design.md)
> 上次更新:2026-05-07
**狀態 legend**`[ ]` 待辦 / `[🔄]` 進行中 / `[x]` 完成
---
## Phase 1Mira daemon 端 callback 支援
- [x] 1.1 改 `/opt/mira/mira-daemon.js`Hetzner mira container`/execute` 接受 `params.callback_url`
- [x] 1.2 fireCallback functiontask done/failed 時 POST callback_urlbody = `{task_id, success, data?, error?}`
- [x] 1.3 callback retry4 次(立即 + 1s/5s/30s backoff),全失敗 log
- [x] 1.4 patch script 寫好 `/tmp/patch-mira-daemon.py`docker cp 進 container(注意:rebuild image 會丟失,需重 patch 或正式 commit 進 Dockerfile/git repo
- [x] 1.5 真實端對端驗證:daemon log 顯示 `[Mira callback] task=task_2_... POST https://cypher.arcrun.dev/workflows/resume OK 200`2026-05-07 07:24:04 + task_3 短測試)
## Phase 2cypher-executor resumable runtime
- [x] 2.1 寫 `paused-runs.ts`81 行):persistPausedRun / loadPausedRun / consumePausedRun + isResumablePending 偵測器,24h TTL
- [x] 2.2 改 `graph-executor.ts` Component case:偵測 pending → 寫 KV + throw WorkflowPaused
- [x] 2.3 改 `cypher-handlers.ts`catch WorkflowPaused → 回 `{success:true, paused:true, task_id, run_id, paused_node_id, trace, graph}`
- [x] 2.4 callback_url 自動注入:componentId==='claude_api' 時 mergedContext.callback_url = PUBLIC_BASE_URL 或預設 cypher.arcrun.dev/workflows/resume
## Phase 3resume endpoint
- [x] 3.1 寫 `routes/resume.ts`POST /workflows/resumeconsumePausedRun → resumeFromPaused
- [x] 3.2 graph-executor 加 `resumeFromPaused()` 方法:把 callback_result 當 paused_node 輸出 + spread 進 ctx + 從下游節點繼續
- [x] 3.3 idempotent 驗證:第二次 callback 回 `{noop:true, reason:"state 不存在或過期"}`
- [x] 3.4 cypher-executor 部署 v0580980b
- [x] 3.5 mount /workflows/resume 進 index.ts
## Phase 4claude_api 容器透傳 callback_url
- [x] 4.1 改 `claude_api/main.go`Input 加 CallbackURLtimeout 預設改 120s
- [x] 4.2 重 build wasm + redeploy claude-api.arcrun.dev (v f926e3dd)
- [x] 4.3 真實端對端驗證:daemon 收到 callback_url → task done 後 POST cypher-executor/workflows/resume → 200 OK
## Phase 5:端對端整合測試
- [ ] 5.1 用 MCP `u6u_execute_workflow` 跑 wiki 合成 + 5KB+ 草稿
- [ ] 5.2 第一次回應應為 `{paused, task_id, run_id}`
- [ ] 5.3 等 daemon callback 進來(log 看到 /workflows/resume 命中)
- [ ] 5.4 觀察 wiki page 真的寫進 KBDB(即使原 MCP call 已斷線)
- [ ] 5.5 trace 含完整節點紀錄(paused → resumed
## Phase 6Trigger 端背景執行(`?async=1`)(2026-05-24 新增,對應 design.md §5
> 根因:project_detector daemon 同步回 done(不切非同步)→ webhook 同步阻塞 31s → 前端 1.5s 斷線殺掉 workflow → tag 沒寫成。對照實驗已證(全程等 31s 成功 / 2s 斷線失敗)。
- [ ] 6.1 `routes/webhooks-named.ts` trigger handler:偵測 `?async=1``c.executionCtx.waitUntil(executeWebhookGraph(...))` 立回 `202 {accepted:true}`;不帶則維持原同步(向後相容)
- [ ] 6.2 背景 run 的 telemetry / execution verdict 照寫(waitUntil 內)
- [ ] 6.3 部署 cypher-executor,驗證:`curl ?async=1` 立即回 202<2s
- [ ] 6.4 e2e`?async=1` 觸發 + 2s 強制斷線 → 等 ~35s 後 raw tag 有寫入(對照本次失敗實驗 caf8b827)
- [ ] 6.5 mira 前端 feed/page.tsxtriggerProjectDetector + triggerWikiSynthesis 觸發 URL 加 `?async=1`
---
## 風險追蹤
- 風險 1daemon callback 進來時,cypher.arcrun.dev 還沒醒(CF Worker cold start)→ 第一次 retry 接住(daemon retry policy 涵蓋)
- 風險 2v1 沒 final_callback 給原 client → 用戶要主動查狀態
- 接受:mira 河道 UI 可定期 refetch wiki page,或用既有 KBDB 觸發機制
- v2 加 final_callback 統一處理
## v2 已記錄
- nested pending(一個 run 多個 paused 節點)
- foreach 內 pendingitem-level resume
- final_callback 給原 clienttrigger 時帶 final_callback_url
- poll_task 零件(外部 API 沒 webhook 時用)
+62
View File
@@ -0,0 +1,62 @@
# Tech Stack — arcrun
## Runtime & Deployment
- **Cloudflare Workers** — all backend services deploy as Workers via Wrangler
- **Cloudflare KV** — workflow definitions, credentials (encrypted), recipes, sessions
- **Cloudflare R2** — WASM binary storage (`WASM_BUCKET`)
- **Cloudflare Pages** — frontend deployment (arcrun.dev landing page)
## Languages & Frameworks
- **TypeScript** — Workers (HTTP routing/orchestration only), CLI, SDKs
- **TinyGo / AssemblyScript** — all component logic, compiled to WASM (WASI preview1)
- **Hono** — HTTP framework for all Workers (routing, middleware, OpenAPI)
- **Zod** — schema validation and OpenAPI spec generation (`@hono/zod-openapi`)
- **React 19** + **Vite** — frontend (landing page)
- **Tailwind CSS v4** — styling
## Testing
- **Vitest** — test runner
- **@cloudflare/vitest-pool-workers** — Workers-specific test pool for cypher-executor
## Package Management
- **pnpm** — used in most packages (some use npm)
---
## Common Commands
### Per-service (run from the service directory)
```bash
# Start local dev server
pnpm dev # or: npm run dev
# Deploy to Cloudflare
pnpm deploy
# Run tests (single pass)
pnpm test # runs: vitest run
```
### WASM Components (TinyGo)
```bash
# Build a component
cd registry/components/{name}/
tinygo build -target=wasi -o main.wasm main.go
```
### CLI
```bash
cd cli/
npm run build
acr --help
```
## API Documentation
Each Worker exposes OpenAPI docs at runtime:
- `/doc` — OpenAPI JSON spec
- `/ui` — Swagger UI
Dev base URLs: Cypher Executor → `http://localhost:8788`
@@ -0,0 +1,112 @@
# thin-shell-alignment — Design
> **狀態**:草案,待確認。對應 `requirements.md`
> **建立**2026-06-27issue #11
---
## 1. 設計總綱
四塊,全守 rule 07(能力落 API、薄殼只暴露、讀同一源):
1. **R1 P0**CLI run 改打真 trigger 端點(一行修,但要端到端驗)。
2. **R2 P1**CLI list 改走 `GET /webhooks/named`KV 源),停止直連 CF KV。
3. **R3 P2**:單邊能力逐項判「補對稱 / 刻意單邊」。
4. **R4 防複發**(治本核心):能力對照清單 + 本機 smoke test,讓假綠當場現形。
---
## 2. R1CLI run 死端點(P0
`cli/run.ts:102` 現打 `${executorUrl}/webhooks/${name}` → 改 `${executorUrl}/webhooks/named/${name}/trigger`
- 對齊真端點 `webhooks-named.ts:279``POST /webhooks/named/:name/trigger`X-Arcrun-API-Key header)。
- 注意:trigger 端點吃 header api_key + body trigger contextrun.ts 現已帶 headers + inputContext,只差路徑)。
- **驗收**leo21c 部署一個 workflow → `acr run <name>`(本機無該 YAML,走「玩法二」)→ 真的觸發執行(200),非 404。
---
## 3. R2list 來源統一(P1
### 3.1 收斂到 `GET /webhooks/named`
- CLI `list.ts` 停止 `CfKvClient` 直連 KV,改 `fetch(${executorUrl}/webhooks/named)`X-Arcrun-API-Key header)。
- MCP `u6u_list_workflows` 從讀 KBDB record 改讀 `GET /webhooks/named`(經 CYPHER_EXECUTOR binding)。
- 兩者同一端點 → 同源、同欄位(#8 1.3b 已補 `GET /webhooks/named` 回 description/created_at/cron_expr)。
### 3.2 為何 list 讀 KV 而非 KBDB entry(職責分)
- **list = 精確列舉「我有哪些 workflow」** → 讀 KV(部署寫入處,權威)。
- **search = 語意找「做某事的 workflow」** → 讀 KBDB entry#8 雙寫的 search-entry)。
- 兩者不同職責、不同源頭,本就該分。KBDB entry 是 search 投影,不是 list 真相(避免 list 受 embed 模組開關影響)。
### 3.3 順手修 CLI list 的 key 前綴 bug(核實補充①)
- CLI 直連 KV 讀 `workflow:` 前綴,但部署寫 `{apiKey}:wf:{name}` → 現狀 CLI list 列不到。改走 `GET /webhooks/named` 後此 bug 自然消失(端點內部用正確前綴 `${apiKey}:wf:`)。
---
## 4. R3P2 單邊能力決策
| 能力 | 現況 | 判定 | 理由 |
|------|------|------|------|
| 驗證 YAML | CLI `acr validate` **純本機**validate.ts:44loadWorkflowYaml+parseTriplets+validateRelations,不打端點);MCP `arcrun_validate_yaml` 打 server `/validate`introspection.ts:34| **隱藏漂移,需收斂**(總管 2026-06-27 核實)| 同「驗證 YAML」能力,CLI 走本機、MCP 走 server = 又一個不同源(同 list 病)。收斂:驗證邏輯落 API(/validate)為單一真相,CLI 改打它;或明確記「本機快驗 vs server 權威驗」兩層定位——但**別讓兩邊邏輯漂移**。實作時定,記進對照清單 |
| `acr creds push`/credentials| CLI only | **刻意單邊**(記明)| credential 上傳含 client 端加密 + 本機檔案路徑,是 CLI 慣例;AI 不該代傳 credentialmindset §6/§7)。記明非疏漏 |
| `u6u_search_workflows`/workflows/search| MCP only | **補 CLI 對稱**(次階段)| 對齊 #8 Phase 5`acr workflow search`),同端點 |
| recipe 6 能力 / execute | 已對齊 ✅ | — | — |
> 判準:消費者是 AI → MCP 必有;含人類互動/本機檔/敏感操作 → 可刻意單邊但記明。**不強求 1:1**。
### 4.1 實作期發現:tag resource_id 語意債(Phase 2.2,需總管確認)
MCP list 改讀 `GET /webhooks/named`(KV 源,主鍵=name)後,tag 過濾(`resource_tag` record 的 `resource_id`)出現語意不一致:
- 舊 `u6u_deploy_workflow``workflow_metadata``workflow_id` `result.workflow_id ?? randomUUID()`**可能是 UUID**)。
- `u6u_tag_resource``resource_id` 由用戶傳入(語意不明確,可能 name 也可能 id)。
- 原 list 用 `w.workflow_id` 比對 tag 集合;改讀 KV 源後只有 `name`KV 無 UUID 概念)。
**過渡處理**list tag 過濾改用 `name` 比對(KV 源唯一鍵)。舊用 UUID 的 tag 會在 re-tag 後對齊。
**待總管確認**:方向①收斂到 KV(主鍵=name)後,tag `resource_id` 應統一為 **name**;是否要 backfill 舊 UUID tagtag 系統不在 #11 明確範圍,但 list 收斂碰到它 → 標記待決,不擅自改 tag 寫入語意。
---
## 5. R4 ⭐:防複發機制(治本核心)
> 根因是假綠(宣稱對齊但端點不存在)。機制目標:**讓「打了不存在的端點」無法悄悄混過**。
### 5.1 推薦:雙層(對照清單 + 本機 smoke test),互補
**層 1:能力對照清單(靜態,文檔 + review 防線)**
- 一張表 `docs/4-guides/cli-mcp-capability-matrix.md``能力 | CLI 端點 | MCP 端點 | server route 存在? | 同源?`
- 新增任何薄殼能力**必填一行**,PR review 對照。
- 治「忘了對齊」+「打不存在端點」的**意識層**。
**層 2:本機 smoke test(動態,端點存在性的客觀證據)** ⭐治本
- 一個**本機手動跑**的 script`scripts/thin-shell-smoke.sh``acr selftest`):對每個薄殼能力打一次真實端點,斷言「非 404」(不要求 200——有些需 auth/資料,但**404 = 端點不存在 = 死端點當場現形**)。
- **守 flag 紅線(C2/R4.2**:本機/手動觸發,**非 CI、非 cron、非輪詢**。對齊「執行鏈路不依賴 CI」「部署走 local script」既有鐵律。
- 治「假綠」的**客觀層**:宣稱對齊前先跑它,死端點無所遁形。這正是當初 MCP `/workflows/deploy` 會被攔下的機制。
**層 3(輕量,PR 時)**:新薄殼工具 PR 描述附「打的端點在 server route 清單裡」的一行證據(grep route 即可)。低成本、補 review。
### 5.2 為何不單靠 CI smoke
- CI 高頻輪詢真實端點 = 違 flag 紅線(rule 05 §3、避免被 flag 鐵律 §3)。
- smoke 是「宣布完成前手動跑一次」的閘,不是每 push 自動跑。對齊 arcrun「執行鏈路不依賴 CI、稀有驗證才 CI」分層。
### 5.3 機制自驗(R4.3
- 驗收:故意把某 CLI 工具的端點改成不存在的路徑 → 跑 smoke → **當場報該能力 404 死端點**。證明機制能攔。
---
## 6. 影響面(框架級,C1
- **改 CLI**`run.ts`trigger 路徑)、`list.ts`(改走 cypher proxy 停直連 KV)、可能補 `acr workflow search`
- **改 MCP**`u6u_list_workflows`(改讀 GET /webhooks/named)、可能補 validate 對稱。
- **改 cypher**:可能無(複用既有 `GET /webhooks/named`);確認它回的欄位夠 list 用(#8 1.3b 已補)。
- **新增**:能力對照清單文檔、smoke test script。
- **不改**deploy 那條(#8/#10);recipe/execute(已對齊)。
---
## 7. 拍板結果(leo 2026-06-27 issue #114 點全定)
1. **Q1 R4 = 雙層(對照清單 + 本機 smoke test)✅** 含 §5.3 機制自驗。守 flag 紅線(本機手動跑非 CI/cron)。
2. **Q2 CLI list = 複用 `GET /webhooks/named`✅**(不新建 proxyCLI/MCP 同源)。
3. **Q3 P2 判定全照 SDD✅**validate 補對稱(**但總管核實揭 validate 是隱藏漂移:CLI 純本機、MCP 打 server /validate,需收斂**,見 §4 表)/creds push 刻意單邊記明/search 補 CLI 次階段。
4. **Q4 smoke test = `scripts/thin-shell-smoke.sh` 本機跑 ✅**
> **狀態:4 點拍定,已點頭實作。** 順序 P0→P1→P2→R4。完成標準=leo21c 端到端(CLI/MCP 真打通 200 非 404、smoke 能攔故意死端點),非 tsc 綠。
@@ -0,0 +1,95 @@
# thin-shell-alignment — Requirements
> **來源**InkStoneCo 總管 GitHub issue #11([全面對齊] CLI/MCP 薄殼漂移系統性盤點 + 防複發機制,leo 發起)。
> **狀態**:草案,待總管/richblack 確認方向後實作。
> **建立**2026-06-27
> **白名單**`.claude/hooks/pre-write-guard.sh` KNOWN_SDDS 已加(2026-06-27 issue #11 授權)。
---
## 1. 問題:CLI/MCP 系統性漂移,根因=假綠
不是單點 bug,是**系統性漂移**。總管盤查 + 本 SDD 核實確認。
**根因(issue #11 git 史盤查)=假綠**MCP deploy 工具建於 2026-06-06 commit「薄殼原則落地 + 部署一致性」——在宣稱「一致性落地」的**同一次 commit** 裡,MCP deploy 卻打了一個不存在的端點 `/workflows/deploy`(必 404、從未端到端跑過)。違 mindset §7「完成=客觀證據非口頭宣布」。**這就是要防複發機制的理由**——光靠「宣稱對齊」會再犯。
---
## 2. 盤點結果(總管盤 + 本 SDD 核實,file:line
### 🔴 P0 死端點(打的 route 不存在 → 404)
| 能力 | 介面 | 打的端點 | 真端點 | file:line | 歸屬 |
|------|------|---------|--------|-----------|------|
| 部署 | MCP | `/workflows/deploy` | `/webhooks/named`(吃 graph | `mcp/u6u_deploy_workflow.ts:22` | **#8 ①-a + #10 ①-b**(本 SDD 不重複改)|
| 執行已部署 | CLI | `/webhooks/<name>` | `/webhooks/named/:name/trigger` | `cli/run.ts:102` | **本 SDD P0** |
✅ 核實 CLI run 死端點:`run.ts:102``${executorUrl}/webhooks/${workflowName}`(缺 `/named/``/trigger`);真端點 `webhooks-named.ts:279``/webhooks/named/:name/trigger`。確認 404。
### 🟡 P1 架構分歧(同能力不同來源)
| 能力 | CLI | MCP | 問題 |
|------|-----|-----|------|
| list workflow | 直連 CF KV 讀 `workflow:` 前綴(`cli/list.ts:35``CfKvClient`| 讀 KBDB `/records?template=workflow_metadata``mcp/u6u_list_workflows.ts:26`)| 讀不同源 → 列出的東西不一樣,違 rule 07 §4 |
> **本 SDD 核實補充(比總管盤點更深,2 個額外問題)**
> 1. **key 前綴對不上**CLI list 讀 `workflow:{name}`,但部署(`webhooks-named.ts` kvKey)寫的是 **`{apiKey}:wf:{name}`** → CLI list **永遠列不到新部署的 workflow**(前綴根本不匹配,不只是「來源不同」)。
> 2. **CLI list 直連底層儲存**`CfKvClient` 直打 Cloudflare KV REST API(繞過 cypher)→ 違薄殼更深一層(薄殼不該直碰儲存),且 self-hosted 用戶得有 CF API token 才能 list。
### 🟢 P2 單邊能力(一邊有一邊無)
- CLI only`acr validate`/cypher/search)、`acr creds push`/credentials)。
- MCP only`u6u_search_workflows`/workflows/search#8 新增)。
- 已對齊✅:recipe 6 能力、execute 本機 YAML(兩邊都打 /cypher/execute)。
---
## 3. 需求(issue #11 任務 → 可驗收條目)
### R1P0 死端點修掉
- **R1.1** CLI run 改打 `/webhooks/named/:name/trigger`(真端點),不再 404。
- **R1.2** MCP deploy 死端點**不在本 SDD 修**(歸 #8 ①-a / #10 ①-b,避免三方重複改 deploy);本 SDD 僅記其存在 + 防複發機制要能攔它。
### R2P1 list 來源統一
- **R2.1** CLI 與 MCP list 都讀**同一個 API 端點** `GET /webhooks/named`KV 源,`webhooks-named.ts`),CLI 停止直連 CF KV。
- **R2.2** list 讀 KV 源(部署寫入處),**KBDB search-entry 只供 search 不供 list**(職責分:list=KV 精確列舉、search=entry 語意檢索)。對齊 #8 雙寫設計。
- **R2.3** CLI list 改走 cypher proxy(薄殼不直碰儲存),self-hosted 用戶不需 CF API token 即可 list。
### R3P2 單邊能力盤點決策
- **R3.1** 逐項決定:該補齊對稱的(薄殼出貨標 §5「CLI+MCP 覆蓋同組能力」)vs 刻意單邊的(記明原因,如 `creds push` 屬 CLI 慣例)。
- **R3.2** 不強求每個端點兩邊各開(齊的單位是「能力」非「端點」,decisions-summary 2026-06-15 釐清)。
### R4 ⭐:防複發機制(本 issue 治本核心,不可省)
- **R4.1** 設計一個讓「假綠」無法再悄悄發生的機制。候選(design 評估擇一/組合):
- **能力對照清單**:一張表「能力 × CLI 端點 × MCP 端點 × 同源?」,新增能力必填、review 對照。
- **end-to-end smoke test**:每個薄殼能力對真實端點跑一次(非 tsc 綠),死端點當場現形。
- **PR 證據要求**:新薄殼工具必附「打的端點在 server route 清單裡存在」的證據。
- **R4.2** 守 flag 紅線:smoke test 本機/手動跑,**非 CI 高頻輪詢**。
- **R4.3** 機制本身要可驗:能攔住一個**故意的死端點**(驗收標準,issue #11 明列)。
---
## 4. 約束(頂層鐵律)
- **C1 框架級**:改全 arcrun 用戶 → 先 SDD 確認方向。
- **C2 守 rule 07**:能力只實作一次;薄殼讀同一份來源、不直碰儲存。
- **C3 守 mindset §7**:完成=leo21c 端到端客觀證據(CLI/MCP 真打通 200 非 404),非 tsc 綠。
- **C4 三方協調**#11(全面盤點+防複發)/ #8deploy 強制+search/ #10(編排下沉)有交集,**deploy 那條不重複改**(本 SDD 只盤點 + 防複發涵蓋它)。
- **C5 署名**:跨 repo comment 開頭 `[arcrun CC]`
---
## 5. 非目標
- ❌ 改 deploy 死端點(歸 #8/#10)。
- ❌ 把防複發機制做成 CI 高頻 gateC2 flag 紅線、rule 05 §3)。
- ❌ 強求 CLI/MCP 端點 1:1(齊的單位是能力非端點)。
---
## 6. 開放問題(design 回答 / 待拍板)
- **Q1**:R4 防複發機制採哪個/哪幾個組合?(design 給推薦 + 為何)
- **Q2**CLI list 改走 cypher proxy,需不需要新 proxy 端點,還是複用 `GET /webhooks/named`?(後者已存在,傾向複用)
- **Q3**:P2 單邊能力,哪些補對稱、哪些刻意單邊?(design 逐項給判準)
- **Q4**smoke test 放哪(CLI 自帶 `acr selftest`?獨立 script?)、怎麼「本機手動跑非輪詢」?
@@ -0,0 +1,56 @@
# thin-shell-alignment — Tasks
> **狀態**:方向待確認,**尚未實作**(全 `[ ]`)。確認後才動 code。
> 對應 `design.md`。每完成一個立刻標 `[x]`,不批次。
> 建立:2026-06-27issue #11
---
## Phase 0:方向確認(前置)
- [x] 0.1 SDD 三件式寫好,核實總管盤點(P0 CLI run 死端點屬實 + 挖到第三漂移:CLI list key 前綴對不上 + 直連 KV)
- [x] 0.2 回報 issue #11 comment(署名 [arcrun CC])— leo 2026-06-27 全拍定,4 點照 SDD
- [x] 0.3 確認 validate 對齊狀態 — **總管核實揭第四漂移:CLI `acr validate` 純本機(validate.ts:44 不打端點)、MCP `arcrun_validate_yaml` 打 server /validate → 需收斂**Phase 3.1 處理)
## Phase 1P0 死端點(R1
- [x] 1.1 CLI `run.ts:102` 改打 `/webhooks/named/:name/trigger`(真端點)— headers 已含 X-Arcrun-API-Key、body 已就緒,只改路徑一行。tsc 綠
- [ ] 1.2 驗證:leo21c 部署 workflow → `acr run <name>`(本機無 YAML 走玩法二)→ 觸發 200 非 404
- [ ] MCP deploy 死端點不在本 SDD,歸 #8 ①-a / #10 ①-b
## Phase 2P1 list 來源統一(R2
- [x] 2.1 CLI `list.ts``CfKvClient` 直連 KV,改 `GET /webhooks/named`X-Arcrun-API-Key)— 整段改寫,tsc 綠
- [x] 2.2 MCP `u6u_list_workflows` 改讀 `GET /webhooks/named`(取代讀 KBDB recordtag 過濾仍走 resource_tag)— registry 簽名加 partnerTokentsc 綠。⚠️ tag resource_id 語意債(UUID vs name)記 design §4,待總管確認 tag 收斂
- [x] 2.3 確認 `GET /webhooks/named` 回欄位夠 list 用(#8 1.3b 已補 description/created_at/cron_expr)— CLI/MCP 都讀 name/description/created_at
- [ ] 2.4 驗證:CLI list 與 MCP list 對同帳號回**同一組** workflow(同源、欄位齊);key 前綴 bug 消失(列得到新部署的)
- [ ] 2.5 驗證:self-hosted 用戶不需 CF API token 即可 list(走 cypher 不直連 KV
## Phase 3P2 單邊能力(R3
- [⏸] 3.1 validate:核實完成——**真漂移且依賴 #10**。CLI 本機驗 YAMLloadWorkflowYaml+parseTriplets+validateRelations);MCP 打 server /validate 但傳的是已解析的 `{nodes,edges}` graphgraphSchema.safeParse)。兩邊**輸入不同層**YAML vs graph),與 deploy 的 YAML→graph 編排債同根。乾淨收斂依賴 #10 編排下沉(YAML→graph 變 API 能力後 validate 才能統一吃 YAML)。**標記依賴 #10,記對照清單,不在本 SDD 強收**
- [ ] 3.2 creds push:記明「刻意單邊」於能力對照清單(含原因:含加密+本機檔,AI 不代傳 credential
- [ ] 3.3 searchCLI `acr workflow search` 對稱補(次階段,同 #8 Phase 5
## Phase 4 ⭐:防複發機制(R4,治本)
- [x] 4.1 能力對照清單 `docs/4-guides/cli-mcp-capability-matrix.md`(能力×CLI端點×MCP端點×route存在?×同源?)— 13 能力盤好,標 3 個已知債連 SDD
- [x] 4.2 本機 smoke test `scripts/thin-shell-smoke.sh`:對每能力打真端點斷言非 404(本機手動跑,非 CI/cron/輪詢)— 跑 prod 通,死端點 exit 1
- [x] 4.3 機制自驗:注入故意死端點 `/this-route-does-not-exist-xyz` → smoke 當場攔下列入死端點清單、exit 1(證明能攔)✅
- 📌 **副產品實證**smoke 對 prod 跑揭出 `search_workflow`/`backfill` 報 404 — 非 bug,是「#8 code 已寫但 prod cypher 未部署」的假綠被當場揭出(正是 #11 治本要點)
## Phase 5:收尾
- [ ] 5.1 tsc 全綠(cli / mcp / cypher 受影響者)
- [ ] 5.2 leo21c 端到端:對齊後的能力 CLI/MCP 都真打通(200 非 404)、防複發機制驗收可攔死端點
- [ ] 5.3 issue #11 comment 回報端到端證據;由實證決定結案
---
## 鐵律提醒
- 能力落 API、薄殼讀同一源、不直碰儲存(rule 07)。
- 完成=leo21c 端到端客觀證據非 tsc 綠(mindset §7,這正是 #11 要治的假綠)。
- smoke test 本機手動跑,非 CI 高頻(flag 紅線)。
- deploy 那條不重複改(#8/#10 處理)。
- 跨 repo comment 署名 [arcrun CC]。
@@ -0,0 +1,355 @@
# Design: 用戶 CC harnessacr install-harness
> 2026-06-03。richblack 已授權建此 SDD(白名單已加)。**待 review design 後動 code。**
> 背景:richblack 釐清「arcrun 是給 CC 的 harness」的真正意思(§0)。
---
## 0. 問題(richblack 原話)
> 「叫 CC 用 Arcrun 開發,它說『好,我先用 Python 測試』,因為它熟悉的還是 Python。
> harness 要:先提醒 CC 要用、它要用時知道去哪取得資源、做錯時被糾正,
> 讓用戶的 CC 不會再問這個笨問題。」
**核心區分(richblack 2026-06-03**
- **開發 arcrun**richblack + 他的 CC):用本 repo 本機 `.claude/CLAUDE.md`SDD 協議、禁令)。
**不給用戶、不進公開 repo**(已 git rm --cached,本機保留供開發)。
- **使用 arcrun**(外部工程師 + 他的 CC):在自己的專案用 arcrun。需要**另一套** harness
透過「安裝」裝進他自己的專案。
用戶不 clone arcrun 來改,他在自己專案工作 → harness 必須裝在**用戶專案目錄**,
他的 CC 才會自動載入(Claude Code 只自動讀「當前工作目錄樹」的 CLAUDE.md / .claude)。
---
## 0.5 設計鐵則:guardrail 擋下時必須指出正路(從內部 harness 的慘痛教訓來)
> 2026-06-03 教訓:內部開發 harness 的 `pre-write-guard.sh` 規則 4.3 擋 CC 建新 SDD 目錄,
> 但 block 訊息只說「先與 richblack 確認」,**沒說「確認後要更新白名單才能往下」** →
> 開發 CC 被自己的 guardrail 卡死、不知正路、動不了。
**用戶 harness 的每個 guardrailhook / 提醒)擋下時,必須在訊息裡給「具體怎麼做才能合法通過」的下一步。**
- 只說「不行」= 把 CC 卡死。
- 要說「不行,因為 X;正確做法是 Y(具體指令 / 檔案 / 步驟)」。
- 這是 DECISIONS §7「會回嘴的 CLI——exit 2 + **指回正路**」的「+指回正路」那半,內部版漏做,用戶版必須做足。
**驗收新增一條**:每個 guardrail 的 block/提醒訊息都含可執行的下一步(§7.6)。
---
## 0.6 骨架:一張前置清單,兩種人同走,必做的事白癡化(richblack 2026-06-03 收斂)
> richblack:「我有一系列必要做的前置環境設定,把它列出。技術好的一一完成;技術不好的只是許願,
> 讓他做盡量少,要做的所有事情白癡化。」
**整份 design 的核心模型**:不是「兩套不同流程」,是**同一張前置清單、差別只在誰執行**。
### 前置環境設定清單(PREREQ,權威清單)
| # | 項目 | 只能用戶做? | 技術好的 | 技術不好的(許願型)|
|---|---|---|---|---|
| P1 | 裝 Node + `npm i -g arcrun` | 否 | 自己跑 | CC 代跑 |
| P2 | 裝 `npm i -g wrangler`CF CLI| 否 | 自己跑 | CC 代跑 |
| P3 | `acr install-harness`(裝防護)| 否 | 自己跑 | CC 代跑(CC 讀 llms.txt 第一步就做)|
| P4 | **建 CF 帳號 + 拿 Account ID + API Token** | **是**(憑證在用戶手上)| 自己建 | **CC 白癡化手把手帶**(§1.0 步驟2|
| P5 | `acr init --self-hosted`(貼 token,自動建 KV/部署/seed| 否 | 自己跑 | CC 代跑(用 P4 拿到的兩串)|
| P6 | `wrangler secret put ENCRYPTION_KEY` ×3 | 否 | 自己跑 | CC 代跑 |
| P7 | 連 arcrun MCP`claude mcp add`,CC 偏好的工具)| 否 | 自己跑 | CC 代跑(install-harness 順便)|
### 兩種執行者
- **技術好**:拿這張清單,一一完成。清單清楚就夠(README / llms.txt 列出即可)。
- **技術不好(許願型,主要用戶)**:他只說「我要用 arcrun 做 X」=許願。CC 拿同一張清單,
**能代勞的全代勞(P1/P2/P3/P5/P6),唯一只能他做的 P4 白癡化成笨到不會錯的手把手步驟。**
### 用戶畫像(richblack 2026-06-03 鎖定第一版)
- **會開 terminal / vscode、會用 CC** → 跑指令、貼 token **不是**障礙(不用把跑指令白癡化)。
- **但 CF 概念對他們太抽象**(richblack 實測:很多人聽不懂 CF)→ **白癡化焦點 = CF 那段**
### 設計要求:白癡化「焦點在 CF」,不是全面白癡化
- **P4(CF)必須白癡化到「不用懂 CF 概念也能完成」**CC 對用戶**絕不講** KV namespace / Workers /
R2 bucket / zone 這些 CF 術語 → 只講「開這個網址、點這個鈕、複製這串、貼回來」。把 CF 的抽象**藏起來**。
- 其餘 CC 能代勞的(P1/P2/P3/P5/P6)→ CC 代跑,用戶會跑指令但不必自己想。
- 目標:**用戶不需要懂 CF 是什麼,照著做就裝好。** 用戶做的事 → 許願 + 跟著 CF 步驟點幾下 + 貼兩串。
→ 後面 §1.0llms.txt 入口)、§2install-harness)、§4hook)都是「讓 CC 能拿著這張清單替用戶跑完」的手段。
---
## 1. harness 三層(對應三需求 + Claude Code 載入機制)
依 claude-code-guide 查證(2026-06-03):
| 需求 | 機制 | 強制力 | 裝進用戶專案 |
|---|---|---|---|
| **事前提醒**「用 arcrun 別自寫 Python」| CLAUDE.mdsession 必載入)| 弱(可能遺忘)| 用戶 `CLAUDE.md` append arcrun 區塊 |
| **資源去哪取** | Skill(按需觸發)+ CLAUDE.md 指令 | 中 | `.claude/skills/arcrun/SKILL.md` |
| **做錯被糾正** | PreToolUse hookexit 2| 強(需 workspace trust| `.claude/hooks/arcrun-guard.sh` + settings.json |
三層互補:CLAUDE.md 會被 compaction 丟、Skill 可能不觸發、hook 最強但需 trust。事前+事中+事後,缺一不可。
---
## 1.0 第一接觸點:用戶只會聊天,CC 讀入口檔自己啟動(richblack 2026-06-03 鎖定用戶畫像)
> richblack:「用戶只會跟 CC 聊天,不裝不打指令、連 slash command 都不會用。
> 路2 = 把 arcrun 網站/GitHub 連結丟給 CC + 一句『我要用 arcrun 做這個專案,怎麼做』,然後兩手一攤。」
**用戶全部操作 = 丟連結 + 一句話。** 其餘全 CC 做。所以 harness 的「第一接觸點」必須是
**CC 在用戶還沒裝任何東西時就讀得到的入口檔**,且讀了就知道正確啟動順序。
### 雞生蛋問題 + 解法
- slash command / hook / skill 都要**先裝進用戶專案**才存在 → 但用戶還沒裝任何東西 → CC 看不到它們。
- 解:用「裝之前就讀得到的入口檔」叫 CC 去裝。兩個入口(CC 丟網站或丟 GitHub 都涵蓋):
- **`arcrun.dev/llms.txt`**(網站根目錄,給 LLM 的標準入口檔)—— 用戶丟網站連結時 CC 會讀。
- **README 開頭一段「給 AI 操盤手」**—— 用戶丟 GitHub 連結時 CC 讀 README。
- 兩者內容一致,核心是:**「第一步:跑 `npm i -g arcrun && acr install-harness`,裝完你就有完整防護」**
+ 裝前先記的鐵則(別寫 Python、用戶只聊天你來操作 CLI)。
### 啟動流程(CC 讀入口檔後照做)
```
1. acr install-harness ← 裝防護(mindset Skill / arcrun-guard hook / /arcrun command / CLAUDE.md 區塊)
2. 手把手帶用戶建 CF 帳號、拿 token ← 唯一用戶親手做的事,CC 用內建話術一步步帶(見下)
3. acr init --self-hosted ← CC 幫跑,貼用戶 token
4. 把用戶需求拆 workflow → acr push ← CC 在 harness 防護下做事
```
→ llms.txt / README「叫 CC 去裝」(裝前就讀得到);install-harness 提供「裝後的完整防護」。
→ 用戶從頭到尾只說人話。
### 步驟 2 細化:CC 手把手帶用戶拿 CF 憑證(richblack 2026-06-03
> richblack:「連 CF 他都不會。但 CC 可以說:你現在去申請 CF 帳號,方法是…,完成後把 xxx 和 ooo 貼給我。」
**這是唯一用戶必須親手做的事**(憑證在用戶手上,CC 拿不到),所以 CC 的引導必須**手把手、零技術假設**。
這套話術要**內建在 llms.txt / harness 指引裡**,讓每個用戶的 CC 都用同一套清楚步驟帶,不自己亂講、不假設用戶懂。
CC 該給用戶的(**不講 CF 術語**,照抄式;用戶不用懂「Workers/KV/R2」是什麼,照著勾就好):
```
我需要你去一個叫 Cloudflare 的免費服務拿「兩串文字」給我(你不用懂它是什麼,照做就好)。
做完把兩串貼回來:
【第一串:帳號代碼】
1. 開 https://dash.cloudflare.com/sign-up 用 email 註冊(免費)。
2. 登入後,畫面右邊有一塊寫 "Account ID",點它旁邊的複製小圖示 → 這是第一串。
【第二串:金鑰】
3. 開 https://dash.cloudflare.com/profile/api-tokens
4. 點藍色 "Create Token" → 找 "Create Custom Token" 那欄點 "Get started"。
5. 在 "Permissions" 區照抄勾三組(不用懂意思,照填,按 "+ Add more" 加組):
Account / Workers Scripts / Edit
Account / Workers KV Storage / Edit
Account / Workers R2 Storage / Edit
6. 一直按 Continue / Create Token,最後會顯示一串長文字 → 立刻複製(只出現這一次)→ 這是第二串。
7. 把兩串貼給我,我幫你裝好,你不用再碰 Cloudflare。
```
**白癡化要點**:CC 全程**不解釋**「什麼是 KV / Worker / token」——用戶聽不懂也不需要懂,
CF 術語當「照抄的咒語」。用戶卡住(找不到鈕)時,CC 用更白話描述位置,**不丟術語**。
CC 拿到兩串 → 跑 `acr init --self-hosted`(貼進去)→ 後續全自動。
**誠實/安全**:token 是用戶的、貼給 CC 用於部署到他自己的 CF;CC 不外傳、用完寫進用戶本機 config(init 已處理)。
---
## 1.5 第四層:slash command `/arcrun`(裝好後的主動入口,可選)
> richblack:「我的使用者技術不好,應該做很少的事,其他交給 CC。harness 一部分寫在 slash command 裡。」
> 定位(richblack 2026-06-03 鎖定用戶畫像後):用戶**只會聊天、不一定會打 slash command**。
> 所以 `/arcrun` **不是主要路徑**——主要路徑是 §1.0(丟連結給 CCCC 讀 llms.txt/README 自己啟動)。
> `/arcrun` 是 install-harness 裝好後的**可選**入口,給「願意打指令的用戶」或「CC 自己引用」用。不強求用戶會用。
**用戶實際路徑(重申,§1.0)**:丟連結 + 一句話 → CC 讀入口檔 → CC 跑 install-harness → CC 引導。
用戶不需要知道 `/arcrun` 存在。它存在是錦上添花(裝好後若用戶想要更明確的入口可打),不是必經。
**為何 command 而非只靠 Skill**claude-code-guide):
- Skill = Claude **自己判斷**要不要觸發(可能不觸發 → 用戶以為在用 arcrun,CC 卻自己寫 Python)。
- Command = 用戶**主動打 `/arcrun`** = 明確宣告「進入 arcrun 模式」→ CC 照 command 流程走,最可靠把用戶帶進正軌。
- **坑(claude-code-guide 警告)**`/arcrun` 不要同時做成 command 又做成 skill(觸發邏輯衝突)。
**`/arcrun` 做成 command(用戶主動入口);mindset 做成 Skill(被動世界觀,名字不同避免撞)。**
**command 檔機制**claude-code-guide 查證):
- 放 `.claude/commands/arcrun.md` → 自動成為 `/arcrun`,clone/放檔即可用,無需安裝/信任。
- 用戶打 `/arcrun <需求>``<需求>` 字串自動串進 prompt(無 $ARGUMENTS 變數,純文字追加)。
- command 檔是 prompt 模板,可指示 CC「讀 arcrun Skill、跑 `acr parts`、輸出 yaml、別寫 Python」。
- ⚠️ command 檔**不可用相對路徑引用 arcrun repo**(用戶專案沒有)→ 指令裡只提「跑 `acr xxx`」「讀你專案的 CLAUDE.md」。
---
## 2. 安裝指令(兩狀況、一指令)
richblack:用戶兩種狀況都「一個指令完成」——已有執行中專案、或新啟動專案。
- `acr install-harness`(主指令,獨立):當前目錄安裝。新舊專案皆可。
- `acr init` 末尾呼叫同一套邏輯(兩者皆裝)。
### install-harness 行為(冪等)
```
acr install-harness (用戶專案根目錄)
1. CLAUDE.md:無→建;有→append arcrun 區塊(<!-- arcrun-harness:start/end --> 包夾,重裝取代區塊不重複)
2. .claude/skills/arcrun-mindset/SKILL.md:複製世界觀 Skill(覆蓋舊版;名 arcrun-mindset,避免與 /arcrun command 撞)
3. .claude/commands/arcrun.md:複製 /arcrun slash command(技術不好用戶的主動入口,§5.1)
4. .claude/hooks/arcrun-guard.sh:複製用戶版 guard(§4
5. .claude/settings.json:無→建並註冊 hook;有→合併(不覆蓋用戶既有 hooks/設定)
6. P7optionalclaude mcp add arcrun MCP —— CC 偏好工具;MCP 對齊未完成前可跳過(§2.5)
7. 印提示:首次開 Claude Code 要 trust 工作區 hook 才生效;技術不好的話直接打 /arcrun 描述需求
```
冪等:重裝不重複、不破壞用戶既有 CLAUDE.md / settings。
**技術不好用戶連 `acr install-harness` 都不用自己跑**:跟 CC 說「幫我把這專案設定成用 arcrun」→
CC 跑 `acr install-harness` → 裝好後用戶打 `/arcrun <需求>` 即可。用戶全程只說人話。
### 2.5 MCPP7):CC 偏好的工具,納入安裝 + updaterichblack 2026-06-03
> richblack:「先前有 MCP,因為是 CC 比較喜歡的工具,也要排入讓它 update。」
- arcrun MCP`@inkstone/arcrun-mcp`)是 **Cloudflare Workers 上的 Remote MCP Server**CC 用 `claude mcp add <url>` 連。
- **install-harness 順便連 MCP**P7):跑 `claude mcp add` 把 arcrun MCP 加進用戶的 Claude Code。
CC 偏好 MCP 工具 → 有 MCPCC 操作 arcrun 更順(直接呼叫 MCP 工具,不用記 acr 指令細節)。
- **`acr update` 納入 MCP**update 時確保 MCP 連線指向最新(URL / 版本對齊)。
- ⚠️ **誠實前置(BACKLOG 既有待辦)**arcrun MCP 本身**尚待對齊**`u6u_*`→arcrun 命名、`finally.click`
arcrun.dev、移除 GUIDE.md 教 `api_config` 的反模式、確認薄殼)。**MCP 對齊是另一條 BACKLOG 線**
本 SDD 只負責「把 MCP 納入 install-harness / update 的接點」。MCP 對齊未完成前,install-harness 的
MCP 步驟可先標 optional(用戶可跳過),對齊後再設為預設。self-host 用戶連哪個 MCP(公共 vs 自部署)待 §8 釐清。
---
## 3. harness 素材從哪來(內嵌 npm 套件)
用戶的 `acr``npm i -g arcrun` 裝的,不能假設用戶有 arcrun repo
→ harness 素材內嵌 npm 套件:放 `cli/harness/`build 進 `dist/``files` 帶上。
install-harness 從已安裝套件目錄(`import.meta.url` 解析)複製到用戶 cwd。
SSOT = `cli/harness/`,與本 repo 開發版 `.claude` **不共用**(對象不同)。
---
## 4. 用戶版 hook 擋什麼(與開發版完全不同)
開發版擋「registry/components 寫 TS / 建 auth worker」——對用戶無意義。
用戶版 `arcrun-guard.sh`(每條都依 §0.5 給正路):
| 偵測 | 動作 | block/提醒訊息含的正路 |
|---|---|---|
| arcrun 專案裡跑 `python *.py`/`node` 寫一次性自動化(典型「我先用 Python」)| **提醒**(不硬擋,避免誤殺正常 python)| 「這專案用 arcrun。串服務/自動化請寫 workflow:先跑 `acr parts` 看零件,寫 .yaml`acr run`。確定要自刻請說明為何 workflow 做不到。」|
| 自寫「打某 API 的 script」而非 recipe | 提醒 | 「打固定 endpoint → 寫 recipe`acr recipe push`。見 arcrun-mindset Skill §1。」|
| 暴露動作(部署 webhook)非 TTY 自動確認 | **exit 2**(明確越界)| 「暴露資料需人類在終端機確認。請把這動作交給人類執行。」|
**誠實限制(mindset §7**:「跑 python」不絕對錯 → 多用「提醒 + 要 CC 自證」而非硬擋。
硬擋(exit 2)只留給「暴露資料未經人類同意」。把關依風險分級,不一刀切誤殺。
---
## 5. CLAUDE.md arcrun 區塊(事前提醒,精簡對外)
```markdown
<!-- arcrun-harness:start -->
## 這個專案用 arcrun 做自動化
需要「串服務 / 排程 / 打 API / 資料自動化」時:
- 用 arcrun 工作流,**不要自己寫 Python/Node 一次性腳本**。工作流是純文字、可複用、跑在你的 Cloudflare。
- 打外部 API → 寫 recipe`acr recipe push`),不自刻 HTTP client。
- 先查能力:`acr parts`(零件)、`acr auth-recipe list`(認證)。
- **不要自製零件**WASM)——零件由 arcrun 維護走 PR;你能擴充的是 recipe + 工作流。
- 開始前讀 arcrun-mindset Skill。
<!-- arcrun-harness:end -->
```
---
## 5.1 `/arcrun` slash command 內容(技術不好用戶的入口)
`.claude/commands/arcrun.md`(裝進用戶專案後,用戶打 `/arcrun <需求>` 觸發):
```markdown
# 用 arcrun 完成這個自動化需求
用戶(可能技術不好)想做一個自動化。你的任務:用 arcrun 把它做出來,全程不要讓用戶寫程式。
## 鐵則
- **用 arcrun 工作流 / recipe,絕不自己寫 Python/Node 腳本。** 用戶選 arcrun 就是不想要一次性腳本。
- 需要打外部 API → 寫 recipe`acr recipe push`),不自刻 HTTP client。
- 不自製零件(WASM)—— 零件由 arcrun 維護。你能用的是現有零件 + recipe + 工作流。
## 步驟
1. 先讀 arcrun-mindset Skill(世界觀)。
2. 跑 `acr parts` 看有哪些零件、`acr auth-recipe list` 看支援的認證。
3. 把用戶需求拆成工作流(哪些零件、什麼順序、什麼條件),寫成 .yaml。
4. 需要 credentialAPI key / token)→ 明確告訴用戶要去哪取得、怎麼 `acr creds push`
5. `acr validate` 通過後,`acr push` 部署,告訴用戶 webhook URL / 怎麼 `acr run`
6. 完成後給客觀證據(HTTP 2xx / trace),不要只說「做好了」。
## 遇到要暴露資料(對外 webhook)
停下來,明確告訴用戶「這會讓 X 可被外部呼叫」,要他同意。不要替他決定公開。
## 用戶的需求
(用戶打在 /arcrun 後面的文字會接在這裡)
```
> 注意:command 檔不可引用 arcrun repo 的相對路徑(用戶專案沒有)。只提「跑 `acr xxx`」「讀 arcrun-mindset Skill」。
---
## 6. 動到的檔(review 後)
| 檔 | 動作 |
|---|---|
| 新增 `llms.txt`(網站根 + repo 根)| **第一接觸點**(§1.0):給 CC 的啟動指南(叫 CC 跑 install-harness + 裝前鐵則)。網站部署到 arcrun.dev/llms.txtrepo 也放一份 |
| README 開頭加「給 AI 操盤手」段 | 用戶丟 GitHub 連結時 CC 讀 README → 同樣導向「第一步 install-harness」(§1.0|
| 新增 `cli/harness/CLAUDE.block.md` | CLAUDE.md 區塊模板(§5|
| 新增 `cli/harness/skills/arcrun-mindset/SKILL.md` | 用戶版 mindset + 資源指引(複用 skills/arcrun-mindset,加「資源去哪取」、去 DECISIONS 引用)|
| 新增 `cli/harness/commands/arcrun.md` | `/arcrun` slash command(§5.1,技術不好用戶入口)|
| 新增 `cli/harness/hooks/arcrun-guard.sh` | 用戶版 guard(§4,每條訊息含正路)|
| 新增 `cli/harness/settings.fragment.json` | hook 註冊片段 |
| 新增 `cli/src/commands/install-harness.ts` | 指令(§2 冪等,複製上述 harness 素材進用戶專案)|
| `cli/src/commands/init.ts` | init 末尾呼叫 install-harness |
| `cli/src/index.ts` | 註冊 `acr install-harness` |
| `cli/package.json` | `files``harness/` 素材(確保 npm 發布帶上)|
不動:本 repo 開發版 `.claude`(對象不同,本機保留)。
命名:Skill 叫 `arcrun-mindset`、command 叫 `/arcrun`(不同名,避免 claude-code-guide 警告的 command/skill 觸發衝突)。
---
## 7. 驗收(客觀證據)
1. 空目錄 `acr install-harness` → 產生 CLAUDE.md + .claude/skills/arcrun + hooks + settings.json。
2. 已有 CLAUDE.md → append 區塊(標記包夾)不破壞既有;重跑不重複。
3. 已有 settings.json(用戶自己 hooks)→ arcrun hook 合併,用戶 hook 不丟。
4. 裝完開 Claude Code → trust → 試「寫 python 自動化」→ 收到提醒指回 arcrun。
5. `npm pack` tarball 含 harness/ 素材。
6. **(§0.5 鐵則)每個 guard 的 block/提醒訊息都含可執行的下一步**——故意觸發每條,確認訊息有正路,不是只說「不行」。
7. 裝完後 `.claude/commands/arcrun.md` 存在 → 用戶打 `/arcrun 每天抓 RSS 存 Sheets` → CC 走 command 流程(讀 Skill、跑 acr parts、產 yaml),不自寫 Python。
8. 技術不好情境:對 CC 說「幫我把專案設定成用 arcrun」→ CC 跑 `acr install-harness` → 用戶全程沒碰指令。
---
## 8. 開放問題(review 拍板)
1. **python 提醒強度**:§4 傾向「提醒 + 要 CC 自證」而非硬擋(避免誤殺正常 python)。要更硬還是分級?
2. **CLAUDE.md append vs 獨立檔**:傾向 append + 標記包夾(CLAUDE.md 必載入、可乾淨移除)。動到用戶的檔可接受?
3. **是否放靜態零件清單**:傾向 CLAUDE.md 只指「跑 acr parts」(動態),不放會過時的靜態清單。
4. **MCPP7self-host 連哪個**:用戶連你的公共 arcrun MCP,還是連他自部署的?(MCP 是 CF Workerself-host 要不要也部署一份 MCP?)+ MCP 對齊(BACKLOG)何時做完才設 P7 為預設。
5. **暴露動作的同意機制:要不要支援「選③ AI 代跑」+「以後不要問我」+ 同意記錄?**(壓測 §9,待 richblack 拍板)
> 來源:壓測報告階段 9(`test_arcrun/docs/壓測報告.md` §9.1–9.8)。壓測者立的設計原則:
> **需同意的動作要依使用者技術能力分級提供路徑,最低能力者也要能用;我們的義務是提醒 + 記錄同意,不是刁難。**
壓測者的「一問三路」模型(使用者選哪種走哪條):
- 選①(專家):使用者自己改 settings.json 加允許清單。
- 選②(中等):**AI 給可複製貼上的完整指令,使用者貼到終端機跑**(真 TTY → guard 自動放行)。→ **A6 已修字串比對 bug,這條現在做得到(hook 能印出含 push 的指令而不自擋)。**
- 選③(最低):**使用者對話中口頭同意 → AI 代跑 → 同意記錄下來(用於法律效益)**,且第一次同意可選「以後同類動作不要再問我」。
**🔴 關鍵安全約束(壓測 §9.5/§9.6 實證,不可違反)**:
- 「人類已同意」的同意訊號**絕不能由 AI 自己產生**AI 自寫 consent.log / env 旗標 / wrapper script 偷渡都已被安全分類器正確擋下)。否則 guard 形同虛設——AI 想公開時先自寫一筆假同意即可繞過。
- 結論(壓測 §9.6):**現行架構下「選③ AI 代跑」沒有正當實現路徑。** guard hook 只認 TTY / env 旗標,這兩者 AI 都無法在「人類同意」語意下合法提供。
- 正解方向(需**上游/harness** 支援,arcrun 控不了 Claude Code harness):同意訊號從「人類產生、AI 控制不了、但 AI 能觸發詢問」的管道進入 hook——例如 `AskUserQuestion` 的人類點擊結果 → 由 **harness(非 AI** 寫一個 hook 能驗證的同意 token(含動作、時效、簽章)→ hook 驗 token 放行。**寫 token 的是 harness 不是 AI** 是整個機制的安全前提。
**待拍板的選項**richblack 決定):
- (a) **維持現狀,只支援選①/②**:選③在 arcrun 端做不到(跨 harness),`acr push` 被擋時誠實告知「選③目前不支援,請選①或②」+ 附選②可貼指令。**不在本地削弱 hook 去假裝支援選③**(壓測 §9.6 結論:要靠繞才能做=設計錯,那就不該繞、該回報)。← 壓測者+本次討論傾向此,A6 已落地選②那條。
- (b) **arcrun 端預留「hook 驗同意 token」那半**:實作 guard 驗證一個帶簽章/時效/動作範圍的同意 token 的能力,token 由未來 harness(非 AI)寫入;harness 那半未到位前選③仍不能用。
- (c) **「以後不要問我」+ 法律記錄**:第一次人類同意(透過合法管道,非 AI 自產)時可選記住,之後同類動作免問;同意記錄含時間戳/動作/同意人,作為**法律歸責 + 軌跡可審**(mindset §7:機制價值是法律憑證,不是技術防偽)。此項依賴 (b) 的「harness 寫、hook 驗」管道才安全成立。
> 已透過 `arcrun_report_feedback` 回報上游(block_id `6284b2ca-d453-4078-85ca-3f50c3507a13`)。
> **本次(2026-06-06)只修了 A6(選②的字串比對 bug);(a)/(b)/(c) 待 richblack 拍板,未動 code。**
@@ -0,0 +1,53 @@
# Tasks: 用戶 CC harnessacr install-harness
> 對應 design.md。**design 待 richblack review 後才動 code。**
> 每完成一個 task 立刻標 [x],不批次。
---
## 待 review 確認(design §8 開放問題)
- [ ] Q1 python 提醒強度:「提醒 + 要 CC 自證」(傾向)vs 硬擋?
- [ ] Q2 CLAUDE.md append(標記包夾,傾向)vs 獨立檔?
- [ ] Q3 CLAUDE.md 放靜態零件清單 vs 只指「跑 acr parts」(傾向動態)?
- [ ] Q4design §8.5,壓測 §9)暴露動作同意機制:選③「AI 代跑」+「以後不要問我」+ 同意記錄要不要支援?(a) 維持選①/② 誠實告知選③不支援(傾向)/ (b) arcrun 預留「hook 驗同意 token」那半 / (c) 法律記錄。**安全約束:同意訊號不可由 AI 自產,需 harness 寫、hook 驗。本次只修了選②的字串 bug(A6),其餘待 richblack 拍板**
## 第一接觸點(§1.0,用戶只丟連結 + 聊天)
- [x] E1 `llms.txt`(repo 根):給 CC 的啟動指南(第一步 install-harness + 鐵則 + 啟動流程 + CF 白話引導)。部署到 arcrun.dev/llms.txt 待網站部署
- [x] E2 README 開頭加「🤖 給 AI 操盤手」段:導向 install-harness + llms.txt
- [x] E3 CF 憑證話術(不講術語、照抄式)內建進 llms.txt(design §1.0 步驟2 已定稿,CF 白癡化焦點)
## A. harness 素材(內嵌 npm 套件,SSOT=cli/harness/
- [x] A1 `cli/harness/CLAUDE.block.md`CLAUDE.md arcrun 區塊(標記包夾)
- [x] A2 `cli/harness/skills/arcrun-mindset/SKILL.md`:mindset + 「資源去哪取」表 + 去 DECISIONS 引用
- [x] A3 `cli/harness/commands/arcrun.md`/arcrun slash command
- [x] A4 `cli/harness/hooks/arcrun-guard.sh`:用戶版 guardpython 提醒不硬擋 / 暴露 exit 2 / 每條含正路)
- [x] A5 `cli/harness/settings.fragment.json`hook 註冊片段
- [x] A6 修正壓測 §9.5 字串比對 bugguard hook 用 `grep "acr push"` 連「輸出/heredoc 裡**提到** push 字串」都擋 → 違反 §0.5「擋下必須印出正路」自身鐵則(連選②的可貼指令都印不出)。啟發式區分「執行 push」vs「展示指令」,誠實標明(mindset §7:shell 層無法 100% 區分,降低誤殺非完美)✓ 改法:awk 抽掉 heredoc 主體 → 看 push 是否在「實際執行命令列」的命令位置(行首 / ; & | && ||);block 訊息一併補上選②可貼指令(符合 §0.5)
## B. install-harness 指令
- [x] B1 `cli/src/commands/install-harness.ts`:冪等安裝 —— CLAUDE.md append(標記包夾)、複製 skill/command/hook、settings 合併不覆蓋
- [x] B2 從已安裝套件目錄解析 harness 路徑(import.meta.url → dist/commands → ../../harness
- [x] B3 `cli/src/index.ts` 註冊 `acr install-harness`
- [x] B4 `cli/src/commands/init.ts` 末尾呼叫 install-harness(失敗不擋 init
- [x] B5 `cli/package.json` `files``harness/`(素材原樣打包,不經 build
- [ ] B6 install-harness 加 P7`claude mcp add` arcrun MCP**待 MCP 對齊**,§2.5optional
- [ ] B7 `acr update` 納入 MCP 連線對齊(**待 MCP 對齊**,§2.5
> MCP 對齊本身(u6u_*→arcrun 命名 / finally.click→arcrun.dev / 移除 api_config 反模式 / 確認薄殼)
> 是**另一條 BACKLOG 線**,不在本 SDD;本 SDD 只做「MCP 納入 install-harness/update 的接點」。
## C. 驗收(design §7,客觀證據)
- [x] C1 空目錄 install-harness → 產生 5 檔(CLAUDE.md + skills/arcrun-mindset + commands/arcrun.md + hooks + settings.json)✓ 實測
- [x] C2 重跑不重複(冪等)✓ 實測:跑兩次 CLAUDE.md 區塊仍 1 對、guard hook 仍 1 個
- [x] C3 已有 CLAUDE.md + settings.json(含用戶自己 hook/設定)→ 合併不破壞 ✓ 實測:原內容/hook/設定全保留
- [ ] C4 trust 後試「寫 python 自動化」→ 收到提醒(需真 Claude Code session 測,hook 邏輯已寫)
- [x] C5 每個 guard 訊息含可執行下一步(§0.5 鐵則)✓ hook 內 remind/block 都帶「正路:」
- [ ] C6 打 `/arcrun <需求>` → CC 走 command 流程(需真 session 測,command 內容已寫)
- [ ] C7 `npm pack` tarball 含 harness/ 素材(待 npm 發布步驟驗)
- [x] C8 cli `tsc --noEmit` exit 0 ✓
- [x] C9 (A6) guard hook 執行 vs 展示啟發式:8/8 案例正確(真執行 4 種全 BLOCK:純執行/cd&&/echo;/recipe;展示 4 種全 ALLOWecho提到/printf提到/cat-heredoc/無關)+ 邊角(env 同意放行、pipe||後執行 BLOCK、heredoc-dash ALLOW);`bash -n` 語法 OK
@@ -0,0 +1,182 @@
# workflow-discovery — Design
> **狀態**:草案,待確認。對應 `requirements.md`
> **建立**2026-06-27issue #8
---
## 1. 設計總綱
三件事,全部把能力落在 APIcypher-executor + KBDB),CLI/MCP 只暴露:
1. **強制 description**:部署端點驗證 description 非空 → 否則 422/400 擋下(兩條路徑共用同一驗證)。
2. **可搜的 metadata**workflow metadata 同時寫進一個 **embeddable entry**(吃 issue #7`/entries/search` 語意 + 降級)。
3. **search_workflow 工具**:薄殼呼叫 cypher 的 workflow 搜尋端點,後者轉發 KBDB `/entries/search`,限本租戶。
---
## 2. 核心決策 Q1metadata 存哪(record vs entry vs 雙寫)
| 方案 | 做法 | 優 | 劣 |
|------|------|----|----|
| **A. records 表加 q 搜尋** | 給 `/records/search``q=` LIKE 掃 slots | 改動小 | records 吃不到 #7 的 Vectorize 語意;要另接一套語意=重造 #7 已有的輪子 |
| **B. 改存 entry(取代 record** | workflow metadata 改寫進 `entries``entry_type=workflow`),description 進可 embed 欄位 | 直接吃 #7 語意 + 降級閉環,零重造 | 既有 `u6u_list_workflows` 讀 record 要改;migration 成本 |
| **C. 雙寫(record + entry** ⭐ | 部署時**同時**寫 workflow_metadata record(保既有 list/get 不動)**** 一個 `entry_type=workflow` 的 embeddable entry(給搜尋) | list/get 零回歸;搜尋吃 #7 語意;兩者解耦各司其職 | 一份資料兩處;要保證一致(部署原子性) |
**推薦:C(雙寫)**,理由:
- **不回歸**`u6u_list_workflows` / `u6u_get_workflow` 繼續讀 record,完全不動(降低框架級風險,守 C1)。
- **吃現成語意**:搜尋走 entry → 直接複用 #7`/entries/search?mode=semantic` + 降級 + capability_hint**零重造**。
- **解耦**record=「我有哪些 workflow」(精確列舉,list 場景);entry=「找做某事的 workflow」(語意檢索,search 場景)。職責不同、欄位不同,本就該分。
- 一致性風險可控:雙寫在同一個部署 API handler 內,entry 寫失敗不阻塞部署但回 warningfire-and-forget + 可補建),對齊 #7 `embedOnWrite``waitUntil` 非阻塞慣例。
> **被否的 A**records 加 q 只能 LIKE,要語意還是得自己接 Vectorize=把 #7 在 entries 做好的事在 records 再做一遍。違反「不重造輪子」。
> **被否的 B**:取代 record 會逼 list/get 一起改,框架級風險放大,不值得。
---
## 3. R1:強制 description(落 API
### 3.1 統一驗證點
> ⚠️ **實作期發現(2026-06-27,已讀 code**`POST /workflows/deploy` **在 cypher-executor 根本不存在**route 清單只有 `/workflows/:name/executions``/workflows/resume`)。MCP `u6u_deploy_workflow.ts:22``http://cypher-executor/workflows/deploy`**目前必 404**。意味 MCP 部署路徑現狀是壞的 / 從未端到端驗過。這影響「兩條路徑」的定義,見 §3.1a 待決。
description 強制**落在 cypher-executor 的部署端點**。實際部署端點有三個:
- `POST /webhooks`token 式匿名):description optional。
- `POST /webhooks/named`CLI `acr push`,具名):description optional → **改必填**
- `POST /workflows/deploy`(MCP 目標):**不存在(404)**。
#### 3.1a 待決:MCP 部署路徑怎麼收斂(新發現,需拍板)
兩個方向:
- **方向 ① MCP 改打 `/webhooks/named`**(推薦):MCP deploy 不再打死端點,改打 CLI 同一條 `/webhooks/named`(傳 name + graph + description)。→ **CLI/MCP 真正共用一條部署端點**(rule 07 薄殼一致的最強形態),死端點順手清掉,description 強制只需做在 `/webhooks/named` 一處。
- **方向 ② 新建 `/workflows/deploy`**:補上這條 route 給 MCP 專用。→ 但會變成「兩條部署端點各做一次 description 強制」,且與 `/webhooks/named` 職責重疊(都是部署具名 workflow),違反「能力只實作一次」。
> **leo 拍板(2026-06-27 issue #8):方向①** ✅。CLI/MCP 共用一條部署端點=rule 07 最強形態;list 改讀 `GET /webhooks/named`(現成端點,回歸可控)。
**強制規則**:缺/空白 trim 後為空 → `400 { error: '<§3.2 定位訊息>', requires: 'description' }`。驗證是 API 行為(R1.3),薄殼不判空(守 rule 07)。
#### 3.1b 方向①實作期再發現:YAML→部署編排被寫在 CLI 介面層(藏的薄殼債)
讀 code 發現「MCP 改打 /webhooks/named」不是換 URL 就好——**`/webhooks/named` 吃的是 graph 物件,不是 YAML**。把 YAML 變 graph 的編排目前在 **CLI `push.ts` 介面層**做了一整段:
1. `loadWorkflowYaml` + `parseTriplets`(解析 YAML flow → triplets
2. `POST /cypher/search`triplets → 執行圖 graph
3. config 套節點、組 `{id, name, nodes, edges}` graph
4. `POST /webhooks/named`(傳 graph
而 MCP `u6u_deploy_workflow``yaml_content` 字串、期待一個**吃 YAML 的 server 端點**(打 `/workflows/deploy``Content-Type: application/yaml`)——這端點從來不存在(故 404)。
**張力**:方向① 若讓 MCP 在介面層複製 CLI 那 4 步編排 → **兩個薄殼各做一次 YAML→graph 編排 = 違 rule 07「能力只實作一次」**(正是薄殼原則要防的)。
**收斂選項(待總管定,§3.1c)**:
- **①-a 最小修**MCP 介面層複製 CLI 的 YAML→graph 步驟,打 /webhooks/named。→ 快,但複製編排=技術債(與方向①初衷「薄殼統一」自相矛盾)。
- **①-b 編排下沉(正解)** ⭐:新增 API 端點 `POST /workflows/deploy`(這次**真的建**,但吃 YAML、職責是「YAML→graph→部署」完整編排),CLI 和 MCP 都只傳 YAML。→ 真正一處編排,兩薄殼都瘦。但要把 CLI push.ts 的編排搬進 API(較大重構,且碰 exposure_consent 流程)。
- **①-c 折中**:先 ①-a 讓 MCP 通(解 404、issue #8 範圍內),①-b 編排下沉另開 issue(薄殼債獨立追)。
> 註:①-b 的 `/workflows/deploy` 與被否的「方向②」不同——②是「為 MCP 另開一條重複端點」,①-b 是「把編排下沉成唯一真相端點,CLI 也改用它」。前者增重複,後者消重複。
### 3.2 description 來源(Q2 定案:操盤 CC 據實生成、用戶可改)
> leo 翻案,但守住誠實精神。目標用戶 low-code**不知道要填 description**;強迫填一個不懂的欄位=違北極星「不增加用戶負擔」。調和關鍵=分清兩種「生成」:
- ✅ **操盤的 CC 據實生成**:CC 剛幫用戶建這工作流,**最懂它串了什麼、做什麼** → 由 CC 寫一句真實的「這做什麼」(leo 例:「呼叫可以 Upsert Google Sheets」)。這是**真描述非假裝**,完全不違 mindset §7。
- ❌ **介面層機械塞佔位**(從 name 複製、塞 `workflow_xxx` 預設)=假描述,**仍禁**(§3.2 原本防的就是這個,保留)。
**落地規則**
- **YAML `description:` 為主**CC 寫 workflow YAML 時就據實填好 description(既有慣例,`registry/examples` 已示範)。
- description **強制非空仍落 API**R1 不變,§3.1 驗證點不動)。
- 部署若收到空 description:**不是擋下逼用戶手填**,而是回明確訊息要求**操盤 CC 據實補一句**再部署(CC 是 description 的生成者,用戶可改)。錯誤訊息對齊此定位:「description 必填:請操盤的 AI 據實寫一句『這工作流能做什麼』(如「呼叫可 Upsert Google Sheets」),用戶可再改。」
- **內容定位**:一句「這工作流**能做什麼**」,**不是寫文章**。供語意搜尋命中,要點是「做什麼」可被自然語言意圖匹配。
- **仍禁**:介面層(CLI/MCP TS)機械從 name 生成佔位(守 rule 07 + 誠實,hook 7.x 範圍)。「據實生成」是操盤 CC 在寫 YAML 當下做的,不是介面層 deploy 時自動補。
---
## 4. R2:可搜的 entry + search_workflow
### 4.1 部署時寫 embeddable entry(雙寫的 entry 那半)
部署成功後(record 寫完),cypher 端再寫一個 entry 到 KBDB
```
POST /kbdb/entries (經 cypher proxy,租戶隔離注 owner_id
{
entry_type: "workflow",
page_name: <workflow name>,
content: <description>, // 被 embed 的主體
metadata: {
embed: true, // #7 精耕開關:只有標 true 才進 Vectorize
workflow_id, name, deployed_at
}
}
```
- `metadata.embed:true` → 命中 #7`embedOnWrite` 精耕條件,description 進 Vectorize(若模組開)。
- 模組沒開 → entry 照樣存(D1),語意搜降級成 LIKE 仍能命中 content/name(不假綠)。
- owner_id 由 cypher proxy 強制注入本租戶(沿用既有隔離,C4/R2.4)。
### 4.2 search_workflow 工具(薄殼)
新 MCP 工具 `search_workflow(query)`,形態抄 `u6u_search_components`
```
search_workflow(query) →
cypher GET /workflows/search?q=<query>&mode=semantic
→ KBDB /entries/search?q=&owner_id=&source=&mode=semantic&entry_type=workflow
→ mode=semantic 開:Vectorize 語意命中
→ 沒開:降級 keyword(LIKE) + capability_hint「叫 CC 幫你開語義查詢」
```
- cypher 加一條 `GET /workflows/search`(薄轉發到 KBDB `/entries/search`,限 `entry_type=workflow` + 本租戶 owner_id)。
- MCP 工具把結果格式化(含 capability_hint 透傳給 AIAI 看到就能主動問用戶開 Vectorize → R2.3)。
- 命名:MCP 既有工具是 `u6u_*` 前綴(`u6u_search_components`)。本工具命名 `u6u_search_workflows`(複數對齊 `u6u_list_workflows`),對外描述「用自然語言找現成工作流」。
### 4.3 Vectorize 開啟(R2.3,不新造)
完全沿用 #7`acr init` 問 / `kbdb_embed:true` + `acr update` → deploy 建 Vectorize index。AI 從 search 回傳的 `capability_hint` 得知未開 → 主動問用戶要不要開。本 SDD **不碰** Vectorize 開關機制,只消費它。
---
## 5. R3:既有工作流回填
**推薦:下次部署強制 + 一次性 backfill 端點(提示式,不自動跑)**
- **下次部署強制**R1 生效後,任何 re-deploy 都會被逼填 description → 自然回填(隨用隨補)。
- **一次性 backfill**:加 `POST /workflows/backfill-search-entries`(限本租戶),把現有 workflow_metadata records 中**已有 description 的**補寫成 entry(讓它們可搜);**無 description 的**列出來回報「這些需要 re-deploy 補 description」,**不**自動編造描述(誠實)。
- backfill 是 **AI/人主動呼叫**CLI/MCP 暴露),**不掛 cron/輪詢**C2 flag 紅線)。
- 不破壞執行:entry 只供搜尋,workflow trigger 仍讀 WEBHOOKS KV / record,互不影響(R3.2)。
---
## 6. 薄殼一致性(rule 07
| 能力 | API(真相) | MCP 薄殼 | CLI 薄殼 |
|------|------------|----------|----------|
| 強制 description | cypher 部署端點驗證 | deploy 工具透傳錯誤)| (push 透傳錯誤)|
| 搜尋 workflow | cypher `/workflows/search` → KBDB `/entries/search` | `u6u_search_workflows`(本 SDD | `acr workflow search`R2.5,次階段,對稱補)|
| backfill | cypher `/workflows/backfill-search-entries` | 可選暴露 | 可選暴露 |
齊的單位是「能力」不是「端點」(decisions-summary 已釐清):search 能力 MCP 先到位(AI 先用),CLI 對稱補可列次階段,不阻塞。
---
## 7. flag 安全自檢(C2
- search_workflowAI 收到自然語言意圖時**主動 call 一次**,無排程、無 webhook 觸發、無跨 repo fan-out。✅
- backfill=人/AI 主動呼叫一次,非 cron。✅
- 雙寫的 entry embed 走 `waitUntil` 非阻塞,是單次部署內的副作用,非輪詢。✅
---
## 8. 影響面(框架級,C1
- **改 cypher-executor**`/webhooks/named` + `/workflows/deploy` 加 description 強制;新增 `/workflows/search` + `/workflows/backfill-search-entries`;部署 handler 加雙寫 entry。
- **改 MCP**`u6u_deploy_workflow` 抓並傳 description;新增 `u6u_search_workflows`
- **改 KBDB**:可能不用改(複用 #7 `/entries/search` + `entry_type=workflow` 過濾;確認 `/entries/search` 支援 `entry_type` filter,若無則補一個 filter 參數——base 通用 filter,不寫死 workflow)。
- **self-hosted 影響**description 強制對所有用戶生效 → 回填策略(R3)讓既有資產平滑過渡,不一刀斷。
- **不改**workflow 執行/trigger 路徑、Vectorize 開關機制(複用 #7)、零件搜尋。
---
## 9. 拍板結果(leo 2026-06-27 issue #84 點全定)
1. **Q1 方案 C(雙寫)✅** — record 保 list/get 零回歸 + entry_type=workflow embeddable entry 吃 #7 語意,waitUntil 非阻塞。
2. **Q2 description「操盤 CC 據實生成、用戶可改」✅(leo 翻案)** — 非「介面層不生成」也非「逼用戶手填」。強制非空仍落 API;空時要求**操盤 CC 據實補一句**(非介面層機械塞佔位,仍禁佔位)。定位=一句「能做什麼」非文章。詳見 §3.2。
3. **Q3 回填「下次部署強制 + 提示式 backfill」✅** — 無 desc 列出待 re-deploy、不自動編造、不掛 cron。
4. **Q4 補 base 通用 entry_type filter ✅** — 總管查證:`/entries/search` 目前**不支援** entry_type 過濾(只有 q/owner_id/source/mode),但 schema 有 entry_type 欄位 + 索引(`0001_base.sql:16-20`)。要改 **4 處**`route`entries.ts:43-77/ `searchEntries` / `semanticSearch`embed.ts/ `kbdb-proxy`。**做成 base 通用 filter 不寫死 workflow**。
> **狀態:4 點拍定,已點頭實作。** 按 tasks.md Phase 1→6 推進,leo21c 端到端綠燈為完成標準。
@@ -0,0 +1,82 @@
# workflow-discovery — Requirements
> **來源**InkStoneCo 總管 GitHub issue #8[地基1])。
> **狀態**:草案,待 richblack/總管確認方向後才實作。
> **建立**2026-06-27
> **白名單**`.claude/hooks/pre-write-guard.sh` KNOWN_SDDS 已加 `docs/3-specs/workflow-discovery`2026-06-27 issue #8 授權)。
---
## 1. 問題陳述(北極星入口缺口)
北極星:**「AI 先查有沒有現成工作流 → 找到就執行它」**。這條的入口現在是斷的。
- **零件可被搜**(雖然目前只是 KV substring,非真語意,但有 `query` 介面):`u6u_search_components(query)``/components/search?q=`
- **工作流不可被搜**:只有 `u6u_list_workflows`list / tag 過濾)與 `u6u_get_workflow`(按 ID)。沒有 `query` 入口。
- **根因**:工作流的 metadata 沒有可語意比對的 `description` 欄位被穩定落庫。
- workflow YAML **慣例上已有 `description:`**`registry/examples/*/workflow.yaml` 就寫了),但:
- 不強制(建工作流可不填)。
- 兩條部署路徑落庫不一致(見 §3 現況核實)。
- 即使有 description,存它的 `workflow_metadata` 是 KBDB **record**(按 template 精確查),不是可被 `/entries/search` 文字/語意搜的 **entry**
**後果**:mira 那批工作流(門鈴/催辦/dashboard/閉環)建了也搜不到;對人也一樣——工作流多了會忘在哪。
---
## 2. 現況核實(2026-06-27,已讀 code
| 事實 | 證據(檔案) |
|------|------|
| workflow YAML 已有 `description:` 慣例 | `registry/examples/webhook-to-http/workflow.yaml:2` |
| CLI `acr push``POST /webhooks/named``description` 已接受但 **optional**(空字串 fallback | `cypher-executor/src/routes/webhooks-named.ts:67,96` |
| MCP `u6u_deploy_workflow``POST /workflows/deploy`,另存 `workflow_metadata` record**slots 無 description**,只從 YAML 抓 `name` | `mcp/src/tools/u6u_deploy_workflow.ts:40-58` |
| `workflow_metadata` record 走 `/records/search?template=`(精確 template 查,**無 q 文字搜尋** | `mcp/src/tools/u6u_list_workflows.ts:26``kbdb/src/routes/records.ts:24` |
| KBDB entries 已有真語意 searchissue #7):`/entries/search?mode=semantic`,未開 Vectorize → 降級 keyword + `capability_hint` | `kbdb/src/routes/entries.ts:43-76``kbdb/src/embed.ts` |
| 「零件可語意搜」其實是 KV prefix + substring `includes(q)`,非真語意(code 自注 Phase 0 | `registry/src/actions/queryComponents.ts:88-132` |
---
## 3. 需求(issue #8 三任務 → 可驗收條目)
### R1description slot,建工作流時強制填
- **R1.1** workflow 部署時 `description` 為**必填**;空白/缺失 → 部署被擋(回明確錯誤,像零件那樣不給空白過)。
- **R1.2** 兩條部署路徑(CLI `acr push` / MCP `u6u_deploy_workflow`)**一致強制**——不能一條擋一條放。
- **R1.3** description 是**能力(API 行為)**,強制邏輯落在 APIcypher-executor 部署端點),不寫進薄殼介面層(守 rule 07)。
### R2search_workflow(query) 工具
- **R2.1** 補 MCP `search_workflow(query)`(命名對齊既有 `u6u_search_components` 形態)。
- **R2.2** **優先語意搜尋**KBDB 未開 Vectorize → **降級關鍵字(LIKE** + 回 `capability_hint` 告知「叫 CC 幫你開語義查詢」(對齊 issue #7 閉環,不假裝有語義、不假綠)。
- **R2.3** Vectorize 的開啟由 **AI 詢問用戶**觸發(`kbdb_embed:true` + redeploy 建 index),對齊 #7 既有開關,不新造機制。
- **R2.4** 搜尋限**本租戶**org_namespace / owner_id 隔離),不跨租戶洩漏。
- **R2.5**(對稱補強,選做)同一機制可順帶讓 CLI 有對等 `acr workflow search`(薄殼一致目標,rule 07 §5;可列為次階段)。
### R3:既有工作流回填策略
- **R3.1** 既有無 description 的 workflow_metadata:設計回填/遷移策略——下次部署強制補、或一次性 migrate 端點提示補。
- **R3.2** 回填不可破壞既有 workflow 執行(description 只影響可發現性,不影響 trigger)。
---
## 4. 跨專案約束(頂層鐵律,issue #8 明列)
- **C1 框架級慎改**:改了影響**全部 arcrun 用戶**(含 self-hosted)→ 先 SDD 確認方向再實作。
- **C2 flag 紅線**search 是 **AI 主動 pull****不掛輪詢**、不掛 Actions/cron fan-out。
- **C3 範圍邊界**:「沒想到要找」那一半(釘選/常用清單)是 **mira 私人環境**的事,另開 mira issue**不在本 repo**
- **C4 薄殼鐵律**:能力(強制填、搜尋)落 API;CLI/MCP 只暴露(rule 07)。
- **C5 誠實不假綠**:未開 Vectorize 就老實降級 + hint,不假裝語義(mindset §7、對齊 #7)。
---
## 5. 非目標(明確不做)
- ❌ 把零件搜尋升級成真語意(那是 registry 的 Phase 2,另案;本 SDD 只管 workflow)。
- ❌ 釘選/常用工作流清單(C3,mira 私人環境)。
- ❌ 跨租戶/公庫工作流市場搜尋(workflow 目前是私有 namespace 資產;公庫化是更大的另案)。
- ❌ 自動觸發式 searchC2 flag 紅線)。
---
## 6. 開放問題(design 要回答 / 待人拍板)
- **Q1**workflow metadata 該從 **record 改存成 entry**(吃 #7 語意)、還是 **records 表自己加 q 搜尋**、還是**雙寫**?(design §2 給方案比較與推薦)
- **Q2**:強制 description 對既有工作流是「下次部署才強制」還是「立即 migrate」?(R3,design 給推薦)
- **Q3**description 從 YAML `description:` 解析強制,還是部署 API 參數強制,還是兩者?(兩條路徑統一點在哪)
@@ -0,0 +1,64 @@
# workflow-discovery — Tasks
> **狀態**:方向待確認,**尚未實作**(全部 `[ ]`)。確認後才動 code。
> 對應 `design.md`。**tasks.md 是唯一進度來源**,每完成一個立刻標 `[x]`,不批次。
> 建立:2026-06-27issue #8
---
## Phase 0:方向確認(前置,擋住所有實作)
- [x] 0.1 SDD 三件式回報到 issue #8 comment,列關鍵決策(方案 C 雙寫 / YAML description 為準 / 提示式回填)
- [x] 0.2 等總管/richblack 點頭;拍板 design §9 的 4 個待決點 — **leo 2026-06-27 全拍定,Q2 翻案(CC 據實生成 vs 介面層不生成)已吸收進 design §3.2**
- [x] 0.3 實作前核實 `/entries/search` 是否支援 `entry_type` filter — **總管查證:不支援(只有 q/owner_id/source/mode),schema 有欄位+索引,要改 4 處(route/searchEntries/semanticSearch/kbdb-proxy),做 base 通用 filter**
---
## Phase 1:強制 descriptionR1,Q2 定案:CC 據實生成、用戶可改)
- [x] 1.1 cypher `POST /webhooks/named``description` 改必填,trim 後空 → 400 + 可操作錯誤訊息(定位:要求操盤 CC 據實寫一句「能做什麼」,非逼用戶手填、非介面層機械塞)— webhooks-named.ts 加驗證,tsc 綠
- [⏸] 1.2/1.3 MCP deploy 改走 /webhooks/named(方向①,leo 拍定)— **卡在 ①-a/b/c 子決策**design §3.1b/c):實作期發現 /webhooks/named 吃 graph 非 YAMLYAML→graph 編排現在寫在 CLI push.ts 介面層;MCP 複製=違 rule 07。等總管定 ①-a(複製)/①-b(編排下沉新 /workflows/deploy 吃 YAMLCLI 也改用)/①-c(先 a 通、b 另開 issue)。**注**:無論哪個,MCP 最終打 /webhooks/named(已強制 description1.1 完成)→ description 強制目標三選項都達成。
- [x] 1.3b(方向①前置,三選項共需)`GET /webhooks/named` 補回 description/created_at/cron_expr 欄位,讓 MCP list 改讀本端點時欄位齊 — webhooks-named.tstsc 綠
- [ ] 1.4 驗證:兩條路徑各跑一次「無 description 部署」→ 都被擋(端到端,非只 tsc)— CLI 路徑已可驗,MCP 待 ①-a/b/c 收
## Phase 2:可搜 entry 雙寫(R2 資料層)
- [x] 2.1 cypher 部署 handlerrecord 寫完後雙寫一個 `entry_type=workflow` entrycontent=description、metadata_json.embed:true、owner_id=apiKey),`waitUntil` 非阻塞 — webhooks-named.ts 加 writeWorkflowSearchEntry helper(注意:KBDB 用 metadata_json 字串非 metadata 物件)。tsc 綠
- [x] 2.2 補 KBDB `/entries/search``entry_type` filterbase 通用,不寫死 workflow)— 改 4 處:searchEntries(entry-crud.ts) + semanticSearch(embed.tsentry_type 已 index) + route(entries.ts) + cypher kbdb-proxy `/kbdb/search` 透傳。kbdb+cypher tsc 綠
- [ ] 2.3 驗證:部署一個帶 description 的 workflow → KBDB 查得到對應 entryowner_id 正確)
## Phase 3search_workflow 工具(R2 介面層)
- [x] 3.1 cypher 新增 `GET /workflows/search?q=&mode=`:轉發 KBDB `/entries/search`(限 entry_type=workflow + 本租戶 owner_id,預設 mode=semantic 自動降級)— webhooks-named.ts。tsc 綠
- [x] 3.2 MCP 新增 `u6u_search_workflows(query)`:呼叫 3.1,格式化結果,透傳 `capability_hint`(AI 看到可主動問用戶開 Vectorize)— 新檔 + registry 註冊。tsc 綠
- [ ] 3.3 驗證 mode=keywordVectorize 未開):LIKE 命中 + 回 capability_hint「叫 CC 幫你開語義查詢」
- [ ] 3.4 驗證 mode=semanticVectorize 開,需 self-hosted leo21c):語意命中,限本租戶
- [ ] 3.5 租戶隔離驗證:A 租戶搜不到 B 租戶的 workflowcount=0
## Phase 4:既有工作流回填(R3)
- [x] 4.1 cypher `POST /workflows/backfill-search-entries`(限本租戶):有 description 的 record → 補寫 entry;無 description 的 → 列出回報,不自動編造 — webhooks-named.tstsc 綠
- [ ] 4.2 backfill 經 CLI/MCP 暴露為主動指令(非 cron,守 C2)
- [ ] 4.3 驗證:對既有 workflow 跑 backfill → 有 desc 的可搜、無 desc 的被正確列出待補
## Phase 5:薄殼對稱補(R2.5,次階段,可獨立驗收)
- [ ] 5.1 CLI `acr workflow search <query>`:對等 MCP 工具(同一 cypher 端點)
- [ ] 5.2 驗證:CLI/MCP 同 query 回同一組結果(底層同端點,差異只來自介面慣例)
## Phase 6:收尾
- [ ] 6.1 tsc 全綠(cypher / mcp / cli / kbdb 受影響者)
- [ ] 6.2 部署 + 端到端實證(leo21c 帳號跑強制填 + 搜尋 + 回填,收客觀證據非自報)
- [ ] 6.3 issue #8 comment 回報端到端綠燈證據;由實證決定結案時機(待端到端綠才 close)
- [ ] 6.4 同步更新 design.md(若實作中發現偏差)+ wiki status
---
## 跨任務鐵律提醒
- 強制填 / 搜尋 / 回填全是**能力 → 落 API**CLI/MCP 只暴露(rule 07)。
- **不假綠**:未開 Vectorize 就老實降級 + hint,不假裝語義(mindset §7)。
- **不自動編造 description**:強制是逼真的描述,自動填 = 假裝有(誠實)。
- **flag 紅線**search/backfill 都是主動 pull,無 cron/輪詢/fan-outC2)。
- 框架級改動 → 端到端實證(leo21c)才算完成,不是 tsc 綠就宣布。
@@ -0,0 +1,138 @@
# 部署慣例(CI/CD)
> **核心原則:新增 Worker = 新目錄 + `wrangler.toml`,不用改 workflow。**
`.github/workflows/deploy.yml` 是**通用掃描式** workflow,不該為每個 Worker 手寫 job。
---
## Workflow 如何找到要部署的 Worker?
```
find . -name 'wrangler.toml' -not -path '*/node_modules/*' -not -name 'wrangler.test.toml'
```
每一個命中的目錄 = 一個部署單位。無論是:
- `cypher-executor/` (orchestration Worker)
- `registry/` (合約管理 Worker)
- `.component-builds/{name}/` (零件 Worker,25+ 個)
- 未來新增的任何 Worker
**無需改 workflow,只要符合掃描規則就會自動部署**。
---
## 觸發邏輯
| 觸發 | 部署範圍 |
|------|---------|
| `push` 到 main | diff 涉及的 Worker 目錄才部署 |
| `push` 到 main + 改 `registry/components/{name}/` | 連動 rebuild `.component-builds/{name}/component.wasm` 再 deploy |
| `workflow_dispatch` + `force_all=true` | 全部 Worker |
| `workflow_dispatch` + `only=a,b,c` | 只部署指定清單 |
| `push` 但 base sha 不可及(首次) | 全部 Worker |
---
## 新增 Worker 的步驟
### 如果是新 WASM 零件 Worker
1. 在 `registry/components/{new_name}/``main.go` + `component.contract.yaml`
2. 在 `.component-builds/{new_name}/` 建 Worker 模板:
- `wrangler.toml`(name/routes/bindings)
- `package.json`(hono + workers-types + wrangler 即可,參考 `auth_static_key/package.json`)
- `tsconfig.json`(可直接複製)
- `src/index.ts`(WASI shim,方案 A:import `../../cypher-executor/src/lib/wasi-shim`)
3. 本地跑 `pnpm install``pnpm-lock.yaml`
4. 本地跑 `tinygo build -target=wasi -o {new_name}.wasm main.go` 先驗證 build 通過
5. Commit push → CI 自動 rebuild WASM + deploy
### 如果是新 orchestration/service Worker
1. 在 repo 根建新目錄(類似 `cypher-executor/`)
2. `wrangler.toml` + `package.json` + `pnpm-lock.yaml` + `src/index.ts` + `tsconfig.json`
3. Push → CI 自動部署
---
## Runtime Secret 管理
**CI 只提供 Cloudflare 驗證,不碰 runtime secret**。
- GH Actions secrets:`CLOUDFLARE_API_TOKEN``CLOUDFLARE_ACCOUNT_ID`(一次性設好)
- Runtime secret(例:`ENCRYPTION_KEY``OPENAI_KEY``GOOGLE_API_KEY`):
- **由 richblack 一次性手動** `wrangler secret put <KEY>` 設進各 Worker
- 不進 CI,不進 `wrangler.toml` `[vars]`
- 需要的 Worker:`auth_static_key``auth_service_account`(兩個都要 `ENCRYPTION_KEY`)
---
## Lockfile 規範
- **統一使用 pnpm**。新增 Worker 只放 `pnpm-lock.yaml`,不要 `package-lock.json`
- 若新建 Worker 時用 `npm install` 產出 `package-lock.json`,**刪掉它**,改跑 `pnpm install`
- `cypher-executor/``registry/``package-lock.json` 已於 2026-04-20 刪除
**現存例外**(歷史遺產,混合期不強制遷移):
- `.component-builds/{if_control, switch, ... 16 個舊邏輯零件}/` 仍是 `package-lock.json`,workflow 有 fallback 分支(`pnpm install --no-frozen-lockfile`)可跑
- `builtins/``landing/` 同上
**新增 Worker 一律 pnpm,不要製造新的混合情況**。
---
## WASM 來源
> **⚠️ 慣例變更(richblack 2026-06-02self-hosted 開源策略)**
> 原慣例「`.component-builds/{name}/component.wasm` 不 commit 進 repo」**已推翻**。
> 現在 **commit `.component-builds/*/component.wasm` 進 repo**,因為 self-host 用戶 / `acr init --self-hosted`
> 從 GitHubcodeload tarball)直接拿這份 wasm 部署到自己的 CF——repo 必須自帶可部署的 wasm。
> 決策依據:`docs/3-specs/arcrun/sdk-and-website/self-hosted-init.md §6`
### 現行規則(2026-06-02 起)
- **`.component-builds/*/component.wasm` → commit 進 repo**(部署來源)。`.gitignore` 用否定規則放行:
```
*.wasm # 預設排除
!.component-builds/**/component.wasm # 例外放行部署物
```
- **`registry/components/*.wasm` → 仍不 commit**(build 中間產物,部署不直接用,`.gitignore` 仍排除)。
- 本地開發 build`cd registry/components/{name} && tinygo build -target=wasi -o {name}.wasm main.go && cp {name}.wasm ../../../.component-builds/{name}/component.wasm`**然後 commit `.component-builds/{name}/component.wasm`**。
- CIdeploy.yml):仍在 deploy 前自動 rebuild + copy(部署 prod 用最新 source;與 repo 內 commit 的 wasm 不衝突——前者給 CI deploy prod,後者給 self-host 用戶當部署來源)。
### 誠實 trade-offmindset §7
commit wasm 進 repo → 每次 rebuild 在 git 歷史累積二進位,**repo 長期會膨脹**。
可接受(self-host 體驗優先),未來若膨脹過劇再考慮 git-lfs / 按需安裝(self-hosted-init.md §6.6)。
---
## 並行度
`max-parallel: 5` — 避免觸發 Cloudflare Workers API rate limit。
Worker 數量 > 5 時,deploy 會分批跑。25 個 Worker 大約 5 輪 × ~30 秒 = 2-3 分鐘可完成全部。
---
## 禁止事項
1. **禁止**為新 Worker 手動加 deploy job 到 `deploy.yml`。通用掃描會自動處理,手加就是重複工作。
2. **禁止**把 runtime secret(API key / encryption key / credential)放進 GH Actions secrets 或 `wrangler.toml` `[vars]`,只能用 `wrangler secret put`
3. **禁止**在 CI 裡跑不必要的測試阻擋 deploy。測試在 PR / 本地跑,`main` 推上去就 deploy(trunk-based)。若要測試關,開新 workflow 檔,不要污染 deploy workflow。
4. **禁止**跳過 TinyGo rebuild 直接 deploy 舊 `.wasm`。CI 的 rebuild 步驟是確保部署的是最新 source。
---
## 驗證指令
本地模擬 CI 的掃描結果:
```bash
find . -name 'wrangler.toml' -not -path '*/node_modules/*' -not -name 'wrangler.test.toml' \
| xargs -n1 dirname | sort -u
```
應列出 ~25 個目錄。任何「我新增了 Worker 但沒被 deploy」的問題,先跑這條確認目錄被掃到。
+312
View File
@@ -0,0 +1,312 @@
# Arcrun for AI Agents
> 給 AI 操盤手(Claude Code、Cursor、Codex、自製 agent)的 onboarding。
> 載入這份就能用 arcrun,不需要讀 SDD 內部架構、不需要 grep codebase、不需要問人。
>
> 對應 SDD`docs/3-specs/llm-interface/`v0.12026-05-16
---
## 1. Arcrun 是什麼(30 秒)
Arcrun = 用 YAML 把 WASM 零件串成可重複執行 workflow 的平台。**用戶寫 YAML,平台跑**。
- 每個零件是 TinyGo / AssemblyScript 編譯的 `.wasm`stdin/stdout JSON I/O
- 每個 workflow 是一份 cypher binding YAML,描述「節點 + 邊」的圖
- 觸發機制:HTTP webhook、cron、callback resume
- 部署完成回 `webhook_url`,用戶或下游服務 POST 即可執行
n8n 從手寫程式碼開始,arcrun 從 AI 描述開始:你跟用戶聊出他想要什麼自動化,**你(AI)寫 YAML 部署,之後不需要 AI 也能跑**。
---
## 2. 連線(一步搞定)
加到你的 MCP configClaude Desktop / Cursor / 任何支援 MCP 的 client):
```json
{
"mcpServers": {
"arcrun": {
"type": "http",
"url": "https://mcp.arcrun.dev/mcp",
"headers": {
"Authorization": "Bearer ak_YOUR_API_KEY"
}
}
}
}
```
> **暫時**M5 完成前 URL 仍是 `https://mcp.finally.click/mcp`,預計 2026-06 切。
> Tool 命名暫時仍是 `u6u_*` prefixM5 一次改 `arcrun_*`
> 本 doc 描述目標狀態,實際用 `list_*` tool 取得當前可用名單。
取得 ak_ 金鑰:到 https://arcrun.dev/meOAuth Google / GitHub 登入),右下角複製。
---
## 3. 五個核心概念
| 概念 | 一句話 |
|---|---|
| **Component(零件)** | WASM Worker,獨立部署成 `arcrun-{kebab}.{user}.workers.dev`。用 `list_components` 看可用清單 |
| **Cypher binding** | YAML 三元組 `A >> 關係 >> B`,定義 workflow 圖。常用關係:`ON_SUCCESS` / `對每個 X` / `IF` |
| **FOREACH** | `>> 對每個 item >> next_node` 迭代陣列。`item` 變數自動可用 |
| **Paused-resume** | claude_api 等需等外部 callback 的零件會「paused」,cypher-executor 透過 `/workflows/resume` 接續 |
| **api_key (ak_xxx)** | 所有 call 必帶(MCP header 或 workflow `{{api_key}}`),同時當 partition key |
---
## 4. 你的第一個 workflow5 分鐘 e2e
### Step 1:看有什麼零件可用
```
arcrun_list_components() # 全部零件名單
# 或
arcrun_search_examples('rag') # 範例庫搜尋(從 use case 找範本)
arcrun_list_skills() # 看 playbook 清單
arcrun_get_skill('build_watcher_workflow') # 拿特定 playbook 細節
```
常用零件:`http_request``claude_api``kbdb_get``kbdb_create_block``telegram``gmail``cron``filter``trigger_workflow`
### Step 2:寫 minimal YAML(從範例改 > 從零寫)
```yaml
name: hello_world
description: 接 webhook,回個 hi
flow:
- "input >> ON_SUCCESS >> say_hi"
config:
say_hi:
component: http_request
url: "https://httpbin.org/post"
method: POST
body_json:
hello: "{{input.name}}"
```
### Step 3dry-run 校驗
```
arcrun_validate_yaml(api_key, graph)
```
### Step 4:部署
```
arcrun_push_workflow(api_key, yaml_content)
```
`{name, webhook_url: 'https://cypher.arcrun.dev/webhooks/named/hello_world/trigger'}`
### Step 5:觸發測試
```
arcrun_run_workflow(api_key, name='hello_world', input={name: 'leo'})
```
### Step 6:看結果 / debug
```
arcrun_list_recent_executions(api_key, workflow_name='hello_world')
# 若 paused
arcrun_list_paused_executions(api_key)
arcrun_get_execution_trace(api_key, task_id='task_XXX')
```
### Step 7:回報(**必做**,見 §8)
```
arcrun_report_feedback(api_key, issue_type='success_story', description='...')
```
---
## 5. URL 慣例(很重要,搞錯會撞 522)
| URL pattern | 用途 |
|---|---|
| `cypher.arcrun.dev` | Orchestration API(你的 workflow CRUD + trigger 走這) |
| `arcrun-{kebab}.{user}.workers.dev` | 零件 workercypher-executor 走 workers.dev 對內 URL,避 CF 同 zone 自循環死鎖) |
| `{kebab}.arcrun.dev` | 零件 worker 對外公開 URL(用戶 / 直接 curl 用,cypher-executor 不要走這) |
| `kbdb-*.arcrun.dev` | KBDB 操作(資料層) |
| `mcp.arcrun.dev` | MCP server**你**用這個) |
| `mcp.finally.click` | MCP server 舊網址(過渡) |
**踩坑警示**cypher-executor 自打 `cypher.arcrun.dev` 或自打 `arcrun-cypher-executor.*.workers.dev` 都會撞 CF self-fetch 防護回 1042/522。要 in-process 觸發另一個 workflow**用內建零件 `trigger_workflow`**(不是 `http_request` 自打)。
---
## 6. 常見錯誤 + 怎麼讀
| error_code | 含義 | 你該做什麼 |
|---|---|---|
| `auth_missing` / `auth_invalid` | ak_ 沒帶 / 錯了 | 去 https://arcrun.dev/me 重拿,更新 MCP config |
| `component_not_found` | 零件名打錯 | call `list_components()` 看正確名 |
| `component_not_in_whitelist` | 零件存在但 cypher-executor 不認 | 告訴用戶聯絡平台維護者(這是平台 bug) |
| `validation_failed` | YAML / schema 不過 | 看 response 的 `next_actions` 陣列,照著修 |
| `running_async` (status field) | workflow 已接受,在背景跑等 callbackclaude_api 等) | **正常**wait 或 call `get_execution_trace` 看狀態。舊名 `paused_awaiting_resume` 2026-05-16 改 |
| `dependency_unavailable` | 下游 APIClaude / Gmail / KBDB)掛 | retry,仍掛 → 公告型 issue |
---
## 7. 不確定的時候,按這個順序
1. **不知道有什麼零件可用**`arcrun_list_components()` / `arcrun_get_component_contract(id)`
2. **不知道用戶有什麼 workflow**`arcrun_list_workflows(api_key)`
3. **想看零件 input/output schema**`arcrun_get_component_contract('xxx')`
4. **要不要 dry-run** → 預設 yes,永遠先 `arcrun_validate_yaml``arcrun_push_workflow`
5. **觸發後不知道結果**`arcrun_list_recent_executions(api_key, name)` / `arcrun_get_execution_trace(api_key, task_id)`paused 不算錯
6. **有沒有現成範例可參考**`arcrun_search_examples('rag llm chain')`(範例庫 10+ 個 workflow
7. **有沒有 playbook**`arcrun_list_skills() / arcrun_get_skill(slug)`5 個 playbookwatcher / paused-debug / migrate-trigger / rag / new-component
8. **不知道下一步** → 看任何 error response 的 `next_actions` 陣列
### 過渡期 tool 命名注意
LI 開發中(2026-05-16~)。目前 arcrun-mcp 同時提供:
- **`arcrun_*`**(新規範,**主用這套**):報 feedback / 工作流 CRUD / 執行 trace
- **`u6u_*`**(舊規範,等 M5 一次切):component 操作、tag 操作、GUI context
`u6u_*` 仍可用。其中 `u6u_deploy_workflow` 是壞的(呼叫不存在的 endpoint),用 `arcrun_push_workflow` 取代。
---
## 8. 回報機制(**必做**
平台用回饋資料 self-improve。**每次部署 workflow / 卡住 / 解掉問題後**call:
```typescript
report_feedback({
workflow_name: "hello_world", // 你剛做的 workflow
issue_type:
| "success_story" // 順利完成,值得記錄這個 pattern
| "doc_unclear" // AGENTS.md / skill / contract 講不清楚
| "tool_missing" // 該有的 MCP tool 沒有
| "error_unhelpful" // 錯誤訊息看不懂下一步
| "unexpected_behavior" // 跟我預期的不一樣
| "feature_request", // 我想要 X 功能
description: "...",
retry_count: 2, // 你試了幾次才搞定
blocked: false, // 完全擋住嗎
suggested_fix: "..." // optional,你建議的修補
})
```
「success_story」也要報,**那是告訴平台「這個 pattern 已經 work,可以推廣」**。
不需要怕回報太多 — 你不報,平台拿 implicit telemetry(每個 deploy / run 平台自己 log)也會看到問題,但 explicit feedback 質感高很多。
---
## 9. KBDB(資料儲存)速覽
arcrun 的「資料庫」是 KBDBCloudflare D1)。萬物皆 blocknote / wiki-page / chat / triplet / template / skill / feedback / 等,靠 `type` 區分。
工具:
- `kbdb_get(type, block_id?, page_name?, ...)` — 讀
- `kbdb_create_block(type, content, ...)` — 建
- `kbdb_patch_block(block_id, content?, tags?, ...)` — 改
- `kbdb_upsert_block(page_name, content, ...)` — page_name 當 idempotency key
寫 workflow 要 RAG / KM / 用戶資料持久化時,直接用這幾個 component(在 YAML `component: kbdb_get` 等)。
完整 KBDB API 將有獨立 SDD`kbdb-llm-interface`),目前看 `https://kbdb.finally.click/ui`Swagger)。
---
## 10. 範例:寫一個 cron watcher(最常見 pattern
```yaml
name: my_watcher
description: 每 5 分鐘掃未處理資料 → 觸發 wiki_synthesis
flow:
- "watch_cron >> ON_SUCCESS >> list_unprocessed"
- "list_unprocessed >> ON_SUCCESS >> filter_new"
- "filter_new >> 對每個 item >> trigger_synthesis"
config:
watch_cron:
component: cron
cron_expr: "*/5 * * * *"
list_unprocessed:
component: kbdb_get
api_key: "{{api_key}}"
type: "note"
source: "user-input"
limit: 20
filter_new:
component: filter
items: "{{list_unprocessed.blocks}}"
condition:
key: "tags_json"
op: "eq"
value: "[]"
trigger_synthesis:
component: trigger_workflow # 不要用 http_request 自打 — 會撞 CF self-fetch
workflow_name: "wiki_synthesis"
api_key: "{{api_key}}"
input:
api_key: "{{api_key}}"
raw_block_id: "{{item.id}}"
```
部署完每 5 分鐘自動跑。
---
## 10.5 內建 magic vars`_` prefix reserved
YAML 內可直接用以下變數,cypher-executor 自動展開為當下時間(UTC):
| 變數 | 範例 | 用途 |
|---|---|---|
| `{{_today}}` | `2026-05-16` | 日 log / page_name |
| `{{_yesterday}}` | `2026-05-15` | digest 取昨日 |
| `{{_now}}` | `2026-05-16T09:30:00.123Z` | ISO 8601 |
| `{{_now_unix}}` | `1778937000123` | unix ms |
| `{{_now_unix_s}}` | `1778937000` | unix sec |
| `{{_iso_week}}` | `2026-W20` | weekly archive (本 doc 推薦) |
| `{{_iso_week_num}}` / `{{_iso_year}}` | `20` / `2026` | 拆開用 |
| `{{_yyyymm}}` / `{{_yyyymmdd}}` | `202605` / `20260516` | 緊湊路徑 |
| `{{_year}}` / `{{_month}}` / `{{_day}}` / `{{_hour}}` / `{{_minute}}` | 各別 zero-padded | 自己拼路徑 |
| `{{_weekday}}` | `0`-`6`0=日)| if-control |
| `{{_iso_weekday}}` | `1`-`7`1=一)| ISO 風格 |
**rule**`_` prefix reserved for system**用戶自己 ctx 變數不要用 `_` 開頭**。
**範例**weekly archive
```yaml
publish_roadmap_archive:
component: kbdb_upsert_block
page_name: "roadmap-{{_iso_week}}" # roadmap-2026-W20
tags_json: '["weekly", "week:{{_iso_week}}"]'
```
---
## 11. 給寫 LI 的 AI 自己的 meta-規範
你(AI)在寫 arcrun workflow 時,**遵守以下習慣**會少踩坑:
1. **永遠先 list → validate → push → run → trace**5 步流程,缺一個都會多繞路
2. **error 一定讀 `next_actions`**:不是讀 `human_message` 然後猜
3. **paused 不是錯**claude_api、外部 OAuth flow 都會 paused,正常
4. **`{{api_key}}` 是 trigger context 帶進來的**:手動觸發要在 body 帶;cron 觸發 cypher-executor 自動塞
5. **新增零件不在 list_components 出來的清單裡** → 平台沒部署該零件,告訴用戶「我們需要先做 component」,不是你寫 workflow 的鍋
6. **完成後 call `report_feedback`**:哪怕 success_story,也回報。AI 用得順不順不能靠人類事後回顧
---
## 12. 進階參考
- 完整 SDD`docs/3-specs/llm-interface/`
- 平台架構(rules):`matrix/arcrun/.claude/rules/`
- 零件開發指南:call `get_component_guide()` MCP tool
- KBDB Swaggerhttps://kbdb.finally.click/ui
- 範例庫(M3 完成後):`registry/examples/`
- 平台週報(M4 完成後):KBDB block `type=arcrun-roadmap`
---
> 本 doc 是 source of truth。每次更新後 GH Actions 自動同步 KBDB block (`type=agent-onboarding`)AI 可透過 `get_onboarding` MCP tool 拿最新版(M1 完成)。
+34
View File
@@ -0,0 +1,34 @@
# 4. Guides — 操作手冊 + 教程
> 「怎樣做」:部署、開發、CLI 用法、壓測、工作流例子。
## 核心操作
| 檔案 | 用途 |
|------|------|
| **05-deploy-convention.md** | 部署流程、掃描式 workflow、lockfile、WASM 來源 |
| **RELEASE-CHECKLIST.md** | 發佈檢查清單 |
## 工具使用
| 檔案 | 用途 |
|------|------|
| **AGENTS.md** | Agent 工具 / MCP 工具描述 |
| **mcp-setup.md** | MCP 安裝 + 配置(搬來中) |
| **mcp-development.md** | MCP 開發指南(搬來中) |
## 開發
| 目錄 | 內容 |
|------|------|
| **examples/** | workflow 範例(搬來中) |
| **skills/** | registry skills 文檔(搬來中) |
| **components/** | 零件開發指南(搬來中) |
## 壓測 + 驗收
測試文件統一移至 `5-records/test-reports/`
---
更新時間:2026-06-08
@@ -0,0 +1,61 @@
# RELEASE-CHECKLIST — 出貨清單(沒有 GitHub Actions,每個 target 分開推,照順序不漏)
> 為什麼要這份:沒有 CI,deploy 是「一個個分開」的動作(git / CF workers / npm CLI)。
> 漏任一步就會「有些新有些舊」——壓測踩過兩次:
> - 第一次:CLI 改了但 npm 沒發 → 用戶 npm 裝到舊 CLI。
> - 第二次(階段 6):cypher 改了但**沒推 main** → `acr init` 從 origin/main codeload 抓到**舊 worker** → 薄殼打不存在的 APIseed 404)。
>
> 核心鐵則:**self-hosted `acr init``origin/main` 抓 worker 源。所以「git push main」必須在「部署」之前。**
> 順序錯了 = deploy 出去的 prod 是新的,但 self-hosted 用戶裝到的是舊的。
---
## 正確順序(照做不會忘)
### 0. 改完 code,先驗證
- [ ] 三端 typecheck 綠:`cd cli && npx tsc --noEmit``cd cypher-executor && npx tsc --noEmit``cd mcp && npx tsc --noEmit`
- [ ] 動到的 .sh`bash -n scripts/<檔>.sh`
### 1. ⬆️ 先 git commit + push**必須在 deploy 之前**
- [ ] `git add -A`(確認 `.env` / secret 沒被加:`git diff --cached --name-only | grep -iE '\.env|secret|token'` 應空)
- [ ] `git commit -m "..."`
- [ ] `git push origin main`
- 理由:self-hosted 從 `origin/main` codeload 抓 worker。沒先 push → 用戶抓到舊碼。
### 2. ✅ 跑出貨前檢查(會擋住「git 沒同步」)
- [ ] `bash scripts/check-release.sh` → 必須全綠(含「0. Git 同步」段)。
- 紅燈「領先 origin/main N commit 未 push」= 回步驟 1。
- 此腳本 git 未同步會 `exit 1`,是 deploy 的前置閘。
### 3. 🚀 deployworker + CLI npm
- [ ] Node ≥ 20(本機若預設舊版:`export PATH="$HOME/.nvm/versions/node/v22.21.0/bin:$PATH"`
- [ ] `bash scripts/local-deploy.sh --all`(或不帶 `--all` 只 deploy diff
- 此腳本**會先自動跑步驟 2 的 git 閘**;未過直接拒絕 deploy(要強推設 `SKIP_GIT_CHECK=true`,自負風險)。
- worker 走 `wrangler deploy`CLI 走 `npm publish`(版本未 bump 會自動 patch +1 + 寫 CHANGELOG)。
- npm publish 需 `npm login``.env``NPM_API_TOKEN`authToken)。
### 4. 🔁 deploy 後線上驗證(確認新碼真的上去了)
- [ ] `curl https://cypher.arcrun.dev/health` → 200
- [ ] 改了 cypher 路由時,**實際打那條新路由**確認存在(例:`curl -X POST https://cypher.arcrun.dev/init/seed``curl https://cypher.arcrun.dev/recipes` 應非空)。
← 這步就是階段 6 的教訓:別只看「部署成功」,要打新端點確認。
- [ ] 改了 CLI`npm view arcrun version` == `cli/package.json` version。
- [ ] landing 有改:確認 arcrun.dev 更新。
### 5. 📣 通知 / 收尾
- [ ] 若是回應壓測:到壓測報告加「開發者回覆」+ 請壓測者重跑。
---
## 一眼對照表:每個 target 怎麼推、誰依賴它
| Target | 推法 | 誰依賴「它在 origin/main」 |
|---|---|---|
| **git origin/main** | `git push origin main` | **self-hosted `acr init` codeload 抓這裡的 worker 源** → 必須最先 |
| CF workers26 個含 mcp | `local-deploy.sh`wrangler deploy | 平台 prodself-hosted 自己 deploy |
| CLInpm `arcrun` | `local-deploy.sh` 第 6 段 / `cd cli && npm publish` | 用戶 `npm i -g arcrun` |
| landingarcrun.dev | `cd landing && wrangler pages deploy` | 訪客 |
## 常見漏失(自我檢查)
- ❌ 「我 deploy 了 prod cypher 但忘了 push main」→ self-hosted 用戶 init 抓舊碼。**先 push 再 deploy。**
- ❌ 「改了 CLI 但版本沒 bump」→ npm publish 跳過(同版)。`local-deploy.sh` 會自動 bump,但手動 publish 時要記得。
- ❌ 「改了 cypher 路由只看到『部署成功』就收工」→ 要實際 curl 新路由確認(步驟 4)。
@@ -0,0 +1,47 @@
# CLI / MCP 能力對照清單(薄殼防漂移)
> **來源**thin-shell-alignment SDDissue #11R4 防複發機制層 1。
> **用途**:每新增一個薄殼能力(CLI 命令 / MCP 工具)**必填一行**PR review 對照。
> **治什麼**:① 打不存在的 server 端點(死端點假綠)② CLI/MCP 同能力不同源(漂移)。
> **配套**`scripts/thin-shell-smoke.sh`(層 2,對真端點打、斷言非 404)。
> **建立**2026-06-27
>
> **填寫規則**
> 1. 「server 端點」必須在 cypher-executor route 清單裡**存在**(用 `grep -rE "Router\.(post|get)\('/xxx'" cypher-executor/src/routes/` 驗)。
> 2. 「同源?」= CLI 與 MCP 是否打**同一個** server 端點。不同源 = 漂移(除非刻意單邊,記明原因)。
> 3. 標 ⚠️ 的是已知債/待收斂項,連到 SDD 對應段。
---
## 對照表
| 能力 | CLI | MCP | server 端點 | route 存在? | 同源? | 備註 |
|------|-----|-----|------------|:---:|:---:|------|
| 部署 workflow | `acr push` | `u6u_deploy_workflow` | `POST /webhooks/named` | ✅ | ⚠️ | MCP 現打死端點 `/workflows/deploy`404)→ 待 #8 ①-a + #10 編排下沉。CLI 走 4 步介面層編排(#10 待下沉)|
| 執行 workflow(已部署)| `acr run <name>` | `u6u_execute_workflow` | `POST /webhooks/named/:name/trigger` | ✅ | ✅ | **#11 P0 已修**CLI 原打死端點 `/webhooks/<name>` → 改打 trigger 真端點 |
| 執行 workflow(本機 YAML| `acr run <file>` | — | `POST /cypher/execute` | ✅ | — | CLI 本機 YAML 直跑;MCP `u6u_execute_workflow` 同打 /cypher/execute |
| list workflow | `acr list` | `u6u_list_workflows` | `GET /webhooks/named` | ✅ | ✅ | **#11 P1 已修**:兩邊原不同源(CLI 直連 KV `workflow:` 前綴對不上 / MCP 讀 KBDB record)→ 收斂到 `GET /webhooks/named`KV 源)|
| get workflow | — | `u6u_get_workflow` | KBDB record / KV| — | — | MCP only;CLI 無對應(次要,可不補)|
| search workflow | (次階段 `acr workflow search`| `u6u_search_workflows` | `GET /workflows/search` | ✅ | — | **#8 新增**;CLI 對稱補列次階段(R3.3)|
| 驗證 YAML | `acr validate`(本機)| `arcrun_validate_yaml`server /validate| `POST /validate` | ✅ | ⚠️ | **真漂移,依賴 #10**CLI 本機驗 YAML、MCP 傳 graph 打 /validate,輸入不同層。乾淨收斂依賴 #10 編排下沉(SDD §4 表 + tasks 3.1|
| 搜尋零件 | `acr parts` | `u6u_search_components` | `GET /components/search`registry| ✅ | ✅ | 同打 registry search(註:目前是 KV substring 非真語意,registry Phase 2 另案)|
| recipe6 能力)| `acr recipe *` | `arcrun_recipe_*` | `/recipes/*` `/public-recipes/*` | ✅ | ✅ | 已對齊 |
| credential 上傳 | `acr creds push` | — | `POST /credentials` | ✅ | — | **刻意單邊**(非疏漏):含 client 端加密 + 本機檔路徑,AI 不代傳 credentialmindset §6/§7|
| KBDB 資料層(template/record/query/search| `acr kbdb *` | `kbdb_*`6 工具)| `/kbdb/*` | ✅ | ✅ | 已對齊(#8 前批)|
| tagcreate/list/delete/tag/untag| — | `u6u_*_tag` / `u6u_tag_resource` | KBDB resource_tag| — | — | MCP only。⚠️ tag resource_id 語意債(UUID vs name),待方向①收斂(SDD §4.1)|
| whoami | `acr whoami` | `arcrun_whoami` | `GET /me` | ✅ | ✅ | 已對齊 |
---
## 已知債(連 SDD
- ⚠️ **MCP deploy 死端點**`/workflows/deploy` 不存在 → #8 ①-a(先擋)+ #10(編排下沉)。
- ⚠️ **validate 漂移**:依賴 #10 編排下沉後才能統一吃 YAML。
- ⚠️ **tag resource_id 語意**UUID vs name 不明確,待方向①收斂統一為 name。
## 防複發檢查點(新增能力時)
1. 新 CLI 命令 / MCP 工具 → **本表加一行**
2. 填「server 端點」前,`grep` 確認該 route 在 cypher-executor 裡**存在**(否則就是死端點)。
3. 同能力兩介面 → 確認「同源」打同一端點;刻意單邊 → 備註記明原因。
4. 宣稱「對齊/完成」前 → 跑 `scripts/thin-shell-smoke.sh`(對真端點斷言非 404)。
@@ -0,0 +1,79 @@
# self-hosted KBDB 能力清單(查詢能力對照)
> 來源:issue #5(普世框架視角——任何 self-hosted 用戶都該知道自架 `arcrun-kbdb` 提供哪些查詢能力)。
> 範圍:**base tierD1-only,免費不綁卡)**。optional 模組(embed/triplet)另標。
> 鐵律:API-as-Wall(只經 HTTP API,不直連 D1)/零建表/零 SQL 暴露給用戶。
---
## 兩條存取路徑
| 路徑 | 對象 | 入口 | 隔離 |
|------|------|------|------|
| **cypher proxy `/kbdb/*`** | app 前端 / CLI`acr kbdb` | `cypher.arcrun.dev/kbdb/*`self-hosted 指自己的 cypher | X-Arcrun-API-Key → owner_id,自動租戶隔離 |
| **raw worker** | 內部 / MCP service binding | `arcrun-kbdb.<sub>.workers.dev` | 無 auth(內網),owner_id 由 caller 帶 |
> **前端存取準則**app(如自架的任何 Next.js 前端)**走 cypher proxy `/kbdb/*`**,不直連 raw worker——proxy 才有 owner_id 租戶隔離。CLI/MCP 是薄殼(thin-shell §0),底層同一條 proxy。
---
## base 查詢能力(現有)
### entries(原子資料 / 樹節點)
| 能力 | 端點 | filter / 參數 |
|------|------|------|
| 建 | `POST /entries` | `entry_type`(必), content, owner_id, parent_id, page_name, metadata_json… |
| 列 + 過濾 | `GET /entries` | `entry_type` / `owner_id` / `parent_id` / `page_name` / **`source`**(#5.1) / `limit` / `offset` |
| 取單筆 | `GET /entries/:id` | — |
| 改 | `PATCH /entries/:id` | 任意可改欄位(proxy 會剝 owner_id 防認領) |
| 刪 | `DELETE /entries/:id` | ⚠️ **raw 有;cypher proxy 暫未開**(見下「擱置」) |
| **關鍵字搜尋** | `GET /entries/search?q=` | D1 `LIKE`,回 `mode:'keyword'`。owner_id 限本租戶 |
- **`source` 過濾(#5.12026-06-26**:按 ingest 來源篩(envelope `source.uri`)。實作走 SQLite
`json_extract(metadata_json,'$.source')`——**source 埋在 metadata_json,零建表**即可查。
例:`GET /kbdb/entries?source=logseq://vault/foo.md`
### templates(虛擬表定義=替代建表)
| 能力 | 端點 |
|------|------|
| 建 / 列 / 取 / 改 slots | `POST /templates``GET /templates``GET /templates/:idOrName``PATCH /templates/:id` |
> 鐵律:template = 萬用表的 slot 定義,**不是建真表**。AI/用戶只能「建 template(name+slots) + 填 record」,無 CREATE TABLE / SQL。
### recordstemplate 實例=填 slot
| 能力 | 端點 |
|------|------|
| 建 | `POST /records`template + values |
| 列某 template 下 | `GET /records/by-template/:template`owner_id 限本租戶) |
| 取單筆 | `GET /records/:recordId` |
| **改 slot 值** | `PATCH /records/:recordId`#6,翻 slot 值=改底層 entries.content,三表 append-only 不破) |
### recipe-stats(市場成功率)
| 能力 | 端點 |
|------|------|
| 記一次成功/失敗 | `POST /recipe-stats/record` |
| 查某 recipe 統計 | `GET /recipe-stats/:canonical_id` |
---
## 語義搜尋(optional embed 模組)
- **base 不含語義搜尋**——關鍵字 `GET /entries/search``LIKE``mode:'keyword'`)。
- 開 **embed 模組**CF Vectorize binding,自付費)後升級語義(`mode:'semantic'`)。是 base 的 optional 模組,
不裝保持輕(free-tier 友善)。開法 + 行為見 issue #7 / kbdb-base SDD T2.4。
---
## 擱置 / 不做(誠實標明,避免用戶誤以為有)
| 項 | 狀態 | 原因 |
|----|------|------|
| cypher proxy `DELETE /kbdb/entries/:id` | ⏸ **暫擱置** | 依賴頂層「死資料自動刪除原則」(mira-dissolve T8 未定)。raw worker 有 DELETE,但裸 delete-by-id 無 owner 檢查,經 proxy 暴露=跨租戶刪除風險 → 補時要先驗 owner_id 才放行。 |
| documents 聚合(GROUP BY page_name → block_count | ❌ **不做** | 「跨 vault 的圖」走 **graph MCP**traverse/neighbors),不靠 KBDB 出 SQL 聚合端點。普世用戶不需要。 |
> 這份**不列**「documents / process-page 待移植」——那是舊 SaaS KBDBkbdb.finally.click/ 舊河道頁視角,
> 新架構不移植。self-hosted base 的能力以本清單為準。
@@ -0,0 +1,189 @@
# 階段一:檔案分類清單
> 共 101 個 .md 檔案,按建議位置分類。
> 信心度:確定/不確定 | 原因
---
## 📌 根目錄 — 保留(核心配置)
| 檔案 | 判定 | 原因 |
|------|------|------|
| CLAUDE.md | ✅ 保留 | 項目規範入口,cc 必讀 |
| README.md | ✅ 保留 | 對外項目說明 |
| DECISIONS.md | ⚠️ → docs/2-architecture/decisions/ | 架構決策歷史,應歸檔 |
| BACKLOG.md | ⚠️ → docs/3-specs/ | 需求清單,隨 SDD 更新 |
| BETA_TEST.md | ⚠️ → docs/5-records/test-reports/ | 測試記錄 |
| RELEASE-CHECKLIST.md | ✅ → docs/4-guides/ | 操作手冊 |
| CONTRIBUTING.md | ✅ → docs/6-user/ | 對外開發指南 |
| AGENTS.md | ⚠️ → docs/4-guides/ | Agent 用法指南 |
---
## 📦 .agents/specs/ → docs/3-specs/
(高優先,直接搬家,保留目錄結構)
### arcrun-core-mvp/
- requirements.md ✅
- design.md ✅
- tasks.md ✅
### arcrun-platform-evolution/
- requirements.md ✅
- design.md ✅
- tasks.md ✅
### arcrun/ (主線)
- arcrun.md ✅
- auth-recipe.md ✅
### arcrun/credential-primitives-wasm/
- design.md ✅
- tasks.md ✅
### arcrun/frontend-redesign/
- requirements.md ✅
- design.md ✅
- tasks.md ✅
- design-source/SOURCE_README.md ✅
- design-source/design-chat.md ✅
### arcrun/kbdb-base/
- design.md ✅
- tasks.md ✅
### arcrun/landing-page.md ✅
### arcrun/sdk-and-website/
- design.md ✅
- requirements.md ✅
- tasks.md ✅
- config-layering.md ✅
- mcp-account-source.md ✅
- self-hosted-init.md ✅
### component-gatekeeping/
- requirements.md ✅
- design.md ✅
- tasks.md ✅
- recipe-push-gatekeeping.md ✅
### component-registry-canon/
- design.md ✅
- tasks.md ✅
### data-exfil-warning/
- requirements.md ✅
- design.md ✅
- tasks.md ✅
### llm-interface/
- requirements.md ✅
- design.md ✅
- tasks.md ✅
### recipe-system/
- design.md ✅
- tasks.md ✅
### resumable-workflow/
- design.md ✅
- tasks.md ✅
### user-cc-harness/
- design.md ✅
- tasks.md ✅
---
## 🏗️ .claude/rules/ → docs/2-architecture/
(高優先,技術棧 + 架構規範)
| 檔案 | 目標位置 | 說明 |
|------|---------|------|
| 00-sdd-protocol.md | docs/2-architecture/ | SDD 協議(流程規範) |
| 01-tech-stack.md | docs/2-architecture/ | 技術棧硬限制(三層語言) |
| 02-forbidden.md | docs/2-architecture/ | 禁止行為(hook 強制) |
| 03-component-architecture.md | docs/2-architecture/ | 零件架構定義(R2/binding/URL |
| 04-current-progress.md | docs/2-architecture/ | 當前進度(動態,常更新) |
| 05-deploy-convention.md | docs/4-guides/deploy/ | 部署慣例(操作手冊) |
| 06-mindset.md | docs/2-architecture/ | 設計哲學(為什麼層) |
| 07-thin-shell.md | docs/2-architecture/ | 薄殼原則鐵律 |
---
## 📋 docs/ 現有 → 重新分類
### docs/incidents/ → docs/5-records/incidents/
- 2026-05-13-cypher-outbound-522.md ✅
- 2026-05-13-chain-ctx-propagation.md ✅
- 2026-05-13-nested-foreach-iterable.md ✅
- 2026-05-29-encryption-key-drift.md ✅
- README.md ✅
### docs/ 根目錄
| 檔案 | 新位置 | 說明 |
|------|--------|------|
| pre-customer-checklist-2026-06-07.md | docs/5-records/test-reports/ | 測試檢查清單 |
| 壓測-recipe-library-2026-06-07.md | docs/5-records/test-reports/ | 壓測報告 |
### docs/user_requirements/ → docs/6-user/ 或 docs/3-specs/
(需求源,可適度歸檔,但常引用)
- credential_parts.md → docs/3-specs/
- wishlist.md → docs/6-user/
- u6u-plan.md → docs/3-specs/ (歷史規劃)
- u6u_design.md → docs/3-specs/
- u6u_system_spec.md → docs/3-specs/
- 其他 ADR → docs/2-architecture/decisions/
---
## 🛠️ 專案子目錄內的文件
### cli/
- CHANGELOG.md → docs/5-records/(版本歷史)
- harness/ → 保留(bundled into npm package
### landing/
- CLAUDE.md → 保留(子項目規範)
- README.md → 保留(對外說明)
- AGENTS.md → docs/4-guides/ agent 文件)
### mcp/
- README.md → 保留(對外說明)
- GUIDE.md → docs/4-guides/mcp-setup.md
- dev/review.md → docs/4-guides/mcp-development.md
### registry/
- examples/README.md → docs/4-guides/workflow-examples.md
- examples/*/description.md → docs/4-guides/examples/ (搬進)
- skills/README.md → docs/4-guides/
- skills/*.md → docs/4-guides/skills/ (搬進)
- components/*/README.md → docs/4-guides/components/ (有名字的元件文件)
### tests/
- TEST_CASES.md → docs/5-records/test-reports/
---
## 📊 統計
| 類別 | 件數 | 狀態 |
|------|------|------|
| docs/1-vision/ | 0(待補) | 待建 |
| docs/2-architecture/ | ~20 | 從 .claude/rules + DECISIONS |
| docs/3-specs/ | ~40 | 從 .agents/specs + user_requirements |
| docs/4-guides/ | ~15 | 從 registry/skills + README 類 |
| docs/5-records/ | ~10 | 從 incidents + test-reports |
| docs/6-user/ | ~5 | 從 CONTRIBUTING + user_requirements |
| .claude/wiki/ | 4+N | 待建 |
| 保留根目錄 | 3 | CLAUDE.md, README.md, .claude/ |
---
## ⏭️ 下一步
確認上述分類無誤後,進行階段二(逐一讀文件,建 wiki)。
@@ -0,0 +1,144 @@
# 官方 KBDB 誤寫清理 SOPissue #3 待辦 1
> **狀態**:runbook 已備妥,**待官方運營方(leo)親自執行**。
> **為什麼不由 CC 直接跑**:對官方 prod D1 執行不可逆 `DELETE`,需官方憑證 + 人類明示確認
> mindset §7「絕不代替人類做有風險的確認」;rule 06)。CC 只備妥可審、防誤刪的腳本,DELETE 由人按下。
> **來源**issue #3leo 2026-06-24 拍板,14-E 遷移善後)。根因 bug 已修(issue #2commit 9c4333d)。
---
## 背景
14-E 遷移期間(issue #2`KBDB_BASE_URL` fallback bug 修好**之前**),mira 的 `_kbdb_client.py`
**~11 萬筆 `owner_id='leo'`** 的資料誤寫進**官方 prod kbdb**`arcrun-kbdb`,非 leo21c self-hosted)。
- **歸屬**:官方 SaaS 庫的清理 = arcrun 官方運營方的事,不是 mira(用戶)。讓用戶拿官方憑證 DELETE 官方 prod 本身違反隔離。
- **重要性**:SaaS 尚未營運,這批誤寫資料不重要 → 可刪。但**官方 prod DELETE 不可逆 → 必須防誤刪**。
---
## 目標庫(精確座標)
| 項目 | 值 |
|------|-----|
| Worker / DB name | `arcrun-kbdb` |
| D1 database_id | `0c580910-e00b-4f8e-9c57-ac54ea52242f`(官方 prod,見 `kbdb/wrangler.toml:13` |
| 官方 CF account | `58309bb9…`(記憶 [[cf-account-official-vs-loadtest]] |
| 誤寫標記 | `entries.owner_id = 'leo'` |
⚠️ **帳號對齊**:執行前確認本機 wrangler 對的是**官方帳號**(不是 leo21c)。
`wrangler whoami` 應顯示官方 uncle6.me account。誤寫在官方庫,所以這次**就是要對官方帳號**操作
(與 self-hosted 部署相反,那邊要避開官方——見記憶 [[selfhosted-deploy-account-override-trap]])。
---
## 表關係(決定刪除範圍)
base 三表(`kbdb/migrations/0001_base.sql`):
- `entries`:主表,誤寫資料在這(`owner_id='leo'`)。
- `entry_values`slot-link`entry_id REFERENCES entries(id)`。若 leo 資料含 record(用 template 組的結構化資料),
其 slot 連結在這。**刪 entries 會留下孤兒 entry_values** → 要一併清。
- `templates``created_by` 可能是 `'leo'`。**先確認** leo 有沒有建 template(待辦 2 步驟會查),
template 較可能是共享/誤建,刪前單獨核實。
---
## SOP(逐步,每步有 gate,防誤刪)
> 全程用 `wrangler d1 execute arcrun-kbdb --remote --command "..."`。**`--remote` 不可漏**(漏了打本地空庫,假綠)。
### 步驟 0:帳號 + 庫核實(gate)
```bash
wrangler whoami # 確認 = 官方 uncle6.me account58309bb9
wrangler d1 info arcrun-kbdb # 確認 database_id = 0c580910...
```
### 步驟 1:備份(整庫導出,防誤刪的底氣)
```bash
wrangler d1 export arcrun-kbdb --remote --output backup-before-cleanup-2026-06-24.sql
ls -lh backup-before-cleanup-2026-06-24.sql # 確認檔案非空、大小合理
```
### 步驟 2:標記 / 核實刪除範圍(**最關鍵的 gate**)
```bash
# 2a. 誤寫總數(應 ~11 萬)
wrangler d1 execute arcrun-kbdb --remote --command \
"SELECT count(*) AS leo_entries FROM entries WHERE owner_id='leo';"
# 2b. 關鍵確認:owner_id='leo' 是否只有這批誤寫,有沒有別的 leo 真資料混入
# 看 entry_type 分布 + 時間範圍(誤寫應集中在 14-E 遷移那段時間)
wrangler d1 execute arcrun-kbdb --remote --command \
"SELECT entry_type, count(*) AS n, min(created_at) AS first_at, max(created_at) AS last_at \
FROM entries WHERE owner_id='leo' GROUP BY entry_type ORDER BY n DESC;"
# 2c. 孤兒 entry_values(會被 entries 刪除留下的)
wrangler d1 execute arcrun-kbdb --remote --command \
"SELECT count(*) AS leo_entry_values FROM entry_values \
WHERE entry_id IN (SELECT id FROM entries WHERE owner_id='leo');"
# 2d. leo 建的 template(單獨核實,勿盲刪——可能是共享/系統 template 誤標)
wrangler d1 execute arcrun-kbdb --remote --command \
"SELECT id, name, created_by FROM templates WHERE created_by='leo';"
```
**Gate 判定**(人類看數字決定):
- 2a count ≈ 11 萬、2b 時間集中在遷移期 → 範圍乾淨,可進步驟 3。
- 若 2b 出現非遷移期、或 entry_type 異常 → **停手**,逐筆核實,別整批刪。
- 2d 若有 template → 個別判斷是否該刪(template 通常想保留,除非確認是誤建)。
### 步驟 3:刪除(確認範圍乾淨後)
```bash
# 3a. 先刪孤兒 entry_values(外鍵指向即將被刪的 entries)
wrangler d1 execute arcrun-kbdb --remote --command \
"DELETE FROM entry_values WHERE entry_id IN (SELECT id FROM entries WHERE owner_id='leo');"
# 3b. 再刪 entries
wrangler d1 execute arcrun-kbdb --remote --command \
"DELETE FROM entries WHERE owner_id='leo';"
# 3c.(可選,僅當步驟 2d 確認某 template 是誤建才刪)
# wrangler d1 execute arcrun-kbdb --remote --command \
# "DELETE FROM templates WHERE created_by='leo' AND id='<確認過的 id>';"
```
### 步驟 4:驗證(刪後核實)
```bash
# 4a. leo 誤寫歸零
wrangler d1 execute arcrun-kbdb --remote --command \
"SELECT count(*) AS remaining_leo FROM entries WHERE owner_id='leo';" # 應 = 0
# 4b. 孤兒 entry_values 歸零
wrangler d1 execute arcrun-kbdb --remote --command \
"SELECT count(*) AS orphan_ev FROM entry_values \
WHERE entry_id NOT IN (SELECT id FROM entries);" # 應 = 0
# 4c. 官方庫其餘資料不受影響(總 entries 數應 = 刪除前總數 - 11 萬)
wrangler d1 execute arcrun-kbdb --remote --command \
"SELECT owner_id, count(*) AS n FROM entries GROUP BY owner_id ORDER BY n DESC;"
```
### 步驟 5:收尾
- 4a/4b/4c 符合預期 → 清理完成。
- 備份檔(`backup-before-cleanup-2026-06-24.sql`)可棄(SaaS 未營運,無長期保留必要)。
- 在 issue #3 comment 回報:刪除數量 + 驗證 count + 確認官方庫其餘不受影響。
---
## 為什麼這樣設計(防誤刪三道閘)
1. **備份先行**(步驟 1):DELETE 不可逆,先有 d1 export 的整庫快照當底氣。
2. **核實再刪**(步驟 2 gate):不盲信「~11 萬都是誤寫」,看 entry_type + 時間分布確認範圍乾淨,
排除別的 leo 真資料混入(issue #3 待辦 1 步驟 2 的「關鍵確認」)。
3. **驗證歸零**(步驟 4):刪後客觀證據(count=0 + 孤兒=0 + 其餘不受影響),不靠「跑完了」口頭宣布
(mindset §7 完成=客觀證據)。
## 待辦 2(願景)落點
`acr migrate` 一等公民雙向遷移已記頂層 `docs/1-vision/product-wishlist.md` C7 + 本 repo backlog,不急做。
詳見 issue #3 待辦 2。
+32
View File
@@ -0,0 +1,32 @@
# 5. Records — 日誌 + 驗收報告
> 線上事件復盤、測試報告、決策軌跡。
## 線上事件
| 檔案 | 日期 | 內容 |
|------|------|------|
| **incidents/2026-05-13-cypher-outbound-522.md** | 2026-05-13 | 同 zone 自循環死鎖(22 分鐘故障) |
| **incidents/2026-05-13-chain-ctx-propagation.md** | 2026-05-13 | workflow chain context 無法向下傳播 |
| **incidents/2026-05-13-nested-foreach-iterable.md** | 2026-05-13 | 巢狀 foreach iterable 爆解析 |
| **incidents/2026-05-29-encryption-key-drift.md** | 2026-05-29 | 多 Worker 的 ENCRYPTION_KEY 不一致 |
## 測試報告
| 檔案 | 日期 | 內容 |
|------|------|------|
| **test-reports/壓測-recipe-library-2026-06-07.md** | 2026-06-07 | kbdb-base §7.5 上線驗收(16 項通過) |
| **test-reports/pre-customer-checklist-2026-06-07.md** | 2026-06-07 | 對客前檢查清單 |
| **test-reports/BETA_TEST.md** | — | 封測計畫(推遲) |
---
## 常見用途
- **線上問題復現** → 查 incidents/
- **功能驗收標準** → 查 test-reports/
- **決策背景** → 查 `.claude/wiki/decisions-summary.md`
---
更新時間:2026-06-08
@@ -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)會踩同樣假設的最好預防。
@@ -0,0 +1,286 @@
# arcrun 封測指南
感謝你參與 arcrun 的封測。
arcrun 是一個讓 AI 和人都能直接讀寫、執行的 workflow 工具。
你的任務是測試核心功能,並記錄任何不符合預期的地方。
---
## 環境安裝(5 分鐘)
```bash
npm install -g arcrun
acr --version # 應顯示 1.1.0 或以上
```
---
## 模式選擇
arcrun 有兩種使用模式:
### Local 模式(不需要帳號,快速試用)
```bash
mkdir my-workflows && cd my-workflows
acr init --local
```
建立 `~/.arcrun/config.yaml`local 模式)和一個 `hello.yaml` 範例。
```bash
acr validate hello.yaml --offline
acr run hello --input input="Hello, arcrun!"
```
預期看到:`"result": "HELLO, ARCRUN!"`
### Standard 模式(需要 API Key,支援 Webhook 部署)
```bash
acr init
```
互動式設定,輸入 email 後自動取得 API Key,存入 `~/.arcrun/config.yaml`
---
## 零件清單
執行以下指令查看所有可用零件:
```bash
acr parts
```
取得單一零件的 config 範本:
```bash
acr parts scaffold string_ops
acr parts scaffold http_request
acr parts scaffold gmail # 含 credentials.yaml 範本
```
---
## 可用零件(21 個,不需要帳號)
### 字串操作 — `string_ops`
```yaml
config:
my_node:
component: string_ops
operation: upper # upper / lower / trim / length / replace / split / join
```
### 數字運算 — `number_ops`
```yaml
config:
my_node:
component: number_ops
operation: add
b: 10 # 加上 10
```
支援:`add` / `sub` / `mul` / `div` / `round` / `floor` / `ceil` / `abs`
### HTTP 請求 — `http_request`
```yaml
config:
my_node:
component: http_request
method: GET # GET / POST / PUT / DELETE
```
```bash
acr run notify --input url="https://httpbin.org/get"
```
### 其他零件
```
if_control 條件分支(ON_SUCCESS / ON_FAIL 路由)
switch 多分支條件
foreach_control 迭代陣列
filter 過濾陣列
set 設定固定值到 context
array_ops 陣列操作(push / pop / slice
date_ops 日期操作(now / format / diff
validate_json 驗證 JSON Schema
ai_transform_compile / ai_transform_run AI 自然語言轉換
```
---
## 動態參數 `{{variable}}`
config 裡的字串欄位支援 `{{variable}}`,從 `--input` 取值:
```yaml
# flexible.yaml
name: flexible
flow:
- "input >> ON_SUCCESS >> process"
config:
process:
component: string_ops
operation: "{{op}}"
```
```bash
acr run flexible --input input="hello" --input op=upper # → HELLO
acr run flexible --input input="HELLO" --input op=lower # → hello
```
---
## 錯誤路由(ON_FAIL
```yaml
# safe-fetch.yaml
name: safe-fetch
flow:
- "input >> ON_SUCCESS >> fetch"
- "fetch >> ON_FAIL >> fallback"
config:
fetch:
component: http_request
method: GET
fallback:
component: string_ops
operation: upper
```
```bash
# 故意讓 fetch 失敗,觸發 fallback
acr run safe-fetch \
--input url="https://invalid.domain.xyz" \
--input input="fallback triggered"
```
---
## 中文語意
flow 支援中文關係詞:
```yaml
flow:
- "輸入 >> 完成後 >> 轉換"
- "轉換 >> 失敗時 >> 錯誤處理"
```
---
## Webhook 部署(Standard 模式)
讓外部網頁或服務能觸發你的 workflow:
```bash
# 部署 workflow
acr push my-workflow.yaml
```
輸出範例:
```
✓ "my-workflow" 已部署
Webhook URLhttps://cypher.arcrun.dev/webhooks/named/my-workflow/trigger
需帶 HeaderX-Arcrun-API-Key: ak_...
curl 觸發範例:
curl -X POST https://cypher.arcrun.dev/webhooks/named/my-workflow/trigger \
-H 'X-Arcrun-API-Key: ak_your-key' \
-H 'Content-Type: application/json' \
-d '{"message": "hello"}'
```
---
## API Recipe(整合外部服務)
不需要 deploy Worker,只要上傳 recipe YAML
```bash
acr recipe push my-recipe.yaml
acr recipe list
acr recipe delete rec_xxxxxxxx
```
Recipe 上傳後會得到 `rec_xxxxxxxx` hash,可直接在 workflow config 的 `component` 欄位使用。
---
## Credential 管理(Standard 模式)
需要帶 token 的零件(gmail、telegram、notion 等)可以提前上傳 credential,執行 workflow 時自動注入。
**加密金鑰在 `acr init` 時已自動取得並存入 `~/.arcrun/config.yaml`,不需要手動設定。**
**步驟一:查看某服務需要哪些 credential**
```bash
acr auth-recipe scaffold notion # 輸出 credentials.yaml 範本 + workflow 使用範例
acr auth-recipe list # 列出所有支援的服務(20 個)
```
**步驟二:建立 credentials.yaml**(參考 scaffold 的輸出):
```yaml
# 範例:Notion
notion_token: "secret_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
# 範例:Telegram Bot
telegram_bot_token: "123456789:your-bot-token"
```
**步驟三:上傳**
```bash
acr creds push credentials.yaml
```
上傳後執行 workflow 時,tokens 自動注入,不需要在 `--input` 手動帶。
### 支援的第三方服務(20 個)
```bash
acr auth-recipe list
```
輸出:Notion、Slack、GitHub、OpenAI、Anthropic、Airtable、Discord、Stripe、Twilio、SendGrid、HubSpot、Linear、Shopify、Resend、Supabase、Typeform、Jira、Google SheetsService Account)、GmailService Account)、Google DriveService Account
---
## 回饋格式
請把你的觀察記錄在 `FEEDBACK.md`,格式不限,但希望包含:
1. **成功的地方** — 哪些功能符合預期?
2. **失敗的地方** — 錯誤訊息是什麼?步驟是?
3. **困惑的地方** — 不知道怎麼用、文件不清楚的地方
4. **想要的功能** — 你覺得少了什麼
---
## 已知限制
- `number_ops` 的數字參數(`a``b`)若從 `--input` 帶入為字串,需要零件自行做型別轉換(目前已支援)
- `ON_FAIL` 觸發時,fallback 節點收到的 context 包含上游的錯誤物件(`{success: false, ...}`
- 多節點串連時,context 為 flat merge,上游的 `data.result` 會直接合併到頂層
- `if_control` 條件為 false 時,不執行任何下游節點(沒有明確的 else 分支)
---
## 有問題?
遇到任何問題直接問。你的 API Key 是確定性的,只要用同一個 email 呼叫 `/register` 就能拿回來:
```bash
curl -X POST https://cypher.arcrun.dev/register \
-H "Content-Type: application/json" \
-d '{"email":"your@email.com"}'
```
@@ -0,0 +1,72 @@
# 交付前自測 Checklistpre-customer)— 2026-06-07
> 給 **你(人)** 在交給客戶前跑一次的精簡清單,不是給 Haiku 操盤的詳細壓測(那份在
> 壓測-recipe-library-2026-06-07.md)。順序照客戶真實旅程。**任一項 ❌ = 不能交付。**
> 過關標準都是客觀證據(HTTP 2xx / D1 數字 / 檔案存在),禁口頭過關。
---
## 1. 雲端後端活著(最快,先確認 deploy 沒掛)
- [ ] `curl https://arcrun-kbdb.uncle6-me.workers.dev/health``{"ok":true}`
- [ ] `curl https://cypher.arcrun.dev/public-recipes?q=gmail``{"found":true,...}`
- [ ] `curl https://cypher.arcrun.dev/health`(或任一既有端點)→ 200
- [ ] npm 上有最新 CLI`npm view arcrun version` → 1.3.2(或更新)
## 2. 冷啟動:空專案能裝起來(客戶第一步)
- [ ] 開**空目錄**`npm i -g arcrun``acr --version` 出版本
- [ ] `acr init`(local 模式)→ 不報錯、寫出 config
- [ ] 跟著它印的「下一步」跑 hello workflow → `acr run hello` 有輸出
- [ ] self-hosted 路徑)`acr init --self-hosted` 無 wrangler 時 → 錯訊清楚教裝 wrangler
## 3. 環境設定(self-hosted,客戶要用自己 CF
- [ ] `acr init --self-hosted --account-id X --api-token Y` → 建 KV + D1 + deploy 成功
- [ ] 印出的「下一步①②」可照抄:.envNAMESPACE+ENCRYPTION_KEY+ wrangler secret put
- [ ] D1 建出來且套了 migration`/recipe-stats/x` 回 stat 結構)
- [ ] 免綁卡(全程沒被要信用卡)
## 4. Recipe 公庫/私庫(本次新功能,重點測)
- [ ] `acr recipe search gmail` → 列出 recipe 含 author/market_stat
- [ ] `acr recipe search 不存在的xyz` → found:false + 創作引導 hint
- [ ] `acr recipe pull gmail_send` → 拉進私庫;`acr recipe list` 看得到
- [ ] 自製 recipe `acr recipe push` → 私庫有
- [ ] `acr recipe submit-p <id>`**跳暴露同意**(未同意/非互動擋住),同意後投稿成功
- [ ] 同 canonical 不同作者 submit-p 兩次 → 公庫並存兩筆(非覆蓋)
## 5. 做一件真實的事(端到端,客戶的目的)
- [ ] 建一個真實 workflow(如 webhook → http_request → 輸出),`acr push` + `acr run` 跑通回 2xx
- [ ] 用到 recipe 的 workflow 跑完 → `curl kbdb/recipe-stats/{uuid}` success_count +1
- [ ] 缺 credential 時 → 誠實標「未驗收:缺 X」,不假裝成功(401/403 不當 bug
## 6. 兩介面一致(CLI + MCPAI 客戶會用 MCP
- [ ] MCP 已裝(`acr mcp-setup` 或 init 自動)→ `.mcp.json` 存在
- [ ] MCP `arcrun_recipe_search` 回的 = CLI `acr recipe search` 回的(同一組)
- [ ] CLI pull 後 MCP `arcrun_recipe_list` 看得到(同一私庫帳號)
⚠️ 若 self-hosted 的 MCP 連到平台而非自己 cypher → 記下(§5.2 已知違反待修)
## 7. 回歸(沒把舊功能弄壞)
- [ ] 既有種子 recipegmail/telegram)的 workflow 跑通(UUID 重構沒破執行鏈)
- [ ] `acr recipe list` 無重複項(同 canonical 舊 key+uuid 兩筆)
- [ ] 既有 workflow push/list/run 全正常
## 8. 誠實性 / 安全界線(交付前必守)
- [ ] 暴露動作(submit-p / push webhook)都有人類明示同意關卡
- [ ] 非互動環境(你直跑)→ 需確認處會停下,不自己偽造同意
- [ ] 任何「未實作/缺 credential」誠實回 success:false 或標未驗收,無假綠
---
## 交付判定
- 全 ✅ → 可交付。
- 任一 ❌ → 修掉再交(記下是哪項)。
- ⚠️(已知違反,如 §5.2 MCP account-source)→ 寫進交付說明的「已知限制」,不假裝沒有。
**禁假綠**:沒實際看到證據的項目標「未測」,不要勾 ✅。
@@ -0,0 +1,162 @@
# 壓測 Test Case — Recipe 公庫/私庫機制 + UUID2026-06-07 deploy 後)
> 對象:本次上線的 kbdb-base §7.5(公庫/私庫雙向、UUID 身份、市場數據)+ 回歸。
>
> **操盤模型:全程 Haiku。Haiku 能搞定是「設計目標」不只是壓測手段(richblack 2026-06-07)。**
> 理由:arcrun 價值=比直接開發容易→用戶省 token+省時間+可重複用。若只有 Sonnet 能驅動 arcrun
> 「省」就不成立(Sonnet 貴)。**Haiku 就能搞定才證明 arcrun 真降低門檻**。前瞻:未來要接 Gemini /
> 更弱模型門檻只會更高,現在用最弱的 Haiku 把介面磨到夠白痴化,未來接別的模型才不會更難。
>
> **∴ Haiku 撞牆 = 設計缺陷訊號(不是「換 Sonnet 解決」),撞牆點就是要修介面的地方。**
> 只有真的撞牆才**暫時**升 Sonnet 跑同一 case,用來判別「是介面問題(Haiku/Sonnet 都該過但 Haiku 過不了→修介面)
> vs 模型能力本質差異」。修完介面再用 Haiku 重測該 case。
> arcrun 是 AI 呼叫的工具 → 壓測打 floor 不打 ceiling。
>
> 介面:兩條都要測(CLI `acr` + MCP tools),驗薄殼一致性(rule 07 §5)。
> 判定原則(mindset §7):完成=客觀證據(HTTP status / D1 數據 / 2xx),不是口頭宣布。
---
## Cold. 冷啟動:空專案 → 能跑(真實第一次體驗,最容易撞牆)— Haiku
> 真實起點:用戶在 VSCode 開一個**空白專案**,叫 Haiku「幫我做 X 工作」。
> 此時 **arcrun 沒裝、環境沒建、CF 沒設**。Haiku 要從零把環境建起來才談得上做事。
> 這是 self-hosted-init.md 流程的 dogfood。**裝不起來後面全白搭 → 這組是壓測 floor 的 floor。**
>
> 測法:給 Haiku 一句話需求(如「幫我做一個每天抓 RSS 存到 Google Sheet 的工作流」),
> **不給任何安裝指示**,看它能否自己摸出完整環境建置。觀察它卡在哪 = 介面要磨白痴化的地方。
| # | 觀察點 | 預期(Haiku 自己走通) | 判定(撞牆=介面缺陷) |
|---|--------|------|------|
| Cold.1 | Haiku 是否知道「要先裝 arcrun」 | 自己找到 `npm i -g arcrun` 或從 MCP/README 得知 | 卡 → 入口可發現性不足 |
| Cold.2 | 選模式:local / standard / self-hosted | Haiku 能依需求選對(要存 credential→standard/self-hosted;純試→local | 卡 → `acr init` 模式說明不夠白痴 |
| Cold.3 | self-hosted 前置:wrangler 未裝時 | 錯訊「npm i -g wrangler 後重跑」→ Haiku 照做 | 卡 → 前置提示不可自癒 |
| Cold.4 | `acr init --self-hosted` 非互動參數 | Haiku 知道要帶 --account-id/--api-token(或被引導) | 卡 → 非互動路徑不明 |
| Cold.5 | 建 .envNAMESPACE + ENCRYPTION_KEY | Haiku 照「下一步①」生成 key 並寫 .env | 卡 → key 生成指令是否現成可抄 |
| Cold.6 | wrangler secret put ENCRYPTION_KEY | Haiku 照「下一步②」對各 worker 設 secret | 卡 → 多 worker 共用 key 是否講清楚([[encryption-key-drift-trap]] |
| Cold.7 | MCP / harness 安裝(acr mcp-setup / install-harness | 自動或被引導補上 | 卡 → 補裝路徑是否被提示 |
| Cold.8 | 環境就緒後,Haiku 能接著做原始需求 | 不卡在環境、進入實際 workflow 建置 | **冷啟動到能做事的端到端卡點數** |
> **核心觀察**:Haiku 從「一句話需求 + 空專案」到「環境就緒能做事」,**全程靠 CLI 輸出 + 錯誤訊息 + MCP 工具描述自我引導**,
> 不靠人類補指示、不靠強模型腦補。每個卡點記下 = arcrun onboarding 要磨白痴化的清單。
> 非互動雷區:mindset §7「非 TTY 直跑就拒絕、不自塞 flag 假裝人類同意」——Haiku 遇到需人類確認處(建 CF 資源、暴露)
> 應停下請用戶確認,不自己偽造同意。測這個界線有沒有守住。
---
## 0. 前置 / 環境健康(回歸,每輪先跑)
| # | 操作 | 預期 | 判定 |
|---|------|------|------|
| 0.1 | `curl https://arcrun-kbdb.uncle6-me.workers.dev/health` | `{"ok":true}` | KBDB 活著 |
| 0.2 | `curl https://cypher.arcrun.dev/public-recipes?q=gmail` | `{found:true, recipes:[...]}` | 公庫端點上線 |
| 0.3 | `curl https://arcrun-kbdb.uncle6-me.workers.dev/recipe-stats/x` | `{success:true, stat:{...0}}` | D1 三表通 |
---
## A. 公庫搜尋 + 落空創作引導(§7.5.6)— Haiku
測「AI 找 recipe,公庫沒有時是否被正確引導去自己做」。
| # | 操作(CLI / MCP) | 預期 | 判定(暴露什麼) |
|---|------|------|------|
| A.1 | `acr recipe search gmail` / `arcrun_recipe_search{query:"gmail"}` | found:true,列 gmail_send 等,各帶 author/market_stat | 搜尋可用 |
| A.2 | 搜一個一定不存在的:`acr recipe search zzz_nonexistent_xyz` | **found:false + hint「可自己做一個投稿成為作者」** | **落空引導是否讓 AI 知道下一步**(不是回空陣列卡住) |
| A.3 | 接 A.2:操盤 AI 讀到 hint 後,**是否自己提議「那我做一個 recipe」** | AI 主動走向 push→submit-p(非停手說「找不到」) | **§7.5.6 閉環是否被 AI 接住**(這是核心壓測點) |
> A.3 是最關鍵的 Haiku 測點:弱模型若能靠 hint 自己走向創作,代表引導設計成功。
---
## B. 公→私 pull(§7.5.3 流1)— Haiku
| # | 操作 | 預期 | 判定 |
|---|------|------|------|
| B.1 | `acr recipe pull gmail_send` / `arcrun_recipe_pull{canonical_id:"gmail_send"}` | ✓ 拉進私庫,提示可用 component: gmail_send | pull 寫進私庫成功 |
| B.2 | `acr recipe list`(私庫)→ 應出現 gmail_send | 私庫有這筆 | pull 確實落地(不是只回成功訊息) |
| B.3 | pull 一個不存在的 `acr recipe pull zzz_nonexistent` | found:false + 創作引導 | pull 落空也引導(不報模糊錯誤) |
| B.4 | 指定作者 `acr recipe pull gmail_send --author=system` | 取 system 版本 | author 參數生效 |
---
## C. 私→公 submit-p + UUID 多作者並存(§7.5.5 app-store)— Haiku(撞牆才升 Sonnet 判別)
測 app-store 模型:同 canonical 多作者並存、submit=新增不覆蓋。
| # | 操作 | 預期 | 判定(暴露什麼) |
|---|------|------|------|
| C.1 | 建一個自製 recipe push 私庫(如 `my_test_api`endpoint 指 httpbin.org/post | ✓ 私庫有 | push 可用 |
| C.2 | `acr recipe submit-p my_test_api`(**需暴露同意**) | 提示暴露警示 → 同意後投稿公庫、領新 uuid | **暴露同意是否擋住**(mindset §6);非互動/未同意是否拒絕 |
| C.3 | 第二次用**不同作者**再 submit-p 同 canonical(模擬 John 版) | 公庫**並存兩筆**同 canonical 不同 uuid/author(非覆蓋) | **app-store 模型驗證**:覆蓋 vs 新增 |
| C.4 | `acr recipe search my_test_api` | 回**多筆**同名不同作者,各帶 market_stat | 多作者並存可被搜到 |
---
## D. 市場數據 per-uuid(§7.5.h)— Haiku(要跑工作流;撞牆才升 Sonnet)
測「跑工作流 → recipe 成功/失敗記到 KBDB per-uuid → 影響市場選擇」。
| # | 操作 | 預期 | 判定 |
|---|------|------|------|
| D.1 | pull 一個能實打通的 recipe(或用 httpbin 自製),建 workflow`acr run` 跑成功 | workflow 回 2xx | 工作流能跑 |
| D.2 | 跑完後 `curl kbdb/recipe-stats/{該 recipe uuid}` | success_count +1 | **5.1 成功記錄落 D1**per-uuid 非 canonical |
| D.3 | 故意讓 recipe 打不通(壞 endpoint)再跑,查 stat | failure_count +1 | 失敗也記、且區分 |
| D.4 | 同 canonical 兩作者版本各跑幾次成功率不同 → `recipe search` | market_stat 區分兩 uuid(不是同一份) | **§7.5.h per-uuid 真正生效**Leo/John 可區分) |
---
## E. 回歸 — 既有能力沒被 UUID 改動破壞(§7.5.f 向後相容)— Haiku
UUID key 重構最大風險 = 破執行鏈。必測既有 recipe 執行不掛。
| # | 操作 | 預期 | 判定 |
|---|------|------|------|
| E.1 | 用一個既有種子 recipegmail/telegram)建 workflow `acr run` | 正常執行(resolveRecipe 向後相容) | **執行鏈沒被 key 重構破壞** |
| E.2 | `acr recipe list` 不出現重複(同 canonical 舊 key + uuid 兩筆) | dedup 正確 | GET dedup 生效 |
| E.3 | `acr recipe delete {某 recipe}` 後再 list | 該 recipe 消失、索引清乾淨 | DELETE 清 uuid+索引 |
| E.4 | 既有 workflow push/list/run(與 recipe 無關) | 全正常 | 沒波及無關功能 |
---
## F. 薄殼一致性(rule 07 §5)— Haiku
同一能力 CLI 和 MCP 走出**同樣結果**(驗薄殼不漂移)。
| # | 操作 | 預期 | 判定 |
|---|------|------|------|
| F.1 | `acr recipe search gmail` vs `arcrun_recipe_search{query:"gmail"}` | 兩者回**同一組** recipe | CLI/MCP 不漂移 |
| F.2 | CLI pull 後,MCP `arcrun_recipe_list` 看得到(反之亦然) | 同一私庫、同一帳號 | **帳號來源統一**(§5.3self-hosted 帳號是否一致) |
| F.3 | MCP submit-p 的 exposure_consent 把關 = CLI 的暴露同意 | 兩者都擋未同意 | 暴露把關一致 |
> F.2 會踩到已知違反(§5.2 MCP account-source):若 self-hosted 用 MCP 連到平台 cypher 而非自己的,
> CLI 和 MCP 會看到不同私庫 → **這是預期會暴露的問題**,記下不算 bug 是待修項。
---
## G. 邊界 / 異常(誠實性測試,mindset §7)— Haiku
| # | 操作 | 預期 | 判定 |
|---|------|------|------|
| G.1 | submit-p 缺 endpoint / 缺 canonical_id | 400 明確錯 | 不假綠、錯訊清楚 |
| G.2 | pull 後私庫改該 recipe 再 submit-p | author 變自己、不冒原作者(derived_from 溯源) | **不冒名**(§7.5.5 |
| G.3 | 缺 credential 的 recipe 跑 workflow | 誠實標「未驗收:缺 X」、401/403 不當 arcrun bug | 不 mock 假綠(mindset §3/§7 |
| G.4 | migrate-uuid 重跑一次 | skipped 全部、migrated 0(冪等) | 重跑安全 |
---
## 判定總表(壓測完填)
每個 case 標:✅ 通過(附證據:HTTP status / D1 數字 / 截圖)/ ⚠️ 暴露問題(描述)/ ❌ 失敗。
**禁假綠**:沒實際拿到 2xx/數據就標「未驗收:缺 X」,不口頭宣布通過。
**重點觀察(全程 Haiku**
0. **Cold.1-8 冷啟動:Haiku 從空專案能否自己把環境裝起來**?← floor 的 floor,裝不起來後面全白搭
1. A.3 落空→Haiku 是否自己走向創作(§7.5.6 閉環被接住)?← 設計目標核心
2. 錯誤訊息的 next_actions 是否讓 Haiku 自癒(不靠更強模型腦補)?
3. F.2 CLI/MCP 帳號是否一致(§5.2 已知違反會在此暴露)?
4. D.4 多作者市場數據是否真正區分?
5. 複雜多步(pull→改→submit-p→跑→看市場)Haiku 能否一氣呵成?
**撞牆處理**Haiku 過不了某 case → 先判「是介面缺陷還是模型本質限制」:
暫時升 Sonnet 跑同一 case。Sonnet 過、Haiku 不過 → **介面缺陷**(修介面後 Haiku 重測)。
兩者都不過 → 功能 bug。記錄撞牆點 = arcrun 要磨白痴化的地方(前瞻 Gemini/更弱模型)。
+435
View File
@@ -0,0 +1,435 @@
# Contributing to arcrun
感謝你考慮貢獻 arcrun!本文件說明如何新增零件(WASM component)並提交至公眾零件庫。
arcrun 的零件**主要由 AI 撰寫**。你不需要是 TinyGo 或 AssemblyScript 專家,只需要把這份文件和你的 API 文件或需求貼給 AI,讓它生成源碼,你負責編譯、測試、提交。
---
## 選擇開發語言
零件只需要輸出符合 **WASI preview1**`.wasm` 檔案,與使用哪個語言無關。
| 語言 | 輸出大小 | AI 撰寫品質 | 說明 |
|------|---------|------------|------|
| **TinyGo** | 極小(10–80KB) | 優秀 | 官方零件使用;語法簡單,AI 出錯率低 |
| **AssemblyScript** | 小(20150KB | 良好 | TypeScript 語法,前端開發者最快上手 |
| **Rust** | 小–中(30–300KB) | 良好 | 效能最強;適合複雜演算法,工具鏈稍複雜 |
**AI 開發建議:**
- 選 **TinyGo**Go 語法與 TypeScript 差異夠大,AI 不易把 TS 邏輯直接搬過來造成錯誤,是最穩的選擇。
- 選 **AssemblyScript**:適合已熟悉 TypeScript 的開發者,但要注意 AS 不是 TS — 提示 AI 時明確說「AssemblyScript,不是 TypeScript」。
- 選 **Rust**:效能要求高時使用;需要更詳細的提示和更仔細的審查。
---
## 零件規格:共通規則
無論使用哪個語言,零件必須遵守:
- **I/O 模型**:從 `stdin` 讀取 JSON,往 `stdout` 輸出 JSON,不使用 return value
- **回傳格式**:成功 `{"success":true,"result":...}`,失敗 `{"success":false,"error":"..."}`
- **不 panic**:任何錯誤都應輸出 `success:false` JSON,不讓執行器收到空輸出
- **不使用網路 / 檔案系統**(功能類零件):`no_network_syscall: true`
- **允許網路**(整合類零件):`no_network_syscall: false`,必須宣告 `credentials_required`
---
## 目錄結構
```
registry/components/my_component/
├── component.contract.yaml # 零件規格宣告(必填)
├── main.go # TinyGo 源碼(TinyGo 零件)
├── assembly/index.ts # AssemblyScript 源碼(AS 零件)
├── src/lib.rs # Rust 源碼(Rust 零件)
└── my_component.wasm # 編譯產出(不提交至 git,CI 自動產生)
```
---
## component.contract.yaml
所有語言共用相同的合約格式:
```yaml
# component_hash_id 由 Registry 在提交時自動派發,格式為 cmp_{8碼hex}
# 提交者不需要填這個欄位,Registry 會根據 canonical_id 確定性生成
# Workflow 引用零件時,用 component_hash_id 才能保證永久不壞:
# component://cmp_a3f9b2c1 ← 推薦,即使 canonical_id 改名也不受影響
# component://string_reverse ← 方便,AI 寫 workflow 時用這個,Registry 自動解析
canonical_id: "string_reverse" # 見下方命名規範
display_name: "字串反轉" # 人類可讀,可中文,供 UI 顯示用
description: > # 語意搜尋用,見下方說明
將字串內容倒序排列,適合測試、資料清洗、回文判斷等場景。
不依賴外部服務,純本地運算。
category: "data" # api / logic / data / ai / style / anim / ui
version: "v1"
author: "@your-github-username"
wasi_target: "preview1"
stability: "floating" # floating / stable / pinned
runtime_compat:
- "cf-workers"
- "workerd"
- "wazero"
constraints:
max_size_kb: 2048
max_cold_start_ms: 50
no_network_syscall: true # 功能類 true,整合類 false
io_model: "stdin_stdout_json"
input_schema:
type: object
required: [text]
properties:
text:
type: string
description: 輸入文字
output_schema:
type: object
properties:
result:
type: string
gherkin_tests:
- scenario: "基本轉換"
given: '{"text":"hello"}'
then_contains: '"result"'
- scenario: "缺少必填欄位"
given: '{}'
then_contains: '"success":false'
config_example: |
transform:
text: "{{input.text}}"
description: "我的零件功能說明。"
```
整合類零件額外加入:
```yaml
credentials_required:
- key: my_api_token
type: api_key
description: "My Service API token"
inject_as: api_token
```
### canonical_id 命名規範
`canonical_id` 是零件的永久識別符,一旦上架不能更改(改名 = 新零件)。命名不統一會導致功能重複,請遵守以下規則:
| category | 格式 | 範例 |
|----------|------|------|
| `api`(整合類) | `{服務名}``{服務名}_{動作}` | `gmail``gmail_send``google_sheets``google_sheets_append``telegram` |
| `data`(資料處理) | `{資料型別}_ops``{動詞}_{名詞}` | `string_ops``array_ops``json_transform``csv_parse` |
| `logic`(控制流) | `{結構名}_control` 或單詞動詞 | `if_control``foreach_control``try_catch``switch``wait` |
| `ai`AI 類) | `ai_{動作}` | `ai_transform_compile``ai_summarize``ai_classify` |
**規則:**
- 全部小寫、底線分隔、最多 4 個單詞
- 禁止:中文、空格、大寫、連字號(`-`)、版本號混入(用 `version` 欄位表達)
- `display_name` 才是人類可讀名稱,可以是「宇宙無敵 gsheets 新增一列」,`canonical_id` 不行
**提交前自問:** 如果有人想用 AI 搜尋「幫我找一個可以新增 Google Sheets 列的零件」,他搜到的名字應該是什麼?答案就是你的 `canonical_id`
### description 寫法(語意搜尋)
`description` 是語意搜尋的索引來源,用自然語言描述「能做什麼、適合什麼情境」,而不是重複零件名稱。
**好的 description**
```yaml
description: >
傳送 Gmail 電子郵件,適合 Workflow 完成後通知使用者、發送訂閱確認信、
錯誤警報通知等場景。支援自訂主旨、內文與收件人。需要 Gmail OAuth token。
```
**不好的 description(等於沒有):**
```yaml
description: "Gmail 發信零件" # 只是名稱的同義詞,搜不到任何額外資訊
```
原則:把這個 description 給一個不知道這個零件存在的人看,他能判斷「這就是我要的東西」嗎?
### aliases(搜尋同義詞)
arcrun 在 `registry/aliases.yaml` 維護一份 scope 級別的同義詞表。當你的零件 `canonical_id` 以已知 scope 為前綴,Registry 建立搜尋索引時會**自動**把對應的同義詞合併進去,不需要在 contract 裡手動填。
例如 `canonical_id: google_sheets_append`Registry 會自動從 aliases.yaml 取得 `google_sheets` scope 的同義詞(`gsheets``試算表``spreadsheet`...),搜這些詞都能找到你的零件。
**如果你的零件有額外的情境同義詞**(超出 scope 範圍),可以在 contract 內手動補充:
```yaml
canonical_id: "google_sheets_append"
aliases:
- "新增資料列" # 情境同義詞,超出 scope 範圍
- "insert row"
# google_sheets scope 的同義詞(gsheets / 試算表 / spreadsheet...
# 由 registry/aliases.yaml 自動合併,不需要重複填寫
```
**想新增新 scope 的同義詞**(例如你要加一個 `notion` 零件):在 `registry/aliases.yaml` 的對應 category 下加一個新 key,開 PRmerge 後所有以 `notion_` 開頭的零件都自動繼承。
> 這個機制目前是手工維護。未來接入 KBDB 後,`canonical_id` 將獲得系統派發的唯一 hash id,同義詞表將成為 KBDB synonym graph 的初始資料。
---
## TinyGo 零件開發
### 環境安裝
```bash
# TinyGo
brew install tinygo # macOS
# 其他平台:https://tinygo.org/getting-started/
# 本機測試執行器
brew install wasmtime # macOS
```
### 給 AI 的提示範本
```
請幫我用 TinyGo 寫一個 arcrun WASM 零件。
需求:[你的需求]
規則:
- 從 stdin 讀取 JSON,往 stdout 輸出 JSON
- 成功回傳 {"success":true,"result":...}
- 失敗回傳 {"success":false,"error":"..."},不 panic
- 不使用網路、不使用檔案系統(純功能類零件)
- import 只用標準庫(encoding/json, os, fmt, strings 等)
請生成 main.go 和 component.contract.yaml。
```
### main.go 範本
```go
package main
import (
"encoding/json"
"fmt"
"os"
)
type Input struct {
Text string `json:"text"`
}
type Output struct {
Success bool `json:"success"`
Result string `json:"result,omitempty"`
Error string `json:"error,omitempty"`
}
func main() {
var input Input
if err := json.NewDecoder(os.Stdin).Decode(&input); err != nil {
writeError("invalid input: " + err.Error())
return
}
if input.Text == "" {
writeError("text is required")
return
}
// 你的邏輯
result := "[transformed] " + input.Text
out, _ := json.Marshal(Output{Success: true, Result: result})
fmt.Println(string(out))
}
func writeError(msg string) {
out, _ := json.Marshal(Output{Success: false, Error: msg})
fmt.Println(string(out))
}
```
### 編譯
```bash
cd registry/components/my_component
tinygo build -o my_component.wasm -target wasi .
```
### 本機測試
```bash
echo '{"text":"hello world"}' | wasmtime run my_component.wasm
# 預期:{"success":true,"result":"[transformed] hello world"}
echo '{}' | wasmtime run my_component.wasm
# 預期:{"success":false,"error":"text is required"}
```
---
## AssemblyScript 零件開發
### 環境安裝
```bash
# Node.js >= 18
node --version
# 初始化 AS 專案
npm init -y
npm install --save-dev assemblyscript
npx asinit .
# 本機測試執行器
brew install wasmtime # macOS
```
### 給 AI 的提示範本
```
請幫我用 AssemblyScript(不是 TypeScript)寫一個 arcrun WASM 零件。
需求:[你的需求]
規則:
- AssemblyScript 是 TypeScript 的子集,編譯為 WASM,不能使用 DOM / Node.js API
- 從 stdin 讀取 JSON(使用 WASI fd_read),往 stdout 輸出 JSON(使用 Console.log
- 成功回傳 {"success":true,"result":...}
- 失敗回傳 {"success":false,"error":"..."}
- 不使用網路、不使用檔案系統
請生成 assembly/index.ts 和 component.contract.yaml。
注意:AssemblyScript 沒有 JSON.parse,需要手動解析或使用 as-json 套件。
```
### assembly/index.ts 範本
```typescript
// AssemblyScript — 注意:這不是 Node.js / TypeScript
// 沒有 DOM、沒有 fetch、沒有 require
import { Console } from "as-wasi/assembly";
import { JSON } from "assemblyscript-json/assembly";
export function _start(): void {
// 從 stdin 讀取輸入
const input = Console.readAll();
// 解析 JSON
const parsed = JSON.parse(input);
if (!parsed.isObj) {
Console.log('{"success":false,"error":"invalid input"}');
return;
}
const obj = parsed as JSON.Obj;
const textVal = obj.getString("text");
if (textVal == null) {
Console.log('{"success":false,"error":"text is required"}');
return;
}
const text = textVal.valueOf();
// 你的邏輯
const result = "[transformed] " + text;
Console.log('{"success":true,"result":"' + result + '"}');
}
```
**專案依賴(package.json):**
```json
{
"dependencies": {
"as-wasi": "^0.4.7",
"assemblyscript-json": "^1.1.0"
}
}
```
### 編譯
```bash
cd registry/components/my_component
npm install
npx asc assembly/index.ts \
--target release \
--outFile my_component.wasm \
--exportRuntime \
--use abort=~lib/wasi_abort
```
### 本機測試
```bash
echo '{"text":"hello world"}' | wasmtime run my_component.wasm
# 預期:{"success":true,"result":"[transformed] hello world"}
```
---
## Rust 零件開發
Rust 零件支援已就緒,但文件尚在完善中。如果你熟悉 Rust + WASM,歡迎參考 [wasm-wasi 官方文件](https://doc.rust-lang.org/stable/reference/linkage.html),核心要求與其他語言相同:WASI preview1stdin/stdout JSON I/O。
基本設定:
```bash
rustup target add wasm32-wasip1
cargo build --target wasm32-wasip1 --release
```
---
## 提交至公眾 Registry
```bash
# 確保 .wasm 已編譯
ls my_component.wasm
# 提交(需要 arcrun.dev API Key
acr parts publish ./registry/components/my_component/
```
提交後流程:
| 狀態 | 說明 |
|------|------|
| `sandbox_pending` | 沙盒驗收執行中 |
| `author_only` | 驗收通過,你自己可用 |
| `public` | 人工審核通過,所有人可用,開始累積統計 |
查詢審核進度:
```bash
acr parts publish --status <submission_id>
```
---
## 常見問題
### `no_network_syscall` 設定錯誤
- **功能類**category: logic / data / ai):`no_network_syscall: true`。這類零件應完全沙盒化。
- **整合類**category: api):`no_network_syscall: false`,因為要呼叫外部 API。
兩者都需要宣告在 `constraints` 下,設錯會在 syscall 掃描步驟被沙盒拒絕。
### `gherkin_tests` 必須包含 happy path 和 error path
至少兩個測試場景:一個輸入正確的 happy path、一個缺少必填欄位或輸入非法的 error path。
### 體積超過上限
- TinyGo:確認使用 `-target wasi`(而非 `-target wasm`),前者體積更小
- AssemblyScript:加上 `--optimize``--target release`
- Rust:使用 `--release` 並加入 `opt-level = "z"``Cargo.toml`
---
## 問題回報
開 Issue[github.com/richblack/arcrun/issues](https://github.com/richblack/arcrun/issues)
+21
View File
@@ -0,0 +1,21 @@
# 6. User — 對外文檔
> 開發者、用戶、社區的文檔。
## 對外開發指南
| 檔案 | 用途 |
|------|------|
| **CONTRIBUTING.md** | 開發者貢獻指南(零件投稿、PR 流程) |
## 用戶需求 & 願景
| 檔案 | 內容 |
|------|------|
| **user_requirements/credential_parts.md** | credential 長期規格(需求源) |
| **user_requirements/wishlist.md** | 用戶願景清單 |
| **user_requirements/u6u_*.md** | u6u 平台進化規劃 |
---
更新時間:2026-06-08
+50
View File
@@ -0,0 +1,50 @@
# Arcrun 文件分類索引
> LLM Wiki 結構:長期記憶系統,防止決策遺忘和重複錯誤。
> 參考:[Andrej Karpathy — A Closer Look at How LLMs Remember](https://gist.github.com/karpathy/442a6bf555914893e9891c11519de94f)
---
## 分類規則
| 目錄 | 用途 | 例子 |
|------|------|------|
| **1-vision/** | 「為什麼」:設計哲學、核心價值、前瞻方向 | arcrun 優勢、Haiku 就能搞定、薄殼原則 |
| **2-architecture/** | 「是什麼」:系統設計、技術棧、組件關係 | MVP 架構、零件模型、KV 存儲方案 |
| **2-architecture/decisions/** | 架構關鍵決策和 trade-off | Service binding 為什麼廢、R2 只用 Phase 5、同 zone 1042 解法 |
| **3-specs/** | 需求和 SDD:設計文件、任務清單 | (從 .agents/specs/ 搬來) |
| **4-guides/** | 操作手冊:如何做、工作流程 | 部署流程、CLI 用法、壓測方法 |
| **5-records/** | 日誌和證據:事件、測試、決策履歷 | Incident、壓測報告、問題追蹤 |
| **5-records/incidents/** | 生產問題復盤:故障原因、改進方案 | 2026-05-13 同 zone 1042、2026-06-06 MCP route bug |
| **5-records/test-reports/** | 壓測結果、驗收報告 | 2026-06-07 公庫上線測試、2026-06-08 Haiku 乾淨重測 |
| **6-user/** | 對外文件:README、教程、FAQ | self-hosted 安裝、開發者指南 |
---
## .claude/wiki/ — LLM 本機記憶
| 檔案 | 用途 | 更新頻率 |
|------|------|---------|
| **INDEX.md** | wiki 目錄和導引 | 每次新增 |
| **mistakes.md** | CC(我)常犯的錯 + 避坑方法 | 每次踩坑後 |
| **status.md** | 當前進度、已知問題、下一步 | 每次 session 結束 |
| **decisions-summary.md** | 架構決策快速參考(不是全文,是摘要 + 連結) | 每次大決策後 |
| **modules/[name].md** | 關鍵模組說明:職責、入口、常見誤解 | 不定期補充 |
---
## 檔案遷移計畫
| 當前位置 | 新位置 | 優先級 |
|----------|--------|--------|
| 根目錄 .md(除 README/CLAUDE | → docs/ 分類 | 高 |
| .agents/specs/ | → docs/3-specs/ | 高 |
| .claude/rules/ | → docs/2-architecture/ + wiki | 中 |
| docs/incidents/ | → docs/5-records/incidents/ | 中 |
| 壓測報告 | → docs/5-records/test-reports/ | 中 |
---
## 下一步
所有檔案整理進此結構後,wiki 將作為 CC 和任何 LLM 的長期記憶入口。
+260
View File
@@ -0,0 +1,260 @@
---
name: wiki-cowork-scan
description: "掃描本機 Documents 下所有裝了 system-dev-template 的資料夾,自動整理 LLM Wiki。支援一般專案、Logseq vault、Obsidian vault 三種結構,偵測方式與 install.sh 一致。觸發時機:使用者說「整理 wiki」「幫我掃 wiki」「更新我的 wiki」「wiki 掃描」,或 Cowork cron 定期觸發。"
---
# Wiki Cowork Scan
## 核心原則
這個 skill 與 Claude Code 的 `/wiki-init` `/wiki-capture` 共用同一套規則:
| 層 | 規則 |
|------------|-------------------------------------------|
| raw source | 只讀,不動 |
| `system-dev/wiki/` | 唯一輸出地點,只增不覆 |
| `CLAUDE.md` | 不動 |
| `logseq/``.obsidian/``assets/` | 絕對不動 |
**CC 和 Cowork 輸出格式相同,任何一方整理過的內容,另一方看到就跳過或補充,不覆蓋。**
---
## 第一步:發現所有目標資料夾
掃描 `~/Documents`(遞迴深度 3 層),找出所有含 `system-dev/wiki/` 的資料夾。
```
~/Documents/
project-a/system-dev/wiki/ ← ✅ 目標
Logseq/system-dev/wiki/ ← ✅ 目標
其他資料夾/ ← ❌ 跳過
```
找到後列出清單,告訴使用者:「找到 N 個 wiki 資料夾,開始整理。」
---
## 第二步:對每個資料夾偵測 vault 類型
進入每個目標資料夾的**根目錄**`system-dev/wiki/` 的上兩層),依序判斷:
### 判斷順序(與 install.sh 一致)
```
if 根目錄有 logseq/ 資料夾
→ vault 類型:Logseq
→ raw sourcepages/、journals/
→ 忽略:logseq/、assets/
else if 根目錄有 .obsidian/ 資料夾
→ vault 類型:Obsidian
→ raw source:根目錄下所有 .md(排除 .obsidian/ 內的檔案)
else
→ vault 類型:一般專案
→ raw sourcedocs/ 下所有 .md
```
---
## 第三步:讀取現有 wiki 狀態
進入 `system-dev/wiki/`,讀取:
- `INDEX.md`:目前已有哪些 wiki 頁面(多角度視圖入口)
- `status.md`:上次整理時間、進度
- `principles.md`(如果有):本專案跨全局的設計原則——整理時必須服從
目的:**知道哪些已整理過,只處理新增或有變動的 raw source**,不重複整理。
---
## 第四步:整理規則
### 核心判準:push vs pullwiki 是給 AI 看的)
整理任何內容前,先判斷它該進 **push 檔****cardspull**——判準是「**CC 做事時會不會被動看見**」:
- **push 檔**`status.md` / `mistakes.md` / `principles.md`):CC session 開始就被 hook 注入。給「CC 不會主動查、但不看就出事」的東西。
- **pull**`cards/`):CC 想到要查才看見。一切知識內容(原文摘要、AI 筆記、決策、概念…)都寫成 cards。
| 內容 | 去哪 | 理由 |
|------|------|------|
| 當前進度、下一步 | `status.md`(push 全文) | 時態狀態,不看會重做 |
| 跨全局設計原則(一行一條,≤15) | `principles.md`(push 全文) | 會被遺忘的盲區,CC 設計時必服從 |
| 踩坑、被糾正的誤解 | `mistakes.md`(push 摘要+按需展開) | 防 CC 不自覺的盲區 |
| 決策、原文摘要、概念知識、其餘一切 | `cards/<bucket>/`(pull) | 知識內容;CC 面對時自然會查 |
> `decisions-summary.md` 已**降級為 cards + INDEX 決策視圖**(決策=知識內容)。既有的保留為相容,不刪。
> CC 與 Cowork **共用此判準**,產出一致:任一方寫進 push 檔或 cards,另一方看到就跳過或補充,不覆蓋。
### 讀 raw source
逐一讀取 raw source 的 `.md` 檔。跳過:
- 檔名以 `.` 開頭的隱藏檔
- `.wikiignore` 裡列出的 glob pattern(如果存在)
- 含有 `<!-- wiki:ignore -->` 標記的區段
### 整理邏輯
每個 raw source 檔案,判斷:
1. **INDEX.md 裡已有對應條目,且 raw source 未修改** → 跳過
2. **INDEX.md 裡已有條目,但 raw source 有新內容** → 更新對應 wiki 頁面,補充新資訊,不刪舊內容
3. **INDEX.md 裡沒有對應條目** → 新建 wiki 頁面
### Wiki 卡片格式(概念原子卡,存到 `cards/<bucket>/`
```markdown
---
tags: [知識管理, AI協作, 方法論]
gloss: 一句話定義這個概念是什麼(給下游語義 normalize 用,選填、deep tier 才產)
---
# 概念全名
← [[<bucket>/00-INDEX]]
**來源**`[raw source 相對路徑]`
**最後更新**YYYY-MM-DD
## 摘要
[一句話核心]
## 重點
- [自包含改寫的要點,不寫「詳見原文」]
## 實體
> 本卡內文的關鍵實體(也是 graph node)。名+描述一起供下游 embedding normalize。
> AI 生產、人不必讀;集中放、一實體一行、不縮排、不重複。
- **原子筆記**atomic note/卡片原子化)— 每張卡只承載一個不可再分論點的知識記錄單元。
- **傳統筆記**(大鍋炒筆記)— 把多主題混雜在同一篇、難精確引用的記錄方式。
## 關聯
### 內文知識關係(內文實體間;端點=上方 `## 實體` 的正規名,一字不差)
- 原子筆記 >> 對立於 >> 傳統筆記
- 傳統筆記 >> 犧牲 >> 精確引用
### 卡片關係(卡對卡)
- [[本卡]] >> 謂詞(動詞短語) >> [[他卡]]
```
### 架構:三層 + 標籤橫切(183 卡實證)
```
INDEX.md ← 頂層:標籤視圖(非資料夾列表)
TAXONOMY.md ← 標籤字典(受控擴充:先查重再登記)
cards/<bucket>/
├── 00-INDEX.md ← 桶子索引(固定名,容器:只連不重寫)
└── <概念全名>.md ← 概念原子卡
```
- **資料夾只是儲存桶,分類由 frontmatter `tags:` 承載**——不繼承原稿目錄,由 AI 重新組織。
- **桶子索引固定名 `00-INDEX.md`**`00-` 排序最前、一眼可辨,載入任何桶先讀它。
- **frontmatter `tags:` 而非行內 `#tag`**:內文常用 `#`(如 `#猜想`),行內標籤會讓 ingest 分不清「分類」與「內文範例」污染 graph;frontmatter 零歧義。標籤只能用 `TAXONOMY.md` 列出的;**禁止繞過字典在卡片直接冒新標籤**,但字典可受控擴充(遇新軸先查重、確認非同義詞,再登記進本 repo 的 TAXONOMY.md)。
- **麵包屑帶路徑**H1 次行 `← [[<bucket>/00-INDEX]]`。指 `00-INDEX` 因固定名跨桶撞名,**一律帶路徑**;卡片間連結用裸 `[[卡名]]`
### 使用 typed-edge 三元組(抓內文實體關係,不只卡對卡)
用**帶語義的三元組** `A >> 謂詞 >> B` 寫進 `## 關聯`。**重點是抓內文裡的實體關係**——卡對卡(`[[卡A]] >> 謂詞 >> [[卡B]]`)只是把既有雙鏈加個動詞、資訊量幾乎沒增加;知識圖譜的價值在內文概念間的關係(`原子筆記 >> 對立於 >> 傳統筆記`,這些 A/B 是內文概念、不是卡標題)。
格式 `A >> 謂詞 >> B`,規則:
1. **方向性**:必須讀成「A(謂詞)B」一句通順的話;A、B 順序=主→賓真實方向。
2. **謂詞用動詞 / 動詞短語**(反駁、奠基於、犧牲),天然帶方向。**禁名詞當謂詞**——`>> 存儲格式 >>``>> 操作體驗 >>` 讀不通,是錯的。
3. **謂詞自由書寫但別太天馬行空**:寫「參考/參照」皆可(下游 embed 自動聚類同義謂詞),別寫「瞄了一眼」這種抓不到同義的。
4. **內文三元組端點用裸文字**(非 `[[wikilink]]`),避免在 Logseq 產生大量紅色斷鏈;卡對卡那層才用 `[[]]`
5. **向後相容**:純 `[[A]]` 仍合法(無類型邊),盡量補謂詞。
> **★ 硬自檢(Haiku 量產必備護欄)★** —— 內文三元組的「端點 = `## 實體` 詞條」
> `A >> 謂詞 >> B` 的 A、B 必須與 `## 實體` 某個粗體正規名【一字不差】。**寫完後逐條自檢**:把 A、B 拿去 `## 實體` 找有沒有完全相同的正規名,沒有 → 這條錯了。
> 修法擇一:(a) 改用實體表已有的詞;(b) 端點確是重要實體 → 補進 `## 實體` 再指它。
> 禁止:端點帶括號註解、端點是整句補語、端點是形容詞短語。
> (實證:光寫規則 Haiku 會略過,端點對不齊 14 條;寫成自檢動作後 14→0。跑 1-2 張看不出,跑 12 張才暴露。)
`>>` 為分隔語法,全程一致即可。這是 Karpathy LLM Wiki「知識互連」的強化版——連結不只存在,還帶類型與方向。
### 萃 glossnode 一句說明,供下游語義 normalize)
每張卡=一個 entity / graph node。deep tier 改寫時,frontmatter 補一句 `gloss:`——這個 node 是什麼的一句定義。下游 KBDB 對「entity 名 + gloss」一起做 embedding 求相似度,自動歸一同義詞(比只對名字準、比手維護 alias 表自動)。
- **在知識生產的當下、由整理者(CC / Cowork)建**:gloss 跟三元組同階段萃,**不留給下游 ingest 臨時補**——下游只有單檔/跨庫視角,編不出貼合的 gloss。
- **選填、deep tier 才產**:淺萃不浪費。
- **gloss ≠ 摘要**`gloss` 是 frontmatter 給機器 normalize 的定義句(「X 是…」);`## 摘要` 是給人讀的核心句。
- **兩層 gloss**:① frontmatter `gloss:` 描述「卡標題」這個 node;② `## 實體` 區塊的每行描述句,描述「內文實體」這些 node。**內文實體也是 graph node、也需描述句**才能被下游 embedding normalize`黃仁勳` vs `Jensen Huang` 靠描述拉近向量)。
- **實體要描述、謂詞不用**:實體同義詞字面差遠需描述拉近;謂詞同義詞字面本就近,裸詞 embed 自動聚類。
- **對齊下游 envelope**frontmatter `gloss:``## 實體` 詞條對應 ingest envelope 的 `nodes[].gloss`
> **改寫時必守**:① 絕不寫入 raw source(只往 `cards/<bucket>/` 寫,事後驗 raw source 0 異動);② 檔名=卡片全名,冒號用全形「:」、斜線用全形「/」,全程一種字元避免斷鏈。
---
## 第五步:更新 INDEX.md 和 status.md
### INDEX.md 格式(頂層 = 標籤視圖)
頂層 INDEX 按 `TAXONOMY.md` 的軸聚類,指向各桶子索引(帶路徑),不是平鋪頁面列表:
```markdown
# Wiki Index
> 最後更新:YYYY-MM-DD HH:MM | 來源:cowork-scan | 總卡數:N
### 知識管理
- [[pkm/00-INDEX]] — PKM 知識管理(N 卡)
### AI 協作
- [[ai/00-INDEX]] — AI 協作(M 卡)
```
桶子索引 `cards/<bucket>/00-INDEX.md` 是容器(只連不重寫,H2/H3 分節列出該桶卡片)。
### status.md 更新
在現有內容**末尾追加**(不覆蓋):
```markdown
## YYYY-MM-DD HH:MMcowork-scan
- vault 類型:[Logseq / Obsidian / 一般專案]
- 掃描檔案:N 個
- 新增頁面:N 個
- 更新頁面:N 個
- 跳過:N 個(未變動)
```
---
## 第六步:回報結果
整理完所有資料夾後,輸出摘要:
```
✅ Wiki 整理完成
資料夾 1~/Documents/project-a
類型:一般專案
新增:3 頁,更新:1 頁,跳過:12 頁
資料夾 2~/Documents/Logseq
類型:Logseq vault
新增:5 頁,更新:2 頁,跳過:47 頁
總計:8 頁新增,3 頁更新
```
---
## 絕對禁止
- ❌ 修改任何 raw source 檔案
- ❌ 修改 `CLAUDE.md`
- ❌ 動 `logseq/``.obsidian/``assets/` 資料夾
- ❌ 刪除 `system-dev/wiki/` 裡已有的頁面(只增補,不刪除)
- ❌ 把機敏資訊(密碼、金鑰、個資)寫進 wiki(遇到跳過並記錄)
- ❌ 整理沒有 `system-dev/wiki/` 的資料夾(那不是這個 skill 的目標)