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:
@@ -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 functions(kv_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 / SDK(Python/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**。
|
||||
|
||||
### 步驟 4:check 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 shim,stdin/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,不監聽 socket(WASI preview1 沒 socket)
|
||||
|
||||
### Host Functions(零件呼叫外部能力的唯一管道)
|
||||
在 `u6u` namespace 下:
|
||||
|
||||
| Host Function | 用途 |
|
||||
|---|---|
|
||||
| `u6u.http_request` | 發 HTTP 請求 |
|
||||
| `u6u.kv_get` | 讀 Cloudflare KV(Worker 側依 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` 套件
|
||||
- JS:Web 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 signing,block
|
||||
|
||||
### 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 routing(Hono 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` zone,cypher `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]]` 新增的 pattern;Edit 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/)實作業務邏輯
|
||||
能力只實作一次,放在 API(cypher-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 shim(POST / → stdin → WASM → stdout → JSON)
|
||||
```
|
||||
|
||||
**src/index.ts 是通用模板**,所有零件都用同一份。這個 TS 只做 WASI runtime,不是業務邏輯。
|
||||
|
||||
---
|
||||
|
||||
## R2(WASM_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 7,2026-06-06)**:self-hosted 的 cypher 與 auth worker 同在 `{sub}.workers.dev` zone,cypher `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-executor(Worker)讀 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 #9(2026-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 要清除的程式碼),在清除前不封測。
|
||||
|
||||
---
|
||||
|
||||
## 目前 Phase:Credential Primitives TS → WASM
|
||||
|
||||
**SDD 位置**:`docs/3-specs/arcrun/credential-primitives-wasm/design.md` + `tasks.md`
|
||||
|
||||
### 已完成
|
||||
|
||||
- **Phase 0.1–0.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.6(host functions)和 0.7(WASM 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 跑 wasm,CF 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)只實作一次,放在 API(cypher-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 層拼湊 upsert(recipe/零件補 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. 呼叫 API(HTTP 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 呼叫的封裝 |
|
||||
| **自家** API(KBDB / 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「不可能繞過」)。
|
||||
@@ -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 WASM:auth_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 recipe(prompt recipe 犍涉 KBDB block 展開,
|
||||
是 BACKLOG 待決策項,不在此範圍)。
|
||||
|
||||
### recipe 與 primitive 的驗收標準不同(早期已定,2026-05 重新確認)
|
||||
|
||||
- **primitive:Gherkin 通過 = 驗收通過。** 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、處理資料、做認證)→ **必須 WASM(TinyGo),用 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 標準介面的 wasm(10–80KB)。這是三層 runtime 的物理前提(見 §4)。
|
||||
TinyGo 為官方首選(語法與 TS 差異夠大,AI 不易把純 TS 邏輯誤搬)。
|
||||
|
||||
---
|
||||
|
||||
## 3. 開源 / 商業邊界
|
||||
|
||||
| 開源核心(MIT) | 服務側(付費 / 需 API Key) |
|
||||
|---|---|
|
||||
| cypher-executor、四種 primitive WASM、CLI(acr)、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_*` WASM(static_key / service_account / oauth2 / mtls)是「注入步驟的後端」,
|
||||
不是用戶會在 workflow 擺的零件。用戶永遠不會直接呼叫 `auth_static_key`——
|
||||
是 auth-dispatcher 在背後呼叫它。
|
||||
→ 「零件白名單」「假零件判準」不適用於 `auth_*`,它們不是用戶零件。
|
||||
|
||||
**credential 系統現状是「新舊兩路並存」的半成品:**
|
||||
- 新路(對的方向):`auth-dispatcher` → HTTP 打 `auth_*` WASM,解密/JWT 全在 WASM 內。
|
||||
已支援 static_key / service_account / oauth2;mtls 尚未(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 binding(CF 機制,a/b 都要 deploy):cypher-binding **不 deploy**,
|
||||
改 workflow 零部署成本。可能慢一點,但「寫好、按下去就跑」(像 n8n)。
|
||||
|
||||
### 三層 runtime 目標
|
||||
零件與執行核心目標是能跑在三種環境:
|
||||
- Tier 1:Cloudflare Workers(現況)
|
||||
- Tier 2:企業自架 workerd(不信任 CF 雲)
|
||||
- Tier 3:極輕量 WASI runtime(wazero,無人機 / 邊緣設備)
|
||||
|
||||
**現在不寫任何 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 Code(CC),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 + CI(2026-05-30):**
|
||||
- 零件投稿是**稀有低頻**事件(primitive 極少、未來絕大部分是 recipe;建零件要過人類閘門)。
|
||||
- 稀有事件用 PR 治理最自然:**PR 必須有人 merge = 人類閘門**(AI 偽造不了 GitHub approve);
|
||||
把關(假零件偵測 / 純WASI / Gherkin / 覆蓋檢查)由 **CI(PR 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-26,issue #4)
|
||||
|
||||
§3.1 舊版「缺能力 → 去補 API」預設 **API 是你能改的**。對**第三方 API**(gsheets 一次倒全部、輸出前無法 filter;
|
||||
無 upsert API 的服務)不成立——若規則又禁用 workflow「倒出來自己篩」,**用戶被自己的規則卡死**。
|
||||
|
||||
**結論**:不廢 §3.1,**收窄**成「禁在薄殼介面層 TS 拼裝 + 禁亂建零件污染零件庫」,並開放「用資料方式自救」的合法路徑。
|
||||
|
||||
**主界線(一句話)**:**「那個 API 你能不能改?」**
|
||||
| 情況 | 正解 |
|
||||
|---|---|
|
||||
| 能打既有 API | recipe |
|
||||
| **自家** API(KBDB/cypher)缺能力 | 補進 API + 可發 issue |
|
||||
| **第三方** API 缺能力 | **可投稿的 workflow 補丁** + 發 issue 建議原廠加 API |
|
||||
| 純計算(轉大寫等) | **code-node**(空白 code 零件寫 JS) |
|
||||
| 真需新穩定能力(極少) | 自建零件 → PR |
|
||||
|
||||
**配套**:補丁 workflow 可呼叫且明示可投稿(wishlist C6)/code-node 在 CF Workers isolate 跑 JS(wishlist 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-26,issue #7 / mira-dissolve T2.4)
|
||||
|
||||
KBDB 查詢三模式:關鍵字(base LIKE,always)/語義(embed 模組)/圖(graph 插件,另 repo)。**語義歸 base 的 optional 模組**。
|
||||
|
||||
**設計鐵則**:
|
||||
- **binding 開/關,不拆 repo**:有 `VECTORIZE`+`AI` binding 才啟用(`embedEnabled()`);沒有 → 降級 LIKE,**API 不變**。
|
||||
- **不裝保持輕**(free-tier 友善):預設關;`acr init` 問、預設 N。
|
||||
- **精耕非地毯式**:只 embed 被標 `metadata_json.embed:true` 的 entry(wiki 段落 + 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-26,issue #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] | [原因] |
|
||||
|
||||
## 影響與後續
|
||||
|
||||
[這個決定影響哪些地方?有什麼技術債或需要注意的事?]
|
||||
@@ -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/** | 零件投稿 gatekeeping(PR + 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.wasm(stdin = 含 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_required(gmail 範例)
|
||||
|
||||
```yaml
|
||||
credentials_required:
|
||||
- key: gmail_token
|
||||
type: google_oauth
|
||||
description: "Google OAuth access token(gmail.send scope)"
|
||||
inject_as: access_token
|
||||
```
|
||||
|
||||
### config_example(gmail 範例)
|
||||
|
||||
```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 模式架構(用戶自己的 KV,arcrun.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-worker(api.arcrun.dev) │
|
||||
│ POST /register → { api_key, tenant_id } │
|
||||
│ ACCOUNTS_KV: { tenant_id, cf_api_token, api_key_hash } │
|
||||
│ ※ 不儲存用戶 credential 或 workflow 內容 │
|
||||
│ │
|
||||
│ cypher-executor(cypher.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 registry(registry.arcrun.dev) │
|
||||
│ GET /components → 零件清單 + 統計 + author + visibility │
|
||||
│ POST /submit → 接收零件,沙盒驗收後設 author_only │
|
||||
│ POST /analytics/record → 執行統計(非同步) │
|
||||
└──────────────────────────────────────────────────────────────┘
|
||||
|
||||
↕ CF KV API(用戶的 cf_api_token,KV 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 KV,arcrun 不會儲存它們。
|
||||
```
|
||||
|
||||
### 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_rate(DESC)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 零件貢獻流程設計
|
||||
|
||||
### `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_required(4 個零件)
|
||||
2.3 補充 config_example(全部 21 個)
|
||||
2.4 驗證 main.go required 與 contract 一致
|
||||
|
||||
Phase 3:credential 注入(Requirement 3)
|
||||
3.1 新增 credential-injector.ts
|
||||
3.2 整合進 graph-executor 節點執行前
|
||||
3.3 測試 gmail 零件端對端(credentials.yaml → push → run)
|
||||
|
||||
Phase 4:CLI(Requirement 4)
|
||||
4.1 acr init(--hosted / --self-hosted 分支)
|
||||
4.2 acr creds push(Hosted 走 API,Self-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 6:Hosted SaaS(Requirement 6)
|
||||
6.1 建立 auth-worker(api.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 2:component.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 3:workflow 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 4:CLI(arcrun,指令 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 5:README 與開源發布準備
|
||||
|
||||
**User Story:** As a 外部開發者, I want 看到清楚的 README,5 分鐘內能完成部署, 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 6:Standard 模式 — 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 1–3 工作在 `matrix` repo 對應目錄驗證後再搬到新 repo。
|
||||
|
||||
**PR #2(claude/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 Bindings(KBDB、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 3:Credential 注入整合
|
||||
|
||||
- [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 4:CLI 開發
|
||||
|
||||
- [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 加密 fallback(base64)已移除,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 6:Standard 模式 — 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 mock,Phase 7 補充
|
||||
- cold-start 測量(sandbox 步驟 b)為 Phase 0 mock,Phase 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 執行一個 .wasm(stdin/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 2:Cypher 語意擴展 + 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` 二進位存 R2,KBDB 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` | 相容 runtime(JSON 陣列) | `["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 Schema(JSON 字串) | `{"type":"object",...}` |
|
||||
| `output_schema` | JSON Schema(JSON 字串) | `{"type":"object",...}` |
|
||||
| `gherkin_tests` | 測試案例(JSON 字串) | `[{"scenario":"..."}]` |
|
||||
| `wasm_r2_key` | R2 物件鍵(wasm 模式) | `components/validate_json/v1.wasm` |
|
||||
| `service_binding_key` | CF binding key(service_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 回傳 ENOSYS(76)
|
||||
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 Block(KBDB 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 Block(KBDB 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 input,WASM 執行路徑的輸出 SHALL 與 HTTP 執行路徑的輸出語意等效。
|
||||
|
||||
**Validates: Requirements 3.1, 3.5, 3.6**
|
||||
|
||||
---
|
||||
|
||||
### Property 5: Dispatcher 錯誤結構完整性
|
||||
|
||||
*For any* (component_id, tier) 組合,若該零件的 `runtime_compat` 不包含當前 tier,Component_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": "發現禁止的 syscall:sock_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 0(WASM 執行核心):**
|
||||
- Property 4:WASM 執行路徑 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 2(Cypher 擴展):**
|
||||
- Property 9:Confluence(三元組順序無關性,fast-check shuffle)
|
||||
- Property 6:URI 解析 round-trip
|
||||
- Property 7:版本選擇策略(floating 最高分、pinned 固定版本)
|
||||
- Property 5:錯誤結構完整性
|
||||
|
||||
**Phase 3(前端畫布):**
|
||||
- Property 10:Web 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 1(Cloudflare Workers)、Tier 2(workerd 地端叢集)、Tier 3(Go + 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 Networking,Tier 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_Registry,THE 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_Registry,THE 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 1(Cloudflare Workers)環境中,以 `workerd` 內建的 WASM 執行能力執行 WASI preview1 零件。
|
||||
4. WHEN 一個零件的 `runtime_compat` 不包含當前執行環境的 Tier,THE 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 5:Cypher 語意關係擴展
|
||||
|
||||
**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 6:Component Dispatcher 多 Tier 路由
|
||||
|
||||
**User Story:** As a 平台架構師, I want Cypher Executor 透過統一的 Component Dispatcher 介面呼叫跨 Tier 零件, so that Workflow 設計者不需要知道零件部署在哪個 Tier。
|
||||
|
||||
#### Acceptance Criteria
|
||||
|
||||
1. THE Component_Dispatcher SHALL 根據以下優先序決定呼叫路徑:(1)Tier 1:Cloudflare Service Binding(若零件部署為 Worker)或 WASM 直接執行;(2)Tier 2:workerd 叢集 HTTP endpoint;(3)Tier 3:Wazero IPC(stdin/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 7:Tier 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_2,THE 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 Components,THE 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、打包 runtime(QuickJS、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 12:KBDB 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 keys(canonical_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` 至 R2,slot `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 一律回傳 ENOSYS(76)
|
||||
- 不引入任何外部依賴(不使用 `@cloudflare/workers-wasi`)
|
||||
- _Requirements: 3.1, 3.3_
|
||||
|
||||
- [x]* 2.2 寫單元測試 for WASI shim
|
||||
- 測試 `fd_read` 正確讀取 stdin buffer(含多次讀取、邊界條件)
|
||||
- 測試 `fd_write` 正確寫入 stdout buffer(fd=1)與 stderr buffer(fd=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_tests(happy path + error path)
|
||||
- `runtime_compat: ["cf-workers","workerd","wazero"]`
|
||||
- _Requirements: 1.1, 1.2, 1.4_
|
||||
|
||||
- [ ]* 3.3 寫單元測試 for validate_json(Gherkin 場景驗證)
|
||||
- 測試合法 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 2:Cypher 語意擴展 + 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` attribute(JSON 字串)、基本折線圖渲染
|
||||
- _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 Components(Dogfooding)
|
||||
- 畫布 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
|
||||
- 存入本地 SQLite(AES-GCM 加密,key 存於設備安全儲存)
|
||||
- 離線時從本地快取讀取,過期時加入 DTN 佇列等待更新
|
||||
- _Requirements: 7.1, 7.3_
|
||||
|
||||
- [ ] 18. Go Cypher Executor(Tier 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 1(wasi-shim.ts)和 Tier 3(Wazero)執行結果一致
|
||||
|
||||
---
|
||||
|
||||
## Notes
|
||||
|
||||
- 標記 `*` 的子任務為選填,可跳過以加速 MVP 交付
|
||||
- 每個任務都引用具體的 Requirements 條款以確保可追溯性
|
||||
- Checkpoint 任務確保每個 Phase 完成後有明確的驗收點
|
||||
- Property tests 使用 fast-check,每個屬性最少執行 100 次迭代
|
||||
- 所有跨服務呼叫只透過 KBDB HTTP API,不直接操作 D1 SQL
|
||||
- TinyGo 零件只使用白名單 import(`os`、`io`、`encoding/json`)
|
||||
|
||||
## Phase 5:u6u-mcp 對齊新 Registry + u6u-gui 前端
|
||||
|
||||
> 壓測前必須完成,讓 AI(u6u-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)
|
||||
@@ -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 token(gmail / google_sheets 等)
|
||||
5. `acr push` 部署 workflow,取得 Webhook URL
|
||||
6. 網頁 POST /webhooks/named/{name}/trigger,結果存 Google Sheets
|
||||
|
||||
---
|
||||
|
||||
## 二、場景各步驟驗證狀態
|
||||
|
||||
### Step 1:acr init → api_key
|
||||
- [x] `acr init` Standard 模式完成,api_key 存入 `~/.arcrun/config.yaml`
|
||||
- [x] 已驗證:`mode: standard, api_key: ak_...` 正確
|
||||
|
||||
### Step 2:acr 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 3:acr 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 4:acr 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 5:acr 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_key,acr init 自動存入 config(CLI 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 #9(2026-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 URL(kbdb-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 #10(2026-05-13 已解決):multi-node chain context propagation 漏失
|
||||
|
||||
**現象**:cypher binding workflow 從第 2 個節點開始,原始 input context(top-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 個 triplet,parent_id 正確接到對應 paragraph。
|
||||
|
||||
#### Edge type 一致化
|
||||
|
||||
抽 `propagateCtx(context, result, upstreamNodeId)` helper,5 個 edge type(PIPE / 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 #3:cypher-executor `scheduled()` handler(2026-05-14 完成)
|
||||
|
||||
**原痛點**:cron 零件只做 expression validation;cypher-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 #1:workflow 缺 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 #11(2026-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 零件收到字串 `"[{...},{...}]"` 不是 array,items 被忽略 → 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 #2:workers_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 cert,free 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 → 522(mira 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 cap(10ms / invocation)→ 對照測試「5 節點 SB chain」跑了 2.2 秒卻通過,**推翻**
|
||||
2. 換懷疑 CF zone 規則 / Bot Fight Mode → dashboard bindings 乾淨無攔截,**推翻**
|
||||
3. 用戶補繳費恢復 Workers Paid → 重測仍全 522,**徹底排除付費假設**
|
||||
4. 看 dashboard Version History:5/8 4 次 + 5/9 5 次 = **9 次 manual `wrangler deploy by uncle6.me`**
|
||||
5. 對照 GitHub Actions:4/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 沒 catch,4/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 的 graph,cypher-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-f(wiki 合成 workflow)卡這裡。
|
||||
|
||||
---
|
||||
|
||||
## 四、封測前 P3(啟動當天)
|
||||
|
||||
- [ ] 用封測者 email 呼叫 `/register`,取得 api_key
|
||||
- [ ] 將 ARCRUN_ENCRYPTION_KEY 以安全方式提供給封測者
|
||||
- [ ] 確認聯絡管道
|
||||
|
||||
---
|
||||
|
||||
## 五、已知限制(封測期間不修)
|
||||
|
||||
1. `if_control` false branch 不路由(條件 false 時後續節點不執行)→ 升級計畫見 P1 #1,2026-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 KV(CRUD)、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_key;acr init 自動儲存;creds push 不需手動設環境變數 |
|
||||
| 1.0.8 | acr push → webhooks/named;config 套入 graph;acr 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
|
||||
|
||||
> 文件類型:SDD(Software 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 的 secret(2026-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_account(Google 家族)
|
||||
|
||||
```
|
||||
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 共用一把 auth,2026-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 KV(2026-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 publish(arcrun@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-06,richblack)
|
||||
|
||||
每個 `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_url(23/23 實測通過)。
|
||||
- API recipe(`POST /recipes`)不帶 secrets(secrets 在 auth recipe),故 help_url 要求在 auth recipe 這層。
|
||||
|
||||
**誠實限制**:help_url 只保證「有指向官方文件」,不保證連結永遠有效(provider 可能改網址)。
|
||||
這是「降低 AI 搜錯」的保險,非絕對正確;連結失效時 AI 仍可 fallback 去搜(但有起點)。
|
||||
+184
@@ -0,0 +1,184 @@
|
||||
# Credential Store 遷移 SDD — KV → D1(目錄)+ Cloudflare Secrets Store(密文)
|
||||
|
||||
> 建立:2026-06-29 by arcrun CC|對應 issue:Arcrun#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 的 A(telegram 一致性,已做)/ C(友善前門)/ Q1(acr parts)不受影響。
|
||||
> 詞彙:component=TinyGo WASM|recipe=http_request+固定設定|workflow=多步 YAML|auth-recipe=credential 怎麼注入。
|
||||
|
||||
---
|
||||
|
||||
## 0. leo 的最終決策(D19,一句話)
|
||||
|
||||
**「擁有目錄,不擁有內容物」**:arcrun 持有 credential 的**目錄**(看得到存了哪些、最後何時用),但**不持有、也讀不回密文明文**——連 owner 自己的界面都讀不到值。
|
||||
|
||||
動機(leo 親述):用戶 API key 外泄被盜刷的經濟損失 + 商譽風險(「就算不是我們過失也會怪在我們身上」)。**我們不持有明文、不持有金鑰 = 不背這個鍋。**
|
||||
|
||||
```
|
||||
密文值(token / SA JSON / private key) → Cloudflare Secrets Store(CF 託管金鑰,arcrun 拿不到明文)
|
||||
credential 目錄(name/service/metadata) → 使用者自己的 D1(不含密文,只含指向 Secrets Store 的 secret_ref)
|
||||
舊自管 ENCRYPTION_KEY + 密文存 KV → 廢掉(回填完成後);KV 降為暫存/熱讀快取,非真相源
|
||||
界面 acr creds list → 讀 D1(顯示清單 + last_used),看得到但讀不回值
|
||||
```
|
||||
|
||||
### n8n credentials UX(D19 定案)
|
||||
- 界面顯示**很多 credentials**(名字 / 服務 / metadata / last_used)。
|
||||
- **不能 read 既有值、不能 edit 既有值**(連 owner 都不行)。只能:
|
||||
- **整筆 replace**(重貼新值覆蓋舊的)
|
||||
- **delete**
|
||||
- read 不回值「不是限制、是設計」——值在 Secrets Store,arcrun 拿不到。
|
||||
|
||||
---
|
||||
|
||||
## 1. 現狀(已核實 code,2026-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 D1(D1 只有 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.sql(kbdb 同一顆 D1,IF 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, -- 對應 service(telegram / 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 binding(wrangler.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 預設走路徑 1(REST by ref)**,並標「待 leo 確認 self-hosted leo21c CF 帳號的 Secrets Store API 路徑與 token 範圍」。施工前需一次 spike 驗證 leo21c 帳號可建 store + worker 可 by-ref 取值。
|
||||
|
||||
**self-hosted(leo21c)**: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 Store(CF 端加密託管)。**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 取密文值(路徑 1:REST 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 row:WASM 解密取明文 → 寫進 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 binding(rule 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 依附本 SDD(store 是 Secrets Store 不是 D1)。
|
||||
|
||||
---
|
||||
|
||||
## 7. 任務分解(待放行後施工;本批僅 SDD)
|
||||
- [ ] T1 spike:leo21c 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.4,leo 明示放行)。
|
||||
|
||||
> **每個 cred 操作跨 TS / WASM / host-function / 兩個 store,必端到端實測**(防再假綠,mindset §7)。T1 不通則整案停,先解 Q-a。
|
||||
+211
@@ -0,0 +1,211 @@
|
||||
# Credential Store 重設計提案(A telegram 一致性 + B KV→D1 + C 友善前門)
|
||||
|
||||
> 建立:2026-06-29 by arcrun CC|更新:2026-06-29(依 leo 最終精確 spec 改寫)|對應 issue:Arcrun#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 / gsheets(http_request + 固定設定)是打 API 的端點,**不刪**。改的是「credential 怎麼餵」,不是 recipe。
|
||||
|
||||
---
|
||||
|
||||
## 0.5 Q1 — list 是動態讀 store 還是 hardcoded?(已核實 code,2026-06-29)
|
||||
|
||||
**結論:recipe/auth-recipe 的「正規清單」是動態的;但 `acr parts` 是 hardcoded 且把 5 個 recipe 混進去 → 那 5 個之外的 pushed recipe 在 `acr parts` 看不到。**
|
||||
|
||||
| 指令 | 讀哪裡 | 動態? | pushed recipe 會出現? |
|
||||
|---|---|---|---|
|
||||
| `acr recipe push` | `POST /recipes` → KV store(recipe.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(零件=WASM)hardcoded 是對的**:零件只能 PR merge 新增(mindset §4 人類閘門),固定、慢增,靜態清單反映真實。
|
||||
- **但 `acr parts` 把 5 個 recipe 也 hardcode 進去**(parts.ts:197-261:gmail_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…=WASM,PR-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 + 實測 live,2026-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」長久存 → **此遷移從未完成**。
|
||||
→ B(KV→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` 補 telegram(inject.path bot_token)/ line_notify(header Bearer)/ kbdb(header Bearer),形態取自 prod。
|
||||
- [x] `recipes.ts` `AuthInjectSpec` 補 `path?`(WASM/SDD §六早有、source 介面漏 → tsc 擋)。
|
||||
- [ ] google_user(oauth2)暫不回灌:內嵌 client_secret 不可進 git + 介面無 oauth2 欄 → 留 Phase D。
|
||||
- 部署後 leo21c 跑 `acr update`(/init/seed) → 23→26,telegram 即與 notion 同鏈可用。
|
||||
- **不刪任何 recipe;不碰 `acr creds push`。** 純補種子 + 補型別欄位。
|
||||
|
||||
### Phase B —(已翻案,見 §4.5)儲存:**KV 維持,不搬 D1**;高敏感可選補 Secrets Store
|
||||
> 早先版本寫「KV→D1 遷移」。§4.5 核實史實 + leo 自己 2026-06-07 拍板(credential 留 KV)後**翻案**:
|
||||
> credential=Redis-class(按 key 直接點查、密文不可 SQL 查),搬 D1 是淨負債。**B 改為:**
|
||||
1. **低敏感 credential 維持 KV**(現狀 CREDENTIALS_KV,AES-GCM)。`acr creds list` 已可用(`GET /credentials` KV.list)。
|
||||
2. **(可選、後續)高敏感(service account JSON/private key)→ Cloudflare Secrets Store**(credential_parts.md §8.2,KV 只存 ref)。唯一值得的儲存升級,與拍板不衝突。
|
||||
3. **不新增 D1 credentials 表、不新增 `cred_get` host function、不做 KV→D1 migration**——這些原計畫取消(好處 KV 已有)。
|
||||
4. 若 leo 仍要 D1:屬新需求(須更新 2026-06-07 拍板)。即便如此,密文本體仍留 KV/Secrets Store,D1 只放非機密 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 端加密 → 存進 D1(Phase 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}`。
|
||||
|
||||
這條鏈與 notion(header 注入)/gsheets(service_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` 只有 KBDB(entries/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_KV,MVP 至今未變) |
|
||||
| **kbdb-base SDD 拍板 2026-06-07(leo 親簽「Leo:同意」)** | **credential 留 KV**(「session、verdict、credential 留 KV=短期高頻純取用,類比 Redis;workflow/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 Store,KV 只存 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-off,mindset §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.2,KV 只存 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` 輸入源,非拆除。
|
||||
- 不新建 component(mindset §1 / rule 02)。
|
||||
- credential 解密仍在 WASM(rule 02 §2.2);KV→D1 只是密文來源從 `kv_get` 換成新 host function `cred_get`(屬 wasi-shim,rule 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 primitive(crypto.subtle / KV / fetch)。
|
||||
|
||||
---
|
||||
|
||||
## 現有 TS 實作(要刪除的)
|
||||
|
||||
| 檔案 | 功能 | 對應 WASM Primitive |
|
||||
|------|------|---------------------|
|
||||
| `credential-injector.ts` — `injectFromAuthRecipe()` | static_key template 展開 | `auth_static_key` |
|
||||
| `credential-injector.ts` — service_account 分支 | JWT signing + token exchange | `auth_service_account` |
|
||||
| `credential-injector.ts` — `decryptCredential()` | AES-GCM 解密 | host function(所有 primitive 共用) |
|
||||
| `credential-injector.ts` — `interpolateTemplate()` | `{{secret.KEY}}` 替換 | 內建在各 primitive |
|
||||
| `jwt-signer.ts` — `exchangeGoogleJwt()` | PEM→PKCS8→RS256→token | `auth_service_account` |
|
||||
| `component-loader.ts` — BUILTIN_API_RECIPES | gmail/telegram/line/gsheets 寫死邏輯 | 刪除,改用 auth recipe + `http_request` 零件 |
|
||||
| `credential-injector.ts` — BUILTIN_CREDENTIALS_MAP | 舊路徑 flat injection | 刪除,統一走 auth recipe |
|
||||
| `arcrun/credentials/` | 重複的 credentials Worker | 刪除,路由已在 cypher-executor |
|
||||
|
||||
---
|
||||
|
||||
## 4 個 WASM Primitive 設計
|
||||
|
||||
### 統一 I/O 介面(stdin/stdout JSON)
|
||||
|
||||
```
|
||||
stdin(Worker → WASM):
|
||||
{
|
||||
"action": "authenticate" | "needs_refresh" | "refresh" | "test",
|
||||
"api_key": "ak_xxx", // 租戶識別,用來組 KV key
|
||||
"service": "openai", // 對應 auth_recipe:{service}
|
||||
"request": { "method": "GET", "url": "/path", "headers": {}, "body": null }
|
||||
}
|
||||
|
||||
WASM 內部流程:
|
||||
1. recipeJSON = kv_get("auth_recipe:" + service)
|
||||
2. 依 recipe.required_secrets 逐一 kv_get("{api_key}:cred:{name}") → {encrypted, iv}
|
||||
3. secrets[name] = crypto_decrypt(encrypted, iv)
|
||||
4. (service_account)crypto_sign_rs256(jwt, pkcs8) + http_request 換 token
|
||||
5. 展開 recipe.inject 的 {{secret.X}} / {{runtime.X}} 模板
|
||||
|
||||
stdout(WASM → Worker):
|
||||
{
|
||||
"success": true,
|
||||
"auth_headers": { "Authorization": "Bearer xxx" },
|
||||
"auth_query": {},
|
||||
"auth_body": {},
|
||||
"runtime": { ... updated runtime state,供下次 refresh 用 }
|
||||
}
|
||||
```
|
||||
|
||||
### auth_static_key
|
||||
|
||||
**位置**:`arcrun/registry/components/auth_static_key/`
|
||||
**語言**:TinyGo 或 AssemblyScript
|
||||
|
||||
功能:
|
||||
1. 讀取 `recipe.inject.header/query/body` 模板
|
||||
2. 用 `secrets` 展開 `{{secret.KEY}}` 模板
|
||||
3. 回傳 `auth_headers` / `auth_query` / `auth_body`
|
||||
|
||||
涵蓋:~80% 服務(Bearer token, API Key, Basic Auth, custom header)
|
||||
|
||||
### auth_service_account
|
||||
|
||||
**位置**:`arcrun/registry/components/auth_service_account/`
|
||||
**語言**:TinyGo 或 AssemblyScript
|
||||
|
||||
功能:
|
||||
1. 從 `secrets.service_account_json` 解析 private key
|
||||
2. JWT signing(RS256:PEM→PKCS8→sign)
|
||||
3. POST token exchange endpoint → 取得 access_token
|
||||
4. 展開 `{{runtime.access_token}}` 模板
|
||||
|
||||
**crypto 考量**:
|
||||
- TinyGo 的 `crypto/rsa` + `crypto/x509` 支援有限
|
||||
- 若 TinyGo 不支援 RS256:使用 host function 讓 Worker 的 `crypto.subtle` 代簽
|
||||
- 或改用 AssemblyScript(有 as-crypto 套件)
|
||||
|
||||
### auth_oauth2(新建)
|
||||
|
||||
**位置**:`arcrun/registry/components/auth_oauth2/`
|
||||
|
||||
功能:
|
||||
1. `needs_refresh`:檢查 `runtime.expires_at` 是否過期
|
||||
2. `refresh`:用 `runtime.refresh_token` + `secrets.client_secret` 換新 token
|
||||
3. `authenticate`:展開 `{{runtime.access_token}}` 到 headers
|
||||
|
||||
### auth_mtls(新建)
|
||||
|
||||
**位置**:`arcrun/registry/components/auth_mtls/`
|
||||
|
||||
功能:
|
||||
1. 從 `secrets` 讀取 client cert + key
|
||||
2. 回傳 TLS 設定(由 Worker runtime 執行實際 mTLS handshake)
|
||||
|
||||
---
|
||||
|
||||
## cypher-executor 改動
|
||||
|
||||
### 保留(TS routing 層)
|
||||
|
||||
- `routes/credentials.ts` — HTTP CRUD for credentials(接收加密的 payload)
|
||||
- `routes/recipes.ts` — HTTP CRUD for auth recipes
|
||||
- `routes/auth.ts` — OAuth flow routing
|
||||
- `graph-executor.ts` — workflow 執行排程
|
||||
- `lib/wasi-shim.ts` — WASM runtime + host functions(加解密 / KV / 簽章 / HTTP 實際由 `crypto.subtle` / env binding / fetch 執行,但**呼叫時機由 WASM 決定**)
|
||||
|
||||
### 修改
|
||||
|
||||
- `actions/credential-injector.ts` — **整檔刪除**,改為新檔 `actions/auth-dispatcher.ts`(約 30 行):
|
||||
1. 查 `resolveAuthRecipe(componentId)` 取得 `primitive` 名稱(static_key / service_account / oauth2 / mtls)
|
||||
2. 呼叫對應的 auth primitive 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 7,2026-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 worker,CF 回 **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 binding(A)後 richblack 拍板廢、改 flag(B)。
|
||||
- **flag 安全(查證官方 docs)**:唯一副作用「Worker fetch 自己 hostname → self-loop」;cypher 只打外部 API + sibling auth worker(皆非自己 hostname)→ 不 self-loop。
|
||||
- **官方/self-host 共用同一份 toml**:官方 cypher 本就跨 zone(cypher.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 Functions(WASM ↔ Worker 的橋接)
|
||||
|
||||
auth primitive WASM 需要呼叫外部能力時,透過 host function。全部放 `u6u` namespace。**錯誤回傳非零 uint32;成功 = 0 且把結果寫入 `outPtr` 指向的 buffer**。
|
||||
|
||||
| Host Function | TinyGo 簽章 | 用途 |
|
||||
|---|---|---|
|
||||
| `http_request` | `(urlPtr/Len, methodPtr/Len, headersPtr/Len, bodyPtr/Len, outPtr, outLenPtr) uint32` | HTTP 請求(已實作) |
|
||||
| `kv_get` | `(keyPtr, keyLen, outPtr, outLenPtr) uint32` | 讀 KV。Worker 依 key 前綴路由到 `CREDENTIALS_KV` / `RECIPES` |
|
||||
| `crypto_decrypt` | `(encPtr, encLen, ivPtr, ivLen, outPtr, outLenPtr) uint32` | AES-GCM 解密。encryption key 由 Worker 從 `env.ENCRYPTION_KEY` 內部讀取,**永遠不暴露給 WASM** |
|
||||
| `crypto_sign_rs256` | `(dataPtr, dataLen, pkcs8Ptr, pkcs8Len, outPtr, outLenPtr) uint32` | Worker 用 `crypto.subtle.sign('RSASSA-PKCS1-v1_5' + SHA-256)`;private key 以 PKCS8 bytes 傳入 |
|
||||
|
||||
這些 host function 在 `lib/wasi-shim.ts` 中以 WASI import 提供。
|
||||
|
||||
### 安全邊界
|
||||
|
||||
- `ENCRYPTION_KEY` 只在 `crypto_decrypt` host function 內部使用,**絕不**經 stdin / 回傳值 / 任何路徑傳給 WASM
|
||||
- `api_key` 經 stdin 傳入 WASM(讓 WASM 自己組 `{api_key}:cred:{name}` KV key)
|
||||
- `kv_get` 在 Worker 側檢查 key 前綴:
|
||||
- `auth_recipe:*` → 讀 `RECIPES`
|
||||
- `{api_key}:cred:*` → 讀 `CREDENTIALS_KV`,且 `{api_key}` 必須等於 stdin 傳入的 api_key(防越權)
|
||||
- 其他前綴 → 回傳錯誤
|
||||
|
||||
---
|
||||
|
||||
## 關於解密位置
|
||||
|
||||
採用**方案 B(唯一方案)**:WASM 透過 host function `crypto_decrypt()` 自行解密。
|
||||
|
||||
- cypher-executor TS 完全不解密、不知道 plaintext
|
||||
- `ENCRYPTION_KEY` 永遠留在 Worker host function 內
|
||||
- WASM 知道要解哪份 ciphertext(經 `kv_get` 讀到的 `{encrypted, iv}`),但拿不到 encryption key
|
||||
- 這樣 TS 層完全沒有零件業務邏輯,符合 CLAUDE.md §禁止行為 1/6
|
||||
|
||||
(歷史註記:曾規劃方案 A「TS 先解密再送 stdin」,已廢棄 — 違反「TS 不得實作零件邏輯」。)
|
||||
|
||||
---
|
||||
|
||||
## §8 `{{credential.X}}` 用戶面注入語法(2026-06-10,richblack 確認 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/stdout(resolve_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_id,6 個 API 零件 + 4 個 auth primitive)+ `wasmWorkerUrl()` URL 慣例輔助函數;解析鏈新增為第 8 層(放在 `BUILTIN_API_RECIPES` fallback 之後,避免 Phase 3 尚未完成時 API 零件 Worker 未部署造成 404;Phase 3 刪除 `BUILTIN_API_RECIPES` 後,API 零件會自然落到此層)。auth primitive 從此層進入。`tsc --noEmit` 通過。
|
||||
|
||||
---
|
||||
|
||||
## Phase 1:auth_static_key WASM(優先,涵蓋 80% 服務)
|
||||
|
||||
方案 B:WASM 自行讀 KV + 解密,TS 不碰 plaintext。
|
||||
|
||||
- [x] 1.1 建立 `arcrun/registry/components/auth_static_key/` 目錄
|
||||
- [x] 1.2 寫 `component.contract.yaml`(input: `{action, api_key, service, request}` → output: `{success, auth_headers, auth_query, auth_body, runtime}`)
|
||||
- [x] 1.3 實作 `main.go`(TinyGo):
|
||||
- 宣告 host imports:`kv_get` / `crypto_decrypt`(static_key 不需要 http_request)
|
||||
- 從 stdin 讀 `{action, api_key, service}`
|
||||
- `kv_get("auth_recipe:" + service)` → recipe JSON → 驗證 `primitive == "static_key"`
|
||||
- 對每個 non-optional `recipe.required_secrets`:`kv_get("{api_key}:cred:{name}")` → `{encrypted, iv}` → `crypto_decrypt` → plaintext
|
||||
- 展開 `{{secret.X}}` / `{{runtime.X}}` 模板於 `inject.header/query/body`;未知 key 展空字串(與 TS parity);其他 namespace 的 `{{...}}` 原樣保留
|
||||
- 輸出 stdout JSON `{success, auth_headers, auth_query, auth_body, runtime}`
|
||||
- [x] 1.4 `tinygo build -o auth_static_key.wasm -target=wasi main.go` — 2026-04-19 編譯通過(1.1MB,在 contract 限制 2MB 內)
|
||||
- [🔄] 1.5 建立 `.component-builds/auth_static_key/`(用 `component-worker-template`)並部署到 `auth-static-key.arcrun.dev`
|
||||
- 2026-04-20 完成**建置**部分:`.component-builds/auth_static_key/{wrangler.toml, package.json, tsconfig.json, src/index.ts, component.wasm}` 全數到位
|
||||
- 方案 A:`src/index.ts` 直接 import `../../../cypher-executor/src/lib/wasi-shim` 的 `createWasiShim` + `createArcrunHostFunctions`(以 `ArcrunHostEnv` 結構型別相容);AES 解密邏輯仍只存在於 wasi-shim.ts 一處(rule 02 §2.2)
|
||||
- 綁同組 KV:CREDENTIALS_KV (e7f4320f88d343f187e35e3543dd74c9) / RECIPES (9cf9db905c6241f78503199e58b2ffe0);ENCRYPTION_KEY 走 `wrangler secret put`
|
||||
- `wrangler deploy --dry-run` 通過(1192 KiB, 419 KiB gzip);實際 `wrangler deploy` + `secret put ENCRYPTION_KEY` 留給 richblack 執行
|
||||
- [x] 1.6 建立 `auth-dispatcher.ts`(取代 `credential-injector.ts`):查 auth recipe → HTTP POST 到對應 auth primitive URL → 合併 `_auth_headers` 進 ctx
|
||||
- 2026-04-20 完成:`cypher-executor/src/actions/auth-dispatcher.ts` 新建,export `tryAuthDispatch(componentId, input, env, apiKey)`
|
||||
- 流程:查 `resolveAuthRecipe` → primitive 在 `SUPPORTED_PRIMITIVES`(目前只有 `static_key`)→ fetch `wasmWorkerUrl('auth_static_key')` → 合併 `_auth_headers/_auth_query/_auth_body`
|
||||
- 自引用防護:`AUTH_PRIMITIVE_IDS` set 排除 4 個 `auth_*` componentId
|
||||
- `wasmWorkerUrl` 從 `component-loader.ts` export 出來共用
|
||||
- `graph-executor.ts` 改為:先試 `tryAuthDispatch`(新路徑),沒命中 fallback 到舊 `injectCredentials`(Phase 1.9 刪)
|
||||
- 檢查過 auth-dispatcher.ts 無 `crypto.subtle` / `interpolate` / `{{secret.` / hard-code API URL,符合 rule 02 §2.2
|
||||
- `tsc --noEmit` 通過
|
||||
- [ ] 1.7 端對端測試:openai recipe → 成功注入 `Authorization: Bearer <openai_key>`
|
||||
- [ ] 1.8 端對端測試:twilio recipe(Basic Auth)→ 成功注入
|
||||
- [ ] 1.9 **刪除 `credential-injector.ts` 整檔**(`decryptCredential` / `decryptSecrets` / `interpolateTemplate` / `BUILTIN_CREDENTIALS_MAP` 全刪)
|
||||
|
||||
---
|
||||
|
||||
## Phase 2:auth_service_account WASM
|
||||
|
||||
- [🔄] 2.1 建立 `arcrun/registry/components/auth_service_account/` 目錄
|
||||
- [🔄] 2.2 寫 `component.contract.yaml`
|
||||
- [🔄] 2.3 實作 `main.go`:
|
||||
- 從 stdin 讀 `{api_key, service}` + `kv_get` 拿 recipe + 解密 SA JSON
|
||||
- 解析 SA JSON 取 `client_email` / `private_key`(PEM)
|
||||
- PEM → PKCS8 bytes(純 Go,base64 decode + 去 header/footer)
|
||||
- 組 JWT header + payload(base64url),呼叫 `crypto_sign_rs256(signingInput, pkcs8)` 拿 signature
|
||||
- 組完整 JWT → `http_request` POST `token_uri` → 拿 `access_token`
|
||||
- 展開 `{{runtime.access_token}}` 模板
|
||||
- [x] 2.4 `tinygo build -o auth_service_account.wasm -target=wasi main.go` — 2026-04-20 編譯通過(1.1MB,在 contract 限制 2MB 內)
|
||||
- [x] 2.5 建立 `.component-builds/auth_service_account/` 並部署到 `auth-service-account.arcrun.dev`
|
||||
- 2026-04-20 完成**建置**部分:`.component-builds/auth_service_account/{wrangler.toml, package.json, tsconfig.json, src/index.ts, component.wasm}` 全數到位
|
||||
- 方案 A:`src/index.ts` 重用 `createArcrunHostFunctions` 提供 kv_get/crypto_decrypt/crypto_sign_rs256,**額外加 `http_request` host function**(token exchange 用,非 crypto 不受 §2.2 約束)。http_request 直接回 response body 原文(WASM 端 json.Unmarshal 找 access_token)
|
||||
- 綁同組 KV:CREDENTIALS_KV / RECIPES;ENCRYPTION_KEY 走 `wrangler secret put`
|
||||
- `wrangler deploy --dry-run` 通過(1248 KiB, 440 KiB gzip);實際 `wrangler deploy` + `secret put ENCRYPTION_KEY` 留給 richblack 執行
|
||||
- `auth-dispatcher.ts` 的 `SUPPORTED_PRIMITIVES` 加入 `'service_account'`,workflow 用 google SA recipe 會自動走新 WASM 路徑
|
||||
- [ ] 2.6 端對端測試:google_sheets_sa recipe → 成功取得 access_token → 注入 header
|
||||
- [x] 2.7 **刪除 `lib/jwt-signer.ts` 整檔** — 2026-04-20 完成
|
||||
- `cypher-executor/src/lib/jwt-signer.ts` 已刪除(RS256 JWT 邏輯移入 `auth_service_account.wasm`)
|
||||
- `credential-injector.ts` 原 line 23 `import { exchangeGoogleJwt }` 移除
|
||||
- `credential-injector.ts` 原 line 140-150 service_account 分支改為 throw(任何 service_account recipe 已被 auth-dispatcher 攔截;這條 TS fallback 若被觸發即表架構錯亂,直接爆錯比沈默解密更安全)
|
||||
- `cypher-executor` tsc --noEmit 通過
|
||||
|
||||
---
|
||||
|
||||
## Phase 3:清理 component-loader 的 TS 實作(全刪)
|
||||
|
||||
目標:`BUILTIN_API_RECIPES` 整段刪除,所有服務走 WASM runner(HTTP URL 路徑)。
|
||||
|
||||
- [x] 3.1 確認 `http_request.wasm` / `gmail.wasm` / `telegram.wasm` / `line_notify.wasm` / `google_sheets.wasm` 都在 `registry/components/` 且可執行 — 2026-04-20 驗證 6 個(含 cron)全數存在,main.go + .wasm 齊備
|
||||
- [x] 3.2 確認上述零件 Worker 都已部署(`{name}.arcrun.dev` 可用) — 2026-04-20 完成**建置**部分
|
||||
- 6 個 Worker 建置到位:`.component-builds/{http_request, gmail, telegram, line_notify, google_sheets, cron}/{wrangler.toml, package.json, tsconfig.json, src/index.ts, component.wasm}`
|
||||
- 方案 A:5 個需 http_request 的零件(http_request/gmail/telegram/line_notify/google_sheets)`src/index.ts` 共用模板;cron 是純計算不註冊 host function
|
||||
- 全部透過 `createWasiShim` 複用 cypher-executor/src/lib/wasi-shim.ts(rule 02 §2.2 邊界)
|
||||
- 6 個 `wrangler deploy --dry-run` 全通過(~1.17 MB / ~413 KB gzip 每個);實際 `wrangler deploy` 留給 richblack 執行
|
||||
- [x] 3.3 `component-loader.ts` 的內建路徑改為查對應 Worker URL → HTTP POST — 2026-04-20 完成
|
||||
- 原本第 7 層是 `BUILTIN_API_RECIPES` fallback、第 8 層是 `WASM_HTTP_RUNNER_IDS` (HTTP URL);兩層合併為第 7 層 `WASM_HTTP_RUNNER_IDS` 直接走 `makeHttpRunner(wasmWorkerUrl(id))`
|
||||
- 解析鏈新編號 1-8,順序不變(外部 URL → recipe hash → component hash → R2 → Service Binding → auth recipe runner → WASM HTTP runner → 找不到)
|
||||
- [x] 3.4 **刪除 `BUILTIN_API_RECIPES` 整個 Record**(`http_request` / `gmail` / `telegram` / `line_notify` / `google_sheets` / `cron` 的 TS 實作全刪) — 2026-04-20 完成
|
||||
- `cypher-executor/src/lib/component-loader.ts` 原 line 253-326 `BUILTIN_API_RECIPES` 常數 + fallback lookup 全刪(約 80 行)
|
||||
- 全域搜尋確認:`gmail.googleapis.com/...messages/send` / `api.telegram.org/bot.*sendMessage` / `sheets.googleapis.com/v4/spreadsheets` / `notify-api.line.me/api/notify` 在 cypher-executor TS 中已不存在(auth-recipe-seeds.ts 的 `base_url` 是 recipe 資料欄位,不是 hard-coded API call)
|
||||
- `cypher-executor` tsc --noEmit 通過
|
||||
- [ ] 3.5 端對端測試:workflow 用 gmail auth recipe + gmail.wasm Worker → 成功發信
|
||||
- [ ] 3.6 端對端測試:workflow 用 http_request.wasm Worker + auth_static_key 注入 → 成功呼叫任意 API
|
||||
|
||||
---
|
||||
|
||||
## Phase 4:auth_oauth2 + auth_mtls WASM(封測後)
|
||||
|
||||
- [ ] 4.1 建立 `arcrun/registry/components/auth_oauth2/`
|
||||
- [ ] 4.2 實作:`needs_refresh` / `refresh` / `authenticate` 三個 action
|
||||
- [ ] 4.3 建立 `arcrun/registry/components/auth_mtls/`
|
||||
- [ ] 4.4 實作:輸出 TLS cert/key(實際 mTLS handshake 由 Worker runtime 執行,WASM 無法做 socket)
|
||||
|
||||
---
|
||||
|
||||
## Phase 5:封測啟動門檻 — 核心穩定驗證
|
||||
|
||||
**全部通過才能啟動封測**。
|
||||
|
||||
- [ ] 5.1 所有 20 個 auth recipe seed 可正常運作(static_key 17 個 + service_account 3 個)
|
||||
- [ ] 5.2 `cypher-executor/src/actions/credential-injector.ts` **不存在**
|
||||
- [ ] 5.3 `cypher-executor/src/lib/jwt-signer.ts` **不存在**
|
||||
- [ ] 5.4 `cypher-executor/src/lib/component-loader.ts` 無 `BUILTIN_API_RECIPES` / `BUILTIN_CREDENTIALS_MAP`
|
||||
- [ ] 5.5 `cypher-executor/src/` 全域搜尋 `crypto.subtle.decrypt` 只出現在 `wasi-shim.ts` 的 `crypto_decrypt` host function
|
||||
- [ ] 5.6 `cypher-executor/src/` 全域搜尋 `crypto.subtle.sign` 只出現在 `wasi-shim.ts` 的 `crypto_sign_rs256` host function
|
||||
- [ ] 5.7 `cypher-executor/src/` 全域搜尋 `interpolate` 回傳 0 筆(template 展開全在 WASM)
|
||||
- [ ] 5.8 全域搜尋 `{{secret\.` / `{{runtime\.` 在 TS 檔案中回傳 0 筆
|
||||
|
||||
---
|
||||
|
||||
## Phase 6:通用 CI/CD deploy workflow
|
||||
|
||||
**背景**(2026-04-20 richblack 決定):現 `.github/workflows/deploy.yml` 只部署 cypher-executor + registry + 已刪除的 credentials,漏掉 Phase 1-3 產出的 8 個 Worker,且硬編碼每個 job 導致未來新增 Worker 都要改 CI。改為**通用掃描式 workflow**:任何含 `wrangler.toml` 的目錄 = 部署單位,改到該目錄下任何檔案 = 觸發重新 deploy。
|
||||
|
||||
**關鍵決策**:
|
||||
- 零件 `.wasm` 由 CI build(不 commit):`registry/components/{name}/main.go` 改動時才重 build,用 timestamp / content hash 判斷
|
||||
- `.component-builds/{name}/component.wasm` 由 CI 從 `registry/components/{name}/{name}.wasm` 複製產生(deploy 前一步)
|
||||
- 統一用 pnpm(`.component-builds/*` 本來就是;順勢把 cypher-executor 的 `package-lock.json` 砍了)
|
||||
- runtime secret(`ENCRYPTION_KEY`)不進 CI,由 richblack 一次性 `wrangler secret put`
|
||||
- registry Worker 的 `wrangler.toml` 現階段不改(職責是合約管理,與封測無關;`sandboxAcceptance.ts` 的 rule 02 §2.2 審查留到 Phase 5 用戶自製零件啟動時)
|
||||
|
||||
### Tasks
|
||||
|
||||
- [x] 6.1 改寫 `.github/workflows/deploy.yml`:動態掃描所有含 `wrangler.toml` 的目錄(排除 `node_modules/` + Pages 專案),用 matrix job fanout 部署;分兩層(tier1=`.component-builds/*`,tier2=其他),tier1 全綠後才 tier2(避免 service binding target 未存在)
|
||||
- [x] 6.2 加上 TinyGo build 步驟:tier1 matrix 一律 setup-tinygo + 從 `registry/components/{name}/main.go` rebuild `.wasm` → copy 到 `.component-builds/{name}/component.wasm`
|
||||
- [x] 6.3 diff-aware:push 到 main 時比對 `github.event.before..github.sha`,只 deploy 有 diff 的 Worker(含 `registry/components/{name}/` 連動 `.component-builds/{name}/`);`workflow_dispatch` 提供 `force_all` + `only` 選項
|
||||
- [x] 6.4 統一 pnpm:刪除 `cypher-executor/package-lock.json` + `registry/package-lock.json`;workflow 優先 `pnpm install --frozen-lockfile`,若該目錄無 `pnpm-lock.yaml` 則 fallback 到 `--no-frozen-lockfile`(混合期容錯)
|
||||
- [x] 6.5 加 `max-parallel: 5` 控制 Workers API rate limit(tier1 和 tier2 各自)
|
||||
- [x] 6.6 驗證:`workflow_dispatch` + `force_all=true` 手動跑一次,24 個 Worker 全綠 — 2026-04-20 完成
|
||||
- 最終綠色 run 24668903627(28/28 jobs,含 discover + summary):tier1 24 個零件 Worker + tier2 2 個 orchestration Worker(cypher-executor / registry)全 success
|
||||
- 過程中修兩輪:先修 `setup-node` 的 `cache: 'pnpm'` 對 legacy `package-lock.json` 目錄失效(改為不用 cache);再修 tier2 三個 package.json(cypher-executor/registry/builtins)遺漏 `wrangler` devDependency + regen pnpm-lock.yaml
|
||||
- ENCRYPTION_KEY secret 已由 richblack 授權、CC 從 .env pipe 到三個 Worker:`arcrun-auth-static-key`、`arcrun-auth-service-account`、`arcrun-cypher-executor`(不顯示內容)
|
||||
- [x] 6.7 文件:在 `.claude/rules/` 加一份 `05-deploy-convention.md`(「新增 Worker = 新目錄 + wrangler.toml,不用改 CI」)
|
||||
|
||||
---
|
||||
|
||||
## Phase 7:auth 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 binding(HTTP URL)。
|
||||
- 後來架構演化:用戶要的是 **recipe(資料,存 KV)+ 固定 primitive**,不是新零件。`acr recipe push` 進 KV **不 deploy**。用戶用「http_request primitive + 不同 recipe」打各種服務,永不新增 primitive。
|
||||
- 結論:「用戶建 workflow 要 deploy」這個禁令前提**在 recipe 模型下不成立**。primitive(http_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 精確定義(官方 docs,2026-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 522(P0#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 走同路綁得上。
|
||||
|
||||
### 修法演進:A(service binding)→ 廢 → B(global_fetch_strictly_public flag)
|
||||
|
||||
**先做了 A(service binding)後評估廢棄,改用 B(flag)**——richblack 2026-06-06 拍板。
|
||||
|
||||
- **A(service 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——查證後子域同 zone(zone=註冊域名)照踩 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 確認 change,design §8)
|
||||
|
||||
> 根因:壓測 401 = workflow 寫 `{{credential.X}}` 但三條 template 展開路徑都不認此 namespace。
|
||||
> 修法:auth_static_key 加 `resolve_credentials` action(WASM 解密),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` 是用戶面入口,與 authenticate(recipe 自動注入)並存不重疊
|
||||
- Phase 0.6(host functions)+ 0.7(WASM runner)是 Phase 1-3 的硬前置,必須先做
|
||||
- 若 TinyGo `encoding/base64` 可用就直接用;若不可用則自行實作(見 gmail/main.go 的 `base64URLEncode`)
|
||||
- `auth_mtls` 的 TLS handshake 無法在 WASM 內做(WASI preview1 沒 socket),只能輸出 cert/key 讓 Worker 在 fetch 時用
|
||||
- **每個 auth primitive WASM 都是獨立部署的 Worker**(透過 `component-worker-template/`),不是從 R2 動態載入
|
||||
- Cypher binding = workflow YAML 裡的 URL 清單,不是 Cloudflare service binding
|
||||
@@ -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 });
|
||||
+86
@@ -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;
|
||||
+255
@@ -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 ← 全站 layout:next/font + design tokens + 全域 CSS 匯入
|
||||
│ ├── globals.css ← 匯入 design-tokens.css;Tailwind @import
|
||||
│ ├── design-tokens.css ← 新增:從 design-source 抽出的 CSS variables(:root {...})
|
||||
│ ├── page.tsx ← Landing(RSC)
|
||||
│ ├── 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 wrapper(fetch ${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 />`。
|
||||
- Hero:heading、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」不動。
|
||||
- Strip:4 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 head:breadcrumb「{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」按鈕本次不做動作,僅保 UI(disabled + tooltip「Coming soon」)。
|
||||
|
||||
### 5.4 API Keys — `app/keys/page.tsx`
|
||||
|
||||
- `"use client"`。
|
||||
- Fetch `/api-keys`:若回傳為空陣列但 `/me` 有 api_key,fallback 顯示 `/me.api_key` 為唯一一列(單 key 相容模式)。
|
||||
- 頂部 new-key-box:只在「剛剛建立新 key」的一次性狀態顯示(`useState` + `sessionStorage` flag,reload 後消失)。
|
||||
- 表格、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 + shell(Logo / 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 看到我已部署的 App(packaged 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-6:Dogfooding
|
||||
- 作為 arcrun 核心維護者,我要前端所有與服務打交道的動作都透過 arcrun 自家 API(cypher-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 library(framer-motion)等第三方 |
|
||||
| 狀態管理 | React 內建(useState / useReducer / Context);持久化用 `localStorage` 或 server session cookie |
|
||||
| 國際化 | 延後;本次一律英文(與設計稿一致)。現有 `?lang=zh` 不擴展 |
|
||||
| 無障礙 | 按鈕 `aria-label`、Form 控件有 `<label>`;鍵盤可完成登入 / 複製 key 流程 |
|
||||
| 效能 | Landing 首屏無阻塞 JS(RSC);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/800;Mono 400/500/600 |
|
||||
|
||||
---
|
||||
|
||||
## 5. 範圍界線
|
||||
|
||||
### 納入
|
||||
- 5 screen(Landing、Auth、Dashboard、API Keys、Workflow Viewer)
|
||||
- Logo(SVG arc wordmark)、Icon set(inline 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_key(driver:dashboard 側欄 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) |
|
||||
| 直接寫第三方 API(Slack / 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-23;richblack 尚未下令動工,**所有 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 Mono;body 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 Hero:heading / eyebrow / CTA / radial-grid bg(CSS only)
|
||||
- [ ] 2.3 Paths 左卡:install tabs 元件(Client Component)+ terminal code blocks(字串改 `acr` 實際指令)
|
||||
- [ ] 2.4 Paths 右卡:chat preview(靜態)
|
||||
- [ ] 2.5 Feature strip(4 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 buttons(Google / 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-box(sessionStorage 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 head:breadcrumb / 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 接真實 user(email / 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 Topbar:back / logo / breadcrumb / title / saved indicator / share / export / edit-in-claude
|
||||
- [ ] 6.3 `<Canvas>`:SVG defs + 節點定位 + bezier edges;資料從 `GET /workflows/:name`
|
||||
- [ ] 6.4 Auto-layout:topological depth → columns,同 depth 平均分配 y
|
||||
- [ ] 6.5 NodeCard 點擊 → DetailPanel
|
||||
- [ ] 6.6 DetailPanel:input / output schema、triplet(disabled)、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` 不再用的 code(rotate/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}`,無 project;recipe 甚至全局共享 | entries 樹狀(entry_type=project/workflow,parent_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-*.ts(8 個 action,較重) | triplet 是 **richblack 的 IP、程式量大** → **獨立 repo / 獨立 worker**,基礎不依賴它,要才裝 |
|
||||
|
||||
### Q1 拍板(richblack 2026-06-07):embed 不拆、triplet 拆
|
||||
|
||||
- **embed → 不拆 repo,binding 開/關**:因為 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 = 可選 hook(Vectorize 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**:隨基礎進 arcrun,Vectorize 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)是最簡情形。存 D1(entry/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 公庫 / 私庫雙向機制(CHANGE,richblack 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_id(migration 後自然帶 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 的逐漸沒人用。
|
||||
|
||||
**身份模型(落地)= UUID(richblack 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 在本庫安裝的**唯一** UUID(pull/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 的「投票」= 不選它 = 市場數據)。
|
||||
|
||||
> 影響現有 code:5.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 遷 D1(CHANGE,2026-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 Q2(recipe + workflow 進 D1):
|
||||
- **recipe**:recipe 本體從 RECIPES KV 遷進 KBDB D1(entry_type='recipe' 或專表)。
|
||||
public-recipes 搜尋、list 改 D1 query(LIKE/WHERE,含 §7.5 多作者/市場排序天生適合 SQL)。
|
||||
執行時 resolveRecipe 也改 D1(但執行是 get-by-key 級,KV get 不爆——可漸進)。
|
||||
- **workflow**:workflow record 從 WEBHOOKS KV 遷 D1(entry_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 本體完整遷 D1(resolveRecipe / 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 變 entry(entry_type=workflow 掛 project parent)。
|
||||
1. Leo:同意
|
||||
3. **triplet 獨立 repo 的接點**:基礎 KBDB 怎麼讓 triplet 模組「掛上」——HTTP hook?還是 triplet worker 自己讀同一個 D1?
|
||||
1. Leo:triplet 其實也就是 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+成功記錄進 D1;session/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.sql(entries/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/triplet(grep 證);不去動原 kbdb repo(官方版仍用,動它有風險)
|
||||
- [x] 0.4 templates/records route 純淨:只依賴 record-crud(D1),無 vectorize/triplet
|
||||
- [x] 0.5 基礎 search = D1 LIKE(GET /entries/search?q=,mode:keyword);語義層留給 embed 模組(未在基礎)
|
||||
|
||||
## Phase 1:embed 模組(CF 內建,binding 開/關,不拆 repo)
|
||||
|
||||
> 狀態:基礎已與 embed 完全解耦(不 import)。embed 模組本身(Vectorize upsert + 語義 search)尚未實作——基礎不依賴它即可,embed 待要用時再加。以下 1.x 未做。
|
||||
|
||||
- [ ] 1.1 embed 改可選 hook:有 env.VECTORIZE 才掛
|
||||
- [ ] 1.2 search 語義層:有 Vectorize 啟用,否則降級 LIKE,API 不變
|
||||
- [ ] 1.3 wrangler.toml:Vectorize/AI binding 可選
|
||||
|
||||
## Phase 2:import 進 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 + D1(Q4 已確認免綁卡):init ensureD1Database 建空 D1 → deploy 部署完對 D1 套 migrations/0001_base.sql(CF /d1/query API,idempotent)建三表 + recipe_stat seed + 注入 wrangler.toml database_id
|
||||
- [ ] 2.4 cypher 改用 KBDB 存專案/工作流歸屬(依 Q1/Q2,漸進)— 未做(後續)
|
||||
|
||||
## Phase 3:triplet 模組(獨立 repo,本 SDD 只定接點)
|
||||
|
||||
- [ ] 3.1 確認基礎不依賴 triplet(Phase 0.3 後驗證)
|
||||
- [ ] 3.2 定義 triplet 掛上基礎的接點(依 Q3)
|
||||
- [ ] 3.3 triplet 本身 → 獨立 repo,不在本 SDD
|
||||
|
||||
## Phase 4:官方共用基礎(方向,搬遷另排)
|
||||
|
||||
- [ ] 4.1 官方版改用同一份基礎 KBDB(binding 全開)→ 不維護兩套
|
||||
|
||||
## Phase 5:recipe 投稿 + 帶 KBDB 成功記錄(免官方驗證)—— 本次要做(design 7)
|
||||
|
||||
- [x] 5.1 recipe 成功記錄落地 D1:判定單位=工作流執行(n8n execution)。GraphExecutor 收集本次用到的 recipe key(usedRecipeKeys=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:公庫/私庫雙向機制(CHANGE,design §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 uuid(resolveRecipe,非 resolveAuthRecipe);usedRecipeServices→usedRecipeKeys;KBDB 星數記 per-uuid;public-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_from;installRecipeRecord helper(recipe:{uuid} + idx:canonical 清單 + idx:installed + idx:hash);POST /recipes=私庫沿用 installed uuid 就地更新;POST /recipes/submit=領新 uuid 新增作者版本(非覆蓋);resolveRecipe 向後相容(uuid→installed→fallback 舊 key,不破執行鏈);DELETE 清 uuid+索引;GET dedup;init-seed 用 UUID(author=system);POST /recipes/migrate-uuid 一次性轉舊 key(增量寫不刪舊、冪等)。cypher tsc exit 0。重複靠市場淘汰、不做檢舉、CC 不檢舉(§7.3+mindset §7)
|
||||
- [x] 7.5.b 公→私 pull(CLI 薄殼):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-p(CLI 薄殼):acr recipe submit-p <canonical_id> [--author] → GET 私庫取全文 → 暴露同意 → POST 公庫 /recipes/submit(新增作者版本)。config 加 DEFAULT_PUBLIC_LIBRARY_URL(公庫=官方 cypher,ARCRUN_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/delete(registerAllRecipeTools 註冊進 registry.ts),全用 cypherFetch 薄殼模式(無業務邏輯)。與 CLI 六能力對齊,MCP 不再落後。submit_p 帶 exposure_consent 把關。mcp tsc exit 0。註:MCP 連平台 cypher(§5.2 account-source 已知違反 pre-existing,沿用既有模式不一併修)
|
||||
|
||||
## Phase 8:KV list 上限威脅免費承諾 → 高頻 list 遷 D1(CHANGE,design §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:') → 改單一固定 key(cron-idx:_all 存 {apiKey}:{name}→cron_expr map)只 get 一次。新增 lib/cron-index.ts(readCronIndex/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 query(LIKE/WHERE,多作者/市場排序天生適合 SQL)
|
||||
- [ ] 8.P2 workflow list 遷 D1:webhooks-list/webhooks-named 的 list 改 D1 query(entry_type=workflow)
|
||||
- [ ] 8.P3 recipe/workflow 本體完整遷 D1:resolveRecipe / KV→D1 讀寫 + migration + 雙寫過渡(KV 舊資料相容)。session/credential/exec-context 留 KV(§6 Q2)
|
||||
|
||||
## Phase 9:KBDB 資料層薄殼補 MCP/CLI(HANDOFF §2,design §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 tool,AI 只有「建 template(name+slots) + 填 record(slot→content)」
|
||||
> 可用(類 Supabase 萬用表);薄殼只經 KBDB service binding 調基本盤 HTTP API,不直連 D1、不寫 SQL。
|
||||
|
||||
- [x] 9.1 **MCP 薄殼**(AI 用,插件也走這條):mcp/src/tools/kbdb_data.ts,6 工具
|
||||
`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)。
|
||||
全走既有 kbdbFetch(KBDB 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 proxy(9.2 的前置,2026-06-14)**:CLI 是 client 只認證到 cypher,達不到獨立
|
||||
KBDB worker(MCP 走內部 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` CRUD(HANDOFF §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→401;A POST→owner_id 自動=A;GET by id 回得到;A list count=1、B list 同 type **count=0**(跨租戶隔離);
|
||||
PATCH 改 content 成功且 owner_id hijack→B 被剝除(仍=A、B 看不到);page_name lookup count=1(mira idempotency 路徑);
|
||||
POST 帶 caller owner_id=B→覆寫成 A。已部署 arcrun-cypher-executor(官方 58309bb9)。smoke 資料已清。
|
||||
- [x] 9.7 **修 `arcrun_report_feedback` 死 route(HANDOFF §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 實際送的 payload(entry_type=agent-feedback + metadata_json/tags_json)POST `/entries`→success、欄位保留;
|
||||
經 cypher proxy 讀回 count=1。已部署 arcrun-mcp(官方 58309bb9)。注:MCP service-binding hop 由既有 kbdb_* 工具
|
||||
(9.1,同 kbdbFetch 路徑,2026-06-14 已驗)佐證 binding 活;本次只修死 URL。smoke 資料已清。
|
||||
|
||||
## Phase 10:base 補 record PATCH(mira-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.3,grep-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.2,base 端)**:核實 `updateTemplate` 已支援
|
||||
`slots` 替換(record-crud.ts:44 既有)→ base 端通用能力已就緒,**無需新 code、不建 triplet 端點**。triplet 兩 slot
|
||||
歸 T3.2(plugin repo)。本條=核實 + 文檔釐清,非改 code。
|
||||
- [ ] 10.3 **(選)`DELETE /records/:id`(T2.3)**:暫不做。依賴頂層 T8.3「死資料自動刪除原則」(design 待觀察未定);
|
||||
且 deprecated record 查詢 `where status=active` 本就濾掉 → 無此端點不阻擋 deprecate 落地。待頂層拍板再補。
|
||||
|
||||
## Phase 11:self-hosted KBDB 查詢能力補缺(issue #5,普世框架視角)—— 本次做
|
||||
|
||||
> 來源:issue #5(原 Mira dogfood 開,Mira 已蒸發 → 當「未來任何 self-hosted 用戶都會撞的框架缺口」處理)。
|
||||
> 四點分流依 leo 2026-06-26 修正指示 + 頂層 mira-dissolve 重審:
|
||||
> 規則判準見 07-thin-shell §3.5(issue #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 12:optional 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 語義 normalize(T3.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 幫開 vectorize(T2.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 0(kbdb/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 core;embed=PGVector;triplet=Apache AGE。
|
||||
- embed 不拆 repo(CF 內建、binding 開關);triplet 拆獨立 repo(IP、action 多)。
|
||||
- 官方與 self-hosted 共用同一基礎,差別只在 binding → 只維護一套。
|
||||
- recipe 投稿入口是另一條線(registry),不在本 SDD。
|
||||
@@ -0,0 +1,325 @@
|
||||
# arcrun.dev Landing Page — SDD
|
||||
|
||||
> **目標**:給工程師一個門面,可以取得 API Key、管理 Key、探索 API(Swagger),同時藉此獲得會員 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.js(App 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 Pages(Next.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 session,key = `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)**:
|
||||
- Google(google_drive recipe 的 OAuth App,或另建 arcrun-login Google App)
|
||||
- GitHub(github 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 callback(Pages Function)
|
||||
├── /dashboard API Key 管理(需登入)
|
||||
├── /api Swagger UI(嵌入 swagger.json)
|
||||
└── /integrations 服務目錄(靜態,20 個 recipe)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. 登入 / OAuth 流程
|
||||
|
||||
### 3.1 流程圖
|
||||
|
||||
```
|
||||
用戶點「Google 登入」
|
||||
→ GET /auth/google/start(Worker 端)
|
||||
→ redirect 到 Google OAuth(state = random, 存 SESSIONS_KV sess:state:{state} = {provider, redirect_back})
|
||||
→ 用戶同意
|
||||
→ GET /auth/callback?code=...&state=...(Worker 端)
|
||||
→ 驗 state
|
||||
→ 用 code 換 access_token(POST google token endpoint)
|
||||
→ 用 token 取 userinfo(GET google userinfo)
|
||||
→ upsert USERS_KV user:{provider}:{provider_id} = {email, display_name, api_key, ...}
|
||||
→ 若新用戶:呼叫現有 /register?email=... 取得 arcrun API Key
|
||||
→ 建立 session:SESSIONS_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 / HTTP(n8n 用戶)
|
||||
|
||||
[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>` JS(CDN)
|
||||
- `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-executor(Worker 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_id(read:user + user:email scope)
|
||||
GITHUB_CLIENT_SECRET GitHub OAuth App client_secret
|
||||
SESSION_SECRET 隨機 32 bytes,用於 HMAC session ID(或直接用 UUID)
|
||||
```
|
||||
|
||||
### landing(Pages Environment Variables)
|
||||
|
||||
```
|
||||
NEXT_PUBLIC_API_BASE https://cypher.arcrun.dev
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 8. 實作步驟(Checklist)
|
||||
|
||||
### Phase 1:cypher-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 2:Next.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 Key,Rotate / 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_* ← 最高,解 #7(AI/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
|
||||
|
||||
### 系統關係圖
|
||||
|
||||
```
|
||||
使用者程式碼
|
||||
├── CLI(acr) → cypher.arcrun.dev(HTTP API)
|
||||
├── Python SDK(arcrun) → cypher.arcrun.dev(HTTP API)
|
||||
└── JS SDK(arcrun / @arcrun/sdk) → cypher.arcrun.dev(HTTP API)
|
||||
|
||||
arcrun.dev 網站(Next.js / Cloudflare Pages)
|
||||
├── /login → /auth/google/start, /auth/github/start(cypher.arcrun.dev)
|
||||
├── /dashboard → /me, /me/api-key/rotate(cypher.arcrun.dev)
|
||||
├── /integrations → /auth-recipes(cypher.arcrun.dev)
|
||||
└── /components → /recipes + 靜態零件清單(embedded)
|
||||
|
||||
cypher.arcrun.dev(Cloudflare 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 ← CredentialsClient(push/list/delete)
|
||||
├── auth.py ← AuthClient(setup/bind/get_token/list_services)
|
||||
└── workflows.py ← WorkflowClient(run/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 TBD(arcrun vs @arcrun/sdk),tsup build
|
||||
├── tsconfig.json ← ES2020, NodeNext
|
||||
└── src/
|
||||
├── index.ts ← export class Arcrun
|
||||
├── crypto.ts ← Web Crypto API AES-GCM encrypt(client 端)
|
||||
├── creds.ts ← CredentialsClient(push/list/delete)
|
||||
├── auth.ts ← AuthClient(setup/bind/getToken/listServices)
|
||||
└── workflows.ts ← WorkflowClient(run/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 Demo(Python/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_schema(required / optional 欄位)
|
||||
│ ├── output_schema
|
||||
│ ├── credentials_required(if any)
|
||||
│ └── config_example(YAML 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 1:Python SDK 重建 + 測試
|
||||
1.1 重建 arcrun/python-sdk/(按本 SDD 的結構)
|
||||
1.2 修正上次的 bug:recipe 回應 wrapper、inject key "header" vs "headers"、secret key mapping
|
||||
1.3 對 cypher.arcrun.dev live 測試全部 API
|
||||
1.4 本地安裝測試(pip install -e .)
|
||||
|
||||
Phase 2:JS SDK 重建 + 測試
|
||||
2.1 重建 arcrun/js-sdk/(按本 SDD 的結構)
|
||||
2.2 同步修正 Python SDK 發現的所有 recipe 格式問題
|
||||
2.3 build(tsup)+ 本地測試
|
||||
|
||||
Phase 3:arcrun.dev 網站補完
|
||||
3.1 新增 /components 頁面
|
||||
3.2 更新首頁 code demo(三種使用方式)
|
||||
3.3 OAuth secrets 設定(需 richblack 操作 GCP / GitHub)
|
||||
3.4 登入流程驗證
|
||||
|
||||
Phase 4:GitHub README + 發布
|
||||
4.1 更新 arcrun/README.md — 三種 Quick Start
|
||||
4.2 pip publish(arcrun)
|
||||
4.3 npm publish(TBD 套件名)
|
||||
4.4 最終驗證:從零開始 pip install / npm install / 打 API
|
||||
```
|
||||
@@ -0,0 +1,132 @@
|
||||
# Design:MCP 統一帳號來源 — 單一 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 已能讀三層 config(env > 專案 `.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 平台預設 MCP,AI 幫他帶 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-14,HANDOFF §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-15,HANDOFF §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 transport(richblack §2:不需要)。
|
||||
- ❌ 不把 init / config 搬進 MCP(init 是本機 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 Worker(workflow 執行、credential 管理、auth recipe、webhook)
|
||||
- `u6u-core/credentials`:credential Worker(AES-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 token(escape hatch,給官方 SDK 用)
|
||||
- **workflows.run()**:觸發已部署的 workflow
|
||||
- **workflows.push()**:上傳 workflow 定義
|
||||
- **Recipe**:描述「如何對某服務認證」的 YAML 設定,存在 RECIPES KV
|
||||
|
||||
---
|
||||
|
||||
## Requirements
|
||||
|
||||
### Requirement 1:Python 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=)` — 建構 client,api_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 client(async 版使用 `httpx.AsyncClient`)。
|
||||
6. THE SDK 位置 SHALL 為 `arcrun/python-sdk/`,build 系統用 `hatchling`(`pyproject.toml`)。
|
||||
|
||||
---
|
||||
|
||||
### Requirement 2:JavaScript/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 對等的 API(camelCase 版):
|
||||
- `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 3:arcrun.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 包含三個 tab:Python、JavaScript、HTTP/curl,展示三種使用方式。
|
||||
|
||||
---
|
||||
|
||||
### Requirement 4:GitHub 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 UI(API 文件)。
|
||||
|
||||
---
|
||||
|
||||
### Requirement 5:SDK 發布
|
||||
|
||||
**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 CLI;self-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 再來)
|
||||
│ 驗 token:CF API GET /accounts/{id}/tokens/verify + GET /accounts/{id}
|
||||
│ 缺權限(Workers Scripts Edit / KV Edit / R2 Edit)→ exit 1 指出缺哪個 scope
|
||||
│
|
||||
├─ 2. 建資源(冪等:先 list 已存在就重用)
|
||||
│ 7 KV:WEBHOOKS / CREDENTIALS_KV / RECIPES / USERS_KV /
|
||||
│ SESSIONS_KV / ANALYTICS_KV / EXEC_CONTEXT(rule 01 資料儲存表)
|
||||
│ 1 R2:WASM_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 的 dir,shell 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_SEEDS(cypher-executor/src/lib/auth-recipe-seeds.ts)
|
||||
│ - API recipe:新增 seed-api-recipes.ts(見 §5)
|
||||
│
|
||||
├─ 7. 寫回 config(mode: 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.ts,richblack 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 tarball(richblack 2026-06-02 定案)
|
||||
|
||||
> 此節**取代初稿的「GitHub release artifact」構想**。richblack 拍板更輕的做法:
|
||||
> 直接把預編譯 wasm commit 進 repo,CLI 從 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 進 repo(self-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 個不 commit(richblack 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 tarball(ref 預設 main,acr 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(後)
|
||||
每個 dir:pnpm install(若有 lock)→ CLOUDFLARE_API_TOKEN=<用戶> wrangler deploy
|
||||
5. 回 cypherExecutorUrl = https://arcrun-cypher-executor.<subdomain>.workers.dev
|
||||
```
|
||||
|
||||
注意:tier2(cypher-executor/registry)是 TS,wrangler 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 tarball(tag)→ 重部署成功。
|
||||
|
||||
---
|
||||
|
||||
## 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 全 strip,name/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.1(seed 機制對所有 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.2(seed 通了,但 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_key;self-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 | **帳號 scope:AI 繞過 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()` 裝完實查 CF(KV/D1 listKv/listD1)+ 打 cypher `/health` 確認真就緒,缺哪項報哪項 + 一鍵補裝(acr update 冪等)。
|
||||
- `initSelfHosted` 開頭接 detect(fatal 即 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` 沒 serve(landing/public 缺檔,404;GitHub 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**」要靠 P3(README/入口
|
||||
> 引導 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 1:Python 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)、list(GET /credentials)、delete
|
||||
- [ ] 1.5 `arcrun/auth.py`:AuthClient — setup(fetch recipe → match secrets → encrypt → push)、bind(fetch recipe → resolve headers from cache → return AuthenticatedClient)、get_token、list_services
|
||||
- [ ] 1.6 `arcrun/workflows.py`:WorkflowClient — run(POST /webhooks/named/{name}/trigger)、push(POST /webhooks/named)、list(GET /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 2:JS/TS SDK
|
||||
|
||||
- [ ] 4. 建立 `arcrun/js-sdk/` 目錄
|
||||
- [ ] 4.1 `package.json`:name TBD(arcrun vs @arcrun/sdk),deps=devDeps only(tsup, 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 3:arcrun.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 4:README + 發布
|
||||
|
||||
- [ ] 11. 更新 `arcrun/README.md`
|
||||
- [ ] 11.1 三種 Quick Start(CLI / 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 5:acr init --self-hosted installer(2026-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 個現役 recipe(kbdb_*/gmail_send/google_sheets_*/telegram_send/line_notify_send)。KBDB Supabase 模式進 seed(finally.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 artifact,richblack 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 typecheck:cli `tsc --noEmit` exit 0
|
||||
|
||||
## Phase 6:壓測四橫向問題修正(2026-06-06,richblack 點名)
|
||||
|
||||
> 來源:`/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 0;hook 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 用戶也有 CF,MCP 一律 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 版本自動化**(richblack:deploy 時自動昇版 + 留記錄,避免忘了改):`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 必須有人 merge(richblack approve);AI 偽造不了 GitHub approve |
|
||||
| 舉證「為何不是工作流」 | PR description,review 時看 |
|
||||
| G1 假零件 / G3 純WASI / G4 Gherkin / 覆蓋檢查 / 黃金向量 | **CI(PR 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/js)self-service 投稿、平台端 sandbox 重跑、`acr parts publish` 加人類閘門(投稿不走 CLI 了)。
|
||||
- ✅ 保留並搬 CI:G1 假零件偵測邏輯(detectFakeComponent.ts)、G3 純WASI(wasmImports.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 + 純WASI(R3)← 擴充:只准 WASI preview1 + u6u host func 白名單
|
||||
├─ G4 gherkin_tests(R1) ← 真跑 WASM,given→stdin→比對 then_contains
|
||||
├─ G5 cold_start(mock,標未實作)
|
||||
└─ G6 runtime_compat(mock,標未實作)
|
||||
→ 全過 → 派 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_acceptance(backfill 既有零件)→ 跳過 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」(heuristic:description 含「打/呼叫 ... API/endpoint」且 input 有 url-like 欄位且無實質邏輯運算),標記疑似。
|
||||
- 退稿訊息:「偵測到疑似假零件(寫死 endpoint / http 子集)。這該是 API recipe(http_request + 固定設定)或工作流,不是零件。見 DECISIONS §1。」
|
||||
- 排除:`auth_*` primitive(credential 後端,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-29,richblack review)**
|
||||
|
||||
### 4.0 為何不能在 registry Worker 跑(原設計作廢)
|
||||
原設計假設 registry Worker instantiate 投稿 wasm 跑 Gherkin。**此假設錯誤**:
|
||||
- **Cloudflare Workers 禁止 request-time 編譯 WASM**(`new WebAssembly.Module(bytes)` / `WebAssembly.compile()` 只能 startup 用 bundle 的 module;workers-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 wasm,given→stdin→run→比對 then_contains。Node 環境能 runtime 編譯 wasm(不像 CF Workers)。
|
||||
3. 任一 scenario 失敗 → 投稿指令本地就擋下,不送出。
|
||||
4. 通過 → 把**測試結果隨投稿上傳**(見 4.2)。
|
||||
|
||||
`runGherkin.ts`(已寫,用 createWasiShim)邏輯正確,只是**執行 venue 從 registry Worker 改成 CLI(Node)**。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 的執行移到 CLI;registry 收 `gherkin_evidence` 存證、可選做輕量一致性檢查(evidence 的 scenario 數與 contract.gherkin_tests 對得上、每個 passed=true),但**不重跑**(跑不了)。
|
||||
- G5/G6(cold_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 的靜態驗收(不需跑 wasm,CF 可跑)或 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 relay(DECISIONS §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 hook(commit 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 不適用 Gherkin(DECISIONS §1:recipe 驗收 = 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 hook:CC 直接在 `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**:G1(detectFakeComponent)、G3(wasmImports)、G5/G6(unimplemented_steps)、
|
||||
G0 registry 人類閘門(保留不刪)。測試 15 綠。
|
||||
- **改去向**:G4 Gherkin / 覆蓋檢查 → 未來接 CI PR check(CI 能跑 wasm)。G1/G3 邏輯可複用。
|
||||
- **不做**:CI PR check(richblack:人工 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 hardcode(gmail_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.ts)fan-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 本地跑,非 registry(design §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 回退 sandboxAcceptance:registry 不跑 Gherkin(移除 await runGherkin),改回靜態步驟
|
||||
- [ ] 4.3 Gherkin 邏輯搬到 CLI 投稿指令(Node 環境 instantiate wasm)
|
||||
- [ ] 4.4 投稿 payload 帶 gherkin_evidence(scenario/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_steps,submit 回應明示
|
||||
|
||||
## 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.sh:mkdir `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 relay,DECISIONS §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_steps(cold_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+ 個零件已經部署成獨立 Worker(kbdb_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 影響(吃狗糧的觀察)
|
||||
- 新 AI(Claude / 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: 一次性 backfill(Phase 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 submit(contract 從 `registry/components/{name}/component.contract.yaml` 讀)
|
||||
|
||||
零件 Worker 部署 ⇒ registry 自動更新。沒有「零件部署了但 registry 不知道」的可能。
|
||||
|
||||
### Layer 3: Discoverability(Phase 3)
|
||||
|
||||
- README 移除「21 個零件」這種寫死數字,改「跑 search 看當前清單」
|
||||
- 加 badge endpoint `registry.arcrun.dev/badge/components.svg` 即時顯示數量
|
||||
- MCP `get_component_guide` 開頭加鐵律:「動工前必須先 search,不是猜」
|
||||
- onboarding kit GitHub template:CLAUDE.md / .cursor/rules / AGENTS.md 三件套,all 強制 search 優先
|
||||
|
||||
### Layer 4: u6u → arcrun Rebrand(Phase 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 內部 ID:u6u-mcp-server → arcrun-mcp-server
|
||||
6. README 全文:u6u → arcrun
|
||||
7. user memory(CLAUDE.md / MEMORY.md)相關提及一併更新
|
||||
8. inkstone-component-registry(舊 worker)廢止 → arcrun-registry 為唯一現役
|
||||
|
||||
**Rebrand 原則:**
|
||||
- 用戶端 config(claude_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 的 rebrand(u6u-mcp 同 monorepo 但獨立 SDD)
|
||||
- Phase 5(用戶自製零件 R2 上傳)— 等 Phase 4 完成後另開 SDD
|
||||
|
||||
**前置依賴(已完成):**
|
||||
- ✅ u6u-mcp Zod 4 → Zod 3 修復(2026-05-07)
|
||||
- ✅ u6u-mcp service binding 改指 arcrun-registry(2026-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` bug(2026-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 個 tool(2026-05-07)
|
||||
|
||||
---
|
||||
|
||||
## Phase 1:Backfill Index(半天,立即見效)
|
||||
|
||||
- [x] 1.1 探查 registry 既有 endpoint:發現
|
||||
- 既有 `POST /components` 強制要 wasm bytes(multipart 或 base64),跑沙盒驗收 + 寫 R2 + 寫 KV
|
||||
- cypher-executor 已不從 R2 動態載 wasm(line 32 標 R2 路徑作廢,零件用獨立 Worker URL)
|
||||
- 結論:R2 是 legacy,registry 真正用途是 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 test(contract 驗證 + 錯誤處理通過)
|
||||
- [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 → 500(auth/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 entries(30 created + 30 idx 共 60 keys)
|
||||
|
||||
---
|
||||
|
||||
## Phase 1.5:砍 R2 dead storage(先於 Phase 2,清架構斷層)
|
||||
|
||||
> 2026-05-07 加入。R2 wasm 路徑早已 dead(cypher-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` 欄位保留為 deprecated(queryComponents 仍會讀 legacy record)
|
||||
- [ ] 1.5.5 廢止 `arcrun-wasm` R2 bucket(30 天觀察期後 → 2026-06-07 之後 `wrangler r2 bucket delete`)
|
||||
- [x] 1.5.6 部署 + smoke test:search 端對端通過(kbdb 找到 3 個零件)
|
||||
- [x] 1.5.7 cypher-executor 補清(2026-06-04 richblack 確認):當初 1.5.2/1.5.3 只清 registry,cypher-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#2(routes/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 step(github actions)— 在 wrangler deploy 之後 curl `/index-only`
|
||||
- [x] 2.2 寫 `registry/scripts/register-component.sh`(本地 + CI 共用 SSOT,python3 + pyyaml 解 YAML,curl POST registry)
|
||||
- [x] 2.3 改 `.github/workflows/deploy.yml` tier1 deploy step 後加 "Register component in registry" step(degraded 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 3:Discoverability(半天)
|
||||
|
||||
- [ ] 3.1 改 GitHub `richblack/arcrun` README
|
||||
- 移除「21 個零件」這種寫死數字
|
||||
- 加「跑 `acr parts list` 或 MCP search 看當前清單」
|
||||
- 加 badge:``
|
||||
- [ ] 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 tools;arcrun MCP 已連線時,**禁止用 Python 直打 HTTP API**」
|
||||
- 內附範例 hello workflow 跟 component
|
||||
- [ ] 3.5 寫 onboarding doc:`docs/onboarding-third-party-engineer.md`
|
||||
- 第三方工程師如何 30 秒內讓 AI 學會用 arcrun
|
||||
|
||||
---
|
||||
|
||||
## Phase 4:u6u → arcrun Rebrand(1 天,最後做)
|
||||
|
||||
> 規劃做完 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 紀錄)
|
||||
|
||||
---
|
||||
|
||||
## 風險追蹤
|
||||
|
||||
- 風險 1:backfill 跑進去發現某些 contract.yaml 格式跟 registry 期望不一樣 → 緩解:dry-run 先看,必要時補 contract 欄位
|
||||
- 風險 2:Phase 4 rebrand 期間用戶 client 設定亂 → 緩解:過渡期 + Deprecation header
|
||||
- 風險 3:自動註冊 hook 失敗導致部署被擋 → 緩解:degraded mode(warning 不 fail)
|
||||
|
||||
---
|
||||
|
||||
## Known Issues(吃狗糧發現的,先記錄)
|
||||
|
||||
### KI-1:u6u-mcp README URL 寫錯
|
||||
- README 寫 `mcp.finally.click/mcp`,實際是 `mcp.finally.click/mcp/mcp`(basePath + route)
|
||||
- 影響:用戶照 README 裝完試打不通
|
||||
- 解法:rebrand 時順便修
|
||||
|
||||
### KI-2:inkstone-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 fallback(split 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 層(防在後,真的暴露前)
|
||||
暴露動作打到 server(webhook 部署 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` 並說明。」
|
||||
|
||||
### 為何兩道都要
|
||||
- 只有 hook:AI 可繞過 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 metadata(WEBHOOKS KV 的 record 加 `exposure_confirmed_at`)。
|
||||
|
||||
## 4. 與既有一致(R5)
|
||||
- 同 component-gatekeeping:AI 不可替人類決定有外洩風險的動作;誠實限制(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 = API;recipe = 打某 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.md(richblack 已 review,Q1-Q3 + 法律憑證 + 保護入口決議)。
|
||||
> 每完成一個 task 立刻標 [x],不批次。
|
||||
|
||||
---
|
||||
|
||||
## 共用:同意憑證機制(§7 法律憑證)
|
||||
- [x] C1 定義 `exposure_consent { confirmed_by, understood, confirmed_at, suppress_future }` 型別
|
||||
- [x] C2 同意 log 寫入動作關聯的 record(webhook 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 非 TTY(AI 直跑)無 --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 tool(AI 直接 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 0:Onboarding(AGENTS.md)
|
||||
|
||||
**位置**:
|
||||
- `arcrun/AGENTS.md`(repo 根,git 可讀)
|
||||
- KBDB `type=agent-onboarding` block(MCP `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. **第一個 workflow(minimal 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 1:arcrun-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 & Discovery(5 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 CRUD(7 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 & Trace(5 tools)
|
||||
|
||||
| Tool | 對應 HTTP | 用途 |
|
||||
|---|---|---|
|
||||
| `arcrun_run_workflow` | cypher POST `/webhooks/named/:name/trigger` | 觸發 + 回 execution_id |
|
||||
| `arcrun_get_execution_trace` | **新增** GET `/executions/:id` | 結構化 trace(per-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 Management(4 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 & Skills(4 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 suggestion,e.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 enum(v1,可加不可刪)
|
||||
|
||||
| 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 2:Skill blocks(KBDB-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 patterns,LLM 包成 skill draft,leo review 後 publish。
|
||||
|
||||
---
|
||||
|
||||
### Layer 3:Examples(可搜尋的範例庫)
|
||||
|
||||
#### 3.1 存放
|
||||
|
||||
- `arcrun/registry/examples/{slug}/`
|
||||
- `workflow.yaml`
|
||||
- `description.md`(解決什麼問題、怎麼 trigger、預期結果)
|
||||
- `tags.json`(`["webhook", "llm", "cron", ...]`)
|
||||
- CI build 出 `examples-index.json` 推 KBDB(type=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 4:Telemetry & 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` push(deploy 記)
|
||||
- `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)
|
||||
- 用戶看自己的 telemetry:MCP `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 + delete,POST 走人類流程(安全考量) |
|
||||
| `/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 這些屬於另一個 SDD(KBDB 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 plan(u6u-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_` 確認所有 client(mira / 自家腳本 / 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 1(gap-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 2(advanced):
|
||||
- `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 3(auto-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 2:gap-fill(1 週)
|
||||
- arcrun-mcp 補上 6 個 Phase 1 tools
|
||||
- 新 endpoints 在 cypher-executor 加
|
||||
- 每個 tool 結構化 error contract
|
||||
|
||||
### Milestone 3:skill + example(1 週)
|
||||
- 種子 5 個 skill blocks
|
||||
- 種子 10 個 example workflows
|
||||
- `search_examples` 跑通
|
||||
|
||||
### Milestone 4:closed loop(半週)
|
||||
- weekly_review workflow 部署
|
||||
- 第一份 arcrun-roadmap block 產出
|
||||
- leo 收到第一份週報
|
||||
|
||||
### Milestone 5:rename + cleanup(1 週)
|
||||
- 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 | 整體退場改 arcrun(repo / worker / tool 命名一次 rename) | u6u 不存在了 |
|
||||
| Deprecation 期 | 不留(一次切換 + 公告) | 用戶量極少,留 alias 反而讓新 AI 學到舊名 |
|
||||
| 擴張 vs 建新 | 擴張既有 u6u-mcp(rename 成 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-friendly(UI),現在 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)
|
||||
|
||||
### P1:Claude 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 tools(HTTP→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 不依賴 CLI(CLI 是人的工具) |
|
||||
|
||||
---
|
||||
|
||||
## 非範圍(這個 SDD 不處理)
|
||||
|
||||
- 不處理 GUI 設計 / 視覺
|
||||
- 不處理 SDK(Python/JS)API 設計
|
||||
- 不處理 user OAuth flow
|
||||
- 不重新設計 KBDB schema(只決定 LI 該包什麼 API)
|
||||
- 不重新設計 cypher binding 語法
|
||||
|
||||
---
|
||||
|
||||
## 功能需求(FR)
|
||||
|
||||
### FR-1:AI 一條指令就能上手 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-3:Dry-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-6:AI 可以回報問題
|
||||
|
||||
- `report_feedback(workflow_name, issue_type, description, ...)` MCP tool
|
||||
- 結構化 enum issue_type(防自由文字難聚合)
|
||||
- 寫進 KBDB 成 `agent-feedback` block,可被定期 review
|
||||
- AI 規範「順利 / 卡住 / 不確定 都該 call」(透過 AGENTS.md 強制)
|
||||
|
||||
### FR-7:Implicit telemetry 不依賴 AI 自覺
|
||||
|
||||
- cypher-executor 每次 deploy / execute / fail 自動寫 `agent-telemetry` block
|
||||
- 含 client_user_agent(哪個 AI 用的)、error_message、duration
|
||||
- 不依賴 AI 主動 call,平台自己收
|
||||
|
||||
### FR-8:Skill 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 個 tool(pre-rename)
|
||||
- arcrun-gui 不動
|
||||
- cypher-executor 既有路由不改 contract(只加新 endpoint)
|
||||
|
||||
M5(rename)階段是 breaking change(leo 拍板),需事前公告。
|
||||
|
||||
### NFR-2:transport 不鎖死
|
||||
|
||||
MCP 主,但 HTTP 同等可用(讓沒 MCP 客戶端的 agent 也可用)。每個 MCP tool 都對應一個 HTTP endpoint。
|
||||
|
||||
### NFR-3:error contract 穩定
|
||||
|
||||
`error_code` enum 是 public API,加新值是 minor,移除值是 major。版本化。
|
||||
|
||||
### NFR-4:feedback 數據可外部 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 test,error_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 已 rename,Worker name 待後段 DNS 遷移
|
||||
|
||||
阻擋項:GH Actions 用戶層被 disable(leo 申訴中)→ 改用本機 wrangler deploy + scripts/local-deploy.sh fallback。
|
||||
|
||||
---
|
||||
|
||||
## Milestone 1:可量測(先收 data)✅
|
||||
|
||||
### M1.1 AGENTS.md v1
|
||||
- [x] 寫 `arcrun/AGENTS.md`(5697355 + 3892dc3,263 行)
|
||||
- [ ] CI hook:repo `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 blocks(event=run_success, workflow_name 對, duration_ms 對)
|
||||
- [x] feedback block 寫入測 schema 通
|
||||
|
||||
---
|
||||
|
||||
## Milestone 2:gap-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 tool(10 個)都用統一 contract(ok, 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 3:skill 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 取代 type;base 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`**(v2,2026-06-14)。
|
||||
舊零件 worker 已不存在;改用 `entry_type` + `page_name` idempotency key(GET ?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 4:closed 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 5:rename + cleanup(u6u → 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 自己的 IDE(Claude 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
|
||||
- [ ] CI(deploy.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` 加「實際部署狀態」附錄
|
||||
- [ ] 寫一篇 retrospective:LI 做完前後 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 子 namespace(mira / 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 寫獨立 node,arcrun 走「容器 + recipe」減少零件數量。
|
||||
|
||||
---
|
||||
|
||||
## 1. 問題
|
||||
|
||||
### 1.1 撞牆現場
|
||||
|
||||
寫 mira wiki 合成 workflow(7-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 合成 workflow(7-B 卡關)
|
||||
- **推廣破功**:arcrun 對外 prop 是「容器 + recipe,AI 不用寫 code」,但 prompt 這層做不到
|
||||
- **未來所有 AI workflow 都會撞同樣問題**:rss-tech-news 評語、河道 AI 副駕、ai-comment、文章摘要⋯ 全部需要組 prompt
|
||||
|
||||
---
|
||||
|
||||
## 2. 設計
|
||||
|
||||
### 2.1 核心:prompt_recipe 平行於 auth_recipe / api_recipe
|
||||
|
||||
**儲存**:`RECIPES` KV,key 格式 `prompt_recipe:{name}`
|
||||
|
||||
**結構**:
|
||||
```yaml
|
||||
id: prompt_recipe:wiki_synthesis
|
||||
version: v1
|
||||
description: "Mira wiki 合成(抽 triplet + 寫 wiki paragraph)"
|
||||
model: sonnet # haiku / sonnet / opus(claude_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-style,parse 失敗回 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 block,cypher-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}}" # ← 沒 recipe,cypher-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 template(schema / skill 模板)
|
||||
- recipe 是「組合配方」(指向哪些 block + 怎麼組),是 platform 層
|
||||
- recipe **裡面** 引用 KBDB block id(fragments.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 管理 tool(list / get / push / delete prompt_recipe)
|
||||
|
||||
**不在範圍內:**
|
||||
- HTTP api_recipe / auth_recipe 改造(已有,不動)
|
||||
- 多模態 prompt(image input)— 等 P2
|
||||
- recipe 沙盒驗收(recipe 是資料不是 code,不需要)
|
||||
|
||||
**前置依賴(已完成):**
|
||||
- ✅ kbdb_get 零件(5.3)
|
||||
- ✅ component-registry MCP backfill(component-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 用法新 node(chat / 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 寫第一個 recipe(wiki_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 | 架構選擇 B:recipe 解析在 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 1:prompt_recipe Schema + KV 規範
|
||||
|
||||
- [x] 1.1 寫 `cypher-executor/src/lib/prompt-recipe-schema.ts`(85 行 Zod schema:fragments / 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
|
||||
- expander:fragments(KBDB) + inputs(context+transform) → 套 {{var}} 模板 → {prompt, model, output_*}
|
||||
- type-check 全通過
|
||||
|
||||
## Phase 2:cypher-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` 帶 recipe,trace 顯示 recipe 展開正確 + claude_api 拿到組好 prompt(Mira 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_api,recipe 在 cypher-executor 解析)
|
||||
- [x] 3.5 端對端測試:用 MCP `u6u_execute_workflow` 跑 wiki_synthesis 成功
|
||||
- input:1 句草稿(黃仁勳 GTC 2026 物理 AI)
|
||||
- output:3 triplets + 3 entities + 1 wiki paragraph + source_summary
|
||||
- 過程修了 KI-14 (service binding 指錯)、KI-15 (token 沒轉發)、KI-16 (Claude markdown fence 沒剝)
|
||||
|
||||
## Phase 4:mira 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 5:MCP 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)
|
||||
|
||||
---
|
||||
|
||||
## 風險追蹤
|
||||
|
||||
- 風險 1:claude_api 改造跟 mira-app 同時動,可能影響河道 AI 副駕
|
||||
- 緩解:向後相容,舊 input 仍可用,mira 河道先不切 recipe
|
||||
- 風險 2:recipe transform 白名單漏了某種需求
|
||||
- 緩解:發現缺什麼再加,第一版優先支援 wiki 用到的(json_array, extract_field, join)
|
||||
- 風險 3:KV 跟 KBDB 都存配置,AI 困惑「該存哪邊」
|
||||
- 緩解:清楚分層 — recipe(容器組合方式) KV,data(schema 文字、skill 模板) KBDB
|
||||
|
||||
---
|
||||
|
||||
## Known Issues(吃狗糧發現,記錄)
|
||||
|
||||
### KI-11:MCP `u6u_execute_workflow` 不暴露 config 欄位 ✅ 修復(2026-05-07)
|
||||
- 已修:tool schema 加 optional `config: Record<string, Record<string, any>>`
|
||||
- 部署:u6u-mcp v11d7e366
|
||||
- 用戶要重啟 client session 才能看到新 schema
|
||||
|
||||
### KI-12:MCP execute 路由打 `/execute` 而非 `/cypher/execute` ✅ 修復(2026-05-07)
|
||||
- 已修:service binding fetch URL 改成 `http://cypher-executor/cypher/execute`
|
||||
- 部署:u6u-mcp v11d7e366
|
||||
|
||||
### KI-14:u6u-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-15:u6u-mcp 沒把 partner token 轉發給 cypher-executor ✅ 修復
|
||||
- 現象:recipe expander 抓 KBDB block 401(沒 auth)
|
||||
- 根因:partnerAuthMiddleware 驗完 token 但只 set org_namespace,沒留 token;execute_workflow tool fetch 沒帶 X-Arcrun-API-Key
|
||||
- 解法:middleware 也 set partner_token、handleMcpRequest + registerAllTools + execute_workflow 多一個 partnerToken 參數、fetch header 加 X-Arcrun-API-Key
|
||||
|
||||
### KI-16:Recipe JSON output 被 Claude 包在 ```json``` markdown fence ✅ 修復
|
||||
- 現象:JSON.parse 失敗 "Unexpected token \`"
|
||||
- 根因:Claude 預設輸出 ```json\n{...}\n``` 包裝
|
||||
- 解法:cypher-executor 解析前 regex 剝 fence
|
||||
|
||||
### KI-13:cypher-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 Workflow(webhook 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": "..." }
|
||||
}
|
||||
```
|
||||
- 失敗也要 callback,body 含 `error` 欄位
|
||||
- 重試策略:3 次 backoff(1s / 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 內 pending(item-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,這裡是點對點 callback,KV 已夠
|
||||
- **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 主動 poll(poll_url 仍有效)
|
||||
- 超過 24h 沒被消化的 task:daemon GC
|
||||
|
||||
### 3.2 cypher-executor 端 resumable runtime
|
||||
|
||||
#### 3.2.1 偵測 pending(graph-executor)
|
||||
|
||||
在 Component case,runner 回傳後:
|
||||
|
||||
```ts
|
||||
result = await runner(mergedContext);
|
||||
|
||||
// 偵測 pending pattern(daemon 約定的回應結構)
|
||||
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_id,daemon 收到 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`(不帶 query,task_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 給原 client(client 在 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 pending(v2)
|
||||
- foreach 內 pending(v2)
|
||||
- final_callback 給原 client(v2)
|
||||
- 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 runtime(pause/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/resume(resumable runtime)跨越 |
|
||||
| daemon callback 進來時 cypher-executor 重啟 → state 還在 KV,OK | 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 存的是執行前的 context,secret 還沒解)|
|
||||
|
||||
---
|
||||
|
||||
## 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 1:Mira daemon 端 callback 支援
|
||||
|
||||
- [x] 1.1 改 `/opt/mira/mira-daemon.js`(Hetzner mira container)`/execute` 接受 `params.callback_url`
|
||||
- [x] 1.2 fireCallback function:task done/failed 時 POST callback_url,body = `{task_id, success, data?, error?}`
|
||||
- [x] 1.3 callback retry:4 次(立即 + 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 2:cypher-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 3:resume endpoint
|
||||
|
||||
- [x] 3.1 寫 `routes/resume.ts`:POST /workflows/resume,consumePausedRun → 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 4:claude_api 容器透傳 callback_url
|
||||
|
||||
- [x] 4.1 改 `claude_api/main.go`:Input 加 CallbackURL;timeout 預設改 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 6:Trigger 端背景執行(`?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.tsx:triggerProjectDetector + triggerWikiSynthesis 觸發 URL 加 `?async=1`
|
||||
|
||||
---
|
||||
|
||||
## 風險追蹤
|
||||
|
||||
- 風險 1:daemon callback 進來時,cypher.arcrun.dev 還沒醒(CF Worker cold start)→ 第一次 retry 接住(daemon retry policy 涵蓋)
|
||||
- 風險 2:v1 沒 final_callback 給原 client → 用戶要主動查狀態
|
||||
- 接受:mira 河道 UI 可定期 refetch wiki page,或用既有 KBDB 觸發機制
|
||||
- v2 加 final_callback 統一處理
|
||||
|
||||
## v2 已記錄
|
||||
|
||||
- nested pending(一個 run 多個 paused 節點)
|
||||
- foreach 內 pending(item-level resume)
|
||||
- final_callback 給原 client(trigger 時帶 final_callback_url)
|
||||
- poll_task 零件(外部 API 沒 webhook 時用)
|
||||
@@ -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-27(issue #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. R1:CLI 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 context(run.ts 現已帶 headers + inputContext,只差路徑)。
|
||||
- **驗收**:leo21c 部署一個 workflow → `acr run <name>`(本機無該 YAML,走「玩法二」)→ 真的觸發執行(200),非 404。
|
||||
|
||||
---
|
||||
|
||||
## 3. R2:list 來源統一(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. R3:P2 單邊能力決策
|
||||
|
||||
| 能力 | 現況 | 判定 | 理由 |
|
||||
|------|------|------|------|
|
||||
| 驗證 YAML | CLI `acr validate` **純本機**(validate.ts:44,loadWorkflowYaml+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 不該代傳 credential(mindset §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 tag?tag 系統不在 #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 #11,4 點全定)
|
||||
|
||||
1. **Q1 R4 = 雙層(對照清單 + 本機 smoke test)✅** 含 §5.3 機制自驗。守 flag 紅線(本機手動跑非 CI/cron)。
|
||||
2. **Q2 CLI list = 複用 `GET /webhooks/named`✅**(不新建 proxy,CLI/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 任務 → 可驗收條目)
|
||||
|
||||
### R1:P0 死端點修掉
|
||||
- **R1.1** CLI run 改打 `/webhooks/named/:name/trigger`(真端點),不再 404。
|
||||
- **R1.2** MCP deploy 死端點**不在本 SDD 修**(歸 #8 ①-a / #10 ①-b,避免三方重複改 deploy);本 SDD 僅記其存在 + 防複發機制要能攔它。
|
||||
|
||||
### R2:P1 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。
|
||||
|
||||
### R3:P2 單邊能力盤點決策
|
||||
- **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(全面盤點+防複發)/ #8(deploy 強制+search)/ #10(編排下沉)有交集,**deploy 那條不重複改**(本 SDD 只盤點 + 防複發涵蓋它)。
|
||||
- **C5 署名**:跨 repo comment 開頭 `[arcrun CC]`。
|
||||
|
||||
---
|
||||
|
||||
## 5. 非目標
|
||||
|
||||
- ❌ 改 deploy 死端點(歸 #8/#10)。
|
||||
- ❌ 把防複發機制做成 CI 高頻 gate(C2 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-27(issue #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 1:P0 死端點(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 2:P1 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 record,tag 過濾仍走 resource_tag)— registry 簽名加 partnerToken,tsc 綠。⚠️ 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 3:P2 單邊能力(R3)
|
||||
|
||||
- [⏸] 3.1 validate:核實完成——**真漂移且依賴 #10**。CLI 本機驗 YAML(loadWorkflowYaml+parseTriplets+validateRelations);MCP 打 server /validate 但傳的是已解析的 `{nodes,edges}` graph(graphSchema.safeParse)。兩邊**輸入不同層**(YAML vs graph),與 deploy 的 YAML→graph 編排債同根。乾淨收斂依賴 #10 編排下沉(YAML→graph 變 API 能力後 validate 才能統一吃 YAML)。**標記依賴 #10,記對照清單,不在本 SDD 強收**
|
||||
- [ ] 3.2 creds push:記明「刻意單邊」於能力對照清單(含原因:含加密+本機檔,AI 不代傳 credential)
|
||||
- [ ] 3.3 search:CLI `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 harness(acr 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 的每個 guardrail(hook / 提醒)擋下時,必須在訊息裡給「具體怎麼做才能合法通過」的下一步。**
|
||||
- 只說「不行」= 把 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.0(llms.txt 入口)、§2(install-harness)、§4(hook)都是「讓 CC 能拿著這張清單替用戶跑完」的手段。
|
||||
|
||||
---
|
||||
|
||||
## 1. harness 三層(對應三需求 + Claude Code 載入機制)
|
||||
|
||||
依 claude-code-guide 查證(2026-06-03):
|
||||
|
||||
| 需求 | 機制 | 強制力 | 裝進用戶專案 |
|
||||
|---|---|---|---|
|
||||
| **事前提醒**「用 arcrun 別自寫 Python」| CLAUDE.md(session 必載入)| 弱(可能遺忘)| 用戶 `CLAUDE.md` append arcrun 區塊 |
|
||||
| **資源去哪取** | Skill(按需觸發)+ CLAUDE.md 指令 | 中 | `.claude/skills/arcrun/SKILL.md` |
|
||||
| **做錯被糾正** | PreToolUse hook(exit 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(丟連結給 CC,CC 讀 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. (P7,optional)claude 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 MCP(P7):CC 偏好的工具,納入安裝 + update(richblack 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 工具 → 有 MCP,CC 操作 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. 需要 credential(API 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.txt;repo 也放一份 |
|
||||
| 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. **MCP(P7)self-host 連哪個**:用戶連你的公共 arcrun MCP,還是連他自部署的?(MCP 是 CF Worker,self-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 harness(acr 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」(傾向動態)?
|
||||
- [ ] Q4(design §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`:用戶版 guard(python 提醒不硬擋 / 暴露 exit 2 / 每條含正路)
|
||||
- [x] A5 `cli/harness/settings.fragment.json`:hook 註冊片段
|
||||
- [x] A6 修正壓測 §9.5 字串比對 bug:guard 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.5;optional)
|
||||
- [ ] 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 種全 ALLOW:echo提到/printf提到/cat-heredoc/無關)+ 邊角(env 同意放行、pipe||後執行 BLOCK、heredoc-dash ALLOW);`bash -n` 語法 OK
|
||||
@@ -0,0 +1,182 @@
|
||||
# workflow-discovery — Design
|
||||
|
||||
> **狀態**:草案,待確認。對應 `requirements.md`。
|
||||
> **建立**:2026-06-27(issue #8)
|
||||
|
||||
---
|
||||
|
||||
## 1. 設計總綱
|
||||
|
||||
三件事,全部把能力落在 API(cypher-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. 核心決策 Q1:metadata 存哪(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 寫失敗不阻塞部署但回 warning(fire-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 透傳給 AI,AI 看到就能主動問用戶開 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_workflow=AI 收到自然語言意圖時**主動 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 #8,4 點全定)
|
||||
|
||||
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 已有真語意 search(issue #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 三任務 → 可驗收條目)
|
||||
|
||||
### R1:description slot,建工作流時強制填
|
||||
- **R1.1** workflow 部署時 `description` 為**必填**;空白/缺失 → 部署被擋(回明確錯誤,像零件那樣不給空白過)。
|
||||
- **R1.2** 兩條部署路徑(CLI `acr push` / MCP `u6u_deploy_workflow`)**一致強制**——不能一條擋一條放。
|
||||
- **R1.3** description 是**能力(API 行為)**,強制邏輯落在 API(cypher-executor 部署端點),不寫進薄殼介面層(守 rule 07)。
|
||||
|
||||
### R2:search_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 資產;公庫化是更大的另案)。
|
||||
- ❌ 自動觸發式 search(C2 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-27(issue #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:強制 description(R1,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 非 YAML,YAML→graph 編排現在寫在 CLI push.ts 介面層;MCP 複製=違 rule 07。等總管定 ①-a(複製)/①-b(編排下沉新 /workflows/deploy 吃 YAML,CLI 也改用)/①-c(先 a 通、b 另開 issue)。**注**:無論哪個,MCP 最終打 /webhooks/named(已強制 description,1.1 完成)→ description 強制目標三選項都達成。
|
||||
- [x] 1.3b(方向①前置,三選項共需)`GET /webhooks/named` 補回 description/created_at/cron_expr 欄位,讓 MCP list 改讀本端點時欄位齊 — webhooks-named.ts,tsc 綠
|
||||
- [ ] 1.4 驗證:兩條路徑各跑一次「無 description 部署」→ 都被擋(端到端,非只 tsc)— CLI 路徑已可驗,MCP 待 ①-a/b/c 收
|
||||
|
||||
## Phase 2:可搜 entry 雙寫(R2 資料層)
|
||||
|
||||
- [x] 2.1 cypher 部署 handler:record 寫完後雙寫一個 `entry_type=workflow` entry(content=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` filter(base 通用,不寫死 workflow)— 改 4 處:searchEntries(entry-crud.ts) + semanticSearch(embed.ts,entry_type 已 index) + route(entries.ts) + cypher kbdb-proxy `/kbdb/search` 透傳。kbdb+cypher tsc 綠
|
||||
- [ ] 2.3 驗證:部署一個帶 description 的 workflow → KBDB 查得到對應 entry(owner_id 正確)
|
||||
|
||||
## Phase 3:search_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=keyword(Vectorize 未開):LIKE 命中 + 回 capability_hint「叫 CC 幫你開語義查詢」
|
||||
- [ ] 3.4 驗證 mode=semantic(Vectorize 開,需 self-hosted leo21c):語意命中,限本租戶
|
||||
- [ ] 3.5 租戶隔離驗證:A 租戶搜不到 B 租戶的 workflow(count=0)
|
||||
|
||||
## Phase 4:既有工作流回填(R3)
|
||||
|
||||
- [x] 4.1 cypher `POST /workflows/backfill-search-entries`(限本租戶):有 description 的 record → 補寫 entry;無 description 的 → 列出回報,不自動編造 — webhooks-named.ts,tsc 綠
|
||||
- [ ] 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-out(C2)。
|
||||
- 框架級改動 → 端到端實證(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-02,self-hosted 開源策略)**:
|
||||
> 原慣例「`.component-builds/{name}/component.wasm` 不 commit 進 repo」**已推翻**。
|
||||
> 現在 **commit `.component-builds/*/component.wasm` 進 repo**,因為 self-host 用戶 / `acr init --self-hosted`
|
||||
> 從 GitHub(codeload 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`**。
|
||||
- CI(deploy.yml):仍在 deploy 前自動 rebuild + copy(部署 prod 用最新 source;與 repo 內 commit 的 wasm 不衝突——前者給 CI deploy prod,後者給 self-host 用戶當部署來源)。
|
||||
|
||||
### 誠實 trade-off(mindset §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」的問題,先跑這條確認目錄被掃到。
|
||||
@@ -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.1,2026-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 config(Claude 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_*` prefix,M5 一次改 `arcrun_*`。
|
||||
> 本 doc 描述目標狀態,實際用 `list_*` tool 取得當前可用名單。
|
||||
|
||||
取得 ak_ 金鑰:到 https://arcrun.dev/me(OAuth 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. 你的第一個 workflow(5 分鐘 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 3:dry-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` | 零件 worker(cypher-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 已接受,在背景跑等 callback(claude_api 等) | **正常**,wait 或 call `get_execution_trace` 看狀態。舊名 `paused_awaiting_resume` 2026-05-16 改 |
|
||||
| `dependency_unavailable` | 下游 API(Claude / 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 個 playbook:watcher / 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 的「資料庫」是 KBDB(Cloudflare D1)。萬物皆 block:note / 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 Swagger:https://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 完成)。
|
||||
@@ -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** → 薄殼打不存在的 API(seed 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. 🚀 deploy(worker + 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 workers(26 個含 mcp) | `local-deploy.sh`(wrangler deploy) | 平台 prod;self-hosted 自己 deploy |
|
||||
| CLI(npm `arcrun`) | `local-deploy.sh` 第 6 段 / `cd cli && npm publish` | 用戶 `npm i -g arcrun` |
|
||||
| landing(arcrun.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 SDD(issue #11)R4 防複發機制層 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 另案)|
|
||||
| recipe(6 能力)| `acr recipe *` | `arcrun_recipe_*` | `/recipes/*` `/public-recipes/*` | ✅ | ✅ | 已對齊 |
|
||||
| credential 上傳 | `acr creds push` | — | `POST /credentials` | ✅ | — | **刻意單邊**(非疏漏):含 client 端加密 + 本機檔路徑,AI 不代傳 credential(mindset §6/§7)|
|
||||
| KBDB 資料層(template/record/query/search)| `acr kbdb *` | `kbdb_*`(6 工具)| `/kbdb/*` | ✅ | ✅ | 已對齊(#8 前批)|
|
||||
| tag(create/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 tier(D1-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.1,2026-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。
|
||||
|
||||
### records(template 實例=填 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 KBDB(kbdb.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 誤寫清理 SOP(issue #3 待辦 1)
|
||||
|
||||
> **狀態**:runbook 已備妥,**待官方運營方(leo)親自執行**。
|
||||
> **為什麼不由 CC 直接跑**:對官方 prod D1 執行不可逆 `DELETE`,需官方憑證 + 人類明示確認
|
||||
> (mindset §7「絕不代替人類做有風險的確認」;rule 06)。CC 只備妥可審、防誤刪的腳本,DELETE 由人按下。
|
||||
> **來源**:issue #3(leo 2026-06-24 拍板,14-E 遷移善後)。根因 bug 已修(issue #2,commit 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 account(58309bb9)
|
||||
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。
|
||||
@@ -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)✓ 200,input 含 `api_key=ak_xxx`
|
||||
- `load_skill`(節點 2)✗ 401 Unauthorized,input 含 `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}}" # ← 原文傳,不展開
|
||||
```
|
||||
|
||||
非阻擋 P1,SDD 待開 `interpolate-nested-config`。當前 workaround:直接看上游節點的 trace output。
|
||||
|
||||
## 未來怎麼避免
|
||||
|
||||
1. **新 edge type 加進來時必須走 baseCtx merge 模式**——可以抽出 helper `mergeCtxForDownstream(context, result)` 強制所有 caller 用,避免漏
|
||||
2. **interpolation 邊界要有測試**:寫一個 2 節點 chain 用 `{{api_key}}` 引用原始 context 的 e2e test,CI 跑過
|
||||
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-07(FOREACH 同類修法)
|
||||
- 受影響 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 chain,wallTime 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 跑同 src,fetch 全通** → 不是 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 direction(u6u-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`
|
||||
- 兩個 caller(line 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 worker(kbdb-get / kbdb-ingest / kbdb-create-block / kbdb-patch-block / claude-api)→ Settings → Domains & Routes → workers.dev → **Enable**
|
||||
|
||||
**未來新增 component worker 時必須**:dashboard 啟用 workers.dev URL(rule 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.md:URL 慣例改為「對內 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.dev(rule 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 步驟 2(credential 注入鏈路)阻擋;Phase 3 降級假零件成 recipe 的前置。
|
||||
|
||||
---
|
||||
|
||||
## 症狀
|
||||
|
||||
`acr recipe test kbdb` 端到端打不到 2xx。直接 probe `auth_static_key`:
|
||||
|
||||
```
|
||||
POST https://auth-static-key.arcrun.dev/ {action:"authenticate", api_key:"ak_…", service:"kbdb"}
|
||||
→ {"success":false, "error":"credential kbdb_api_key 解密失敗", ...}
|
||||
```
|
||||
|
||||
前置都綠(排除誤判方向):
|
||||
- `auth_recipe:kbdb` 存在、`primitive=static_key`(kv_get 命中 410 bytes)
|
||||
- `kbdb_api_key` credential 存在 KV(kv_get 命中 108 bytes 的 `{encrypted, iv}`)
|
||||
- 失敗精準落在「解密」這一步
|
||||
|
||||
## 定位(key-fingerprint 診斷,只印 SHA-256 前綴,不印 key/明文)
|
||||
|
||||
在 `aesGcmDecrypt`(`wasi-shim.ts`)暫加:
|
||||
|
||||
```
|
||||
console.error(`[decrypt] ENCRYPTION_KEY sha256_prefix=${fpHex} keyLen=${len}`)
|
||||
```
|
||||
|
||||
deploy auth-static-key + `wrangler tail` 抓到:
|
||||
|
||||
| 來源 | keyLen | sha256 前綴 | 格式 |
|
||||
|---|---|---|---|
|
||||
| 加密端(CLI `~/.arcrun/config.yaml` 的 `encryption_key`) | 64 | `fa84f2ce9027` | hex(→32 bytes)✓ |
|
||||
| 解密端(`arcrun-auth-static-key` 的 `ENCRYPTION_KEY` secret) | **44** | **`ff219b123c89`** | base64 ✗ |
|
||||
|
||||
**兩個 mismatch 同時存在**:值不同 + 格式不同。`hexToUint8Array` 套在 44-char base64 上會解成垃圾 bytes,AES-GCM 必失敗。
|
||||
|
||||
漂移源頭:`arcrun/.env` 裡的 `ENCRYPTION_KEY` 就是那把錯的 base64(`ff219b123c89`),有人拿它去 `wrangler secret put` 設進 auth-static-key。
|
||||
|
||||
## 為什麼正本是 64-hex
|
||||
|
||||
`/register`(register.ts:42)把 `encryption_key: c.env.ENCRYPTION_KEY` 原樣回給用戶 —— 即 **cypher-executor 的** `ENCRYPTION_KEY`。用戶 config 是 64-hex(`fa84f2ce9027`),所以正本 = cypher-executor 那把 64-hex。CLI 加密 credential 也用這把。auth-static-key 必須跟它一致才能解開。
|
||||
|
||||
診斷用完即移除(`wasi-shim.ts` 還原,git diff 為空)。
|
||||
|
||||
## 附帶 bug:Body has already been used
|
||||
|
||||
修對 key 後,`/execute` 端到端從 500 變成「Node n1 failed: Body has already been used」。
|
||||
|
||||
`component-loader.ts` 的 `makeRecipeRunner` / `makeAuthRecipeRunner`:
|
||||
|
||||
```ts
|
||||
const data = await res.json().catch(() => res.text()); // ✗ res.json() 失敗時 body 已消費
|
||||
```
|
||||
|
||||
KBDB `/health` 回非 JSON(純文字)→ `res.json()` throw → `.catch(() => res.text())` 第二次讀 body → throw。
|
||||
|
||||
修法 — 讀一次:
|
||||
|
||||
```ts
|
||||
async function readBodyOnce(res: Response): Promise<unknown> {
|
||||
const text = await res.text();
|
||||
try { return JSON.parse(text); } catch { return text; }
|
||||
}
|
||||
```
|
||||
|
||||
## 修法步驟
|
||||
|
||||
1. `cd .component-builds/auth_static_key && wrangler secret put ENCRYPTION_KEY`,貼正本 64-hex(= `~/.arcrun/config.yaml` 的 `encryption_key`)。**richblack 手動**(rule 05:runtime secret 不進 CI、CC 不碰)。
|
||||
2. `component-loader.ts` 加 `readBodyOnce()`,兩處 `res.json().catch(...)` 換掉。`tsc --noEmit` 綠,deploy cypher-executor。
|
||||
3. 修正源頭文件 `arcrun/.env` 的 `ENCRYPTION_KEY` 改成 64-hex(避免下次再設錯)。
|
||||
|
||||
## 驗證證據
|
||||
|
||||
- 直接 probe auth-static-key:**HTTP 200**, `success:true`, 產出 `Authorization: Bearer …`
|
||||
- 端到端 `/execute`:**HTTP 200**, trace 乾淨
|
||||
- auth 確證:直接 curl KBDB `/blocks` 不帶 token → `401 {"error":"Missing token"}`;經 cypher-executor(注入 token)→ 過 auth,進 KBDB handler 回 ZodError(缺 `content`)。**無 401 = token 被接受**。
|
||||
|
||||
## 教訓
|
||||
|
||||
- **同一把 key 出現在 ≥2 個 Worker 的 secret = 漂移風險**。auth-static-key / auth_service_account / cypher-executor 都讀 `ENCRYPTION_KEY`,靠人各設一次必漂。長期應有單一發放來源或部署時自動同步。
|
||||
- **debug 加密問題,先比 key 指紋(SHA-256 前綴),不要碰 key 明文**。一個 fingerprint log 就分辨出「值錯」vs「格式錯」vs「資料壞」。
|
||||
- **`res.json().catch(() => res.text())` 是反模式** —— body 只能讀一次。永遠先 `res.text()` 再 `JSON.parse`。
|
||||
@@ -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 URL:https://cypher.arcrun.dev/webhooks/named/my-workflow/trigger
|
||||
需帶 Header:X-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 Sheets(Service Account)、Gmail(Service Account)、Google Drive(Service 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 @@
|
||||
# 交付前自測 Checklist(pre-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 成功
|
||||
- [ ] 印出的「下一步①②」可照抄:.env(NAMESPACE+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 + MCP,AI 客戶會用 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. 回歸(沒把舊功能弄壞)
|
||||
|
||||
- [ ] 既有種子 recipe(gmail/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 公庫/私庫機制 + UUID(2026-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 | 建 .env(NAMESPACE + 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 | 用一個既有種子 recipe(gmail/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.3,self-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/更弱模型)。
|
||||
@@ -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** | 小(20–150KB) | 良好 | 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,開 PR,merge 後所有以 `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 preview1,stdin/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)
|
||||
@@ -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
|
||||
@@ -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 的長期記憶入口。
|
||||
@@ -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 source:pages/、journals/
|
||||
→ 忽略:logseq/、assets/
|
||||
|
||||
else if 根目錄有 .obsidian/ 資料夾
|
||||
→ vault 類型:Obsidian
|
||||
→ raw source:根目錄下所有 .md(排除 .obsidian/ 內的檔案)
|
||||
|
||||
else
|
||||
→ vault 類型:一般專案
|
||||
→ raw source:docs/ 下所有 .md
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 第三步:讀取現有 wiki 狀態
|
||||
|
||||
進入 `system-dev/wiki/`,讀取:
|
||||
|
||||
- `INDEX.md`:目前已有哪些 wiki 頁面(多角度視圖入口)
|
||||
- `status.md`:上次整理時間、進度
|
||||
- `principles.md`(如果有):本專案跨全局的設計原則——整理時必須服從
|
||||
|
||||
目的:**知道哪些已整理過,只處理新增或有變動的 raw source**,不重複整理。
|
||||
|
||||
---
|
||||
|
||||
## 第四步:整理規則
|
||||
|
||||
### 核心判準:push vs pull(wiki 是給 AI 看的)
|
||||
|
||||
整理任何內容前,先判斷它該進 **push 檔** 還 **cards(pull)**——判準是「**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「知識互連」的強化版——連結不只存在,還帶類型與方向。
|
||||
|
||||
### 萃 gloss(node 一句說明,供下游語義 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:MM|cowork-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 的目標)
|
||||
Reference in New Issue
Block a user