feat(harness): 用戶 CC harness(acr install-harness)+ 公開 repo 只留對外

讓「用 arcrun 開發」的用戶,他的 CC 自動載入 arcrun 防護、不退回自寫 Python。

新增 user-cc-harness(SDD: .agents/specs/user-cc-harness,本機):
- acr install-harness:冪等裝進用戶當前專案(新/舊專案皆可),acr init 末尾也順便裝
  - CLAUDE.md arcrun 區塊(標記包夾,append 不破壞既有)
  - .claude/skills/arcrun-mindset(世界觀 + 資源去哪取 acr parts/auth-recipe)
  - .claude/commands/arcrun.md(/arcrun slash command)
  - .claude/hooks/arcrun-guard.sh(python→提醒不硬擋、暴露→exit 2、每條含正路)
  - settings.json 合併 hook(不覆蓋用戶既有 hooks/設定)
- llms.txt + README「給 AI」段:第一接觸點(用戶丟連結,CC 讀了知道第一步 install-harness)
  含 CF 憑證白話照抄式引導(不對用戶講 KV/Worker/R2 術語)
- harness 素材內嵌 npm 套件(cli/harness/,files 帶上),不依賴用戶有 arcrun repo
- 實測:空目錄/冪等/既有檔合併皆通過,tsc exit 0,npm pack 含 harness 5 檔

公開 repo 清理(richblack:用戶要用不要開發 arcrun):
- git rm --cached 移除開發痕跡 + 思考過程出公開 repo(本機保留供 richblack 開發):
  .claude/CLAUDE.md/AGENTS.md/.agents/docs/DECISIONS/BACKLOG/landing/.github
- .gitignore 防回流;補 MIT LICENSE

MCP(P7)納入 install-harness/update 的接點已設計,實作待 MCP 對齊(BACKLOG 另一條線)。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
uncle6me-web
2026-06-03 20:02:44 +08:00
parent 922a57fe34
commit 037cf9b6a4
139 changed files with 442 additions and 30768 deletions
-611
View File
@@ -1,611 +0,0 @@
# Design Document: arcrun MVP
## Overview
arcrun MVP 的核心設計原則是**最小異動、最快可用**。所有目標都能透過以下三個操作達成:
1. **Cherry-pick**:從 `matrix` 搬移指定目錄,不重寫
2. **Carve-out**:移除 cypher-executor 中與 InkStone 耦合的程式碼路徑
3. **Supplement**:補充 contract.yaml 缺少的欄位、新增 CLI
不建立新的抽象層,不改變現有零件邏輯,只做讓開源可用所需的最小改動。
---
## Architecture
### 目標 Repo 結構
```
arcrun/(新獨立開源 repo
├── README.md
├── CONTRIBUTING.md
├── cypher-executor/
│ ├── src/
│ │ ├── index.ts
│ │ ├── types.ts
│ │ ├── graph-executor.ts
│ │ ├── lib/
│ │ │ ├── component-loader.ts ← 改:只從 WASM_BUCKET 讀,移除 KBDB/REGISTRY 邏輯
│ │ │ ├── component-dispatcher.ts
│ │ │ ├── wasm-executor.ts
│ │ │ ├── wasi-shim.ts
│ │ │ └── constants.ts ← 改:移除 MINI_ME / KBDB 特殊零件 hardcode
│ │ └── actions/
│ │ ├── triplet-parser.ts
│ │ ├── graph-builder.ts
│ │ ├── execution-evaluator.ts
│ │ ├── execution-logger.ts
│ │ ├── webhook-handlers.ts
│ │ ├── webhook-graph-resolver.ts ← 改:加入 credential 注入邏輯
│ │ └── (移除 autoPublishMissing.ts)
│ └── wrangler.toml ← 改:移除 9 個 InkStone bindings,新增 CREDENTIALS_KV
├── credentials/ ← 直接搬移,無需修改
│ └── src/...
├── builtins/ ← 直接搬移,無需修改
│ └── src/...
└── registry/
└── components/ ← 搬移後補充 contract.yaml
├── gmail/
├── google_sheets/
├── telegram/
├── line_notify/
├── ... (其餘 17 個零件)
└── cli/ ← 新增:arcrun CLI
├── package.json
├── tsconfig.json
└── src/
├── index.ts
├── commands/
│ ├── init.ts
│ ├── creds.ts
│ ├── push.ts
│ ├── run.ts
│ ├── validate.ts
│ ├── parts.ts
│ ├── list.ts
│ └── logs.ts
└── lib/
├── config.ts # 讀寫 ~/.arcrun/config.yaml
├── cf-api.ts # Cloudflare KV / R2 HTTP API wrapper
└── yaml-parser.ts # workflow.yaml 解析與三元組轉換
```
---
## Component Loader 改造(關鍵變更)
### 現況(matrix 版)
```typescript
// component-loader.ts 現有四層優先序:
// 1. 特殊零件 hardcode → MINI_ME / KBDB Service Binding
// 2. 內建零件 Map → 本地純轉換
// 3. 新版:查詢 KBDB record 含 component_type → WASM 或 Service Binding
// 4. 舊版 fallback:查詢無 component_type 的 KBDB record
```
### 開源版(arcrun
```typescript
// component-loader.ts 簡化為三層:
// 1. 內建零件 Map → 本地純轉換(passthrough / counter 等,保留)
// 2. WASM_BUCKET R2 直讀 → component-name.wasm
// 3. 找不到 → 回傳結構化錯誤
async function loadComponent(componentId: string, env: Env) {
// 層 1:內建零件(無需 R2
if (BUILTIN_COMPONENTS.has(componentId)) {
return BUILTIN_COMPONENTS.get(componentId)
}
// 層 2:從 WASM_BUCKET R2 讀取
const wasmKey = `${componentId}/${componentId}.wasm`
const wasmObj = await env.WASM_BUCKET.get(wasmKey)
if (wasmObj) {
return { type: 'wasm', buffer: await wasmObj.arrayBuffer() }
}
// 層 3:找不到
throw new Error(`Component not found: ${componentId}. 請確認 ${wasmKey} 存在於 WASM_BUCKET。`)
}
```
移除:`MINI_ME``KBDB` 特殊零件的 hardcode 路徑(`comp_claude_chat``comp_kbdb_search``comp_kbdb_history`)。
---
## Credential 注入流程設計
### 執行時序
```
acr run newsletter_subscribe
cypher-executor POST /webhook/:id
webhook-graph-resolver 讀 WEBHOOKS KV → workflow 定義
graph-executor 執行節點 send_thanks
執行前:credential-injector(新增)
查 send_thanks 對應零件 canonical_id = "gmail"
讀 registry/components/gmail/component.contract.yaml
發現 credentials_required: [{key: "gmail_token", inject_as: "access_token"}]
GET CREDENTIALS_KV["gmail_token"] → AES-GCM 解密
input.access_token = decryptedToken
wasm-executor 執行 gmail.wasmstdin = 含 access_token 的完整 input
回傳結果
```
### credential-injector 實作位置
放在 `cypher-executor/src/actions/credential-injector.ts`(新增),由 `graph-executor.ts` 在每個節點執行前呼叫。
```typescript
async function injectCredentials(
componentId: string,
input: Record<string, unknown>,
env: Env
): Promise<Record<string, unknown>> {
const contract = await loadContract(componentId) // 從 WASM_BUCKET 或本地讀取
if (!contract.credentials_required) return input
const enriched = { ...input }
for (const cred of contract.credentials_required) {
const record = await env.CREDENTIALS_KV.get(cred.key)
if (!record) {
throw new Error(
`缺少 credential: ${cred.key}\n修復:在 credentials.yaml 加入 ${cred.key} 後執行 acr creds push`
)
}
const { encrypted, iv } = JSON.parse(record)
enriched[cred.inject_as] = await decrypt(encrypted, iv, env.ENCRYPTION_KEY)
}
return enriched
}
```
---
## workflow.yaml 解析設計
### CLI push 流程
```
acr push newsletter_subscribe.yaml
yaml-parser.ts 讀取 workflow.yaml
解析 flow[] 三元組 → triplets: [{subject, relation, object}]
驗證關係詞(拒絕 PIPE
POST cypher-executor /cypher/search → ExecutionGraph(節點 + 邊)
合併 config: + ExecutionGraph → WorkflowDefinition
PUT WEBHOOKS KV[workflow_name] = JSON.stringify(WorkflowDefinition)
輸出 webhook URL
```
### workflow.yaml 格式(確認版)
```yaml
name: newsletter_subscribe
description: 訂閱電子報,發感謝信並記錄到 GSheets
flow:
- "input >> 完成後 >> send_thanks"
- "input >> 完成後 >> save_to_sheet"
- "send_thanks >> 完成後 >> output"
- "send_thanks >> 失敗時 >> notify_error"
- "save_to_sheet >> 完成後 >> output"
config:
send_thanks:
to: "{{input.email}}"
subject: "感謝訂閱!"
body: "歡迎加入!"
# access_token 由 credentials.yaml 的 gmail_token 自動注入
save_to_sheet:
action: write
spreadsheet_id: "{{creds.sheet_id}}"
range: "訂閱者!A:B"
values: [["{{input.email}}", "{{input.timestamp}}"]]
notify_error:
chat_id: "{{creds.telegram_chat_id}}"
text: "發信失敗:{{input.email}}"
```
---
## contract.yaml 補充欄位格式
### credentials_requiredgmail 範例)
```yaml
credentials_required:
- key: gmail_token
type: google_oauth
description: "Google OAuth access tokengmail.send scope"
inject_as: access_token
```
### config_examplegmail 範例)
```yaml
config_example: |
send_email: # 節點名稱(可自訂)
to: "" # 收件人 Email(必填)
subject: "" # 主旨(必填)
body: "" # 內文(必填)
# access_token 由 credentials.yaml 的 gmail_token 自動注入
```
### 各零件 credentials_required 對照表
| 零件 | key | type | inject_as |
|------|-----|------|-----------|
| gmail | gmail_token | google_oauth | access_token |
| google_sheets | google_oauth | google_oauth | access_token |
| telegram | telegram_bot_token | telegram_bot_token | bot_token |
| line_notify | line_token | line_token | token |
---
## CLI 技術設計
### 依賴
```json
{
"dependencies": {
"commander": "^12.0.0",
"js-yaml": "^4.1.0",
"chalk": "^5.3.0",
"ora": "^8.0.1"
}
}
```
### config.yaml 格式(~/.arcrun/config.yaml
```yaml
cloudflare_account_id: abc123
webhooks_kv_id: xyz789
credentials_kv_id: abc456
wasm_bucket: arcrun-wasm
cypher_executor_url: https://cypher-executor.xxx.workers.dev
credentials_worker_url: https://arcrun-credentials.xxx.workers.dev
api_token: ***(加密存本機)
```
### Cloudflare API 操作
CLI 使用 Cloudflare REST API(不依賴 Wrangler CLI):
- KV 寫入:`PUT /client/v4/accounts/{id}/storage/kv/namespaces/{ns_id}/values/{key}`
- KV 讀取:`GET /client/v4/accounts/{id}/storage/kv/namespaces/{ns_id}/values/{key}`
- KV 列出:`GET /client/v4/accounts/{id}/storage/kv/namespaces/{ns_id}/keys`
---
## wrangler.toml 變更對照
### 移除(InkStone 專屬)
```toml
# 全部移除:
[[services]]
binding = "KBDB"
service = "inkstone-kbdb-api"
[[services]]
binding = "REGISTRY"
service = "inkstone-component-registry"
[[services]]
binding = "CLINIC_GDRIVE"
service = "clinic-gdrive"
# ... CLINIC_EXCEL, CLINIC_ANALYSIS, CLINIC_RENDER, CLINIC_GSHEETS
[[services]]
binding = "AICEO"
service = "inkstone-aiceo-bot"
[[services]]
binding = "MINI_ME"
service = "inkstone-mini-me"
```
### 保留
```toml
[[kv_namespaces]]
binding = "EXEC_CONTEXT"
[[kv_namespaces]]
binding = "WEBHOOKS"
[[r2_buckets]]
binding = "WASM_BUCKET"
[ai]
binding = "AI"
```
### 新增
```toml
[[kv_namespaces]]
binding = "CREDENTIALS_KV"
id = "" # 用戶自行填入
```
---
## Standard 模式架構(用戶自己的 KVarcrun.dev 的引擎)
### 儲存責任分界
```
arcrun.dev 負責:
WASM_BUCKET 公眾零件庫(.wasm 二進位)
ANALYTICS_KV 零件執行統計
ACCOUNTS_KV API Key → tenant_id + CF API Token 對應
SUBMISSIONS_KV 零件提交審核狀態
用戶自己負責(一個 CF KV,arcrun.dev 不存取明文):
USER_KV
workflow:{name} → workflow 執行圖(JSON
cred:{key} → AES-GCM 加密 credential
```
### 完整系統圖
```
┌──────────────────────────────────────────────────────────────┐
│ arcrun.dev(你的 Cloudflare 帳號) │
│ │
│ auth-workerapi.arcrun.dev
│ POST /register → { api_key, tenant_id } │
│ ACCOUNTS_KV: { tenant_id, cf_api_token, api_key_hash } │
│ ※ 不儲存用戶 credential 或 workflow 內容 │
│ │
│ cypher-executorcypher.arcrun.dev,共享) │
│ X-Arcrun-API-Key → tenant_id → cf_api_token │
│ 用 cf_api_token 呼叫 CF KV API → 讀用戶自己的 USER_KV │
│ WASM_BUCKET: gmail.wasm / telegram.wasm / ...(共享) │
│ │
│ public registryregistry.arcrun.dev
│ GET /components → 零件清單 + 統計 + author + visibility │
│ POST /submit → 接收零件,沙盒驗收後設 author_only │
│ POST /analytics/record → 執行統計(非同步) │
└──────────────────────────────────────────────────────────────┘
↕ CF KV API(用戶的 cf_api_tokenKV Edit 權限)
┌──────────────────────────────────────────────────────────────┐
│ 用戶自己的 CF 帳號 │
│ USER_KV │
│ workflow:newsletter → { triplets, config } │
│ cred:gmail_token → { encrypted, iv } │
│ cred:telegram_bot → { encrypted, iv } │
└──────────────────────────────────────────────────────────────┘
```
### acr init 互動流程
```
$ acr init
? 你的 Cloudflare Account ID: abc123
? USER_KV Namespace ID(在 CF Dashboard 建立一個 KV 後貼上): kv_xyz
? CF API Token(只需 KV Edit 權限,arcrun 用此存取你的 KV: ***
? Email(取得 arcrun.dev API Key: you@example.com
→ 呼叫 POST https://api.arcrun.dev/register { email, cf_api_token_hash }
→ 取得 api_key: ak_xxxxxxxx
✓ 設定完成 → ~/.arcrun/config.yaml
✓ 建立 credentials.yaml(已加入 .gitignore
你的 credential 與 workflow 存在你自己的 CF KVarcrun 不會儲存它們。
```
### API Key 驗證與 KV 存取 Middleware
```typescript
// cypher-executor/src/lib/tenant.ts(新增)
export async function resolveUserKv(request: Request, env: Env) {
if (env.MULTI_TENANT === 'false') {
// Self-hosted:直接用本地 KV binding
return { kv: env.LOCAL_KV, prefix: '' }
}
const apiKey = request.headers.get('X-Arcrun-API-Key')
if (!apiKey) throw new Response('Missing API Key', { status: 401 })
const hash = await sha256(apiKey)
const account = await env.ACCOUNTS_KV.get(`hash:${hash}`)
if (!account) throw new Response('Invalid API Key', { status: 401 })
const { cf_api_token, account_id, kv_namespace_id } = JSON.parse(account)
// 回傳 CF KV API wrapper,用用戶自己的 token 存取
return {
kv: new CfKvClient({ cf_api_token, account_id, kv_namespace_id }),
prefix: ''
}
}
```
### USER_KV Key Schema
```
Standard 模式(用戶自己的 KV):
workflow:{name} → WorkflowDefinition JSON
cred:{key} → { encrypted (base64), iv (base64) }
Self-hosted(本地 KV binding):
維持現有 key 格式,無 prefix
```
---
## 執行統計設計
### Analytics Record(非同步,不阻擋執行)
```typescript
// cypher-executor/src/actions/analytics.ts(新增)
export function recordExecution(
componentId: string,
version: string,
success: boolean,
durationMs: number
): void {
// fire-and-forget,不 await
fetch('https://registry.arcrun.dev/analytics/record', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ canonical_id: componentId, version, success, duration_ms: durationMs })
}).catch(() => {}) // 統計失敗不影響執行
}
```
### 統計聚合(registry Worker
```
ANALYTICS_KV 結構:
"stats:gmail:v1" → { total_runs: 140382, success_runs: 139444, total_ms: 16845840 }
每次 POST /analytics/record
原子更新(KV 樂觀鎖)→ total_runs++, success_runs += success, total_ms += duration_ms
GET /components 回傳:
success_rate = success_runs / total_runs * 100
avg_duration_ms = total_ms / total_runs
排序:total_runs × success_rateDESC
```
---
## 零件貢獻流程設計
### `acr parts publish` 流程
```
$ acr parts publish gmail-v2
1. CLI 讀取 registry/components/gmail-v2/ 目錄
- component.contract.yaml(必須有 author 欄位)
- main.go
- gmail-v2.wasm
2. POST https://registry.arcrun.dev/submit
multipart form:
contract: <yaml content>
source: <main.go content>
wasm: <binary>
Header: X-Arcrun-API-Key: ak_xxxxx
3. registry 回應:
{ submission_id: "sub_abc123", status: "pending_review" }
4. CLI 輸出:
✓ 提交成功(submission_id: sub_abc123
查詢進度:acr parts publish --status sub_abc123
```
### Registry 沙盒驗收與 visibility 狀態機
```
POST /submit 觸發(非同步執行):
[整合類零件:gmail、telegram、google_sheets、line_notify、http_request]
Step 1: 體積檢查(< 2048KB
Step 2: syscall 掃描(無 filesystem syscall;網路 syscall 允許,因需呼叫外部 API)
通過 → visibility: author_only(作者立即可用,等人工審核)
[功能類零件:所有其他零件]
Step 1: 體積檢查(< 2048KB
Step 2: 冷啟動時間(< 50ms
Step 3: syscall 掃描(無網路 / 無 filesystem
Step 4: Gherkin 測試(contract 中所有 scenario 100% 通過)
通過 → visibility: author_only(作者立即可用,等人工審核)
任一步驟失敗 → status: rejected(回傳 failed_step + reason
人工審核通過 → visibility: public
- 零件出現在所有人的 GET /components
- 開始累積公開執行統計
人工審核拒絕 → visibility 維持 author_only
- 作者仍可使用,但收到拒絕原因
- 作者修改後可重新提交
acr parts 顯示規則:
visibility: author_only → [待審核] 只有你可用(不顯示統計)
visibility: public → ★ 成功率 | N 次執行 | by @author
任一失敗 → status: rejected
- 回傳 { failed_step, reason },格式與 Requirement 2 相同
```
---
## 開發順序(Phase 對齊 requirements
```
Phase 1:搬移與清理(Requirement 1
1.1 建立 arcrun repo,搬移四個目錄
1.2 清理 cypher-executor/wrangler.toml
1.3 改寫 component-loader(移除 KBDB/REGISTRY/MINI_ME 路徑)
1.4 移除 autoPublishMissing.ts(依賴 REGISTRY binding
1.5 本機 wrangler dev 測試 /health
Phase 2:零件完整度(Requirement 2
2.1 審查 21 個零件 contract.yaml(表格回報)
2.2 補充 credentials_required4 個零件)
2.3 補充 config_example(全部 21 個)
2.4 驗證 main.go required 與 contract 一致
Phase 3credential 注入(Requirement 3
3.1 新增 credential-injector.ts
3.2 整合進 graph-executor 節點執行前
3.3 測試 gmail 零件端對端(credentials.yaml → push → run
Phase 4CLIRequirement 4
4.1 acr init--hosted / --self-hosted 分支)
4.2 acr creds pushHosted 走 APISelf-hosted 走 KV
4.3 acr push
4.4 arcrun run
4.5 acr validate
4.6 acr parts / acr parts scaffold / acr parts publish
4.7 acr list / acr logs
Phase 5:開源發布(Requirement 5
5.1 撰寫 README.md(含 --hosted 快速開始)
5.2 撰寫 CONTRIBUTING.md
5.3 確認無 InkStone 內部資訊殘留
5.4 GitHub 發布 + npm publish
Phase 6Hosted SaaSRequirement 6
6.1 建立 auth-workerapi.arcrun.dev
6.2 cypher-executor 加入 tenant middleware
6.3 CREDENTIALS_KV key schema 加 tenant prefix
6.4 部署至 arcrun.dev
Phase 7:統計與貢獻(Requirement 7 + 8
7.1 analytics.ts(執行後 fire-and-forget
7.2 registry /analytics/record 端點
7.3 ANALYTICS_KV 聚合邏輯
7.4 GET /components 加入統計排序
7.5 POST /submit 沙盒驗收 + author 寫入
7.6 acr parts publish 指令
```
@@ -1,180 +0,0 @@
# Requirements Document
## Introduction
arcrun MVP 是從 `matrix` monorepo 中 cherry-pick 出最小可獨立運作的 AI 工作流執行引擎,目標是作為**獨立開源 repo**(`arcrun`)發布。
**背景**`matrix` 因為同時承載 InkStone 內部服務(KBDB、CLINIC_*、AICEO、MINI_ME 等)與核心執行引擎,複雜度過高,難以讓外部開發者使用或貢獻。MVP 的任務是將執行引擎從內部服務中解耦,讓任何人都能自行在 Cloudflare 上部署一套完整的 AI 工作流系統。
**護城河邏輯**
- 開源:cypher-executor(執行引擎)、WASM 零件庫(21 個)、credentials Worker、CLI
- Hosted SaaS:一行指令註冊取得 API Key,直接使用公眾零件庫,無需部署任何 Worker
- 閉源(InkStone 付費):KBDB 向量搜尋、graph 查詢、Persona SDK、MatchGPT
**不在此次範圍**:KBDB 整合、前端管理介面、向量搜尋、新增 WASM 零件。
---
## Glossary
- **cypher-executor**:原 `matrix/cypher-executor`,執行 workflow 的 Cloudflare Worker。開源版移除所有 InkStone 內部 Service Binding,只保留 KV / R2 / Workers AI。
- **component(零件)**:以 TinyGo 編譯的 `.wasm` 檔案,以 WASI preview1 / stdin-stdout JSON 為 I/O 模型。
- **component.contract.yaml**:每個零件的規格宣告,含 `canonical_id``input_schema``output_schema``gherkin_tests`,開源版補充 `credentials_required``config_example`
- **credentials Worker**`arcrun/credentials`,以 AES-GCM 加密存取 API token,部署在用戶自己的 CF 帳號。
- **WASM_BUCKET**arcrun.dev 的 R2 bucket,儲存所有公眾 `.wasm` 零件二進位,由 Arcrun 負責。
- **USER_KV**:用戶自己 CF 帳號下的 KV Namespace,同時存放 workflow YAML 與加密 credential,由用戶負責,Arcrun 不經手。
- **workflow.yaml**:用戶撰寫的工作流定義,`flow:``>>` 三元組描述,`config:` 對應各節點參數,存在用戶自己的 USER_KV。
- **CLI(套件名 arcrun,指令 acr**Node.js/TypeScript CLI 工具,管理 credentials、workflow 的上傳與執行。安裝:`npm i -g arcrun`,使用:`acr <指令>`
- **credentials_required**contract.yaml 新欄位,宣告零件需要哪個 credential 以及注入到哪個 input 欄位。
- **config_example**contract.yaml 新欄位,提供 `acr parts scaffold` 指令使用的 config 範本。
- **Standard 模式(預設)**:用戶只需在自己 CF 帳號開一個 KV(存 credential + workflow),使用 arcrun.dev 的執行引擎與公眾零件庫,無需部署任何 Worker。
- **Self-hosted 模式**:用戶自行部署全套 Worker 至自己的 Cloudflare 帳號,有完全控制權,可貢獻零件至公眾庫。
- **auth-worker**arcrun.dev 上的帳號服務 Worker,處理 `POST /register` 自動發放 API Key,不儲存用戶 credential。
- **tenant_id**:每個 API Key 對應的租戶識別碼,用於讓 cypher-executor 知道要用哪個 Cloudflare API Token 去存取用戶的 USER_KV。
- **public registry**arcrun.dev 上的公眾零件庫,所有人共用,有執行統計與 author 資訊。
- **`acr parts publish`**:CLI 指令,自架用戶將自製零件提交至公眾 registry 審核。
- **execution analytics**:每次零件執行後非同步記錄的統計資料(使用次數、成功率),公開顯示於 `acr parts`
- **visibility**contract.yaml 欄位,值為 `author_only`(沙盒通過後作者立即可用)或 `public`(人工審核後所有人可用)。
---
## Requirements
### Requirement 1:搬移 cypher-executor 至獨立 repo 並移除 InkStone bindings
**User Story:** As a 開源用戶, I want 自行部署 cypher-executor 至我的 Cloudflare 帳號, so that 我不需要依賴 InkStone 的任何服務就能執行 AI 工作流。
#### Acceptance Criteria
1. THE cypher-executor `wrangler.toml` SHALL 移除以下 Service Bindings`KBDB``REGISTRY``CLINIC_GDRIVE``CLINIC_EXCEL``CLINIC_ANALYSIS``CLINIC_RENDER``CLINIC_GSHEETS``AICEO``MINI_ME`
2. THE cypher-executor `wrangler.toml` SHALL 保留以下 bindings`EXEC_CONTEXT`KV)、`WEBHOOKS`KV)、`WASM_BUCKET`R2)、`AI`Workers AI)。
3. THE cypher-executor `wrangler.toml` SHALL 新增 `CREDENTIALS_KV`KV Namespace binding),用於 credential 解密注入。
4. THE component-loader SHALL 從 `WASM_BUCKET` R2 直接讀取 `.wasm` 檔案,不透過任何 Service Binding 或外部 HTTP 查詢。
5. WHEN cypher-executor 收到執行請求,THE cypher-executor SHALL 不依賴 `KBDB``REGISTRY` 或任何 InkStone 內部 Service Binding,只使用 KV / R2 / Workers AI 完成執行。
6. THE arcrun repo SHALL 包含以下目錄:`cypher-executor/``credentials/``builtins/``registry/components/`21 個零件)。
---
### Requirement 2component.contract.yaml 完整度補充
**User Story:** As a 零件使用者, I want 每個零件的 contract.yaml 都有 `credentials_required``config_example`, so that CLI 能自動注入 credential,用戶也能快速知道如何設定節點。
#### Acceptance Criteria
1. THE `credentials_required` 欄位 SHALL 出現在以下 4 個零件的 contract.yaml 中:`gmail``google_sheets``telegram``line_notify`
2. WHEN `credentials_required` 存在,THE 欄位 SHALL 包含以下子欄位:`key`(對應 credentials.yaml 的 key 名稱)、`type`token 類型,如 `google_oauth``telegram_bot_token`)、`description`(說明)、`inject_as`(執行時注入到 input 的哪個欄位名稱)。
3. THE `config_example` 欄位 SHALL 出現在所有 21 個零件的 contract.yaml 中。
4. WHEN `config_example` 存在,THE 欄位 SHALL 為 YAML 字串,內容為可直接貼入 workflow.yaml `config:` 區塊的範本,需有人類可讀的說明註解。
5. FOR 需要 credential 的零件,THE `config_example` SHALL 包含一行註解,說明哪個 credential key 會被自動注入到哪個欄位(如 `# access_token 由 credentials.yaml 的 gmail_token 自動注入`)。
6. THE main.go 的 `required` 欄位 SHALL 與 contract 的 `input_schema.required[]` 保持一致,不得有欄位名稱不符。
---
### Requirement 3workflow YAML 格式與執行時 credential 注入
**User Story:** As a 工作流設計者, I want 用有語意的關係詞撰寫 workflow.yaml,且 credential 自動注入, so that workflow 定義中完全不出現明文 token。
#### Acceptance Criteria
1. THE workflow.yaml `flow:` 欄位 SHALL 以 `"A >> 關係詞 >> B"` 三元組陣列描述資料流。
2. THE cypher-executor SHALL 支援以下關係詞:`完成後``失敗時``對每個``條件滿足時``ON_SUCCESS``ON_FAIL``FOREACH``IF``ON_CLICK``CALLS_SUBFLOW`
3. THE cypher-executor SHALL 拒絕使用 `PIPE` 關係詞,並回傳明確錯誤訊息。
4. WHEN cypher-executor 執行一個節點,THE cypher-executor SHALL 查詢該節點對應零件的 `credentials_required`,若存在則從 `CREDENTIALS_KV` 解密對應 credential,並注入到 input 的 `inject_as` 欄位。
5. THE credential 注入 SHALL 發生在 WASM 執行前,用戶的 workflow `config:` 中不需也不應包含 token 值。
6. IF `credentials_required` 宣告的 credential key 在 `CREDENTIALS_KV` 中不存在,THE cypher-executor SHALL 回傳結構化錯誤,包含缺少的 key 名稱與修復步驟說明。
---
### Requirement 4CLIarcrun,指令 acr)核心指令
**User Story:** As a 開發者, I want 透過 `acr` CLI 管理 workflow 與 credentials, so that 不需要直接操作 Cloudflare KV / R2 API 就能完成部署與執行。
#### Acceptance Criteria
1. THE CLI SHALL 以 Node.js/TypeScript 實作,套件名 `arcrun`bin 名 `acr`,可透過 `npm i -g arcrun` 安裝,依賴只使用 `commander``js-yaml``chalk``ora`
2. THE `acr init` 指令 SHALL 以互動式問答產生 `~/.arcrun/config.yaml`,問答內容為:CF Account ID、USER_KV namespace ID、CF API Token(用於 cypher-executor 代存取用戶 KV)、email(取得 arcrun.dev API Key);並建立空白本機 `credentials.yaml`
3. THE `acr creds push [credentials.yaml]` 指令 SHALL 讀取 credentials.yaml,逐一加密上傳至用戶自己的 USER_KV,並顯示每個 key 的上傳結果。
4. THE `acr push <workflow.yaml>` 指令 SHALL 解析 `flow:` 三元組,轉換成執行圖,連同 `config:` 存入 `WEBHOOKS KV`,並輸出 webhook URL。
5. THE `acr run <workflow_name> [--input key=value...]` 指令 SHALL 觸發 cypher-executor 執行指定 workflow,顯示各節點執行結果;失敗時顯示具體節點、原因與修復步驟。
6. THE `acr validate <workflow.yaml>` 指令 SHALL 在執行前驗證:YAML 格式、關係詞合法性(無 PIPE)、所有節點在 config 中有對應、所有零件存在於 WASM_BUCKET、所有 credentials 已上傳至 CREDENTIALS_KV。
7. THE `acr parts` 指令 SHALL 列出所有可用零件(按類型分組),顯示每個零件的必填欄位與所需 credential。
8. THE `acr parts scaffold <component>` 指令 SHALL 從 contract 的 `config_example` 輸出可直接貼入 workflow.yaml 的 config 範本,以及對應的 credentials.yaml 欄位範本。
9. THE `acr list` 指令 SHALL 列出 WEBHOOKS KV 中所有已上傳的 workflow,顯示名稱與更新時間。
10. THE `acr logs <workflow_name>` 指令 SHALL 顯示最近執行記錄,包含時間、成功/失敗狀態、執行時間,失敗時顯示失敗節點與原因。
---
### Requirement 5README 與開源發布準備
**User Story:** As a 外部開發者, I want 看到清楚的 README5 分鐘內能完成部署, so that 降低試用門檻,吸引社群貢獻。
#### Acceptance Criteria
1. THE README.md SHALL 包含以下章節:專案定位(開源核心 vs 閉源付費服務說明)、快速開始(`acr init``acr creds push``acr push``acr run` 四步驟)、零件列表(21 個零件分類說明)、workflow YAML 語法說明(三元組 + 關係詞表格)、自行部署說明(Cloudflare Workers 部署步驟)。
2. THE README.md 快速開始 SHALL 以 `newsletter_subscribe` 為範例 workflow,展示 gmail + google_sheets + telegram 的完整串接。
3. THE repo SHALL 包含 `CONTRIBUTING.md`,說明如何新增零件(TinyGo 開發環境、contract.yaml 格式、本機測試指令)。
4. THE repo SHALL 確保所有 InkStone 內部資訊(Worker URL、KV namespace ID、帳號資訊)不出現在任何已提交的檔案中。
5. WHEN cypher-executor 部署後第一次被呼叫,THE cypher-executor SHALL 能正常回應 health check`GET /health` 回傳 `{ ok: true }`),不需要任何 InkStone 服務可用。
---
### Requirement 6Standard 模式 — API Key 註冊與用戶 KV 存取
**User Story:** As a 新用戶, I want 只需開一個 CF KV 就能開始使用 Arcrun,不需要部署任何 Worker, so that 最低門檻試用整個平台,且我的 credential 永遠在我自己的環境。
#### Acceptance Criteria
1. THE auth-worker SHALL 提供 `POST /register` 端點,接受 `{ email }` 後自動生成 API Key(格式:`ak_` 前綴 + 32 字元隨機字串),無需人工審核,立即回傳 `{ api_key, tenant_id }`
2. THE auth-worker SHALL 將 `{ tenant_id, email, created_at, api_key_hash }` 存入 `ACCOUNTS_KV`,只存 hash 不存明文 API Key。arcrun.dev 不儲存任何用戶 credential 或 workflow 內容。
3. WHEN `acr init` 執行,THE CLI SHALL 互動式詢問以下資料並寫入 `~/.arcrun/config.yaml`
- CF Account ID(用戶自己的)
- USER_KV namespace ID(用戶自己開的,存 credential + workflow
- CF API Token(供 cypher-executor 用 CF API 存取用戶 KV,只需 KV Edit 權限)
- email(呼叫 `POST https://api.arcrun.dev/register` 取得 API Key
4. THE cypher-executor SHALL 在每個 request 的 header 讀取 `X-Arcrun-API-Key`,驗證後取得該 tenant 的 CF API Token,用 Cloudflare API 存取用戶自己的 USER_KV;缺少或無效的 API Key 回傳 `401 Unauthorized`
5. THE `acr creds push` 指令 SHALL 使用用戶的 CF API Token,直接呼叫 Cloudflare KV API 將加密 credential 寫入用戶自己的 USER_KV,不經過 arcrun.dev。
6. THE `acr push <workflow.yaml>` 指令 SHALL 同樣直接寫入用戶自己的 USER_KV,不經過 arcrun.dev。
7. WHEN Self-hosted 模式,THE cypher-executor SHALL 可透過環境變數 `MULTI_TENANT=false` 停用 API Key 驗證,直接使用本地 KV binding,與現有行為相容。
---
### Requirement 7:公眾零件庫執行統計與貢獻榮譽
**User Story:** As a 零件使用者, I want 在 `acr parts` 看到每個零件的真實執行統計與作者資訊, so that 我能選擇最可靠的零件;As a 零件貢獻者, I want 我的名字和統計數字公開顯示, so that 我有動機將好零件推入公眾庫而非留在私庫。
#### Acceptance Criteria
1. THE contract.yaml SHALL 新增可選欄位 `author`GitHub username,如 `@alice`),在 `acr parts` 顯示時一起展示。
2. WHEN cypher-executor 執行完一個零件節點,THE cypher-executor SHALL 非同步 POST 以下資料至 `https://registry.arcrun.dev/analytics/record`,不阻擋主流程:
```json
{ "canonical_id": "gmail", "version": "v1", "success": true, "duration_ms": 120 }
```
不含任何用戶資料或 tenant_id。
3. THE public registry SHALL 聚合每個零件的執行統計:`total_runs`(總執行次數)、`success_rate`(成功率,百分比)、`avg_duration_ms`(平均執行時間)。
4. THE `acr parts` 指令 SHALL 顯示每個零件的統計資料,格式為:
```
• gmail Gmail 發信 by @alice
★ 99.2% 成功 | 140,382 次執行 | 平均 120ms
```
5. IF 零件存在於用戶自架的私有 WASM_BUCKET 而非公眾庫,THE `acr parts` SHALL 顯示該零件但標註 `[私有]`,不顯示統計數字與 author。
6. THE public registry SHALL 在 `GET /components` 回傳的零件清單中,依 `total_runs × success_rate` 排序,讓高品質高使用量的零件排在前面。
---
### Requirement 8:零件貢獻流程與 visibility 狀態
**User Story:** As a 零件開發者, I want 提交零件後立即能自己使用,等審核通過後公開給所有人, so that 不用等待審核就能驗證自己的零件是否有用。
#### Acceptance Criteria
1. THE contract.yaml SHALL 包含 `visibility` 欄位,值為 `author_only`(沙盒通過後作者立即可用)或 `public`(人工審核通過後所有人可用)。
2. THE `acr parts publish <component>` 指令 SHALL 打包指定零件的原始碼、`component.contract.yaml`、`.wasm`POST 至 `https://registry.arcrun.dev/submit`(帶 `X-Arcrun-API-Key` header)。原始碼語言不限,但編譯產出必須為 WASM + WASI preview1。
3. WHEN 零件提交後,THE registry SHALL 依零件類型執行不同層級的沙盒驗收:
- **整合類**(需呼叫外部 API,如 gmail、telegram):體積 / syscall 掃描通過 → `author_only`
- **功能類**(純邏輯,如 string_ops、if_control):體積 / syscall 掃描 / Gherkin 測試全通過 → `author_only`
- 任一必要步驟失敗 → `rejected`(回傳具體失敗步驟與原因)
4. WHEN 零件 visibility 為 `author_only`THE registry SHALL 讓該零件只對提交者的 API Key 可見,`acr parts` 顯示時標註 `[待審核]`,其他用戶看不到。
5. WHEN 人工審核通過,THE registry SHALL 將 visibility 改為 `public`,零件立即出現在所有人的 `acr parts` 清單,並開始累積公開執行統計。
6. WHEN 審核拒絕,THE registry SHALL 回傳具體失敗原因,零件保留 `author_only` 狀態讓作者繼續修改後重新提交。
7. THE `acr parts publish` 指令 SHALL 在提交後顯示 `submission_id`、目前 visibility 狀態,以及查詢審核進度的指令提示。
8. THE `acr parts` 指令 SHALL 對 `author_only` 零件顯示「[待審核] 只有你可用」,對 `public` 零件顯示執行統計與 author,讓貢獻者清楚知道零件的可用範圍。
-206
View File
@@ -1,206 +0,0 @@
# Implementation Plan: arcrun MVP
## Overview
依照 Design 的七個 Phase 實作。原則:最小異動,不重寫現有邏輯,只 cherry-pick + carve-out + supplement。
所有 Phase 13 工作在 `matrix` repo 對應目錄驗證後再搬到新 repo。
**PR #2claude/review-mvp-specs-8Bvdu)狀態:** 初始實作已提交,已修復以下問題後準備 merge:
- CF API Token 傳至 arcrun.dev 安全問題(已修復)
- 加密 fallback 格式不相容(已修復)
- submitComponent KBDB 依賴(已修復,改用 SUBMISSIONS_KV
- Webhook 路由缺 analytics(已修復)
- `require()` 在 ES module 中(已修復)
- api 類零件 `no_network_syscall: true` 錯誤(已修復)
---
## Phase 1:搬移與清理
- [x] 1. 建立 `arcrun` 獨立 repo 並初始化
- [x] 1.1 在 GitHub 建立新的 public repo(使用 matrix monorepo 的 `arcrun/` 子目錄代替,PR #2
- [x] 1.2 設定 `.gitignore`(排除 `node_modules/``.wrangler/``credentials.yaml``~/.arcrun/`
- [x] 1.3 從 `matrix` cherry-pick 四個目錄:
- `matrix/cypher-executor/``arcrun/cypher-executor/`
- `matrix/u6u-core/credentials/``arcrun/credentials/`
- `matrix/u6u-core/registry/components/``arcrun/registry/components/`
- _Requirements: 1.6_
- [x] 2. 清理 `cypher-executor/wrangler.toml`
- [x] 2.1 移除 9 個 InkStone Service BindingsKBDB、REGISTRY、CLINIC_*、AICEO、MINI_ME
- [x] 2.2 確認保留:`EXEC_CONTEXT``WEBHOOKS``WASM_BUCKET``AI`
- [x] 2.3 新增 `CREDENTIALS_KV``ANALYTICS_KV` KV namespace binding
- [x] 2.4 更新 `name``arcrun-cypher-executor`
- _Requirements: 1.1, 1.2, 1.3_
- [x] 3. 改寫 `cypher-executor/src/lib/component-loader.ts`
- [x] 3.1 移除對 MINI_ME、KBDB、InkStone bindings 的 hardcode
- [x] 3.2 實作三層邏輯:builtin Map → WASM_BUCKET R2 直讀 → 結構化錯誤
- _Requirements: 1.4, 1.5_
- [x] 4. 移除對 InkStone bindings 的依賴程式碼
- [x] 4.1 刪除 `autoPublishMissing.ts`(依賴 REGISTRY binding
- [x] 4.2 移除所有 `env.KBDB``env.REGISTRY``env.MINI_ME``env.AICEO``env.CLINIC_*` 引用
- _Requirements: 1.1, 1.5_
- [ ] 5. 本機驗證
- [ ] 5.1 `cd arcrun/cypher-executor && wrangler dev` 能啟動(無 binding 錯誤)
- [ ] 5.2 `GET /health` 回傳 `{ ok: true }`
- [ ] 5.3 上傳 `validate_json.wasm` 到 WASM_BUCKET,執行 `POST /execute` 能正常回傳結果
- _Requirements: 1.5, 5.5_
---
## Phase 2:零件完整度補充
- [x] 6. api 類零件 `no_network_syscall` 修正
- [x] 6.1 gmail、telegram、google_sheets、line_notify、http_request 改為 `no_network_syscall: false`
- _Requirements: 2.1_
- [ ] 7. 審查 21 個零件 contract.yaml 並補充 `credentials_required`
- [ ] 7.1 確認 gmail、google_sheets、telegram、line_notify 有 `credentials_required`PR #2 已加入,需驗證格式正確)
- [ ] 7.2 確認所有 21 個零件有 `config_example` 欄位
- [ ] 7.3 驗證 `main.go` required 欄位與 `contract.yaml` input_schema.required[] 一致
- _Requirements: 2.1, 2.2, 2.3_
---
## Phase 3Credential 注入整合
- [x] 10. `credential-injector.ts` 已實作(`arcrun/cypher-executor/src/actions/credential-injector.ts`
- [x] 10.1 讀取 contract.yaml from R2,解析 `credentials_required`
- [x] 10.2 從 `CREDENTIALS_KV` 讀取 AES-GCM 加密 token,注入到 input 對應欄位(inject_as
- [x] 10.3 credential 不存在時拋出結構化錯誤(含 key 名稱與修復步驟)
- _Requirements: 3.4, 3.5, 3.6_
- [ ] 11. 驗證 credential 注入整合進 graph-executor
- [ ] 11.1 確認 `graph-executor.ts` 在節點執行前正確呼叫 `injectCredentials`
- [ ] 11.2 確認注入只影響 WASM input,不修改 WEBHOOKS KV 中儲存的 workflow 定義
- _Requirements: 3.4, 3.5_
- [ ] 12. 端對端測試(手動)
- [ ] 12.1 建立 `credentials.yaml`,加入測試 token
- [ ] 12.2 執行 `acr creds push`,確認寫入 CREDENTIALS_KV 格式為 `{ encrypted, iv }`(無 `mode: 'base64'`
- [ ] 12.3 執行含 credential 的 workflow,確認 inject_as 欄位正確注入
- _Requirements: 3.4, 3.5_
---
## Phase 4CLI 開發
- [x] 13. CLI 專案骨架已建立(`arcrun/cli/`
- [x] 13.1 `package.json`name: `arcrun`bin: `acr`
- [x] 13.2 `tsconfig.json`module: NodeNext
- [x] 13.3 所有 10 個指令已實作骨架
- _Requirements: 4.1_
- [x] 14. `acr init` 已實作,修正項:
- [x] 14.1 Standard 模式不再傳送 `cf_api_token` 至 arcrun.dev(只傳 `email`
- [x] 14.2 `require()` 改用 `await import()` 修正 ES module 相容
- [ ] 14.3 **待補**`acr init` 需詢問 `ARCRUN_ENCRYPTION_KEY` 並寫入 config(目前加密 key 需手動設定)
- _Requirements: 4.2, 6.3_
- [x] 15. `acr creds push` 已實作
- [x] 15.1 讀取 `credentials.yaml`AES-GCM 加密後寫入用戶 CF KV`cred:{name}`
- [x] 15.2 加密 fallbackbase64)已移除,key 不足時直接拋錯提示生成指令
- _Requirements: 4.3, 6.5_
- [x] 16. `acr push` 已實作
- _Requirements: 4.4_
- [x] 17. `acr run` 已實作
- _Requirements: 4.5_
- [ ] 18. `acr validate` credential 檢測邏輯有誤,需修復
- [ ] 18.1 `extractCredentialRefs()` 目前掃描 `{{creds.xxx}}` 語法,但 injection 使用 `inject_as` key
- [ ] 18.2 改為讀取 contract.yaml 的 `credentials_required[].key`,與 `cred:{key}` KV 存在性比對
- _Requirements: 4.6_
- [x] 19. `acr parts``acr parts scaffold``acr parts publish` 已實作
- [ ] 19.1 `acr parts` 中 YAML 解析改用 `js-yaml`(目前用 regex,可能解析失敗)
- _Requirements: 4.7, 4.8_
- [x] 20. `acr list``acr logs` 已實作
- _Requirements: 4.9, 4.10_
---
## Phase 5:開源發布準備
- [x] 21. README.md 已撰寫(`arcrun/README.md`
- [x] 22. CONTRIBUTING.md 已撰寫(`arcrun/CONTRIBUTING.md`
- [ ] 23. 安全審查(PR merge 前執行)
- [ ] 23.1 搜尋 `.workers.dev` InkStone 網域
- [ ] 23.2 確認 wrangler.toml 所有 KV id 欄位留空
- [ ] 23.3 確認 `credentials.yaml``.gitignore`
- _Requirements: 5.4_
- [ ] 24. 發布(安全審查後)
- [ ] 24.1 `npm publish`CLI package `arcrun`
- _Requirements: 5.1_
---
## Phase 6Standard 模式 — auth-worker 與用戶 KV 代存取
- [ ] 25. 建立 `auth-worker`(新 Worker,部署至 `api.arcrun.dev`
- [ ] 25.1 建立 `auth-worker/` 目錄,初始化 Hono + wrangler.toml
- [ ] 25.2 實作 `POST /register`:接收 `{ email, account_id, kv_namespace_id }` + CF API Token 透過 header 傳入
- **不在 request body 中接收 CF API Token**Token 透過 header `CF-Api-Token` 傳入,減少 TLS 以外的洩漏面)
- 生成 `tenant_id``api_key`,存入 `ACCOUNTS_KV`
- [ ] 25.3 Bindings`ACCOUNTS_KV`
- _Requirements: 6.1, 6.2_
- [ ] 26. 改造 `cypher-executor` 支援 multi-tenant 用戶 KV 代存取
- [ ] 26.1 讀取 `MULTI_TENANT` env var(目前已宣告但未讀取),實作 tenant middleware
- [ ] 26.2 `X-Arcrun-API-Key` → 查 `ACCOUNTS_KV` → 取得用戶 cf_api_token + kv_namespace_id → 建立 `CfKvClient`
- [ ] 26.3 `CfKvClient` 已實作(`arcrun/cli/src/lib/cf-api.ts`),需移植到 `cypher-executor/src/lib/`
- [ ] 26.4 `credential-injector.ts` 改用 userKv 取得加密 credential
- [ ] 26.5 webhook 路由注入 userKv
- _Requirements: 6.4, 6.5, 6.6_
- [ ] 27. 端對端測試(用戶 KV 隔離)
- _Requirements: 6.4, 6.5_
---
## Phase 7:公眾零件統計與貢獻審核
- [x] 28. Analytics 基礎設施已建立
- [x] 28.1 `execution-logger.ts` 建立,`writeExecutionVerdict` 寫入 `ANALYTICS_KV`fire-and-forget
- [x] 28.2 `/execute` 路由已整合 `waitUntil(writeExecutionVerdict(...))`
- [x] 28.3 `/webhooks/:token/trigger` 路由已補上 `waitUntil(writeExecutionVerdict(...))`
- _Requirements: 7.2_
- [ ] 29. registry Worker analytics 端點
- [ ] 29.1 新增 `POST /analytics/record` 路由,原子更新 `ANALYTICS_KV`
- [ ] 29.2 `GET /components` 回傳加入 `total_runs``success_rate``avg_duration_ms`
- _Requirements: 7.3, 7.6_
- [x] 30. `author` 欄位已加入 contract.yaml 規格
- _Requirements: 7.1_
- [x] 31. 零件提交審核流程已實作(`arcrun/registry/src/actions/submitComponent.ts`
- [x] 31.1 沙盒驗收流程(sandboxAcceptance.ts):size_check + syscall_scan 已實作;cold_start + gherkin_tests 為 Phase 0 mock
- [x] 31.2 `SUBMISSIONS_KV` 儲存元數據,預設 `visibility: author_only`
- [ ] 31.3 `PATCH /submit/:id/approve` → 將 visibility 改為 `public`(待實作)
- [ ] 31.4 Gherkin 測試執行(取代 mock
- _Requirements: 8.2, 8.3, 8.4, 8.5_
---
## 待辦(無相依順序,可平行處理)
- [ ] A. `builtins/` 清理:`initComponents.ts` 仍用舊的 HTTP endpoint 模式上架零件(`buildComponentDefs` 含 URL),應改為呼叫 `POST /submit` 送 WASM binary + contract,或直接移除 builtins(功能已整合到 registry
- [ ] B. `validate` 指令 credential 檢測邏輯修復(見 Phase 4 Task 18
- [ ] C. `acr init` 加入 `ARCRUN_ENCRYPTION_KEY` 設定步驟
- [ ] D. `acr parts` YAML 解析改用 `js-yaml`
---
## Notes
- 標記 `*` 的子任務為選填,可跳過以加速 MVP 交付
- Gherkin 測試執行(sandbox 步驟 d)為 Phase 0 mockPhase 7 補充
- cold-start 測量(sandbox 步驟 b)為 Phase 0 mockPhase 2 補充
- CF API Token 永遠不離開用戶本機,arcrun.dev 只收 email + account_id + kv_namespace_id
@@ -1,822 +0,0 @@
# Design Document: u6u Platform Evolution
## Overview
u6u 平台演進的核心目標是將現有的「HTTP endpoint 零件 + 單一 Cloudflare 部署」架構,演進為「WASM 零件模型 + 三層物理部署 + 雙面翻轉畫布」的完整平台。
設計的最高原則是 **Dogfooding**:每一層都是下一層的第一個用戶。底層先建立最小可運行的能力,再用自己的方式往上蓋。這確保每個設計決策都被真實使用場景驗證,而非紙上談兵。
### Bootstrap 順序(不可跳過)
```
Phase 0:最小 WASM 執行核心
→ Component Dispatcher 能在 CF Workers 執行一個 .wasmstdin/stdout JSON
→ validate_json.wasm 作為第一個真實零件(TinyGo< 50KB,驗證整個 pipeline
→ Component Registry API/guide、/validate-contract、/components
Phase 1:遷移現有零件
→ 將 u6u-builtins 的 20 個 HTTP endpoint 逐一遷移為 .wasm
→ 遷移期間 Component Dispatcher 雙模式並存(HTTP fallback
→ 每個零件附帶 component.contract.yaml
Phase 2Cypher 語意擴展 + Multi-Tier Dispatcher
→ 支援 IS_A、ON_SUCCESS、ON_FAIL、CALLS_SUBFLOW、ON_CLICK
→ Component Dispatcher 路由層(Tier 1 CF / Tier 2 workerd / Tier 3 Wazero
Phase 3:前端畫布(用自己的 Web Components 開發)
→ 先建立 Web Components 零件庫(u6u-btn、u6u-card 等)
→ 畫布本身用這些 Web Components 組裝
→ 雙面翻轉介面
```
### 關鍵設計約束
- **KBDB 不變量**:永遠只有三張表(blocks / templates / slots),不新增表
- **API-First 鐵律**:所有跨服務通訊只透過 HTTP API,禁止相對路徑引用
- **零件 I/O 不變量**:唯一合法的 I/O 模型是 `stdin_stdout_json`
- **Tier 3 約束**:無 V8、無 Node.js、無網路,所有零件必須在 Wazero 上跑
---
## Architecture
### 系統全景圖
```mermaid
graph TB
subgraph "Tier 1 — Cloudflare Workers(雲端)"
CE[Cypher Executor<br/>GraphExecutor]
CD[Component Dispatcher<br/>路由層]
CR[Component Registry<br/>KBDB HTTP API]
KBDB[(KBDB<br/>blocks/templates/slots<br/>+ Vectorize)]
R2[(R2<br/>.wasm 二進位)]
CE --> CD
CD --> CR
CR --> KBDB
CR --> R2
end
subgraph "Tier 2 — workerd self-hosted(企業地端)"
T2D[Tier 2 Dispatcher<br/>同 wasi-shim,不同部署]
T2R[本地 Registry 快取]
T2D --> T2R
end
subgraph "Tier 3 — 邊緣載具"
T3E[Go 排程引擎]
Wazero[Wazero Runtime]
SQLite[(SQLite<br/>本地 KBDB)]
DTN[DTN 佇列]
T3E --> Wazero
T3E --> SQLite
T3E --> DTN
end
subgraph "前端"
Canvas[雙面翻轉畫布<br/>React 19 + Web Components]
WC[Web Components 零件庫<br/>u6u-btn / u6u-card / ...]
Canvas --> WC
end
CD -->|WASM 執行| Tier1WASM[.wasm 執行]
CD -->|Cypher binding| ExtSvc[外部服務<br/>MCP / n8n / 任意 URL]
CD -->|HTTP| T2D
T2D -->|Wazero IPC| Wazero
DTN -->|Burst 傳輸| T2D
Canvas -->|u6u:trigger event| CE
```
### Cypher Binding 的正確定義
**Cypher binding** 是 u6u 的核心執行機制,指「用 Cypher 三元組語法把零件串接成工作流,串接關係儲存在 KBDB,不寫死在程式碼裡」。
這個概念相對於 Cloudflare Workers 原生的 **Service Binding**(需要 deploy、串接關係寫死在 wrangler.toml)。
`cypher-executor` 就是執行 Cypher binding 的引擎。
**零件本身只有兩種 component_type**
| component_type | 說明 | 需要 deploy |
|---|---|---|
| `wasm` | 所有後端零件(內建或用戶自建),本地 WASM 執行 | 否 |
| `service_binding` | 多個零件預組合成單一高頻零件的效能最佳化(如 OAuth + GSheets 常用組合) | 是 |
> **重要:`cypher_binding` 不是 component_type。** 它是整個執行引擎的名字,描述「零件如何被串接」,而不是「零件如何被執行」。所有零件(不管是內建還是用戶自建、不管是打外部 API 還是純邏輯)都是 `.wasm`,透過 Cypher 三元組串接。
> **所有後端零件都是 `.wasm`。** 需要呼叫外部 HTTP API 的零件(如 google-sheets、http-request),透過 WASI shim 注入的 **host function** 發出網路請求,不在 .wasm 內部直接呼叫網路 syscall。
### Component Dispatcher 路由決策樹
```mermaid
flowchart TD
A[Cypher Executor 呼叫零件 id] --> B{查 Component Registry<br/>取得合約}
B --> C{component_type?}
C -->|wasm| E{當前 Tier?}
C -->|service_binding| SB{有 CF Service Binding?}
SB -->|是| SBExec[CF Service Binding 執行<br/>需 deploy,效能最佳]
SB -->|否| SBErr[回傳錯誤:binding 未宣告]
E -->|Tier 1 / Tier 2| I[workerd WASM<br/>WebAssembly.instantiate<br/>+ WASI shim(兩者相同)]
E -->|Tier 3| L[Wazero IPC<br/>stdin/stdout,完全離線]
I --> RC{runtime_compat<br/>包含 cf-workers?}
RC -->|否| J[回傳 RUNTIME_INCOMPATIBLE 錯誤]
RC -->|是| Exec[執行 .wasm]
```
### KBDB 資料模型(tpl-component
```mermaid
erDiagram
TEMPLATES {
string template_id "tpl-component"
string name
string description
}
BLOCKS {
string block_id "comp-{id}-{version}"
string template_id
string user_id
string page_name
}
SLOTS {
string slot_id
string block_id
string key
string value
}
TEMPLATES ||--o{ BLOCKS : "defines"
BLOCKS ||--o{ SLOTS : "has"
```
---
## Components and Interfaces
### 1. Component Registry`u6u-core/registry/`
Component Registry 是 KBDB 的薄包裝層,透過 HTTP API 管理零件合約。
#### 零件命名機制
零件有兩個名稱,職責完全不同:
| 欄位 | 由誰決定 | 用途 | 範例 |
|---|---|---|---|
| `display_name` | 建立者自由取 | 顯示用,不影響任何邏輯 | `宇宙無敵 GSheets 超級寫入器` |
| `canonical_id` | Registry AI 正規化後確認 | 搜尋、版本控制、Cypher 引用的唯一鍵 | `gsheets_create_table` |
**canonical_id 正規化流程:**
```
提交者輸入 display_name
Registry 用 Workers AI 建議 canonical_id
(格式:{service}_{verb}_{object},全小寫底線)
同時搜尋 Vectorize,若相似度 > 0.9 的 canonical_id 已存在
→ 提示「可能與 gsheets_create_table 重複,是否作為新版本提交?」
提交者確認或修改 canonical_id
上架,canonical_id 永久不變
```
#### 零件分類機制
採用「強制 category + 自由 tags」雙層分類:
- **`category`**:強制填,有限集合,定義前後端邊界
- `logic`:後端邏輯零件(.wasm,純計算/轉換)
- `api`:後端 API 零件(.wasm + cypher_binding,呼叫外部服務)
- `ui`:前端 UI 元件(Web Component,瀏覽器執行)
- `style`:前端樣式零件(CSS tokens
- `anim`:前端動畫零件
- **`tags`**:自由增加,跨零件共享語意
- 例:`gsheets_create_table``["google", "sheets", "spreadsheet", "storage", "write"]`
- 例:`excel_write_row``["microsoft", "excel", "spreadsheet", "storage", "write"]`
- 搜尋「外部存儲」時,兩個都能透過 Vectorize 語意搜尋找到
**HTTP 端點:**
```
GET /components/guide → 機器可讀開發指引(Markdown)
POST /components/validate-contract → 驗證 component.contract.yaml 格式
POST /components → 提交零件(.wasm + contract)觸發沙盒驗收
GET /components/:id → 取得零件合約(最優版本)
GET /components/:id/versions → 取得所有版本清單(含評分)
GET /components/search?q=... → 語意搜尋零件
```
**KBDB 整合:**
- 每個零件版本 = 一個 Block`block_id = comp-{id}-{version}`
- Template = `tpl-component`(預先建立,不新增表)
- `.wasm` 二進位存 R2KBDB slot 只存 `wasm_r2_key`
- `description` + `tags` 欄位寫入 Vectorize 索引,支援語意搜尋
**Slot 欄位對應(tpl-component):**
| Slot key | 說明 | 範例值 |
|---|---|---|
| `canonical_id` | 正規化功能名稱(永久不變,搜尋/版本控制用) | `gsheets_create_table` |
| `display_name` | 建立者自取的顯示名稱 | `宇宙無敵 GSheets 超級寫入器` |
| `category` | 零件分類(有限集合) | `logic` / `api` / `ui` / `style` / `anim` |
| `version` | 實作版本 | `v1` |
| `wasi_target` | WASM 目標 | `preview1` |
| `stability` | 穩定性標籤 | `floating` |
| `runtime_compat` | 相容 runtimeJSON 陣列) | `["cf-workers","wazero"]` |
| `component_type` | 零件類型 | `wasm` / `service_binding` |
| `max_size_kb` | 體積上限 | `2048` |
| `max_cold_start_ms` | 冷啟動上限 | `50` |
| `no_network_syscall` | 禁止網路 syscall | `true` |
| `input_schema` | JSON SchemaJSON 字串) | `{"type":"object",...}` |
| `output_schema` | JSON SchemaJSON 字串) | `{"type":"object",...}` |
| `gherkin_tests` | 測試案例(JSON 字串) | `[{"scenario":"..."}]` |
| `wasm_r2_key` | R2 物件鍵(wasm 模式) | `components/validate_json/v1.wasm` |
| `service_binding_key` | CF binding keyservice_binding 模式) | `CLINIC_GSHEETS` |
| `description` | 自然語言描述(寫入 Vectorize) | `在 Google Sheets 建立新工作表` |
| `tags` | 自由標籤(JSON 陣列,跨零件共享語意) | `["google","sheets","storage","write"]` |
| `success_rate` | 成功率(0-1 | `0.98` |
| `avg_duration_ms` | 平均執行時間 | `12` |
| `call_count` | 被調用次數 | `1024` |
| `status` | 狀態 | `active` / `deprecated` / `tombstone` |
| `deprecated_at` | 棄用時間戳記 | `1700000000000` |
### 2. Component Dispatcher`cypher-executor/src/lib/component-loader.ts` 擴展)
Component Dispatcher 是 `createComponentLoader` 的升級版,新增 WASM 執行路徑。
**介面定義:**
```typescript
// 零件類型(只有兩種)
type ComponentType =
| 'wasm' // 所有後端零件,透過 Cypher binding 串接,本地 WASM 執行
| 'service_binding'; // 效能最佳化:CF Service Binding,需 deploy,用於高頻預組合零件
// 新版 ComponentDescriptor
type ComponentDescriptor = {
component_type: ComponentType;
// WASM 模式
wasm_r2_key?: string;
runtime_compat?: string[];
max_cold_start_ms?: number;
// Service Binding 模式(CF Worker 間高效呼叫,需 deploy
binding?: string; // wrangler.toml 中宣告的 binding key
path?: string;
};
```
**Tier 1 WASM 執行(CF Workers 原生):**
Cloudflare Workers 原生支援 `WebAssembly.instantiate`,但 WASI preview1 需要手動實作 WASI imports。設計採用輕量 WASI shim 方案:
```typescript
// WASI preview1 shim(只實作 stdin/stdout/stderr,其餘 syscall 回傳 ENOSYS
function createWasiImports(stdin: Uint8Array): {
imports: WebAssembly.Imports;
getStdout: () => Uint8Array;
} {
const stdoutChunks: Uint8Array[] = [];
let stdinOffset = 0;
return {
imports: {
wasi_snapshot_preview1: {
fd_write: (fd: number, iovs: number, iovs_len: number, nwritten: number) => { /* ... */ },
fd_read: (fd: number, iovs: number, iovs_len: number, nread: number) => { /* ... */ },
proc_exit: (code: number) => { throw new Error(`wasm exit: ${code}`); },
// 其餘 syscall 回傳 ENOSYS76
fd_seek: () => 76,
fd_close: () => 0,
environ_get: () => 0,
environ_sizes_get: () => 0,
args_get: () => 0,
args_sizes_get: () => 0,
clock_time_get: () => 0,
random_get: (buf: number, buf_len: number) => { /* crypto.getRandomValues */ return 0; },
},
},
getStdout: () => { /* 合併 stdoutChunks */ },
};
}
```
> **設計決策**:不使用 `@cloudflare/workers-wasi`(已停止維護)。改用自製輕量 WASI shim,只實作 `fd_read`/`fd_write`/`proc_exit`/`random_get`,其餘 syscall 回傳 `ENOSYS`。這足以支援 TinyGo/Rust/AssemblyScript 的 stdin/stdout 零件,且不引入外部依賴。
**執行流程:**
```
1. 從 R2 取得 .wasm 二進位(ArrayBuffer
2. WebAssembly.compile(buffer) → WebAssembly.Module
3. 建立 WASI imports shim(注入 stdin = JSON.stringify(input)
4. WebAssembly.instantiate(module, imports)
5. 呼叫 _start() 或 main()
6. 從 stdout buffer 讀取輸出
7. JSON.parse(stdout) → output
```
**R2 快取策略:**
- 第一次呼叫:從 R2 fetch `.wasm``WebAssembly.compile` 後快取 `WebAssembly.Module`Worker 記憶體,跨請求共享)
- 後續呼叫:直接用快取的 Module,只重新 instantiate(避免重複編譯)
### 3. Cypher Triplet Parser 擴展(`cypher-executor/src/actions/triplet-parser.ts`
現有 parser 只支援 `PIPE / IF / FOREACH / CONTINUE`。需擴展支援新語意關係。
**新增 EdgeType**
```typescript
export type EdgeType =
| 'PIPE' | 'IF' | 'FOREACH' | 'CONTINUE' // 現有
| 'IS_A' | 'ON_SUCCESS' | 'ON_FAIL' // 新增:執行語意
| 'ON_CLICK' | 'CALLS_SUBFLOW' // 新增:觸發語意
| 'CONTAINS' | 'HAS_STYLE' | 'HAS_BEHAVIOR'; // 新增:結構語意
```
**URI 協議解析:**
```typescript
// 節點 componentId 解析
function resolveComponentId(uri: string): {
type: 'component' | 'workflow' | 'ui' | 'style';
canonicalId: string;
stability: 'floating' | 'stable' | 'pinned';
pinnedVersion?: string;
} {
// component://validate_json@stable → { type: 'component', canonicalId: 'validate_json', stability: 'stable' }
// component://validate_json@pinned:v1 → { type: 'component', canonicalId: 'validate_json', stability: 'pinned', pinnedVersion: 'v1' }
// workflow://wf_save_to_db → { type: 'workflow', canonicalId: 'wf_save_to_db', stability: 'floating' }
// ui://u6u-btn → { type: 'ui', canonicalId: 'u6u-btn', stability: 'floating' }
}
```
**ON_SUCCESS / ON_FAIL 執行語意:**
GraphExecutor 需要區分「節點執行成功」vs「節點執行失敗」,而非依賴 context 欄位:
```typescript
// 在 executeNode 中,捕捉 try/catch 後分別走 ON_SUCCESS / ON_FAIL 邊
case 'ON_SUCCESS':
// 只在上游節點成功時執行
if (!nodeError) {
result = await this.executeNode(nextNode, ...);
}
break;
case 'ON_FAIL':
// 只在上游節點失敗時執行(接收 error context
if (nodeError) {
result = await this.executeNode(nextNode, graph, { ...context, error: nodeError }, ...);
}
break;
```
**CALLS_SUBFLOW 執行語意:**
```typescript
case 'CALLS_SUBFLOW': {
// 從 KBDB 載入子 Workflow 定義
const subWorkflowId = nextNode.componentId!.replace('workflow://', '');
const subGraph = await loadWorkflowFromKBDB(subWorkflowId, env);
const subExecutor = new GraphExecutor(loader);
const subResult = await subExecutor.execute(subGraph, result as Record<string, unknown>, kvNamespace);
result = { ...(result as Record<string, unknown>), ...subResult.data as Record<string, unknown> };
break;
}
```
### 4. Web Components 零件庫(`u6u-core/web-components/`
Web Components 以原生 Custom Elements API 實作,不依賴任何框架。
**`<u6u-btn>` 介面:**
```typescript
// HTML attributes
interface U6uBtnAttributes {
label: string; // 顯示文字
color?: string; // 主題色(CSS custom property
tooltip?: string; // 滑鼠懸停提示(純靜態)
workflow?: string; // workflow://id
disabled?: boolean;
}
// 發出的自訂事件
interface U6uTriggerEvent extends CustomEvent {
detail: {
workflowId: string;
payload: Record<string, unknown>;
};
}
```
**`<u6u-card>` Smart Container 邏輯:**
```typescript
// u6u-card 攔截子元件的 u6u:trigger 事件
// 收集同容器內所有 u6u-text-input / u6u-text-field 的值
// 合併至 payload 後再向上冒泡
connectedCallback() {
this.addEventListener('u6u:trigger', (e: Event) => {
const trigger = e as CustomEvent;
e.stopPropagation();
const inputs = this.querySelectorAll('u6u-text-input, u6u-text-field');
const collected: Record<string, unknown> = {};
inputs.forEach(input => {
const name = input.getAttribute('name');
const value = (input as any).value;
if (name) collected[name] = value;
});
this.dispatchEvent(new CustomEvent('u6u:trigger', {
bubbles: true,
composed: true,
detail: {
...trigger.detail,
payload: { ...trigger.detail.payload, ...collected },
},
}));
});
}
```
### 5. 雙面翻轉畫布(`inkstone-admin/frontend/web/`
畫布本身用 React 19 + Web Components 組裝,體現 dogfooding 原則。
**翻轉狀態機:**
```mermaid
stateDiagram-v2
[*] --> UIView: 初始狀態
UIView --> LogicView: 點擊翻面按鈕
LogicView --> UIView: 點擊翻面按鈕
LogicView --> Editing: 修改三元組
Editing --> Saving: 確認儲存
Saving --> LogicView: KBDB 寫入成功
Saving --> LogicView: KBDB 寫入失敗(顯示錯誤)
```
---
## Data Models
### Component Contract YAML(完整規格)
```yaml
canonical_id: "validate_json" # 正規化功能名稱(永久不變,Registry AI 正規化後確認)
display_name: "JSON 格式驗證器" # 建立者自取,顯示用
category: "logic" # logic | api | ui | style | anim
version: "v1" # 實作版本
wasi_target: "preview1" # WASM 目標格式
stability: "floating" # floating | stable | pinned
runtime_compat:
- "cf-workers"
- "workerd"
- "wazero"
constraints:
max_size_kb: 2048
max_cold_start_ms: 50
no_network_syscall: true
no_filesystem_syscall: true
io_model: "stdin_stdout_json" # 唯一合法值
input_schema:
type: object
required: ["json_string"]
properties:
json_string:
type: string
description: "待驗證的 JSON 字串"
output_schema:
type: object
properties:
valid:
type: boolean
error:
type: string
description: "驗證失敗時的錯誤訊息"
gherkin_tests:
- scenario: "合法 JSON 通過驗證"
given: '{"json_string":"{\"key\":\"value\"}"}'
then_contains: '{"valid":true}'
- scenario: "非法 JSON 回傳錯誤"
given: '{"json_string":"not-json"}'
then_contains: '{"valid":false,"error":'
tags: ["validation", "json", "utility"]
description: "驗證輸入字串是否為合法 JSON 格式"
```
### 零件開發語言決策
**內建零件使用 TinyGo**(純邏輯零件)和 TinyGo + `json.RawMessage`(需要任意 HTTP body 的零件)。不引入 Rust 作為內建零件語言。
**用戶自建零件支援三種語言,按難度分層:**
| 語言 | 目標用戶 | JSON 能力 | 備註 |
|---|---|---|---|
| **AssemblyScript** | 一般用戶(TS 背景) | 社群套件 `assemblyscript-json`,支援動態 JSON | 語法最接近 TS,門檻最低;靜默錯誤風險,沙盒驗收必須通過 |
| **TinyGo** | 技術較強用戶(Go 背景) | 靜態 struct 完整支援;`json.RawMessage` 處理任意 body | 編譯期報錯,AI 生成安全性較高 |
| **Rust** | 進階用戶 | `serde_json::Value` 完整動態 JSON | 生態最成熟,體積最小;學習曲線陡 |
**`/components/guide` 端點提供三份語言範例**,用戶根據自身背景選擇。
**內建零件 JSON 策略(TinyGo):**
```go
// 固定 schema 零件(google-sheets、gmail 等)→ 靜態 struct
type Input struct {
SpreadsheetId string `json:"spreadsheet_id"`
Range string `json:"range"`
AccessToken string `json:"access_token"`
}
// 任意 body 零件(http-request)→ json.RawMessage 傳遞 raw bytes,不解析
type Input struct {
URL string `json:"url"`
Method string `json:"method"`
Body json.RawMessage `json:"body"` // 任意 JSON,不解析
}
```
### Workflow Cypher 三元組(完整語法)
```yaml
kind: Workflow
id: wf_submit_form
triplets:
# 節點類型宣告
- "btn_submit >> IS_A >> ui://u6u-btn"
- "step_validate >> IS_A >> component://validate_json"
- "step_save >> IS_A >> component://kbdb_write"
# 前端觸發後端
- "btn_submit >> ON_CLICK >> step_validate"
# 成功/失敗分支
- "step_validate >> ON_SUCCESS >> step_save"
- "step_validate >> ON_FAIL >> step_notify_error"
# 子流程呼叫
- "step_save >> ON_SUCCESS >> CALLS_SUBFLOW >> workflow://wf_notify_user"
# 容器結構
- "card_main >> CONTAINS >> btn_submit"
- "card_main >> CONTAINS >> input_name"
```
### Evaluation BlockKBDB tpl-evaluation
每次 Workflow 執行後,Evaluator Agent 寫入一個 Evaluation Block
| Slot key | 說明 |
|---|---|
| `run_id` | 執行唯一 ID |
| `workflow_id` | Workflow ID |
| `component_id` | 被評價的零件 ID |
| `verdict` | `success` / `failed` / `timeout` |
| `duration_ms` | 執行時間 |
| `error_message` | 失敗訊息(可選) |
| `evaluated_at` | 評價時間戳記 |
### Pitfall BlockKBDB tpl-pitfall
| Slot key | 說明 |
|---|---|
| `component_id` | 問題零件 ID |
| `failure_pattern` | 失敗模式描述 |
| `first_seen_at` | 首次發現時間戳記 |
| `occurrence_count` | 發生次數 |
---
## Correctness Properties
*A property is a characteristic or behavior that should hold true across all valid executions of a system — essentially, a formal statement about what the system should do. Properties serve as the bridge between human-readable specifications and machine-verifiable correctness guarantees.*
### Property Reflection(去重分析)
在寫出最終屬性前,先做冗餘分析:
- **1.1 + 1.2**:都是合約欄位完整性驗證,合併為 Property 1「合約格式完整性」
- **2.1 + 2.2**:驗收流程執行 + 失敗回報,合併為 Property 2「沙盒驗收流程正確性」
- **2.3 + 2.4**:提交後可讀取 + 冪等提交,合併為 Property 3「零件提交冪等性與持久性」
- **3.1 + 3.5**WASM 執行 + 雙模式路由,合併為 Property 4「Component Dispatcher 路由正確性」
- **3.4 + 6.4**:不相容 Tier 回傳錯誤 + 結構化錯誤,合併為 Property 5「Dispatcher 錯誤結構完整性」
- **4.1**URI 解析 round-trip,獨立為 Property 6
- **4.2 + 4.4**:版本選擇算法(floating 最高分 + pinned 固定版本),合併為 Property 7「版本選擇策略正確性」
- **4.5**:版本保留不變量,獨立為 Property 8
- **5.7**Confluence 屬性,獨立為 Property 9
- **8.3 + 8.4 + 8.5**Web Components 事件與渲染,合併為 Property 10「Web Components 事件與渲染冪等性」
- **10.6 + 12.5**:評價冪等性 + 查詢冪等性,合併為 Property 11「系統操作冪等性」
最終保留 11 個屬性,每個提供獨立驗證價值。
---
### Property 1: 合約格式完整性
*For any* component contract object,若缺少任何必填欄位(`id``version``wasi_target``stability``runtime_compat``constraints.max_size_kb``constraints.max_cold_start_ms``constraints.no_network_syscall``constraints.io_model``input_schema``output_schema``gherkin_tests`),合約驗證器 SHALL 拒絕該合約並回傳包含缺失欄位名稱的錯誤;反之,包含所有必填欄位的合約 SHALL 通過格式驗證。
**Validates: Requirements 1.1, 1.2, 1.4**
---
### Property 2: 沙盒驗收流程正確性
*For any* 提交的零件(.wasm + contract),若零件在驗收步驟 N 失敗,Component_Registry 的回應 SHALL 包含步驟 N 的名稱與具體失敗原因,且不執行步驟 N+1 之後的步驟。
**Validates: Requirements 2.1, 2.2**
---
### Property 3: 零件提交冪等性與持久性
*For any* 通過驗收的零件(id, version),提交後從 Component_Registry 讀取該零件的合約,所有欄位值 SHALL 與提交時的合約完全一致(序列化 round-trip);對相同 (id, version) 重複提交 N 次,KBDB 中 SHALL 只存在一個對應的 Block。
**Validates: Requirements 2.3, 2.4**
---
### Property 4: Component Dispatcher 路由正確性
*For any* 零件合約,若 `io_model = "stdin_stdout_json"`Component_Dispatcher SHALL 使用 WASM 執行路徑,將 input JSON 寫入 stdin,從 stdout 讀取 output JSON;若 `io_model = "http_endpoint"`SHALL 使用 HTTP 路徑。對任意合法 JSON inputWASM 執行路徑的輸出 SHALL 與 HTTP 執行路徑的輸出語意等效。
**Validates: Requirements 3.1, 3.5, 3.6**
---
### Property 5: Dispatcher 錯誤結構完整性
*For any* (component_id, tier) 組合,若該零件的 `runtime_compat` 不包含當前 tierComponent_Dispatcher 的錯誤回應 SHALL 同時包含:零件 id、當前 tier 名稱、已嘗試的呼叫路徑清單,三個欄位缺一不可。
**Validates: Requirements 3.4, 6.4**
---
### Property 6: 零件 URI 解析 Round-Trip
*For any* 合法的零件 URI 字串(格式為 `component://id``component://id@stable``component://id@pinned:vN`),解析後再重新序列化 SHALL 產生與原始 URI 語意等效的字串;解析出的 `id``stability``pinnedVersion` 欄位 SHALL 與原始 URI 中的對應部分完全一致。
**Validates: Requirements 4.1**
---
### Property 7: 版本選擇策略正確性
*For any* 零件 id 下的版本集合(每個版本有 success_rate、avg_duration_ms、call_count 評分),當 stability = `floating` 時,Component_Dispatcher SHALL 選取「成功率 × 速度評分 × 被調用次數」最高的版本;當 stability = `pinned:vN` 時,無論版本集合中其他版本的評分如何,SHALL 永遠選取版本 vN。
**Validates: Requirements 4.2, 4.4**
---
### Property 8: 歷史版本永久保留不變量
*For any* 已上架的零件版本(id, version),無論該版本後來被標記為 `deprecated``tombstone`,其 `.wasm` 二進位 SHALL 永遠可從 R2 讀取,且 `pinned` 引用 SHALL 永遠能透過 Component_Dispatcher 執行該版本。
**Validates: Requirements 4.5, 10.5**
---
### Property 9: Cypher 三元組解析 Confluence(順序無關性)
*For any* 合法的 Cypher 三元組集合,無論三元組在輸入陣列中的排列順序如何,`parseTriplets` 產生的執行圖(節點集合、邊集合、拓撲結構)SHALL 語意等效——即相同的節點 id 集合、相同的 (from, to, type) 邊集合。
**Validates: Requirements 5.7**
---
### Property 10: Web Components 事件與渲染冪等性
*For any* workflow URI 設定於 `<u6u-btn>``workflow` attribute,使用者點擊後發出的 `u6u:trigger` 事件 detail 中的 `workflowId` SHALL 與 URI 中的 id 完全一致;*For any* 一組具名 `<u6u-text-input>` 元件置於 `<u6u-card>` 內,觸發事件後收集到的 payload SHALL 包含所有輸入元件的 name-value 對;*For any* attribute 值,對同一 Web Component 設定相同 attribute 值 N 次,渲染結果 SHALL 與設定一次相同(冪等渲染)。
**Validates: Requirements 8.3, 8.4, 8.5**
---
### Property 11: 系統操作冪等性
*For any* Workflow 執行日誌(run_id),Evaluator_Agent 對相同 run_id 處理 N 次,KBDB 中 SHALL 只存在一個對應的 Evaluation Block,不產生重複記錄;*For any* Component_Registry 讀取操作的查詢參數,在 KBDB 資料不變的前提下,對相同參數呼叫 N 次 SHALL 回傳完全相同的結果。
**Validates: Requirements 10.6, 12.5**
---
## Error Handling
### Component Dispatcher 錯誤分類
| 錯誤類型 | 觸發條件 | 回應格式 |
|---|---|---|
| `COMPONENT_NOT_FOUND` | KBDB 中找不到零件 | `{ error: "COMPONENT_NOT_FOUND", component_id, tier }` |
| `RUNTIME_INCOMPATIBLE` | runtime_compat 不含當前 Tier | `{ error: "RUNTIME_INCOMPATIBLE", component_id, tier, attempted_paths: [] }` |
| `WASM_EXECUTION_TIMEOUT` | 超過 max_cold_start_ms | `{ error: "WASM_EXECUTION_TIMEOUT", component_id, timeout_ms }` |
| `WASM_INVALID_OUTPUT` | stdout 不是合法 JSON | `{ error: "WASM_INVALID_OUTPUT", component_id, raw_output }` |
| `WASM_SYSCALL_VIOLATION` | .wasm 嘗試網路/檔案 syscall | `{ error: "WASM_SYSCALL_VIOLATION", component_id, syscall_name }` |
| `CONTRACT_VALIDATION_FAILED` | 合約格式不合規 | `{ error: "CONTRACT_VALIDATION_FAILED", missing_fields: [] }` |
### 沙盒驗收失敗回應格式
```json
{
"success": false,
"failed_step": "syscall_scan",
"reason": "發現禁止的 syscallsock_connect",
"guide_anchor": "#syscall-constraints",
"component_id": "my_component",
"version": "v1"
}
```
### Tier 3 離線錯誤處理
Tier 3 在離線環境中,所有無法執行的操作都寫入 DTN 佇列,不拋出錯誤:
```go
// Go 排程引擎的錯誤處理策略
type DTNEntry struct {
Type string // "missing_component" | "sync_log" | "request_wasm"
Payload json.RawMessage
CreatedAt time.Time
RetryCount int
}
```
### Web Components 錯誤邊界
`<u6u-btn>``workflow` attribute 未設定時,點擊不發出事件,僅在 console 輸出警告:
```
[u6u-btn] workflow attribute is not set, click event ignored
```
---
## Testing Strategy
### 雙軌測試策略
本功能採用「單元測試 + 屬性測試」雙軌策略:
- **單元測試(Vitest)**:驗證具體範例、邊界條件、錯誤情境
- **屬性測試(fast-check**:驗證上述 11 個 Correctness Properties,每個屬性最少執行 100 次迭代
### 屬性測試配置
使用 `fast-check`(已在 tech stack 中),每個屬性測試標記格式:
```typescript
// Feature: arcrun-platform-evolution, Property N: {property_text}
it.prop([fc.record({ id: fc.string(), version: fc.string(), ... })])(
'Property 1: 合約格式完整性',
(contract) => {
// ...
},
{ numRuns: 100 }
);
```
### 各 Phase 測試重點
**Phase 0WASM 執行核心):**
- Property 4WASM 執行路徑 round-trip`validate_json.wasm` 作為 ground truth
- Property 1:合約格式驗證
- 單元測試:WASI shim 的 `fd_read`/`fd_write` 正確性
**Phase 1(零件遷移):**
- Property 3:提交冪等性(20 個零件各提交兩次,驗證無重複)
- Property 2:沙盒驗收流程(各步驟失敗案例)
- Property 8:歷史版本保留(deprecate 後仍可讀取)
**Phase 2Cypher 擴展):**
- Property 9Confluence(三元組順序無關性,fast-check shuffle
- Property 6URI 解析 round-trip
- Property 7:版本選擇策略(floating 最高分、pinned 固定版本)
- Property 5:錯誤結構完整性
**Phase 3(前端畫布):**
- Property 10Web Components 事件與渲染冪等性(`@testing-library/react` + fast-check
- Property 11:評價冪等性(Evaluator Agent 重複處理)
### 整合測試
以下場景使用整合測試(1-3 個具體範例,不用 PBT):
- Tier 1 CF Workers 環境中實際執行 `validate_json.wasm`(驗證 WASM 在 Workers 環境可運行)
- KBDB tpl-component Template 建立與 Slot 讀寫(驗證 KBDB 整合)
- R2 `.wasm` 上傳與讀取(驗證 R2 整合)
- Vectorize 語意搜尋(驗證「查詢 Google Sheets 資料」能找到 `gsheets_get_entries`
### 單元測試重點(非 PBT
- WASI shim`fd_read` 正確讀取 stdin、`fd_write` 正確寫入 stdout
- `evaluateCondition`:現有條件評估函數的邊界案例
- `resolveComponentId`:URI 解析的邊界案例(空字串、特殊字元)
- `<u6u-card>` Smart Container:巢狀容器的事件冒泡行為
@@ -1,219 +0,0 @@
# Requirements Document
## Introduction
u6u 平台演進規格描述從現況(HTTP endpoint 零件、單一 Cloudflare 部署、無前端畫布)
到目標架構(WASM 零件模型、三層物理部署、雙面翻轉畫布)的完整演進路徑。
本規格涵蓋三個核心演進軸:
1. **零件模型遷移**:將 20 個內建零件從 Cloudflare Worker HTTP endpoint 遷移至 WASI preview1 `.wasm` 格式,附帶 `component.contract.yaml`,以 stdin/stdout JSON 作為唯一 I/O 模型。
2. **多 Tier 執行抽象**:讓 Cypher Executor 透過統一的 Component Dispatcher 介面,跨 Tier 1Cloudflare Workers)、Tier 2workerd 地端叢集)、Tier 3Go + Wazero 邊緣載具)呼叫零件。
3. **前端雙面畫布**:建立以 Web Components 為基礎的視覺化畫布,正面為 UI 視圖,反面為 Cypher 邏輯視圖,智慧容器自動打包表單值。
---
## Glossary
- **Component(零件)**:系統最小執行單元,一個零件只做一件事,以 `.wasm`WASI preview1)或 Web Component 形式存在。
- **Component_Contract**:每個零件附帶的 `component.contract.yaml`,定義 id、version、wasi_target、stability、runtime_compat、constraints、input_schema、output_schema、gherkin_tests。
- **Component_Registry**KBDB 中儲存所有零件合約與 `.wasm` 位置的索引,以 `tpl-component` Template Block 實作。
- **Component_Dispatcher**Cypher Executor 內部的路由層,根據零件的 `runtime_compat` 與目標 Tier 決定呼叫路徑(Service Binding / workerd HTTP / Wazero IPC)。
- **Cypher_Executor**Workflow 執行引擎,解析三元組語法,透過 GraphExecutor 執行節點,現部署於 Cloudflare Workers。
- **Cypher_Triplet**`"A >> 關係 >> B"` 格式的三元組,描述節點間的語意關係。
- **KBDB**:三位一體記憶庫(blocks / templates / slots),搭配 Cloudflare Vectorize,是平台唯一的持久化狀態來源。
- **Tier_1**:雲端層,Cloudflare Workers + D1 + Vectorize + R2,全球無伺服器部署。
- **Tier_2**:企業地端層,workerd 叢集 + Kùzu 或 PostgreSQL + AGE,高機密內網環境。
- **Tier_3**:邊緣載具層,Go 排程引擎 + 內嵌 Wazero + SQLite,無 V8、無網路的極限環境(無人機、AGV)。
- **WASI_Preview1**WebAssembly System Interface preview1 規格,零件唯一合法的 WASM 目標格式。
- **Canvas(畫布)**:前端雙面翻轉介面,正面為 UI 視圖,反面為 Cypher 邏輯視圖。
- **Smart_Container**:畫布上的排版容器(如 `<u6u-card>`),自動打包同容器內所有輸入元件的值並附加至觸發事件的 payload。
- **Forge_AI**:工匠 AI,負責在 Tier 2 地端接收零件規格、生成 TinyGo 程式碼、編譯並測試 `.wasm`
- **Evaluator_Agent**:強制評價代理,每次 Workflow 執行後自動評估成功率、效能、警告訊息。
- **Pitfall_Block**KBDB 中記錄已知問題的 Block,AI 搜尋時強制讀取以繞道。
- **Stability_Tag**:零件版本穩定性標籤,值為 `floating`AI 自動選最優)、`stable`(人工確認才換)、`pinned`(版本凍結)。
- **DTN**Delay-Tolerant NetworkingTier 3 邊緣載具在間歇性網路下的短點射傳輸協議。
---
## Requirements
### Requirement 1:零件合約規格標準化
**User Story:** As a 平台架構師, I want 每個零件都有標準化的 `component.contract.yaml` 合約, so that AI 能透過統一介面讀取零件能力,並在任何 Tier 上驗證相容性。
#### Acceptance Criteria
1. THE Component_Contract SHALL 包含以下必填欄位:`canonical_id`(功能合約名稱,永久不變)、`display_name`(人類可讀名稱,可自由撰寫)、`description`(語意搜尋用途,需精確描述零件能做什麼、適用情境,至少 20 字)、`version`(實作版本)、`wasi_target`(值為 `"preview1"`)、`stability`(值為 `floating``stable``pinned` 之一)、`runtime_compat`(陣列,值為 `cf-workers``workerd``wazero` 的子集)、`constraints``input_schema``output_schema``gherkin_tests`
1a. THE `canonical_id` SHALL 遵循以下命名規範,以確保全庫一致性:
- 格式:`{scope}_{action}``{scope}_{object}``{scope}`(單詞),全部小寫底線,不超過 4 個單詞
- **整合類**category: api):以服務名稱為 scope,可加動作;範例:`gmail``gmail_send``google_sheets``google_sheets_append``telegram``telegram_send`
- **資料處理類**category: data):以資料型別或操作為 scope;範例:`string_ops``array_ops``date_ops``json_transform`
- **控制流類**category: logic):以控制結構名稱命名;範例:`if_control``foreach_control``try_catch``switch``wait`
- **AI 類**category: ai):以 `ai_` 為前綴;範例:`ai_transform_compile``ai_summarize``ai_classify`
- 禁止:中文、空格、大寫、連字號(`-`)、版本號混入 id`gmail_v2``version: v2` 表達)
1b. THE `display_name` SHALL 為人類可讀的自由格式名稱(可中文、可含空格);此欄位供 UI 顯示用,不作為系統識別符。範例:`canonical_id: google_sheets_append``display_name: "Google Sheets — 新增一列"`
1c. THE `description` SHALL 用於 Vectorize 語意搜尋索引;撰寫時應以「能做什麼、適合什麼情境」為核心,避免只寫零件名稱的同義詞。範例:`"傳送 Gmail 電子郵件,適合 Workflow 完成時通知使用者、訂閱確認信、錯誤警報等場景。需要 Gmail OAuth token。"` 而非 `"Gmail 發信零件"`
2. THE Component_Contract SHALL 在 `constraints` 中包含以下欄位:`max_size_kb`(上限 2048)、`max_cold_start_ms`(上限 50)、`no_network_syscall`(布林值)、`io_model`(值為 `"stdin_stdout_json"`)。
3. WHEN 一個零件的 `id` 已存在於 Component_RegistryTHE Component_Registry SHALL 允許以新 `version` 值新增該零件的新實作,而不覆蓋舊版本。
4. THE Component_Contract SHALL 在 `gherkin_tests` 中至少包含一個正常情境(happy path)與一個錯誤情境(error path)的測試案例。
5. IF 一個零件的 `input_schema``output_schema` 涉及序列化或反序列化操作,THEN THE Component_Contract SHALL 包含一個 round-trip 測試案例,驗證 `parse(format(x)) == x`
---
### Requirement 2:零件沙盒驗收流程
**User Story:** As a 零件提交者(AI 或開發者), I want 提交的零件自動通過沙盒驗收, so that 只有符合品質標準的零件才能進入零件宇宙。
#### Acceptance Criteria
1. WHEN 一個零件被提交至 Component_RegistryTHE Component_Registry SHALL 依序執行以下驗收步驟:(a)體積檢查(`.wasm` 小於 `max_size_kb`)、(b)冷啟動時間測量(小於 `max_cold_start_ms`)、(c)syscall 掃描(不含網路或檔案系統 syscall)、(d)Gherkin 測試執行(所有 scenario 100% 通過)、(e)多 runtime 相容測試(`runtime_compat` 列出的所有 runtime)。
2. IF 任一驗收步驟失敗,THEN THE Component_Registry SHALL 拒絕該零件上架,並回傳包含失敗步驟名稱與具體原因的錯誤訊息。
3. WHEN 所有驗收步驟通過,THE Component_Registry SHALL 將零件合約存入 KBDB 的 `tpl-component` Template Block,並記錄上架時間戳記。
4. THE Component_Registry SHALL 以冪等方式執行驗收流程,對相同 `id``version` 的重複提交回傳相同結果而不重複執行測試。
---
### Requirement 3:現有 HTTP 零件遷移至 WASM
**User Story:** As a 平台開發者, I want 將現有 20 個 HTTP endpoint 零件遷移為 WASI preview1 `.wasm` 格式, so that 零件能在 Tier 3 邊緣載具(無 V8、無網路)上執行,消除技術債。
#### Acceptance Criteria
1. THE Component_Dispatcher SHALL 支援以 WASM 模式呼叫零件:讀取 `.wasm` 二進位、透過 WASI preview1 runtime 執行、將 input JSON 寫入 stdin、從 stdout 讀取 output JSON。
2. WHEN 一個 WASM 零件需要呼叫外部 HTTP API(如 Google Sheets),THE Component_Dispatcher SHALL 透過 host function 注入方式提供網路能力,而非允許 `.wasm` 內部直接發出網路 syscall。
3. THE Component_Dispatcher SHALL 在 Tier 1Cloudflare Workers)環境中,以 `workerd` 內建的 WASM 執行能力執行 WASI preview1 零件。
4. WHEN 一個零件的 `runtime_compat` 不包含當前執行環境的 TierTHE Component_Dispatcher SHALL 回傳錯誤,說明該零件不相容於當前 Tier,而非嘗試執行。
5. THE Component_Dispatcher SHALL 在遷移期間同時支援舊有 HTTP endpoint 模式(Service Binding 或外部 URL)與新 WASM 模式,以 Component_Contract 的 `io_model` 欄位區分呼叫路徑。
6. FOR ALL 現有 20 個內建零件,遷移後的 WASM 版本 SHALL 通過與原 HTTP 版本相同的 Gherkin 測試案例(round-trip 等效性)。
---
### Requirement 4:零件版本控制與穩定性標籤
**User Story:** As a Workflow 設計者, I want 在 Cypher 三元組中指定零件的穩定性需求, so that 關鍵業務流程不會因 AI 自動升級零件而中斷。
#### Acceptance Criteria
1. THE Cypher_Executor SHALL 支援以下三種零件引用語法:`component://id`(預設 floating)、`component://id@stable``component://id@pinned:vN`
2. WHEN 一個 Workflow 引用 `component://id`floating),THE Component_Dispatcher SHALL 從 Component_Registry 選取該 `id` 下「成功率 × 速度 × 被調用次數」評分最高的版本執行。
3. WHEN 一個 Workflow 引用 `component://id@stable`THE Component_Dispatcher SHALL 使用當前標記為 stable 的版本,並在有更優版本時記錄提示至 KBDB,但不自動切換。
4. WHEN 一個 Workflow 引用 `component://id@pinned:vN`THE Component_Dispatcher SHALL 永遠使用版本 `vN`,即使該版本已被標記為 Deprecated。
5. THE Component_Registry SHALL 保留所有歷史版本的 `.wasm` 二進位,不因版本淘汰而刪除檔案。
---
### Requirement 5Cypher 語意關係擴展
**User Story:** As a Workflow 設計者, I want Cypher 三元組支援完整的語意關係集合, so that 能描述條件分支、子流程呼叫、前端觸發等複雜業務邏輯。
#### Acceptance Criteria
1. THE Cypher_Executor SHALL 解析並執行以下語意關係:`IS_A`(節點類型宣告)、`ON_SUCCESS`(成功後繼)、`ON_FAIL`(失敗後繼)、`ON_CLICK`(前端點擊觸發)、`CALLS_SUBFLOW`(呼叫子 Workflow)、`CONTAINS`(容器包含關係)、`HAS_STYLE`(樣式關聯)、`HAS_BEHAVIOR`(行為關聯)。
2. WHEN 解析 `IS_A` 關係,THE Cypher_Executor SHALL 從 Component_Registry 載入對應的零件合約,並以合約的 `input_schema` 驗證節點的輸入 context。
3. WHEN 解析 `ON_SUCCESS``ON_FAIL` 關係,THE Cypher_Executor SHALL 根據上游節點的執行結果(成功或拋出錯誤)決定走向,而非依賴 context 中的特定欄位。
4. WHEN 解析 `CALLS_SUBFLOW` 關係,THE Cypher_Executor SHALL 以當前 context 作為子 Workflow 的 initialContext 執行,並將子 Workflow 的輸出合併回主流程 context。
5. WHEN 解析 `ON_CLICK` 關係,THE Cypher_Executor SHALL 接受來自前端 Smart_Container 打包的 payload,並以該 payload 作為 Workflow 的 initialContext。
6. THE Cypher_Executor SHALL 支援 URI 協議前綴:`component://`(零件引用)、`workflow://`Workflow 引用)、`ui://`(前端零件引用)、`style://`(樣式零件引用)。
7. FOR ALL 合法的 Cypher 三元組序列,THE Cypher_Executor SHALL 保證解析結果的冪等性:對相同輸入三元組集合,無論排列順序,產生語意等效的執行圖(Confluence 屬性)。
---
### Requirement 6Component Dispatcher 多 Tier 路由
**User Story:** As a 平台架構師, I want Cypher Executor 透過統一的 Component Dispatcher 介面呼叫跨 Tier 零件, so that Workflow 設計者不需要知道零件部署在哪個 Tier。
#### Acceptance Criteria
1. THE Component_Dispatcher SHALL 根據以下優先序決定呼叫路徑:(1Tier 1Cloudflare Service Binding(若零件部署為 Worker)或 WASM 直接執行;(2Tier 2workerd 叢集 HTTP endpoint;(3Tier 3Wazero IPCstdin/stdout)。
2. WHEN Component_Dispatcher 在 Tier 1 環境中呼叫一個 `runtime_compat` 包含 `cf-workers` 的零件,THE Component_Dispatcher SHALL 優先使用 Cloudflare Service Binding,若 binding 不存在則退回 WASM 執行模式。
3. WHEN Component_Dispatcher 在 Tier 3 環境中呼叫零件,THE Component_Dispatcher SHALL 只使用 Wazero 執行本地 `.wasm` 檔案,不發出任何網路請求。
4. IF Component_Dispatcher 無法在當前 Tier 找到可用的呼叫路徑,THEN THE Component_Dispatcher SHALL 回傳結構化錯誤,包含:零件 id、當前 Tier、嘗試的呼叫路徑清單。
5. THE Component_Dispatcher SHALL 對每次零件呼叫記錄執行時間(ms)、成功或失敗狀態,並非同步寫入 KBDB 的 Evaluation Block,不阻擋主流程。
6. WHILE Component_Dispatcher 執行零件呼叫,THE Component_Dispatcher SHALL 強制套用 Component_Contract 中的 `max_cold_start_ms` 作為逾時上限,超時後回傳逾時錯誤。
---
### Requirement 7Tier 3 邊緣離線生存能力
**User Story:** As a 邊緣載具操作者(無人機、AGV), I want 載具在完全離線環境中仍能執行預載的 Workflow, so that 業務不因網路中斷而停擺。
#### Acceptance Criteria
1. THE Tier_3 執行引擎 SHALL 在無網路連線的環境中,使用本地 SQLite 作為 KBDB 替代儲存,執行預先下載的 Cypher Workflow 與 `.wasm` 零件。
2. WHEN Tier_3 執行引擎在執行中發現缺少所需零件,THE Tier_3 執行引擎 SHALL 記錄缺失零件的 `id``input_schema` 至本地 DTN 佇列,待下次連網時以 Burst 傳輸方式送至 Tier_2 請求代工。
3. WHEN Tier_3 執行引擎收到來自 Tier_2 的新 `.wasm` 零件,THE Tier_3 執行引擎 SHALL 在執行前對該零件進行 syscall 掃描,確認不含網路或檔案系統 syscall,通過後才載入執行。
4. THE Tier_3 執行引擎 SHALL 在 Cypher 圖譜執行中途動態替換失敗零件(如感測器零件因環境變化失效),以 Component_Registry 中相同 `input_schema` 的備用零件繼續執行,不中斷整體 Workflow。
5. WHEN Tier_3 執行引擎重新連線至 Tier_2THE Tier_3 執行引擎 SHALL 將本地執行日誌(包含 trace、評價結果、Pitfall 記錄)同步至 Tier_2 的 KBDB,確保全局狀態一致。
---
### Requirement 8:前端 Web Components 零件庫
**User Story:** As a 前端開發者, I want 一套以 Web Components 標準實作的 u6u UI 零件庫, so that 畫布上的 UI 元件能在任何現代瀏覽器中獨立運作,不依賴特定前端框架。
#### Acceptance Criteria
1. THE Canvas SHALL 提供以下核心 Web Components`<u6u-btn>`(按鈕)、`<u6u-text-input>`(文字輸入)、`<u6u-text-field>`(多行文字)、`<u6u-chart>`(圖表)、`<u6u-card>`(智慧容器)。
2. THE `<u6u-btn>` SHALL 支援以下 HTML attributes`label`(顯示文字)、`color`(主題色)、`tooltip`(滑鼠懸停提示,純靜態,不觸發 Webhook)、`workflow`(綁定的 Workflow URI,格式為 `workflow://id`)。
3. WHEN `<u6u-btn>``workflow` attribute 被設定且使用者點擊按鈕,THE `<u6u-btn>` SHALL 發出 `u6u:trigger` 自訂事件,事件 detail 包含 `{ workflowId, payload }`
4. THE `<u6u-card>` SHALL 在接收到子元件的 `u6u:trigger` 事件時,自動收集同容器內所有 `<u6u-text-input>``<u6u-text-field>` 的當前值,合併至事件的 `payload` 後再向上冒泡。
5. FOR ALL Web ComponentsTHE Canvas SHALL 保證元件的 HTML attribute 變更能即時反映至視覺渲染,且渲染結果與 attribute 值之間的對應關係具有冪等性(相同 attribute 值永遠產生相同渲染結果)。
---
### Requirement 9:雙面翻轉畫布介面
**User Story:** As a 業務使用者(非工程師), I want 畫布上每個 UI 元件都能翻面查看並編輯其 Cypher 邏輯連線, so that 不需要寫程式就能理解並修改業務邏輯。
#### Acceptance Criteria
1. THE Canvas SHALL 為每個 UI 零件提供「翻面」操作,切換至邏輯視圖後,顯示該零件關聯的 Cypher 三元組(以視覺化節點連線方式呈現)。
2. WHEN 使用者在邏輯視圖中修改 Cypher 連線(新增、刪除或修改三元組),THE Canvas SHALL 即時更新對應 Workflow 的 KBDB Block,並在正面 UI 視圖中反映連線狀態變更(如按鈕顏色或 badge 提示)。
3. THE Canvas SHALL 在邏輯視圖中提供 Workflow URI 選擇器,列出 KBDB 中所有可用的 Workflow,讓使用者透過下拉選單完成 `ON_CLICK >> workflow://id` 的綁定,不需手動輸入 URI。
4. WHEN 使用者在畫布上將兩個 UI 零件拖入同一個 `<u6u-card>` 容器,THE Canvas SHALL 自動在邏輯視圖中顯示 Smart_Container 的自動打包關係,說明哪些輸入值會被自動收集。
5. THE Canvas SHALL 在使用者嘗試替換一個已綁定 Workflow 的 UI 零件時,只顯示 Component_Registry 中具備相同觸發能力(即 `u6u:trigger` 事件)的候選零件,過濾掉不相容的零件。
---
### Requirement 10:自動演化評價迴圈
**User Story:** As a 平台維運者, I want 每次 Workflow 執行後自動觸發 AI 評價, so that 系統能持續識別問題零件並累積避坑知識。
#### Acceptance Criteria
1. WHEN 一個 Workflow 執行完畢(無論成功或失敗),THE Evaluator_Agent SHALL 在執行結束後非同步評估以下維度:執行狀態(成功 / 失敗 / 逾時)、各節點執行時間、零件錯誤率趨勢。
2. WHEN Evaluator_Agent 發現某零件的錯誤率在連續 5 次執行中超過 50%THE Evaluator_Agent SHALL 在 KBDB 中為該零件建立 Pitfall_Block,記錄:零件 id、失敗模式描述、首次發現時間戳記。
3. WHEN Component_Dispatcher 在 Component_Registry 搜尋零件時,THE Component_Dispatcher SHALL 讀取目標零件的所有關聯 Pitfall_Block,並在選擇版本時降低有 Pitfall 記錄的版本的評分權重。
4. WHEN 一個零件連續 30 天無任何 Workflow 引用,THE Component_Registry SHALL 將該零件標記為 `Deprecated`,並從預設搜尋結果中移除,但保留 `.wasm` 二進位與合約。
5. WHEN 一個 `Deprecated` 零件再經過 90 天仍無引用,THE Component_Registry SHALL 將該零件移入墓地(tombstone 狀態),從所有搜尋結果中移除,但 `pinned` 版本的 `.wasm` 永遠保留且可被 Component_Dispatcher 存取。
6. THE Evaluator_Agent SHALL 以冪等方式處理重複的執行日誌,對相同 `run_id` 的重複評價請求回傳相同結果而不重複建立 Pitfall_Block。
---
### Requirement 11:零件開發指引(Component Authoring Guide
**User Story:** As a 零件開發者(使用自己的 AI 工具,如 Claude、GPT、本地模型), I want 平台提供完整的零件開發指引, so that 我的 AI 能根據指引生成符合合約規格的 `.wasm` 零件,並一次通過沙盒驗收。
#### Acceptance Criteria
1. THE Component_Registry SHALL 在 `GET /components/guide` 端點提供機器可讀的開發指引文件(Markdown 格式),內容包含:零件合約 YAML 完整範例、I/O 模型說明(stdin/stdout JSON)、各語言(TinyGo、Rust、AssemblyScript)的最小可運行範例程式碼、本地測試指令(`wasmtime` 執行方式)、常見錯誤與解法。
2. THE Component_Registry SHALL 在開發指引中明確列出所有禁止行為:網路 syscall、檔案系統 syscall、打包 runtimeQuickJS、Node.js 等)、超過 2MB、混合前後端邏輯於同一零件。
3. THE Component_Registry SHALL 在開發指引中提供 `component.contract.yaml` 的 JSON Schema 定義,讓開發者的 AI 能在提交前自行驗證合約格式正確性。
4. WHEN 一個零件提交驗收失敗,THE Component_Registry SHALL 在錯誤回應中附上指向開發指引對應章節的錨點連結(如 `#syscall-constraints`),讓開發者的 AI 能直接定位修復方向。
5. THE Component_Registry SHALL 提供 `POST /components/validate-contract` 端點,接受 `component.contract.yaml` 內容,回傳格式驗證結果(欄位完整性、schema 合法性、gherkin_tests 最低數量),讓開發者在提交 `.wasm` 前先驗證合約。
6. FOR ALL 開發指引中的程式碼範例,THE Component_Registry SHALL 保證範例能通過 Requirement 2 定義的沙盒驗收流程(指引本身是可執行的 ground truth)。
---
### Requirement 12KBDB Component Registry 整合
**User Story:** As a 系統開發者, I want Component Registry 完全以 KBDB 的 Template/Block/Slot 機制實作, so that 零件狀態與平台其他知識共享同一個持久化層,不引入新的資料庫。
#### Acceptance Criteria
1. THE Component_Registry SHALL 以 KBDB 的 `tpl-component` Template 儲存零件合約,每個零件版本對應一個 Block,Block 的 slots 對應合約的各欄位(id、version、wasi_target、stability、runtime_compat、constraints 等)。
2. THE Component_Registry SHALL 以 KBDB 的 Vectorize 索引零件的 `description``tags` 欄位,支援語意搜尋(如「查詢 Google Sheets 資料」能找到 `gsheets_get_entries`)。
3. WHEN Component_Dispatcher 搜尋零件時,THE Component_Registry SHALL 回傳按「成功率 × 速度評分 × 被調用次數」排序的版本清單,最多回傳 10 個候選版本。
4. THE Component_Registry SHALL 透過 KBDB 的 HTTP API 存取所有資料,不直接操作 D1 SQL,符合平台的 API-First 通訊鐵律。
5. FOR ALL Component_Registry 的讀取操作,THE Component_Registry SHALL 保證在 KBDB 資料不變的情況下,對相同查詢參數回傳相同結果(查詢冪等性)。
@@ -1,411 +0,0 @@
# Implementation Plan: u6u Platform Evolution
## Overview
依照 Bootstrap 順序分四個 Phase 實作,每個 Phase 都是下一個 Phase 的基礎。
技術棧:TypeScript、Hono、Zod、Vitest、fast-check,部署於 Cloudflare Workers。
---
## Phase 0:最小 WASM 執行核心
- [x] 1. 建立 Component Registry 基礎架構(`u6u-core/registry/`
- [x] 1.1 建立 `tpl-component` Template Block(透過 KBDB HTTP API
- 呼叫 KBDB `/templates` 建立 `tpl-component` template(若不存在)
- 定義所有 slot keyscanonical_id、display_name、category、version、wasi_target、stability、runtime_compat、constraints、input_schema、output_schema、gherkin_tests、wasm_r2_key、cypher_binding_url、service_binding_key、description、tags、success_rate、avg_duration_ms、call_count、status、deprecated_at
- _Requirements: 12.1_
- [x] 1.2 實作 `POST /components/validate-contract` 端點
- 以 Zod schema 驗證 component.contract.yaml 所有必填欄位
- 回傳缺失欄位清單(`missing_fields: string[]`
- _Requirements: 1.1, 1.2, 11.5_
- [ ]* 1.3 寫 property test for 合約格式完整性
- **Property 1: 合約格式完整性**
- **Validates: Requirements 1.1, 1.2, 1.4**
- 用 fast-check 生成隨機缺少任意必填欄位的合約物件,驗證 validator 必定拒絕並回傳該欄位名稱
- 用 fast-check 生成包含所有必填欄位的合約物件,驗證 validator 必定通過
- [x] 1.4 實作 `GET /components/guide` 端點
- 回傳 Markdown 格式開發指引(TinyGo 白名單、禁止行為、contract YAML 範例、wasmtime 測試指令)
- _Requirements: 11.1, 11.2, 11.3_
- [x] 1.5 實作 `POST /components` 零件提交端點(沙盒驗收流程)
- 依序執行五個驗收步驟:(a) 體積檢查、(b) 冷啟動時間測量、(c) syscall 掃描、(d) Gherkin 測試執行、(e) runtime 相容測試
- 任一步驟失敗立即停止,回傳 `{ success: false, failed_step, reason, guide_anchor, component_id, version }`
- 通過後以 KBDB HTTP API 寫入 Block`block_id = comp-{id}-{version}`
- 同時上傳 `.wasm` 至 R2slot `wasm_r2_key` 記錄 R2 key
- _Requirements: 2.1, 2.2, 2.3_
- [ ]* 1.6 寫 property test for 沙盒驗收流程正確性
- **Property 2: 沙盒驗收流程正確性**
- **Validates: Requirements 2.1, 2.2**
- 用 fast-check 生成在步驟 N 失敗的零件,驗證回應包含步驟 N 名稱與原因,且不執行步驟 N+1
- [ ]* 1.7 寫 property test for 零件提交冪等性與持久性
- **Property 3: 零件提交冪等性與持久性**
- **Validates: Requirements 2.3, 2.4**
- 用 fast-check 生成通過驗收的零件,提交後讀取合約驗證所有欄位 round-trip 一致
- 對相同 (id, version) 重複提交 N 次,驗證 KBDB 只存在一個 Block
- [x] 2. 實作 WASI preview1 shim 與 WASM 執行核心(`cypher-executor/src/lib/`
- [x] 2.1 實作輕量 WASI preview1 shim`wasi-shim.ts`
- 實作 `fd_read`(從 stdin buffer 讀取)、`fd_write`(寫入 stdout/stderr buffer)、`proc_exit`(拋出 Error)、`random_get``crypto.getRandomValues`
- 其餘 syscall 一律回傳 ENOSYS76
- 不引入任何外部依賴(不使用 `@cloudflare/workers-wasi`
- _Requirements: 3.1, 3.3_
- [x]* 2.2 寫單元測試 for WASI shim
- 測試 `fd_read` 正確讀取 stdin buffer(含多次讀取、邊界條件)
- 測試 `fd_write` 正確寫入 stdout bufferfd=1)與 stderr bufferfd=2
- 測試 `proc_exit` 拋出 Error
- [x] 2.3 實作 Tier 1 WASM 執行器(`wasm-executor.ts`
- 從 R2 fetch `.wasm` ArrayBuffer
- `WebAssembly.compile` 後快取 `WebAssembly.Module`Worker 記憶體,跨請求共享)
- 建立 WASI shim,注入 stdin = `JSON.stringify(input)`
- `WebAssembly.instantiate(module, imports)` → 呼叫 `_start()``main()`
- 從 stdout buffer 讀取輸出,`JSON.parse` 後回傳
- 套用 `max_cold_start_ms` 逾時(`Promise.race`
- _Requirements: 3.1, 3.3, 6.6_
- [ ]* 2.4 寫 property test for Component Dispatcher 路由正確性
- **Property 4: Component Dispatcher 路由正確性**
- **Validates: Requirements 3.1, 3.5, 3.6**
- 用 fast-check 生成合法 JSON input,驗證 WASM 執行路徑輸出與預期語意等效
- [x] 3. 建立 `validate_json.wasm` 第一個真實零件(TinyGo
- [x] 3.1 撰寫 `validate_json` TinyGo 原始碼(`u6u-core/registry/components/validate_json/main.go`
- 只使用白名單 import`os``io``encoding/json`
- 讀取 stdin JSON,解析 `json_string` 欄位,嘗試 `json.Unmarshal`
- 成功輸出 `{"valid":true}`,失敗輸出 `{"valid":false,"error":"..."}`
- _Requirements: 3.1, 11.6_
- [x] 3.2 撰寫 `validate_json` component.contract.yaml
- 包含所有必填欄位、gherkin_testshappy path + error path
- `runtime_compat: ["cf-workers","workerd","wazero"]`
- _Requirements: 1.1, 1.2, 1.4_
- [ ]* 3.3 寫單元測試 for validate_jsonGherkin 場景驗證)
- 測試合法 JSON 輸入回傳 `{"valid":true}`
- 測試非法 JSON 輸入回傳 `{"valid":false,"error":...}`
- [x] 4. Checkpoint — Phase 0 驗收
- 確認 `validate_json.wasm` 能在 CF Workers 環境中透過 WASM 執行器執行
- 確認 Component Registry `/guide``/validate-contract``/components` 端點可用
- 確認所有 Phase 0 測試通過,向使用者確認是否繼續 Phase 1
---
## Phase 1:遷移現有零件(20 個 HTTP → WASM
- [x] 5. 升級 Component Dispatcher 支援雙模式(`cypher-executor/src/lib/component-loader.ts`
- [x] 5.1 重構 `ComponentDescriptor` 型別(移除舊 `http_endpoint`,新增 `component_type`
- 定義 `ComponentType = 'wasm' | 'cypher_binding' | 'service_binding'`
- 新版 `ComponentDescriptor` 欄位:`component_type``wasm_r2_key``runtime_compat``max_cold_start_ms``url`cypher_binding)、`method``binding`service_binding)、`path`
- _Requirements: 3.5_
- [x] 5.2 實作路由決策邏輯(`component-dispatcher.ts`
- 查 Component Registry 取得合約
-`component_type` 分流:`wasm` → WASM 執行器;`cypher_binding` → HTTP POST 到外部 URL`service_binding` → CF Service Binding
- 檢查 `runtime_compat` 是否包含當前 Tier,不包含則回傳 `RUNTIME_INCOMPATIBLE` 錯誤
- _Requirements: 3.4, 6.1, 6.2_
- [ ]* 5.3 寫 property test for Dispatcher 錯誤結構完整性
- **Property 5: Dispatcher 錯誤結構完整性**
- **Validates: Requirements 3.4, 6.4**
- 用 fast-check 生成 (component_id, tier) 組合,當 runtime_compat 不含當前 tier,驗證錯誤回應同時包含 component_id、tier、attempted_paths 三個欄位
- [x] 6. 遷移 20 個內建零件(`u6u-core/builtins/``u6u-core/registry/components/`
- [x] 6.1 為每個零件撰寫 TinyGo 原始碼與 component.contract.yaml(批次作業)
- 每個零件:只用白名單 import、stdin/stdout JSON I/O、附帶 gherkin_tests
- 需要外部 API 的零件(如 gsheets):改用 `cypher_binding` 模式,contract 中記錄 `cypher_binding_url`
- _Requirements: 3.6, 11.6_
- [x] 6.2 透過 `POST /components` 批次提交 20 個零件至 Component Registry
- 每個零件通過沙盒驗收後自動寫入 KBDB
- 驗證 20 個零件的 Gherkin 測試全部通過
- _Requirements: 2.1, 3.6_
- [ ]* 6.3 寫 property test for 歷史版本永久保留不變量
- **Property 8: 歷史版本永久保留不變量**
- **Validates: Requirements 4.5, 10.5**
- 用 fast-check 生成已上架零件,標記為 deprecated 後,驗證 `.wasm` 仍可從 R2 讀取,pinned 引用仍可執行
- [x] 7. 實作 Component Registry 查詢端點
- [x] 7.1 實作 `GET /components/:id``GET /components/:id/versions`
- 透過 KBDB HTTP API 查詢 `tpl-component` blocks
- `/versions` 回傳按「成功率 × 速度評分 × 被調用次數」排序的版本清單(最多 10 個)
- _Requirements: 12.3_
- [x] 7.2 實作 `GET /components/search?q=...` 語意搜尋
- 呼叫 KBDB Vectorize API,以 `description` + `tags` 欄位做語意搜尋
- _Requirements: 12.2_
- [ ]* 7.3 寫單元測試 for 查詢冪等性
- 驗證相同查詢參數在 KBDB 資料不變時回傳相同結果
- [x] 8. Checkpoint — Phase 1 驗收
- 確認 20 個零件全部通過沙盒驗收並存入 KBDB
- 確認 Component Dispatcher 雙模式路由正確(WASM + cypher_binding
- 確認所有 Phase 1 測試通過,向使用者確認是否繼續 Phase 2
---
## Phase 2Cypher 語意擴展 + Multi-Tier Dispatcher
- [x] 9. 擴展 Cypher Triplet Parser`cypher-executor/src/actions/triplet-parser.ts`
- [x] 9.1 新增 EdgeType 定義
- 在現有 `PIPE | IF | FOREACH | CONTINUE` 基礎上新增:`IS_A | ON_SUCCESS | ON_FAIL | ON_CLICK | CALLS_SUBFLOW | CONTAINS | HAS_STYLE | HAS_BEHAVIOR`
- _Requirements: 5.1_
- [x] 9.2 實作 URI 協議解析函數(`resolveComponentId`
- 解析 `component://id``component://id@stable``component://id@pinned:vN``workflow://id``ui://id``style://id`
- 回傳 `{ type, canonicalId, stability, pinnedVersion? }`
- _Requirements: 4.1, 5.6_
- [ ]* 9.3 寫 property test for 零件 URI 解析 Round-Trip
- **Property 6: 零件 URI 解析 Round-Trip**
- **Validates: Requirements 4.1**
- 用 fast-check 生成合法 URI 字串,解析後再序列化,驗證語意等效;驗證解析出的 id、stability、pinnedVersion 與原始 URI 完全一致
- [ ]* 9.4 寫 property test for Cypher 三元組解析 Confluence
- **Property 9: Cypher 三元組解析 Confluence(順序無關性)**
- **Validates: Requirements 5.7**
- 用 fast-check 生成合法三元組集合,用 `fc.shuffledSubarray` 打亂順序,驗證 `parseTriplets` 產生相同節點集合與邊集合
- [x] 10. 擴展 GraphExecutor 執行語意(`cypher-executor/src/graph-executor.ts`
- [x] 10.1 實作 `IS_A` 關係處理
- 從 Component Registry 載入零件合約,以 `input_schema` 驗證節點輸入 context
- _Requirements: 5.2_
- [x] 10.2 實作 `ON_SUCCESS` / `ON_FAIL` 分支執行
-`executeNode` 的 try/catch 中,成功走 `ON_SUCCESS` 邊,失敗走 `ON_FAIL` 邊(傳遞 error context
- _Requirements: 5.3_
- [x] 10.3 實作 `CALLS_SUBFLOW` 子流程呼叫
- 從 KBDB 載入子 Workflow 定義,建立子 GraphExecutor 執行,將輸出合併回主流程 context
- _Requirements: 5.4_
- [x] 10.4 實作 `ON_CLICK` 前端觸發處理
- 接受來自前端 Smart Container 打包的 payload,作為 Workflow initialContext
- _Requirements: 5.5_
- [x] 10.5 實作 `CONTAINS` / `HAS_STYLE` / `HAS_BEHAVIOR` 結構語意解析(不執行,僅記錄圖結構)
- _Requirements: 5.1_
- [x] 11. 實作版本選擇策略(Component Dispatcher 升級)
- [x] 11.1 實作 floating 版本選擇算法
- 從 KBDB 查詢該 id 下所有版本,計算「成功率 × 速度評分 × 被調用次數」,選取最高分版本
- _Requirements: 4.2_
- [x] 11.2 實作 stable / pinned 版本選擇
- `stable`:使用當前標記為 stable 的版本,有更優版本時記錄提示至 KBDB 但不切換
- `pinned:vN`:永遠使用版本 vN,即使已 deprecated
- _Requirements: 4.3, 4.4_
- [ ]* 11.3 寫 property test for 版本選擇策略正確性
- **Property 7: 版本選擇策略正確性**
- **Validates: Requirements 4.2, 4.4**
- 用 fast-check 生成版本集合(各有不同 success_rate、avg_duration_ms、call_count),驗證 floating 選最高分;驗證 pinned:vN 無論其他版本評分如何永遠選 vN
- [x] 12. 實作 Evaluator Agent 與評價迴圈(`cypher-executor/src/actions/`
- [x] 12.1 實作 `execution-evaluator.ts`(擴展現有 `execution-logger.ts`
- Workflow 執行完畢後非同步寫入 KBDB Evaluation Block`tpl-evaluation`
- 記錄:run_id、workflow_id、component_id、verdict、duration_ms、error_message、evaluated_at
- 冪等處理:相同 run_id 不重複建立 Block
- _Requirements: 10.1, 10.6_
- [x] 12.2 實作 Pitfall Block 建立邏輯
- 偵測某零件連續 5 次執行錯誤率 > 50%,建立 `tpl-pitfall` Block
- 版本選擇時降低有 Pitfall 記錄的版本評分權重
- _Requirements: 10.2, 10.3_
- [x] 12.3 實作零件自動 Deprecated / Tombstone 狀態轉換
- 連續 30 天無引用 → 標記 `deprecated`,從預設搜尋移除
- 再 90 天無引用 → 標記 `tombstone`,從所有搜尋移除(pinned `.wasm` 永久保留)
- _Requirements: 10.4, 10.5_
- [ ]* 12.4 寫 property test for 系統操作冪等性
- **Property 11: 系統操作冪等性**
- **Validates: Requirements 10.6, 12.5**
- 用 fast-check 生成 run_id,對相同 run_id 呼叫 Evaluator N 次,驗證 KBDB 只存在一個 Evaluation Block
- 驗證相同查詢參數在資料不變時回傳相同結果
- [x] 13. Checkpoint — Phase 2 驗收
- 確認新 EdgeType 全部可解析執行(IS_A、ON_SUCCESS、ON_FAIL、CALLS_SUBFLOW、ON_CLICK
- 確認版本選擇策略(floating / stable / pinned)行為正確
- 確認 Evaluator Agent 冪等寫入 KBDB
- 確認所有 Phase 2 測試通過,向使用者確認是否繼續 Phase 3
---
## Phase 3:前端畫布(Web Components + 雙面翻轉)
- [x] 14. 建立 Web Components 零件庫(`u6u-core/web-components/`
- [x] 14.1 實作 `<u6u-btn>` Custom Element
- 支援 attributes`label``color``tooltip``workflow``disabled`
- `workflow` 設定且點擊時發出 `u6u:trigger` CustomEvent`{ workflowId, payload }`
- `workflow` 未設定時點擊不發出事件,console 輸出警告
- _Requirements: 8.2, 8.3_
- [x] 14.2 實作 `<u6u-text-input>``<u6u-text-field>` Custom Elements
- 支援 `name``placeholder``value` attributes
- `value` property 可被 `<u6u-card>` 讀取
- _Requirements: 8.1_
- [x] 14.3 實作 `<u6u-card>` Smart Container
- 攔截子元件的 `u6u:trigger` 事件(`stopPropagation`
- 收集同容器內所有 `<u6u-text-input>` / `<u6u-text-field>` 的 name-value 對
- 合併至 payload 後重新發出 `u6u:trigger``bubbles: true, composed: true`
- _Requirements: 8.4_
- [x] 14.4 實作 `<u6u-chart>` Custom Element(基礎版)
- 支援 `data` attributeJSON 字串)、基本折線圖渲染
- _Requirements: 8.1_
- [ ]* 14.5 寫 property test for Web Components 事件與渲染冪等性
- **Property 10: Web Components 事件與渲染冪等性**
- **Validates: Requirements 8.3, 8.4, 8.5**
- 用 fast-check 生成 workflow URI,驗證點擊後 `u6u:trigger` detail.workflowId 與 URI id 完全一致
- 用 fast-check 生成具名 input 集合置於 u6u-card,驗證收集到的 payload 包含所有 name-value 對
- 用 fast-check 生成 attribute 值,對同一元件設定相同值 N 次,驗證渲染結果冪等
- [x] 15. 建立雙面翻轉畫布(`inkstone-admin/frontend/web/`
- [x] 15.1 實作翻轉狀態機(`Canvas.tsx`
- 狀態:`UIView``LogicView`(點擊翻面按鈕切換)
- `LogicView``Editing`(修改三元組)→ `Saving`(確認儲存)→ `LogicView`
- _Requirements: 9.1_
- [x] 15.2 實作邏輯視圖(Cypher 三元組視覺化)
- 顯示零件關聯的 Cypher 三元組(節點連線方式)
- 修改三元組後即時更新 KBDB Workflow Block(透過 KBDB HTTP API
- _Requirements: 9.2_
- [x] 15.3 實作 Workflow URI 選擇器
- 列出 KBDB 中所有可用 Workflow,下拉選單完成 `ON_CLICK >> workflow://id` 綁定
- _Requirements: 9.3_
- [x] 15.4 實作 Smart Container 拖放與自動打包關係顯示
- 拖入同一 `<u6u-card>` 時,邏輯視圖自動顯示 CONTAINS 關係與自動打包說明
- _Requirements: 9.4_
- [x] 15.5 實作零件替換過濾器
- 替換已綁定 Workflow 的 UI 零件時,只顯示具備 `u6u:trigger` 能力的候選零件
- _Requirements: 9.5_
- [x] 15.6 在畫布中整合 u6u Web ComponentsDogfooding
- 畫布 UI 本身使用 `<u6u-btn>``<u6u-card>``<u6u-text-input>` 組裝
- 驗證 Web Components 在 React 19 環境中正確運作
- _Requirements: 8.1, 9.1_
- [ ]* 15.7 寫整合測試 for 畫布翻轉流程
- 測試 UIView → LogicView → Editing → Saving → LogicView 完整狀態轉換
- 測試 KBDB 寫入成功與失敗兩種情境
- [x] 16. Final Checkpoint — 全平台驗收
- 確認四個 Phase 的所有測試通過(`pnpm test` in each service
- 確認 Dogfooding:畫布本身用 u6u Web Components 組裝,每一層都是下一層的第一個用戶
- 確認 KBDB 不變量:仍只有三張表(blocks / templates / slots
- 確認 API-First 鐵律:所有跨服務通訊只透過 HTTP API
- 向使用者確認所有任務完成
---
## Phase 4:邊緣基礎設施(Tier 3 支援)
> 這個 Phase 不在零件遷移範圍內,是獨立的基礎設施工作。
- [ ] 17. Credentials 邊緣支援評估與改寫(`u6u-core/credentials/`
- [ ] 17.1 評估 Tier 3 是否需要 Credentials
- 場景:無人機在有網路時呼叫外部 API(如取得感測器資料),需要 access_token
- 結論:Tier 3 需要在連網時從 Tier 2 取得 Credential,離線時使用本地快取的加密 token
- _Requirements: 7.1, 7.2_
- [ ] 17.2 實作 Credential 本地快取機制(Tier 3 用)
- Tier 3 Go 排程引擎在連網時從 Tier 2 Credentials Worker 取得加密 token
- 存入本地 SQLiteAES-GCM 加密,key 存於設備安全儲存)
- 離線時從本地快取讀取,過期時加入 DTN 佇列等待更新
- _Requirements: 7.1, 7.3_
- [ ] 18. Go Cypher ExecutorTier 3 邊緣執行引擎)(`u6u-core/executor/`
- [ ] 18.1 用 Go 實作 Cypher 三元組解析器
- 解析 `"A >> 關係 >> B"` 格式,建立執行圖(nodes + edges
- 支援 IS_A、ON_SUCCESS、ON_FAIL、CALLS_SUBFLOW、ON_CLICK 語意關係
- 對應 `cypher-executor/src/actions/triplet-parser.ts` 的 Go 版本
- _Requirements: 5.1, 5.7_
- [ ] 18.2 用 Go + Wazero 實作 WASM 零件執行器
- 載入本地 `.wasm` 檔案,透過 Wazero 原生 WASI preview1 執行
- 注入 `u6u` host module`http_request` host function,透過 DTN 或直接 HTTP
- stdin/stdout JSON I/O,與 Tier 1/2 的 `wasm-executor.ts` 語意等效
- _Requirements: 7.1, 7.4_
- [ ] 18.3 實作 DTN 佇列(離線請求緩衝)
- 零件需要網路但當前離線時,寫入本地 SQLite DTN 佇列
- 連網時 Burst 傳輸:批次送出佇列中的請求,接收回應後繼續執行
- _Requirements: 7.2, 7.5_
- [ ]* 18.4 整合測試:validate_json.wasm 在 Wazero 執行
- 確認同一個 `.wasm` 在 Tier 1wasi-shim.ts)和 Tier 3Wazero)執行結果一致
---
## Notes
- 標記 `*` 的子任務為選填,可跳過以加速 MVP 交付
- 每個任務都引用具體的 Requirements 條款以確保可追溯性
- Checkpoint 任務確保每個 Phase 完成後有明確的驗收點
- Property tests 使用 fast-check,每個屬性最少執行 100 次迭代
- 所有跨服務呼叫只透過 KBDB HTTP API,不直接操作 D1 SQL
- TinyGo 零件只使用白名單 import`os``io``encoding/json`
## Phase 5u6u-mcp 對齊新 Registry + u6u-gui 前端
> 壓測前必須完成,讓 AIu6u-mcp)和人類(u6u-gui)都能操作新的 WASM 零件架構。
- [x] 19. 更新 u6u-mcp 對齊新 Component Registry`u6u-mcp/src/tools/`
- [x] 19.1 更新 `u6u_publish_component`
- 舊:呼叫 `/components/publish`payload 為 `{ component_id, gherkin, api_config }`
- 新:呼叫 `POST /components`payload 為 `{ contract: ComponentContract, wasm_base64: string }`
- 新增 `contract``wasm_base64` 參數,更新工具描述說明 TinyGo 零件提交流程
- [x] 19.2 更新 `u6u_search_components`
- 舊:呼叫 `/components/match`(不存在的端點)
- 新:呼叫 `GET /components/search?q={query}`(新 Registry 語意搜尋端點)
- 更新工具描述:AI 可用自然語言搜尋零件(如「查詢 Google Sheets 資料」)
- [x] 19.3 更新 `u6u_get_component`
- 舊:讀舊格式 slots`component_id``name``published_at`
- 新:對齊 `tpl-component` slot 欄位(`canonical_id``display_name``category``version``stability``wasm_r2_key` 等)
- 呼叫新 Registry `GET /components/:id` 端點
- [x] 19.4 新增 `u6u_get_component_guide` 工具
- 呼叫 `GET /components/guide`,回傳開發指引給 AI
- AI 在開發新零件前可先讀取指引,確保生成符合規範的 TinyGo 程式碼
- [x] 20. 建立 u6u-gui 前端(`u6u-gui/`
- [x] 20.1 建立 Cloudflare Pages 專案結構
- React 19 + Vite + Tailwind CSS v4
- 整合 `@u6u/web-components`alias 指向 `u6u-core/web-components/src`
- wrangler.toml 設定 Pages 部署
- [x] 20.2 建立主畫布頁面(`/canvas`
- 整合 `Canvas.tsx`(從 inkstone-admin 移植)
- 連接 Cypher Executor API`POST /cypher/execute`
- 連接 Component Registry API(搜尋、查詢零件)
- AI 操作後(透過 u6u-mcp 修改 KBDB)畫布即時反映變更
- [x] 20.3 建立零件庫頁面(`/components`
- 列出所有已上架零件(呼叫 `GET /components/search`
- 顯示零件合約、評分、版本歷史
- 提供「提交新零件」入口(連結到開發指引)
- [x] 20.4 建立 Workflow 管理頁面(`/workflows`
- 列出所有 Workflow(從 KBDB 查詢 `tpl-workflow`
- 點擊進入畫布編輯
- 顯示執行歷史(Evaluation Block
- [x] 20.5 部署至 Cloudflare Pages
- `pnpm build && npx wrangler pages deploy dist`
- 設定環境變數(KBDB_URL、CYPHER_URL、REGISTRY_URL
-381
View File
@@ -1,381 +0,0 @@
# arcrun — 進度與待辦
> 設計細節見 `arcrun/README.md`(產品說明)和 `arcrun/BETA_TEST.md`(封測指南)。
> 這份文件只記錄:目前狀態、還差什麼、封測能不能啟動。
---
## 一、封測目標場景
封測者是工程師朋友,有自己的網頁,需要後端自動化。目標是他能在 AI 協助下,一次或很少次完成以下完整流程:
1. `acr init` 取得 api_key
2. `acr parts scaffold` 查零件格式,AI 幫寫 workflow YAML
3. 若內建零件不足,`acr recipe push` 增加打外部 API 的 recipe
4. `acr creds push` 上傳 OAuth tokengmail / google_sheets 等)
5. `acr push` 部署 workflow,取得 Webhook URL
6. 網頁 POST /webhooks/named/{name}/trigger,結果存 Google Sheets
---
## 二、場景各步驟驗證狀態
### Step 1acr init → api_key
- [x] `acr init` Standard 模式完成,api_key 存入 `~/.arcrun/config.yaml`
- [x] 已驗證:`mode: standard, api_key: ak_...` 正確
### Step 2acr parts scaffold → AI 看到零件格式
- [x] `acr parts` 列出 21 個零件,完全內建,不依賴 registry.arcrun.dev
- [x] `acr parts scaffold google_sheets` 輸出 spreadsheet_id / range / operation / values 格式與 credentials.yaml 範本
- [x] 已驗證:輸出可直接貼入 YAML
### Step 3acr recipe push → 打外部 API
- [x] `acr recipe push` 上傳成功,回傳 rec_hash
- [x] workflow 使用 `component: rec_xxxxxxxx`acr push 後 trigger 能正確呼叫外部 API
- [x] 已驗證(2026-04-18):httpbin_post recipe → trigger → httpbin.org/post 回傳正確 ✅
### Step 4acr creds push → 自動注入 token
- [x] `POST /credentials` API 完成,以 `{api_key}:cred:{name}` 存入 KV
- [x] Webhook trigger 時 injectCredentials 從 KV 取得 token 自動注入
- [x] `/register` 現在回傳 `encryption_key``acr init` 自動存入 config
- [x] `acr creds push` 從 config 讀 encryption_key,不再需要手動設定環境變數
- [x] 已驗證(2026-04-18):beta@arcrun.dev 帳號完整流程:init → creds push → trigger → credential 注入成功 ✅
### Step 5acr push → Webhook URL
- [x] `acr push workflow.yaml` 部署成功,顯示 Webhook URL 和完整 curl 範例
- [x] config 中的 `component` / 參數在 push 時套入 graph 節點
- [x] 已驗證(2026-04-18):sheet-test workflow push 成功 ✅
### Step 6:網頁 POST → 執行 → 結果到 Google Sheets
- [x] `POST /webhooks/named/{name}/trigger -H 'X-Arcrun-API-Key: ...'` 觸發執行正常
- [x] google_sheets 零件有實作(append row 到 Sheets API
- [x] 已驗證(2026-04-18):trigger sheet-test → 報「缺少 credential」(符合預期,credential 未上傳)✅
- [ ] **未驗證**:真實 google_oauth token + acr creds push → trigger → Google Sheets 實際寫入
- 需要真實 OAuth token 才能完整驗證
---
## 三、封測啟動阻擋項
P0 全部清除才啟動封測。
| # | 項目 | 狀態 | 說明 |
|---|------|------|------|
| 1 | acr parts scaffold 正確輸出 | ✅ 完成 | 21 個零件內建清單 |
| 2 | acr recipe push 端對端 | ✅ 完成 | httpbin_post 驗證通過 |
| 3 | acr creds push 代碼 | ✅ 完成 | 需 ARCRUN_ENCRYPTION_KEY |
| 4 | credential 注入端對端 | ✅ 完成 | 無 token 時錯誤訊息正確 |
| 5 | acr push + webhook trigger | ✅ 完成 | 端對端驗證通過 |
| 6 | acr creds push 實測 | ✅ 完成 | /register 回傳 encryption_keyacr init 自動存入 configCLI 1.0.9|
| 7 | Google Sheets 真實寫入 | ⚠️ 部分驗證 | credential 注入已驗證;實際 Sheets 寫入需真實 OAuth token |
| 8 | 第三方服務認證 recipe | ✅ 完成 | 20 個服務(Notion/Slack/GitHub/OpenAI 等),CLI 1.1.0 |
| **9** | **cypher-executor outbound HTTP fetch 全失效** | ✅ **已解決 2026-05-13**(CF 同 zone 自循環死鎖,改走 workers.dev)| 詳見下方專段 |
| **10** | **multi-node chain context propagation 漏失** | ✅ **已解決 2026-05-13**ON_SUCCESS/ON_FAIL/IF/ON_CLICK 沒 spread baseCtx| 詳見下方專段 |
**目前狀況**P0 全部解決。
- #9 修復方式:component worker URL 從 `*.arcrun.dev`(同 cypher zone)改走 `arcrun-{name}.{WORKER_SUBDOMAIN}.workers.dev`(避開同 zone 自循環)
- #10 修復方式:4 個 edge type 補 `{...baseCtx, ...result}`,跟 PIPE/FOREACH 一致
兩個 P0 解完 mira 7 節點 workflow 端對端通(含真 Claude 16 秒呼叫)。
---
### ✅ P0 #92026-05-13 已解決):cypher-executor outbound fetch 全失效
**完整事件報告(含誤判路徑)**[docs/incidents/2026-05-13-cypher-outbound-522.md](../../../docs/incidents/2026-05-13-cypher-outbound-522.md)
**修復方式**cypher-executor fetch component worker 從 `*.arcrun.dev`(同 zone)改走 `arcrun-{name}.{WORKER_SUBDOMAIN}.workers.dev`。對外 `cypher.arcrun.dev` 不變,用戶 0 感知。
**改動檔案**2026-05-13):
- `cypher-executor/src/lib/component-loader.ts``wasmWorkerUrl(canonicalId, subdomain)` 簽名加 subdomain 參數 + URL pattern 改 workers.dev
- `cypher-executor/src/actions/auth-dispatcher.ts`:同步新簽名
- `cypher-executor/src/types.ts``Bindings``WORKER_SUBDOMAIN: string`
- `cypher-executor/wrangler.toml``[vars]``WORKER_SUBDOMAIN = "uncle6-me"`
- 5 個 component worker 在 dashboard 啟用 workers.dev URLkbdb-get / kbdb-ingest / kbdb-create-block / kbdb-patch-block / claude-api**未來新 component 也都要開**
**驗證**cypher-executor → kbdb-get / claude-api 從 522 → 200。mira `acr run wiki_synthesis` 5 節點 workflow 跑通前 3 節點(kbdb_get chain)。
**Self-hosted fork 注意**:必須改 `wrangler.toml [vars] WORKER_SUBDOMAIN` 為自己的 CF 帳號 subdomain,並把所有 component worker 在 dashboard 啟用 workers.dev URL。
---
### ✅ P0 #102026-05-13 已解決):multi-node chain context propagation 漏失
**現象**cypher binding workflow 從第 2 個節點開始,原始 input contexttop-level `api_key` / `mira_token` 等)丟失,下游節點 `{{api_key}}` 模板原文未替換傳給零件 → 401 Unauthorized 或類似錯。
**測試重現**
```yaml
flow:
- "input >> ON_SUCCESS >> n1"
- "n1 >> ON_SUCCESS >> n2"
config:
n1: { component: kbdb_get, api_key: "{{api_key}}", block_id: "{{b1}}" }
n2: { component: kbdb_get, api_key: "{{api_key}}", block_id: "{{b2}}" }
context: { api_key: "ak_xxx", b1: "...", b2: "..." }
```
n1 收到 ctx 含 `api_key / b1 / b2` ✓ → 跑通。
n2 收到的 ctx 只有 `n1.output spread`blocks/count/success/block_id),**`api_key / b1 / b2` 不見**`{{api_key}}` 原文傳到零件回 401。
**根因**`graph-executor.ts` 在 PIPE / FOREACH 邊類型已修「baseCtx result」,但 **ON_SUCCESS / ON_FAIL / IF / ON_CLICK 四個 edge type 沒套同模式**,直接把 `result` 當下游 ctx 傳,丟掉原始 context。
**修法**`cypher-executor/src/graph-executor.ts` line 407 / 415 / 423 / 472):
```typescript
// 改前
result = await this.executeNode(nextNode, graph, result, ...);
// 改後(同 PIPE/FOREACH 模式)
const baseCtx = (typeof context === 'object' && context !== null) ? context as Record<string, unknown> : {};
const baseResult = (typeof result === 'object' && result !== null) ? result as Record<string, unknown> : {};
const mergedCtx = { ...baseCtx, ...baseResult };
result = await this.executeNode(nextNode, graph, mergedCtx, ...);
```
**驗證**mira `acr run wiki_synthesis` 7 節點 workflow 端對端跑通(16 秒,含真 Claude 呼叫)。每個節點都拿到正確 `api_key` 不再 401。
**歷史脈絡**:類似問題 2026-05-07 commit e8fca33 在 FOREACH edge 已修一次("FOREACH preserves outer context"),但當時沒同步處理另外 4 個 edge type。本次補完。
---
### ✅ P0 #10 補完三個衍生問題(2026-05-13 晚 ~ 2026-05-14
P0 #10 修完後 mira 嘗試做 wiki 多段結構,又踩出三個 cypher binding 設計缺陷。**都是同一天解掉**。
#### A. interpolateData() 不遞迴 nested object
**現象**`set` / `kbdb_create_block``values: { text: "{{classify.data.text}}" }``tags_json: ["facet:{{paragraph.facet}}"]` 等 nested config 內的 `{{x}}` 不被替換,原文傳給零件。
**根因**`interpolateData()` 只 iterate top-level,對非 string 值(object / array)直接 pass-through 不下沉。
**修法**:拆 `interpolateString` + `interpolateValue`(遞迴 object / array),`interpolateData` 改 call `interpolateValue`
**測試**`set values: { text: "hello {{name}}", arr: ["item {{name}}"] }``name=world` → 全展開。
#### B. ctx 沒存上游 output 的 node id namespace
**現象**`{{classify.data.text}}` 找不到上游 classify 的 output;只能用 `{{data.text}}`(直接 spread 取),但會被下個節點覆蓋,多節點 chain 用不了。
**根因**`propagateCtx` 只把上游 result spread 進 ctx,沒額外存 `[node.id]: result`
**修法**`propagateCtx` 改回傳 `{ ...baseCtx, ...baseResult, [upstreamNodeId]: upstreamResult }`。讓下游能用 `{{node_id.data.text}}` 從 namespace 取,永不被覆蓋。
**測試**5 節點 chain 用 `{{load_schema.blocks.0.content}}` / `{{classify.data.text}}` 全展開。
#### C. FOREACH 找 iterable 只看 result,不看 ctx + 不看 nested
**現象**mira wiki_synthesis 雙重 FOREACH(外層 `對每個 paragraph`、內層 `對每個 triplet`),外層 OK,內層跑 0 次。
**根因 (C1)**`getIterableFromContext(result, key)` 只看當前節點 output。`result``create_paragraph` output`{data, success}`),不含 paragraphs。但 `paragraphs` 早就在 ctx 從 classify spread 來。
**根因 (C2)**:當外層 FOREACH 把 `paragraph` item 注入 ctx,內層 FOREACH 要找 `paragraph.triplets``getIterableFromContext` 只看 top-level,看不到 `paragraph` 物件裡的 `triplets`
**修法**
- (C1) FOREACH `result` 找不到 iterable → fallback 找 `context`
- (C2) `getIterableFromContext` 加一輪「掃 ctx 內每個 object 找 nested key」
**測試**mira wiki_synthesis 3 層樹(wiki-page → paragraphs → triplets)端對端跑通,KBDB 內驗證 `物理 AI` wiki 有 2 段 paragraph + 4 個 tripletparent_id 正確接到對應 paragraph。
#### Edge type 一致化
`propagateCtx(context, result, upstreamNodeId)` helper5 個 edge typePIPE / ON_SUCCESS / ON_FAIL / IF / ON_CLICK / FOREACH)全部用同一 function 組下游 ctx。**未來新 edge type 必須用這 helper**,避免再漏。
#### CLI validator 同步
`cli/src/lib/yaml-parser.ts` validateRelations 加 regex 支援 `對每個 X` / `FOREACH X` 迭代器命名(之前 validator 字串完全比對擋住,但 graph-builder 執行端早已支援)。
---
### 三-A、P1 待改進(不擋封測,但 mira 已踩到)
#### ✅ P1 #3cypher-executor `scheduled()` handler2026-05-14 完成)
**原痛點**cron 零件只做 expression validationcypher-executor 沒 `scheduled()` handler。寫了 cron 首節點的 workflow 不會真的跑。
**之前的 workaround**(已撤):mira 寫了個 `/mira/wiki-from-raw` route 從前端 fire-and-forget 觸發 wiki_synthesis。但這違反「一律 arcrun-native」原則,也讓 arcrun 永遠補不齊缺失。**已刪 route,回 arcrun-native 路線**。
**落地**
1. `wrangler.toml``[triggers] crons = ["* * * * *"]`(每分鐘 tick
2. `src/lib/cron-match.ts`5 欄位 cron expression matcher(支援 `*` / `N` / `*/N` / `1-5` / `5,10` 組合)
3. `src/scheduled.ts`scheduled handler 掃 KV `cron-idx:` prefix,比對 controller.scheduledTime,匹配就 `executeWebhookGraph` 背景跑
4. `routes/webhooks-named.ts`acr push 偵測首節點是 cron 零件 → 抽 `cron_expr` 存進 record + 額外寫 `cron-idx:{api_key}:{name}` 輕量 index entry。DELETE 一併清理
5. `src/index.ts`export default 改 `{ fetch, scheduled }`
6. cypher-executor 自己加 `workers_dev = true` 給未來 self-trigger 用(fork 用 path-based 子 trigger 也走 workers.dev 避同 zone
**workflow YAML 慣例**
```yaml
flow:
- "my_cron >> ON_SUCCESS >> downstream_node"
config:
my_cron:
component: cron
cron_expr: "*/5 * * * *" # 每 5 分鐘
```
acr push 就會自動建立 cron-idx 並開始定時觸發。
**測試**`tests/arcrun-test/cron_heartbeat.yaml` — 每分鐘 fire 一次 + set 節點 log。
`wrangler tail arcrun-cypher-executor` 應看 `[scheduled] trigger cron_heartbeat ...`
**對應 use case**mira `mira_feed_watcher`(7B.3h,下一輪做)/ RSS 每日抓 / voice-stt 每小時掃 / 等所有 cron-driven source。
---
#### P1 #1workflow 缺 IF/branch 能力(2026-05-14 mira 7B.3f 提出)
**現象**mira 想做「找有則 PATCH 沒則 CREATE」(index-entry upsert),arcrun 目前只有 `ON_SUCCESS` + `對每個 X`FOREACH+ 已存在但壞掉的 `if_control`(見已知限制 #1),沒有 `>> ON_TRUE >>` / `>> ON_FALSE >>` 條件路由。
**短期 workaround**(已採用,2026-05-14):建 `kbdb_upsert_block` 零件,把分支邏輯封進零件內部(GET by page_name → 找到 PATCH 沒找到 POST)。caller 看到的是單純的 upsert 介面。
**長期解**:升 `if_control` false branch 路由 / 加 `>> ON_TRUE >>` edge type,讓 workflow 層可表達分支。對未來所有「找則改否則建」/「條件分流」場景都會撞到,不只 mira。
**位置**cypher-executor/src/graph-executor.ts edge type 處理(5 個 edge type 抽出 `propagateCtx` 後新增 IF 應該不難)+ cli/src/lib/yaml-parser.ts validator。
---
#### P0 #112026-05-14 已解決):interpolateString stringify array 撐爆下游
**現象**mira_feed_watcher 用 `items: "{{list_raws.blocks}}"` 把 kbdb_get 拿到的 blocks 陣列傳給 filter 零件。watcher 跑 264ms 完成、0 raw 處理。
**根因**`interpolateString` 看到模板就用 `String.replace`,非 string 值(陣列)一律 `JSON.stringify`。filter 零件收到字串 `"[{...},{...}]"` 不是 arrayitems 被忽略 → 0 matches → FOREACH 跑 0 次。
**修法**`interpolateString` 加 single-ref pass-through 規則:若整個值是純單一 `{{x}}` 引用,回 raw value(保留 array / object 型別)。多 ref / 混合文字仍 stringify 拼接字串。
**測試**mira_feed_watcher 推到 prod 後下一個 cron tick 觀察 wiki-processed tag 是否在 raws 上出現。
---
### 三-B、新零件加入紀錄
| 日期 | 零件 | 動機 | 對應 SDD |
|---|---|---|---|
| 2026-05-14 | `kbdb_upsert_block` | mira 7B.3f index-entry per-entity upsert,繞過 workflow 缺 IF/branch 能力(P1 #1)。內部 GET by page_name → 找到 PATCH 沒找到 POST。page_name 當 idempotency key。 | polaris/mira/.agents/specs/mira-app/design.md §3.5.12.4.1 |
**新零件 checklist(避免 P1 #1 重蹈 kbdb_upsert_block 漏白名單覆轍)**
每加一個 API/data 零件(不是 logic primitive),都要:
- [ ] `registry/components/{name}/main.go` + `component.contract.yaml` + `go.mod`
- [ ] `tinygo build -target=wasi`
- [ ] `.component-builds/{name}/` 完整 4 檔(`wrangler.toml``workers_dev = true` + `pnpm-lock.yaml` + `tsconfig.json` + `src/index.ts`
- [ ] **`cypher-executor/src/lib/component-loader.ts``WASM_HTTP_RUNNER_IDS` 加 canonical_id**(漏這條 cypher-executor 永遠拋「找不到零件」,端對端會死靜悄悄)
- [ ] `acr validate workflow.yaml`
- [ ] 直接 curl `https://{kebab}.arcrun.dev` + `https://arcrun-{kebab}.{WORKER_SUBDOMAIN}.workers.dev` 都 200
---
### 三-C、P1 #2workers_dev = true 全 component 自動化(2026-05-14 已收)
**原痛點**:每新部署一個 component worker,要去 CF Dashboard 手動 Enable workers.dev URL,否則 cypher-executor fetch 該 worker 會 404。
**解**32 個 `.component-builds/*/wrangler.toml` 全部加 `workers_dev = true`。CI 每次 deploy 自動啟用對應 workers.dev URL,零手動。
**未來新 component**:模板 (`component-worker-template/`) 應該預設帶 `workers_dev = true`,新人 fork 不會踩。已列入「新零件 checklist」第 3 條。
**為何不走 `*.acr-comp.uncle6.me` 自訂 zone**CF Universal SSL 只發一層子域,sub-sub `*.acr-comp.uncle6.me` 不蓋;要 ACM ($10/月) 才能簽。違反 arcrun「fork 後 self-host 用 free tier 跑得起來」核心目標。workers_dev=true 走 CF 默認的 workers.dev certfree tier OK,更乾淨。
---
### 原 P0 #9 調查紀錄(保留作歷史參考)
**現象**cypher-executor 的 `makeHttpRunner` (`cypher-executor/src/lib/component-loader.ts:142`) 對任何 outbound URL fetch 都回 CF **522 (origin timeout, ~1000ms)**
**測試矩陣**mira repo `polaris/mira/arcrun/wiki_synthesis.yaml` 端對端壓測時發現):
| 路徑 | 結果 | 證明 |
|---|---|---|
| 本機 curl → kbdb-get.arcrun.dev | 200 (22ms) | KBDB worker 本身健康 |
| cypher-executor → kbdb-get.arcrun.dev (HTTP) | **522** (1002ms) | outbound HTTP fetch 壞 |
| cypher-executor → claude-api.arcrun.dev (HTTP) | **522** | 同 zone 也壞 |
| cypher-executor → httpbin.org (外部) | **522** | 不只是 same-account loop |
| cypher-executor → string_ops (Service Binding) | 200 ✅ | SVC_* 路徑正常 |
| acr run hello (built-in via SB) | ✅ | hello.yaml 仍跑得通 |
**衍生小 bug**`set` 零件 input schema 變了(要 `assignments` 陣列或 `values` 物件,不是 `value`)。tests/arcrun-test/hello.yaml 跑 string_ops 沒踩到。
**影響範圍(封測啟動阻擋)**
- 任何用戶 workflow 含 outbound HTTP 都壞:
- 用戶 `acr recipe push` 後 trigger → 打外部 API 全 522(推翻 P0 #2 4/18 紀錄)
- 用戶要存資料進 arcrun 內建 KBDB → 522mira 7B.3c 卡這裡)
- 任何 auth primitive 走獨立 Worker URL 路徑 → 522
- Google Sheets 寫入 → 522(推翻 P0 #7「待真實 OAuth 驗證」評估,根本還沒到 OAuth 步驟就壞)
- 只有「全內建邏輯零件 + 純 service binding」workflow 還能跑
**根因(2026-05-13 確認):5/8-5/9 9 次 manual `wrangler deploy` 把含 WIP bug 的 cypher-executor 推上 prod**
調查路徑(按時序):
1. 一開始懷疑 free tier CPU cap10ms / invocation)→ 對照測試「5 節點 SB chain」跑了 2.2 秒卻通過,**推翻**
2. 換懷疑 CF zone 規則 / Bot Fight Mode → dashboard bindings 乾淨無攔截,**推翻**
3. 用戶補繳費恢復 Workers Paid → 重測仍全 522**徹底排除付費假設**
4. 看 dashboard Version History5/8 4 次 + 5/9 5 次 = **9 次 manual `wrangler deploy by uncle6.me`**
5. 對照 GitHub Actions4/24 後完全沒 deploy(最後是 commit e222116 `fix(wasi-shim)`
6. 對照 git:本機 main **領先 origin/main 3 commits 未 push**,含:
- `497f92a feat(arcrun): recipe system + resumable workflow + component registry canon`
- `e8fca33 feat(cypher): 3-node wiki workflow end-to-end (FOREACH + nested interp + unified parsing)`
- `519423c feat(arcrun): mira wiki page with tag filter + accumulated WIP`(自描含 `cypher-executor: auth-dispatcher / wasi-shim adjustments (WIP)`
**結論**:那 9 次 manual deploy 把上面 3 個 unpushed commit 的 cypher-executor 改動推上 prod,其中至少一個改動破壞了 outbound fetch(最可能是 519423c WIP 內的 wasi-shim / auth-dispatcher 改動)。GitHub Actions 因為沒 push 沒跑,CI 沒 catch4/18-4/24 那段 SDD「驗證通過」的紀錄是 truth,現在 prod 是壞的版本。
**為何 SB 路徑沒事 / HTTP 路徑全死**SB 走 cypher-executor 內部 service binding API`env.SVC_X.fetch()`),不經過 outbound HTTP code path。HTTP 路徑走 `makeHttpRunner` (component-loader.ts:142) 的 `fetch(url, ...)`,這條路被 WIP code 弄壞。具體壞在哪要 diff 那 3 個 commit 的 cypher-executor 改動才知道。
**驗證 wrangler tail 證據**trigger 任何 outbound HTTP 的 graphcypher-executor 自己 `wallTime: 497ms, cpuTime: 2ms, outcome: ok`、無 logs、無 exceptions。代表 cypher-executor 把「fetch 失敗的 522 response」當作 component 正常輸出包回 client,自己沒撞任何錯。
**解法(三選一)**
- **A. Rollback prod 到 4/24 的 e222116** — CF dashboard → arcrun-cypher-executor → Deployments → 找 4/24 那筆 → Rollback。5 分鐘恢復 outbound fetch,丟失 wiki workflow / recipe / resumable 等 cypher 端 WIP 改動(但前端、registry components、KBDB blocks 都不丟,因為它們是別的 worker / 別的儲存)。**richblack 操作。**
- **B. Diff 3 個 unpushed commit 找出壞掉的改動修掉** — 不丟功能,但要動 src code 走 SDD 協議,30min - 數小時。
- **C. 架構切換**mira 老闆 2026-05-13 提的):sub-workflow 自殺交棒模式,cypher-executor 不再做集中 graph executor。從根本繞開「cypher-executor 一個 invocation 跑長 graph」這條脆弱路徑。一勞永逸但是大改。
**衍生小 bug 仍要修**(跟付費無關):`set` 零件 input schema 變了(要 `assignments` 陣列或 `values` 物件,不是 `value`)。要嘛 update set 零件 contract 容錯,要嘛文件化新 schema。
**為什麼這直接擋封測**
封測場景 Step 6「網頁 POST → 結果存 Google Sheets」走 google_sheets 零件 (HTTP outbound to googleapis.com)。如果 cypher-executor outbound 全壞,**封測者跑任何含外部 API 的 workflow 都會 522**,不是「Google Sheets 實際寫入未驗證」級別的小事。
**也直接擋 mira**[polaris/mira/.agents/specs/mira-app/tasks.md] 7B.3c-fwiki 合成 workflow)卡這裡。
---
## 四、封測前 P3(啟動當天)
- [ ] 用封測者 email 呼叫 `/register`,取得 api_key
- [ ] 將 ARCRUN_ENCRYPTION_KEY 以安全方式提供給封測者
- [ ] 確認聯絡管道
---
## 五、已知限制(封測期間不修)
1. `if_control` false branch 不路由(條件 false 時後續節點不執行)→ 升級計畫見 P1 #12026-05-14 mira 用 `kbdb_upsert_block` workaround
2. 多節點 context 不自動解包(上游輸出 flat merge,下游需從 `data.result` 取值)
3. 用戶自製邏輯零件(Phase 5)封測後才實作
---
## 六、實作進度
| Phase | 內容 | 狀態 |
|-------|------|------|
| 0 | Workers 部署、CI/CD、DNS | ✅ |
| 1 | CLI 基礎(init / validate / run / parts | ✅ |
| 2 | /register、/cypher/execute、21 個零件 | ✅ |
| 3 | Service Binding 架構、{{variable}} 插值、ON_FAIL 修正 | ✅ |
| 4 | 動態 Recipe KVCRUD)、acr recipe 指令 | ✅ |
| 5 | 用戶自製邏輯零件(WASM push) | ⏸ 封測後 |
| 6 | Credential 多租戶({api_key}:cred:{name})、acr creds push | ✅ |
| 7 | acr parts 內建清單、acr parts scaffold | ✅ |
| 8 | /webhooks/named、acr push 改版、config 套入 graph | ✅ |
### CLI 版本
| 版本 | 變更 |
|------|------|
| 1.1.0 | auth recipe 系統:20 個服務預建(Notion/Slack/GitHub/OpenAI/Google SA 等);acr auth-recipe 指令 |
| 1.0.9 | /register 回傳 encryption_keyacr init 自動儲存;creds push 不需手動設環境變數 |
| 1.0.8 | acr push → webhooks/namedconfig 套入 graphacr parts 內建清單 |
| 1.0.7 | acr creds push → POST /credentials |
| 1.0.6 | acr recipe push / list / delete |
| 1.0.5 | hello.yaml 改 string_ops--version 修正 |
| 1.0.4 | config/context 分離 |
| 1.0.3 | 初始發布 |
-274
View File
@@ -1,274 +0,0 @@
# Auth Recipe System — SDD
> 文件類型:SDDSoftware Design Document
> 建立:2026-04-19
> 狀態:實作中
---
## 一、目標
封測前完成,讓封測者碰到「我要連 X 服務」都有辦法,而不是「還沒做」。
**精神**`http_request` 是容器零件,auth recipe 是「如何對這個服務認證」的設定層,兩者分離。新增一個服務 = 寫一份 YAML,不需要改程式碼、不需要重新部署 Worker。
---
## 二、三層模型
```
Layer 3: Auth Recipe (YAML/JSON in RECIPES KV)
公共,描述「如何對某服務認證」
key: auth_recipe:{service}
例: auth_recipe:notion, auth_recipe:slack
↓ 引用
Layer 2: Auth Primitive (TypeScript in Worker)
四個通用認證邏輯:static_key | oauth2 | service_account | mtls
封測只做 static_key 和 service_account (Google JWT)
↑ 使用
Layer 1: Tenant Secret (CREDENTIALS_KV)
每個 tenant 自己的加密 credential
key: {api_key}:cred:{name}
```
---
## 三、Auth Recipe Schema
```typescript
interface AuthRecipeDefinition {
kind: 'auth_recipe'; // 區別 RecipeDefinition 用
service: string; // canonical_id, e.g. "notion"
version: number;
primitive: 'static_key' | 'oauth2' | 'service_account' | 'mtls';
base_url: string;
display_name?: string;
description?: string;
// service_account 用
service_account_kind?: 'google_jwt';
token_exchange?: {
endpoint: string; // e.g. https://oauth2.googleapis.com/token
scopes: string[];
};
required_secrets: Array<{
key: string; // CREDENTIALS_KV 的名稱
label: string; // UI/CLI 顯示
type?: 'string' | 'json_blob'; // default: string
help?: string;
help_url?: string;
}>;
inject: {
header?: Record<string, string>; // "Authorization": "Bearer {{secret.token}}"
query?: Record<string, string>;
body?: Record<string, string>;
// path:注入 endpoint URL path 的 secret2026-05-29 加)。
// 解 telegram 類「token 在 URL path」(/bot{token}/)—— header/query/body 都不適用。
// key = 模板變數名,API recipe 的 endpoint 用 {{auth.K}} 引用。
// 例:auth_recipe:telegram inject.path = { bot_token: "{{secret.telegram_bot_token}}" }
// recipe:telegram_send endpoint = "https://api.telegram.org/bot{{auth.bot_token}}/sendMessage"
path?: Record<string, string>;
};
created_at: number;
updated_at: number;
}
```
**Template 語法**
- `{{secret.KEY}}` → 從 tenant 的 CREDENTIALS_KV 解密取值
- `{{runtime.access_token}}` → service_account JWT exchange 後取得的短期 token
---
## 四、KV 儲存
沿用現有 `RECIPES` KV namespace,不新增 binding。
```
auth_recipe:{service} → AuthRecipeDefinition JSON
```
與現有 `recipe:{id}` / `idx:{hash}` 的 key 不衝突。
---
## 五、執行流程
### 5.1 static_key(涵蓋 ~80% 服務)
```
trigger → graph-executor
→ injectCredentials(componentId, input, env, apiKey)
→ resolveAuthRecipe("notion", RECIPES KV)
→ 取得 required_secrets: [{key: "notion_token", ...}]
→ 從 CREDENTIALS_KV 讀 "{api_key}:cred:notion_token"
→ AES-GCM 解密
→ 展開 inject.header templates ({{secret.notion_token}} → 實際值)
→ 注入 _auth_headers, _auth_query, _auth_body 到 input
→ makeAuthRecipeRunner(recipe)
→ 合併 _auth_headers 到 fetch headers
→ 呼叫 recipe.base_url + input._path
→ 回傳結果
```
### 5.2 service_accountGoogle 家族)
```
injectCredentials
→ resolveAuthRecipe("google_sheets_sa", RECIPES KV)
→ 解密 service_account_json (JSON blob)
→ signGoogleJwt(serviceAccountJson, scopes) via crypto.subtle (RSASSA-PKCS1-v1_5 + SHA-256)
→ POST token_exchange.endpoint → 取得 access_token
→ 展開 inject.header: { Authorization: "Bearer {{runtime.access_token}}" }
→ 注入 _auth_headers
```
---
## 六、Context key 慣例
注入後的認證資訊以 `_auth_` 前綴攜帶,不污染業務欄位:
| Key | 說明 |
|---|---|
| `_auth_headers` | `Record<string, string>` — 要合併進 fetch headers |
| `_auth_query` | `Record<string, string>` — 要附加到 URL query string |
| `_auth_body` | `Record<string, string>` — 要合併進 request body |
| `_auth_path` | `Record<string, string>` — endpoint URL path 用(2026-05-29 加)。`makeRecipeRunner` 的 endpoint interpolate 用 `{{auth.K}}` 從這裡取值 |
`makeAuthRecipeRunner` / `makeRecipeRunner` 在發出 fetch 前讀取這些 `_auth_*` 欄位,
之後從 auto-body 中剔除所有 `_` 前綴欄位(不洩漏給下游)。
## 七、API recipe 的 auth_service(多 recipe 共用一把 auth2026-05-29 加)
`RecipeDefinition``auth_service?: string` 欄位:API recipe **自報它屬於哪個服務**
auth-dispatcher 用它查 `auth_recipe:{auth_service}`,而非假設 componentId == service name。
- 讓多個 recipe 共用同一把 auth`recipe:kbdb_get` / `kbdb_create_block` 都設 `auth_service: "kbdb"`
→ 共用唯一的 `auth_recipe:kbdb`,加新 action 不必複製 auth recipe。
- auth-dispatcher 解析順序:先查 `recipe:{componentId}``auth_service`,有就用它;
沒有則 fallback 把 componentId 當 service name(向後相容舊行為)。
- 這是「服務身分標籤」非「許可清單」:auth_recipe 只定義「怎麼認證」,不含「誰准用」。
授權由發 API key 的服務裁決,arcrun 不做內部授權判斷(見 DECISIONS.md「arcrun 不做授權判斷」)。
---
## 七、向後相容
- 現有 `BUILTIN_API_RECIPES`gmail, google_sheets, telegram, line_notify**不動**
- 現有 `BUILTIN_CREDENTIALS_MAP` **不動**
- auth recipe 解析在 component-loader step 5.5(新增),在 step 6 KV recipe 和 step 7 builtin 之前
-`auth_recipe:{service}` 不存在 → 繼續往下走,行為與現在完全相同
---
## 八、新增/修改的檔案
| 檔案 | 類型 | 說明 |
|---|---|---|
| `cypher-executor/src/routes/recipes.ts` | 修改 | 加 `AuthRecipeDefinition` 型別、`resolveAuthRecipe``/auth-recipes` CRUD routes |
| `cypher-executor/src/actions/credential-injector.ts` | 修改 | 加 auth recipe 分支:static_key + service_account |
| `cypher-executor/src/lib/jwt-signer.ts` | 新增 | Google JWT signing via crypto.subtle |
| `cypher-executor/src/lib/component-loader.ts` | 修改 | step 5.5 auth recipe lookup + `makeAuthRecipeRunner` |
| `cypher-executor/src/lib/auth-recipe-seeds.ts` | 新增 | 20 個常用服務的 auth recipe 定義 |
| `cli/src/commands/auth-recipe.ts` | 新增 | `acr auth-recipe list/info/scaffold` |
| `cli/src/commands/parts.ts` | 修改 | `cmdPartsScaffold` fallback 到 auth recipe |
| `cli/src/index.ts` | 修改 | 註冊 auth-recipe 指令 |
---
## 九、封測前預計的 Auth Recipe 清單(20 個)
### static_key 類(~80% 服務)
| service | 認證方式 | credential key |
|---|---|---|
| `notion` | Bearer token (header) | `notion_token` |
| `slack` | Bot Token (Bearer) | `slack_bot_token` |
| `github` | PAT (Bearer) | `github_token` |
| `openai` | API key (Bearer) | `openai_api_key` |
| `anthropic` | API key (x-api-key) | `anthropic_api_key` |
| `airtable` | PAT (Bearer) | `airtable_token` |
| `discord` | Bot token ("Bot TOKEN") | `discord_bot_token` |
| `stripe` | Secret key (Bearer) | `stripe_secret_key` |
| `twilio` | AccountSid + AuthToken (Basic Auth) | `twilio_account_sid`, `twilio_auth_token` |
| `sendgrid` | API key (Bearer) | `sendgrid_api_key` |
| `hubspot` | Private App token (Bearer) | `hubspot_token` |
| `linear` | API key (Bearer) | `linear_api_key` |
| `shopify` | Admin API token (X-Shopify-Access-Token) | `shopify_access_token` |
| `resend` | API key (Bearer) | `resend_api_key` |
| `supabase` | Service role key (Bearer + apikey) | `supabase_service_key` |
| `typeform` | PAT (Bearer) | `typeform_token` |
| `jira` | API token + email (Basic Auth) | `jira_api_token`, `jira_email` |
### service_account 類(Google 家族,JWT signing
| service | scopes | credential key |
|---|---|---|
| `google_sheets_sa` | spreadsheets | `google_service_account` |
| `google_gmail_sa` | gmail.send | `google_service_account` |
| `google_drive_sa` | drive | `google_service_account` |
> 注意:三個 Google 服務可共用同一個 `google_service_account` credential,只是 scope 不同。
---
## 十、實作進度
### Server (cypher-executor)
- [x] `AuthRecipeDefinition` 型別 + `resolveAuthRecipe`
- [x] `/auth-recipes` CRUD routes
- [x] `injectFromAuthRecipe` — static_key primitive
- [x] `lib/jwt-signer.ts` — Google JWT via crypto.subtle
- [x] `injectFromAuthRecipe` — service_account primitive
- [x] `makeAuthRecipeRunner` in component-loader
- [x] step 5.5 in createComponentLoader
- [x] auth-recipe-seeds.ts (20 services)
- [x] seed script / deploy seeds to KV2026-04-19 全部 ✅)
### CLI (arcrun)
- [x] `commands/auth-recipe.ts` — list / info / scaffold
- [x] 更新 `commands/parts.ts` — scaffold fallback
- [x] 更新 `index.ts` — 註冊指令
- [x] 版本升 1.1.0
- [x] npm publisharcrun@1.1.0
### 驗證
- [ ] notion (static_key) 端對端
- [ ] google_sheets_sa (service_account) 端對端
- [ ] 舊有 google_sheets builtin 向後相容確認
---
## 十一、長期演進:TinyGo WASM Primitive(封測後)
> 參考:`docs/user_requirements/arcrun/credential_parts.md`
**目前封測版**Layer 2 primitive 邏輯在 `cypher-executor` TypeScript 中實作(`credential-injector.ts`)。
**長期目標**:四個 primitive 各自編譯為獨立 TinyGo WASM,取代現有 TS 實作:
```
arcrun/registry/components/auth_static_key/ ← TinyGo WASM
arcrun/registry/components/auth_oauth2/ ← TinyGo WASM
arcrun/registry/components/auth_service_account/ ← TinyGo WASM
arcrun/registry/components/auth_mtls/ ← TinyGo WASM
```
每個 primitive 實作統一 interface`Authenticate` / `NeedsRefresh` / `Refresh` / `Test`)。
切換時 `cypher-executor``injectFromAuthRecipe` 改為呼叫對應 WASM,邏輯不變。
**何時做**:封測驗證完成、TinyGo crypto 支援確認後(特別是 RS256/ES256 JWT signing)。
在此之前,**不建立任何 TypeScript SDK 或 Python SDK 來包裝 credential 邏輯**。
### 禁止的做法
- ❌ 建立 `js-sdk/``python-sdk/` 包裝 credential 加解密
- ❌ 在 client 端重實作 AES-GCM encrypt/decrypt
- ❌ 用 TypeScript 重寫已計劃用 TinyGo 實作的 primitive 邏輯
@@ -1,178 +0,0 @@
# Design Document: Credential Primitives TS → WASM 改寫
## Overview
`cypher-executor` 中以 TypeScript 實作的 credential 注入邏輯,改寫為 4 個獨立的 WASM 零件。這是 `credential_parts.md` 長期規格的實現,不再是「未來 Phase」。
**動機**:TS 實作無法在地端(workerd)和邊緣端(Wazero)執行。WASM 零件跨 runtime 可攜,符合 u6u 三層部署架構。
**嚴格規範(richblack 2026-04-19 確認)**cypher-executor TS **完全不實作**任何 credential / auth / template / JWT / 解密邏輯。所有業務邏輯必須在 TinyGo WASM 零件內。TS 僅負責 HTTP routing + 呼叫 WASM + host function 提供 runtime primitivecrypto.subtle / KV / fetch)。
---
## 現有 TS 實作(要刪除的)
| 檔案 | 功能 | 對應 WASM Primitive |
|------|------|---------------------|
| `credential-injector.ts``injectFromAuthRecipe()` | static_key template 展開 | `auth_static_key` |
| `credential-injector.ts` — service_account 分支 | JWT signing + token exchange | `auth_service_account` |
| `credential-injector.ts``decryptCredential()` | AES-GCM 解密 | host function(所有 primitive 共用) |
| `credential-injector.ts``interpolateTemplate()` | `{{secret.KEY}}` 替換 | 內建在各 primitive |
| `jwt-signer.ts``exchangeGoogleJwt()` | PEM→PKCS8→RS256→token | `auth_service_account` |
| `component-loader.ts` — BUILTIN_API_RECIPES | gmail/telegram/line/gsheets 寫死邏輯 | 刪除,改用 auth recipe + `http_request` 零件 |
| `credential-injector.ts` — BUILTIN_CREDENTIALS_MAP | 舊路徑 flat injection | 刪除,統一走 auth recipe |
| `arcrun/credentials/` | 重複的 credentials Worker | 刪除,路由已在 cypher-executor |
---
## 4 個 WASM Primitive 設計
### 統一 I/O 介面(stdin/stdout JSON
```
stdinWorker → WASM):
{
"action": "authenticate" | "needs_refresh" | "refresh" | "test",
"api_key": "ak_xxx", // 租戶識別,用來組 KV key
"service": "openai", // 對應 auth_recipe:{service}
"request": { "method": "GET", "url": "/path", "headers": {}, "body": null }
}
WASM 內部流程:
1. recipeJSON = kv_get("auth_recipe:" + service)
2. 依 recipe.required_secrets 逐一 kv_get("{api_key}:cred:{name}") → {encrypted, iv}
3. secrets[name] = crypto_decrypt(encrypted, iv)
4. service_accountcrypto_sign_rs256(jwt, pkcs8) + http_request 換 token
5. 展開 recipe.inject 的 {{secret.X}} / {{runtime.X}} 模板
stdoutWASM → Worker):
{
"success": true,
"auth_headers": { "Authorization": "Bearer xxx" },
"auth_query": {},
"auth_body": {},
"runtime": { ... updated runtime state,供下次 refresh 用 }
}
```
### auth_static_key
**位置**`arcrun/registry/components/auth_static_key/`
**語言**TinyGo 或 AssemblyScript
功能:
1. 讀取 `recipe.inject.header/query/body` 模板
2.`secrets` 展開 `{{secret.KEY}}` 模板
3. 回傳 `auth_headers` / `auth_query` / `auth_body`
涵蓋:~80% 服務(Bearer token, API Key, Basic Auth, custom header
### auth_service_account
**位置**`arcrun/registry/components/auth_service_account/`
**語言**TinyGo 或 AssemblyScript
功能:
1.`secrets.service_account_json` 解析 private key
2. JWT signingRS256PEM→PKCS8→sign
3. POST token exchange endpoint → 取得 access_token
4. 展開 `{{runtime.access_token}}` 模板
**crypto 考量**
- TinyGo 的 `crypto/rsa` + `crypto/x509` 支援有限
- 若 TinyGo 不支援 RS256:使用 host function 讓 Worker 的 `crypto.subtle` 代簽
- 或改用 AssemblyScript(有 as-crypto 套件)
### auth_oauth2(新建)
**位置**`arcrun/registry/components/auth_oauth2/`
功能:
1. `needs_refresh`:檢查 `runtime.expires_at` 是否過期
2. `refresh`:用 `runtime.refresh_token` + `secrets.client_secret` 換新 token
3. `authenticate`:展開 `{{runtime.access_token}}` 到 headers
### auth_mtls(新建)
**位置**`arcrun/registry/components/auth_mtls/`
功能:
1.`secrets` 讀取 client cert + key
2. 回傳 TLS 設定(由 Worker runtime 執行實際 mTLS handshake
---
## cypher-executor 改動
### 保留(TS routing 層)
- `routes/credentials.ts` — HTTP CRUD for credentials(接收加密的 payload
- `routes/recipes.ts` — HTTP CRUD for auth recipes
- `routes/auth.ts` — OAuth flow routing
- `graph-executor.ts` — workflow 執行排程
- `lib/wasi-shim.ts` — WASM runtime + host functions(加解密 / KV / 簽章 / HTTP 實際由 `crypto.subtle` / env binding / fetch 執行,但**呼叫時機由 WASM 決定**)
### 修改
- `actions/credential-injector.ts`**整檔刪除**,改為新檔 `actions/auth-dispatcher.ts`(約 30 行):
1.`resolveAuthRecipe(componentId)` 取得 `primitive` 名稱(static_key / service_account / oauth2 / mtls
2. 載入對應的 `auth_{primitive}.wasm`
3. 送 stdin`{ action, api_key, service, request }`**不送 secrets、不送 recipe plaintext**
4. WASM 透過 host function 自行 `kv_get` 讀 recipe + 加密 secret`crypto_decrypt` 解密
5. 讀 stdout → 合併 `_auth_headers` / `_auth_query` / `_auth_body` 進 ctx
- `lib/component-loader.ts`**刪除 `BUILTIN_API_RECIPES`**(含 http_request / gmail / telegram / line_notify / google_sheets 的 TS 實作),全部改走 WASM runner。每個 `.wasm` 零件都已編譯並以獨立 Worker 部署(`{canonical-id-kebab}.arcrun.dev`)。loader 新增的「WASM runner」路徑就是「canonical_id → HTTP URL 查表後 fetch」,**不做** WASM instantiate。
- **R2 動態注入 WASM 路徑作廢**richblack 2026-04-19 確認:CF workerd 無法以 R2 物件臨時 instantiate WASM)。用戶自製零件(Phase 5)同樣走「產生獨立 Worker」流程,不從 R2 讀。
### 刪除
- `lib/jwt-signer.ts` — 整檔刪除,RS256 簽章移入 `auth_service_account` WASM(透過 host function `crypto_sign_rs256`
- `credential-injector.ts` 整檔刪除(見上)
- `component-loader.ts``BUILTIN_API_RECIPES` 整段刪除
- `BUILTIN_CREDENTIALS_MAP` 已在 `credential-injector.ts` 內,隨檔一併刪
---
## Host FunctionsWASM ↔ Worker 的橋接)
auth primitive WASM 需要呼叫外部能力時,透過 host function。全部放 `u6u` namespace。**錯誤回傳非零 uint32;成功 = 0 且把結果寫入 `outPtr` 指向的 buffer**。
| Host Function | TinyGo 簽章 | 用途 |
|---|---|---|
| `http_request` | `(urlPtr/Len, methodPtr/Len, headersPtr/Len, bodyPtr/Len, outPtr, outLenPtr) uint32` | HTTP 請求(已實作) |
| `kv_get` | `(keyPtr, keyLen, outPtr, outLenPtr) uint32` | 讀 KV。Worker 依 key 前綴路由到 `CREDENTIALS_KV` / `RECIPES` |
| `crypto_decrypt` | `(encPtr, encLen, ivPtr, ivLen, outPtr, outLenPtr) uint32` | AES-GCM 解密。encryption key 由 Worker 從 `env.ENCRYPTION_KEY` 內部讀取,**永遠不暴露給 WASM** |
| `crypto_sign_rs256` | `(dataPtr, dataLen, pkcs8Ptr, pkcs8Len, outPtr, outLenPtr) uint32` | Worker 用 `crypto.subtle.sign('RSASSA-PKCS1-v1_5' + SHA-256)`private key 以 PKCS8 bytes 傳入 |
這些 host function 在 `lib/wasi-shim.ts` 中以 WASI import 提供。
### 安全邊界
- `ENCRYPTION_KEY` 只在 `crypto_decrypt` host function 內部使用,**絕不**經 stdin / 回傳值 / 任何路徑傳給 WASM
- `api_key` 經 stdin 傳入 WASM(讓 WASM 自己組 `{api_key}:cred:{name}` KV key
- `kv_get` 在 Worker 側檢查 key 前綴:
- `auth_recipe:*` → 讀 `RECIPES`
- `{api_key}:cred:*` → 讀 `CREDENTIALS_KV`,且 `{api_key}` 必須等於 stdin 傳入的 api_key(防越權)
- 其他前綴 → 回傳錯誤
---
## 關於解密位置
採用**方案 B(唯一方案)**WASM 透過 host function `crypto_decrypt()` 自行解密。
- cypher-executor TS 完全不解密、不知道 plaintext
- `ENCRYPTION_KEY` 永遠留在 Worker host function 內
- WASM 知道要解哪份 ciphertext(經 `kv_get` 讀到的 `{encrypted, iv}`),但拿不到 encryption key
- 這樣 TS 層完全沒有零件業務邏輯,符合 CLAUDE.md §禁止行為 1/6
(歷史註記:曾規劃方案 A「TS 先解密再送 stdin」,已廢棄 — 違反「TS 不得實作零件邏輯」。)
---
## 不做的事
- ❌ 不改 recipe YAML schema — 沿用現有格式
- ❌ 不改 KV 儲存結構 — `auth_recipe:{service}` / `{api_key}:cred:{name}` 不變
- ❌ 不改 SDK API — SDK 仍是 HTTP thin wrapper
- ❌ 不建新的 Worker — 在 cypher-executor 內完成
@@ -1,168 +0,0 @@
# Implementation Tasks: Credential Primitives TS → WASM
**嚴格規範(richblack 2026-04-19**cypher-executor TS 不得實作任何 credential / auth / template / JWT / 解密邏輯。全部走 TinyGo WASM + host functions(方案 B)。
**封測狀態**:推遲(richblack 2026-04-19 決定)。先完成 Phase 1-3 清除違規 TS,再啟動封測。
---
## Phase 0:核心合併(u6u-core → arcrun
- [x] 0.1 把 `u6u-core/builtins/` 搬到 `arcrun/builtins/`
- [x] 0.2 確認 `arcrun/registry/components/` 21 個零件的 contract.yaml 完整(21/21
- [x] 0.3 刪除 `arcrun/credentials/` 整個目錄(重複,credential route 已在 cypher-executor
- [x] 0.4 更新 `arcrun/cypher-executor/wrangler.toml`:確認 CREDENTIALS_KV binding 存在
- [x] 0.5 刪除 `matrix/u6u-core/` 整個目錄(2026-04-19 完成,只剩 credentials/ 已被 cypher-executor 取代)
- [x] 0.6 在 `cypher-executor/src/lib/wasi-shim.ts` 新增 host functions
- `u6u.kv_get(keyPtr, keyLen, outPtr, outLenPtr) uint32` — 依 key 前綴路由到 `CREDENTIALS_KV` / `RECIPES`,越權檢查 api_key
- `u6u.crypto_decrypt(encPtr, encLen, ivPtr, ivLen, outPtr, outLenPtr) uint32` — 用 `env.ENCRYPTION_KEY` + `crypto.subtle` AES-GCM 解密;key 不暴露給 WASM
- `u6u.crypto_sign_rs256(dataPtr, dataLen, pkcs8Ptr, pkcs8Len, outPtr, outLenPtr) uint32``crypto.subtle.sign('RSASSA-PKCS1-v1_5' + SHA-256)`
- 2026-04-19 完成:wasi-shim.ts 新增 `createArcrunHostFunctions(env, apiKey)` factory,集中 AES-GCM 解密 + RSA sign + KV 前綴路由越權檢查。WASI imports 的 u6u namespace wiring 本來就已接好(只是當時沒有實作 factory)。typecheck 通過。
- [x] 0.7 在 `cypher-executor/src/lib/component-loader.ts` 新增 WASM runner 路徑:
- 所有 WASM 零件(含 auth primitive、API 零件、未來用戶自製)一律走 HTTP URL(`{canonical-id-kebab}.arcrun.dev`)到獨立 Worker
- **R2 動態注入路徑作廢**richblack 2026-04-19 確認:CF workerd 不支援以 R2 物件臨時 instantiate WASM;用戶自製零件同樣走「產生獨立 Worker」流程,不走 R2)
- cypher-executor 本身**不做** WASM instantiate,也不直接呼叫 `createArcrunHostFunctions`;那個 factory 是**零件 Worker 側**`.component-builds/{name}/src/index.ts`)用的,在 Phase 1 建立 auth_static_key Worker 時接上
- 2026-04-19 完成:`component-loader.ts` 新增 `WASM_HTTP_RUNNER_IDS`10 個 canonical_id6 個 API 零件 + 4 個 auth primitive)+ `wasmWorkerUrl()` URL 慣例輔助函數;解析鏈新增為第 8 層(放在 `BUILTIN_API_RECIPES` fallback 之後,避免 Phase 3 尚未完成時 API 零件 Worker 未部署造成 404Phase 3 刪除 `BUILTIN_API_RECIPES` 後,API 零件會自然落到此層)。auth primitive 從此層進入。`tsc --noEmit` 通過。
---
## Phase 1auth_static_key WASM(優先,涵蓋 80% 服務)
方案 BWASM 自行讀 KV + 解密,TS 不碰 plaintext。
- [x] 1.1 建立 `arcrun/registry/components/auth_static_key/` 目錄
- [x] 1.2 寫 `component.contract.yaml`input: `{action, api_key, service, request}` → output: `{success, auth_headers, auth_query, auth_body, runtime}`
- [x] 1.3 實作 `main.go`TinyGo):
- 宣告 host imports`kv_get` / `crypto_decrypt`static_key 不需要 http_request
- 從 stdin 讀 `{action, api_key, service}`
- `kv_get("auth_recipe:" + service)` → recipe JSON → 驗證 `primitive == "static_key"`
- 對每個 non-optional `recipe.required_secrets``kv_get("{api_key}:cred:{name}")``{encrypted, iv}``crypto_decrypt` → plaintext
- 展開 `{{secret.X}}` / `{{runtime.X}}` 模板於 `inject.header/query/body`;未知 key 展空字串(與 TS parity);其他 namespace 的 `{{...}}` 原樣保留
- 輸出 stdout JSON `{success, auth_headers, auth_query, auth_body, runtime}`
- [x] 1.4 `tinygo build -o auth_static_key.wasm -target=wasi main.go` — 2026-04-19 編譯通過(1.1MB,在 contract 限制 2MB 內)
- [🔄] 1.5 建立 `.component-builds/auth_static_key/`(用 `component-worker-template`)並部署到 `auth-static-key.arcrun.dev`
- 2026-04-20 完成**建置**部分:`.component-builds/auth_static_key/{wrangler.toml, package.json, tsconfig.json, src/index.ts, component.wasm}` 全數到位
- 方案 A:`src/index.ts` 直接 import `../../../cypher-executor/src/lib/wasi-shim``createWasiShim` + `createArcrunHostFunctions`(以 `ArcrunHostEnv` 結構型別相容);AES 解密邏輯仍只存在於 wasi-shim.ts 一處(rule 02 §2.2)
- 綁同組 KV:CREDENTIALS_KV (e7f4320f88d343f187e35e3543dd74c9) / RECIPES (9cf9db905c6241f78503199e58b2ffe0);ENCRYPTION_KEY 走 `wrangler secret put`
- `wrangler deploy --dry-run` 通過(1192 KiB, 419 KiB gzip);實際 `wrangler deploy` + `secret put ENCRYPTION_KEY` 留給 richblack 執行
- [x] 1.6 建立 `auth-dispatcher.ts`(取代 `credential-injector.ts`):查 auth recipe → HTTP POST 到對應 auth primitive URL → 合併 `_auth_headers` 進 ctx
- 2026-04-20 完成:`cypher-executor/src/actions/auth-dispatcher.ts` 新建,export `tryAuthDispatch(componentId, input, env, apiKey)`
- 流程:查 `resolveAuthRecipe` → primitive 在 `SUPPORTED_PRIMITIVES`(目前只有 `static_key`)→ fetch `wasmWorkerUrl('auth_static_key')` → 合併 `_auth_headers/_auth_query/_auth_body`
- 自引用防護:`AUTH_PRIMITIVE_IDS` set 排除 4 個 `auth_*` componentId
- `wasmWorkerUrl``component-loader.ts` export 出來共用
- `graph-executor.ts` 改為:先試 `tryAuthDispatch`(新路徑),沒命中 fallback 到舊 `injectCredentials`(Phase 1.9 刪)
- 檢查過 auth-dispatcher.ts 無 `crypto.subtle` / `interpolate` / `{{secret.` / hard-code API URL,符合 rule 02 §2.2
- `tsc --noEmit` 通過
- [ ] 1.7 端對端測試:openai recipe → 成功注入 `Authorization: Bearer <openai_key>`
- [ ] 1.8 端對端測試:twilio recipeBasic Auth)→ 成功注入
- [ ] 1.9 **刪除 `credential-injector.ts` 整檔**`decryptCredential` / `decryptSecrets` / `interpolateTemplate` / `BUILTIN_CREDENTIALS_MAP` 全刪)
---
## Phase 2auth_service_account WASM
- [🔄] 2.1 建立 `arcrun/registry/components/auth_service_account/` 目錄
- [🔄] 2.2 寫 `component.contract.yaml`
- [🔄] 2.3 實作 `main.go`
- 從 stdin 讀 `{api_key, service}` + `kv_get` 拿 recipe + 解密 SA JSON
- 解析 SA JSON 取 `client_email` / `private_key`PEM
- PEM → PKCS8 bytes(純 Gobase64 decode + 去 header/footer
- 組 JWT header + payloadbase64url),呼叫 `crypto_sign_rs256(signingInput, pkcs8)` 拿 signature
- 組完整 JWT → `http_request` POST `token_uri` → 拿 `access_token`
- 展開 `{{runtime.access_token}}` 模板
- [x] 2.4 `tinygo build -o auth_service_account.wasm -target=wasi main.go` — 2026-04-20 編譯通過(1.1MB,在 contract 限制 2MB 內)
- [x] 2.5 建立 `.component-builds/auth_service_account/` 並部署到 `auth-service-account.arcrun.dev`
- 2026-04-20 完成**建置**部分:`.component-builds/auth_service_account/{wrangler.toml, package.json, tsconfig.json, src/index.ts, component.wasm}` 全數到位
- 方案 A:`src/index.ts` 重用 `createArcrunHostFunctions` 提供 kv_get/crypto_decrypt/crypto_sign_rs256,**額外加 `http_request` host function**(token exchange 用,非 crypto 不受 §2.2 約束)。http_request 直接回 response body 原文(WASM 端 json.Unmarshal 找 access_token)
- 綁同組 KV:CREDENTIALS_KV / RECIPES;ENCRYPTION_KEY 走 `wrangler secret put`
- `wrangler deploy --dry-run` 通過(1248 KiB, 440 KiB gzip);實際 `wrangler deploy` + `secret put ENCRYPTION_KEY` 留給 richblack 執行
- `auth-dispatcher.ts``SUPPORTED_PRIMITIVES` 加入 `'service_account'`,workflow 用 google SA recipe 會自動走新 WASM 路徑
- [ ] 2.6 端對端測試:google_sheets_sa recipe → 成功取得 access_token → 注入 header
- [x] 2.7 **刪除 `lib/jwt-signer.ts` 整檔** — 2026-04-20 完成
- `cypher-executor/src/lib/jwt-signer.ts` 已刪除(RS256 JWT 邏輯移入 `auth_service_account.wasm`)
- `credential-injector.ts` 原 line 23 `import { exchangeGoogleJwt }` 移除
- `credential-injector.ts` 原 line 140-150 service_account 分支改為 throw(任何 service_account recipe 已被 auth-dispatcher 攔截;這條 TS fallback 若被觸發即表架構錯亂,直接爆錯比沈默解密更安全)
- `cypher-executor` tsc --noEmit 通過
---
## Phase 3:清理 component-loader 的 TS 實作(全刪)
目標:`BUILTIN_API_RECIPES` 整段刪除,所有服務走 WASM runnerHTTP URL 路徑)。
- [x] 3.1 確認 `http_request.wasm` / `gmail.wasm` / `telegram.wasm` / `line_notify.wasm` / `google_sheets.wasm` 都在 `registry/components/` 且可執行 — 2026-04-20 驗證 6 個(含 cron)全數存在,main.go + .wasm 齊備
- [x] 3.2 確認上述零件 Worker 都已部署(`{name}.arcrun.dev` 可用) — 2026-04-20 完成**建置**部分
- 6 個 Worker 建置到位:`.component-builds/{http_request, gmail, telegram, line_notify, google_sheets, cron}/{wrangler.toml, package.json, tsconfig.json, src/index.ts, component.wasm}`
- 方案 A:5 個需 http_request 的零件(http_request/gmail/telegram/line_notify/google_sheets)`src/index.ts` 共用模板;cron 是純計算不註冊 host function
- 全部透過 `createWasiShim` 複用 cypher-executor/src/lib/wasi-shim.ts(rule 02 §2.2 邊界)
- 6 個 `wrangler deploy --dry-run` 全通過(~1.17 MB / ~413 KB gzip 每個);實際 `wrangler deploy` 留給 richblack 執行
- [x] 3.3 `component-loader.ts` 的內建路徑改為查對應 Worker URL → HTTP POST — 2026-04-20 完成
- 原本第 7 層是 `BUILTIN_API_RECIPES` fallback、第 8 層是 `WASM_HTTP_RUNNER_IDS` (HTTP URL);兩層合併為第 7 層 `WASM_HTTP_RUNNER_IDS` 直接走 `makeHttpRunner(wasmWorkerUrl(id))`
- 解析鏈新編號 1-8,順序不變(外部 URL → recipe hash → component hash → R2 → Service Binding → auth recipe runner → WASM HTTP runner → 找不到)
- [x] 3.4 **刪除 `BUILTIN_API_RECIPES` 整個 Record**`http_request` / `gmail` / `telegram` / `line_notify` / `google_sheets` / `cron` 的 TS 實作全刪) — 2026-04-20 完成
- `cypher-executor/src/lib/component-loader.ts` 原 line 253-326 `BUILTIN_API_RECIPES` 常數 + fallback lookup 全刪(約 80 行)
- 全域搜尋確認:`gmail.googleapis.com/...messages/send` / `api.telegram.org/bot.*sendMessage` / `sheets.googleapis.com/v4/spreadsheets` / `notify-api.line.me/api/notify` 在 cypher-executor TS 中已不存在(auth-recipe-seeds.ts 的 `base_url` 是 recipe 資料欄位,不是 hard-coded API call)
- `cypher-executor` tsc --noEmit 通過
- [ ] 3.5 端對端測試:workflow 用 gmail auth recipe + gmail.wasm Worker → 成功發信
- [ ] 3.6 端對端測試:workflow 用 http_request.wasm Worker + auth_static_key 注入 → 成功呼叫任意 API
---
## Phase 4auth_oauth2 + auth_mtls WASM(封測後)
- [ ] 4.1 建立 `arcrun/registry/components/auth_oauth2/`
- [ ] 4.2 實作:`needs_refresh` / `refresh` / `authenticate` 三個 action
- [ ] 4.3 建立 `arcrun/registry/components/auth_mtls/`
- [ ] 4.4 實作:輸出 TLS cert/key(實際 mTLS handshake 由 Worker runtime 執行,WASM 無法做 socket
---
## Phase 5:封測啟動門檻 — 核心穩定驗證
**全部通過才能啟動封測**
- [ ] 5.1 所有 20 個 auth recipe seed 可正常運作(static_key 17 個 + service_account 3 個)
- [ ] 5.2 `cypher-executor/src/actions/credential-injector.ts` **不存在**
- [ ] 5.3 `cypher-executor/src/lib/jwt-signer.ts` **不存在**
- [ ] 5.4 `cypher-executor/src/lib/component-loader.ts``BUILTIN_API_RECIPES` / `BUILTIN_CREDENTIALS_MAP`
- [ ] 5.5 `cypher-executor/src/` 全域搜尋 `crypto.subtle.decrypt` 只出現在 `wasi-shim.ts``crypto_decrypt` host function
- [ ] 5.6 `cypher-executor/src/` 全域搜尋 `crypto.subtle.sign` 只出現在 `wasi-shim.ts``crypto_sign_rs256` host function
- [ ] 5.7 `cypher-executor/src/` 全域搜尋 `interpolate` 回傳 0 筆(template 展開全在 WASM
- [ ] 5.8 全域搜尋 `{{secret\.` / `{{runtime\.` 在 TS 檔案中回傳 0 筆
---
## Phase 6:通用 CI/CD deploy workflow
**背景**(2026-04-20 richblack 決定):現 `.github/workflows/deploy.yml` 只部署 cypher-executor + registry + 已刪除的 credentials,漏掉 Phase 1-3 產出的 8 個 Worker,且硬編碼每個 job 導致未來新增 Worker 都要改 CI。改為**通用掃描式 workflow**:任何含 `wrangler.toml` 的目錄 = 部署單位,改到該目錄下任何檔案 = 觸發重新 deploy。
**關鍵決策**:
- 零件 `.wasm` 由 CI build(不 commit):`registry/components/{name}/main.go` 改動時才重 build,用 timestamp / content hash 判斷
- `.component-builds/{name}/component.wasm` 由 CI 從 `registry/components/{name}/{name}.wasm` 複製產生(deploy 前一步)
- 統一用 pnpm(`.component-builds/*` 本來就是;順勢把 cypher-executor 的 `package-lock.json` 砍了)
- runtime secret(`ENCRYPTION_KEY`)不進 CI,由 richblack 一次性 `wrangler secret put`
- registry Worker 的 `wrangler.toml` 現階段不改(職責是合約管理,與封測無關;`sandboxAcceptance.ts` 的 rule 02 §2.2 審查留到 Phase 5 用戶自製零件啟動時)
### Tasks
- [x] 6.1 改寫 `.github/workflows/deploy.yml`:動態掃描所有含 `wrangler.toml` 的目錄(排除 `node_modules/` + Pages 專案),用 matrix job fanout 部署;分兩層(tier1=`.component-builds/*`,tier2=其他),tier1 全綠後才 tier2(避免 service binding target 未存在)
- [x] 6.2 加上 TinyGo build 步驟:tier1 matrix 一律 setup-tinygo + 從 `registry/components/{name}/main.go` rebuild `.wasm` → copy 到 `.component-builds/{name}/component.wasm`
- [x] 6.3 diff-aware:push 到 main 時比對 `github.event.before..github.sha`,只 deploy 有 diff 的 Worker(含 `registry/components/{name}/` 連動 `.component-builds/{name}/`);`workflow_dispatch` 提供 `force_all` + `only` 選項
- [x] 6.4 統一 pnpm:刪除 `cypher-executor/package-lock.json` + `registry/package-lock.json`;workflow 優先 `pnpm install --frozen-lockfile`,若該目錄無 `pnpm-lock.yaml` 則 fallback 到 `--no-frozen-lockfile`(混合期容錯)
- [x] 6.5 加 `max-parallel: 5` 控制 Workers API rate limit(tier1 和 tier2 各自)
- [x] 6.6 驗證:`workflow_dispatch` + `force_all=true` 手動跑一次,24 個 Worker 全綠 — 2026-04-20 完成
- 最終綠色 run 24668903627(28/28 jobs,含 discover + summary):tier1 24 個零件 Worker + tier2 2 個 orchestration Worker(cypher-executor / registry)全 success
- 過程中修兩輪:先修 `setup-node``cache: 'pnpm'` 對 legacy `package-lock.json` 目錄失效(改為不用 cache);再修 tier2 三個 package.json(cypher-executor/registry/builtins)遺漏 `wrangler` devDependency + regen pnpm-lock.yaml
- ENCRYPTION_KEY secret 已由 richblack 授權、CC 從 .env pipe 到三個 Worker:`arcrun-auth-static-key``arcrun-auth-service-account``arcrun-cypher-executor`(不顯示內容)
- [x] 6.7 文件:在 `.claude/rules/` 加一份 `05-deploy-convention.md`(「新增 Worker = 新目錄 + wrangler.toml,不用改 CI」)
---
## Notes
- 方案 B 是唯一方案(方案 A 已廢棄,違反 CLAUDE.md §禁止行為)
- Phase 0.6host functions+ 0.7WASM runner)是 Phase 1-3 的硬前置,必須先做
- 若 TinyGo `encoding/base64` 可用就直接用;若不可用則自行實作(見 gmail/main.go 的 `base64URLEncode`
- `auth_mtls` 的 TLS handshake 無法在 WASM 內做(WASI preview1 沒 socket),只能輸出 cert/key 讓 Worker 在 fetch 時用
- **每個 auth primitive WASM 都是獨立部署的 Worker**(透過 `component-worker-template/`),不是從 R2 動態載入
- Cypher binding = workflow YAML 裡的 URL 清單,不是 Cloudflare service binding
@@ -1,25 +0,0 @@
# 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)
@@ -1,56 +0,0 @@
// 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 />);
@@ -1,92 +0,0 @@
// 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 });
@@ -1,86 +0,0 @@
// 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 });
@@ -1,95 +0,0 @@
# 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
@@ -1,128 +0,0 @@
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;
@@ -1,90 +0,0 @@
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;
@@ -1,126 +0,0 @@
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;
@@ -1,168 +0,0 @@
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;
@@ -1,255 +0,0 @@
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;
@@ -1,300 +0,0 @@
# Frontend Redesign — Design
> 讀此檔前請先讀 `requirements.md` 和 `design-source/index.html`。
> 視覺 spec 的 single source of truth 是 `design-source/`Claude Design 匯出的 HTML/JSX prototype)。
---
## 1. 架構總覽
```
landing/ (Next.js 15 App Router)
├── app/
│ ├── layout.tsx ← 全站 layoutnext/font + design tokens + 全域 CSS 匯入
│ ├── globals.css ← 匯入 design-tokens.cssTailwind @import
│ ├── design-tokens.css ← 新增:從 design-source 抽出的 CSS variables:root {...}
│ ├── page.tsx ← LandingRSC
│ ├── auth/
│ │ └── page.tsx ← Auth"use client"
│ ├── dashboard/
│ │ └── page.tsx ← Dashboard"use client",仍靠 middleware 保護)
│ ├── keys/
│ │ └── page.tsx ← API Keys"use client"
│ ├── workflows/
│ │ ├── page.tsx ← Workflows 清單(redirect 到 dashboard 的 table,本身極簡)
│ │ └── [name]/page.tsx ← Workflow Viewer"use client"
│ ├── integrations/page.tsx ← 保留現有
│ ├── api-docs/page.tsx ← 保留現有
│ └── login/page.tsx ← 保留現有(redirect /auth 同義;見 §9 遷移策略)
├── components/
│ ├── shell/
│ │ ├── Logo.tsx
│ │ ├── Icon.tsx
│ │ ├── TopNav.tsx
│ │ ├── Footer.tsx
│ │ └── Sidebar.tsx
│ ├── primitives/
│ │ ├── Button.tsx ← btn / btn-primary / btn-secondary / btn-ghost 對應 class
│ │ ├── Pill.tsx
│ │ ├── Toggle.tsx
│ │ ├── Terminal.tsx ← landing hero 右卡用
│ │ └── ChatPreview.tsx ← landing hero 右卡用
│ └── workflow/
│ ├── Canvas.tsx ← wf-viewer 本體(節點 + SVG edges
│ ├── NodeCard.tsx
│ ├── DetailPanel.tsx
│ ├── Minimap.tsx
│ └── ZoomControls.tsx
├── lib/
│ ├── api.ts ← typed fetch wrapperfetch ${API_BASE}${path}, credentials: 'include'
│ ├── workflows.ts ← listWorkflows / getWorkflow / getWorkflowYaml
│ ├── apiKeys.ts ← listKeys / createKey / patchKey / deleteKey
│ └── me.ts ← 已存在邏輯,集中到此
├── middleware.ts ← 擴展 matcher(加 /keys, /workflows/*
└── ...(既有 package.json / wrangler.toml 不變)
```
**路由對照設計稿的 5 screen**
| Screen | Route |
|---|---|
| Landing | `/` |
| Auth | `/auth`(新增;`/login` 保留並內部 `redirect('/auth')` |
| Dashboard | `/dashboard` |
| API Keys | `/keys` |
| Workflow Viewer | `/workflows/[name]` |
---
## 2. Design tokens 對應
設計稿所有 CSS 變數抄進 `app/design-tokens.css`**不解析、不改名**
```css
:root {
--bg: #0F0F0F;
--bg-1: #141414;
--card: #1A1A1A;
--card-2: #222222;
--line: #262626;
--line-2: #303030;
--text: #EDEDED;
--text-dim: #A0A0A0;
--text-mute: #6B6B6B;
--primary: #6366F1;
--primary-2: #8B5CF6;
--primary-soft: rgba(99, 102, 241, 0.12);
--primary-ring: rgba(99, 102, 241, 0.32);
--success: #22C55E;
--warn: #F59E0B;
--danger: #EF4444;
--gradient: linear-gradient(135deg, #6366F1 0%, #8B5CF6 100%);
--gradient-soft: linear-gradient(135deg, rgba(99,102,241,0.16) 0%, rgba(139,92,246,0.16) 100%);
}
```
並在 Tailwind v4 的 `@theme inline` block 內對應出:
```css
@theme inline {
--color-bg: var(--bg);
--color-card: var(--card);
--color-card-2: var(--card-2);
--color-line: var(--line);
--color-line-2: var(--line-2);
--color-text: var(--text);
--color-text-dim: var(--text-dim);
--color-text-mute: var(--text-mute);
--color-primary: var(--primary);
--color-primary-2: var(--primary-2);
}
```
這樣 JSX 裡可用 `bg-bg / text-text-dim / border-line`,又保留 CSS 變數語義。
**現有的 `--background: #0a0a0a` 要換成 `#0F0F0F`**(視覺 breaking change;受影響:所有沿用 `bg-[#0a0a0a]` 的 inline 值)。
---
## 3. 字型
```tsx
// app/layout.tsx
import { Inter, JetBrains_Mono } from 'next/font/google';
const inter = Inter({
subsets: ['latin'],
variable: '--font-inter',
weight: ['300', '400', '500', '600', '700', '800'],
});
const mono = JetBrains_Mono({
subsets: ['latin'],
variable: '--font-mono',
weight: ['400', '500', '600'],
});
// body class = `${inter.variable} ${mono.variable}`
```
`globals.css` 中的 `body { font-family: var(--font-inter), -apple-system, sans-serif; }``.mono` class 用 `font-family: var(--font-mono)`
**移除**
- `design-source/index.html` 第 7-9 行的 `<link rel="preconnect"> / <link href="fonts.googleapis.com">`(不寫入 production)。
- React / Babel standalone script 標籤(prototype 專用,不進 production)。
---
## 4. 元件 porting 規則
Claude Design 用了 `window.Icon / window.Logo / window.AppIcon / window.TopNav ...` 的 globals 風格 — 那是 prototype 專用。Port 到 Next.js 時:
1. 每個元件拆單檔、具名 export。
2. 用 Tailwind + `className` 模板字串;共用 variant(如 btn)用 `cva`-style helper 即可(自己寫 5 行的 `clsx`-alike 函式),**不引入 class-variance-authority / clsx 套件**(避免新依賴)。
3. Icon 的 `paths` 直接搬,但每個 icon 拆成自己的 functional component 或集中在一個 `<Icon name="..." />`(沿用 design source 的 pattern)。
4. SVG arc wordmark 的 logo 直接 port。
---
## 5. 各 screen 實作細節
### 5.1 Landing — `app/page.tsx`
- 結構:`<TopNav />` + `<Hero />` + `<Paths />` + `<Strip />` + `<Footer />`
- Heroheading、eyebrow、CTA、radial grid bg(純 CSS)。
- Paths 左卡(Developer):install tabs (`npm` / `pip` / `bun`) + 兩個 terminal block**code 範例用 dogfooding 範例**`acr` CLI),不留 `Arcrun` SDK 假 API。
- Paths 右卡(Everyone):chat preview 結構保留;assistant 對話中的 tool call 用「arcrun · digest/weekly」不動。
- Strip4 cell。
- `LandingClientTabs` 因為有 tabs state,需標 `"use client"`;外層保持 RSC。
### 5.2 Auth — `app/auth/page.tsx`
- `"use client"`。state`mode: 'signin' | 'signup'`, `email`, `pw`, `remember`
- Submit`fetch(${API_BASE}/auth/password-login, { method: 'POST', credentials: 'include' })`**若 cypher-executor 尚未支援 password auth,先顯示「Password 登入尚未開放,請用 OAuth」警示,不偽造成功流程**)。
- OAuth 按鈕:直接 `<a href={API_BASE}/auth/google/start?redirect=/dashboard>`,和現行 `/login` 同樣機制。
- 下標提示「By signing up, you agree to our Terms ...」保留 static 字串。
- 保留 `/login` 路由向後相容(RSC 裡 `redirect('/auth')`)。
### 5.3 Dashboard — `app/dashboard/page.tsx`
- `"use client"` 或 split(外層 RSC 抓 /me,內層 Client)。
-`<Sidebar current="dashboard" />` + main。
- 主要區塊:
- Main headbreadcrumb「{email 的 domain} Dashboard」、heading「Welcome back, {display_name}」、subtitle 顯示 app/workflow 總數(從 `/apps` + `/workflows` 計算;若 endpoint 404 → 顯示 `—`)。
- Apps Grid`/apps` 的結果渲染;每列永遠有一個 `app-empty` 卡(新建 CTA)。
- Workflows Table`/workflows` 的結果渲染;空時改為全寬「No workflows yet. Run `acr push` to add one.」內嵌指令框。
- 「Open app」「View」按鈕導向 `/workflows/[name]`
- 「Edit in Claude」按鈕本次不做動作,僅保 UIdisabled + tooltip「Coming soon」)。
### 5.4 API Keys — `app/keys/page.tsx`
- `"use client"`
- Fetch `/api-keys`:若回傳為空陣列但 `/me` 有 api_keyfallback 顯示 `/me.api_key` 為唯一一列(單 key 相容模式)。
- 頂部 new-key-box:只在「剛剛建立新 key」的一次性狀態顯示(`useState` + `sessionStorage` flagreload 後消失)。
- 表格、toggle、trash:對應 PATCH / DELETE。
- 「Create new key」按鈕:呼叫 `POST /api-keys`,拿到後打 highlight box。
- Revoke 警告文字維持設計稿「within 60 seconds」。
### 5.5 Workflow Viewer — `app/workflows/[name]/page.tsx`
- `"use client"`param `name` 來自動態路由。
- Mount 後呼叫 `GET /workflows/:name`:後端回傳 `{ name, nodes: Node[], edges: Edge[], yaml, last_run: {...} }`(若 endpoint 未實作 → 顯示「Workflow viewer 尚未啟用」empty state,不用假資料)。
- `<Canvas>` 內:
- SVG 的 `<marker>`, `<path>` 定義抄設計稿。
- Node 用絕對定位(x/y 直接用 API 資料;資料沒有 coord 時做自動 layout — 階段性做簡單 dagre-free 的「column by depth」排版,避免新依賴)。
- 點選節點 → 右側 detail panel 顯示 input/output schema;若 type 含 `ai.*`,顯示 triplet 編輯器(model / temp / prompt)— 編輯本次 **read-only**disabled input + 「Edit via acr CLI」提示)。
- 「Export YAML」按 `GET /workflows/:name/yaml``download` blob。
- 「Edit in Claude」:本次只開新 tab 到 `https://claude.ai/new?q=...`(文案「coming soon」按鈕),避免偽裝已整合。
- Zoom controls、minimap:純 UI`zoom` state 實際不套 transform(或簡單 `style={{ transform: scale(zoom/100) }}` 套在 `.wf-nodes` + svg)。
---
## 6. API wrapper`lib/api.ts`
```ts
export const API_BASE = process.env.NEXT_PUBLIC_API_BASE ?? 'https://cypher.arcrun.dev';
export async function arcrunFetch<T>(path: string, init: RequestInit = {}): Promise<T> {
const res = await fetch(`${API_BASE}${path}`, {
credentials: 'include',
headers: { 'Accept': 'application/json', ...(init.headers ?? {}) },
...init,
});
if (res.status === 401 && typeof window !== 'undefined') {
window.location.href = `/auth?redirect=${encodeURIComponent(location.pathname)}`;
throw new Error('unauthenticated');
}
if (!res.ok) throw new Error(`arcrun ${path}: ${res.status}`);
return res.json() as Promise<T>;
}
```
所有頁面透過這個 wrapper。**禁止在 page.tsx 裡 hard-code `fetch('https://...')`**(測試可以 grep)。
---
## 7. Middleware
```ts
export const config = {
matcher: ['/dashboard/:path*', '/keys/:path*', '/workflows/:path*'],
};
```
現有邏輯(讀 `arcrun_session` cookie,沒有就 redirect `/login?redirect=...`)保留,`/login` 改為內部 redirect `/auth`
---
## 8. 不做的設計稿功能
| 設計元素 | 取捨 |
|---|---|
| 底部的 `proto-switch`5 個 screen 切換 pill | **刪**。那是 prototype 用的 demo 切換器,不進 production。 |
| Sidebar 的 `count` badge | 先保留;數字從 `/workflows` / `/apps` 的長度派生;無資料時藏起來。 |
| Sidebar bottom 的 avatar + "Maya Rivera / maya@northwind.co" | 換成 `{display_name} / {email}`(真資料)。 |
| Workflow Viewer 的 triplet 可編輯 | 本次 disabled,僅顯示。 |
| 「Edit in Claude」整合 | 按鈕保留,點擊開新 tab 到 claude.ai,不串 MCP/API。 |
| 多 workspace breadcrumb | 固定顯示用戶 email domain 或「Personal」。 |
---
## 9. 既有頁面遷移
| 既有 | 處理 |
|---|---|
| `/page.tsx` | **rewrite**:沿用設計稿結構,code demo 字串改為 `acr` 實際指令(現有的 `auth.bind(...)` 寫法可保留在 Python tab |
| `/login` | 改為 `redirect('/auth')`Next.js RSC redirect),保留舊連結相容 |
| `/dashboard` | **rewrite**:舊 dashboard 變成 API Keys 獨立頁 + 新 Dashboard 總覽。原本 dashboard 裡的 Key 卡片搬到 `/keys`。 |
| `/api-docs` | 不動 |
| `/integrations` | 不動;在 Dashboard Apps Grid 旁提供 link |
---
## 10. 開發順序(高度相依)
`tasks.md`。總則:
1. 先做 design tokens + shellLogo / Icon / Button / Sidebar / TopNav / Footer — 其他頁面都吃這些。
2. 然後 Landing(可直接驗證視覺基準)。
3. 然後 Auth(獨立)。
4. 然後 API Keys(後端依賴少)。
5. 然後 Dashboard(依賴 `/workflows` + `/apps`,若未實作先 empty state)。
6. 最後 Workflow Viewer(依賴最重,多 endpoint)。
---
## 11. 風險與未解
| 風險 | 緩解 |
|---|---|
| cypher-executor 尚未有 `/workflows`, `/apps`, `/api-keys` CRUD | 前端先做,統一走 404 → empty state;另開 task 去 cypher-executor SDD 增補。本次 SDD 不負責後端實作。 |
| Password auth 沒實作 | Auth 頁 email/password form 在 submit 時顯示「OAuth only」提示 |
| `acr push` 未記錄 node 座標 | Canvas 自動排版(by topological depth),不強制 YAML 載入 layout |
| `next-on-pages``"use client"` 大量頁面的 edge runtime 支援 | 本來就用 `next-on-pages`,問題不大;必要時 per-page `export const runtime = 'edge'` |
| 舊 `/dashboard` 的 bookmark 使用者 | 現行 `/dashboard` 的 Key 管理被搬走;保留 Key 區塊 + 顯示提示「New page: /keys」引導 |
---
## 12. 與封測的關係
此 SDD 的實作**不解除封測阻擋**(封測阻擋在 Credential Primitives WASM)。此重設計與 Phase 0.6 / 0.7 / 1-3 是並行軌道。richblack 可決定先後順序,但本 SDD 獨立可 ship。
@@ -1,159 +0,0 @@
# Frontend Redesign — Requirements
> 來源:Claude Design bundle `JAdpACs3cSyw_vN6Ketj1Q`(已歸檔於 `design-source/`)。
> 此 SDD 擴展 `../landing-page.md` 的範圍:landing 從「單頁 + OAuth + Dashboard」升級為「5 screen app shell」。
---
## 1. 背景
`arcrun/landing/` 目前只提供:
- `/` — Landing hero + code demo
- `/login` — Google / GitHub OAuth
- `/dashboard` — API Key 管理
- `/api-docs` — Swagger UI(外部)
- `/integrations` — 20 個 recipe 靜態清單
Claude Design 交付一套完整 5 screen 設計(Landing / Auth / API Keys / Dashboard / Workflow Viewer),與現有前端相比多了:
- **Dashboard 的 Apps Grid 與 Workflows Table**(現在沒有應用/workflow 清單 UI
- **API Keys 獨立頁**(現在和 dashboard 混在一起)
- **Workflow Viewer**node-based canvas,對應 YAML workflow 的視覺化(目前 acr push YAML 後只有 CLI 輸出)
---
## 2. User Stories
### US-1:新訪客認識 arcrun
- 作為沒用過 arcrun 的工程師,我要在 landing 看到兩條路(For Developers / For Everyone),5 秒內判斷這是否符合我要的用法。
- 驗收:hero + 雙 path card + feature strip 在首屏呈現;CTA「Start free」可點擊到 `/auth`
### US-2:取得 / 登入會員
- 作為訪客,我要用 GitHub / Google / Email 登入建立帳號。
- 驗收:`/auth` 支援 Sign in / Sign up tabs、GitHub + Google OAuth、Email + Password 表單。OAuth 成功後導向 `/dashboard`
### US-3:看見應用全貌
- 作為已登入用戶,我要在 Dashboard 看到我已部署的 Apppackaged workflow)和 Workflow 清單。
- 驗收:
- Apps Grid:每個 app 卡片顯示名稱、描述、「Open app」「Edit in Claude」。
- Workflows Table:顯示 workflow 名稱、節點數、最後修改、執行次數、狀態。
- 若 app / workflow 為空,顯示新建 CTA(非硬編 mock)。
### US-4:管理 API Keys
- 作為已登入用戶,我要建立、檢視、停用、刪除 API Key。
- 驗收:
- 剛產生的 Key 顯示在頂部高亮卡片(含警示文字「不會再顯示」)。
- 全部 Keys 在下方表格顯示(名稱、遮蔽後 key、建立時間、最後使用、狀態、toggle、trash)。
- Rotate / Revoke 立即生效(呼叫 cypher-executor)。
### US-5:檢視 Workflow graph
- 作為已登入用戶,我要點 workflow 進到 viewer,看到節點 DAG 與每個節點的 I/O schema 與參數。
- 驗收:
- Canvas 顯示節點(含 icon / type badge),節點以 bezier 曲線相連。
- 選中節點後右側面板顯示 input / output schema、configuration(針對 ai.completion 等節點顯示 triplet 編輯器)。
- 工具列含 Share / Export YAML / Edit in Claude。
- Minimap + zoom 控制顯示可用。
- 「Export YAML」呼叫 cypher-executor 取得該 workflow 的原始 YAML。
### US-6Dogfooding
- 作為 arcrun 核心維護者,我要前端所有與服務打交道的動作都透過 arcrun 自家 APIcypher-executor)完成,不依賴第三方 OAuth / workflow / backend 服務。
- 驗收(見 §6)。
---
## 3. 非功能需求
| 項目 | 規範 |
|---|---|
| 技術棧 | Next.js 15 App Router、React 19、Tailwind v4、TypeScript(沿用 `landing/` 現有堆疊) |
| 部署 | Cloudflare Pages`@cloudflare/next-on-pages`),沿用 `landing/wrangler.toml` |
| 字型 | Inter、JetBrains Mono — 用 `next/font/google`,不拉 unpkg / fonts.googleapis.com `<link>` |
| 依賴 | 僅 Next / React / Tailwind;禁止 tRPC、React Query、SWR、Auth.js/Clerk、ReactFlow/XYFlow、Radix、shadcn CLI install、animation libraryframer-motion)等第三方 |
| 狀態管理 | React 內建(useState / useReducer / Context);持久化用 `localStorage` 或 server session cookie |
| 國際化 | 延後;本次一律英文(與設計稿一致)。現有 `?lang=zh` 不擴展 |
| 無障礙 | 按鈕 `aria-label`、Form 控件有 `<label>`;鍵盤可完成登入 / 複製 key 流程 |
| 效能 | Landing 首屏無阻塞 JSRSC);Dashboard / Workflow Viewer 可為 Client Component |
---
## 4. 視覺基準
| 項目 | 值(source of truth`design-source/index.html` 的 CSS 變數) |
|---|---|
| 主背景 | `#0F0F0F`(現有是 `#0a0a0a` — 本次改為 `#0F0F0F` |
| Card | `#1A1A1A` |
| Line | `#262626` / `#303030` |
| Primary | `#6366F1`indigo`#8B5CF6`violet)漸層 |
| Text | `#EDEDED` / dim `#A0A0A0` / mute `#6B6B6B` |
| 字型 | Inter 300/400/500/600/700/800Mono 400/500/600 |
---
## 5. 範圍界線
### 納入
- 5 screenLanding、Auth、Dashboard、API Keys、Workflow Viewer
- LogoSVG arc wordmark)、Icon setinline SVG,從 primitives.jsx port
- TopNav / Footer / Sidebar 三個 shell 元件
### 不納入(本次 SDD
- **Multi-tenant workspace 切換**(設計稿有 "Northwind" breadcrumb,本次純顯示用戶 email
- **Multi-API-key CRUD 後端**cypher-executor 現只支援每帳號一把 key,多 key table 先以「目前只支援一把」狀態呈現 — 見 §6.2)
- **Workflow 編輯**(只做 read-only viewer;編輯仍走 acr CLI / YAML
- **即時執行狀態 stream**minimap / zoom 僅 UI,不做真實 pan-zoom transform
- **i18n 中英切換**、**Hall of Fame**、**Donate**
- **Swagger UI 頁(/api-docs** — 保留現狀
---
## 6. API 依賴(全部打 `cypher.arcrun.dev`
### 6.1 既有(已實作於 cypher-executor
- `GET /me` — 取得登入用戶 + api_keydriverdashboard 側欄 avatar / API Keys 頁的單把 key
- `PUT /me/api-key/rotate``DELETE /me/api-key` — Rotate / Revoke
- `GET /auth/google/start``GET /auth/github/start` — OAuth 起點
- `POST /auth/logout`
- `POST /webhooks/named/{name}/trigger` — 觸發(給 Landing 文案展示)
### 6.2 需新增的 endpoint(阻擋項;**本 SDD 只定義契約,cypher-executor 實作歸屬於另一個 task**
| Method | Path | 說明 | 用途 |
|---|---|---|---|
| GET | `/workflows` | 列出當前 api_key 名下的 workflow(名稱、nodes、modified、run_count、status | Dashboard / Workflows Table |
| GET | `/workflows/:name` | 取得 workflow 詳細(含 graph 節點 + edges + YAML | Workflow Viewer |
| GET | `/workflows/:name/yaml` | 下載 raw YAML | Workflow Viewer Export |
| GET | `/apps` | 列出 app= workflow 標上 icon/tone/description metadata | Dashboard / Apps Grid |
| GET | `/api-keys` | 列出多把 key(若後端仍是單把,回傳單元素 array) | API Keys 頁 |
| POST | `/api-keys` | 建立新 key | API Keys 頁「Create new key」 |
| PATCH | `/api-keys/:id` | `{ active: boolean }` 切換 | API Keys 頁 toggle |
| DELETE | `/api-keys/:id` | 刪除 | API Keys 頁 trash |
**在後端尚未實作前**:前端用型別化 fetch wrapper 封裝呼叫;遇到 404 顯示 empty state(而非假資料)。本 SDD 明確禁止 hard-code mock fixture。
### 6.3 登入前後可用的公開資訊
- Integrations 清單(20 個 recipe):現有 `/integrations` 頁已有靜態清單,繼續複用。
---
## 7. Dogfooding 紅線
| 禁止 | 用哪個 arcrun 內部替代 |
|---|---|
| Auth0 / Clerk / Supabase Auth | cypher-executor `/auth/*` + session cookie(現行 `arcrun_session` |
| Segment / PostHog | 不加,或用後續 arcrun `analytics` 零件 |
| Vercel KV / Upstash | Cloudflare KV(經 cypher-executor |
| 直接寫第三方 APISlack / Notion 等)作 dashboard demo | 透過 arcrun workflow + trigger 模擬 |
| ReactFlow / XYFlow | 純 SVG 手刻(設計稿本來就是手刻) |
---
## 8. 驗收總清單
- [ ] 5 個 screen 全部在 `/`, `/auth`, `/dashboard`, `/keys`, `/workflows/[id]` 可達。
- [ ] 設計稿的 spacing / color / border-radius 100% 對得上(以 `design-source/index.html` CSS 變數為準)。
- [ ] middleware 保護 `/dashboard`, `/keys`, `/workflows/*`(未登入 → `/auth?redirect=...`)。
- [ ] 只打 `cypher.arcrun.dev`(可透過 `NEXT_PUBLIC_API_BASE` override),grep 結果不含其他外部 API host。
- [ ] `package.json` 新增依賴 = 0(本次不引入新 npm 套件)。
- [ ] 無 mock 資料:若後端未實作,顯示 loading 或 empty state,不編假陣列給 UI。
- [ ] `pnpm build` 通過,`next-on-pages` 輸出無 edge-runtime 錯誤。
@@ -1,140 +0,0 @@
# Frontend Redesign — Tasks
> 進度來源:本檔。完成一項立刻 `[x]`,不批次。
> 本 SDD 建立於 2026-04-23richblack 尚未下令動工,**所有 task 預設 `[ ]`**。
---
## Phase 0 — SDD 建立(本次)
- [x] 取得 Claude Design bundle,歸檔至 `design-source/`
- [x] 撰寫 `requirements.md`
- [x] 撰寫 `design.md`
- [x] 撰寫 `tasks.md`(本檔)
- [ ] richblack review + 認可 → 開 Phase 1
**等 richblack 明確說「開始動工」之前,不觸 `landing/` 任何檔案。**
---
## Phase 1 — Tokens + Shell(無外部 API 依賴)
- [ ] 1.1 建 `landing/app/design-tokens.css`,抄 design-source CSS :root 變數
- [ ] 1.2 更新 `landing/app/globals.css``@import './design-tokens.css'``@theme inline` 對應 token → Tailwind color
- [ ] 1.3 `layout.tsx` 引入 `next/font/google` 的 Inter + JetBrains Monobody bg 改 `var(--bg)`
- [ ] 1.4 建 `landing/components/shell/Icon.tsx`(港設計稿 primitives.jsx 的所有 icon
- [ ] 1.5 建 `landing/components/shell/Logo.tsx`SVG arc wordmark
- [ ] 1.6 建 `landing/components/shell/TopNav.tsx`
- [ ] 1.7 建 `landing/components/shell/Footer.tsx`
- [ ] 1.8 建 `landing/components/shell/Sidebar.tsx`(含頭像、項目清單、登出按鈕)
- [ ] 1.9 建 `landing/components/primitives/Button.tsx`variants: primary / secondary / ghost / danger-ghost / sm / lg
- [ ] 1.10 建 `landing/components/primitives/Pill.tsx``Toggle.tsx`
- [ ] 1.11 Lint + build pass
---
## Phase 2 — Landing`/`
- [ ] 2.1 Rewrite `app/page.tsx`,結構照 design-source/screens/Landing.jsx
- [ ] 2.2 Heroheading / eyebrow / CTA / radial-grid bgCSS only
- [ ] 2.3 Paths 左卡:install tabs 元件(Client Component+ terminal code blocks(字串改 `acr` 實際指令)
- [ ] 2.4 Paths 右卡:chat preview(靜態)
- [ ] 2.5 Feature strip4 cell
- [ ] 2.6 TopNav / Footer 接上
- [ ] 2.7 Responsive(≤ 768px: paths 單欄、hero h1 縮 1 級)
- [ ] 2.8 視覺比對 design-source/index.html(截圖對比 / DOM spec 檢查)
---
## Phase 3 — Auth`/auth``/login` redirect
- [ ] 3.1 建 `app/auth/page.tsx`Client
- [ ] 3.2 Sign in / Sign up tabs + OAuth buttonsGoogle / GitHub)接既有 `/auth/google/start` / `/auth/github/start`
- [ ] 3.3 Email / Password form — submit 顯示「OAuth only」提示(待後端支援)
- [ ] 3.4 `/login` 頁改為 `redirect('/auth')`
- [ ] 3.5 視覺比對 design-source/screens/Auth.jsx
---
## Phase 4 — API Keys`/keys`
- [ ] 4.1 建 `lib/api.ts` fetch wrapper
- [ ] 4.2 建 `lib/apiKeys.ts`listKeys / createKey / patchKey / deleteKey)— 後端未實作時回 `[{ ...from /me }]` 的 fallback
- [ ] 4.3 建 `app/keys/page.tsx`:頂部 new-key-boxsessionStorage flag)、全表格
- [ ] 4.4 「Create new key」呼叫 `POST /api-keys`(後端未實作 → 顯示「coming soon」toast
- [ ] 4.5 Toggle active / trash 接 PATCH / DELETE
- [ ] 4.6 middleware.ts matcher 加 `/keys/:path*`
- [ ] 4.7 原 `/dashboard` 頁的 Key 卡區塊移除,放提示「API Keys 已搬到 /keys」
---
## Phase 5 — Dashboard`/dashboard`
- [ ] 5.1 建 `lib/workflows.ts``lib/apps.ts`
- [ ] 5.2 Rewrite `app/dashboard/page.tsx`(保留 /me session 檢查)
- [ ] 5.3 Main headbreadcrumb / greeting / summary counters
- [ ] 5.4 Apps Grid + empty-state 卡(`/apps` 404 → 只顯示 empty-state
- [ ] 5.5 Workflows Table`/workflows` 404 → 顯示「no workflows — acr push」CTA
- [ ] 5.6 Sidebar 接真實 useremail / display_name)、登出按鈕接 `/auth/logout`
- [ ] 5.7 視覺比對 design-source/screens/Dashboard.jsx
---
## Phase 6 — Workflow Viewer`/workflows/[name]`
- [ ] 6.1 建 `app/workflows/[name]/page.tsx`Client
- [ ] 6.2 Topbarback / logo / breadcrumb / title / saved indicator / share / export / edit-in-claude
- [ ] 6.3 `<Canvas>`SVG defs + 節點定位 + bezier edges;資料從 `GET /workflows/:name`
- [ ] 6.4 Auto-layouttopological depth → columns,同 depth 平均分配 y
- [ ] 6.5 NodeCard 點擊 → DetailPanel
- [ ] 6.6 DetailPanelinput / output schema、tripletdisabled)、last run stats(可選)
- [ ] 6.7 Export YAML`GET /workflows/:name/yaml` → blob download
- [ ] 6.8 Minimap(純顯示)、ZoomControls(簡單 scale
- [ ] 6.9 middleware.ts matcher 加 `/workflows/:path*`
---
## Phase 7 — 清理 + 收尾
- [ ] 7.1 刪除舊 `/dashboard` 不再用的 coderotate/revoke 若全搬到 /keys
- [ ] 7.2 Grep 檢查:除 `cypher.arcrun.dev` 外無任何第三方 API host
- [ ] 7.3 Grep 檢查:無 `mock` / `fixture` / 硬編的假資料陣列(`app-empty` 的字串常數除外)
- [ ] 7.4 Grep 檢查:無新增 npm 依賴(`git diff landing/package.json` 應只改版本,不加條目)
- [ ] 7.5 `pnpm build` 通過;`next-on-pages` 通過
- [ ] 7.6 local `pnpm dev` 手工巡覽 5 screen,每個截圖比對 design-source
- [ ] 7.7 更新 `.agents/specs/arcrun/arcrun.md`,加一段「CLI 1.2.0 搭配新 landing」之類的進度註記
- [ ] 7.8 richblack 認可 → 合併 / deploy
---
## 需要 cypher-executor 搭配的 endpoint(不屬於本 SDD
若 richblack 決定新 endpoint 要和前端同 PR 做:
- [ ] 後端:`GET /workflows`
- [ ] 後端:`GET /workflows/:name` + `/yaml`
- [ ] 後端:`GET /apps`
- [ ] 後端:`GET /api-keys``POST /api-keys``PATCH /api-keys/:id``DELETE /api-keys/:id`
否則:以 empty state 呈現,封測也能運作。
---
## KBDB 整合(配合 matrix/kbdb/.agents/specs/arcrun-key-auth/
Arcrun 用戶的 `ak_xxx` Key 同時可用於 KBDB(捆綁服務,自動開通)。
cypher-executor 需在以下時機呼叫 KBDB
- [ ] 後端:OAuth callback 成功 → `POST /partners`(建立 KBDB partner 記錄)
- [ ] 後端:`PUT /me/api-key/rotate` → 舊 partner revoke + 建新 partner 記錄
- [ ] 後端:`DELETE /me/api-key` → KBDB partner revoke
詳細設計見 `matrix/kbdb/.agents/specs/arcrun-key-auth/design.md`
---
## 目前狀態
- **進度**Phase 0 已完成(4/5;最後一項等 richblack 認可)。
- **阻擋**richblack 認可 + 「開始動工」指令。
- **未啟動**Phase 1-7 全部 `[ ]`
-325
View File
@@ -1,325 +0,0 @@
# arcrun.dev Landing Page — SDD
> **目標**:給工程師一個門面,可以取得 API Key、管理 Key、探索 APISwagger),同時藉此獲得會員 Email。
> **原則**:先快速可用,不追求功能完整。榮譽牆、Python Lib 是後期。
---
## 0. 範圍(這份 SDD 涵蓋)
| 功能 | 說明 |
|---|---|
| 首頁 Hero | 說明 arcrun 是什麼,CTA 取得 API Key |
| OAuth 登入 | Google / GitHub(用自己的 auth recipe — dogfooding |
| API Key 管理 | 查看、Rotate、Revoke |
| Swagger UI | 嵌入 `/api`,讓工程師直接試打 |
| 榮譽牆 `/integrations` | 靜態骨架,先列 20 個 recipe,無動態數字 |
| 中英切換 | `?lang=zh` |
**不在本次範圍**Python lib、Donate 整合、Social Proof 即時數字、貢獻者排行。
---
## 1. 技術選型
### 1.1 框架:Next.jsApp Router
**選 Next.js 而非 Astro 的原因**
- `finally-click` 已有完整 Next.js + OAuth 回調實作,可直接複用模式
- API Key 管理頁有登入態保護需求,Next.js 的 middleware 最直接
- Cloudflare Pages 支援 Next.js`@cloudflare/next-on-pages`
- Astro 在動態路由保護上摩擦較多
### 1.2 部署:Cloudflare Pages
```
arcrun.dev → Cloudflare PagesNext.js
API calls → cypher.arcrun.dev(現有 Worker
```
### 1.3 儲存:現有 cypher-executor CREDENTIALS_KV + 新增 USERS_KV
現有 cypher-executor Worker 已有:
- `CREDENTIALS_KV``{api_key}:cred:{name}` 存 tenant credentials
- `RECIPES`auth recipes
新增需求:
- **USERS_KV**:存 user 帳號,key = `user:{provider}:{provider_user_id}`
- value: `{ email, display_name, api_key, created_at, provider }`
- **SESSIONS_KV**:存 login sessionkey = `sess:{session_id}`
- value: `{ api_key, email, expires_at }`
- TTL = 7 天
兩個 KV 都加到 cypher-executor `wrangler.toml`
### 1.4 OAuth — Dogfooding 自己的 Auth Recipe
登入用 arcrun 自己的 auth recipe
- 不是用 arcrun auth recipe 的 `http_request` runner 去打第三方
- 而是 **直接複用 recipe YAML 裡定義的 OAuth App 設定(client_id/secret**
- Worker 端實作 standard OAuth2 authorization_code flow
**支援提供商(MVP**
- Googlegoogle_drive recipe 的 OAuth App,或另建 arcrun-login Google App
- GitHubgithub recipe 的 OAuth App
登入 OAuth App 與 auth recipe 的 OAuth App **可以是同一個**(只要 scopes 包含 `openid profile email`),但更乾淨的做法是登入用獨立的 Google/GitHub App(只要 email scope),auth recipe 用的是資源存取 App。
**決策:登入用獨立 OAuth App**
- `GOOGLE_CLIENT_ID``GOOGLE_CLIENT_SECRET` — 只申請 `openid profile email`
- `GITHUB_CLIENT_ID``GITHUB_CLIENT_SECRET` — 只申請 `read:user` + `user:email`
- 以 Worker Secret 方式存入 cypher-executor
---
## 2. 頁面結構
```
arcrun.dev/
├── / 首頁(Hero + Code snippet + CTA
├── /login 登入頁(Google / GitHub 按鈕)
├── /auth/callback OAuth callbackPages Function
├── /dashboard API Key 管理(需登入)
├── /api Swagger UI(嵌入 swagger.json
└── /integrations 服務目錄(靜態,20 個 recipe)
```
---
## 3. 登入 / OAuth 流程
### 3.1 流程圖
```
用戶點「Google 登入」
→ GET /auth/google/startWorker 端)
→ redirect 到 Google OAuthstate = random, 存 SESSIONS_KV sess:state:{state} = {provider, redirect_back}
→ 用戶同意
→ GET /auth/callback?code=...&state=...Worker 端)
→ 驗 state
→ 用 code 換 access_tokenPOST google token endpoint
→ 用 token 取 userinfoGET google userinfo
→ upsert USERS_KV user:{provider}:{provider_id} = {email, display_name, api_key, ...}
→ 若新用戶:呼叫現有 /register?email=... 取得 arcrun API Key
→ 建立 sessionSESSIONS_KV sess:{session_id} = {api_key, email, ...}TTL=7d
→ Set-Cookie: arcrun_session={session_id}; HttpOnly; Secure; SameSite=Lax
→ redirect 到 /dashboard
```
### 3.2 「若新用戶取得 API Key」的邏輯
現有 `/register` endpoint 接受 `email` 回傳 `api_key`HMAC 確定性)。
但 landing page 需要的是**真正綁定到用戶帳號的 key**,且用戶可以 rotate/revoke。
**方案:延伸現有 register endpoint**
`/register` 目前:`HMAC(secret, email)` → 確定性 api_key,存 CREDENTIALS_KV
新增邏輯:
1. 若 USERS_KV 已有此 user → 直接用記錄裡的 api_key
2. 若新 user → 呼叫現有 `/register`(保持 HMAC 確定性邏輯)→ 拿到 api_key → 存入 USERS_KV
**好處**:不破壞現有 register 邏輯;登入後的 dashboard 顯示的 key = 現有 key = 封測用的 key。
### 3.3 Rotate / Revoke
- **Rotate**:產生新 UUID v4 key → 更新 USERS_KV 記錄 → 舊 key 失效(透過把新 key 加到 CREDENTIALS_KV,舊 key 的資料都跟著 API Key 命名空間走,所以 credentials 會留在舊 namespace
- 簡化版:Rotate 後顯示提示「您的 workflow credentials 已和舊 Key 分離,請重新設定」
- **Revoke**USERS_KV 記錄 `revoked: true` → Worker middleware 拒絕此 key
---
## 4. API 端點(新增到 cypher-executor
```
GET /auth/google/start → redirect 到 Google OAuth
GET /auth/github/start → redirect 到 GitHub OAuth
GET /auth/callback?code=&state= → 換 token、建立 session
POST /auth/logout → 清 session cookie
GET /me → 回傳當前登入用戶資訊(需 session cookie 或 API Key
PUT /me/api-key/rotate → 產生新 key
DELETE /me/api-key → Revoke(標記撤銷)
```
---
## 5. 前端頁面設計
### 5.1 首頁(/
```
Hero:
Stop fighting OAuth.
One API key. Every service. Works anywhere.
[Get API Key — Free] [View on GitHub]
Before/After:
40 行 OAuth 程式碼 → auth.bind("google_drive")
Code Demo(三個 tab):
Python / JavaScript / HTTPn8n 用戶)
[Get Free API Key] 按鈕
```
### 5.2 登入頁(/login
```
arcrun
登入或建立帳號
[Continue with Google]
[Continue with GitHub]
不需要信用卡。API Key 立即可用。
```
### 5.3 Dashboard/dashboard
```
歡迎,{display_name}
您的 API Key
┌────────────────────────────────┐
│ ak_xxxxxxxxxxxxxxxxxxxx [複製] │
└────────────────────────────────┘
[Rotate Key] [Revoke Key]
使用說明:
Authorization: Bearer {key}
或 X-Arcrun-API-Key: {key}
[登出]
```
### 5.4 Swagger UI/api
- 嵌入 `<SwaggerUIBundle>` JSCDN
- `url: 'https://cypher.arcrun.dev/swagger.json'`(現有 Worker 已有 `/docs` openapi endpoint
- 頂部說明:「這是 arcrun 的原始 API。Python / JS lib 是它的包裝,任何能發 HTTP request 的工具都能直接用。」
### 5.5 服務目錄(/integrations
- 靜態列出 20 個 auth recipe(從 seed data 產生)
- 每個 recipe:名稱、認證方式(static_key / service_account)、所需 credentials
- 「找不到你要的服務?開 PR 貢獻 Recipe」CTA
---
## 6. 檔案結構
```
arcrun/landing/ ← 新 Next.js 專案
├── app/
│ ├── layout.tsx
│ ├── page.tsx 首頁
│ ├── login/
│ │ └── page.tsx
│ ├── dashboard/
│ │ ├── page.tsx
│ │ └── middleware.ts (或 root middleware
│ ├── api-docs/
│ │ └── page.tsx Swagger UI
│ └── integrations/
│ └── page.tsx
├── middleware.ts 保護 /dashboard(讀 cookie
├── lib/
│ └── auth.ts session helpers
├── public/
├── next.config.ts
├── package.json
└── wrangler.toml CF Pages 設定
```
cypher-executor 新增:
```
arcrun/cypher-executor/src/routes/
├── auth.ts ← 新增(OAuth start/callback/logout/me
```
cypher-executor wrangler.toml 新增:
```toml
[[kv_namespaces]]
binding = "USERS_KV"
id = "<to be created>"
[[kv_namespaces]]
binding = "SESSIONS_KV"
id = "455d0505c7534883a4d4985ab8295857"
```
---
## 7. 環境變數 / Secrets
### cypher-executorWorker Secrets
```
GOOGLE_CLIENT_ID Google OAuth App client_id(僅 openid profile email scope
GOOGLE_CLIENT_SECRET Google OAuth App client_secret
GITHUB_CLIENT_ID GitHub OAuth App client_idread:user + user:email scope
GITHUB_CLIENT_SECRET GitHub OAuth App client_secret
SESSION_SECRET 隨機 32 bytes,用於 HMAC session ID(或直接用 UUID
```
### landingPages Environment Variables
```
NEXT_PUBLIC_API_BASE https://cypher.arcrun.dev
```
---
## 8. 實作步驟(Checklist
### Phase 1cypher-executor 後端擴充
- [x] `wrangler kv:namespace create USERS_KV` → 填入 wrangler.toml (id: 25bef01d079148919578894434d58c4d)
- [x] `wrangler kv:namespace create SESSIONS_KV` → 填入 wrangler.toml (id: 455d0505c7534883a4d4985ab8295857)
- [x] 建立 `arcrun/cypher-executor/src/routes/auth.ts`
- [x] GET `/auth/google/start`
- [x] GET `/auth/github/start`
- [x] GET `/auth/callback`(換 token → userinfo → upsert USERS_KV → 建 session → Set-Cookie → redirect
- [x] POST `/auth/logout`
- [x] GET `/me`(讀 session cookie 或 API Key header
- [x] PUT `/me/api-key/rotate`
- [x] DELETE `/me/api-key`revoke
- [x]`src/index.ts` 掛載 `authRouter`
- [ ] `wrangler secret put GOOGLE_CLIENT_ID` 等 4 個 secrets ← **用戶需自建 Google/GitHub OAuth App**
- [x] `wrangler deploy` ← 已部署(Worker version 7877857b
### Phase 2Next.js Landing 專案
- [x] `npx create-next-app@latest arcrun/landing --typescript --tailwind --app`
- [x] 設定 `@cloudflare/next-on-pages`Next.js 15 + .npmrc legacy-peer-deps
- [ ] 建立 `middleware.ts`(保護 /dashboard,讀 `arcrun_session` cookie)← 待做
- [x] 首頁(`app/page.tsx`):Hero + Code Demo tab + CTA
- [x] 登入頁(`app/login/page.tsx`):Google / GitHub 按鈕(href 到 cypher.arcrun.dev/auth/google/start
- [x] Dashboard`app/dashboard/page.tsx`):顯示 API KeyRotate / Revoke 按鈕
- [x] Swagger UI`app/api-docs/page.tsx`):client component,動態 import swagger-ui CDN
- [x] 服務目錄(`app/integrations/page.tsx`):靜態,列 20 個 recipe
- [ ] 中英切換 ← 低優先,可延後
- [x] `wrangler pages deploy` → https://42a8d302.arcrun-landing.pages.dev
- [ ] Cloudflare dashboard 設定 arcrun.dev custom domain → arcrun-landing Pages project
### Phase 3:驗收(待 OAuth Secrets 填入後)
- [ ] Google / GitHub OAuth 完整流程(登入 → dashboard → 看到 key
- [ ] Rotate:新 key 出現
- [ ] Revoke:舊 key 的 API 呼叫回傳 401
- [ ] Swagger UI 正常載入,可試打 `/health`
- [x] `/integrations` 正確列出 20 個服務
---
## 9. 待決事項(開始實作前確認)
| 問題 | 預設決策 |
|---|---|
| Google OAuth App 是否要另建(只有 email scope? | 是,另建;auth recipe 的 App 不動 |
| Rotate 後舊 credentials 是否遷移? | 不遷移,顯示提示 |
| Domain arcrun.dev 是否已購入且在 Cloudflare | 假設是(wrangler.toml 有設 zone_name |
| 登入後 redirect 預設到 /dashboard | 是,可從 `?redirect=` 覆寫 |
@@ -1,281 +0,0 @@
# Design Document: arcrun SDK Libraries + Website
## Overview
本設計涵蓋 arcrun 的三個新增交付物:
1. Python SDK lib`pip install arcrun`
2. JS/TS SDK lib`npm install arcrun``@arcrun/sdk`
3. arcrun.dev 網站完善(零件列表、recipe 列表、登入管理)
**設計原則:修改不重建。** SDK 是 `cypher.arcrun.dev` HTTP API 的 thin wrapper。不在 client 端重新實作任何 server 端已有的邏輯(workflow 執行、credential 注入、auth recipe 解析)。唯一在 client 做的是 AES-GCM 加密(因為 server 的 POST /credentials 期望收到加密後的 payload)。
---
## Architecture
### 系統關係圖
```
使用者程式碼
├── CLIacr → cypher.arcrun.devHTTP API
├── Python SDKarcrun → cypher.arcrun.devHTTP API
└── JS SDKarcrun / @arcrun/sdk → cypher.arcrun.devHTTP API
arcrun.dev 網站(Next.js / Cloudflare Pages
├── /login → /auth/google/start, /auth/github/startcypher.arcrun.dev
├── /dashboard → /me, /me/api-key/rotatecypher.arcrun.dev
├── /integrations → /auth-recipescypher.arcrun.dev
└── /components → /recipes + 靜態零件清單(embedded
cypher.arcrun.devCloudflare Worker — cypher-executor,不改)
├── POST /credentials ← 接收 { name, encrypted, iv }
├── GET /credentials ← 列出 credential 名稱
├── DELETE /credentials/:name ← 刪除 credential
├── GET /auth-recipes ← 列出 20 個 auth recipe
├── GET /auth-recipes/:service ← 單一 recipe 詳情
├── POST /webhooks/named ← 部署 workflow
├── POST /webhooks/named/:name/trigger ← 觸發 workflow
├── GET /webhooks/named ← 列出 workflow
├── POST /register ← 註冊取得 API Key
├── GET /me ← 當前用戶資訊
└── /auth/* ← OAuth 流程
```
---
## Python SDK`arcrun/python-sdk/`
### 目錄結構
```
arcrun/python-sdk/
├── pyproject.toml ← hatchling build, name="arcrun", deps=[httpx>=0.27, cryptography>=42]
├── README.md
└── arcrun/
├── __init__.py ← from .client import Arcrun
├── client.py ← Arcrun class(主入口)
├── crypto.py ← AES-GCM 加密(client 端,用 cryptography 套件)
├── creds.py ← CredentialsClientpush/list/delete
├── auth.py ← AuthClientsetup/bind/get_token/list_services
└── workflows.py ← WorkflowClientrun/push/list/delete
```
### API 設計
```python
from arcrun import Arcrun
# 建構 — api_key 從參數 > 環境變數 > ~/.arcrun/config.yaml 自動取得
client = Arcrun()
# 或明確指定
client = Arcrun(api_key="ak_xxx", encryption_key="hexstring")
# Auth:設定並綁定服務
client.auth.setup("openai", api_key="sk-xxx") # 加密 + 上傳
openai_client = client.auth.bind("openai") # 取回 pre-auth client
response = openai_client.get("/models") # httpx.Client
token = client.auth.get_token("openai") # raw token string
services = client.auth.list_services() # [{ service, display_name, ... }]
# Credentials:低階操作
client.creds.push("my_token", "value123")
names = client.creds.list()
client.creds.delete("my_token")
# Workflows
result = client.workflows.run("my-flow", {"email": "user@example.com"})
url = client.workflows.push("my-flow", graph_dict)
workflows = client.workflows.list()
```
### Credential 加密流程
```
setup("openai", api_key="sk-xxx")
1. GET /auth-recipes/openai → recipe(含 required_secrets, inject
2. 對應 required_secrets[0].key = "openai_api_key"
3. crypto.py 用 encryption_key AES-GCM 加密 "sk-xxx"
4. POST /credentials → { name: "openai_api_key", encrypted, iv }
5. 本地 _cred_cache["openai_api_key"] = "sk-xxx"(供 bind() 用)
bind("openai")
1. GET /auth-recipes/openai → recipe.inject.header = { Authorization: "Bearer {{secret.openai_api_key}}" }
2. 用 _cred_cache["openai_api_key"] 替換 template → "Bearer sk-xxx"
3. 回傳 AuthenticatedClient(base_url="https://api.openai.com/v1", headers={"Authorization": "Bearer sk-xxx"})
```
**注意**`bind()` 依賴 `setup()` 在同一 session 建立的 `_cred_cache`。跨 session 使用時(credential 已上傳但 cache 不存在),`bind()` 無法解析 template — 此時 `get_token()` 也無法返回值。**這是已知限制,封測期間先接受。** 長期解法是 server 提供 `/credentials/:name/secret` 解密端點(u6u-core/credentials 已有)。
### 關鍵差異:crypto.py 的定位
`crypto.py` 只做 **加密**encrypt),不做解密。
功能等同 `u6u-core/credentials/src/actions/crypto.ts``encrypt()` 函數。
解密只在 server 端發生(cypher-executor 的 `credential-injector.ts``u6u-core/credentials/getCredentialSecret.ts`)。
---
## JS/TS SDK`arcrun/js-sdk/`
### 目錄結構
```
arcrun/js-sdk/
├── package.json ← name TBDarcrun vs @arcrun/sdk),tsup build
├── tsconfig.json ← ES2020, NodeNext
└── src/
├── index.ts ← export class Arcrun
├── crypto.ts ← Web Crypto API AES-GCM encryptclient 端)
├── creds.ts ← CredentialsClientpush/list/delete
├── auth.ts ← AuthClientsetup/bind/getToken/listServices
└── workflows.ts ← WorkflowClientrun/push/list/delete
```
### API 與 Python SDK 對等
```typescript
import { Arcrun } from 'arcrun' // or '@arcrun/sdk'
const client = new Arcrun() // reads ARCRUN_API_KEY from env
await client.auth.setup('openai', { api_key: 'sk-xxx' })
const oai = await client.auth.bind('openai')
const models = await (await oai.get('/models')).json()
const token = await client.auth.getToken('openai')
const services = await client.auth.listServices()
await client.creds.push('my_token', 'value')
const names = await client.creds.list()
const result = await client.workflows.run('my-flow', { email: 'user@example.com' })
```
### Build 產物
```
dist/
├── index.js ← ESM
├── index.cjs ← CJS
├── index.d.ts ← TypeScript 型別
└── index.d.cts
```
### Crypto 實作
使用 Web Crypto API`crypto.subtle`),相容 Node 18+ / browsers / CF Workers / Deno
```typescript
async function encrypt(plaintext: string, hexKey: string): Promise<{ encrypted: string; iv: string }> {
const key = await crypto.subtle.importKey('raw', hexToBytes(hexKey), { name: 'AES-GCM' }, false, ['encrypt']);
const iv = crypto.getRandomValues(new Uint8Array(12));
const ciphertext = await crypto.subtle.encrypt({ name: 'AES-GCM', iv }, key, new TextEncoder().encode(plaintext));
return { encrypted: toBase64(ciphertext), iv: toBase64(iv.buffer) };
}
```
---
## arcrun.dev 網站
### 現有狀態(`arcrun/landing/`
已完成:
- [x] `/` — Hero + Code DemoPython/JS/HTTP tabs
- [x] `/login` — Google + GitHub OAuth 按鈕(前端 OK,需設 OAuth secrets
- [x] `/dashboard` — API Key 查看/Copy/Rotate/Revoke(依賴 `/me` API
- [x] `/integrations` — 20 個 recipe 靜態卡片
- [x] `/api-docs` — Swagger UI CDN 嵌入
- [x] `middleware.ts` — 保護 `/dashboard`(未登入 → `/login`
- [x] Cloudflare Pages 部署
待完成:
- [ ] OAuth secrets 設定(`GOOGLE_CLIENT_ID` / `GOOGLE_CLIENT_SECRET` / `GITHUB_CLIENT_ID` / `GITHUB_CLIENT_SECRET`
- [ ] `/components` 頁面(零件列表 — 21 個 WASM 零件的 input/output/config_example
- [ ] 首頁 code demo 更新為三種使用方式(CLI / Python / JS
- [ ] 登入流程真實驗證
### 新增頁面:`/components`
```
/components
├── 零件卡片(21 個)
│ ├── canonical_id
│ ├── display_name
│ ├── description
│ ├── input_schemarequired / optional 欄位)
│ ├── output_schema
│ ├── credentials_requiredif any
│ └── config_exampleYAML code block
└── 分類篩選(邏輯 / API / 控制流)
```
資料來源:靜態嵌入(從 `registry/components/*/component.contract.yaml` 在 build 時讀取),不依賴 runtime API。
### OAuth 設定(待 richblack 操作)
需要在 Cloudflare Worker 設定以下 secrets
```bash
wrangler secret put GOOGLE_CLIENT_ID --name arcrun-cypher-executor
wrangler secret put GOOGLE_CLIENT_SECRET --name arcrun-cypher-executor
wrangler secret put GITHUB_CLIENT_ID --name arcrun-cypher-executor
wrangler secret put GITHUB_CLIENT_SECRET --name arcrun-cypher-executor
wrangler secret put SESSION_SIGNING_SECRET --name arcrun-cypher-executor
```
---
## server 端需要的修改
### cypher-executor 修改(最小化)
目前 `POST /credentials` 端點(`routes/credentials.ts`)接收 `{ name, encrypted, iv }` 後直接存 KV。
SDK 需要的改動:
1. **`GET /auth-recipes` 回應格式**:目前 list 端點回 `{ recipes: [...] }` 但 recipe 的 `service` 欄位是 key — SDK 已在 list_services() 正確處理 ✅
2. **`GET /auth-recipes/:service` 回應格式**:目前回 `{ success: true, recipe: {...} }` — SDK 需讀 `body.recipe` 而非 body 本身 ✅
3. **`POST /credentials` 不需改動** — SDK 自己做 AES-GCM 加密後送 `{ name, encrypted, iv }`
4. **未來**:新增 `GET /credentials/:name/secret` 端點(解密返回 plaintext),讓跨 session 的 `bind()` 能工作。但此端點在 `u6u-core/credentials/src/actions/getCredentialSecret.ts` 已有實作 — 需要在 cypher-executor 整合或 Service Binding 到 u6u-credentials Worker。**封測後再做。**
---
## 不做的事(明確排除)
- ❌ 不在 SDK 裡做 workflow 解析或 YAML 處理 — 那是 CLI 的職責
- ❌ 不在 SDK 裡做 server-side 解密 — 解密只在 server 端
- ❌ 不建新的 credentials Worker — 用現有的
- ❌ 不建新的 KV namespace — 用現有的 CREDENTIALS_KV
- ❌ 不改 cypher-executor 的 credential-injector.ts — 那已經完成且測試通過
---
## 實作順序
```
Phase 1Python SDK 重建 + 測試
1.1 重建 arcrun/python-sdk/(按本 SDD 的結構)
1.2 修正上次的 bugrecipe 回應 wrapper、inject key "header" vs "headers"、secret key mapping
1.3 對 cypher.arcrun.dev live 測試全部 API
1.4 本地安裝測試(pip install -e .
Phase 2JS SDK 重建 + 測試
2.1 重建 arcrun/js-sdk/(按本 SDD 的結構)
2.2 同步修正 Python SDK 發現的所有 recipe 格式問題
2.3 buildtsup+ 本地測試
Phase 3arcrun.dev 網站補完
3.1 新增 /components 頁面
3.2 更新首頁 code demo(三種使用方式)
3.3 OAuth secrets 設定(需 richblack 操作 GCP / GitHub
3.4 登入流程驗證
Phase 4GitHub README + 發布
4.1 更新 arcrun/README.md — 三種 Quick Start
4.2 pip publisharcrun
4.3 npm publishTBD 套件名)
4.4 最終驗證:從零開始 pip install / npm install / 打 API
```
@@ -1,131 +0,0 @@
# Requirements: arcrun SDK Libraries + Website
## Introduction
arcrun 目前有三個使用介面:
1. **CLI**`acr` 指令)— 已完成,用 YAML 定義 workflow 並推送執行
2. **Python / JS SDK lib**(本次新增)— `pip install arcrun` / `npm install arcrun`,讓開發者在寫程式時直接用 arcrun 功能
3. **arcrun.dev 網站**(本次完成)— 登入取得 API Key、管理 Key、瀏覽零件 / recipe 列表
**核心原則**SDK lib 是 `cypher.arcrun.dev` HTTP API 的 thin wrapper。所有業務邏輯(加解密、credential 注入、workflow 執行)都在 server 端完成。Client 端不重做 server 已有的邏輯。
**現有基礎設施**(不重建,直接使用):
- `cypher.arcrun.dev`cypher-executor Workerworkflow 執行、credential 管理、auth recipe、webhook
- `u6u-core/credentials`credential WorkerAES-GCM 加解密)— arcrun/credentials 是其 cherry-pick
- `arcrun/cli`CLI 工具(已發布 npm `arcrun@1.1.0`
- `arcrun/landing`Next.js 前端(已部署 Cloudflare Pages,有 hero/login/dashboard/integrations 骨架)
---
## Glossary
- **SDK lib**Python / JS 套件,wrapping `cypher.arcrun.dev` HTTP API,安裝後可在程式碼中直接使用
- **auth.setup()**:上傳一個服務的 credential(如 Notion token、OpenAI API Key)到 arcrun
- **auth.bind()**:取回已設定服務的 pre-authenticated HTTP client
- **auth.get_token()**:取回某服務的 raw tokenescape hatch,給官方 SDK 用)
- **workflows.run()**:觸發已部署的 workflow
- **workflows.push()**:上傳 workflow 定義
- **Recipe**:描述「如何對某服務認證」的 YAML 設定,存在 RECIPES KV
---
## Requirements
### Requirement 1Python SDK`pip install arcrun`
**User Story:** As a Python 開發者, I want `pip install arcrun` 後在程式碼中使用 arcrun, so that 不用離開寫程式環境就能串接 20+ 服務。
#### Acceptance Criteria
1. THE Python SDK SHALL 以 `arcrun` 套件名發布到 PyPI,支援 Python 3.10+。
2. THE SDK SHALL 提供以下 API
- `Arcrun(api_key=, base_url=)` — 建構 clientapi_key 支援從環境變數 `ARCRUN_API_KEY``~/.arcrun/config.yaml` 自動讀取
- `client.health()` — 健康檢查
- `client.auth.list_services()` — 列出可用 auth recipe 服務
- `client.auth.setup(service, **kwargs)` — 上傳 credential
- `client.auth.bind(service)` — 取得 pre-authenticated HTTP client
- `client.auth.get_token(service)` — 取得 raw token
- `client.creds.push(name, value)` — 上傳加密 credential
- `client.creds.list()` — 列出 credential 名稱
- `client.creds.delete(name)` — 刪除 credential
- `client.workflows.run(name, input)` — 觸發 workflow
- `client.workflows.push(name, graph)` — 上傳 workflow
- `client.workflows.list()` — 列出已部署 workflow
3. THE SDK 的 credential 加密 SHALL 在 client 端完成(使用 `cryptography` 套件 AES-GCM),然後以 `POST /credentials` 上傳加密後的 `{ name, encrypted, iv }` 到 server。
4. THE `auth.bind()` SHALL 從 server 取得 auth recipe 的 inject template,在 client 端用 cache 的 plaintext 值填入,回傳 pre-configured `httpx.Client`
5. THE SDK SHALL 使用 `httpx` 做 HTTP clientasync 版使用 `httpx.AsyncClient`)。
6. THE SDK 位置 SHALL 為 `arcrun/python-sdk/`build 系統用 `hatchling``pyproject.toml`)。
---
### Requirement 2JavaScript/TypeScript SDK`npm install arcrun`
**User Story:** As a JS/TS 開發者, I want `npm install arcrun` 後在程式碼中使用 arcrun, so that 可以嵌入現有 Node.js / Deno / Cloudflare Workers 專案。
#### Acceptance Criteria
1. THE JS SDK SHALL 以 `arcrun` 套件名發布到 npm,提供 ESM + CJS 雙格式 + TypeScript 型別定義。
2. THE SDK SHALL 提供與 Python SDK 對等的 APIcamelCase 版):
- `new Arcrun({ apiKey?, baseUrl? })` — 讀 `process.env.ARCRUN_API_KEY`
- `client.health()` — 回傳 `Promise<unknown>`
- `client.auth.listServices()` / `setup()` / `bind()` / `getToken()`
- `client.creds.push()` / `list()` / `delete()`
- `client.workflows.run()` / `push()` / `list()` / `delete()`
3. THE SDK 的 credential 加密 SHALL 使用 Web Crypto API`crypto.subtle` AES-GCM),相容 Node 18+、browsers、Cloudflare Workers、Deno。
4. THE `auth.bind()` SHALL 回傳一個有 `get/post/put/delete/patch` 方法的 `AuthenticatedClient`base URL + auth headers 已配置。
5. THE SDK SHALL 使用原生 `fetch()` API,不依賴外部 HTTP client 套件。
6. THE SDK 位置 SHALL 為 `arcrun/js-sdk/`build 用 `tsup`ESM + CJS + DTS),`tsconfig.json` target ES2020 + NodeNext module。
7. THE JS SDK 套件名與 CLI 套件名衝突(都叫 `arcrun`),SHALL 使用 `@arcrun/sdk` 或由 richblack 決定套件名。
---
### Requirement 3arcrun.dev 網站完成
**User Story:** As a 潛在用戶, I want 在 arcrun.dev 上登入取得 API Key、瀏覽零件和 recipe 列表, so that 我可以評估 arcrun 是否符合需求並立即開始使用。
#### Acceptance Criteria
1. THE 網站 SHALL 在 `arcrun.dev` 提供以下頁面:
- `/` — 首頁 Hero + 三種使用方式(CLI / Python / JS
- `/login` — Google + GitHub OAuth 登入
- `/dashboard` — 登入後顯示 API Key(查看/Copy/Rotate/Revoke
- `/integrations` — 列出 20 個 auth recipe 服務,可按分類篩選
- `/components` — 列出所有零件(21 個 WASM 零件),顯示 input/output schema、config_example
- `/api-docs` — Swagger UI,可直接試打 API
2. THE 登入 SHALL 使用 Google + GitHub OAuth,流程走 `cypher.arcrun.dev``/auth/*` 端點。
3. THE 登入後 SHALL 自動對該 email 呼叫 `/register` 取得 API Key(若已有則取回現有 key)。
4. THE `/dashboard` SHALL 允許 Rotate(生成新 key)、Revoke(標記失效)、Copy to clipboard。
5. THE 網站 SHALL 部署在 Cloudflare Pages(現有 `arcrun/landing`),使用 Next.js App Router。
6. THE 首頁 code demo 區 SHALL 包含三個 tabPython、JavaScript、HTTP/curl,展示三種使用方式。
---
### Requirement 4GitHub README 更新
**User Story:** As a GitHub 訪客, I want README 清楚說明三種使用方式, so that 我能選擇最適合的方式開始用 arcrun。
#### Acceptance Criteria
1. THE `arcrun/README.md` SHALL 包含三種 Quick Start
- **CLI**`npm i -g arcrun && acr init && acr push workflow.yaml && acr run`
- **Python**`pip install arcrun && from arcrun import Arcrun && ...`
- **JavaScript**`npm install arcrun && import { Arcrun } from 'arcrun' && ...`
2. THE README SHALL 包含完整零件列表(21 個)和 auth recipe 列表(20 個服務)。
3. THE README SHALL 連結到 `arcrun.dev`(取得 API Key)和 Swagger UIAPI 文件)。
---
### Requirement 5SDK 發布
**User Story:** As a SDK 使用者, I want 公開安裝並直接使用, so that 不需要從原始碼 build。
#### Acceptance Criteria
1. THE Python SDK SHALL 發布到 PyPI`pip install arcrun` 可安裝。
2. THE JS SDK SHALL 發布到 npm`npm install arcrun`(或 `@arcrun/sdk`)可安裝。
3. THE 發布前 SHALL 完成以下測試(對 `cypher.arcrun.dev` live API):
- `health()`
- `auth.list_services()`
- `auth.setup()` + `auth.bind()` ✅(至少一個 static_key 服務如 openai
- `creds.push()` + `creds.list()`
- `workflows.list()`
@@ -1,224 +0,0 @@
# Design 補充:`acr init --self-hosted` 一鍵自動化(installer 模式)
> 2026-06-01 初稿 → 2026-06-02 定案改寫(richblack 拍板 installer 形態)。
> 本檔是 `sdk-and-website/design.md` 的單檔補充(規則 02 §4.3 允許)。
> **狀態:design 已與 richblack 對齊;實作前讀 §6 前置依賴。**
> 背景:戰法從 SaaS 轉 self-hosted 開源(docs/HANDOFF-self-host-harness.md §0)。
---
## 1. 定案形態(richblack 2026-06-02
**arcrun CLI = installer / orchestrator**(類似 rustup / nvm:工具本身小,按需從遠端拉真正內容)。
### 用戶只做 4 件事,中間什麼都不用懂:
1. 申請 CF 帳號
2. 安裝 CF CLI`wrangler`
3. 安裝 arcrun CLI`npm i -g arcrun`
4. `acr init --self-hosted`(貼 CF Account ID + API Token)→ **完成,其餘看機器跑**
### CLI 自動做(用戶無感):
- 驗 CF token 權限
- 建 7 個 KV namespace + 1 個 R2 bucket(冪等)
- **從 GitHub release 下載預編譯部署物**(含 24 個 `.wasm` + 各 Worker 的 wrangler.toml + cypher-executor/registry
- 把建好的 KV namespace id 注入各 wrangler.toml + cypher-executor 的 `WORKER_SUBDOMAIN`
- **`wrangler deploy` 部署全部 Worker**(用戶已裝 wrangler
- seed auth recipe + API recipe 進 RECIPES KV
- 寫回 `~/.arcrun/config.yaml`
- 印出「手動 `wrangler secret put ENCRYPTION_KEY` ×3」提示(secret 不自動化,rule 05
### 關鍵技術決策(richblack 2026-06-02
| 決策 | 選擇 | 理由 |
|---|---|---|
| 零件部署物 | **預編譯 `.wasm`**(不在用戶端 build)| 用戶不懂 tinygo、也不該懂。下載即用。 |
| 部署工具 | **wrangler**shell out| 用戶已裝 CF CLIself-host 本來就有上傳能力。CLI 不自己重寫 CF Script Upload API。 |
| 源碼來源 | **GitHub release tarball**(含預編譯 wasm)| 版本明確、不需用戶有 git、`acr update` 拉新 release 同一條路。 |
| 為何不是 git clone | repo **沒 commit `.wasm`**rule 05 build 產物不 commit)→ clone 拿不到 wasm | 必須走含 wasm 的 release artifact。 |
---
## 2. 為什麼是 installer 而非「repo 內掃 wrangler.toml」(推翻初稿)
初稿假設「用戶在 repo 內跑、CLI 掃 wrangler.toml」。**推翻**,因為:
- npm 全域裝的 `acr` 手上**沒有** 24 個 Worker 源碼。
- repo 沒 commit `.wasm`(已查證 `git ls-files .component-builds | grep .wasm` = 0)→ 連 clone 都拿不到可部署的 wasm。
- 用戶不該需要懂 git / tinygo / repo 結構。
→ 正解:CLI 當 installer,從 **GitHub release(含預編譯 wasm** 拉部署物到暫存目錄,在暫存目錄注入 KV id 後 `wrangler deploy`
---
## 3. 流程設計(`initSelfHosted` 改寫)
```
acr init --self-hosted
├─ 1. 問 2 輸入:CF Account ID + CF API Token
wrangler 是否已裝?which wrangler;沒裝 → 提示先裝 CF CLI 再來)
│ 驗 tokenCF API GET /accounts/{id}/tokens/verify + GET /accounts/{id}
│ 缺權限(Workers Scripts Edit / KV Edit / R2 Edit)→ exit 1 指出缺哪個 scope
├─ 2. 建資源(冪等:先 list 已存在就重用)
│ 7 KVWEBHOOKS / CREDENTIALS_KV / RECIPES / USERS_KV /
│ SESSIONS_KV / ANALYTICS_KV / EXEC_CONTEXTrule 01 資料儲存表)
│ 1 R2WASM_BUCKET
├─ 3. 下載部署物:GitHub release tarball → 解壓到暫存目錄 (~/.arcrun/.deploy-<ver>/)
│ 內含:cypher-executor/ + registry/ + .component-builds/*(每個含預編譯 component.wasm + wrangler.toml
├─ 4. 注入設定到暫存目錄的 wrangler.toml(不改用戶 repo,改暫存副本)
│ - 各 Worker 的 KV binding id ← step 2 建立的
│ - cypher-executor [vars] WORKER_SUBDOMAIN ← CF API GET /accounts/{id}/workers/subdomain
├─ 5. 部署:對暫存目錄每個含 wrangler.toml 的 dirshell out
│ `wrangler deploy`env CLOUDFLARE_API_TOKEN=<token>, CLOUDFLARE_ACCOUNT_ID=<id>
│ 分兩層:tier1 = .component-builds/*(先)→ tier2 = cypher-executor / registry(後)
│ 每個 wrangler.toml 已含 workers_dev = true → workers.dev URL 自動啟用
├─ 6. seed recipe 進 RECIPES KV(部署後打新 cypher URL,或直接 CF KV API 寫)
│ - auth recipe:重用 AUTH_RECIPE_SEEDScypher-executor/src/lib/auth-recipe-seeds.ts
│ - API recipe:新增 seed-api-recipes.ts(見 §5
├─ 7. 寫回 configmode: self-hosted + 所有 id + cypher_executor_url = 部署後 workers.dev URL
└─ 8. 印手動 secret 提示:
wrangler secret put ENCRYPTION_KEY --name arcrun-cypher-executor
wrangler secret put ENCRYPTION_KEY --name arcrun-auth-static-key
wrangler secret put ENCRYPTION_KEY --name arcrun-auth-service-account
(三 Worker 共用同一把 key,見 memory: encryption-key-drift-trap
```
### `acr update`(同一條路,未來新零件)
- 拉新 GitHub release → 解壓 → 注入既有 config 的 KV id → wrangler deploy 變動的 Worker。
- 第一期至少做到「重跑等效 init 的部署步驟」;diff-only 部署可後續優化。
---
## 4. 動到的檔案
| 檔案 | 動作 |
|---|---|
| `cli/src/commands/init.ts` | 改寫 `initSelfHosted()`line 105-131)為 installer 流程 |
| `cli/src/lib/cf-api.ts` | 擴充:KV namespace 建立/list、R2 bucket 建立、subdomain 查詢、token verify |
| 新增 `cli/src/lib/deploy.ts`(暫定)| 下載 release tarball + 解壓 + 注入 wrangler.toml + shell out wrangler deploy |
| 新增 `cli/src/commands/update.ts`(暫定)| `acr update`:拉新 release 重部署 |
| 新增 `cli/src/lib/api-recipe-seeds.ts` | API recipe **種子資料**installer 用;放 CLI 端,**不放 cypher-executor/src**——rule 02 §2.2 hook 擋 cypher-executor TS hard-code endpoint,且 seed 資料本就屬 installer 職責)|
| 新增 `cypher-executor/scripts/seed-api-recipes.ts` | seed **腳本**(給 prod 補灌用,import CLI 的種子資料;`scripts/` 不受 §2.2 hook 管)|
| `cli/src/index.ts` | 註冊 `acr update` 指令 |
**不動**cypher-executor 執行路徑、既有零件 wasm 源碼、config 讀取端(config.ts:52 已支援 self-hosted)。
---
## 5. API recipe seed(新增 seed-api-recipes.tsrichblack 2026-06-02 定)
codebase 只有 auth recipe seed。新增 `seed-api-recipes.ts`,把現役 API recipe hard-code 成種子。
### 現役 API recipe(從 prod KV 查得,2026-06-01
- `kbdb_get`+ create_block / patch_block / delete / ingest)→ auth_service: kbdb
- `gmail_send` → google_gmail_sa
- `google_sheets_append` / `google_sheets_read` → google_sheets_sa
- `telegram_send` → telegram
- `line_notify_send` → line_notify
### KBDB recipe 採 Supabase 模式(richblack 2026-06-02
- **KBDB 是 richblack 提供的服務**(跟 arcrun 一樣),採「基礎免費、大量收費」。
- KBDB recipe **進 seed**(展示能力 = 引子,Supabase 模式)。使用者要用 → 去 **arcrun 取統一 API Key**(已有 /register 入口),把 key 設成 credential。
- ⚠️ **FOLLOW-UP(交 KBDB 端)**:現役 endpoint 是 `kbdb.finally.click{{_path}}`。richblack:這是 KBDB 端要改的問題——KBDB 該用統一對外網址提供大家用,不是 finally.click。**seed 先照現況進;KBDB 端改網址後同步更新 seed。** 此事不擋 init 實作。
---
## 6. 部署物產製:commit wasm 進 repo + codeload tarballrichblack 2026-06-02 定案)
> 此節**取代初稿的「GitHub release artifact」構想**。richblack 拍板更輕的做法:
> 直接把預編譯 wasm commit 進 repoCLI 從 GitHub codeload tarball 拿。不需 release.yml 機制。
### 6.1 策略
- **repo 自帶可部署的 wasm**:刪 `.gitignore``*.wasm` 排除,commit 預編譯 wasm 進 repo。
→ repo 本身就是部署來源,CLI 直接拿、用戶用自己的 CF token deploy。
- **CLI 走 codeload tarball**`https://codeload.github.com/richblack/arcrun/tar.gz/{ref}`
ref = main 或 tag)。用戶不需 git、版本可控(tag)。`acr update` 拉新 ref。
- **理由**richblack):「我在我的 CF 能用 = 我已擁有 wasm;用戶指向我的 GitHub 取得 wasm
用他自己的 CF credential deploy。開源,看不看源碼不重要,體驗好最重要。」
### 6.2 ⚠️ 推翻既有鐵律(rule 05)— 需同步改規則
`.claude/rules/05-deploy-convention.md` 明文「`.component-builds/{name}/component.wasm` **不 commit 進 repo**
build 產物)」「Phase 1-3 暫時 commit 過,**之後會加 .gitignore 清理**」。
**本決策反向**commit wasm 進 repoself-host 需 repo 自帶可部署 wasm)。
**實作時必須同步改 rule 05 + .gitignore**,否則 pre-write hook / 規則與實作打架。
→ deploy.yml 的 CI rebuild 步驟仍保留(CI 部署 prod 時用最新 source rebuild,與 commit 的 wasm 不衝突;
commit 的 wasm 是給「self-host 用戶 + acr init」用的部署來源)。
### 6.3 只 commit 部署所需的 wasm(省空間)
- 實況(2026-06-02 查):`registry/components/*.wasm` 23 個(build 中間產物)+
`.component-builds/*/component.wasm` 22 個(部署物),共 **~50MB**。
- **部署只需 `.component-builds/*/component.wasm`**wrangler deploy 認這個)。
**只 commit `.component-builds/*/component.wasm`22 個),不必 commit registry 那 23 個**(省一半)。
`.gitignore` 改成:保留排除 `registry/components/**/*.wasm`(中間產物),只放行 `.component-builds/**/component.wasm`
- ⚠️ **誠實 trade-off**mindset §7):commit wasm 進 repo → 每次 wasm rebuild 都在 git 歷史累積二進位,
**repo 長期會膨脹**。可接受(self-host 體驗優先),但記錄此代價;未來若膨脹過劇,再考慮 release artifact / git-lfs。
### 6.3.1 「錯做成零件」的 3 個不 commitrichblack 2026-06-02
實際 commit 的是 **19 個正當零件**,不是 22。排除的 3 個:`claude_api` / `km_writer` / `kbdb_upsert_block`
- **原因(richblack 修正「待刪」說法)**:它們**不是 endpoint 薄殼,是把工作流硬塞進零件**(違反 DECISIONS §1)。
例:`kbdb_upsert_block` 的 upsert 邏輯應在 KBDB API 那邊(API 提供 upsert endpoint),零件只該驅動它;
現在卻把「GET 找→有則 PATCH 無則 POST」整段工作流塞進零件。本質是工作流/recipe,被錯做成零件。
- **為何「現在就不 commit」而非「先 commit 之後刪」**commit 二進位進 git 歷史後,即使日後 `git rm`
歷史裡仍永久殘留(repo 體積已被佔),除非 rewrite history(很麻煩)。**錯誤的東西不灌進永久歷史。**
- **落地**`.gitignore` 放行 22 個後**再排除這 3 個**(後出現規則勝出);`deploy.ts discoverWorkerDirs`
只部署「同時有 wrangler.toml + component.wasm」的目錄 → self-host 用戶 codeload 拿到的目錄缺這 3 個 wasm → 自然跳過。
- **後續**:這 3 個的降級(變回工作流/recipe)是 BACKLOG 既有待辦,本次不處理,但確保它們不進 self-host 部署來源。
### 6.4 CLI deploy 流程(deploy.ts downloadAndDeploy 補實作)
```
1. 下載 codeload tarballref 預設 mainacr update 可帶 tag)→ 解壓 ~/.arcrun/.deploy-<ref>/
2. 讀解壓出的 .component-builds/* + cypher-executor/ + registry/
3. 各 wrangler.toml 注入 ctx.kvNamespaceIds + cypher-executor WORKER_SUBDOMAIN
4. tier1=.component-builds/*(先)→ tier2=cypher-executor/registry(後)
每個 dirpnpm install(若有 lock)→ CLOUDFLARE_API_TOKEN=<用戶> wrangler deploy
5. 回 cypherExecutorUrl = https://arcrun-cypher-executor.<subdomain>.workers.dev
```
注意:tier2cypher-executor/registry)是 TSwrangler deploy 會在用戶端用內建 esbuild bundle
(不需額外工具,richblack 確認源碼可見不重要、體驗優先 → artifact 含 TS 源碼即可)。
### 6.5 實作順序
1.`.gitignore`(放行 `.component-builds/**/component.wasm`+ commit 22 個 wasm。
2. 同步改 rule 05(記錄此決策推翻原慣例)。
3. 補實 `deploy.ts downloadAndDeploy`codeload 下載 + 注入 + wrangler deploy)。
4. **在 1-2 完成前,downloadAndDeploy 維持誠實 unimplemented,不假裝(mindset §7)。**
### 6.6 未來方向:零件按需安裝(richblack 2026-06-02,現在不做)
- 現在 `acr init --self-hosted` **全裝基礎零件**22 個一次部署)。簡單、夠用。
- **未來若零件數量真的變很多**,再思考「按需安裝」(只裝 workflow 實際用到的零件 / 用戶選裝)。
- **現在不做的理由**(DECISIONS 附錄「會不會累積成債」):零件目前少且未來絕大多數是 recipe
(不需 deploy)→ 為「零件爆量」做按需安裝基建 = 為不存在的規模做自動化 = 過度工程。
零件真的爆量再回頭做,屆時是「未來一次性處理的設計點」,現在不必焦慮。
---
## 7. 驗收標準(客觀證據,mindset §7)
1. richblack 用**全新 CF 帳號** + wrangler 已裝 + 一個 CF API Token 跑 `acr init --self-hosted`
→ 全程無手動建 KV / 無手動 clone / 無 tinygo / 無手動填 namespace id。
2. 跑完印 secret 提示,richblack 手動 `wrangler secret put ENCRYPTION_KEY` ×3。
3. `acr push` 一個含 http_request + 自建 recipe 的 workflow → trigger → **HTTP 2xx + execution trace**
4. 冪等:重跑 init 不重建已存在 KV / 不報錯。
5. `acr update` 拉新 codeload tarballtag)→ 重部署成功。
---
## 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)。
@@ -1,138 +0,0 @@
# Implementation Plan: arcrun SDK Libraries + Website
## Overview
按 Design 的四個 Phase 實作。原則:修改不重建,SDK 是 HTTP API thin wrapper,加密只在 client 做 encrypt(不做 decrypt)。
**前置依賴**:必須先完成 `credential-primitives-wasm/tasks.md` 的 Phase 0-3(核心合併 + WASM primitives),確認核心穩定後才開始建三個介面。
---
## Phase 0(前置):核心合併 + WASM 改寫
> 詳見 `.agents/specs/arcrun/credential-primitives-wasm/tasks.md`
>
> 摘要:
> - 合併 u6u-core → arcrun(搬 builtins、刪重複 credentials
> - credential-injector TS → auth_static_key / auth_service_account WASM
> - 刪除 component-loader 內建 API recipes TS
> - 驗證 20 個 auth recipe 正常運作
---
## Phase 1Python SDK
- [ ] 1. 建立 `arcrun/python-sdk/` 目錄
- [ ] 1.1 `pyproject.toml`name=arcrun, deps=[httpx>=0.27, cryptography>=42], build-system=hatchling
- [ ] 1.2 `arcrun/__init__.py``from .client import Arcrun`
- [ ] 1.3 `arcrun/crypto.py`AES-GCM encrypt only(使用 `cryptography` 套件)
- [ ] 1.4 `arcrun/creds.py`CredentialsClient — push(加密 + POST /credentials)、listGET /credentials)、delete
- [ ] 1.5 `arcrun/auth.py`AuthClient — setupfetch recipe → match secrets → encrypt → push)、bindfetch recipe → resolve headers from cache → return AuthenticatedClient)、get_token、list_services
- [ ] 1.6 `arcrun/workflows.py`WorkflowClient — runPOST /webhooks/named/{name}/trigger)、pushPOST /webhooks/named)、listGET /webhooks/named)、delete
- [ ] 1.7 `arcrun/client.py`Arcrun class — 讀 api_key / encryption_key 從 param > env > config.yaml
- [ ] 2. 修正上次已知的 bug
- [ ] 2.1 `_fetch_recipe()` 回應是 `{ success: true, recipe: {...} }`,需讀 `.recipe` 欄位
- [ ] 2.2 `inject` 下的 key 是 `header`singular),不是 `headers`
- [ ] 2.3 `required_secrets[].key` 是 prefixed(如 `openai_api_key`),setup() 的 kwargs alias 要能對應
- [ ] 2.4 `list_services()` 回應的 recipe 用 `service` 欄位(不是 `service_id`
- [ ] 3. 測試(對 cypher.arcrun.dev live API
- [ ] 3.1 `health()``{"ok": true}`
- [ ] 3.2 `auth.list_services()` → 20 個服務
- [ ] 3.3 `auth.setup("openai", api_key="sk-test-dummy")` → 成功
- [ ] 3.4 `auth.bind("openai")` → AuthenticatedClient with Authorization header
- [ ] 3.5 `auth.get_token("openai")` → "sk-test-dummy"
- [ ] 3.6 `creds.push("test_token", "value123")` → 成功
- [ ] 3.7 `creds.list()` → 含 "test_token"(注意 KV eventual consistency
- [ ] 3.8 `workflows.list()` → []
- [ ] 3.9 cleanup: `creds.delete("test_token")`
---
## Phase 2JS/TS SDK
- [ ] 4. 建立 `arcrun/js-sdk/` 目錄
- [ ] 4.1 `package.json`name TBDarcrun vs @arcrun/sdk),deps=devDeps onlytsup, typescript, @types/node
- [ ] 4.2 `tsconfig.json`ES2020, NodeNext
- [ ] 4.3 `src/crypto.ts`Web Crypto API AES-GCM encrypt only
- [ ] 4.4 `src/creds.ts`CredentialsClient — push/list/delete via fetch
- [ ] 4.5 `src/auth.ts`AuthClient — setup/bind/getToken/listServices
- [ ] 4.6 `src/workflows.ts`WorkflowClient — run/push/list/delete
- [ ] 4.7 `src/index.ts`export class Arcrun + re-exports
- [ ] 5. 同步修正(與 Python SDK 同樣的 recipe 格式問題)
- [ ] 5.1 `_fetchRecipe()``body.recipe`
- [ ] 5.2 inject key: `header` not `headers`
- [ ] 5.3 setup() secret key alias matching
- [ ] 5.4 listServices() 用 `service` 欄位
- [ ] 6. Build + 測試
- [ ] 6.1 `tsup` build → dist/index.js + dist/index.cjs + dist/index.d.ts
- [ ] 6.2 Node.js 腳本對 live API 測試(同 Python 測試項目)
---
## Phase 3arcrun.dev 網站
- [ ] 7. 新增 `/components` 頁面
- [ ] 7.1 從 `registry/components/*/component.contract.yaml` 讀取 21 個零件資料
- [ ] 7.2 卡片顯示:canonical_id, display_name, description, input required/optional, credentials_required, config_example
- [ ] 7.3 分類篩選:邏輯類 / API 類 / 控制流類
- [ ] 8. 更新首頁
- [ ] 8.1 Code demo tabs 改為 CLI / Python / JS 三個
- [ ] 8.2 CLI tab 展示 `acr init → acr push → acr run`
- [ ] 8.3 Python tab 展示 `pip install arcrun → Arcrun() → auth.setup → auth.bind`
- [ ] 8.4 JS tab 展示 `npm install arcrun → new Arcrun() → auth.setup → auth.bind`
- [ ] 9. OAuth 流程補完
- [ ] 9.1 確認 cypher-executor 的 `/auth/google/start``/auth/github/start``/auth/callback` 路由正確
- [ ] 9.2 提供 richblack OAuth secrets 設定指令清單
- [ ] 9.3 richblack 設定 secrets 後驗證登入流程
- [ ] 10. 部署
- [ ] 10.1 Cloudflare Pages build + deploy
- [ ] 10.2 驗證所有頁面可存取
---
## Phase 4README + 發布
- [ ] 11. 更新 `arcrun/README.md`
- [ ] 11.1 三種 Quick StartCLI / Python / JS
- [ ] 11.2 零件列表(21 個)
- [ ] 11.3 Auth Recipe 列表(20 個服務)
- [ ] 11.4 連結到 arcrun.dev 和 Swagger UI
- [ ] 12. 發布
- [ ] 12.1 Python SDK`pip install build && python -m build && twine upload dist/*`
- [ ] 12.2 JS SDK`npm run build && npm publish`
- [ ] 12.3 驗證:從零開始 `pip install arcrun` / `npm install arcrun` + hello world
---
## Phase 5acr init --self-hosted installer2026-06-02 新增)
> 定稿 design`self-hosted-init.md`。CLI = installer:建 KV/R2 + 拉預編譯 wasm + wrangler deploy + seed。
> 用戶只做:申請 CF 帳號 → 裝 wrangler → 裝 acr → acr init --self-hosted。其餘自動。
> 背景:戰法轉 self-hosted 開源(docs/HANDOFF-self-host-harness.md)。
- [x] 13.1 API recipe 種子 — **位置修正**:種子資料放 `cli/src/lib/api-recipe-seeds.ts`installer 用,避開 cypher §2.2 hook),seed 腳本 `cypher-executor/scripts/seed-api-recipes.ts`import 種子,給 prod 補灌)。10 個現役 recipekbdb_*/gmail_send/google_sheets_*/telegram_send/line_notify_send)。KBDB Supabase 模式進 seedfinally.click 是 KBDB 端 follow-up,已註於 api-recipe-seeds.ts
- [x] 13.2 `cli/src/lib/cf-api.ts` 新增 `CfAccountClient`verifyAccess / listKvNamespaces / ensureKvNamespace(冪等)/ ensureR2Bucket(冪等)/ getWorkersSubdomain
- [x] 13.3 `cli/src/commands/init.ts` `initSelfHosted()` 改寫:驗 token → 建 7 KV + R2 → 查 subdomain → downloadAndDeploy → 寫 config → seed(部署完成時)→ 印 secret 提示。誠實:部署未自動化時明說,不假綠
- [x] 13.4 `cli/src/lib/deploy.ts`REQUIRED_KV/R2/SECRET 常數 + wranglerAvailable() + **downloadAndDeploy 已補實**codeload tarball 下載 + 解壓 + discoverWorkerDirs 分 tier + injectWranglerConfig 注入 KV id/subdomain + runWranglerDeploy;部分失敗誠實收集回報,不假綠)
- [x] 13.5 `cli/src/commands/update.ts` + index.ts 註冊 `acr update`self-hosted 重部署,同走 downloadAndDeploy
- [x] 13.6 部署物產製:**改用 commit wasm 進 repo + codeload**(取代 release artifactrichblack 2026-06-02,§6
- `.gitignore` 否定規則放行 `.component-builds/**/component.wasm`(registry 中間產物仍排除)→ 已驗 git check-ignore
- rule 05 同步改(記錄推翻「wasm 不 commit」+ trade-off
- commit 22 個 `.component-builds/*/component.wasm` 進 repo
- [ ] 13.7 驗收:全新 CF 帳號跑 acr init --self-hosted 全自動;acr push workflow → trigger 2xx + trace**待 richblack 用第二帳號實測** + push 含 wasm 的 commit 到 GitHub 後 codeload 才拿得到)
- [x] 13.8 typecheckcli `tsc --noEmit` exit 0
## Notes
- JS SDK 套件名需 richblack 決定(`arcrun` 已被 CLI 佔用 → 可能用 `@arcrun/sdk`
- OAuth secrets 設定需 richblack 手動操作(GCP Console + GitHub Settings
- `bind()` 跨 session 限制是已知的,封測期間先接受
- credential 加密用的 `encryption_key` 目前由 `/register` 回傳,`acr init` 自動存入 config
@@ -1,218 +0,0 @@
# Design: Component Gatekeeping(零件投稿真把關)
> 2026-05-29。實作 requirements.md 的 R1-R6。
---
## ⚠️ 方向修正(richblack 2026-05-30):投稿走 GitHub PR,廢 registry self-service
**零件投稿管道 = GitHub PR,不是 registry submit API。** 理由與影響見下;以下 §0-§9 的 registry submit 設計,
凡屬「self-service 投稿管道」者**作廢**,凡屬「把關邏輯」者**搬到 CI(PR check)跑**。
**為何改:** primitive 極少、未來絕大部分是 recipe → 新增零件是稀有低頻事件,不需 self-service 自動化管道。
PR 天然滿足每道閘門:
| 設計的閘門 | PR 怎麼天然滿足 |
|---|---|
| G0 人類閘門 | PR 必須有人 mergerichblack approve);AI 偽造不了 GitHub approve |
| 舉證「為何不是工作流」 | PR descriptionreview 時看 |
| G1 假零件 / G3 純WASI / G4 Gherkin / 覆蓋檢查 / 黃金向量 | **CIPR check)跑** —— CI 有 tinygo + 能 runtime 跑 wasm**繞開 CF Workers 不能 runtime 編譯 wasm 的 venue 牆** |
**§8 衝突釐清:** DECISIONS §8「不依賴 GitHub Actions」指**執行鏈路**init/push/run/recipe,常態高頻,
用戶機器+CF)。**零件投稿是稀有低頻、該由 PR 治理**,用 PR/CI 不違反 §8——反而更對(CI 能跑 wasm,
registry Worker 不能)。需在 DECISIONS 補這個區別(待 richblack 確認改穩定文件)。
**哪些作廢 / 哪些保留:**
- ❌ 作廢:registry submit API 當主投稿管道、四路(CLI/MCP/py/jsself-service 投稿、平台端 sandbox 重跑、`acr parts publish` 加人類閘門(投稿不走 CLI 了)。
- ✅ 保留並搬 CI:G1 假零件偵測邏輯(detectFakeComponent.ts)、G3 純WASIwasmImports.ts)、G4 Gherkin 真跑(CI 能跑 wasm)、B 覆蓋檢查、黃金向量人工核對。
- ✅ 已 commit 的 registry G0/G1/G3 程式碼**保留不刪**(無害,且 G1/G3 邏輯被 CI 複用),但 registry submit 不再是主管道。
- ✅ R5 本機 hook(擋 CC 直接造零件目錄)仍要 —— 它擋的是「繞過 PR 直接改 repo」,與 PR 管道互補。
**以下 §0-§9 為原 design,閱讀時套用上述修正。**
---
## 0. 架構總覽
### 0.0 範圍:把關跨公共庫 + self-hosted 私人庫(richblack 2026-05-29
把關**不是公共庫專屬**。每個 self-hosted 部署有自己的零件庫(自己的 registry Worker)。
**加入公共庫或任何 self-hosted 私人庫,都跑同一套把關鏈(G0-G6 + 本機 hook)。**
- 實作上天然成立:把關邏輯在 registry Worker code 裡,self-hosted 跑同一份 registry → 把關跟著走,不需公私庫分兩套。
- G0 人類閘門在 self-hosted 下,「人類」= 部署擁有者本人(防的是「他的 AI 自作主張把東西做成零件」,不是防他本人;他自己確認 + 舉證即可過)。
- 理由:self-hosted 一樣有「自用服務沒驗證就變零件」的風險,且私人庫零件之後可能貢獻回公共。
### 0.1 把關鏈
投稿零件的唯一入口是 registry Worker 的 submit。把關鏈(依序,任一失敗即退稿):
```
submit 請求(帶 wasm + contract + 人類確認憑證 + 舉證)
├─ G0 人類閘門(R4) ← 最先擋:沒人類確認 + 舉證 → 403
├─ G1 假零件偵測(R2 ← contract/原碼有外部 URL 或 http 子集 → 退稿指回正路
├─ G2 size_check(已有)
├─ G3 syscall_scan + 純WASIR3)← 擴充:只准 WASI preview1 + u6u host func 白名單
├─ G4 gherkin_testsR1 ← 真跑 WASMgiven→stdin→比對 then_contains
├─ G5 cold_startmock,標未實作)
└─ G6 runtime_compatmock,標未實作)
→ 全過 → 派 hash → 寫 KV
```
另一道獨立防線:**本機 hook**(R5),擋 CC 繞過 API 直接在 repo 造零件目錄。
---
## 1. G0 人類閘門(R4)— registry submit endpoint
### 1.1 請求格式增欄
submit 請求 body 增兩個欄位:
```ts
interface SubmitRequest {
wasm_base64: string;
contract: ComponentContract;
human_confirmation?: {
confirmed_by_human: true; // 必須為 literal true
reason_why_not_workflow: string; // 非空,AI 舉證
confirmed_at: string; // ISO timestamp
};
skip_acceptance?: boolean; // 既有:backfill 用(仍保留)
}
```
### 1.2 閘門邏輯
```
若 skip_acceptancebackfill 既有零件)→ 跳過 G0(這些是已驗、已部署的存量,不是新投稿)
否則(新投稿):
若無 human_confirmation 或 reason_why_not_workflow 空 → 403
"建零件需人類確認。請用 `acr parts publish`(會互動式問你),
並說明為何工作流做不到。預設假設工作流能做——先試工作流 / recipe。"
記錄 reason_why_not_workflow 進 KV metadata(軌跡可審)
```
### 1.3 四路收斂(CLI / MCP / Python / JS
- 它們建零件都呼叫 registry submit endpoint → G0 在 endpoint,自動四路通管。
- **CLI `acr parts publish`**:強制互動式 prompt 問人類「(1) 工作流為何做不到?(2) 確認要建零件?」,把答案組成 `human_confirmation` 送出。非互動環境(AI 直跑)`acr` 偵測 stdin 非 TTY → 拒絕並提示「需人類互動」。
- MCP / Python / JS lib:傳 `human_confirmation` 才能成功;它們的 SDK 文件註明此欄位需人類提供。
- **誠實限制**(寫進 mindset + 文件):AI 技術上能偽造 `confirmed_by_human:true`。靠 reason 留記錄 + mindset 明示「絕不代替人類確認建零件」+ 軌跡可審,讓偽造成明確越界,不聲稱不可能繞過。
## 2. G1 假零件偵測(R2
新增 `detectFakeComponent(contract, wasmBytes): string | null`
- (a) **外部 URL/domain**:掃 contract 的 description / input_schema / output_schema 文字,及 wasm binary 文字,比對 URL pattern`https?://`、常見 domain)。命中 → 退稿。
- (b) **http_request 子集**:若 contract 宣告只做「打固定 endpoint」(heuristicdescription 含「打/呼叫 ... API/endpoint」且 input 有 url-like 欄位且無實質邏輯運算),標記疑似。
- 退稿訊息:「偵測到疑似假零件(寫死 endpoint / http 子集)。這該是 API recipehttp_request + 固定設定)或工作流,不是零件。見 DECISIONS §1。」
- 排除:`auth_*` primitivecredential 後端,DECISIONS §3b 不適用假零件判準)、`http_request` 自己。
## 3. G3 純 WASI 把關(R3
擴充現有 `scanSyscalls`
- 現況:掃 `FORBIDDEN_SYSCALLS` 黑名單。
- 擴充:改為「import 白名單」——解析 wasm import section,確認所有 import module 只屬 `wasi_snapshot_preview1` + `u6u`host functions)。出現其他 module → 退稿(runtime 鎖定風險)。
- 實作:簡易 wasm import section 解析(不需完整 wasm parser,掃 import 段的 module name 字串)。
## 4. G4 Gherkin 真實作(R1)— **修訂(2026-05-29richblack review**
### 4.0 為何不能在 registry Worker 跑(原設計作廢)
原設計假設 registry Worker instantiate 投稿 wasm 跑 Gherkin。**此假設錯誤**
- **Cloudflare Workers 禁止 request-time 編譯 WASM**`new WebAssembly.Module(bytes)` / `WebAssembly.compile()` 只能 startup 用 bundle 的 moduleworkers-types 把 `Module` 標 abstract 正反映此限制)。registry 收到的是 runtime 投稿 bytes → 跑不了。
- DECISIONS §8:第一期**不依賴 GitHub Actions** → 也不能靠 CI 跑 Gherkin。
- 剩下唯一一致 venue = **投稿者本地機器**(有 tinygo + 能跑 wasm,與現有 build 流程同環境)。
### 4.1 正確設計:Gherkin 在投稿指令本地跑
零件投稿走一個**獨立 CLI 指令**(既有指令;「本地或公共都是投稿」):
1. 本地 `tinygo build`(或讀已 build 的 .wasm)。
2. **本地跑 Gherkin**:對每個 `gherkin_tests[]`,用 Node 的 WebAssembly + 同一份 wasi-shim instantiate wasmgiven→stdin→run→比對 then_contains。Node 環境能 runtime 編譯 wasm(不像 CF Workers)。
3. 任一 scenario 失敗 → 投稿指令本地就擋下,不送出。
4. 通過 → 把**測試結果隨投稿上傳**(見 4.2)。
`runGherkin.ts`(已寫,用 createWasiShim)邏輯正確,只是**執行 venue 從 registry Worker 改成 CLINode**。registry 端不再跑 Gherkin。
### 4.2 「平台看得到測試結果」(呼應 §3c:執行者不能驗證自己)
投稿 payload 帶 `gherkin_evidence`:每個 scenario 的 `{scenario, given, actual_stdout, passed}`
- registry 存進 KV metadata(軌跡可審)。
- 平台看得到**原始 stdout**,不是只看投稿者宣稱的「passed」。
- **誠實限制**(同人類閘門):本地跑 + 自報結果,AI 技術上能偽造 actual_stdout。靠軌跡可審 + mindset 明示 + 未來 §3c 的 test/relay(投稿走 relay 讓平台當下親跑,第一期後)補強。第一期是「本地跑 + 上傳證據 + 可審」,不聲稱不可繞過。
### 4.3 公私庫分流(投稿指令旗標)
- 預設投稿 → **私人庫**self-hosted 自己的 registry)。
- `-p` / `--public` → 推**公共庫**。
- 兩者都跑同一套把關(§0.0:跨公私庫同一套)。差別只在目標 registry。
### 4.4 registry 端對應
- registry submit 仍跑 G1(假零件)、G3(純WASI)——這兩個是**靜態掃描,不需執行 wasm,CF Worker 可跑**。
- G4 Gherkin 的執行移到 CLIregistry 收 `gherkin_evidence` 存證、可選做輕量一致性檢查(evidence 的 scenario 數與 contract.gherkin_tests 對得上、每個 passed=true),但**不重跑**(跑不了)。
- G5/G6cold_start/runtime_compat)維持 unimplemented_steps 標記。
## 4.5 信任模型與發佈風險(richblack 2026-05-29 定,重要——勿誤讀為零風險)
**Gherkin 全綠 ≠ 零件安全可發佈。** 把關能驗到什麼、不能驗到什麼,要說清楚:
### 把關各層各防什麼
| 層 | 防得了 | 防不了 |
|---|---|---|
| G4 Gherkin | 「零件對宣稱的 given 不 work」 | 「Gherkin 寫得避重就輕、只測會過的 case」——投稿者可寫不完整的測試,沒測到的 input 仍可能出錯/惡意 |
| G1 假零件 / G3 純WASI | 連外 / 非 WASI / 寫死 endpoint | 沙箱內的邏輯錯誤 |
| **純 WASI 沙箱(真正的發佈底氣)** | **框死零件能做的事**:只能 stdin→stdout,無網路 syscall、無檔案系統、無 host 能力(除白名單 host func)→ 就算邏輯有問題或惡意,傷害被沙箱框死 | 沙箱允許範圍內的錯誤輸出 |
| 市場機制(§1) | 長尾:別人用了發現不對 → 回報 → 修 | 投稿當下測不到的 |
**結論:發佈非零風險,是風險可控。** 主要安全保證是**純 WASI 沙箱框死能力**(不是 Gherkin)。Gherkin 驗行為契約、沙箱框死破壞力、市場補長尾,三者合起來讓「發佈陌生人的零件」風險可控到能接受。
### 信任層級(誰跑 Gherkin 決定可信度,§3c:執行者不能驗證自己)
- **第一期**:投稿者**本地跑 + evidence 上傳可審**。AI 技術上能偽造 actual_stdout(它控制本地環境)。**誠實標明:第一期 evidence 可造假**,靠純 WASI 沙箱 + 軌跡可審 + mindset 撐著。
- **未來**:平台**自己重編源碼 + 重跑 Gherkin**(投稿者無法造假「跑的結果」)。需要能 runtime 跑 wasm 的 venue——**非 CF Worker(不能 runtime 編譯)、非 CI(§8 不依賴)**——可能是公共庫專屬的 sandbox 服務。列未來,可能擋公共庫「完全可信發佈」。
- 再往後:§3c 的 test/relay(投稿走 relay 讓平台當下親跑)。
## 5. G5/G6 mock 標未實作(R3 誠實)
- cold_start / runtime_compat 保留 mock,但 **SandboxResult 增 `unimplemented_steps: string[]`**,回傳時明列 `["cold_start","runtime_compat"]`,submit 回應與文件明示「這兩步未實作、未真正驗證」。不回 `success:true` 假裝全綠——回 success 但附 unimplemented 清單。
## 5.5 黃金向量:人工核對 + B 覆蓋檢查(richblack 2026-05-30 定)
Claude.ai 建議用「黃金向量 + 把關自己重跑」自動驗收,防放水的 Gherkin。**價值保留,實作降級**,理由:
- **primitive 極少、未來絕大部分是 recipe**(人類閘門 + 工作流優先把零件擋在源頭)。現役 17 白名單 + cron/platform_crypto,未來極少新增。
- 「把關自動重跑向量」是為「零件大量增加」做的規模化基建——但零件不會大量增加 → 為不存在的規模做自動化 = 過度工程(DECISIONS 附錄「會不會累積成債」判準)。
- 且「把關自己重跑 wasm」撞 venue 牆(CF 不能 runtime 編譯 wasm,同 §4.0)→ 需平台端 sandbox(第一期沒有)。
**降級後做法:**
- **A 黃金向量 conformance → 人工核對**:黃金向量當「人類閘門時用 CC 核對 primitive 的對照表」。新增 primitive(極稀有)時,人類閘門已要你親自確認,那一刻用 CC 本地跑向量核對(本地有 tinygo + 能跑 wasm,繞開 venue 牆)、人工凍結。**不做機器自動重跑。**
- **B 覆蓋檢查 hook → 現在做(純靜態、不可造假、成本低、價值與零件數無關)**:靜態 parse contract`input_schema` 每個 required 欄位至少出現在一個 Gherkin given、`output_schema` 每個欄位至少被一個 then_contains 斷言。缺 → exit 2 指出漏哪個欄位。擋「只測 happy path、不碰宣告過的行為面」。
- **初始向量來源(信任根:寫向量≠寫實作)**:另起 session 從 contract 語義寫、不看實作原碼(primitive 語義客觀如 add(2,2)=4)。人工核對用,不急、新增 primitive 時逐個補,不必 21 個一次到位。
- 殘留(誠實):向量/覆蓋檢查擋不住「列出的 case 全對、沒列到的 case 錯」。交給「用」——出 bug 補進向量,永不 regress。是會長大的網,非設一次完美。
## 6. R5 白名單 + 本機 hook
- `registry/MVP_COMPONENTS.txt`:一行一個白名單 canonical_id(現役 22 個)。
- `pre-write-guard.sh` 增規則:寫入 `registry/components/{name}/...``{name}` 不在 MVP_COMPONENTS.txt → exit 2,訊息「新增零件需走 submit API 人類閘門,不可直接造 repo 目錄」。
- `pre-bash-guard.sh` 增規則:`mkdir .../registry/components/{白名單外}` → exit 2。
- `.ts` 偵測現有 hook 已做(rule 1.1)。
- **B 覆蓋檢查**5.5):可放 registry submit 的靜態驗收(不需跑 wasmCF 可跑)或 pre-write hook,擋宣告過的欄位沒被 Gherkin 測到。
## 7. 範圍邊界
- **動 registry TS**sandboxAcceptance / submitComponent / routes / types+ **CLI**acr parts publish,既有指令)+ **hook**
- 不動 cypher-executor 執行路徑、不動既有零件 wasm。
- backfill 路徑(skip_acceptance)保持可用,不被新閘門擋(存量零件不需人類閘門)。
- CLI/MCP/Python/JS 四路:本期至少做 CLI `acr parts publish` + registry endpoint 強制;MCP/Python/JS 補 `human_confirmation` 欄位支援(薄)。
## 8. 驗收標準
- 投一個寫死 endpoint 的假零件 → G1 退稿(終端輸出)。
- 投一個 `.ts` 進 registry/components → hook exit 2。
- 投一個白名單外的新零件目錄(本機造)→ hook exit 2。
- 無 human_confirmation 的 submit → 403。
- 帶 human_confirmation + 過 Gherkin 的真零件 → 通過、寫 KV、reason 留 metadata。
- Gherkin given/then 對真零件跑綠;故意改壞 then_contains → 退稿。
- cold_start/runtime_compat 在回應裡列入 unimplemented_steps(不假綠)。
## 9. 決議(richblack 2026-05-29 design review 定)
- **Q1 → 消解**Gherkin 測的零件**永遠是封閉邏輯(框架),不連外**。任何要加外部 URL 的東西按定義就是 recipe,不是零件——這種「連外零件」根本不該存在(會被 G1 假零件偵測擋下、降成 recipe)。所以 G4 Gherkin 只跑不需 host function 的封閉邏輯零件,**不需要 mock host func、不需要 skip 機制**。零件用 `u6u.http_request` 連外 = G1 直接退稿。
- **Q2 → 兩者都硬擋**(a) contract/原碼有具體外部 URL/domain → 硬退稿;(b) 宣告能力是 http_request 子集 → 也硬退稿。理由:與 Q1 一致——零件不該連外,這兩個 pattern 都是「該是 recipe 的東西偽裝成零件」,硬擋無誤殺顧慮(真的要連外就去做 recipe)。
- **Q3 → submit 過閘門後自動 append**:人類閘門通過 + 驗收綠的零件,submit 成功時自動把 canonical_id append 進 `MVP_COMPONENTS.txt`。白名單反映「已正當投稿的零件」,不需手動維護。本機 hook 讀此檔擋「白名單外的直接造目錄」。
### Q1 連帶結論(強化 G1
既然「零件不連外、連外即 recipe」是硬規則,G1 假零件偵測 = G4 Gherkin 的前置守門:
G1 擋掉所有連外/http 子集的投稿 → 能進到 G4 的必然是封閉邏輯零件 → Gherkin 必然不需 host func。
兩道閘門邏輯自洽。
@@ -1,105 +0,0 @@
# Design 補充:recipe 入庫把關(push 那一刻)
> 2026-06-01。本檔是 `component-gatekeeping/design.md` 的單檔補充(規則 02 §4.3 允許)。
> **狀態:待 richblack review 才動 code(這是 change)。**
> 背景:richblack 2026-06-01 方向修正(見下「方向定調」)+ docs/HANDOFF-self-host-harness.md。
---
## 0. 方向定調(richblack 2026-06-01
把關的對象與位置整個移位了,先講清楚才不會做歪:
### 0.1 零件這條路 = 封鎖,且「不再有假零件這件事」
- 零件由維護者(richblack)管理,**CC 不能自製/修改零件**。
- 封鎖機制 = **零件投稿走 GitHub PR + 人 merge**DECISIONS §8 / design 頂部方向修正)。
AI 偽造不了 GitHub approve,這是天然人類閘門。CC 在本機產不出能進庫的零件。
- → 因此「擋假零件」(原 W1)這件事**不存在了**:CC 根本造不出零件,workflow 引用 recipe
(如 `component: kbdb_get`)是**合法且未來唯一的擴充方式**,不該被當假零件擋。
### 0.2 零件 PR 把關 = 人工,不自動化(除非未來爆量)
- richblack 2026-06-01 澄清 BACKLOG 步驟5「不做 hook/自動化」的真意:
**零件真實數量很少、絕大多數是 recipe** → 原本想做的「驗證零件 PR 的自動化機制」
CI 跑 Gherkin/沙箱/向量)**不需要**,量少 → **有 PR 進來就人工檢查**
**只有零件開發量變很大時**才回頭想自動化。
- → component-gatekeeping 的 G4/覆蓋檢查/黃金向量自動化 = **不做**(人工取代),與既有 tasks 收尾一致。
### 0.3 真正的 harness 把關 = recipe 入庫(push)那一刻
CC 唯一能擴充的是 recipe。recipe 一律用「**推(push)**」,**自有庫與公共庫同一套指令**。
把關依庫別分強度:
| 庫別 | 能做到的把關 | 機制 |
|---|---|---|
| **自有庫(self-hosted** | 只能**提醒**(無法在別人機器強制) | (1) 資料外流提醒 (2) 打通檢查 |
| **公共庫** | 維護者機制**檢核實際打通、真收到成功回傳** | PR/CI relayDECISIONS §3c,第一期後)|
---
## 1. 自有庫 push 把關(self-hosted,第一期做)
`acr recipe push` 的兩個提醒。**提醒級 = 告知 + 需人類明示同意,不硬擋**(self-hosted 是用戶自己的庫,
他同意後就是他的責任 — mindset §6 / data-exfil-warning 既有原則)。
### 1.1 資料外流提醒(W2.2
- **觸發**push 的 recipe / 或部署的 workflow 會讓「資料對外可見」——主要是產生**對外可被呼叫的 webhook**
`POST /webhooks/named/...` 對外 trigger URL),或 recipe 把本地資料 POST 到外部服務。
- **行為**:CLI 印明確警示「這個動作會讓 X 對外界可見/可呼叫,確認要繼續嗎?」→ 需人類明示同意(y/N)。
非 TTY(AI 直跑)→ 拒絕,提示「需人類確認」(mindset §7:絕不代替人類做暴露確認)。
- **與既有 data-exfil-warning 的關係**:已有 API 層 + pre-bash hookcommit 51d40ee 等)。
本項確認**涵蓋 recipe push 這條路徑**;若已涵蓋則只補文件,若沒涵蓋則補上 push 路徑的提醒。
- **誠實限制**AI 技術上能偽造 exposure_consent。價值是法律歸責 + 軌跡可審,不聲稱不可繞過(mindset §7)。
### 1.2 打通檢查(W2.3
- **目的**recipe 是「指向外部 API 的指針」,正確性一半在「打不打得通」(DECISIONS §1 recipe 驗收標準 = 2xx)。
- **行為**push 時(或 push 後)對 recipe 的 endpoint **實打一次**,回報 HTTP status。
- 2xx → 「✓ recipe 打通(HTTP 200)」
- 4xx/5xx → 「⚠️ recipe 未打通(HTTP 401/404/...)」+ 誠實標原因(如「缺 credential → 先 acr creds push」)
- 連不上 → 「⚠️ 無法連線」
- **self-hosted 是提醒級**:打不通**不硬擋 push**(用戶可能就是要先 push 再設 credential),只如實回報。
- **誠實**mindset §7):缺 credential 打不到 2xx 就誠實標「未驗收:缺 X」,不 mock 充綠燈。
### 1.3 動到的檔案(待 review 後)
| 檔案 | 動作 |
|---|---|
| `cli/src/commands/recipe.ts` | push 流程加 (1) 資料外流提醒 prompt (2) 打通檢查(實打 endpoint 回報 status |
| `.claude/hooks/*`(如需)| 確認 data-exfil pre-bash hook 涵蓋 recipe push;缺則補 |
**不動**cypher-executor 執行路徑、零件、credential 解密邏輯。
---
## 2. 公共庫 push 把關(第一期後)
- recipe 進公共庫 = 別人會用 → 需維護者機制檢核「**實際打通、真收到成功回傳**」(不是投稿者自報)。
- 機制:DECISIONS §3c 的 **test/relay**——push 公共庫走 relay,維護者當下親見真實打通記錄
(執行者不能驗證自己,§7 閉環)。
- **範圍**:依賴公共庫 + relay 基建,**第一期不做**(第一期是 self-hosted + 提醒級)。
- 本檔只記框架,第一期不實作。
---
## 3. 同一套指令、不同把關強度的切分(W2.4)
- `acr recipe push`(自有庫,預設)→ §1 提醒級。
- `acr recipe push --public`(公共庫,未來)→ §2 relay 檢核級。
- 同一指令、旗標分流(呼應 design §4.3 公私庫分流:`-p`/`--public`)。
- 第一期只實作預設(自有庫)路徑。
---
## 4. 驗收標準(客觀證據,mindset §7)
第一期(自有庫提醒級):
1. `acr recipe push` 一個會產對外 webhook 的東西 → CLI 印資料外流警示 + 要人類同意;非 TTY → 拒絕。
2. `acr recipe push` 一個 endpoint 可達的 recipe → 打通檢查回報「✓ HTTP 2xx」。
3. `acr recipe push` 一個缺 credential 的 recipe → 回報「⚠️ 未打通:缺 credential」(誠實,不假綠),但仍允許 push。
4. 確認 workflow 引用 recipe`component: kbdb_get`**不再被任何 validate 步驟當假零件擋**(W1 已作廢)。
---
## 5. 與既有 SDD 的一致性確認(無新矛盾)
- 不動「零件投稿走 PR + 人工檢查」(§0.1/0.2,與 design 頂部方向修正、DECISIONS §8 一致)。
- 不重啟「零件 PR 自動化把關」(§0.2,與 BACKLOG 步驟5 真意一致)。
- 資料外流提醒延續既有 data-exfil-warning 原則(mindset §6),只確認涵蓋 recipe push 路徑。
- 打通檢查 = recipe 驗收標準 2xx 的落地(DECISIONS §1)。
@@ -1,53 +0,0 @@
# Requirements: Component Gatekeeping(零件投稿真把關)
> 2026-05-29 建立(richblack 確認)。對應第一期 BACKLOG 步驟 4(補零件庫真把關)+ 步驟 5(人類閘門 + 白名單 hook)。
> 判準源:DECISIONS.md §1(工作流是 default / 建零件人類閘門 / ABC 三管齊下)、§7(讓 AI 不做歪三層機制 + 閉環)、§3c/§7(禁假綠)。
---
## 背景
第一期要把「零件投稿」從「無審核」變成「真的會擋」的把關。現況:
- `registry/src/actions/sandboxAcceptance.ts`size_check / syscall_scan 已實作;cold_start / gherkin_tests / runtime_compat 是 `return null` mock。
- `submitComponent.ts`:跑 sandboxAcceptance → 派 hash → 寫 KV。無假零件偵測、無人類閘門。
- 風險(DECISIONS):CC 把自用服務(通訊錄/帳本)做成零件進公共庫 → 全生態能打 → 機密外洩。
## 需求
### R1 — Gherkin 驗收真實作(取代 mock
- `runGherkinTests` 要真的跑零件 WASM:對每個 `gherkin_tests[]` 的 given → 餵 stdin → 比對 stdout 是否 `then_contains`
- 失敗 → 退稿,回明確 reason + guide anchor。
- recipe 不適用 GherkinDECISIONS §1recipe 驗收 = 2xx);本需求只針對 WASM 零件投稿。
### R2 — 假零件偵測(退稿並指回正路)
依 DECISIONS §1 判準,投稿零件若滿足任一,退稿:
- (a) contract 或(可得的)原碼出現具體外部服務 URL / domain
- (b) 宣告能力是 http_request 子集(打某固定 endpoint
退稿訊息要指回正路:「這該是 recipe / 工作流,不是零件」。
### R3 — 純 WASI preview1 / 零宿主依賴把關
- 擴充 syscall 掃描:確認零件只依賴 WASI preview1 + u6u host functions 白名單,無其他宿主特定依賴(避免 runtime 鎖定債,DECISIONS §4)。
- 冷啟動 / runtime 相容兩步可暫留 mock,但**明確標「未實作」**,不可假裝通過(§3c/§7 禁假綠)。
### R4 — 建零件的人類閘門(核心,BACKLOG 步驟 5 強化規格)
- **把關點 = 建立零件的 API 本身**registry submit endpoint),不是寫檔案。CLI / MCP / Python lib / JS lib 四路全收斂到這關。
- submit 預設**拒絕**,除非請求帶:
- (a) 人類已確認憑證(CLI `acr component create` 強制互動式問人類,AI 非互動環境生不出)
- (b) 舉證 `reason_why_not_workflow`(AI 須證明工作流為何做不到;舉證責任在 AI,預設假設工作流能做)
- 缺 → 403 + 指回正路(先試工作流)。
- 誠實限制:沒技術能 100% 防 AI 假冒人類確認;靠閘門 + 舉證留記錄 + mindset 明示「絕不代替人類確認」+ 軌跡可審計,讓假冒成明確越界。
### R5 — 白名單 + 本機 hook 兜底
- `MVP_COMPONENTS.txt` 白名單檔。
- pre-write / pre-bash hookCC 直接在 `registry/components/{白名單外新名}/` 造目錄或 mkdir → exit 2(擋繞過 API 直接改 repo 等 CI 部署的旁門)。
- pre-write hook`registry/components/` 下出現 `.ts`(非 AssemblyScript)→ exit 2(現有 hook 已部分做)。
### R6 — 不限制自由
- 不做「audience 宣告 / 禁止自用進公共庫」式的限制(DECISIONS:別人要建零件是他的自由)。
- 唯一硬約束:零件 = 只打一個 endpoint 的薄殼。閘門是「要建得先說服人 + 舉證」的摩擦,非禁止。
- arcrun 不做授權判斷(能否打通由發 key 的服務裁決)——把關針對「是否該是零件」,不針對「誰能打哪個 API」。
## 非目標
- Phase 5 用戶自製零件 R2 上傳(未啟用)。
- registry KV schema 大改(用既有結構)。
- 完整的 audit trail 系統(DECISIONS §7 事後機制第一層,另議)。
@@ -1,102 +0,0 @@
# Tasks: Component Gatekeeping
> 對應 design.md。每完成一個 task 立刻標 [x],不批次。
> Design 已 richblack 確認(2026-05-29,含 Q1-Q3 決議)。
---
## ⚠️ 收尾狀態(2026-05-30,方向修正後)
投稿改走 **GitHub PR**(廢 registry self-service,見 design 頂部「方向修正」)。本 SDD 收尾於:
- **已完成且 commit**G1detectFakeComponent)、G3wasmImports)、G5/G6unimplemented_steps)、
G0 registry 人類閘門(保留不刪)。測試 15 綠。
- **改去向**G4 Gherkin / 覆蓋檢查 → 未來接 CI PR checkCI 能跑 wasm)。G1/G3 邏輯可複用。
- **不做**CI PR checkrichblack:人工 review 就夠,primitive 極少)、R5 本機 hook
PR/merge + G1 + 沙箱已防「未經同意變公共零件」,hook 過度工程)、registry self-service、
acr parts publish 加閘門、平台 sandbox 重跑。
- **黃金向量**:人工核對(另起 session 從語義寫),不急、不機器自動化。
- **轉出範圍**:真正的裸奔風險在「資料外流」(recipe/webhook 把資料送出去),不分公私庫
→ 另開新 SDD「資料外流警示」(API 層警示 + AI 動手前 hook)。用戶 API 保護(入站認證)
+ recipe/part/function 架構釐清 → 記 BACKLOG 待決策。
---
## G1 假零件偵測(R2,Q2=兩者硬擋)
- [x] 1.1 `registry/src/actions/detectFakeComponent.ts`(a) 外部 URL/domain 偵測(掃 contract 文字 + wasm binary 文字)硬擋;(b) http_request 子集偵測硬擋
- [x] 1.2 排除 `auth_*` primitive 與 `http_request` 自己
- [x] 1.3 接進 sandboxAcceptance 步驟鏈(fake_component_scan 為第一步)
- [x] 1.4 退稿訊息指回正路(「這該是 recipe/工作流」)
## G3 純 WASI 把關(R3
- [x] 3.1 `wasmImports.ts` 解析 wasm import section,取出所有 import module name(已對真實零件驗證)
- [x] 3.2 白名單:只准 `wasi_snapshot_preview1` + `u6u`;其他 module → 退稿
- [x] 3.3 接進 scanSyscalls(白名單為主,黑名單為次)
## G4 Gherkin 真實作(R1)— venue 修訂:CLI 本地跑,非 registrydesign §4 修訂)
> CF Worker 不能 runtime 編譯 wasm + §8 不依賴 CI → Gherkin 在投稿指令本地(Node)跑。
> 在第一段(tinygo build→wasm 之後)測,跟 worker 無關。registry 只存 evidence 不重跑。
- [x] 4.1 `runGherkin.ts`createWasiShim 跑 wasm,邏輯正確)—— 但 venue 要從 registry 改 CLI
- [ ] 4.2 回退 sandboxAcceptanceregistry 不跑 Gherkin(移除 await runGherkin),改回靜態步驟
- [ ] 4.3 Gherkin 邏輯搬到 CLI 投稿指令(Node 環境 instantiate wasm
- [ ] 4.4 投稿 payload 帶 gherkin_evidencescenario/given/actual_stdout/passed),registry 存 metadata 可審
- [ ] 4.5 誠實標明第一期 evidence 可造假(mindset + 文件);平台重跑列未來
- [ ] 4.6 投稿指令(暫名 acr component submit+ 公私庫分流(-p 公共)— 新 CLI 工程
## G5/G6 誠實標未實作(R3 禁假綠)
- [x] 5.1 SandboxResult 增 `unimplemented_steps: string[]`
- [x] 5.2 cold_start / runtime_compat 列入 unimplemented_stepssubmit 回應明示
## G0 人類閘門(R4,核心)
- [x] 0.1 submit 請求增 `human_confirmation`SubmitOptions in submitComponent.ts+ route 解析(multipart/JSON 皆支援)
- [x] 0.2 submit 邏輯:非 skip_acceptance 的新投稿,無 human_confirmation/空 reason → gateError(指回正路)
- [x] 0.3 human_confirmation + gherkin_evidence 寫進 KV metadata(軌跡可審)
- [ ] 0.4 CLI `acr parts publish`(既有指令):互動式問人類(工作流為何做不到 + 確認),非 TTY 拒絕
- [ ] 0.5 MCP / Python lib / JS lib 補 human_confirmation 欄位支援(薄)
- [ ] 0.6 誠實限制寫進 mindset Skill(步驟 7+ SDK 文件
> 命名修正(2026-05-29):投稿走**既有** `acr parts publish`cli/src/commands/parts.ts),
> 非另建 acr component create(符合「修改現有不重建」)。G0-CLI(0.4)與 G4-CLI 合併在此指令做。
## R5 白名單 + 本機 hook
- [ ] 5.3 `registry/MVP_COMPONENTS.txt`(現役 22 個 canonical_id
- [ ] 5.4 submit 過閘門成功 → 自動 append canonical_id 進白名單(Q3
- [ ] 5.5 pre-write-guard.sh:寫 `registry/components/{白名單外}/` → exit 2
- [ ] 5.6 pre-bash-guard.shmkdir `registry/components/{白名單外}` → exit 2
## W1 ~~CLI workflow validate 擋假零件式 component 名~~2026-06-01 作廢,方向修正)
> **作廢原因(richblack 2026-06-01**:「擋假零件」這件事不再存在——因為**自製/修改零件的路
> 已被封鎖**(CC 根本造不出零件),workflow 引用 recipe(如 component: kbdb_get)是**合法且
> 未來唯一的擴充方式**,不該被當「假零件」擋。把關點從「workflow validate」**移到 recipe 入庫
> push)那一刻**。已動的 yaml-parser.ts `LEGAL_PRIMITIVES`/`findSuspectComponents` 已回退。
> 取而代之 → 見 W2。
## W2 封鎖自製零件 + recipe 入庫把關(2026-06-01 新方向)
> richblack 2026-06-01 定調:
> - 零件由維護者管理,**CC 不能自製/修改零件**(hook + CLI 拒絕)→ 不再有「假零件」。
> - CC 唯一能擴充的是 **recipe**。recipe 一律用「推(push)」,**自有庫與公共庫同一套指令**。
> - 把關依庫別分強度:
> - **自有庫(self-hosted**:只能**提醒**(無法在別人機器強制)。兩個提醒:
> (1) 資料外流提醒——某動作會讓外界看到你的東西(如 workflow 產對外 webhook),同意後是他的責任;
> (2) 打通檢查——查他要打的 API 是否打得通(2xx)。
> - **公共庫**:由維護者機制檢核「實際打通、真收到成功回傳」(PR/CI relayDECISIONS §3c,第一期後)。
> 屬 change,需先寫 design 給 richblack review 才動 code。本節先記框架。
- [x] W2.1 封鎖自製零件 — **釐清完成(richblack 2026-06-02**:靠「零件投稿走 GitHub PR + 人 merge」
天然閘門(DECISIONS §8)。BACKLOG 步驟5「不做 hook」真意 = 零件少、不為零件 PR 蓋自動化把關
(量少人工檢查;爆量才回頭想),**不是**不阻止自製。無矛盾,不需新做 hook。
- [x] W2.2 `acr recipe push` 資料外流提醒 — **既有實作已涵蓋**recipe.ts:70-79 `obtainExposureConsent`
exposure-warning.ts:互動打資源名確認、非 TTY 拒絕、首次問記住)。data-exfil-warning SDD 已做,確認涵蓋 recipe push 路徑。
- [x] W2.3 `acr recipe push` 打通檢查 — **新增** `probeRecipeEndpoint`recipe.ts):push 成功後實打 endpoint
回報 2xx/⚠。提醒級不硬擋;endpoint 含 {{模板}} → 誠實說明待 run 才知;401/403 → 標「多半缺 credential,非 recipe bug」(不假綠,mindset §7
- [ ] W2.4 公共庫 push--public= 維護者 relay 檢核(DECISIONS §3c)— 第一期後,本期只做自有庫提醒級
## 驗收(design §8
- [ ] V1 投寫死 endpoint 假零件 → G1 退稿(終端輸出)
- [ ] V2 投 `.ts` 進 registry/components → hook exit 2
- [ ] V3 本機造白名單外零件目錄 → hook exit 2
- [ ] V4 無 human_confirmation 的 submit → 403
- [ ] V5 帶 human_confirmation + 過 Gherkin 真零件 → 通過 + reason 留 metadata + 白名單 append
- [ ] V6 故意改壞 then_contains → Gherkin 退稿
- [ ] V7 回應含 unimplemented_stepscold_start/runtime_compat 不假綠)
@@ -1,162 +0,0 @@
# SDD: arcrun Component Registry 正典化(Component Registry Canon
> 2026-05-07 建立。狗糧發現的根本問題:registry 活著但 index 空的,AI 找不到零件就會繞回 Python。
> 範圍:**讓 registry 成為零件 metadata 的 SSOT**,含 u6u → arcrun rebrand。
---
## 1. 問題
### 1.1 表象
- `registry.arcrun.dev/components/search?q=*` 永遠回 0 結果
- MCP `u6u_search_components` 找不到任何零件
- `acr parts list` 同樣空
### 1.2 根因
`matrix/arcrun/registry/components/` 下 30+ 個零件已經部署成獨立 Workerkbdb_ingest, claude_api, kbdb_create_block, kbdb_patch_block, http_request, string_ops, ⋯),但**它們的 contract.yaml 沒有透過 `POST /components/submit` 進 registry index**。
部署路徑:
```
registry/components/{name}/main.go ← TinyGo 寫的零件
↓ tinygo build
.component-builds/{name}/component.wasm
↓ wrangler deploy
{name}.arcrun.dev (Worker) ← 零件可被 HTTP 呼叫了
registry index? ← 這步從來沒做
```
### 1.3 影響(吃狗糧的觀察)
- 新 AIClaude / Gemini / Codex)進來不知道有什麼零件 → 自己寫 Python 直打 API
- arcrun 想推「AI-first 自服務」整個破功
- 文件寫得再好都救不了 — 因為 README 只能寫概念,零件清單必須是 API 動態查
---
## 2. 目標
**Registry 是零件 metadata 的 SSOT**
- 零件 Worker 在跑 ⇔ registry 有對應 entry(雙向綁定)
- AI 透過 MCP `search_components` 永遠找得到所有現役零件
- README 不寫死數量,動態 badge 即時反映
- 第三方裝完 MCP 30 秒內能找到第一個可用零件
---
## 3. 三層設計
### Layer 1: 一次性 backfillPhase 1
`matrix/arcrun/registry/components/*/component.contract.yaml`,把每個 contract POST 進 registry index。
工具:`matrix/arcrun/registry/scripts/backfill-index.ts`
- 讀檔 → 解析 YAML → 呼叫 registry submit endpoint
- idempotent:已存在不重複寫(registry 端要支援 upsert
- 跳過沙盒驗收(這些零件已驗過、已部署,不用重跑 gherkin tests
### Layer 2: 部署即註冊(Phase 2
`.github/workflows/deploy.yml`
- 通用掃描掃到 `.component-builds/{name}/wrangler.toml` 部署成功後
- post-deploy step 自動呼叫 registry submitcontract 從 `registry/components/{name}/component.contract.yaml` 讀)
零件 Worker 部署 ⇒ registry 自動更新。沒有「零件部署了但 registry 不知道」的可能。
### Layer 3: DiscoverabilityPhase 3
- README 移除「21 個零件」這種寫死數字,改「跑 search 看當前清單」
- 加 badge endpoint `registry.arcrun.dev/badge/components.svg` 即時顯示數量
- MCP `get_component_guide` 開頭加鐵律:「動工前必須先 search,不是猜」
- onboarding kit GitHub templateCLAUDE.md / .cursor/rules / AGENTS.md 三件套,all 強制 search 優先
### Layer 4: u6u → arcrun RebrandPhase 4
`matrix/u6u-mcp/` 跟所有 `u6u_*` tool 名搬到 arcrun 命名空間。
理由:
- u6u 是申請 arcrun.dev 之前的暫名,現在已過時
- 命名混亂阻礙推廣(「為什麼 arcrun 文件叫 u6u_*?」)
- 第三方看到 u6u 不知道是同一個產品
範圍:
1. 目錄:`matrix/u6u-mcp/``matrix/arcrun-mcp/`
2. Worker name`u6u-mcp``arcrun-mcp`
3. Tool 前綴:`u6u_search_components``arcrun_search_components`14 個 tool
4. Hostname`mcp.finally.click``mcp.arcrun.dev`finally.click 保留 redirect 到 arcrun.dev 過渡期)
5. Repo / Worker 內部 IDu6u-mcp-server → arcrun-mcp-server
6. README 全文:u6u → arcrun
7. user memoryCLAUDE.md / MEMORY.md)相關提及一併更新
8. inkstone-component-registry(舊 worker)廢止 → arcrun-registry 為唯一現役
**Rebrand 原則:**
- 用戶端 configclaude_desktop_config.json 等)給過渡期:兩個 URL 都活,舊的回 deprecation header 提示換新
- Tool 前綴 `u6u_*``arcrun_*` 沒有過渡期(一刀切,因為前綴是 AI 看的,不是用戶記憶肌肉)
- 文件 / repo 內所有 reference 立即改
---
## 4. 範圍邊界
**在本 SDD 範圍內:**
- ✅ Phase 1: backfill index
- ✅ Phase 2: 部署即註冊 hook
- ✅ Phase 3: README + badge + onboarding kit
- ✅ Phase 4: u6u → arcrun rebrand(含目錄 / worker / hostname / tool 前綴 / 文件)
**不在範圍內:**
- 新零件開發(這是 polaris 業務範圍)
- registry KV schema 改動(用既有結構)
- u6u-gui 的 rebrandu6u-mcp 同 monorepo 但獨立 SDD
- Phase 5(用戶自製零件 R2 上傳)— 等 Phase 4 完成後另開 SDD
**前置依賴(已完成):**
- ✅ u6u-mcp Zod 4 → Zod 3 修復(2026-05-07
- ✅ u6u-mcp service binding 改指 arcrun-registry2026-05-07
- ✅ arcrun-registry Worker 部署在 registry.arcrun.dev
---
## 5. 驗收標準
### Phase 1 驗收
- `u6u_search_components("kbdb")` 回非空結果,含 `kbdb_ingest` / `kbdb_create_block` / `kbdb_patch_block`
- `acr parts list` CLI 端對端能列出
- registry KV 內至少 30 entries
### Phase 2 驗收
- 部署任一既有零件後,registry 30 秒內 reflect 更新
- 部署一個全新零件,無需手動 publish,registry 自動有
- CI workflow 不會因 registry 寫入失敗就擋部署(degraded mode:寫入失敗 log warning 但不 fail
### Phase 3 驗收
- README 沒有「21 個零件」「30 個零件」這種寫死數字
- badge SVG 渲染正確、數字跟 KV 一致
- onboarding kit clone 下來,照 README 跑能 30 秒內 list 到零件
### Phase 4 驗收
- `mcp.arcrun.dev/mcp/mcp` 通,回的 tool 名都是 `arcrun_*`
-`mcp.finally.click/mcp/mcp` 仍可用但回 deprecation header
- README / docs / GUIDE 全部 u6u 字樣消除
- `matrix/u6u-mcp/` 目錄不存在,改為 `matrix/arcrun-mcp/`
- 用戶記憶(`~/.claude/.../MEMORY.md`arcrun MCP 設定範例已更新
---
## 6. 風險與緩解
| 風險 | 緩解 |
|---|---|
| backfill 把 contract 灌進去後,沙盒驗收覆蓋既有資料 | registry submit 加 `skip_acceptance=true` flag,僅 backfill 用 |
| 部署 hook 寫入失敗擋掉部署 | hook degraded mode:失敗只 warning,不 fail 部署 |
| Rebrand 把現役 client 弄壞 | 過渡期:舊 hostname 跟 worker 並存 1 個月 |
| Tool 前綴改名 AI 適應期 | 不過渡,一刀切(前綴是 system instruction 範圍,AI 一個 prompt 就學會)|
| 既有用戶 config 寫死 finally.click | 提前公告 + 過渡期 + 舊 endpoint 自動 redirect / proxy |
---
## 7. 變更紀錄
| 版本 | 日期 | 內容 |
|---|---|---|
| v1.0 | 2026-05-07 | 初版。吃狗糧發現 registry 空的,三層設計(backfill / auto-register / discoverability+ u6u → arcrun rebrand 一併納入。 |
@@ -1,159 +0,0 @@
# Tasks — Component Registry Canon
> 對應 SDD[design.md](design.md)
> 上次更新:2026-05-07
**狀態 legend**`[ ]` 待辦 / `[🔄]` 進行中 / `[x]` 完成
---
## Phase 0:前置(已完成)
- [x] 0.1 u6u-mcp Zod 4 → Zod 3 降版修 tools/list `_zod undefined` bug2026-05-07
- [x] 0.2 u6u-mcp service binding `inkstone-component-registry``arcrun-registry`2026-05-07
- [x] 0.3 確認 `mcp.finally.click/mcp/mcp` 端對端通,tools/list 回 14 個 tool2026-05-07
---
## Phase 1Backfill Index(半天,立即見效)
- [x] 1.1 探查 registry 既有 endpoint:發現
- 既有 `POST /components` 強制要 wasm bytesmultipart 或 base64),跑沙盒驗收 + 寫 R2 + 寫 KV
- cypher-executor 已不從 R2 動態載 wasmline 32 標 R2 路徑作廢,零件用獨立 Worker URL)
- 結論:R2 是 legacyregistry 真正用途是 metadata 索引給 AI 搜尋
- 決策:**加新 endpoint `POST /components/index-only`** 接 contract(無 wasm、無沙盒),專供 backfill 跟「已部署但未索引」零件用
- [x] 1.1.1 加 `src/actions/indexOnlyComponent.ts`metadata-only 寫 KV,冪等)
- [x] 1.1.2 加 `src/routes/components.ts``POST /index-only` route
- [x] 1.1.3 部署 + smoke testcontract 驗證 + 錯誤處理通過)
- [x] 1.2 寫 `matrix/arcrun/registry/scripts/backfill-index.mjs`zero-build node script,用 js-yaml
- [x] 1.3 dry-run 確認 30 個 component 全 parse 通
- [x] 1.4 跑真 backfill(過程中發現並修了兩個 schema 問題):
- schema enum `category``auth` / `ai` / `platform`types.ts
- `max_cold_start_ms` 上限放寬 50 → 500auth/ai 含 crypto 需要)
- `no_network_syscall` / `no_filesystem_syscall` 改 optional
- `max_size_kb` 上限放寬 2048 → 8192
- index-only route 對缺 gherkin/description/tags 的零件補 placeholder(不擋索引)
- [x] 1.5 驗證:MCP `u6u_search_components("kbdb")` 回 3 個零件(kbdb_ingest / kbdb_create_block / kbdb_patch_block
- [ ] 1.6 驗證:`acr parts list` CLI 端對端能列
- [x] 1.7 驗證:registry KV 30 entries30 created + 30 idx 共 60 keys
---
## Phase 1.5:砍 R2 dead storage(先於 Phase 2,清架構斷層)
> 2026-05-07 加入。R2 wasm 路徑早已 deadcypher-executor 不從 R2 讀),保留只會誤導 AI。
> SDD design.md 的「Phase 5 用戶自製零件 R2 上傳」一併廢止。
- [x] 1.5.1 改 `submitComponent.ts`:移除 R2 寫入段落,保留 KV 寫入
- [x] 1.5.2 移除 `wrangler.toml``[[r2_buckets]] WASM_BUCKET` binding
- [x] 1.5.3 移除 `types.ts` Bindings 的 `WASM_BUCKET: R2Bucket`
- [x] 1.5.4 既有 `wasm_r2_key` 欄位保留為 deprecatedqueryComponents 仍會讀 legacy record
- [ ] 1.5.5 廢止 `arcrun-wasm` R2 bucket30 天觀察期後 → 2026-06-07 之後 `wrangler r2 bucket delete`
- [x] 1.5.6 部署 + smoke testsearch 端對端通過(kbdb 找到 3 個零件)
## Phase 2:部署即註冊(1-2 天)
- [x] 2.1 選擇方案:CI stepgithub actions)— 在 wrangler deploy 之後 curl `/index-only`
- [x] 2.2 寫 `registry/scripts/register-component.sh`(本地 + CI 共用 SSOTpython3 + pyyaml 解 YAMLcurl POST registry
- [x] 2.3 改 `.github/workflows/deploy.yml` tier1 deploy step 後加 "Register component in registry" stepdegraded mode:失敗只 warning
- [x] 2.4 本地驗 `bash scripts/register-component.sh kbdb_ingest` → 200 + already_indexed
- [ ] 2.5 真正 push 一個新零件驗 CI hook 端對端(需要等下次新增零件時驗)
- [ ] 2.6 文件化:`docs/contributing-components.md`「新增零件的標準流程」
- [ ] 2.7 廢止 `u6u_publish_component` tool 的「需手動 publish」假設(rebrand 一起做)
---
## Phase 3Discoverability(半天)
- [ ] 3.1 改 GitHub `richblack/arcrun` README
- 移除「21 個零件」這種寫死數字
- 加「跑 `acr parts list` 或 MCP search 看當前清單」
- 加 badge`![components](https://registry.arcrun.dev/badge/components.svg)`
- [ ] 3.2 加 `matrix/arcrun/registry/src/routes/badge.ts`
- GET `/badge/components.svg` 回 shields.io 格式 SVG
- count 從 KV 即時 query
- cache 1 分鐘(`Cache-Control: max-age=60`
- [ ] 3.3 改 MCP `u6u_get_component_guide` tool(之後改名 `arcrun_*`
- 開頭加「鐵律:動工前必須先 search_components,找不到才 publish」
- [ ] 3.4 onboarding kit GitHub template repo(建議名 `arcrun-quickstart`
- 三件套:CLAUDE.md / `.cursor/rules/arcrun.mdc` / AGENTS.md
- 強制:「呼叫 Claude/任何 AI 前,先 list MCP toolsarcrun MCP 已連線時,**禁止用 Python 直打 HTTP API**」
- 內附範例 hello workflow 跟 component
- [ ] 3.5 寫 onboarding doc`docs/onboarding-third-party-engineer.md`
- 第三方工程師如何 30 秒內讓 AI 學會用 arcrun
---
## Phase 4u6u → arcrun Rebrand1 天,最後做)
> 規劃做完 Phase 1-3 驗證 OK 才動 rebrand,避免邊改邊驗。
### 4.1 Repo & Worker
- [ ] 4.1.1 `git mv matrix/u6u-mcp matrix/arcrun-mcp`(或 cp + rm,視 git history 偏好)
- [ ] 4.1.2 改 `matrix/arcrun-mcp/wrangler.toml`
- `name = "u6u-mcp"``name = "arcrun-mcp"`
- 加 route `mcp.arcrun.dev/*`,舊 `studio.finally.click/mcp*` 保留 1 個月
- [ ] 4.1.3 改 `package.json``@inkstone/u6u-mcp-worker``@arcrun/mcp-worker`
### 4.2 Tool 前綴改名
- [ ] 4.2.1 14 個 tool 檔案 rename`u6u_*.ts``arcrun_*.ts`
- [ ] 4.2.2 每個 tool 內部 `server.tool("u6u_xxx", ...)``server.tool("arcrun_xxx", ...)`
- [ ] 4.2.3 `src/tools/registry.ts` import 路徑全改
- [ ] 4.2.4 `src/index.ts` `serverInfo.name``u6u-mcp-server``arcrun-mcp-server`
### 4.3 文件
- [ ] 4.3.1 README.md 全文 u6u → arcrun
- [ ] 4.3.2 GUIDE.md 同上
- [ ] 4.3.3 GitHub `richblack/arcrun` README 補 MCP 段落(之前沒提)
- [ ] 4.3.4 任何提到 `u6u-mcp` / `mcp.finally.click` 的 docs 更新
### 4.4 用戶記憶
- [ ] 4.4.1 `~/.claude/projects/.../memory/MEMORY.md` 加 arcrun MCP entry
- URL: `https://mcp.arcrun.dev/mcp/mcp`
- tool 前綴: `arcrun_*`
- finally.click 過渡期到何時
- [ ] 4.4.2 polaris/mira/CLAUDE.md 提到 daemon / arcrun / MCP 的部分對齊新命名
### 4.5 過渡期(舊 endpoint 不立刻砍)
- [ ] 4.5.1 舊 `mcp.finally.click/mcp/mcp` 加回應 header `Deprecation: true` + `Link: <https://mcp.arcrun.dev/mcp/mcp>; rel="successor-version"`
- [ ] 4.5.2 舊 worker 繼續服務 30 天(2026-06-07 為止)
- [ ] 4.5.3 廢止排程:2026-06-07 後舊 worker 改回 410 Gone + 提示換新 URL
### 4.6 驗證
- [ ] 4.6.1 `mcp.arcrun.dev/mcp/mcp` initialize + tools/list + 一個 tool call 全通
- [ ] 4.6.2 我的 Claude Code config 切到新 URL,用 `mcp__arcrun__search_components` 端對端測
- [ ] 4.6.3 grep `u6u``matrix/arcrun-mcp/` 結果為 0(除了 changelog 紀錄)
---
## 風險追蹤
- 風險 1backfill 跑進去發現某些 contract.yaml 格式跟 registry 期望不一樣 → 緩解:dry-run 先看,必要時補 contract 欄位
- 風險 2Phase 4 rebrand 期間用戶 client 設定亂 → 緩解:過渡期 + Deprecation header
- 風險 3:自動註冊 hook 失敗導致部署被擋 → 緩解:degraded modewarning 不 fail
---
## Known Issues(吃狗糧發現的,先記錄)
### KI-1u6u-mcp README URL 寫錯
- README 寫 `mcp.finally.click/mcp`,實際是 `mcp.finally.click/mcp/mcp`basePath + route
- 影響:用戶照 README 裝完試打不通
- 解法:rebrand 時順便修
### KI-2inkstone-component-registry 跟 arcrun-registry 並存
- 兩個 worker 都活著,u6u-mcp 之前指錯
- inkstone-component-registry 是舊版(2026-03-24)、arcrun-registry 是現役(2026-04-16
- 解法:Phase 1 backfill 完成後,inkstone-component-registry worker 廢止
### KI-3:search 對自然語言不夠靈敏(吃狗糧第一個發現)
- 現象:
- `search("從 KBDB 讀取或查詢 block")` → 0 結果
- `search("kbdb")` → 3 結果(kbdb_ingest / kbdb_patch_block / kbdb_create_block
- 根因:搜尋走 embedding(bge-m3)相似度,但既有零件清單少(30 個)+ description 寫得正式,自然語言整句的 embedding 跟 description 距離太遠
- 影響:**致命** — AI 第一句永遠是自然語言整句,回 0 就會放棄 search 改寫 Python
- 解法(Phase 3 處理):
1. embedding search 之外加 keyword fallbacksplit query → 對 canonical_id / display_name / tags 做 ILIKE
2. 或 lower threshold(目前 SCORE_THRESHOLD = 0.5,可能過高)
3. MCP get_component_guide 教 AI 「找不到時拆關鍵字再 search」
- 優先級:P1(會擋推廣)
-117
View File
@@ -1,117 +0,0 @@
# Design: 資料外流警示(Data Exfiltration Warning
> 2026-05-30。實作 requirements.md。**本 design 需 richblack review 後才動 code。**
> 觸發策略(richblack 定):**只在「資料變成可被外部呼叫」時問**(暴露面),不管「我去打別人 API」(出站,高頻低風險)。
---
## 0. 核心定義:什麼是「資料變成可被外部呼叫」
警示的觸發點 = 一個動作**讓某份資料 / 能力變成「別人能呼叫得到」**。這是真正的裸奔動作。
**不觸發**:「我自己的 workflow 去打別人的 API」(出站)——那是我主動用別人服務,不是把我的東西開放出去。
### 哪些動作屬於「變成可被外部呼叫」(要警示)
| 動作 | 為何是暴露 |
|---|---|
| 部署 webhook trigger`acr push` workflow → 可被 POST 觸發) | workflow 變成一個對外可呼叫的 endpoint。誰打它就能跑它、拿它的輸出 |
| recipe 貢獻到公共庫(未來飛輪項) | recipe(含 endpoint 設定)變成全生態可見可用 |
| 把 workflow / recipe 的可見性改為 public(若未來有此欄位) | 同上 |
### 哪些動作**不**觸發(避免盲目按 yes)
- `acr run`(本機跑,不暴露)
- `acr recipe push`(存私人 KV,綁 api_key,只有自己 + 自己的 workflow 用——**不是**對外暴露)
- ⚠️ 待 design review 確認:recipe push 現況是私人的,**不暴露**。除非未來加「公共」旗標才觸發。
- workflow 裡「打別人 API」的 http 節點(出站,不暴露我的資料給別人呼叫)
- 查詢類、creds push(上傳加密 credential 是保護不是暴露)
### 灰色地帶(design review 要定)
- **webhook trigger 現況**:要 `X-Arcrun-API-Key`(owner 的 key)才能打 → 嚴格說「只有我能打」,不算對全世界暴露。
但:(a) key 一旦外流就全開;(b) 用戶可能不知道「部署 webhook = 開了一個 endpoint」。
**是否警示 webhook 部署?** 傾向「是」,因為用戶可能不知情它變成 endpoint,且這是「把 workflow 變 API」的那一步(richblack 最早的風險點)。
## 1. 兩道防線
### 1a. hook(防在前,AI 動手當下)
AI 在本機寫「會變成可被外部呼叫的東西」前,hook 警告 + 要人類確認。可偵測的訊號:
- 寫 workflow YAML 含 webhook trigger / 對外觸發設定。
- 跑 `acr push`(部署 webhook)、未來的 recipe 公共貢獻指令。
- pre-bash / pre-write hook 偵測這些 → 輸出警示「這會把 X 變成可被外部呼叫,需人類確認」。
- **誠實限制**:hook 偵測的是「動作形態」(部署 webhook),不是「資料是否敏感」(機器判不準)。
### 1b. API 層(防在後,真的暴露前)
暴露動作打到 serverwebhook 部署 endpoint / 未來公共貢獻 endpoint)時,server 要求帶
「人類已確認暴露」的明示旗標,沒帶 → 拒絕 + 提示。與 component-gatekeeping 的 human_confirmation 同模式:
```ts
exposure_confirmation?: {
confirmed_by_human: true;
understood_exposure: string; // 人類說明「我知道這會把什麼開放給誰」
confirmed_at: string;
}
```
缺 → 拒絕,訊息:「部署 webhook = 開一個對外可呼叫的 endpoint。確認你知道這會暴露 [workflow 名]
的能力/輸出?用 `acr push --confirm-exposure` 並說明。」
### 為何兩道都要
- 只有 hookAI 可繞過 CLI 直接打 server API → API 層補。
- 只有 API 層:AI 已經寫好暴露的東西才在送出時被擋,浪費 + 用戶較晚看到 → hook 提前。
- 同 component-gatekeeping 的雙層精神。
## 2. 警示要讓人看得懂(R1
不是「確認嗎 Y/N」,是說清楚風險:
```
⚠️ 這個動作會把 workflow "contacts_lookup" 變成可被外部呼叫的 endpoint:
https://cypher.arcrun.dev/webhooks/named/contacts_lookup/trigger
任何持有觸發憑證的人都能呼叫它、取得它的輸出。
這個 workflow 讀取:[盤出 workflow 用到的資料源,若可得]
需要保護(要求呼叫者認證)嗎?目前的觸發認證是:[現況]
確認部署?(需人類明示)
```
- 「workflow 讀取什麼資料源」:盡力從 workflow 定義盤(用了哪些 recipe / endpoint),盤不出就標「無法自動判斷,請自行確認」。誠實。
## 3. known-destinations / 不重複問(R4 避免盲目按 yes)
觸發策略已經很窄(只暴露動作),但同一個 workflow 重複部署不該每次問。
- 首次部署某 workflow 為 webhook → 問。人類確認後記住(該 workflow 標記 exposure-confirmed)。
- 之後同 workflow 更新 → 不重問(除非暴露面變大,如新增對外觸發)。
- 記在哪:workflow metadataWEBHOOKS KV 的 record 加 `exposure_confirmed_at`)。
## 4. 與既有一致(R5
- 同 component-gatekeepingAI 不可替人類決定有外洩風險的動作;誠實限制(AI 能偽造 confirmed_by_human,靠軌跡可審 + mindset)。
- 同「arcrun 不做授權判斷」:不判斷「該不該暴露」,只「攔下來讓人類明示同意」。不禁止暴露,要明示同意。
## 5. 範圍邊界
- **動**webhook 部署路徑(webhooks-named.ts)的 exposure_confirmation + CLI `acr push --confirm-exposure` 互動 + hook。
- **不動**:用戶 API 入站保護機制(發 key/權限/限流,另列 BACKLOG);recipe 私人 push(不暴露,不擋);出站 http 節點(不擋)。
- recipe 公共貢獻路徑未實作 → 本系統只要求它**未來**內建 exposure 閘門(記進那條 BACKLOG 項)。
## 6. 決議(richblack 2026-05-30 design review
- **Q1 → recipe push 也警示,公私一視同仁。** 不是因為 recipe 本身暴露,而是統一原則「凡有資料去向/暴露面的動作都警示」。用戶可選「以後不要警示」(記偏好)。理由見 §7 同意 log。
- **Q2 → webhook 部署要警示,但角度是「提醒 + 提供保護」不是「擋」。** 用不用認證是用戶決定(如美國氣象 API 本就無 key 公開)。我們警示時**順便提醒「可用 arcrun 提供的保護措施」**(接「用戶 API 保護機制」資安優勢,BACKLOG 待決策)。首次問、記住(§3)。
- **Q3 → hook 偵測 `acr push` 指令**(簡單版,pre-bash 攔指令)。
## 7. 同意 = 法律憑證(richblack 2026-05-30,重要)
每次人類同意「暴露/送資料」的動作,**留 log(誰、何時、同意了什麼)**。這不只是「軌跡可審」,是**法律保護**:
- 真發生資料外洩 / 糾紛時,有「用戶在 [時間] 明示知情同意把 [什麼] 暴露給 [誰]」的證據 → 避免訴訟風險(責任在明示同意的用戶,不在 arcrun)。
- 「以後不要警示」這個選擇**本身也要 log**(用戶在 [時間] 選擇了不再對 [X] 警示 = 他知道風險並接受)。
- 同意 log 存放:與動作關聯(webhook record / recipe record 的 `exposure_consent: { confirmed_by, understood, confirmed_at, suppress_future }`)。
- 誠實限制同前:AI 能偽造 confirmed_by_human。但**法律意義上,憑證存在 + 可審 = 用戶有機會知情**,這道防線的價值是法律歸責不是技術防偽。
→ 這把 §1b 的 `exposure_confirmation` 升級為**帶法律意義的同意憑證**,所有暴露/送資料動作(recipe push / webhook 部署)共用此機制。
## 8. 警示是「保護措施的入口」(不只是攔)
警示訊息除了說明風險,**主動提供 arcrun 的保護措施**(產品價值,非只防呆):
```
⚠️ 這個動作會把 [X] 開放/送出。
arcrun 可以幫你保護它:
- 要求呼叫者帶 API Key(你發給特定對象)
- 設定權限 / 限流
一個動作就能加上。要加保護嗎?還是確認公開(如公開資料 API)?
```
(具體保護機制是 BACKLOG「用戶 API 保護機制」待決策項——本系統先在警示處**提示它存在**,實作後接上。)
@@ -1,56 +0,0 @@
# Requirements: 資料外流警示(Data Exfiltration Warning
> 2026-05-30 建立(richblack 確認)。資安優先、及早做。
> 判準源:DECISIONS §7(讓 AI 不做歪 + 閉環)、§0(減少不可控依賴 / 風險);BACKLOG 步驟 5b。
---
## 背景與風險
arcrun 讓「產生 API / 把資料送出去」變很簡單(一堆資料 + webhook trigger = APIrecipe = 打某 endpoint)。
**這個「簡單」本身是風險**:簡單到 AI 可能在用戶不知情下,把含個資的東西變成可被呼叫的 endpoint,或把
敏感資料送到非預期對象。
**richblack 的核心情境**:用戶有個 Google Sheets 存所有朋友的個資。正確用法是「call 它查詢」(自用)。
但若 AI 把它變成一個 recipe / workflow**送資料到非預期對象**(公開 webhook、公司群、外部 endpoint),
且**沒做認證** → 所有資料裸奔。
**關鍵原則(richblack 2026-05-30):**
1. **不分公私庫都要警示**。私人庫(公司用)一樣會出事(不小心把個資 POST 到公司群)。觸發點不是「推公共」,
是「這動作會讓資料流向某處」。
2. **不禁止**用戶公開 / 送資料(他要放什麼給誰是他的自由)——**但要確定他自己明示同意了**,不是 AI 替他決定。
3. **兩道防線**(a) hook 在 AI 動手前警告(防在前);(b) API 層在資料真送出前攔(防在後)。不論哪條路都攔。
## 需求
### R1 — API 層警示(資料送出前需人類同意,不分公私庫)
任何「把資料送往某目的地」的 API 動作,在執行/儲存前需人類明示同意。涵蓋(待 design 盤準):
- `acr recipe push`recipe 定義一個 endpoint(資料去向)。
- `acr push`workflow):workflow 可能含「讀敏感源 → 送往某 endpoint」的節點。
- webhook trigger 部署:把 workflow 變成可被呼叫的 API。
- 未來「recipe 貢獻公共庫」路徑(BACKLOG 飛輪項)。
同意內容要讓人看得懂風險:**這個動作會把 [什麼資料] 送到 [哪個目的地],需不需要 credential 保護?確認?**
### R2 — hook 在 AI 動手前警告
AI 要做這類動作(寫含外部 endpoint 的 recipe/workflow、跑會送資料的指令)前,hook 先警告 + 要人類確認。
這是「防在前」——在 AI 本機動手當下就提醒,不等到 API 層才攔(省一趟、且更早讓人看到)。
### R3 — 誠實的偵測範圍(不假裝能全防)
「敏感資料送到非預期對象」機械上難完整偵測。本系統做**可機械判斷的部分**,誠實標明擋不住的:
- 能偵測:動作含「外部 endpoint」(recipe endpoint / workflow http 節點 / webhook 對外)。
- 難偵測:「這份資料是否敏感」「這個目的地是否非預期」——這需要語境,機器判不準。
- 做法:偵測到「資料 → 外部目的地」的動作就警示,由**人類**判斷該資料/目的地是否該放行(人類同意是判斷點,機器只負責「攔下來問」)。
### R4 — 不擋自用、不製造過度摩擦
- 純自用、不送資料出去的動作(acr run 本機、查詢類)不該被警示淹沒。
- 警示要精準在「資料外流」動作,不是每個動作都問(否則用戶會盲目按 yes,警示失效)。
### R5 — 與既有閘門一致
- 與「建零件人類閘門」(component-gatekeeping)同精神:AI 不可替人類決定有外洩風險的動作。
- 與「arcrun 不做授權判斷」一致:不判斷「該不該送」,只「攔下來讓人類明示同意」。
## 非目標
- 用戶自己的 API 保護機制(入站認證:發 key 給別人 / 權限 / 限流)—— 另列 BACKLOG 待決策。
- 完整 DLP(資料外洩防護)系統 / 內容掃描判斷敏感度 —— 機器判不準,不做。
- recipe 公共貢獻路徑本身 —— 未實作(飛輪項),本系統只要求它未來內建同意閘門。
-40
View File
@@ -1,40 +0,0 @@
# Tasks: 資料外流警示
> 對應 design.mdrichblack 已 reviewQ1-Q3 + 法律憑證 + 保護入口決議)。
> 每完成一個 task 立刻標 [x],不批次。
---
## 共用:同意憑證機制(§7 法律憑證)
- [x] C1 定義 `exposure_consent { confirmed_by, understood, confirmed_at, suppress_future }` 型別
- [x] C2 同意 log 寫入動作關聯的 recordwebhook record / recipe record),可審
- [x] C3 「以後不要警示」(suppress_future)本身也 log(用戶知風險並接受)
## API 層警示(R1,防在後)
- [x] A1 webhook 部署(webhooks-named.ts POST)要 exposure_consent,缺且未 suppress → 拒絕 + 提示
- [x] A2 recipe push/recipes POST)同上(公私一視同仁)
- [x] A3 首次暴露某資源問、記住(exposure_confirmed / suppress_future)→ 之後不重問(§3)
- [x] A4 警示訊息說明風險 + 盤資料源(盡力,盤不出標「請自行確認」)+ **提示 arcrun 保護措施**(§8
## CLI 警示(互動 + 旗標)
- [x] B1 `acr push`:部署前互動式警示(首次某 workflow),人類確認組 exposure_consent 送出;`--confirm-exposure` 跳過互動(CI/非 TTY);`--suppress-warning` 記偏好
- [x] B2 `acr recipe push`:同上
- [x] B3 非 TTYAI 直跑)無 --confirm-exposure → 拒絕並提示「需人類確認暴露」
## hook(R2,防在前,Q3=偵測指令)
- [x] H1 pre-bash-guard:偵測 `acr push` / `acr recipe push` → 警示「這會把 X 變可被外部呼叫,需人類確認」
## mindset / 文件
- [x] M1 誠實限制(AI 能偽造 confirmed_by_human,靠憑證可審 + 法律歸責)寫進 mindset Skill(步驟 7+ 文件
## 驗收
- [ ] V1 acr push 部署 webhook(首次)→ 互動警示 + 說明暴露 + 提示保護 的終端輸出
- [x] V2 非 TTY 跑 acr push 無 --confirm-exposure → 拒絕的輸出
- [x] V3 webhook 部署 API 無 exposure_consent → 拒絕的輸出
- [x] V4 同一 workflow 二次部署 → 不重問(已記住)
- [ ] V5 --suppress-warning 後 → 不再警示,但 suppress 選擇有 log
- [x] V6 同意後 → exposure_consent 寫進 record 可查(法律憑證)
## 範圍邊界
- 不動用戶 API 入站保護機制(發 key/權限/限流)—— BACKLOG 待決策,本系統只在警示處「提示它存在」。
- 不擋出站 http 節點(不暴露我的資料)、不擋 acr run(本機)。
-642
View File
@@ -1,642 +0,0 @@
# Design: LI (LLM Interface) for arcrun
> v0.1 — 2026-05-16
> 對應 requirements.md(同目錄)
---
## 1. 設計哲學
| 過去 (UI 時代) | 現在 (LI 時代) |
|---|---|
| 使用者是人,要學軟體 | 使用者是 AI,要被軟體 onboard |
| 操作靠視覺、滑鼠 | 操作靠 MCP tool call |
| 錯誤訊息是技術 stack trace | 錯誤訊息是「下一步該做什麼」 |
| 文件是 long-form 教學 | 文件是結構化 schema + 可程式查 |
| 用戶教學是 onboarding 課程 | 「教學」是 MCP 工具自己會回 hint |
| 回饋靠 helpdesk 工單 | 回饋是 MCP toolAI 直接 call |
**原則**
1. **discoverable** — AI 不靠 grep / 不靠人就能知道有什麼
2. **idempotent** — 同樣輸入兩次結果一樣,可預測
3. **dry-run by default** — preview 是預設、commit 是 explicit
4. **structured errors** — error 是 JSON 含 next_actions,不是字串
5. **closed loop** — AI 卡住的 data 自動回流,平台 self-improving
---
## 2. 系統架構
```
┌────────────────────────────────────────────────────────────────┐
│ AI agent (Claude / Cursor / ...) │
└──────────────────┬──────────────────────────────┬───────────────┘
│ │
│ ① Read AGENTS.md │ ② MCP tool calls
▼ ▼
┌────────────────────────┐ ┌──────────────────────────────────┐
│ AGENTS.md │ │ arcrun-mcp (擴張 arcrun-mcp) │
│ (repo + KBDB block) │ │ 25 tools, 5 categories │
└────────────────────────┘ └──────────────┬───────────────────┘
┌────────┬─────────┬────────┴────────┬──────────┐
▼ ▼ ▼ ▼ ▼
┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌──────────┐
│ cypher- │ │registry │ │ kbdb │ │ Skills │ │ Examples │
│executor │ │ │ │ │ │ (KBDB) │ │ (git+KV) │
│ 31 路由 │ │ │ │ 50 路由 │ │ │ │ │
└─────────┘ └─────────┘ └─────────┘ └─────────┘ └──────────┘
│ ③ AI 部署完 / 卡住 call feedback
┌─────────────┴──────────────┐
│ report_feedback / agent-telemetry │
│ (寫進 KBDB) │
└─────────────┬──────────────┘
│ ④ 每週聚合
┌──────────────────────────────┐
│ agent_feedback_weekly_review │
│ (arcrun workflow, dogfood) │
│ → arcrun-roadmap KBDB block │
└──────────────────────────────┘
```
---
## 3. 五層 LI 模型
### Layer 0OnboardingAGENTS.md
**位置**
- `arcrun/AGENTS.md`repo 根,git 可讀)
- KBDB `type=agent-onboarding` blockMCP `get_onboarding_doc()` 拉)
**結構(控 5-8K tokens**
1. **30 字 What** — 「arcrun 是用 YAML 把零件串成 workflow 的平台」
2. **5 個核心概念** — component / cypher binding YAML / FOREACH / paused-resume / api_key
3. **連線**:「在你的 MCP config 加 `https://mcp.arcrun.dev/mcp`bearer `ak_xxx`
4. **第一個 workflowminimal example** — 10 行 YAML,含部署 + 觸發
5. **5 個 URL 慣例與為什麼**workers.dev vs arcrun.dev / KBDB endpoints
6. **常見錯誤 5 個 + 怎麼讀**
7. **不確定的時候**:明確列「先 call list_X 再下手」
8. **回報機制**:完成任何 workflow deploy / 卡住,**必須** call `report_feedback`
**部署機制**
- repo `arcrun/AGENTS.md` 是 source of truth
- GH Actions 監聽該檔變動 → 自動 PATCH KBDB onboarding block
- MCP `get_onboarding_doc()` 從 KBDB 讀(最新)
---
### Layer 1arcrun-mcp(主戰場)
#### 1.1 命名與部署
- **Server URL**: `https://mcp.arcrun.dev/mcp`(唯一 URL
- **舊 URL**`mcp.finally.click` 直接退場(leo 2026-05-16 拍板,理由:「finally.click 是還沒申請新網址的暫用,那是一個服務,arcrun 是底層」)
- **Worker rename**`u6u-mcp``arcrun-mcp`wrangler name 改),repo 路徑 `matrix/u6u-mcp/``matrix/arcrun-mcp/`
- **Tool prefix**: 統一 `arcrun_*`(單一 rename**不保留 `u6u_*` alias**,不留 deprecation 期。用戶量還很少,一次切換可接受。切換前在 mira / telegram 公告)
#### 1.2 Tool 完整清單(25 個,5 類)
##### A. Onboarding & Discovery5 tools
| Tool | 對應 HTTP | 用途 |
|---|---|---|
| `arcrun_get_onboarding` | KBDB GET `/blocks?type=agent-onboarding` | 拉 AGENTS.md 內容 |
| `arcrun_list_components` | registry GET `/components` | 所有可用零件 + 1 行 desc |
| `arcrun_get_component_contract` | registry GET `/components/:id` | input/output schema + gherkin tests |
| `arcrun_list_recipes` | cypher GET `/recipes` + `/auth-recipes` | API recipe 一覽 |
| `arcrun_search_examples` | KBDB POST `/search`type=workflow-example| 用 use case 搜範例 |
##### B. Workflow CRUD7 tools
| Tool | 對應 HTTP | 用途 |
|---|---|---|
| `arcrun_list_workflows` | cypher GET `/webhooks/named` | 用戶現有 workflow |
| `arcrun_get_workflow` | cypher GET `/webhooks/named/:name` | 拿 YAML / graph |
| `arcrun_validate_yaml` | cypher POST `/validate` | dry-run 校驗,不部署 |
| `arcrun_push_workflow` | cypher POST `/webhooks/named` | 部署(也含 validate |
| `arcrun_delete_workflow` | cypher DELETE `/webhooks/named/:name` | 刪 |
| `arcrun_preview_workflow` | **新增** POST `/preview` | 不寫 KV、模擬 execute |
| `arcrun_diff_workflow` | **新增** POST `/webhooks/named/:name/diff` | 既有 vs 新 YAML 差異 |
##### C. Execution & Trace5 tools
| Tool | 對應 HTTP | 用途 |
|---|---|---|
| `arcrun_run_workflow` | cypher POST `/webhooks/named/:name/trigger` | 觸發 + 回 execution_id |
| `arcrun_get_execution_trace` | **新增** GET `/executions/:id` | 結構化 traceper-node status |
| `arcrun_list_recent_executions` | **新增** GET `/workflows/:name/executions?limit=10` | 最近 N 次 |
| `arcrun_list_paused_executions` | **新增** GET `/executions/paused` | 卡 callback 的 |
| `arcrun_resume_execution` | cypher POST `/workflows/resume` | 手動 resume |
##### D. Component & Recipe Management4 tools
| Tool | 對應 HTTP | 用途 |
|---|---|---|
| `arcrun_search_components` | registry POST search | 語意搜尋 |
| `arcrun_publish_component` | registry POST publish | 上 WASM |
| `arcrun_create_recipe` | cypher POST `/recipes` | 建 API recipe |
| `arcrun_create_auth_recipe` | cypher POST `/auth-recipes` | 建 auth recipe |
##### E. Feedback & Skills4 tools
| Tool | 對應 HTTP | 用途 |
|---|---|---|
| `arcrun_report_feedback` | KBDB POST `/blocks`type=agent-feedback| AI 主動回報 |
| `arcrun_list_skills` | KBDB GET `/blocks?type=agent-skill` | 列可用 playbook |
| `arcrun_get_skill` | KBDB GET `/blocks/:id` | 拿 skill 內容 |
| `arcrun_publish_skill` | KBDB POST `/blocks`type=agent-skill| AI 把學到的存回去 |
#### 1.3 Tool contract 統一規範
每個 tool 都遵守:
**Input**
- Zod schema declarative
- 每個參數有 `.describe('...')`(給 AI 看的)
- optional 標 default
**Output(成功)**
```typescript
{
ok: true,
data: T,
hints?: string[], // optional next-step suggestione.g.「你剛部署了,可 call run_workflow 測試」
}
```
**Output(失敗)**
```typescript
{
ok: false,
error_code: 'enum_value',
human_message: string,
next_actions: string[], // 可程式化的下一步
detail?: unknown, // 原始錯誤(debug
}
```
#### 1.4 error_code enumv1,可加不可刪)
| error_code | 對應狀況 |
|---|---|
| `auth_missing` | api_key 沒帶 |
| `auth_invalid` | api_key 無效 |
| `not_found` | workflow / block 不存在 |
| `validation_failed` | YAML / schema 不過 |
| `component_not_found` | 零件 ID 沒對應 |
| `component_not_in_whitelist` | 零件存在但 cypher-executor 不認 |
| `paused_awaiting_resume` | workflow 在等 callback |
| `rate_limited` | 太頻繁 |
| `internal_error` | 平台 bug(含 detail |
| `dependency_unavailable` | 下游服務(claude / mira daemon / KBDB)掛 |
---
### Layer 2Skill blocksKBDB-native playbook
#### 2.1 Schema
```typescript
type AgentSkill = {
id: string;
type: 'agent-skill';
page_name: `skill-${slug}`;
content: string; // markdown playbook
tags_json: string[]; // ['watcher', 'debug', 'rag', ...]
source: 'manual' | 'auto-extracted';
metadata_json: {
when_to_use: string;
example_use_case: string;
contributed_by?: string; // agent user_agent
success_count?: number;
};
};
```
#### 2.2 種子 skill(先建 5 個,以 mira 經驗為本)
| skill page_name | 用途 |
|---|---|
| `skill-build_watcher_workflow` | cron 掃資料 → 觸發處理(mira_feed_watcher pattern |
| `skill-debug_paused_workflow` | claude_api / 任何 async callback paused 怎麼追 |
| `skill-migrate_http_to_trigger_workflow` | self-fetch 換 trigger_workflow(剛踩過的) |
| `skill-rag_with_arcrun` | 用 KBDB + claude_api 做 RAG |
| `skill-add_new_wasm_component` | 從 TinyGo 寫到 deploy 全流程 |
#### 2.3 自動萃取機制(後期)
`agent_feedback_weekly_review` 跑出來的 Top patternsLLM 包成 skill draftleo review 後 publish。
---
### Layer 3Examples(可搜尋的範例庫)
#### 3.1 存放
- `arcrun/registry/examples/{slug}/`
- `workflow.yaml`
- `description.md`(解決什麼問題、怎麼 trigger、預期結果)
- `tags.json``["webhook", "llm", "cron", ...]`
- CI build 出 `examples-index.json` 推 KBDBtype=workflow-example,內容是 YAML + tags
#### 3.2 搜尋
- `arcrun_search_examples(use_case)` 走 KBDB `/search`(既有 semantic
- 命中 → 回 YAML + description + 「你可以基於這個改 X」hint
#### 3.3 第一批範例(10 個)
| slug | 用途 |
|---|---|
| `webhook-to-slack` | 簡單轉發 |
| `cron-watcher` | mira_feed_watcher 簡化版 |
| `llm-classify` | claude_api 分類 |
| `rag-search-answer` | 從 KBDB 找 context → 回答 |
| `email-summary` | gmail → claude → telegram |
| `pdf-to-blocks` | convert → ingest |
| `github-issue-bot` | webhook → claude → 留 comment |
| `daily-digest` | cron → 多源聚合 → 推送 |
| `parallel-fanout` | 一份輸入分發多 workflow |
| `error-retry` | try_catch + wait + retry |
---
### Layer 4Telemetry & Feedback Loop
#### 4.1 兩條線
```
explicit feedback (AI call) implicit telemetry (platform auto-log)
│ │
▼ ▼
KBDB block type= KBDB block type=
agent-feedback agent-telemetry
│ │
└──────────────┬───────────────────────┘
agent_feedback_weekly_review
(arcrun workflow, 週一 9am cron)
LLM 聚合 + 分類
KBDB block type=arcrun-roadmap
leo 收到 + 寫進 mira 河道)
```
#### 4.2 agent-feedback schema
```typescript
type AgentFeedback = {
type: 'agent-feedback';
content: string; // description 原文
source: 'mcp-tool-call';
user_id: string; // 用戶 namespace
metadata_json: {
workflow_name?: string;
issue_type: 'doc_unclear' | 'tool_missing' | 'error_unhelpful'
| 'unexpected_behavior' | 'feature_request' | 'success_story';
retry_count?: number;
blocked: boolean;
suggested_fix?: string;
agent_user_agent: string; // 'claude-code/1.x' etc
};
tags_json: ['agent-feedback', `issue:${issue_type}`];
};
```
#### 4.3 agent-telemetry schema
```typescript
type AgentTelemetry = {
type: 'agent-telemetry';
source: 'cypher-executor';
metadata_json: {
event_type: 'deploy_success' | 'deploy_fail' | 'run_success'
| 'run_fail' | 'validation_error' | 'mcp_tool_call';
workflow_name?: string;
component_id?: string;
error_code?: string;
duration_ms: number;
api_key_hash: string; // 雜湊,不存原值
agent_user_agent: string;
};
};
```
寫入點:
- `webhook-handlers.executeWebhookGraph` 完成(成功 / 失敗都記)
- `routes/webhooks-named.ts` pushdeploy 記)
- `routes/cypher.ts` validate 失敗(validation_error 記)
- arcrun-mcp 每個 tool call wrap 記(mcp_tool_call
#### 4.4 隱私
- api_key 一律 SHA-256 截前 16 字元(不可逆,可聚合)
- workflow content 不 log(只記 name
- 用戶看自己的 telemetryMCP `arcrun_my_telemetry(limit)`,只回自己 hash
#### 4.5 weekly review workflow
```yaml
name: agent_feedback_weekly_review
description: 每週一聚合 AI 回饋 + telemetry,產出 Top 痛點
flow:
- "weekly_cron >> ON_SUCCESS >> fetch_feedback"
- "fetch_feedback >> ON_SUCCESS >> fetch_telemetry"
- "fetch_telemetry >> ON_SUCCESS >> aggregate"
- "aggregate >> ON_SUCCESS >> llm_summarize"
- "llm_summarize >> ON_SUCCESS >> publish_roadmap_block"
- "publish_roadmap_block >> ON_SUCCESS >> notify_telegram"
config:
weekly_cron:
component: cron
cron_expr: "0 9 * * 1" # 週一 9 am UTC
fetch_feedback:
component: kbdb_get
type: 'agent-feedback'
created_after: "{{ now - 7d }}"
limit: 500
fetch_telemetry:
component: kbdb_get
type: 'agent-telemetry'
created_after: "{{ now - 7d }}"
limit: 5000
aggregate:
component: filter # 或寫個 `group_by` 邏輯零件
items: "{{fetch_feedback.blocks}}"
group_by: 'metadata_json.issue_type'
llm_summarize:
component: claude_api
prompt: |
你是 arcrun 平台的回饋分析師。下面是這週 AI agent 的所有回饋 +
telemetry 失敗事件。請:
1. 列 Top 5 痛點(含證據引用)
2. 為每個痛點建議具體改動(文件 / MCP tool / 錯誤訊息)
3. 評估嚴重程度(blocked AI vs 略不便)
Feedback{{aggregate}}
Telemetry 失敗事件:{{fetch_telemetry.blocks}}
publish_roadmap_block:
component: kbdb_create_block
type: 'arcrun-roadmap'
page_name: 'roadmap-week-{{date.iso_week}}'
content: "{{llm_summarize.data.text}}"
notify_telegram:
component: telegram
chat_id: "{{ leo_chat_id }}"
text: "Arcrun 週報出爐:{{publish_roadmap_block.data.id}}"
```
**這個 workflow 是 dogfood inception**arcrun 自己跑分析 arcrun 自己。
---
## 4. Coverage Matrix(人類 vs AI
> 這份表是 NFR-5「覆蓋率可量化」的具體實踐。每個 release 重新檢視。
### 4.1 人類 GUI 能做的事 × LI 對應
| 人類在 arcrun-gui 做的 | LI MCP 工具 | 對等? |
|---|---|---|
| 看現有 workflow 列表 | `arcrun_list_workflows` | ✅ |
| 點開 workflow 看 YAML | `arcrun_get_workflow` | ✅ |
| 編輯 workflow YAML | `arcrun_push_workflow`(含 update | ✅ |
| 執行 workflow | `arcrun_run_workflow` | ✅ |
| 看執行結果 | `arcrun_get_execution_trace` | ✅(新增) |
| 翻零件庫 | `arcrun_list_components` + search | ✅ |
| 看零件 contract | `arcrun_get_component_contract` | ✅ |
| 上傳 WASM 零件 | `arcrun_publish_component` | ✅ |
| 設 credential | `arcrun_create_recipe` + (手動 push credential | ⚠️ 部分(credential 提交需安全考量,AI 不全自動) |
| 翻歷史執行 | `arcrun_list_recent_executions` | ✅(新增) |
| 看 paused 工作流 | `arcrun_list_paused_executions` | ✅(新增) |
| 手動 resume | `arcrun_resume_execution` | ✅ |
| 視覺化 canvas 拖拉 | — | ❌(純視覺,LI 不複製) |
| 看排版 prototype | — | ❌(同上) |
| Flip UI/Logic view | — | ❌(同上) |
| Action log(操作軌跡) | `arcrun_my_telemetry` | ✅(含更多資料)|
**Gap**:3 個視覺類動作 LI 不需要對等;其他全等。
### 4.2 cypher-executor 31 路由 × LI 暴露
| 路由 | LI 暴露? | 工具 |
|---|---|---|
| `/health``/docs``/openapi.json` | ❌ infra | — |
| `/execute` | ✅ | `arcrun_run_workflow`(內部) |
| `/cypher/search``/cypher/execute` | ✅ | discovery 內部用 |
| `/workflows/resume` | ✅ | `arcrun_resume_execution` |
| `/webhooks/named*` | ✅ | CRUD 對應 5 個 tools |
| `/webhooks/*` (anonymous) | ❌ deprecated | — |
| `/credentials*` | ⚠️ | 只 list + deletePOST 走人類流程(安全考量) |
| `/recipes*``/auth-recipes*` | ✅ | 4 個 tools |
| `/validate` | ✅ | `arcrun_validate_yaml` |
| `/auth/*``/register``/me*` | ❌ admin | — |
**新增需建路由**`/preview``/executions/:id``/workflows/:name/executions``/executions/paused``/webhooks/named/:name/diff``/my-telemetry`(共 6 個)
### 4.3 KBDB 50 路由 × LI 暴露
LI **不直接 expose KBDB 50 個路由**。AI 透過 arcrun-mcp 的 abstracted tool`get_skill` / `list_skills` / `report_feedback` 等)間接用 KBDB。
例外(值得直接 expose 的):
- `arcrun_kbdb_search(query)` — 走 `/search` semantic
- `arcrun_kbdb_get_block(id)` — 看 block 內容(debug 用)
KBDB schema 設計 / triplets / records 這些屬於另一個 SDDKBDB MCP),不在本 LI 範圍。
---
## 5. AGENTS.md 模板
```markdown
# Arcrun for AI Agents
## What is Arcrun (30 sec)
Arcrun lets you compose Cloudflare Workers (WASM components) into workflows via YAML.
You write YAML, push to KV, trigger via webhook or cron. Each component is a
TinyGo/AssemblyScript WASM with stdin/stdout JSON I/O.
## Connect (one step)
Add to your MCP config:
\`\`\`json
{
"mcpServers": {
"arcrun": {
"url": "https://mcp.arcrun.dev/mcp",
"headers": {"Authorization": "Bearer $ARCRUN_API_KEY"}
}
}
}
\`\`\`
Get your ak_ key at https://arcrun.dev/me.
## Your first workflow (5 min)
1. `arcrun_list_components()` → see what's available
2. Write 3-line YAML in chat (assistant does this)
3. `arcrun_validate_yaml(yaml)` → confirm
4. `arcrun_push_workflow(yaml)` → deploy
5. `arcrun_run_workflow(name, input)` → test
6. `arcrun_get_execution_trace(id)` → see results
## Core concepts (5 things)
- **Component**: WASM Worker. Use existing or `arcrun_publish_component`.
- **Cypher binding YAML**: triplets `A >> relation >> B` define graph.
- **FOREACH**: `>> 對每個 X >> next_node` iterates lists.
- **Paused-resume**: claude_api etc pause workflow; resumes via callback.
- **api_key**: ak_xxx, all calls need this in MCP auth.
## URL conventions (why this matters)
- `cypher.arcrun.dev` = orchestration API (you push here)
- `arcrun-{kebab}.{user}.workers.dev` = component workers (workers.dev avoids CF self-fetch)
- `kbdb-*.arcrun.dev` = KBDB component workers (your data lives here)
## Common errors
| error_code | What to do |
|---|---|
| `component_not_found` | call `list_components`, check spelling |
| `validation_failed` | read `next_actions` field, fix YAML |
| `paused_awaiting_resume` | normal for claude_api; wait or `get_execution_trace` |
| `auth_invalid` | ak_ wrong, re-fetch from /me |
## Unsure? Do this:
- Don't know what's available → `list_components` / `list_workflows` / `search_examples`
- Don't know syntax → `get_skill('skill-build_X')` for playbooks
- Hit weird behavior → **MUST** call `report_feedback(issue_type='unexpected_behavior', ...)`
## Feedback (required)
After every workflow deploy or debug session, call:
\`\`\`
arcrun_report_feedback({
workflow_name: '...',
issue_type: 'success_story' | 'doc_unclear' | 'tool_missing' | 'error_unhelpful' | 'feature_request',
description: '...',
blocked: false,
retry_count: N,
suggested_fix: '...' // optional
})
\`\`\`
Even "success" stories help — they tell us what's working.
```
---
## 6. Migration planu6u-mcp → arcrun-mcp 一次切換)
### 6.1 切換方式
leo 2026-05-16 拍板:u6u branding 整體退場,**單一 rename,不留 alias 也不留 deprecation 期**。
- Worker 部署 name 改:`u6u-mcp``arcrun-mcp`wrangler.toml
- 路由改:`mcp.finally.click/*``mcp.arcrun.dev/*`DNS + worker route 同步切)
- 舊 URL 退場:`mcp.finally.click` 不保留(要嘛 410 Gone,要嘛 301 redirect 到 arcrun.dev landing 一個說明頁)
- Tool 名一次改:所有 `u6u_*` rename 成 `arcrun_*`**舊名直接消失**
- repo 路徑改:`matrix/u6u-mcp/``matrix/arcrun-mcp/`
### 6.2 切換前須做
- [ ] 全 monorepo `grep u6u_` 確認所有 clientmira / 自家腳本 / leo 自己的 IDE 配置)
- [ ] 公告:在 mira 河道 + telegram 通知「ak_xxx 用戶請更新 MCP 配置:URL → mcp.arcrun.dev」
- [ ] 切換當天 worker 部署兩個 name(過渡 1 天可回滾),確認流量切完才把舊 worker disable
### 6.3 為什麼不做 deprecation
- 用戶量極少(dogfood 階段,主要是 leo 自己)
- 留 alias 會讓新 AI agent 學到舊名,違背「LI 一致性」原則
- 90 天監控成本 > 一次切換 + 公告
### 6.2 新增 tools(按 phase
Phase 1gap-fill):
- `arcrun_validate_yaml`(既有 `/validate` 已存,包 MCP 即可)
- `arcrun_get_execution_trace`(需 cypher-executor 新加 `/executions/:id`
- `arcrun_list_recent_executions`(新 endpoint
- `arcrun_list_paused_executions`(新 endpoint
- `arcrun_report_feedback`(新 tool
- `arcrun_get_onboarding`KBDB read
Phase 2advanced):
- `arcrun_preview_workflow`(新 endpoint,沒 KV side-effect
- `arcrun_diff_workflow`
- `arcrun_my_telemetry`
- `arcrun_search_examples`KBDB write + search 已存)
- `arcrun_list_skills` / `arcrun_get_skill` / `arcrun_publish_skill`
Phase 3auto-loop):
- `agent_feedback_weekly_review` workflow
- `arcrun-roadmap` block 生成
- LLM extract skill 自動化
---
## 7. 開發順序與里程碑
### Milestone 1:可量測(1 週)
- 寫 AGENTS.md v1
- 加 implicit telemetry 寫入點(cypher-executor
- 加 `arcrun_report_feedback` MCP tool
- 建 `agent-feedback` / `agent-telemetry` KBDB template
- 開始收 data
### Milestone 2gap-fill1 週)
- arcrun-mcp 補上 6 個 Phase 1 tools
- 新 endpoints 在 cypher-executor 加
- 每個 tool 結構化 error contract
### Milestone 3skill + example1 週)
- 種子 5 個 skill blocks
- 種子 10 個 example workflows
- `search_examples` 跑通
### Milestone 4closed loop(半週)
- weekly_review workflow 部署
- 第一份 arcrun-roadmap block 產出
- leo 收到第一份週報
### Milestone 5rename + cleanup1 週)
- arcrun-mcp → arcrun-mcp 公開
- 舊 tool 加 deprecation warning
- AGENTS.md 同步 KBDB
---
## 8. 決策紀錄
### 8.1 已拍板(2026-05-16 leo
| 決策 | 結果 | 理由 |
|---|---|---|
| MCP server URL | `mcp.arcrun.dev` 單一 URL,舊 `mcp.finally.click` 直接退場 | finally.click 是還沒申請新網址的暫用,那是一個服務,arcrun 是底層 |
| u6u branding | 整體退場改 arcrunrepo / worker / tool 命名一次 rename | u6u 不存在了 |
| Deprecation 期 | 不留(一次切換 + 公告) | 用戶量極少,留 alias 反而讓新 AI 學到舊名 |
| 擴張 vs 建新 | 擴張既有 u6u-mcprename 成 arcrun-mcp | 不 fork,零移轉痛 |
| AGENTS.md 位置 | repo `arcrun/AGENTS.md` + 自動同步 KBDB block | 兩面都拿到(git-access 也 OK,純 MCP 也 OK |
| feedback 寫入 auth | 要(驗 ak_ 存在即可,不查餘額) | 防 spam |
### 8.2 仍可商議(小議題)
| 議題 | 建議 |
|---|---|
| LI 是否包含 KBDB MCP | 部分(abstracted),KBDB MCP 另立 SDD `kbdb-llm-interface` |
| Telemetry 保留多久 | 90 天 hot + 1 年 cold archive |
| AGENTS.md 第一版用中文還是英文 | 中文(leo 自家 + mira 一致),英文版 v2 開源時補 |
---
## 9. 跨 SDD 連動
| 其他 SDD | 連動點 |
|---|---|
| `credential-primitives-wasm` | LI 不重做 auth,只用 `auth_static_key` 等既有零件 |
| `recipe-system` | LI 暴露 recipe CRUD tool 對應 |
| `component-registry-canon` | LI `list_components` / `get_contract` 走 registry |
| `resumable-workflow` | LI `list_paused_executions` / `resume` 用 |
| `arcrun-platform-evolution` | LI 是 evolution 之一,未來分 user-tier 時要考慮 |
| **mira-app** (polaris/mira) | LI 是 mira dogfood 痛點轉化的產物,roadmap 含對 mira 的回饋 |
-246
View File
@@ -1,246 +0,0 @@
# Requirements: LI (LLM Interface) for arcrun
> 把 arcrun 平台對「AI 操盤手」的完整使用面,當成 first-class product 設計。
> 對比:以前做網站要 user-friendlyUI),現在 AI 是主要用戶(LI)。
---
## 背景
arcrun 是 n8n-like workflow 平台,平台本身的 end-user 有兩類:
- **人類**:透過 arcrun-gui canvas / arcrun.dev landing 操作
- **AI agent**:透過 MCP / API 操作(Claude Code、Cursor、Codex、自製 agent 等)
過去設計集中在「人」(UI / docs)。AI 對 arcrun 的「可用性」沒被當第一公民。
### 證據:3 天 mira dogfood 累積的 14 個痛點(baseline
| 編號 | 痛點 | 性質 |
|---|---|---|
| 1 | 新零件 deploy 要手動 dashboard 點 workers.dev | platform DX |
| 2 | CF 同 zone self-fetch 死鎖 | platform infra |
| 3 | `http_request` 4xx 回 success=true cascade | error semantics |
| 4 | interpolateString array stringified | dev experience |
| 5 | `kbdb_upsert_block` 沒在 `WASM_HTTP_RUNNER_IDS` 白名單 | discovery |
| 6 | `resumeFromPaused` paused_node_id 沒 namespace | implicit behavior |
| 7 | `acr validate` 不認「對每個 X」 | validator stale |
| 8 | cron 從零寫起 | feature gap |
| 9 | CF Pages 沒接 auto-deploy | infra setup |
| 10 | watcher self-fetch 死鎖(剛解) | architecture |
| 11 | skill 改要寫 python script | tooling gap |
| 12 | 不知道有什麼零件可用 | **LI gap** |
| 13 | 不知道現有 workflow 怎麼長 | **LI gap** |
| 14 | trace / log 看不到結構化的 | **LI gap** |
**14 個裡有 7 個(編號 3-7, 12-14)純粹是 LI 缺失**。如果 LI 完整,等於把 50% 的踩坑時間還給開發者。
---
## 目標用戶(personas
### P1Claude Code / Cursor 等 IDE-embedded AI(主力)
特徵:
- 跟 leo 一起 pair programming
- 有 file access、shell 能力
- 透過 MCP / curl / SDK 跟 arcrun 對話
需求:
- 知道 arcrun 全貌(不靠人類解釋)
- 知道目前用戶有什麼資產(workflow / component / template / credential
- 寫 YAML 不靠猜
- 部署前能 dry-run
- 部署後能看執行結果
- 卡住能 introspect + ask
### P2:用戶私人 agent(次要,未來)
特徵:
- 在 mira / 用戶 app 內跑
- 透過 MCP 對 arcrun
- 沒有檔案系統,只有 HTTP
需求:基本同 P1,但**完全靠 MCP**(沒 grep / file read fallback
### P3:自製腳本 / SDK 使用者
不在本 SDD 範圍。SDK 既有規格走 `.agents/specs/arcrun/sdk-and-website/`
---
## 範圍(系統涵蓋)
LI 不是「新建一個 service」,是**跨 5 個既有系統的橫向 layer**:
| 系統 | 既有狀態 | LI 涵蓋的部分 |
|---|---|---|
| **arcrun cypher-executor** | 31 HTTP 路由 | 哪些對外、哪些 AI 該看得到 |
| **arcrun registry** | component 管理 Worker | discovery layer |
| **arcrun-mcp**(目前 `matrix/u6u-mcp/`,本 SDD M5 改名) | 15 MCP toolsHTTP→MCP 薄包裝) | 主擴張面(gap-fill) |
| **arcrun-gui**(目前 `matrix/u6u-gui/`,本 SDD M5 改名) | 人類 canvas IDE | 取其 endpoint 觀念,不取其 UI 元素 |
| **kbdb** | 50 個 HTTP 路由 | KBDB 該不該直接給 AI、用什麼姿勢 |
| **arcrun CLI (acr)** | Node CLI | LI 不依賴 CLICLI 是人的工具) |
---
## 非範圍(這個 SDD 不處理)
- 不處理 GUI 設計 / 視覺
- 不處理 SDKPython/JSAPI 設計
- 不處理 user OAuth flow
- 不重新設計 KBDB schema(只決定 LI 該包什麼 API)
- 不重新設計 cypher binding 語法
---
## 功能需求(FR
### FR-1AI 一條指令就能上手 arcrun
- 提供 `AGENTS.md`onboarding doc),AI 載入就能用
- MCP server URL 寫進 doc,「直接 connect」一步搞定
- 不需讀 SDD / 不需 grep codebase / 不需問人
### FR-2:完整 CRUD + Discovery 對等
人類在 arcrun-gui 能做的 8 個動作(list_workflows / get / update / execute / search_components / get_component / list_templates / list_credentials),**MCP 至少同等覆蓋**,不能有「人類能做但 AI 不能做」的 gap。
### FR-3Dry-run 是預設行為
- `validate_workflow(yaml)` MCP tool(取既有 `/validate` 路由)
- `preview_workflow(yaml, input)` 不寫入 KV,模擬執行
- AI 養成習慣「先 dry-run 再 push」
### FR-4:可診斷的 trace
- `get_execution_trace(execution_id)` 回結構化 JSON:每個 node 的 status / input / output / error / duration
- `list_paused_executions()` 列卡住的執行(callback 沒回時 debug 用)
- 不靠 `wrangler tail` 純文字
### FR-5:錯誤訊息是「給 AI 看的下一步」
所有 MCP tool error response 必須含:
- `error_code`(穩定字串 enum,可程式化 catch)
- `human_message`(描述)
- `next_actions`(陣列,可選:「call X」/「執行 Y」/「修改 Z」)
範例:
```json
{
"error_code": "component_not_in_whitelist",
"human_message": "零件 'filter' 不在用戶可用清單",
"next_actions": [
"call list_components() 看完整可用清單",
"若該零件需平台啟用,告訴用戶執行 `acr enable filter`",
"若是自製零件,先 push 零件再 push workflow"
]
}
```
### FR-6AI 可以回報問題
- `report_feedback(workflow_name, issue_type, description, ...)` MCP tool
- 結構化 enum issue_type(防自由文字難聚合)
- 寫進 KBDB 成 `agent-feedback` block,可被定期 review
- AI 規範「順利 / 卡住 / 不確定 都該 call」(透過 AGENTS.md 強制)
### FR-7Implicit telemetry 不依賴 AI 自覺
- cypher-executor 每次 deploy / execute / fail 自動寫 `agent-telemetry` block
- 含 client_user_agent(哪個 AI 用的)、error_message、duration
- 不依賴 AI 主動 call,平台自己收
### FR-8Skill blocks 可重用
- 「pattern / playbook」存 KBDB type=`agent-skill`
- MCP `list_skills(tag)` / `get_skill(id)` 給 AI 查
- 範例:`build_watcher_workflow` / `debug_paused_workflow` / `migrate_http_to_trigger_workflow`
### FR-9:範例庫可搜尋
- `search_examples(use_case)` 給 use case → 回相似 workflow YAML
- 範例存 `registry/examples/` git + 一份 index 在 KV
- 進階:semantic search(既有 KBDB `/search/embed`
### FR-10:定期 review 機制(自動化)
- `agent_feedback_weekly_review` workflow**arcrun 自己跑**
- 每週一聚合 feedback + telemetry,產出 Top N 痛點 + 建議
- 寫進 `arcrun-roadmap` KBDB block
---
## 非功能需求(NFR
### NFR-1:向下相容
LI 在「擴張」階段(M1-M4)不破壞既有:
- M1-M4 階段:arcrun-mcp 既有 15 tool 保留,新增 13 個 toolpre-rename
- arcrun-gui 不動
- cypher-executor 既有路由不改 contract(只加新 endpoint
M5rename)階段是 breaking changeleo 拍板),需事前公告。
### NFR-2transport 不鎖死
MCP 主,但 HTTP 同等可用(讓沒 MCP 客戶端的 agent 也可用)。每個 MCP tool 都對應一個 HTTP endpoint。
### NFR-3error contract 穩定
`error_code` enum 是 public API,加新值是 minor,移除值是 major。版本化。
### NFR-4feedback 數據可外部 export
`agent-feedback` / `agent-telemetry` 走 KBDB `/blocks` 標準 API,任何 AI / 人都能拉。不鎖死在 dashboard。
### NFR-5:覆蓋率可量化
- 「人類在 arcrun-gui 能做但 MCP 不能做」清單必須能列出
- 每次 LI 改動後,這份清單往 0 收斂
---
## 成功指標
| 指標 | 量測 | 目標 |
|---|---|---|
| 新 AI agent 上手時間(從 zero 到第一個 workflow 部署) | 手動實驗 | < 10 min |
| 一次 workflow 部署需要的 MCP call 次數 | telemetry | < 5 calls |
| AI 回報「文件不清楚」週次數 | feedback aggregation | 持續下降 |
| 「MCP 缺工具」issue 類型佔比 | feedback type ratio | < 10% |
| Implicit telemetry 與 explicit feedback 比例 | 計數 | 9:1 不算問題(implicit 量大正常) |
| Coverage gap(人類能做 vs AI 能做) | 手動審查 | 0 |
---
## 風險與假設
### 假設
- MCP 是主要 AI 接觸點(不是 SDK / CLI
- AI 願意按 AGENTS.md 規範 call feedback tool
- KBDB 能承載 telemetry 量級(短期:< 1000 events / day
### 風險
| 風險 | 應對 |
|---|---|
| AI 不照 AGENTS.md 規範用 | implicit telemetry 不依賴自覺,仍能收 data |
| MCP tool 設計錯誤越改越亂 | 每個 tool 有 contract testerror_code enum 版本化 |
| feedback 雜訊太多無法 review | review workflow 用 LLM 聚合,不靠人讀原始 |
| arcrun-mcp 重構搞壞既有用戶 | 用戶量還很少(dogfood 階段),一次切換可接受;切換前公告 |
| KBDB telemetry 量爆掉 | sample rate / 老資料自動 archive |
---
## 決策(2026-05-16 leo 拍板)
1. **deployment 名稱**:✅ `mcp.arcrun.dev` 單一 URL
- `mcp.finally.click` 直接退場,不並存
- 理由(leo):「finally.click 是還沒申請新網址的暫用,那是一個服務,arcrun 是底層」
2. **u6u branding 整體退場**:✅ u6u 命名全部改 arcrun
- `u6u-mcp` repo → `arcrun-mcp`
- `u6u-gui` repo → `arcrun-gui`
- 工具命名 `u6u_*``arcrun_*`**單一 rename,不留 alias 也不留 deprecation 期**
- 理由:「u6u 不存在,改成 Arcrun」
3. **AGENTS.md 位置**:✅ repo `arcrun/AGENTS.md` + 自動同步 KBDB block(兩面都拿到)
4. **feedback 寫入需 auth**:✅ 驗 ak_ 存在即可(防 spam,tier 低不查餘額)
-272
View File
@@ -1,272 +0,0 @@
# Tasks: LI (LLM Interface) for arcrun
> SDD: design.md + requirements.md(同目錄)
> 進度標記:`[ ]` pending / `[🔄]` doing / `[x]` done / `[⏸]` blocked
## 進度速覽(2026-05-16
- **M1 完成**AGENTS.md / telemetry helper / report_feedback tool 全部 deploy + e2e 驗證 ✅
- **M2.1 完成**3 個 introspection endpoints + index 強 consistent 修補 ✅
- **M2.2 部分**4 個 introspection + 5 個 CRUD = 9/13 tools,剩下 preview/diff/auth-recipes
- **M3.1/M3.3 完成**5 個 skill blocks + 10 個 example workflows ✅
- **M3.4 完成**sync-registry-to-kbdb.py 跑通,15 blocks 進 KBDB ✅
- **M4 完成**weekly_review workflow 跑通,產出第一份 arcrun-roadmap block ✅
- **M5 大 rename**repo / dir / SDD 已 renameWorker name 待後段 DNS 遷移
阻擋項:GH Actions 用戶層被 disableleo 申訴中)→ 改用本機 wrangler deploy + scripts/local-deploy.sh fallback。
---
## Milestone 1:可量測(先收 data)✅
### M1.1 AGENTS.md v1
- [x] 寫 `arcrun/AGENTS.md`5697355 + 3892dc3263 行)
- [ ] CI hookrepo `AGENTS.md` 變動 → 自動同步 KBDB block
- [ ] `arcrun_get_onboarding` MCP tool(讀 KBDB block
### M1.2 Implicit telemetry 收集 ✅
- [x] 建 KBDB block type=`agent-telemetry`slots 直接 metadata_json 不走 template
- [x] `webhook-handlers.executeWebhookGraph` 末尾加 telemetry(成功 / 失敗 / paused 都記)
- [x] `routes/webhooks-named.ts` push deploy 事件(deploy_success
- [x] `routes/validate.ts` validation 失敗事件(schema_failed / edge_node_missing
- [x] `hashApiKey` SHA-256 截 16 字元 helper
- [x] 隱私:只記 workflow name 不記 content
- [x] 實測:KBDB block `68635dcb-62e5-49ca-9c67-33f4ca82b7a0` event=run_success, paused_awaiting_resume
### M1.3 Explicit feedback tool ✅
- [x] KBDB block type=`agent-feedback`
- [x] arcrun-mcp tool `arcrun_report_feedback` (commit e637c3e)
- [x] Zod enum 鎖死 issue_type
- [x] user_id 從 partnerAuth 取
- [x] tags_json auto: ['agent-feedback', 'issue:{type}', 'blocked'?, 'wf:{name}'?]
- [x] schema 實測:KBDB block `80f1d2d1-c95a-4dfe-a889-d23b2e9b247d`
### M1.4 驗收 ✅
- [x] 觸發 mira watcher → KBDB agent-telemetry 即時出現
- [x] curl + python verify 8 個 telemetry blocksevent=run_success, workflow_name 對, duration_ms 對)
- [x] feedback block 寫入測 schema 通
---
## Milestone 2gap-fill(補 MCP 工具)
目標:人類 GUI 能做的,AI 透過 MCP 都能做。
### M2.1 新增 cypher-executor 路由 ✅
- [x] `GET /executions/:task_id` — 回結構化 paused state (989fbeb)
- [x] `GET /workflows/:name/executions?limit=10` — 走 ANALYTICS_KV stats:* prefix (989fbeb)
- [x] `GET /executions/paused` — 改 per-user index 強 consistent (4e7880c)
- [ ] `POST /preview` — dry-run,不寫 KV(暫緩)
- [ ] `POST /webhooks/named/:name/diff` — 新舊 YAML diff(暫緩)
- [ ] `GET /my-telemetry?limit=N` — 用戶自己看 telemetry(暫緩)
### M2.2 MCP tools ✅ (9/13)
完成(commit faf75cd + f91b1fd):
- [x] `arcrun_validate_yaml` — wrap /validate
- [x] `arcrun_get_execution_trace`
- [x] `arcrun_list_recent_executions`
- [x] `arcrun_list_paused_executions`
- [x] `arcrun_push_workflow` — wrap /webhooks/named POST(取代壞掉的 u6u_deploy_workflow
- [x] `arcrun_list_workflows`
- [x] `arcrun_get_workflow`
- [x] `arcrun_delete_workflow` (require confirm:true literal)
- [x] `arcrun_run_workflow` (paused 視為 success)
暫緩(等 endpoint 完成):
- [ ] `arcrun_resume_execution` — 包既有 /workflows/resume
- [ ] `arcrun_preview_workflow` — 待 M2.1 /preview
- [ ] `arcrun_diff_workflow` — 待 M2.1 diff
- [ ] `arcrun_list_recipes` / `create_recipe`
- [ ] `arcrun_list_auth_recipes` / `create_auth_recipe`
- [ ] `arcrun_my_telemetry`
### M2.3 Error contract 統一 ✅
- [x] `error_code` enum v1 定義在 design.md §1.4 + cypher-executor /executions/* 路由都用
- [x] arcrun-mcp `lib/cypher-client.ts` errorResponse() / successResponse() 統一 helper
- [x] 所有新 MCP tool10 個)都用統一 contractok, data?, error_code?, human_message?, next_actions?, hints?
- [ ] cypher-executor 既有 route(非 /executions/*)改成統一格式(暫緩)
- [ ] 每個 error_code 對應 unit test(暫緩)
### M2.4 驗收(部分)
- [ ] 模擬 zero-knowledge AI 跑 hello workflow(待 leo 提供 pk_live
- [ ] 量測:from list_components 到 run_workflow 成功 MCP call < 5
- [ ] 比較人類 GUI 路徑,clickwise 對等
---
## Milestone 3skill blocks + examples ✅
目標:AI 寫第一個 workflow 不靠猜,有範本和 playbook。
### M3.1 種子 skill blocks ✅ (commit 388c193)
- [x] `skill-build_watcher_workflow` — cron + 過濾 + trigger 模式
- [x] `skill-debug_paused_workflow` — claude_api callback 流程 + 怎麼追
- [x] `skill-migrate_http_to_trigger_workflow` — 從 self-fetch 換 trigger_workflow
- [x] `skill-rag_with_arcrun` — KBDB search + claude_api 組裝
- [x] `skill-add_new_wasm_component` — TinyGo 寫 + push + 註冊白名單
### M3.2 MCP tools(暫緩,待 M5
- [ ] `arcrun_list_skills(tag?)`
- [ ] `arcrun_get_skill(id)`
- [ ] `arcrun_publish_skill` — AI 把學到的回存
### 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 索引 + 搜尋 ✅ (commit 37379b7)
- [x] scripts/sync-registry-to-kbdb.py — 把 registry/examples + skills 同步進 KBDB
- 走 kbdb-upsert-block.arcrun.dev (idempotentpage_name 為 key)
- examples → type=workflow-example, page_name=example-{slug}
- skills → type=agent-skill, page_name=skill-{slug}
- 實測 15 blocks created → 第二次 sync 全 PATCH 成功 (idempotent)
- [ ] `arcrun_search_examples(use_case)` MCP tool(待 M2.x 補)
---
## Milestone 4closed loop ✅
目標:data 收得到 → 平台自己消化產出 roadmap。
### M4.1 Weekly review workflow ✅ (mira commit de11625)
- [x] 寫 `polaris/mira/arcrun/agent_feedback_weekly_review.yaml`
- [x] cron `0 9 * * 1` (台灣 17:00 週一)
- [x] `acr push` 部署
- [x] 手動觸發測試一次(5/6 nodes success,唯一 fail 是 notify_leo 缺 credential
### M4.2 LLM 聚合 prompt ✅
- [x] prompt 結構化:數字 / Top 5 痛點(含證據 / 嚴重度)/ 成功 pattern / 下週優先 3 件
- [x] 一律繁體中文 + 引用 block_id 為證據
- [x] 存 KBDB type=`arcrun-roadmap`, page_name=roadmap-latest(每週覆蓋)
- [x] 實測產出真有用:抓到「paused_awaiting_resume 語意不清」「data 量太少」「自動建議包 skill」三個真實 LI 改進建議
### M4.3 通知
- [x] notify_leo 節點:telegram chat_id from secret
- [ ] leo 補 telegram_bot_token credential 後生效
- [ ] 同時寫進 mira 河道(讓 leo 在熟悉介面看)— 暫緩
### M4.4 驗收
- [x] 第一次手動觸發 → 收到第一份 roadmap (KBDB block id e924c231-cf5e-4541-89d8-da550ecae2f3)
- [ ] cron 自動跑首次(下週一驗證)
- [ ] leo review 後挑 1-2 個 issue 修補
- [ ] 跑第二週 → 確認該 issue 從 top list 消失
---
## Milestone 5rename + cleanupu6u → arcrun 一次切換)
目標:完成 LI 品牌化,u6u branding 整體退場。**leo 2026-05-16 拍板:單一 rename,不留 alias 也不留 deprecation 期**。
### M5.1 切換前準備
- [ ] 全 monorepo `grep -rn "u6u_\|mcp\.finally\.click"` 列出所有受影響檔案 + 用戶配置
- [ ] mira 河道 + telegram 公告:「ak_xxx 用戶請更新 MCP 配置 URL → mcp.arcrun.dev」(**至少切換前 24h**
- [ ] 列 leo 自己的 IDEClaude Code / Cursor)配置位置,準備同步更新
### M5.2 Repo / Worker rename
- [ ] `matrix/u6u-mcp/``matrix/arcrun-mcp/` (git mv)
- [ ] `matrix/u6u-gui/``matrix/arcrun-gui/` (git mv)
- [ ] `arcrun-mcp/wrangler.toml`: name = `arcrun-mcp`
- [ ] `arcrun-gui/wrangler.toml`: name = `arcrun-gui`
- [ ] DNS`mcp.arcrun.dev` route 接到 arcrun-mcp worker
- [ ] CIdeploy.yml)若有寫死 path 同步改
### M5.3 Tool rename(一次切換)
- [ ] 所有 `u6u_*` MCP tool 改 `arcrun_*`(不留 alias
- [ ] AGENTS.md 全用新名
- [ ] `arcrun-mcp/README.md` / `GUIDE.md` 全部用新名
### M5.4 舊 URL 退場
- [ ] `mcp.finally.click` 接 410 Gone + 訊息「請改用 mcp.arcrun.dev」
- 或 301 redirect 到 arcrun.dev landing 一個說明頁
- [ ] DNS 紀錄保留 30 天(防意外 client 還沒切)後刪除
### M5.5 文件最終化
- [ ] `arcrun/AGENTS.md` 最終版發布
- [ ] `matrix/arcrun/.agents/specs/llm-interface/design.md` 加「實際部署狀態」附錄
- [ ] 寫一篇 retrospectiveLI 做完前後 AI 使用 arcrun 的 time-to-first-workflow 對比
### M5.6 連動(不在本 SDD 範圍但要追蹤)
- [ ] `matrix/arcrun/.agents/specs/arcrun-core-mvp/` SDD 改名(另立 task,跨 SDD rename
- [ ] `matrix/arcrun/.agents/specs/arcrun-platform-evolution/` 同上
- [ ] 兩個 SDD 重命名屬「u6u 品牌退場」系列,需要單獨追蹤 task
---
## Backlog(暫不排)
### B.1 KBDB MCP 獨立 SDD
- LI 範圍只包 KBDB 的 `agent-*` template
- 完整 KBDB AI 介面(type=note/page/triplet/template/record 等)另立 SDD `kbdb-llm-interface`
- 跟 mira KM 系統互動最密
### B.2 多 agent 隔離
- 多 AI 共用同 ak_xxx 時,telemetry 區隔 agent_user_agent
- 進階:每個 AI 子 namespacemira / cursor / 自製 agent
### B.3 AGENTS.md i18n
- v1 純中文(leo + 自家用)
- v2 英文版(給開源用戶)
### B.4 自動 skill 萃取
- weekly_review 產出的 pattern 自動包成 skill draft
- leo review approve → publish
### B.5 SDK 對等(python-sdk / js-sdk
- SDK 提供和 MCP 同樣的 25 個 method
- 給「不想用 MCP 的人」也能 AI-friendly
- 走 sdk-and-website SDD 範圍
### B.6 GUI side 補 LI 看板
- arcrun-gui 加 `/li-dashboard` 顯示用戶自己的 telemetry / feedback
- 不阻擋 LI 推出(leo 先看 KBDB 原始 block 即可)
---
## 依賴關係
```
M1 (data 收集)
M2 (MCP gap-fill)
M3 (skill + examples) ← 可與 M2 並行後段
M4 (closed loop) ←─── 需 M1 data 累積 1-2 週
M5 (rename)
```
---
## 工估算
| Milestone | 工 | 阻擋項 |
|---|---|---|
| M1 | 5 個工作日 | 無 |
| M2 | 5 個工作日 | M1 完(telemetry 先就位才好驗證 M2 改動) |
| M3 | 5 個工作日 | M2 完(tool 介面定型才寫 skill |
| M4 | 3 個工作日 | M1 data 累積 1 週 |
| M5 | 5 個工作日 | M2-M4 完 |
| **總** | **23 個工作日 (~5 週)** | |
實際視 leo 排程,可邊用邊改、不必一氣呵成。**M1 是硬前置**——資料不收,改了也不知道改對沒。
-240
View File
@@ -1,240 +0,0 @@
# SDD: arcrun Recipe System(容器 + Recipe 模式)
> 2026-05-07 建立。吃狗糧寫 wiki 合成 workflow 時撞牆發現的平台缺口。
> 核心原則:**一個 WASM 零件 = 容器,內容(recipe)存資料庫**。
> n8n 為每種 API 寫獨立 nodearcrun 走「容器 + recipe」減少零件數量。
---
## 1. 問題
### 1.1 撞牆現場
寫 mira wiki 合成 workflow7-B)時:
- 流程:`kbdb_get(stale)` → foreach → `kbdb_get(drafts)``claude_api(合成 prompt)``kbdb_ingest`
- 第三步要組 prompt`schema 內容 + skill 模板 + drafts array + existing_entities`
- cypher binding 內建 `{{var}}` 模板太弱(只支援 top-level,不支援嵌套 / array → string
- 沒有 `string_template` 零件、沒有 `array_to_markdown` 零件
- 寫專用 `wiki_prompt_builder` 零件 = 走 n8n 老路,每個 AI workflow 都要寫一個
### 1.2 根因
**arcrun recipe 系統只覆蓋 HTTP / auth 兩層**
| Recipe 種類 | 存哪 | 容器 | 狀態 |
|---|---|---|---|
| auth_recipe | RECIPES KV (`auth_recipe:{service}`) | auth_static_key / auth_oauth2 / ... | ✅ 已有 |
| api_recipe | RECIPES KV (`rec_{hash}`) | http_request | ✅ 已有(hard-code 在 cypher-executor 待清,Phase 1-3 處理)|
| **prompt_recipe** | ❌ 不存在 | claude_api(容器) | **缺** |
`claude_api` 零件目前吃 `prompt: string`(已組好的字串),沒有「recipe 模式」可以讓 AI 用「組合配方」的方式呼叫。
### 1.3 影響
- **致命**:寫不出第一個 wiki 合成 workflow7-B 卡關)
- **推廣破功**arcrun 對外 prop 是「容器 + recipeAI 不用寫 code」,但 prompt 這層做不到
- **未來所有 AI workflow 都會撞同樣問題**rss-tech-news 評語、河道 AI 副駕、ai-comment、文章摘要⋯ 全部需要組 prompt
---
## 2. 設計
### 2.1 核心:prompt_recipe 平行於 auth_recipe / api_recipe
**儲存**`RECIPES` KVkey 格式 `prompt_recipe:{name}`
**結構**
```yaml
id: prompt_recipe:wiki_synthesis
version: v1
description: "Mira wiki 合成(抽 triplet + 寫 wiki paragraph"
model: sonnet # haiku / sonnet / opusclaude_api 沿用既有 routing
# 從 KBDB / 其他來源取的 fragment(在 prompt 組合時抓並插入)
fragments:
- var: schema
source: kbdb_block
block_id: "7a4e456e-1b0f-406a-8842-5e01d1cf1eef" # mira-wiki-schema
field: content
- var: skill_template
source: kbdb_block
block_id: "85e3b81e-dca8-4131-bcdc-990bd0d3a16f" # source-skill-wiki-synthesis
field: content
# 從 workflow context 取(input/前置節點輸出)
inputs:
- var: drafts # 草稿 array
from: "ctx.read_drafts.blocks"
transform: "json_array" # 轉成 JSON array string
- var: existing_entities
from: "ctx.read_entities.blocks"
transform: "extract_field:page_name" # 抽 array 的 page_name 欄位 join 成 list
- var: entity_name
from: "ctx.loop.item" # foreach 迴圈當前元素
# 最終 prompt 由 fragments + inputs 套進 skill_template 組成
prompt_assembly:
system: "{{schema}}" # 直接用 schema 當 system prompt
user: "{{skill_template}}" # skill template 內含 {{drafts}} {{existing_entities}} {{entity_name}} 變數
# 期待輸出
output:
format: json # claude_api 自動 parse 為 object
schema: # zod-styleparse 失敗回 success:false
type: object
required: [triplets, entities, paragraphs, source_summary]
```
### 2.2 Recipe 解析在 cypher-executor(架構選擇 B
**設計決策**2026-05-07):recipe 解析跟 prompt 組裝**在 cypher-executor TS**,不改既有 claude_api WASM。
理由:
1. recipe 解析是 cypher-executor 既有 `api_recipe / auth_recipe` 同性質工作
2. 既有 claude_api 已部署 + 已測試,不動影響面最小
3. transform 邏輯(json_array / extract_field 等)TS 寫起來比 TinyGo 簡單 10 倍
4. 不違反 §1.6 — skill 還是 KBDB blockcypher-executor 只是組合者,不寫死 prompt
**流程:**
```
workflow YAML 節點 config 出現 `recipe: prompt_recipe:xxx`
cypher-executor graph-executor.ts
在執行該節點前 → 偵測 recipe 欄位 → 走 recipe expander
recipe expander(新 module
1. 從 RECIPES KV 抓 `prompt_recipe:xxx` 定義
2. 按 fragments 規則 → 用既有 KBDB client 抓 block content
3. 按 inputs 規則 → 從 context 取值 + 跑 transform
4. 組 system prompt + user prompt
5. 把 {prompt, model, mira_token, ...} 當作節點實際 input
loader 呼叫 claude_api 容器(不知道 recipe 存在,仍吃舊介面)
claude_api 容器 → Mira daemon → 回 LLM 結果
graph-executor 取結果 → 按 recipe.output 規則 parse JSON / 驗 schema
```
**對 claude_api 容器的影響**:完全沒有。它仍吃 `{mira_token, prompt, model}`
**對 workflow 作者的體驗**
```yaml
config:
synthesize:
component: claude_api
recipe: "prompt_recipe:wiki_synthesis" # ← cypher-executor 偵測到這欄位,自動解析
mira_token: "{{secret.mira_token}}"
```
不寫 recipe 走舊路:
```yaml
config:
reply:
component: claude_api
prompt: "{{ctx.user_message}}" # ← 沒 recipecypher-executor 直接透傳
mira_token: "{{secret.mira_token}}"
```
### 2.3 Workflow YAML 體驗
```yaml
name: wiki_synthesis
flow:
- "input >> 完成後 >> read_stale"
- "read_stale >> 對每個 >> read_drafts"
- "read_drafts >> 完成後 >> synthesize"
- "synthesize >> 完成後 >> write_wiki"
config:
read_stale:
component: kbdb_get
page_name: "mira-wiki-index-stale"
read_drafts:
component: kbdb_get
page_name: "{{loop.item}}" # entity name
synthesize:
component: claude_api
recipe: "prompt_recipe:wiki_synthesis" # ← 重點:指 recipe,不寫 prompt
mira_token: "{{secret.mira_token}}"
write_wiki:
component: kbdb_ingest
text: "{{prev.paragraphs}}"
```
**AI 寫這 workflow 只需要:**
1. 知道有 `kbdb_get / claude_api / kbdb_ingest` 三個容器(MCP search 找得到)
2. 知道有 `prompt_recipe:wiki_synthesis` 這個配方(MCP search 找得到)
3. 不需要懂 prompt 怎麼組、不需要看 wiki schema 文字
### 2.4 Recipe 是 KBDB block 還是 KV
**選 KV**`RECIPES` namespace),跟既有 auth_recipe / api_recipe 一致:
- key: `prompt_recipe:{name}`
- value: YAML/JSON
- CLI 跟 MCP 用既有 `recipe push` / `recipe list` 工具管理(不需新工具)
**不選 KBDB block**
- 雖然 polaris/mira/CLAUDE.md §1.6 說「source-skill 存 KBDB block」
- 但 §1.6 講的是 mira 業務的 skill templateschema / skill 模板)
- recipe 是「組合配方」(指向哪些 block + 怎麼組),是 platform 層
- recipe **裡面** 引用 KBDB block idfragments.source: kbdb_block)— 兩層關係清楚
---
## 3. 範圍邊界
**在本 SDD 範圍內:**
- ✅ Phase 1: prompt_recipe schema + RECIPES KV 規範
- ✅ Phase 2: claude_api 改吃 recipe(向後相容舊 prompt 參數)
- ✅ Phase 3: 寫第一個 recipe `prompt_recipe:wiki_synthesis`
- ✅ Phase 4: 用此 recipe 完成 mira 7-B workflow
- ✅ Phase 5: MCP 加 recipe 管理 toollist / get / push / delete prompt_recipe
**不在範圍內:**
- HTTP api_recipe / auth_recipe 改造(已有,不動)
- 多模態 promptimage input)— 等 P2
- recipe 沙盒驗收(recipe 是資料不是 code,不需要)
**前置依賴(已完成):**
- ✅ kbdb_get 零件(5.3
- ✅ component-registry MCP backfillcomponent-registry-canon Phase 1
---
## 4. 為什麼這個設計重要
| n8n | arcrun |
|---|---|
| Gmail node、Slack node、OpenAI node、Anthropic node、各 LLM node ⋯(每種 API 一個 node| `http_request` 容器 + 各 service 的 api_recipe |
| 每個 LLM 用法新 nodechat / completion / embedding| `claude_api` 容器 + 各用途的 prompt_recipe |
| AI 要學「Gmail node 怎麼用」「Slack node 怎麼用」⋯ | AI 要學「容器 + 配方」一次學會 |
| 零件數爆炸(500+) | 容器固定(< 30),配方無限擴充 |
| 配方藏在程式碼 | 配方在 KV,AI 直接 CRUD |
**對 AI 推廣**:第三方 AI 看到「30 個容器 + 100 個配方」遠比「500 個 node」好理解,且配方是文字資料不是 code,AI 寫配方比寫 node 簡單。
---
## 5. 風險與緩解
| 風險 | 緩解 |
|---|---|
| recipe 結構過度複雜,AI 寫不出來 | Phase 3 寫第一個 recipewiki_synthesis)作為範本,未來 AI 抄 |
| 向後相容讓 claude_api 變兩條路 | 內部統一用 recipe path,舊 prompt 參數 → 自動轉成 inline recipe |
| recipe 引用 KBDB block id 寫死,block 改 id 就壞 | KBDB block 用 `page_name` 識別比 id 穩定,recipe 支援 `block_page_name` 欄位 |
| KV 寫入頻繁的 transform 邏輯(json_array, extract_field:x)→ 變 mini DSL | 限制 transform 種類(10 個內),列白名單,超過就請寫零件 |
---
## 6. 變更紀錄
| 版本 | 日期 | 內容 |
|---|---|---|
| v1.0 | 2026-05-07 | 初版。吃狗糧寫 wiki 合成 workflow 撞到「prompt 組裝缺口」,補 prompt_recipe 層平行於既有 auth_recipe / api_recipe。 |
| v1.1 | 2026-05-07 | 架構選擇 Brecipe 解析在 cypher-executor TS(不改 claude_api WASM)。減少改動面、可單元測試、跟既有 api_recipe 同層次。 |
-110
View File
@@ -1,110 +0,0 @@
# Tasks — Recipe System (容器 + Recipe 模式)
> 對應 SDD[design.md](design.md)
> 上次更新:2026-05-07
**狀態 legend**`[ ]` 待辦 / `[🔄]` 進行中 / `[x]` 完成
---
## Phase 1prompt_recipe Schema + KV 規範
- [x] 1.1 寫 `cypher-executor/src/lib/prompt-recipe-schema.ts`85 行 Zod schemafragments / inputs / prompt_assembly / output + transform 白名單 7 個)
- [x] 1.2 確認 cypher-executor wrangler.toml 已有 RECIPES KV binding
- [x] 1.3 寫 recipe loader (`recipe-loader.ts` 50 行) + transforms (`recipe-transforms.ts` 58 行) + expander (`recipe-expander.ts` 127 行)
- transform 7 個:json_array / to_string / join / markdown_list / extract_field / first / pluck_content
- expanderfragments(KBDB) + inputs(context+transform) → 套 {{var}} 模板 → {prompt, model, output_*}
- type-check 全通過
## Phase 2cypher-executor recipe expander(架構選擇 B,不改 claude_api
- [x] 2.1 寫 `recipe-expander.ts`127 行:load → fragments → inputs+transform → 套模板 → 回傳 prompt+model+output_*
- [x] 2.2 寫 `recipe-transforms.ts`58 行:7 個 transform
- [x] 2.3 改 `graph-executor.ts` Component case:偵測 `node.data.recipe` → 呼叫 expandPromptRecipe → merge 進 mergedContext
- [x] 2.4 output parser hook:執行完若 `_recipe_output_format === 'json'` 自動 parse + required_fields 驗證
- [x] 2.5 部署 cypher-executor v426b099e
- [x] 2.6 端對端驗證:用 curl 打 `/cypher/execute` 帶 recipetrace 顯示 recipe 展開正確 + claude_api 拿到組好 promptMira daemon 端 522 timeout 是 daemon 問題,不是 recipe 系統)
- [x] 2.7 [紅利修復] cypher-executor `WASM_HTTP_RUNNER_IDS` 加 5 個 mira 零件(claude_api / kbdb_*)— 短期解,根本修法見 KI-13
## Phase 3:第一個 recipe — wiki_synthesis
- [x] 3.1 寫 `polaris/mira/recipes/wiki_synthesis.json`4 fragments + 4 inputs + system/user template + json output
- [x] 3.2 用 `wrangler kv key put --remote` 推進 RECIPES KV (key: `prompt_recipe:wiki_synthesis`)
- [x] 3.3 確認 KV 寫入成功(wrangler kv get 驗證)
- [ ] 3.4 不適用(架構選擇 B 不改 claude_apirecipe 在 cypher-executor 解析)
- [x] 3.5 端對端測試:用 MCP `u6u_execute_workflow` 跑 wiki_synthesis 成功
- input1 句草稿(黃仁勳 GTC 2026 物理 AI
- output3 triplets + 3 entities + 1 wiki paragraph + source_summary
- 過程修了 KI-14 (service binding 指錯)、KI-15 (token 沒轉發)、KI-16 (Claude markdown fence 沒剝)
## Phase 4mira 7-B 用 recipe 完成 wiki workflow
- [🔄] 4.1 寫 `polaris/mira/workflows/wiki_synthesis.yaml`cypher binding YAML
- 用 `recipe: prompt_recipe:wiki_synthesis` 指 recipe
- 4-5 個節點:read_stale → foreach → read_drafts → synthesize → write_wiki + log
- [ ] 4.2 用 MCP `u6u_execute_workflow` sandbox 跑(試一個 entity 不真寫 KBDB
- [ ] 4.3 用 MCP `u6u_deploy_workflow` 部署到 cypher-executor
- [ ] 4.4 手動觸發 cron,驗 wiki page 真的出現
- [ ] 4.5 在 mira/wiki/ 前端看到第一張 AI 合成 wiki page
## Phase 5MCP recipe 管理 tools
- [ ] 5.1 加 MCP tool `arcrun_list_recipes(prefix?)`:列所有 prompt_recipe
- [ ] 5.2 加 MCP tool `arcrun_get_recipe(name)`:取單一 recipe 內容
- [ ] 5.3 加 MCP tool `arcrun_push_recipe(name, yaml_content)`upsert recipe
- [ ] 5.4 加 MCP tool `arcrun_delete_recipe(name)`
- [ ] 5.5 既有 auth_recipe / api_recipe 也通用同套 tool(不只 prompt_recipe
---
## 風險追蹤
- 風險 1claude_api 改造跟 mira-app 同時動,可能影響河道 AI 副駕
- 緩解:向後相容,舊 input 仍可用,mira 河道先不切 recipe
- 風險 2recipe transform 白名單漏了某種需求
- 緩解:發現缺什麼再加,第一版優先支援 wiki 用到的(json_array, extract_field, join
- 風險 3:KV 跟 KBDB 都存配置,AI 困惑「該存哪邊」
- 緩解:清楚分層 — recipe(容器組合方式) KVdataschema 文字、skill 模板) KBDB
---
## Known Issues(吃狗糧發現,記錄)
### KI-11MCP `u6u_execute_workflow` 不暴露 config 欄位 ✅ 修復(2026-05-07
- 已修:tool schema 加 optional `config: Record<string, Record<string, any>>`
- 部署:u6u-mcp v11d7e366
- 用戶要重啟 client session 才能看到新 schema
### KI-12MCP execute 路由打 `/execute` 而非 `/cypher/execute` ✅ 修復(2026-05-07
- 已修:service binding fetch URL 改成 `http://cypher-executor/cypher/execute`
- 部署:u6u-mcp v11d7e366
### KI-14u6u-mcp service binding 指向已廢棄的 inkstone-cypher-executor ✅ 修復
- 現象:MCP 路徑跑 workflow trace 顯示 synth 變 Output、config 被忽略
- 根因:`u6u-mcp/wrangler.toml` services binding 是舊 worker `inkstone-cypher-executor`,不是現役 `arcrun-cypher-executor`
- 解法:改 service name + redeploy
### KI-15u6u-mcp 沒把 partner token 轉發給 cypher-executor ✅ 修復
- 現象:recipe expander 抓 KBDB block 401(沒 auth
- 根因:partnerAuthMiddleware 驗完 token 但只 set org_namespace,沒留 tokenexecute_workflow tool fetch 沒帶 X-Arcrun-API-Key
- 解法:middleware 也 set partner_token、handleMcpRequest + registerAllTools + execute_workflow 多一個 partnerToken 參數、fetch header 加 X-Arcrun-API-Key
### KI-16Recipe JSON output 被 Claude 包在 ```json``` markdown fence ✅ 修復
- 現象:JSON.parse 失敗 "Unexpected token \`"
- 根因:Claude 預設輸出 ```json\n{...}\n``` 包裝
- 解法:cypher-executor 解析前 regex 剝 fence
### KI-13cypher-executor `WASM_HTTP_RUNNER_IDS` 寫死白名單
- 現象:每加新零件要回 cypher-executor 改白名單 + 重部署
- 影響:違反 arcrun「容器+ recipe,新零件無需改 platform」承諾
- 短期解:手動加進白名單(claude_api / kbdb_* 已加)
- 根本解:改成從 component-registry KV 動態查 canonical_id
- 優先級:P1(推廣破口),需新 SDD `cypher-executor-dynamic-component-discovery`
---
## 對外推廣(Phase 6+,本 SDD 不執行,記錄)
- README 示範「容器 + recipe = 一個 service」(Gmail / Slack / Claude)
- onboarding kit GitHub template 內含 5 個經典 recipe 當範例
- 「recipe market」想法:用戶分享 recipe 幫他人少寫 prompt
-285
View File
@@ -1,285 +0,0 @@
# SDD: Resumable Workflowwebhook callback 喚醒)
> 2026-05-07 建立。狗糧寫 wiki 合成 workflow 時,Mira daemon 對長草稿(>2KB)切非同步模式回 `{pending, task_id, poll_url}`cypher-executor 沒處理就直接傳下游。
> 本 SDD 解這層:**workflow 跑到一半遇到 pending 任務 → 暫停 + 持久化狀態 → 外部 callback 進來時喚醒繼續**。
> 範圍:兩家自家服務之間(Mira daemon ↔ cypher-executor)走 webhook 推。對外服務無 webhook 的場景留 wishlist 用 poll 解。
---
## 1. 問題
### 1.1 撞牆現場
wiki 合成 workflow 第一節點 `claude_api(recipe:wiki_synthesis)`
- 短草稿(< 2KB)→ daemon 同步回 `{success, data: {text}}`recipe output parser 解 JSON 成功
- 長草稿(> 2KB)→ daemon 估 75s,切非同步模式回:
```json
{
"success": true,
"pending": true,
"task_id": "task_14_1778133152480",
"poll_url": "https://mira.uncle6.me/mira/execute/task_14_1778133152480",
"estimated_seconds": 75
}
```
cypher-executor 拿到這個物件就當 result,但裡面沒 `data.text`,下游 recipe output parser 找不到要 parse 的東西,整個 workflow 算「success」但實際上 wiki 還沒生出來。
### 1.2 現有 toolkit 不夠
- `wait` 零件:固定 sleep N ms,沒 retry / 條件判斷
- `http_request` 零件:通用 HTTP,不認 daemon 的 polling 協議
- cypher-executor `visited` Set:擋住節點重訪,沒辦法做迴圈式 poll
- Worker CPU 30s 限制:同步 poll 75s 任務不可能
### 1.3 Push vs Pull 抉擇(2026-05-07 拍板)
| | Webhook 推 | Poll 拉 |
|---|---|---|
| 適用 | 雙方都自家 | 對方無 callback 能力 |
| Worker 時間消耗 | 趨近 0 | 全程占用 |
| 時長限制 | 無 | Worker CPU 30s |
| 工程位置 | runtime 能力(cypher-executor| 零件(poll_task |
**走 Webhook 推**(自家服務優先,poll_task 進 wishlist)。
---
## 2. 設計
### 2.1 三層改動
**A. Mira daemon 端(infra/cloud-cto**
- `/mira/execute` 接受新欄位 `callback_url: string`optional
- task 完成時 POST 到 `callback_url`body
```json
{
"task_id": "task_14_xxx",
"success": true,
"data": { "text": "..." }
}
```
- 失敗也要 callbackbody 含 `error` 欄位
- 重試策略:3 次 backoff1s / 5s / 30s),最後失敗就放棄(task 狀態存進 daemon 自己 KV
**B. cypher-executor 端(resumable runtime**
新概念:**workflow run 可以暫停**。
設計:
1. 新 KV namespace(或用既有 `EXEC_CONTEXT`)存暫停的 run state
- key: `paused_run:{task_id}``paused_run:{run_id}`
- value: `{ run_id, graph, paused_node_id, paused_node_pending_result, context, trace_so_far, kv_store_ref, expires_at }`
2. graph-executor 偵測節點 result 含 `pending: true` + `task_id` → 暫停 + 寫 KV + 回 `{paused: true, task_id, run_id}`
3. 新 endpoint `POST /workflows/resume`
- body: `{ task_id, result }`result 是 daemon callback 給的完整資料)
- 從 KV 拿 paused state → merge result 進 paused_node 的 output → 從下個節點繼續執行
4. claude_api 容器呼叫 daemon 時自動帶 `callback_url`
- `https://cypher.arcrun.dev/workflows/resume?task_id={預先派發的 task_id}`
- 但 task_id 是 daemon 自己派的,cypher-executor 不知道。需先 daemon 派完 task_id 才能組 URL
- 解:daemon 改成「先回 task_id,再啟動實際工作 + 完成時 callback」— 兩階段 hand-shake
實際流程(兩階段):
```
cypher-executor Mira daemon
│ │
│ POST /mira/execute │
│ { prompt, │
│ callback_url: "?run_id=R1" }
├─────────────────────────────>│
│ │ 立即回 task_id(決定走非同步)
│<─────────────────────────────┤ { pending, task_id: T9 }
│ │
├─ 看到 pending → 寫 KV │ 啟動實際 LLM 任務
│ paused_run:T9 = {run R1, │
│ paused_node, ctx, ...} │
│ │
│ 立即回 client (MCP)
│ { paused, task_id: T9 } │
│ │
⋯⋯⋯⋯⋯ 75s 後 ⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯
│ │ task done
│ POST /workflows/resume │
│ { task_id: T9, result: {...} }
│<─────────────────────────────┤
│ │
│ 從 KV 取 paused_run:T9 │
│ → merge result 進 paused 節點 │
│ → 從下個節點繼續 │
│ │
│ run 跑完 → 寫 trace │
│ → 通知 client (?) │
│ │
```
### 2.2 範圍邊界
**第一版(v1)做:**
- ✅ 單節點 pending → resume(最常見:claude_api 拿到 daemon pending
- ✅ daemon 加 callback_url 支援
- ✅ cypher-executor `/workflows/resume` endpoint
- ✅ run state 寫 EXEC_CONTEXT KV,含 24h TTL(避免 KV 累積)
- ✅ 整合測:用 wiki 合成跑長草稿,驗 callback 進來能繼續
**第一版不做:**
- ❌ 多節點都 pending 的 nested 場景(例如 claude_api → 又一個 claude_api)— v2
- ❌ foreach 內 pendingitem-level resume)— v2
- ❌ pending 期間用戶看到「進度」的前端 UI — 走 trace 有 paused 標記,前端 polling 自己做即可
- ❌ pending callback 失敗時的 retry / DLQ — v2,先記 log
**前置依賴:**
- ✅ recipe-system 已部署(cypher-executor 已會解 recipe
- ✅ Mira daemon 在 Hetzner,可改 code
### 2.3 為什麼不用 Cloudflare Queues / Durable Objects
- **CF Queues**:適合大量 fan-out,這裡是點對點 callbackKV 已夠
- **Durable Objects**long-lived state 比 KV 強,但成本高 + 複雜
- **EXEC_CONTEXT KV**:既有 binding,工程量最小
未來真撞到 KV 限制(每 partner 寫入頻率上限)再升級。
---
## 3. 詳細設計
### 3.1 daemon 端 callback 機制
`infra/cloud-cto/index.js`Mira daemon):
```js
// /mira/execute handler
{
// 既有 input + 新加:
callback_url: string // optional
}
// 處理邏輯:
// 1. 啟動 task(既有邏輯)
// 2. 預估時間 > 30s → 切非同步:
// - 立即回 { success: true, pending: true, task_id, poll_url, estimated_seconds }
// - 背景 task 完成時:
// if (callback_url) POST callback_url with { task_id, success, data, error? }
// (不論用戶有沒有 poll,callback 一定會送)
```
callback 失敗策略:
- 3 次重試(1s / 5s / 30s
- 全失敗:task 狀態維持完成,等 client 主動 pollpoll_url 仍有效)
- 超過 24h 沒被消化的 taskdaemon GC
### 3.2 cypher-executor 端 resumable runtime
#### 3.2.1 偵測 pendinggraph-executor
在 Component caserunner 回傳後:
```ts
result = await runner(mergedContext);
// 偵測 pending patterndaemon 約定的回應結構)
if (isResumablePending(result)) {
await persistPausedRun(this.env.EXEC_CONTEXT, taskIdFromResult(result), {
run_id, graph, paused_node_id: node.id, paused_context: context,
paused_result: result, trace_so_far: trace, expires_at: Date.now() + 24*60*60*1000
});
// 提早結束此 run,回 paused 狀態
return { paused: true, task_id, run_id };
}
// ... 既有的 recipe output parsing / kvSetNodeOutput / 等
```
`isResumablePending(result)` = `result?.pending === true && typeof result?.task_id === 'string'`
#### 3.2.2 callback URL 注入(claude_api 之前的 layer
問題:claude_api 容器發 daemon 請求時,要帶 `callback_url`。但 task_id 是 daemon 派的,URL 裡只能放 run_iddaemon 收到 callback 時填 task_id
`callback_url = https://cypher.arcrun.dev/workflows/resume?run_id={current_run_id}`
但 cypher-executor 端用 task_id 找 paused state(一個 run 可能多個 pending),所以 callback URL 應該是:
`callback_url = https://cypher.arcrun.dev/workflows/resume`(不帶 querytask_id 在 body
**實作位置**:在 graph-executor 呼叫 claude_api 前,自動注入 `callback_url` 到 mergedContext
```ts
if (node.componentId === 'claude_api' && this.env?.PUBLIC_BASE_URL) {
mergedContext.callback_url = `${this.env.PUBLIC_BASE_URL}/workflows/resume`;
}
```
> 暫先用「componentId 寫死匹配」是 hacky,未來 component contract 加 `supports_async_callback: true` 標記就 generic 了。
#### 3.2.3 resume endpoint
`POST /workflows/resume`
```ts
{
task_id: string, // daemon 給的
success: boolean,
data?: { text: string }, // 跟同步呼叫一樣的結構
error?: string
}
```
處理:
1. 從 EXEC_CONTEXT KV `paused_run:{task_id}` 拿 state
2. 沒拿到(過期 / 重複 callback)→ 回 200 + log
3. 把 callback 給的 result 當作 paused_node 的 output
4. 重建 GraphExecutor,從下個節點繼續執行
5. 跑完寫完整 trace
**問題:resume 後沒辦法再回給原 client。** 用戶最初打 `/cypher/execute`(同步),拿到 `{paused, task_id}` 之後就斷了;resume 跑完 result 沒地方送。
**v1 解法**resume 完寫進 `analytics_kv` 或 D1,**用戶要主動 query**。簡單但 UX 差。
**v2 想法**resume 完發另一個 webhook 給原 clientclient 在 trigger 時帶 final_callback_url)。
---
## 4. 範圍
**在本 SDD 範圍內:**
- 4.1 daemon `/mira/execute` 加 callback_url 支援
- 4.2 cypher-executor 偵測 pending + 持久化 paused state
- 4.3 cypher-executor `/workflows/resume` endpoint
- 4.4 callback_url 自動注入(claude_api 場景)
- 4.5 wiki 合成 workflow 用長草稿端對端測試
**不在本 SDD 範圍:**
- nested pendingv2
- foreach 內 pendingv2
- final_callback 給原 clientv2
- poll_task 零件(wishlist
---
## 5. 驗收標準
1. wiki 合成 workflow 餵 5KB+ 草稿,跑完後 wiki page 有寫進 KBDB(不再 trace `pending` 假成功)
2. trace 有 `paused` 紀錄,能看到 task_id
3. 從 daemon 觸發 callback 後 < 5s 內 cypher-executor 把 paused state 撿起來繼續
4. 24h 沒 callback 的 paused state KV 自動 expire(看 KV TTL 列表)
---
## 6. 風險
| 風險 | 緩解 |
|---|---|
| daemon callback 進來時 cypher-executor 重啟 → state 還在 KVOK | KV 持久化 |
| 同 task_id 重複 callback(網路重試)→ 重複執行下游 | resume endpoint idempotent:拿到 state 後立刻刪 KV,重複 callback 找不到 state |
| daemon callback 失敗(網路)| daemon 端 3 retry + 24h GC,超過就需手動干預(v1 接受) |
| paused state 含敏感資料(partner key| KV 有 24h TTL;不寫 plaintext secrets(既有 credential injection 在執行前才解,paused state 存的是執行前的 contextsecret 還沒解)|
---
## 7. 變更紀錄
| 版本 | 日期 | 內容 |
|---|---|---|
| v1.0 | 2026-05-07 | 初版。狗糧 wiki 合成撞 daemon 非同步 → 補 resumable workflow runtime。第一版只做單節點 pending + claude_api callback 注入。|
-61
View File
@@ -1,61 +0,0 @@
# Tasks — Resumable Workflow
> 對應 SDD[design.md](design.md)
> 上次更新:2026-05-07
**狀態 legend**`[ ]` 待辦 / `[🔄]` 進行中 / `[x]` 完成
---
## Phase 1Mira daemon 端 callback 支援
- [x] 1.1 改 `/opt/mira/mira-daemon.js`Hetzner mira container`/execute` 接受 `params.callback_url`
- [x] 1.2 fireCallback functiontask done/failed 時 POST callback_urlbody = `{task_id, success, data?, error?}`
- [x] 1.3 callback retry4 次(立即 + 1s/5s/30s backoff),全失敗 log
- [x] 1.4 patch script 寫好 `/tmp/patch-mira-daemon.py`docker cp 進 container(注意:rebuild image 會丟失,需重 patch 或正式 commit 進 Dockerfile/git repo
- [x] 1.5 真實端對端驗證:daemon log 顯示 `[Mira callback] task=task_2_... POST https://cypher.arcrun.dev/workflows/resume OK 200`2026-05-07 07:24:04 + task_3 短測試)
## Phase 2cypher-executor resumable runtime
- [x] 2.1 寫 `paused-runs.ts`81 行):persistPausedRun / loadPausedRun / consumePausedRun + isResumablePending 偵測器,24h TTL
- [x] 2.2 改 `graph-executor.ts` Component case:偵測 pending → 寫 KV + throw WorkflowPaused
- [x] 2.3 改 `cypher-handlers.ts`catch WorkflowPaused → 回 `{success:true, paused:true, task_id, run_id, paused_node_id, trace, graph}`
- [x] 2.4 callback_url 自動注入:componentId==='claude_api' 時 mergedContext.callback_url = PUBLIC_BASE_URL 或預設 cypher.arcrun.dev/workflows/resume
## Phase 3resume endpoint
- [x] 3.1 寫 `routes/resume.ts`POST /workflows/resumeconsumePausedRun → resumeFromPaused
- [x] 3.2 graph-executor 加 `resumeFromPaused()` 方法:把 callback_result 當 paused_node 輸出 + spread 進 ctx + 從下游節點繼續
- [x] 3.3 idempotent 驗證:第二次 callback 回 `{noop:true, reason:"state 不存在或過期"}`
- [x] 3.4 cypher-executor 部署 v0580980b
- [x] 3.5 mount /workflows/resume 進 index.ts
## Phase 4claude_api 容器透傳 callback_url
- [x] 4.1 改 `claude_api/main.go`Input 加 CallbackURLtimeout 預設改 120s
- [x] 4.2 重 build wasm + redeploy claude-api.arcrun.dev (v f926e3dd)
- [x] 4.3 真實端對端驗證:daemon 收到 callback_url → task done 後 POST cypher-executor/workflows/resume → 200 OK
## Phase 5:端對端整合測試
- [ ] 5.1 用 MCP `u6u_execute_workflow` 跑 wiki 合成 + 5KB+ 草稿
- [ ] 5.2 第一次回應應為 `{paused, task_id, run_id}`
- [ ] 5.3 等 daemon callback 進來(log 看到 /workflows/resume 命中)
- [ ] 5.4 觀察 wiki page 真的寫進 KBDB(即使原 MCP call 已斷線)
- [ ] 5.5 trace 含完整節點紀錄(paused → resumed
---
## 風險追蹤
- 風險 1daemon callback 進來時,cypher.arcrun.dev 還沒醒(CF Worker cold start)→ 第一次 retry 接住(daemon retry policy 涵蓋)
- 風險 2v1 沒 final_callback 給原 client → 用戶要主動查狀態
- 接受:mira 河道 UI 可定期 refetch wiki page,或用既有 KBDB 觸發機制
- v2 加 final_callback 統一處理
## v2 已記錄
- nested pending(一個 run 多個 paused 節點)
- foreach 內 pendingitem-level resume
- final_callback 給原 clienttrigger 時帶 final_callback_url
- poll_task 零件(外部 API 沒 webhook 時用)
-62
View File
@@ -1,62 +0,0 @@
# 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`
-31
View File
@@ -1,31 +0,0 @@
#!/bin/bash
# .claude/hooks/post-edit-remind-tasks.sh
# arcrun PostToolUse hook for Write / Edit / MultiEdit
#
# 職責:改完 code 後立刻提醒 CC 更新對應 tasks.md,避免批次更新
# 退出 code:不 block,只提醒(exit 0
set -o pipefail
INPUT=$(cat)
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // .tool_input.path // ""')
# 只針對程式碼檔案提醒(不含 tasks.md / CLAUDE.md / rules/*.md 本身)
if [[ "$FILE_PATH" =~ \.(go|ts|tsx|js|jsx|py|rs|wasm)$ ]] && [[ "$FILE_PATH" != *"tasks.md"* ]]; then
cat >&2 <<EOF
📌 PostEdit 提醒(by arcrun hook
剛修改了:${FILE_PATH}
下一步動作:
1. 找到對應的 .agents/specs/*/tasks.md
2. 如果這個 task 已完成 → 立刻把 [ ] 改成 [x](或把進行中的 [🔄] 改成 [x]
3. 如果發現新的 sub-task → 立刻加入 tasks.md
4. 不要等到 session 結束才批次更新
違反 SDD 協議會在 Stop hook 被提醒,建議現在就處理。
EOF
fi
exit 0
-106
View File
@@ -1,106 +0,0 @@
#!/bin/bash
# .claude/hooks/pre-bash-guard.sh
# arcrun PreToolUse guard for Bash
#
# 職責:擋下會違反 CLAUDE rules 的 shell 指令
# 退出 code
# 0 = 允許
# 2 = 擋下(stderr 訊息會回傳給 CC)
set -o pipefail
INPUT=$(cat)
CMD=$(echo "$INPUT" | jq -r '.tool_input.command // ""')
block() {
local rule="$1"
local reason="$2"
local fix="$3"
cat >&2 <<EOF
❌ BLOCKED by arcrun CLAUDE rules
違反項:${rule}
指令:${CMD}
原因:${reason}
正確做法:${fix}
參考:.claude/rules/02-forbidden.md
EOF
exit 2
}
# ─────────────────────────────────────────────────────────────────────────────
# 規則 1.2 / 3.3:禁止用 mkdir 建立違規的 auth/credential worker 目錄
# ─────────────────────────────────────────────────────────────────────────────
if echo "$CMD" | grep -qE "mkdir.*((auth|credential|jwt|oauth)[-_]worker|new[-_](auth|credential|jwt|oauth))"; then
block "1.2/3.3" \
"偵測到嘗試建立新的 auth/credential/jwt/oauth Worker 目錄" \
"auth primitive 放在 registry/components/auth_*/;不需要另建 worker 目錄"
fi
# 禁止建立同名零件的平行目錄
if echo "$CMD" | grep -qE "mkdir.*/(gmail|telegram|google[-_]sheets|line[-_]notify|http[-_]request)[-_](v2|v3|new|worker|backup)"; then
block "3.3" \
"禁止為既有零件建立平行目錄(v2/new/worker/backup" \
"直接改 registry/components/<n>/main.go"
fi
# ─────────────────────────────────────────────────────────────────────────────
# 規則 1.3:禁止 wrangler init / generate auth-* credential-* jwt-*
# ─────────────────────────────────────────────────────────────────────────────
if echo "$CMD" | grep -qE "wrangler[[:space:]]+(init|generate).*[[:space:]](auth|credential|jwt|oauth)[-_]"; then
block "1.3" \
"禁止用 wrangler init/generate 建立 auth/credential/jwt Worker" \
"auth primitive 透過 component-worker-template/ 搭配 WASM binary 部署,不要 wrangler init"
fi
# ─────────────────────────────────────────────────────────────────────────────
# 規則 3.1Service Binding 新增警示
# ─────────────────────────────────────────────────────────────────────────────
# 偵測在 wrangler.toml 新增 [[services]] 的 echo/cat/sed 操作(非 100% 準確,但夠用)
if echo "$CMD" | grep -qE "echo.*\[\[services\]\].*>>"; then
block "3.1" \
"偵測到要在 wrangler.toml 新增 [[services]] binding" \
"零件串接一律走 HTTP URLcypher binding),不新增 service binding。若有特殊需求,先與 richblack 確認"
fi
# ─────────────────────────────────────────────────────────────────────────────
# 一般性危險指令
# ─────────────────────────────────────────────────────────────────────────────
if echo "$CMD" | grep -qE "rm[[:space:]]+-rf[[:space:]]+(/|/\*|~|\\\$HOME|\.)"; then
block "general" \
"偵測到危險的 rm -rf 指令" \
"明確指定要刪的目錄,不要對根目錄 / home / 當前目錄遞迴刪除"
fi
# 禁止 force push 到 main
if echo "$CMD" | grep -qE "git[[:space:]]+push.*--force.*(main|master)"; then
block "general" \
"禁止 force push 到 main/master" \
"用 feature branch,或和 richblack 確認後手動操作"
fi
# ─────────────────────────────────────────────────────────────────────────────
# 資料外流警示(data-exfil-warning SDD R2):AI 動手把資料變成可被外部呼叫前先擋
# `acr push`(部署 webhook/ `acr recipe push`(定義資料去向)= 暴露面動作。
# 不含 `acr creds push`(上傳加密 credential 是保護,非暴露)。
#
# 信任修正(2026-05-30):無「旗標放行」捷徑——AI 自己能加的旗標 = 自己批准自己。
# 這類動作一律擋,必須由人類在終端機親自執行(CLI 會跳互動、要人類輸入資源名確認)。
# ─────────────────────────────────────────────────────────────────────────────
# 只在「指令本身就是執行 acr push / acr recipe push」時擋(錨定到指令開頭,
# 允許前置 cd .. && 或環境變數)。避免誤判 git commit -m "...acr push..." 這類
# 「字串裡剛好提到 acr push」的情況(commit message / echo / grep 不該被擋)。
if echo "$CMD" | grep -qE "(^|&&|;|\|)[[:space:]]*(cd[[:space:]][^&;|]*(&&|;)[[:space:]]*)?([A-Za-z_]+=[^[:space:]]*[[:space:]]+)*acr[[:space:]]+(recipe[[:space:]]+)?push\b" \
&& ! echo "$CMD" | grep -qE "acr[[:space:]]+creds[[:space:]]+push\b"; then
cat >&2 <<'EOF'
❌ BLOCKED:資料外流警示(arcrun data-exfil-warning
原因:acr push / acr recipe push 會把資料/workflow 變成「可被外部呼叫」(暴露面)。
這種動作你(AI)不能自行執行,也沒有旗標捷徑——需人類明示知情同意(法律憑證)。
正確做法:
- 把這件事交給人類:請人類在終端機親自執行(CLI 會跳互動、要人類輸入資源名確認)
- 人類第一次確認後 server 會記住,之後同資源不用再確認
參考:.agents/specs/data-exfil-warning/
EOF
exit 2
fi
exit 0
-193
View File
@@ -1,193 +0,0 @@
#!/bin/bash
# .claude/hooks/pre-write-guard.sh
# arcrun PreToolUse guard for Write / Edit / MultiEdit
#
# 職責:擋下會違反 CLAUDE rules 的檔案寫入操作
# 退出 code
# 0 = 允許
# 2 = 擋下(stderr 訊息會回傳給 CC)
#
# 依賴:jq
set -o pipefail
INPUT=$(cat)
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // .tool_input.path // ""')
# 取得將要寫入的內容(Write: contentEdit: new_stringMultiEdit: edits[].new_string 全部串起來)
CONTENT=$(echo "$INPUT" | jq -r '
.tool_input.content
// .tool_input.new_string
// (.tool_input.edits // [] | map(.new_string // "") | join("\n"))
// ""
')
block() {
local rule="$1"
local reason="$2"
local fix="$3"
cat >&2 <<EOF
❌ BLOCKED by arcrun CLAUDE rules
違反項:${rule}
檔案:${FILE_PATH}
原因:${reason}
正確做法:${fix}
參考:.claude/rules/02-forbidden.md
EOF
exit 2
}
# ─────────────────────────────────────────────────────────────────────────────
# 規則 1.1registry/components/ 下不准 TS(除非是 AssemblyScript
# ─────────────────────────────────────────────────────────────────────────────
if [[ "$FILE_PATH" == *"registry/components/"* && "$FILE_PATH" == *.ts ]]; then
# 允許 asconfig.json 同目錄的 AssemblyScript
COMP_DIR=$(dirname "$FILE_PATH")
if [[ ! -f "$COMP_DIR/asconfig.json" ]]; then
block "1.1" \
"registry/components/ 下禁止 TypeScript(除非是 AssemblyScript 且同目錄有 asconfig.json" \
"零件必須用 TinyGo (main.go) 或 AssemblyScript 實作並編譯成 .wasm"
fi
fi
# ─────────────────────────────────────────────────────────────────────────────
# 規則 1.2:禁止在非法位置新增 auth/credential 實作
# ─────────────────────────────────────────────────────────────────────────────
# 合法位置:registry/components/auth_static_key | auth_oauth2 | auth_service_account | auth_mtls
if [[ "$FILE_PATH" =~ auth[-_](static[-_]key|oauth2|service[-_]account|mtls) ]]; then
if [[ "$FILE_PATH" != *"registry/components/auth_"* ]]; then
block "1.2" \
"auth primitive 實作只能放在 registry/components/auth_<type>/" \
"改去 registry/components/auth_static_key/ 等目錄,用 TinyGo 實作 main.go"
fi
fi
# ─────────────────────────────────────────────────────────────────────────────
# 規則 2.1:禁止新增含特定關鍵字的 TS 檔案(credential-injector / jwt-signer 等)
# ─────────────────────────────────────────────────────────────────────────────
if [[ "$FILE_PATH" == *.ts ]]; then
BASE=$(basename "$FILE_PATH")
# 既有的 credential-injector.ts / jwt-signer.ts 允許修改(為了刪除),但不准新增同名
if [[ "$BASE" =~ ^(credential[-_]injector|jwt[-_]signer)\.ts$ ]]; then
if [[ ! -f "$FILE_PATH" ]]; then
block "2.1" \
"禁止新增 ${BASE}(Phase 1-3 的目標是刪除此類檔案,不是重建)" \
"credential 注入 / JWT signing 屬於 WASM 零件職責,改去 registry/components/auth_*/"
fi
fi
fi
# ─────────────────────────────────────────────────────────────────────────────
# 規則 2.2cypher-executor TS 裡不准實作業務邏輯(只准 wasi-shim.ts 做 crypto
# ─────────────────────────────────────────────────────────────────────────────
if [[ "$FILE_PATH" == *"cypher-executor/src/"* && "$FILE_PATH" == *.ts ]]; then
BASE=$(basename "$FILE_PATH")
# crypto.subtle.decrypt:只准在 wasi-shim.ts
if echo "$CONTENT" | grep -qE "crypto\.subtle\.decrypt"; then
if [[ "$BASE" != "wasi-shim.ts" ]]; then
block "2.2" \
"AES-GCM 解密(crypto.subtle.decrypt)只准出現在 wasi-shim.ts 的 crypto_decrypt host function" \
"把解密邏輯移到 wasi-shim.ts 的 host function;或讓 WASM 零件透過 u6u.crypto_decrypt 呼叫"
fi
fi
# crypto.subtle.sign with RSASSA:只准在 wasi-shim.ts
if echo "$CONTENT" | grep -qE "crypto\.subtle\.sign.*RSASSA"; then
if [[ "$BASE" != "wasi-shim.ts" ]]; then
block "2.2" \
"RS256 簽章只准出現在 wasi-shim.ts 的 crypto_sign_rs256 host function" \
"把簽章移到 wasi-shim.ts;或讓 auth_service_account WASM 透過 u6u.crypto_sign_rs256 呼叫"
fi
fi
# Template 展開:{{secret.X}} 或 {{runtime.X}} 屬於 WASM 職責
# 例外:auth-recipe-seeds.ts 是 recipe 資料定義(會被序列化寫進 RECIPES KV),
# 其中的 {{secret.X}} / {{runtime.X}} 是「資料字面值」而非 TS 展開邏輯,
# 真正的展開仍在 WASM auth primitive 內完成。
if [[ "$BASE" != "auth-recipe-seeds.ts" ]] && echo "$CONTENT" | grep -qE "\{\{(secret|runtime)\." ; then
block "2.2" \
"Template 展開({{secret.X}} / {{runtime.X}})屬於 WASM auth primitive 職責" \
"把這段邏輯改寫到 registry/components/auth_static_key/main.goTinyGo"
fi
# Hard-code 的 BUILTIN_API_RECIPES / BUILTIN_CREDENTIALS_MAP 新增
if echo "$CONTENT" | grep -qE "(BUILTIN_API_RECIPES|BUILTIN_CREDENTIALS_MAP)\s*[:=]"; then
# 允許「把它設成空物件」或「刪除」,但不准新增實作
if echo "$CONTENT" | grep -qE "BUILTIN_API_RECIPES.*=.*\{\s*[a-zA-Z]"; then
block "2.2" \
"禁止在 TS 裡新增 BUILTIN_API_RECIPES / BUILTIN_CREDENTIALS_MAP 實作" \
"API 呼叫邏輯屬於各自的 WASM 零件(gmail.wasm / telegram.wasm 等),cypher-executor 只做 routing"
fi
fi
# Hard-code API endpoint 實作
HARDCODED_APIS=(
"gmail\.googleapis\.com/gmail/v1/users/me/messages/send"
"api\.telegram\.org/bot.*sendMessage"
"sheets\.googleapis\.com/v4/spreadsheets"
"notify-api\.line\.me/api/notify"
)
for PATTERN in "${HARDCODED_APIS[@]}"; do
if echo "$CONTENT" | grep -qE "$PATTERN"; then
# 允許 wasi-shim.ts 裡的 http_request host function(它只是 proxy
if [[ "$BASE" != "wasi-shim.ts" ]]; then
block "2.2" \
"禁止在 cypher-executor TS 裡 hard-code API endpoint(偵測到: $PATTERN" \
"把 API 呼叫移到對應的 WASM 零件(registry/components/gmail/main.go 等)"
fi
fi
done
# exchangeGoogleJwt / 類似 token exchange function
if echo "$CONTENT" | grep -qE "(exchangeGoogleJwt|exchangeServiceAccountJwt|signGoogleJwt)"; then
if [[ "$BASE" != "wasi-shim.ts" ]]; then
block "2.2" \
"Token exchange 邏輯屬於 auth_service_account WASM 零件" \
"改到 registry/components/auth_service_account/main.go"
fi
fi
fi
# ─────────────────────────────────────────────────────────────────────────────
# 規則 3.3:禁止建立 *-v2 / new-* / *-worker 類複製貼上目錄
# ─────────────────────────────────────────────────────────────────────────────
if [[ "$FILE_PATH" =~ /(auth|credential|jwt|oauth|gmail|telegram|google-sheets|line-notify|http-request)[-_](v2|v3|new|worker|backup|temp)/ ]]; then
block "3.3" \
"禁止為同一零件建立平行目錄(v2/new/worker/backup 等)" \
"直接修改 registry/components/<name>/main.go 即可;需要版本管理請用 git branch"
fi
if [[ "$FILE_PATH" =~ /new-(auth|credential|jwt|oauth|gmail|telegram)/ ]]; then
block "3.3" \
"禁止為同一零件建立 new-<name>/ 平行目錄" \
"直接修改 registry/components/<name>/main.go"
fi
# ─────────────────────────────────────────────────────────────────────────────
# 規則 4.3:禁止自行在 .agents/specs/ 下建新 SDD 目錄
# ─────────────────────────────────────────────────────────────────────────────
if [[ "$FILE_PATH" == *".agents/specs/"* ]]; then
# 檢查是否在已知 SDD 目錄內
KNOWN_SDDS=(
".agents/specs/arcrun"
".agents/specs/u6u-core-mvp"
".agents/specs/u6u-platform-evolution"
".agents/specs/component-registry-canon"
".agents/specs/component-gatekeeping" # 2026-05-29 richblack 確認新建(Phase 3 把關)
".agents/specs/data-exfil-warning" # 2026-05-30 richblack 確認新建(資料外流警示)
)
IN_KNOWN=false
for K in "${KNOWN_SDDS[@]}"; do
if [[ "$FILE_PATH" == *"$K/"* ]]; then
IN_KNOWN=true
break
fi
done
if [[ "$IN_KNOWN" == "false" ]]; then
block "4.3" \
"禁止自行在 .agents/specs/ 下建立新的頂層 SDD 目錄" \
"先與 richblack 確認 SDD 範圍。若是現有 SDD 的補充檔案,請放到已知 SDD 目錄下"
fi
fi
exit 0
-73
View File
@@ -1,73 +0,0 @@
#!/bin/bash
# .claude/hooks/session-start-load-sdd.sh
# arcrun SessionStart hook
#
# 職責:session 啟動時注入當前進度、絕對禁令、SDD 位置,避免 CC 靠記憶猜測
# 輸出走 stdout,會注入到 CC 的 context
set -o pipefail
cat <<'EOF'
============================================================
🚨 arcrun 工作規範(SessionStart 注入)
============================================================
📌 絕對禁令(違反會被 pre-write / pre-bash hook 直接 block):
1. registry/components/ 下禁止 TypeScript 實作零件
→ 只能 TinyGo (main.go) 或 AssemblyScript,編譯成 .wasm
2. cypher-executor TS 裡禁止實作 credential/auth/JWT 業務邏輯
→ crypto.subtle.decrypt / sign 只准出現在 wasi-shim.ts 的 host function
{{secret.X}} template 展開屬於 WASM 零件職責
→ 禁止 hard-code gmail/telegram/sheets API endpoint
3. 禁止新增 Service Binding 綁零件
→ 零件串接走 HTTP URLcypher binding = YAML 裡的 URL 清單)
→ Cypher binding 不是 Cloudflare 的 binding 機制
4. 每個 WASM 零件 = 獨立 Worker = 公開 URL
→ 不是從 R2 動態讀(平台零件已 bundle 進各自 Worker
→ R2 只用於 Phase 5 的用戶自製零件(未啟用)
📋 任何 code 變動前,必須:
1. 讀 .agents/specs/arcrun/arcrun.md(總進度)
2. 讀對應 task 的 SDDdesign.md + tasks.md
3. 在回覆開頭宣告(格式見 .claude/rules/00-sdd-protocol.md):
📋 已讀 SDD<檔案清單>
🎯 本次對應 task<task 編號>
📐 本次 task 的 SDD 規範摘要:<重點>
🚧 執行範圍:修改/建立/刪除 <檔案>
4. 每完成一個 task,立刻更新 tasks.md 的 [x],不批次
🔥 當前進行中 PhaseCredential Primitives TS → WASM
SDD.agents/specs/arcrun/credential-primitives-wasm/
已完成:Phase 0.1-0.5(核心合併 + u6u-core 刪除)
未完成硬前置:
- Phase 0.6 wasi-shim 加 host functionskv_get / crypto_decrypt / crypto_sign_rs256
- Phase 0.7 component-loader WASM runner 路徑
未完成主要任務:
- Phase 1 auth_static_key WASM 零件(TinyGo
- Phase 2 auth_service_account WASM 零件
- Phase 3 刪除 cypher-executor 的三套違規 TS
* src/actions/credential-injector.ts(整檔刪)
* src/lib/jwt-signer.ts(整檔刪)
* src/lib/component-loader.ts 的 BUILTIN_API_RECIPES + BUILTIN_CREDENTIALS_MAP(整段刪)
⛔ 封測狀態:推遲(richblack 2026-04-19 決定)
原因:違規 TS 未清,不封測。
📚 詳細規範:
.claude/rules/00-sdd-protocol.md — SDD 協議
.claude/rules/01-tech-stack.md — 技術棧硬限制
.claude/rules/02-forbidden.md — 禁止清單(hook 強制)
.claude/rules/03-component-architecture.md — 零件架構(R2/cypher binding/service binding 的正確定義)
.claude/rules/04-current-progress.md — 當前進度 + SDD 索引
.claude/rules/06-mindset.md — mindset(為什麼層):工作流是default/零件稀有/AI→工具/誠實不假綠
============================================================
EOF
exit 0
-47
View File
@@ -1,47 +0,0 @@
#!/bin/bash
# .claude/hooks/stop-check-sync.sh
# arcrun Stop hook
#
# 職責:session 結束前檢查 code 變動是否有對應的 SDD / tasks.md 更新
# 退出 code:不 block,只警告
set -o pipefail
# 檢查 .agents/specs 下本次 session 是否有變動
SPECS_DIFF=$(git -C "$(pwd)" status --porcelain -- '.agents/specs/' 2>/dev/null | head -20)
CODE_DIFF=$(git -C "$(pwd)" status --porcelain -- '*.go' '*.ts' '*.tsx' '*.py' 'cypher-executor/' 'registry/' 'cli/' 2>/dev/null | head -20)
if [[ -n "$CODE_DIFF" && -z "$SPECS_DIFF" ]]; then
cat >&2 <<EOF
⚠️ Stop hook 警告(by arcrun hook
偵測到本 session 有程式碼變動,但 .agents/specs/ 下的 SDD 文件沒有任何變動。
未 commit 的程式碼變動:
$(echo "$CODE_DIFF" | head -10)
請在結束前確認:
1. 對應的 tasks.md 是否已更新 [x]
2. 是否有架構變動需要更新 design.md?
3. 是否有 SDD 範圍外的 change 未標記?
SDD 協議要求:code 和 SDD 必須同步更新。
參考:.claude/rules/00-sdd-protocol.md
EOF
fi
# 若有暫存的 tasks.md 變動,提醒 commit
TASKS_DIFF=$(git -C "$(pwd)" status --porcelain -- '.agents/specs/**/tasks.md' 2>/dev/null | head -5)
if [[ -n "$TASKS_DIFF" ]]; then
cat >&2 <<EOF
📝 提醒:tasks.md 有未 commit 的變動
$(echo "$TASKS_DIFF")
記得在結束前 commit。
EOF
fi
exit 0
-79
View File
@@ -1,79 +0,0 @@
# SDD 協議(每次啟動必讀)
## 第零原則:沒讀 SDD 不准動 code
任何 `.go` / `.ts` / `.tsx` / `.wasm` 相關變動,**必須**按以下順序執行。**不得簡化,不得跳過**。
### 步驟 1:讀總進度
先讀 `.agents/specs/arcrun/arcrun.md`,了解當前 Phase。
### 步驟 2:定位對應 SDD
根據任務性質找對應 SDD
| 任務類型 | 對應 SDD |
|---------|---------|
| Auth primitive WASM 零件(static_key/oauth2/service_account/mtls | `.agents/specs/arcrun/credential-primitives-wasm/` |
| 清除 cypher-executor 裡的 TS 業務邏輯 | `.agents/specs/arcrun/credential-primitives-wasm/` |
| WASI shim host functionskv_get / crypto_decrypt / crypto_sign_rs256 | `.agents/specs/arcrun/credential-primitives-wasm/` |
| Auth Recipe 系統(recipe schema、KV 格式) | `.agents/specs/arcrun/auth-recipe.md` |
| Landing Page | `.agents/specs/arcrun/landing-page.md` |
| CLI / SDKPython/JS | `.agents/specs/arcrun/sdk-and-website/` |
| arcrun-core-mvp 整體架構 | `.agents/specs/arcrun-core-mvp/` |
| Platform Evolution | `.agents/specs/arcrun-platform-evolution/` |
| Credential 長期規格(需求源) | `docs/user_requirements/credential_parts.md` |
`design.md``tasks.md` 兩份。
### 步驟 3:宣告(強制格式)
開始動手前,在回覆開頭**逐字**貼出以下宣告:
```
📋 已讀 SDD
- .agents/specs/arcrun/arcrun.md(當前 Phase<phase 名稱>
- <對應 SDD 的 design.md 路徑>
- <對應 SDD 的 tasks.md 路徑>
🎯 本次對應 task<task 編號,例如 "Phase 1.3 實作 auth_static_key main.go">
📐 本次 task 的 SDD 規範摘要:
- <重點 1>
- <重點 2>
- <重點 3>
🚧 執行範圍:
- 會修改:<檔案清單>
- 會建立:<檔案清單>
- 會刪除:<檔案清單>
```
**不做這個宣告 = 違反 SDD 協議 = 停手等 richblack**。
### 步驟 4check tasks.md 狀態
動手前:在 tasks.md 把對應 task 的 `- [ ]` 改成 `- [🔄]`(進行中標記)。
完成後:改成 `- [x]`,不批次更新,每完成一個就立刻改。
## 什麼算「任務超出 SDD 範圍」?
以下情況屬於 **change**,不是 **modify****必須停手並與 richblack 確認**
- SDD 沒寫到的新功能
- 新增頂層目錄
- 新增新的 Worker(不管是 cypher-executor / registry / 零件 worker
- 修改架構決策(例如「改用 xxx 取代 yyy」)
- 跨多個子系統的連鎖修改
**停手不是怯懦,是專業**。猜錯方向比慢一小時更糟。
## 發現 SDD 本身有問題怎麼辦?
- SDD 和實作不一致 → 停手,列出矛盾點,與 richblack 確認哪一邊是對的
- SDD 規範之間互相矛盾(例如禁令 A 和設計 B 衝突)→ 停手,引用矛盾原文,與 richblack 確認
- **不可以自行猜哪個是對的**。CC 之前兩天就是這樣走錯的。
## 為什麼這個協議存在
arcrun 規範已經足夠細緻,CC 之前出錯不是因為不懂,而是因為**沒讀**或**讀了覺得「大概是這個意思」就動手**。SDD 協議強制把「先讀 → 定位 → 宣告 → 執行 → 更新」做成一條死規矩,沒有繞過去的路徑。
-76
View File
@@ -1,76 +0,0 @@
# 技術棧硬限制
## 三層語言對應(絕對不可混用)
| 層級 | 語言 | 位置 | 職責 |
|-----|------|------|------|
| 零件(Component | **TinyGo 或 AssemblyScript → WASM** | `registry/components/{name}/` | 所有業務邏輯 |
| 零件 Worker 包裝 | TypeScript(固定模板,不寫業務邏輯) | `.component-builds/{name}/` | WASI shimstdin/stdout JSON |
| Orchestration Worker | TypeScript + Hono | `cypher-executor/` | HTTP routing、workflow 執行排程、host functions |
| CLI | TypeScript + Node.js | `cli/` | `acr` 指令 |
| Python SDK | Python | `python-sdk/` | HTTP thin wrapper + client 端加密 |
| JS SDK | TypeScript + Web Crypto | `js-sdk/` | HTTP thin wrapper + client 端加密 |
| Frontend | React 19 + Vite + Tailwind v4 | `landing/` | Cloudflare Pages |
## 零件實作規範
### 只有兩種合法語言
- **TinyGo**`tinygo build -target=wasi -o {name}.wasm main.go`
- **AssemblyScript**`asc main.ts --target release -o {name}.wasm`
### I/O 模型
- **stdin**JSON input
- **stdout**JSON output
- 不用 HTTP server,不監聽 socketWASI preview1 沒 socket
### Host Functions(零件呼叫外部能力的唯一管道)
`u6u` namespace 下:
| Host Function | 用途 |
|---|---|
| `u6u.http_request` | 發 HTTP 請求 |
| `u6u.kv_get` | 讀 Cloudflare KVWorker 側依 key 前綴路由到正確 KV) |
| `u6u.crypto_decrypt` | AES-GCM 解密(encryption key 永不暴露給 WASM |
| `u6u.crypto_sign_rs256` | RSA-SHA256 簽章(PKCS8 bytes 傳入) |
**所有 host function 在 `cypher-executor/src/lib/wasi-shim.ts` 實作**。零件透過 WASI import 使用。
## 資料儲存
| 儲存 | 用途 | Key 格式 |
|-----|------|---------|
| Cloudflare KV `WEBHOOKS` | workflow 定義(cypher binding YAML | `webhook:{api_key}:{name}` |
| Cloudflare KV `CREDENTIALS_KV` | 加密 credential | `{api_key}:cred:{name}` |
| Cloudflare KV `RECIPES` | auth recipe / 動態 API recipe | `auth_recipe:{service}`, `rec_{hash}` |
| Cloudflare KV `USERS_KV` | 用戶資料 | `user:{api_key}` |
| Cloudflare KV `SESSIONS_KV` | session | `session:{token}` |
| Cloudflare KV `ANALYTICS_KV` | 執行分析 | `execution:{timestamp}:{id}` |
| Cloudflare KV `EXEC_CONTEXT` | workflow 執行中的 context | `ctx:{execution_id}:{node_id}` |
| Cloudflare R2 `WASM_BUCKET` | **只用於用戶自製零件**Phase 5 以後啟用) | `{api_key}:cmp:{hash}` |
**警告:R2 不存平台內建零件的 WASM**。平台零件已 bundle 進各自的 Worker binary`[[wasm_modules]]``import ... assert { type: 'webassembly' }`)。
## 加解密規範
- **演算法**AES-GCM 256-bit
- **加密位置**Client 端(CLI / Python SDK / JS SDK
- Python`cryptography` 套件
- JSWeb Crypto API`crypto.subtle`
- **解密位置**Server 端 **WASM primitive**(透過 host function `crypto_decrypt`
- cypher-executor TS **不解密**,只提供 host function
- `ENCRYPTION_KEY` 只在 Worker host function 內部讀取,**永不經 stdin / 回傳值傳給 WASM**
- **傳輸格式**`{ name, encrypted, iv }`iv base64、encrypted base64
## 網路部署
- **平台 API(對外)**`cypher.arcrun.dev`cypher-executor
- **Landing**`arcrun.dev`
- **每個零件 Worker**
- **對內(cypher-executor 用來 fetch component,避開同 zone 死鎖)**`arcrun-{kebab}.{WORKER_SUBDOMAIN}.workers.dev`
- 例:`arcrun-kbdb-get.uncle6-me.workers.dev`
- cypher-executor 從 `wrangler.toml [vars] WORKER_SUBDOMAIN` 組出此 URL
- **對外(可選,零件對全網開放被 curl 用)**`{kebab}.arcrun.dev`
- 例:`gmail.arcrun.dev``kbdb-get.arcrun.dev`
- 仍允許保留,但**禁止 cypher-executor 透過此 URL fetch**(會撞同 zone 自循環,見 [docs/incidents/2026-05-13-cypher-outbound-522.md](../../docs/incidents/2026-05-13-cypher-outbound-522.md)
- **新增 component worker 部署清單**`name = "arcrun-{kebab}"` + `[[routes]]` 對外(可選)+ dashboard 啟用 workers.dev(必須)
- **部署工具**Wrangler
-132
View File
@@ -1,132 +0,0 @@
# 禁止行為清單(零容忍)
**這份清單由 `.claude/hooks/*.sh` 強制執行。違反會 block 工具呼叫(exit 2**。
---
## 第一類:零件實作層級的禁令
### 1.1 禁止在 `registry/components/` 下建立 TypeScript 檔案
零件**只能**用 TinyGo`.go`)或 AssemblyScript`.ts` 但需 `asconfig.json`)實作,並編譯成 `.wasm`
cypher-executor/registry Worker 或 `.component-builds/` 內的 TS 不算零件邏輯,那是 WASI shim。
**Hook 會擋**:新增 `registry/components/*/{檔案}.ts`(除非目錄內有 `asconfig.json` 明確標記為 AssemblyScript)。
### 1.2 禁止建立新的 `auth_*` 目錄以外的 auth 實作
所有 auth 邏輯只能在:
- `registry/components/auth_static_key/`
- `registry/components/auth_oauth2/`
- `registry/components/auth_service_account/`
- `registry/components/auth_mtls/`
**不可以**出現 `cypher-executor/src/auth-primitive/``cypher-executor/src/lib/auth-*.ts``auth-worker/``credential-worker/` 等目錄。
**Hook 會擋**`mkdir``Write` 到上述違規路徑。
### 1.3 禁止用 `wrangler init/generate` 建立 auth/credential/jwt 相關的 TS Worker
Auth primitive 必須透過 `component-worker-template/` 搭配 WASM binary 部署。
**Hook 會擋**bash 指令含 `wrangler (init|generate) ... auth_``... credential_``... jwt_` 的 pattern。
---
## 第二類:cypher-executor TS 的禁令
### 2.1 禁止新增任何 credential / auth / jwt 相關的 TS 檔案
**已存在但要刪**Phase 1-3 範圍):
- `cypher-executor/src/actions/credential-injector.ts` → 刪除(走 WASM auth primitive
- `cypher-executor/src/lib/jwt-signer.ts` → 刪除(RS256 移入 auth_service_account WASM
- `cypher-executor/src/lib/component-loader.ts``BUILTIN_API_RECIPES``BUILTIN_CREDENTIALS_MAP` → 整段刪除
**Hook 會擋**:新增任何路徑含以下關鍵字的 `.ts` 檔案:
- `credential-injector``credential_injector`
- `jwt-signer``jwt_signer`
- `auth-dispatcher` 的 TS 若嘗試在裡面實作 credential 解密 / template 展開 / JWT signingblock
### 2.2 禁止在 cypher-executor 任何 TS 裡實作以下邏輯
這些邏輯全部屬於 WASM 零件職責:
- AES-GCM 解密(`crypto.subtle.decrypt`)— 只准出現在 `wasi-shim.ts``crypto_decrypt` host function
- RSA-SHA256 簽章(`crypto.subtle.sign` with RSASSA-PKCS1-v1_5)— 只准出現在 `wasi-shim.ts``crypto_sign_rs256` host function
- Template 展開(`{{secret.X}}` / `{{runtime.X}}` 替換)— 只能在 WASM 零件內
- PEM → PKCS8 解析
- JWT header/payload/signature 組裝
- Token exchange(拿 service account JWT 換 access_token
- 具體 API call 實作(例如 gmail send / telegram sendMessage / google sheets append
**Hook 會擋**
- Write/Edit 到 `cypher-executor/src/` 下的 `.ts` 時,內容含:
- `crypto\.subtle\.decrypt` 且檔名不是 `wasi-shim.ts`
- `crypto\.subtle\.sign.*RSASSA` 且檔名不是 `wasi-shim.ts`
- `interpolateTemplate``\{\{secret\.` 的模板邏輯
- `BUILTIN_API_RECIPES``BUILTIN_CREDENTIALS_MAP`(新增用)
- `gmail.googleapis.com/gmail/v1/users/me/messages/send` 類 hard-code API URL
- `api.telegram.org/bot.*sendMessage`
- `sheets.googleapis.com/v4/spreadsheets`
- `notify-api.line.me/api/notify`
### 2.3 cypher-executor TS 的合法職責(允許)
- HTTP routingHono routes
- workflow 執行排程(`graph-executor.ts`
- 呼叫 WASM 零件(透過 HTTP fetch 到對應 Worker URL,或 Service Binding fallback
- 提供 host function`wasi-shim.ts``kv_get` / `crypto_decrypt` / `crypto_sign_rs256`
- KV/R2/Service Binding 存取封裝
---
## 第三類:架構層級的禁令
### 3.1 禁止新增 Service Binding
**Cypher binding 不是 Cloudflare service binding**。它是 YAML/KV 裡的 URL 清單。
零件串接(workflow 層)一律走 HTTP URL,不走 `[[services]]`
15 個現有的 `SVC_*` 綁定(`cypher-executor/wrangler.toml`)是歷史遺產(邏輯零件效能優化),**保留但不新增**。
**Hook 會擋**bash 指令含 `wrangler tail` 以外、涉及 `[[services]]` 新增的 patternEdit wrangler.toml 新增 `[[services]]` 區塊時警告確認。
### 3.2 禁止以「從 R2 取 WASM」為設計
平台內建零件已 bundle 進各自 Worker,不從 R2 取。
R2 只在 Phase 5(用戶自製零件)啟用。
**Hook 會警告**TS 中出現 `env.WASM_BUCKET.get(` 的新增 code(除非在明確標註的 Phase 5 user-submit 路徑中)。
### 3.3 禁止複製貼上 Worker 程式碼到新目錄
要改 `gmail` 零件 → 改 `registry/components/gmail/main.go`,重新編譯、部署。
**不准**新建 `gmail-v2/``new-gmail/``gmail-worker/` 等目錄。
**Hook 會擋**`mkdir``Write``{component-name}-v2/``new-{component-name}/``{component-name}-worker/` 類路徑。
### 3.4 禁止在 SDK 內做 server 職責
- **禁止**SDK 裡做 server 端解密、credential-injector 重實作、workflow executor、auth recipe 解析
- **允許**SDK 做 HTTP thin wrapper + client 端加密(AES-GCM
---
## 第四類:流程層級的禁令
### 4.1 禁止沒讀 SDD 就動 code
`00-sdd-protocol.md`
### 4.2 禁止批次更新 tasks.md
每完成一個 task 就立刻 mark `- [x]`。不准「先全部做完再一次更新」。
### 4.3 禁止新建 SDD 而不事先與 richblack 確認
SDD 屬於架構決策,必須人確認。CC 不可以自行在 `.agents/specs/` 底下建新目錄。
例外:在現有 SDD 目錄內新增 `requirements.md` / `design.md` / `tasks.md` 的單檔補充(需在 CLAUDE.md 已註記的 SDD 範圍內)。
---
## Hook Block 訊息格式
當 hook 擋住一個操作時,訊息格式統一為:
```
❌ BLOCKED by arcrun CLAUDE rules
違反項:<禁令編號,例如 2.2>
原因:<簡短說明>
正確做法:<該改去哪裡、該用什麼方式>
參考:.claude/rules/<對應檔案>
```
這樣 CC 拿到錯誤訊息後有機會自行導正,不是被擋死就愣住。
-161
View File
@@ -1,161 +0,0 @@
# 零件架構與部署模式(必讀,CC 最常搞錯的地方)
## 第一核心概念:每個 WASM 零件 = 一個獨立 Worker = **兩個** URL
**不是**從 R2 即時載入 WASM 執行。
**不是**用 service binding 串零件。
**不是**一個 Worker 裡跑多個零件。
**是**:每個零件都是獨立部署的 Worker,每個都有**兩個 URL**
| URL 類型 | Pattern | 用途 |
|---|---|---|
| 對內(cypher-executor 用)| `arcrun-{kebab}.{WORKER_SUBDOMAIN}.workers.dev` | cypher-executor fetch component 走這個,避開同 zone 自循環死鎖(P0 #9|
| 對外(直接 curl 用,可選)| `{kebab}.arcrun.dev` | 用戶單獨打 component 測試或 self-hosted 用法 |
例:`kbdb_get` 零件:
- 對內:`arcrun-kbdb-get.uncle6-me.workers.dev`cypher-executor 走這個)
- 對外:`kbdb-get.arcrun.dev`(用戶 / 直 curl
**為什麼這樣設計**CF Workers 「同 zone 自循環防護」會讓綁 `cypher.arcrun.dev/*` 的 cypher-executor fetch 同 zone `*.arcrun.dev` 撞 522。完整事件報告:[docs/incidents/2026-05-13-cypher-outbound-522.md](../../docs/incidents/2026-05-13-cypher-outbound-522.md)。改走 workers.dev 子域繞過。
### 零件 Worker 的結構
```
registry/components/{name}/
├── main.go ← TinyGo 原始碼(實際零件邏輯)
├── component.contract.yaml ← 輸入/輸出規格
└── {name}.wasm ← TinyGo 編譯產物
```
部署時,透過 `component-worker-template/` 把 WASM 包進一個 Hono Worker
```
.component-builds/{name}/
├── package.json
├── wrangler.toml ← name = "arcrun-{name}"route = "{name}.arcrun.dev"
├── component.wasm ← 從 registry/components/{name}/ 複製過來
└── src/index.ts ← 固定的 WASI shimPOST / → stdin → WASM → stdout → JSON
```
**src/index.ts 是通用模板**,所有零件都用同一份。這個 TS 只做 WASI runtime,不是業務邏輯。
---
## R2WASM_BUCKET)的真正用途
R2 存 WASM 只是**用戶自製零件上傳**用的。
**平台內建零件不從 R2 讀取**——它們在部署時就已 bundle 進 Worker 的 binary(透過 `[[wasm_modules]]``import` with `assert { type: 'webassembly' }`)。
Phase 5(封測後)才會啟用「用戶 push 自製零件 → 存 R2 → 動態執行」這條路徑。
**結論:當 CC 問「怎麼從 R2 取出 WASM」時,幾乎都是走錯路徑**。平台零件是獨立 Worker,走 HTTP 呼叫,不是 R2 動態載入。
---
## Cypher binding 的正確定義
**Cypher binding 不是 Cloudflare 的任何 binding 機制。**
Cypher binding 是一張 YAML 清單,內容是「一個 workflow 要呼叫哪些零件 URL」。存放在:
- 本地:`workflow.yaml`(用戶寫的 workflow
- KV`WEBHOOKS` KV(用戶 `acr push` 後存入)
Cypher executor 執行 workflow 時:
1. 從 KV 讀出 workflow YAML
2. 按 graph 順序解析每個節點的 `component`
3. 用 HTTP fetch 打對應的零件 URL
4. 把 output 當作下個節點的 input
**這就是 Cypher binding——用 HTTP URL 把零件串起來,存在 YAML/KV 裡**。
### 為什麼不能用 Service Binding
Service binding 需要 `wrangler.toml` 裡寫死 `[[services]]`,且要 redeploy 才生效。arcrun 是類 n8n 服務,用戶建立新 workflow 時**絕對不可能**要他 redeploy。所以 workflow 層一定要 HTTP。
### Service Binding 的僅存合法用途
只在 `cypher-executor` 和**平台內建邏輯零件之間**保留(效能優化,避免公網往返)。看 `cypher-executor/wrangler.toml` 裡的 15 個 `[[services]]` 綁定就是這個用途。
**禁止新增任何 Service Binding**。所有新零件(特別是 auth primitive)都走 HTTP URL 路徑。
---
## 零件之間怎麼串:實際流程
假設 workflow 是:webhook → gmail(要 auth)→ google_sheets(要 auth
```
用戶 POST https://cypher.arcrun.dev/webhooks/named/xxx/trigger
cypher-executorWorker)讀 workflow YAML
├─ 節點 1: component = gmail
│ a. 查 auth_recipe:gmail → primitive = static_key
│ b. HTTP POST https://auth-static-key.arcrun.dev
│ { action: "authenticate", api_key, service: "gmail" }
│ → 回傳 { auth_headers: { Authorization: "Bearer ..." } }
│ c. HTTP POST https://gmail.arcrun.dev
│ { to, subject, body, _auth_headers }
│ → gmail 零件 Worker 執行 WASM → 回傳 { success, data }
└─ 節點 2: component = google_sheets
... 相同模式
```
**cypher-executor 本身不做 credential 解密、不做 JWT signing、不做 auth header 組裝**。這些全在 auth primitive WASM 零件內,cypher-executor 只負責 HTTP routing 和工作流排程。
---
## 實際禁令(CC 看這裡)
### 禁止在 `registry/components/` 下建立 TypeScript 檔案
零件邏輯一律 TinyGo 或 AssemblyScript,編譯成 `.wasm`
### 禁止把 auth 邏輯寫在 `cypher-executor/src/`
credential 解密、JWT signing、template 展開(`{{secret.X}}`)全部屬於 auth primitive WASM 零件的職責。cypher-executor 只呼叫它們。
### 禁止問「怎麼從 R2 取 WASM」
平台內建零件**不從 R2 取**。每個零件已部署成獨立 Worker,走 HTTP URL。用戶自製零件才用 R2(Phase 5,未啟用)。
### 禁止新增 Service Binding
15 個現有的 SVC_* 是歷史遺產(邏輯零件效能優化),不新增。新零件(尤其 auth primitive)一律走 HTTP URL。
### 禁止重建已存在的零件 Worker
要改 `gmail` 零件邏輯 → 改 `registry/components/gmail/main.go`,重新編譯 `.wasm`,重新部署對應 Worker。**不要**在 `cypher-executor/src/lib/` 或其他地方建「新的 gmail 實作」。
---
## 部署一個新零件的完整步驟(auth_static_key 為例)
1. 建立 `registry/components/auth_static_key/`
- `main.go`TinyGo 實作)
- `component.contract.yaml`IO 規格)
2. 編譯:`cd registry/components/auth_static_key && tinygo build -target=wasi -o auth_static_key.wasm main.go`
3. 建立 `.component-builds/auth_static_key/`
- 複製 `component-worker-template/src/index.ts`
- 複製 `component-worker-template/package.json`
- 新建 `wrangler.toml`
```toml
name = "arcrun-auth-static-key"
main = "src/index.ts"
compatibility_date = "2025-02-19"
[vars]
COMPONENT_ID = "auth_static_key"
[[routes]]
pattern = "auth-static-key.arcrun.dev/*"
zone_name = "arcrun.dev"
```
- 複製 `auth_static_key.wasm` 到此目錄為 `component.wasm`
4. `cd .component-builds/auth_static_key && pnpm install && pnpm deploy`
5. **Dashboard 啟用 workers.dev URL**(必須,否則 cypher-executor fetch 不到):
- Workers & Pages → `arcrun-auth-static-key` → Settings → Domains & Routes → workers.dev → Enable
- 啟用後 URL`arcrun-auth-static-key.{WORKER_SUBDOMAIN}.workers.dev`
6. 驗證對外:`curl https://auth-static-key.arcrun.dev` → 應回 `{ok: true, component: "auth_static_key"}`
7. 驗證對內:`curl https://arcrun-auth-static-key.{WORKER_SUBDOMAIN}.workers.dev` → 應同樣回 200
8. cypher-executor 透過 `wasmWorkerUrl()` 自動組對內 URL 呼叫(不用手動註冊)
**這是唯一正確的部署流程**。任何偏離這個流程的「替代方案」都要先和 richblack 確認。
**Step 5 為什麼必須**:見 arcrun.md P0 #92026-05-13)。cypher-executor 走對內 URL 避開同 zone 自循環死鎖;若 workers.dev 未啟用,cypher-executor fetch 該 component 會 404。
-77
View File
@@ -1,77 +0,0 @@
# 當前進度(SessionStart 會注入此檔重點)
> 更新時間:2026-04-19
> 權威來源:`.agents/specs/arcrun/credential-primitives-wasm/tasks.md`
> 此檔僅摘要,詳細狀態以 tasks.md 為準。
---
## 封測狀態
**原定明天封測,richblack 決定推遲**,原因:cypher-executor 有三套 TS 業務邏輯違反「零件一律 WASM」架構原則(Phase 1-3 要清除的程式碼),在清除前不封測。
---
## 目前 PhaseCredential Primitives TS → WASM
**SDD 位置**`.agents/specs/arcrun/credential-primitives-wasm/design.md` + `tasks.md`
### 已完成
- **Phase 0.10.5**:核心合併(u6u-core 併入 arcrun、21 個零件 contract 完整、刪除重複 `credentials/` 目錄、CREDENTIALS_KV binding 確認、刪除 `matrix/u6u-core/`
- `registry/components/` 下 21 個零件(邏輯 + API)都有 `main.go` + `.wasm`
### 進行中 / 未完成
| Task | 狀態 | 阻擋關係 |
|-----|------|---------|
| 0.6 wasi-shim 新增 `kv_get` / `crypto_decrypt` / `crypto_sign_rs256` host functions | ⬜ 未開始 | **Phase 1-3 的硬前置** |
| 0.7 component-loader 新增 WASM runner 路徑 | ⬜ 未開始 | **Phase 1-3 的硬前置** |
| 1.1-1.8 `auth_static_key` WASM 零件(TinyGo | ⬜ 未開始 | 涵蓋 80% 服務 |
| 2.1-2.6 `auth_service_account` WASM 零件(JWT signing | ⬜ 未開始 | Google Service Account 等 |
| 3.1-3.5 清除 `component-loader.ts``BUILTIN_API_RECIPES` | ⬜ 未開始 | 要先有 Phase 1-2 的 WASM 零件 |
| 4.1-4.4 `auth_oauth2` + `auth_mtls`(封測後) | ⬜ 未開始 | 非阻擋項 |
| 5.1-5.7 核心穩定驗證(全域搜尋確認無殘餘 TS) | ⬜ 未開始 | 封測啟動門檻 |
### Phase 1-3 要**徹底刪除**的 TS 檔案(不是搬、不是改,是刪)
| 檔案 | 違反什麼 |
|-----|---------|
| `cypher-executor/src/actions/credential-injector.ts` | AES 解密、template 展開、JWT 邏輯 —— 應在 WASM |
| `cypher-executor/src/lib/jwt-signer.ts` | RS256 JWT 簽章邏輯 —— 應在 `auth_service_account.wasm` |
| `cypher-executor/src/lib/component-loader.ts``BUILTIN_API_RECIPES`~100 行) | gmail/telegram/line/gsheets/http_request/cron 的 TS 實作 —— 應全部走對應 WASM 零件 |
---
## 下一個 session 第一件要做的事
**讀 `.agents/specs/arcrun/credential-primitives-wasm/tasks.md`**,然後決定從 Phase 0.6 還是 0.7 開始。
0.6host functions)和 0.7WASM runner)是並列的前置工作,哪個先都可以,但都要在 Phase 1 開始之前完成。
---
## SDD 索引
| 子系統 | SDD |
|--------|-----|
| **主要(正在動)** Credential Primitives WASM 改寫 | `.agents/specs/arcrun/credential-primitives-wasm/` |
| **LI (LLM Interface)** — AI 操盤手使用體驗(2026-05-16 新建,mira dogfood 痛點轉化) | `.agents/specs/llm-interface/` |
| arcrun 總進度 | `.agents/specs/arcrun/arcrun.md` |
| Auth Recipe 系統(schema、預建 20 個服務) | `.agents/specs/arcrun/auth-recipe.md` |
| Landing Page | `.agents/specs/arcrun/landing-page.md` |
| SDK + Website | `.agents/specs/arcrun/sdk-and-website/design.md` |
| arcrun MVP 整體 | `.agents/specs/arcrun-core-mvp/design.md` |
| Credential 長期規格(需求源) | `docs/user_requirements/credential_parts.md` |
| Platform Evolution | `.agents/specs/arcrun-platform-evolution/design.md` |
| Tech Stack 詳細 | `.agents/steerings/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`
-138
View File
@@ -1,138 +0,0 @@
# 部署慣例(CI/CD)
> **核心原則:新增 Worker = 新目錄 + `wrangler.toml`,不用改 workflow。**
`.github/workflows/deploy.yml` 是**通用掃描式** workflow,不該為每個 Worker 手寫 job。
---
## Workflow 如何找到要部署的 Worker?
```
find . -name 'wrangler.toml' -not -path '*/node_modules/*' -not -name 'wrangler.test.toml'
```
每一個命中的目錄 = 一個部署單位。無論是:
- `cypher-executor/` (orchestration Worker)
- `registry/` (合約管理 Worker)
- `.component-builds/{name}/` (零件 Worker,25+ 個)
- 未來新增的任何 Worker
**無需改 workflow,只要符合掃描規則就會自動部署**。
---
## 觸發邏輯
| 觸發 | 部署範圍 |
|------|---------|
| `push` 到 main | diff 涉及的 Worker 目錄才部署 |
| `push` 到 main + 改 `registry/components/{name}/` | 連動 rebuild `.component-builds/{name}/component.wasm` 再 deploy |
| `workflow_dispatch` + `force_all=true` | 全部 Worker |
| `workflow_dispatch` + `only=a,b,c` | 只部署指定清單 |
| `push` 但 base sha 不可及(首次) | 全部 Worker |
---
## 新增 Worker 的步驟
### 如果是新 WASM 零件 Worker
1. 在 `registry/components/{new_name}/``main.go` + `component.contract.yaml`
2. 在 `.component-builds/{new_name}/` 建 Worker 模板:
- `wrangler.toml`(name/routes/bindings)
- `package.json`(hono + workers-types + wrangler 即可,參考 `auth_static_key/package.json`)
- `tsconfig.json`(可直接複製)
- `src/index.ts`(WASI shim,方案 A:import `../../cypher-executor/src/lib/wasi-shim`)
3. 本地跑 `pnpm install``pnpm-lock.yaml`
4. 本地跑 `tinygo build -target=wasi -o {new_name}.wasm main.go` 先驗證 build 通過
5. Commit push → CI 自動 rebuild WASM + deploy
### 如果是新 orchestration/service Worker
1. 在 repo 根建新目錄(類似 `cypher-executor/`)
2. `wrangler.toml` + `package.json` + `pnpm-lock.yaml` + `src/index.ts` + `tsconfig.json`
3. Push → CI 自動部署
---
## Runtime Secret 管理
**CI 只提供 Cloudflare 驗證,不碰 runtime secret**。
- GH Actions secrets:`CLOUDFLARE_API_TOKEN``CLOUDFLARE_ACCOUNT_ID`(一次性設好)
- Runtime secret(例:`ENCRYPTION_KEY``OPENAI_KEY``GOOGLE_API_KEY`):
- **由 richblack 一次性手動** `wrangler secret put <KEY>` 設進各 Worker
- 不進 CI,不進 `wrangler.toml` `[vars]`
- 需要的 Worker:`auth_static_key``auth_service_account`(兩個都要 `ENCRYPTION_KEY`)
---
## Lockfile 規範
- **統一使用 pnpm**。新增 Worker 只放 `pnpm-lock.yaml`,不要 `package-lock.json`
- 若新建 Worker 時用 `npm install` 產出 `package-lock.json`,**刪掉它**,改跑 `pnpm install`
- `cypher-executor/``registry/``package-lock.json` 已於 2026-04-20 刪除
**現存例外**(歷史遺產,混合期不強制遷移):
- `.component-builds/{if_control, switch, ... 16 個舊邏輯零件}/` 仍是 `package-lock.json`,workflow 有 fallback 分支(`pnpm install --no-frozen-lockfile`)可跑
- `builtins/``landing/` 同上
**新增 Worker 一律 pnpm,不要製造新的混合情況**。
---
## WASM 來源
> **⚠️ 慣例變更(richblack 2026-06-02self-hosted 開源策略)**
> 原慣例「`.component-builds/{name}/component.wasm` 不 commit 進 repo」**已推翻**。
> 現在 **commit `.component-builds/*/component.wasm` 進 repo**,因為 self-host 用戶 / `acr init --self-hosted`
> 從 GitHubcodeload tarball)直接拿這份 wasm 部署到自己的 CF——repo 必須自帶可部署的 wasm。
> 決策依據:`.agents/specs/arcrun/sdk-and-website/self-hosted-init.md §6`
### 現行規則(2026-06-02 起)
- **`.component-builds/*/component.wasm` → commit 進 repo**(部署來源)。`.gitignore` 用否定規則放行:
```
*.wasm # 預設排除
!.component-builds/**/component.wasm # 例外放行部署物
```
- **`registry/components/*.wasm` → 仍不 commit**(build 中間產物,部署不直接用,`.gitignore` 仍排除)。
- 本地開發 build`cd registry/components/{name} && tinygo build -target=wasi -o {name}.wasm main.go && cp {name}.wasm ../../../.component-builds/{name}/component.wasm`**然後 commit `.component-builds/{name}/component.wasm`**。
- CIdeploy.yml):仍在 deploy 前自動 rebuild + copy(部署 prod 用最新 source;與 repo 內 commit 的 wasm 不衝突——前者給 CI deploy prod,後者給 self-host 用戶當部署來源)。
### 誠實 trade-offmindset §7
commit wasm 進 repo → 每次 rebuild 在 git 歷史累積二進位,**repo 長期會膨脹**。
可接受(self-host 體驗優先),未來若膨脹過劇再考慮 git-lfs / 按需安裝(self-hosted-init.md §6.6)。
---
## 並行度
`max-parallel: 5` — 避免觸發 Cloudflare Workers API rate limit。
Worker 數量 > 5 時,deploy 會分批跑。25 個 Worker 大約 5 輪 × ~30 秒 = 2-3 分鐘可完成全部。
---
## 禁止事項
1. **禁止**為新 Worker 手動加 deploy job 到 `deploy.yml`。通用掃描會自動處理,手加就是重複工作。
2. **禁止**把 runtime secret(API key / encryption key / credential)放進 GH Actions secrets 或 `wrangler.toml` `[vars]`,只能用 `wrangler secret put`
3. **禁止**在 CI 裡跑不必要的測試阻擋 deploy。測試在 PR / 本地跑,`main` 推上去就 deploy(trunk-based)。若要測試關,開新 workflow 檔,不要污染 deploy workflow。
4. **禁止**跳過 TinyGo rebuild 直接 deploy 舊 `.wasm`。CI 的 rebuild 步驟是確保部署的是最新 source。
---
## 驗證指令
本地模擬 CI 的掃描結果:
```bash
find . -name 'wrangler.toml' -not -path '*/node_modules/*' -not -name 'wrangler.test.toml' \
| xargs -n1 dirname | sort -u
```
應列出 ~25 個目錄。任何「我新增了 Worker 但沒被 deploy」的問題,先跑這條確認目錄被掃到。
-60
View File
@@ -1,60 +0,0 @@
# arcrun mindset(給 CC 的世界觀 — 走歪前先讀這個)
> 這是「為什麼」層,不是禁令層(禁令在 02-forbidden)。
> 違反禁令會被 hook 擋;違反 mindset 不會被擋,但會讓你做出「技術上對、方向上錯」的東西。
> 來源:2026-05 richblack 與 CC 的一連串判準釐清(權威記錄在 DECISIONS.md)。
>
> 註:這是步驟 7「arcrun-mindset Skill」的前身。Skill 正式建立前,本檔是 CC 的 mindset 來源。
---
## 1. 工作流是 default,零件是稀有例外
AI 開發時的預設順序:
1. **預設寫工作流**(串服務 / 自用 / 給少數人用 → 全部先工作流,用 http_request + 流程控制 primitive 串)。
2. **要打的服務不提供串接但有 API,且有必要讓全 arcrun 生態重用** → 才建零件(零件 = API 薄殼,只打一個 endpoint)。
3. **建零件前必問「你有必要嗎?」**:看到「有 API 可包成零件」≠「該包」。自用 → 工作流。
**CC 的典型走歪**:把「需要一個能力」直接翻成「做一個零件」,把「能包」當「該包」。
mira 的 claude_api / km_writer 就是這樣被錯做成零件的(其實是自用服務膠水,該是工作流)。
## 2. Arcrun 是 AI 呼叫的工具(AI → 工具),不是工具回頭呼叫 AI
需要 AI 判斷 / 轉換時,是**操盤的 CC 自己做**,再呼叫工作流做確定性的下一步。
**不要在工作流裡放零件回頭呼叫一個 LLM**。n8n 需要 AI 節點是因為它沒大腦;arcrun 的大腦就是 CC。
ai_transform_compile/run 因此被刪除。)
## 3. arcrun 不做授權判斷
「能不能打通」由發 API key 的服務裁決,不是 arcrun。401/403 是對方服務在行使授權,不是 arcrun 的 bug。
auth_recipe 只定義「怎麼認證」,不含「誰准用」清單。不要加「arcrun 替用戶擋掉某些 endpoint」的功能。
## 4. 零件投稿走 GitHub PR(人 merge = 人類閘門)
零件投稿不是 registry self-service,是 GitHub PR。人 merge = 天然人類閘門(AI 偽造不了 GitHub approve),
把關(假零件偵測 / 純WASI / Gherkin)由 CI PR check 跑(CI 能 runtime 跑 wasmCF Worker 不能)。
§8「不依賴 CI」指執行鏈路(高頻);零件投稿稀有,走 PR/CI 是例外、不違反。
## 5. 發佈安全的底氣是純 WASI 沙箱,不是 Gherkin
Gherkin 全綠 ≠ 零件安全(投稿者可寫避重就輕的 Gherkin)。真正框死破壞力的是**純 WASI 沙箱**
(零件只能 stdin→stdout、無網路 syscall、無檔案系統)。Gherkin 驗契約 + 沙箱框死 + 市場補長尾 = 風險可控,非零風險。
## 6. 暴露 / 送資料的動作 → 人類明示同意(資料外流警示)
把資料 / workflow 變成「可被外部呼叫」(部署 webhook、recipe push= 暴露面 → 需人類明示同意,不分公私庫。
**不禁止**用戶公開(他的自由),但要**確定他自己明示同意**(不是 AI 替他決定)。
警示同時是「保護措施入口」(提示可加 API Key / 權限 / 限流)。
## 7. 誠實限制(最重要的 mindset:不假裝、不假綠)
- **AI 技術上能偽造人類確認**confirmed_by_human、exposure_consent、gherkin_evidence 都能塞)。
這些機制的價值是**法律歸責 + 軌跡可審**,不是技術防偽。**絕不在文件 / 程式裡聲稱「不可能繞過」。**
- **絕不代替人類做有風險的確認**(建零件、暴露資料)。非 TTY(你直跑)就拒絕,不要自己塞 flag 假裝人類同意了 —— 那是明確越界。
- **禁假綠**DECISIONS §3c/§7):stub / 未實作就回 success:false 或明確標 unimplemented,不要回傳假資料假裝成功。
缺 credential 打不到 2xx 就誠實標「未驗收:缺 X」,不 mock 充綠燈。
- **完成 = 客觀證據**(編譯 exit code / HTTP status + trace),不是口頭宣布「我做好了」。
---
詳細判準與來龍去脈見 `DECISIONS.md`。每條都有對應的慘痛教訓,不是憑空規定。
-61
View File
@@ -1,61 +0,0 @@
{
"hooks": {
"SessionStart": [
{
"matcher": "startup|resume|clear",
"hooks": [
{
"type": "command",
"command": "bash .claude/hooks/session-start-load-sdd.sh",
"timeout": 5
}
]
}
],
"PreToolUse": [
{
"matcher": "Write|Edit|MultiEdit",
"hooks": [
{
"type": "command",
"command": "bash .claude/hooks/pre-write-guard.sh",
"timeout": 5
}
]
},
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "bash .claude/hooks/pre-bash-guard.sh",
"timeout": 5
}
]
}
],
"PostToolUse": [
{
"matcher": "Write|Edit|MultiEdit",
"hooks": [
{
"type": "command",
"command": "bash .claude/hooks/post-edit-remind-tasks.sh",
"timeout": 5
}
]
}
],
"Stop": [
{
"hooks": [
{
"type": "command",
"command": "bash .claude/hooks/stop-check-sync.sh",
"timeout": 5
}
]
}
]
}
}
-56
View File
@@ -1,56 +0,0 @@
name: Deploy Landing
# 自動部署 landing/ (Next.js on Cloudflare Pages)
# 觸發:push 到 main 且 landing/ 有變動,或手動
# 為何獨立檔:landing 是 Pages 不是 Worker,跟 deploy.yml (掃 wrangler.toml) 邏輯不同
# Secret 需要:CLOUDFLARE_API_TOKEN, CLOUDFLARE_ACCOUNT_ID(既有)
on:
push:
branches: [main]
paths:
- 'landing/**'
- '.github/workflows/deploy-landing.yml'
workflow_dispatch:
concurrency:
group: deploy-landing-${{ github.ref }}
# 連續 push landing 改動時,舊 build 自動 cancellanding build ~3 min 是大頭,省 minutes 關鍵)
cancel-in-progress: true
jobs:
deploy:
name: Build + Deploy to CF Pages
runs-on: ubuntu-latest
permissions:
contents: read
deployments: write
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
with:
version: 10
- uses: actions/setup-node@v4
with:
node-version: 22
- name: Install
working-directory: landing
run: pnpm install --no-frozen-lockfile
- name: Build (Next.js)
working-directory: landing
run: pnpm next build
- name: Build for Pages
working-directory: landing
run: pnpm exec next-on-pages
- name: Deploy to Cloudflare Pages
uses: cloudflare/wrangler-action@v3
with:
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
command: pages deploy landing/.vercel/output/static --project-name arcrun-landing --branch main --commit-message "ci ${{ github.sha }}"
-350
View File
@@ -1,350 +0,0 @@
name: Deploy Workers
# 通用 deploy workflow:掃描 repo 內所有含 wrangler.toml 的目錄 → matrix fanout 部署
# 新增 Worker = 新目錄 + wrangler.toml + (src/index.ts | component.wasm 等) + pnpm-lock.yaml
# 不用改本檔。詳見 .claude/rules/05-deploy-convention.md。
on:
push:
branches: [main]
# 2026-05-16 leo Actions 配額用爆後加 paths-ignore:純文件 commit 完全不觸發 workflow
# (之前每個 push 都會跑 discover job 吃 ~1 mindoc-only commit 多時積很多)
# worker code 變動仍由 discover job 用 git diff 過濾
paths-ignore:
- '**/*.md'
- 'AGENTS.md'
- '.agents/**'
- 'docs/**'
- 'registry/examples/**'
- 'registry/skills/**'
- '.gitignore'
- 'LICENSE'
workflow_dispatch:
inputs:
force_all:
description: "Deploy all Workers regardless of diff"
type: boolean
default: false
only:
description: "Comma-separated Worker dirs to deploy (overrides diff; empty = auto)"
type: string
default: ""
concurrency:
group: deploy-${{ github.ref }}
# 連續 push 時取消舊跑(節省 GH Actions minutes 配額;M5 之前 LI 開發頻率高)
# 2026-05-16 leo 帳號 Actions 用爆配額被 disable 一次,改用此設定降風險
cancel-in-progress: true
jobs:
# ── Job 1:掃描所有 wrangler.toml 目錄,輸出 deploy matrix ──────────────
# 分兩層:
# tier1 = .component-builds/*(零件 Worker,互不相依,全部平行)
# tier2 = 其他(cypher-executor/registry/builtins,可能透過 service binding 相依於 tier1)
# tier1 全綠後才啟動 tier2,避免 service binding target 未存在造成首次部署失敗。
discover:
name: Discover Workers
runs-on: ubuntu-latest
outputs:
tier1: ${{ steps.emit.outputs.tier1 }}
tier2: ${{ steps.emit.outputs.tier2 }}
tier1_count: ${{ steps.emit.outputs.tier1_count }}
tier2_count: ${{ steps.emit.outputs.tier2_count }}
steps:
- uses: actions/checkout@v5
with:
# 抓全部 history 以確保 github.event.before 可及
# (fetch-depth: 2 會在大批 commit push / force-push 時失效)
fetch-depth: 0
- name: Enumerate & filter
id: emit
env:
FORCE_ALL: ${{ github.event.inputs.force_all }}
ONLY: ${{ github.event.inputs.only }}
EVENT: ${{ github.event_name }}
run: |
set -euo pipefail
# 所有含 wrangler.toml 的 Worker 目錄,排除:
# - node_modules/
# - wrangler.test.toml(測試用)
# - Pages 專案(含 pages_build_output_dir,另有 build pipeline,不適用本 workflow)
mapfile -t all_dirs < <(
find . -type f -name 'wrangler.toml' \
-not -path '*/node_modules/*' \
-not -name 'wrangler.test.toml' \
| while read f; do
# 排除 Pages 專案(wrangler pages deploy 與 wrangler deploy 流程不同)
if grep -q 'pages_build_output_dir' "$f"; then
continue
fi
dirname "$f"
done \
| sort -u \
| sed 's|^\./||'
)
echo "Found ${#all_dirs[@]} worker dirs"
for d in "${all_dirs[@]}"; do echo " - $d"; done
# 決定要部署哪些
declare -a targets=()
if [[ "$EVENT" == "workflow_dispatch" && -n "$ONLY" ]]; then
# 手動觸發 + 指定清單
IFS=',' read -ra req <<< "$ONLY"
for r in "${req[@]}"; do
r="${r// /}"
for d in "${all_dirs[@]}"; do
if [[ "$d" == "$r" ]]; then
targets+=("$d")
fi
done
done
elif [[ "$EVENT" == "workflow_dispatch" && "$FORCE_ALL" == "true" ]]; then
targets=("${all_dirs[@]}")
elif [[ "$EVENT" == "push" ]]; then
# diff 過濾:哪些 worker 目錄有變動?
# 也要連動 registry/components/{name}/ — 改 main.go 應該 redeploy .component-builds/{name}/
base_sha="${{ github.event.before }}"
head_sha="${{ github.sha }}"
# 若 base 為 0000...(首次 push)或 base 在本地不可及,fallback 為全部
if [[ -z "$base_sha" || "$base_sha" == "0000000000000000000000000000000000000000" ]]; then
echo "No base sha, deploy all"
targets=("${all_dirs[@]}")
elif ! git cat-file -e "$base_sha" 2>/dev/null; then
echo "Base sha $base_sha unreachable locally, deploy all"
targets=("${all_dirs[@]}")
else
# 取得 diff 的檔案路徑
mapfile -t changed < <(git diff --name-only "$base_sha" "$head_sha" || true)
echo "Changed files:"
for f in "${changed[@]}"; do echo " $f"; done
for d in "${all_dirs[@]}"; do
# 判斷:若 d 下任何檔案變動,或 d 是 .component-builds/{name} 且 registry/components/{name}/ 下變動
hit=0
for f in "${changed[@]}"; do
if [[ "$f" == "$d"/* ]]; then hit=1; break; fi
done
if [[ $hit -eq 0 && "$d" == .component-builds/* ]]; then
name="${d#.component-builds/}"
for f in "${changed[@]}"; do
if [[ "$f" == "registry/components/$name"/* ]]; then hit=1; break; fi
done
fi
if [[ $hit -eq 1 ]]; then targets+=("$d"); fi
done
fi
else
targets=("${all_dirs[@]}")
fi
echo "Deploying ${#targets[@]} workers (will be split into 2 tiers):"
for t in "${targets[@]}"; do echo " - $t"; done
# 分成兩層:
# tier1 = .component-builds/*(零件 Worker,需要 WASM build)
# tier2 = 其他(orchestration Worker,可能有 service binding 相依於 tier1)
emit_json() {
local -n arr=$1
local out="["
local first=1
for t in "${arr[@]}"; do
local name needs_wasm
name="$(basename "$t")"
needs_wasm="false"
if [[ "$t" == .component-builds/* ]]; then
needs_wasm="true"
name="${t#.component-builds/}"
fi
if [[ $first -eq 0 ]]; then out+=","; fi
out+="{\"name\":\"$name\",\"path\":\"$t\",\"needsWasm\":$needs_wasm}"
first=0
done
out+="]"
echo "$out"
}
declare -a tier1=() tier2=()
for t in "${targets[@]}"; do
if [[ "$t" == .component-builds/* ]]; then
tier1+=("$t")
else
tier2+=("$t")
fi
done
tier1_json=$(emit_json tier1)
tier2_json=$(emit_json tier2)
echo "Tier 1 (${#tier1[@]}):"
for t in "${tier1[@]}"; do echo " - $t"; done
echo "Tier 2 (${#tier2[@]}):"
for t in "${tier2[@]}"; do echo " - $t"; done
echo "tier1=$tier1_json" >> "$GITHUB_OUTPUT"
echo "tier2=$tier2_json" >> "$GITHUB_OUTPUT"
echo "tier1_count=${#tier1[@]}" >> "$GITHUB_OUTPUT"
echo "tier2_count=${#tier2[@]}" >> "$GITHUB_OUTPUT"
# ── Job 2a:Tier 1 並行部署(零件 Worker,需要 WASM build) ───────────────
# tier1 所有 Worker 互不相依,全部平行;tier1 全綠後才啟動 tier2。
deploy-tier1:
name: Deploy tier1/${{ matrix.worker.name }}
needs: discover
if: needs.discover.outputs.tier1_count != '0'
runs-on: ubuntu-latest
strategy:
fail-fast: false
max-parallel: 5
matrix:
worker: ${{ fromJson(needs.discover.outputs.tier1) }}
steps:
- uses: actions/checkout@v5
- uses: pnpm/action-setup@v4
with:
version: 10
- uses: actions/setup-node@v5
with:
node-version: '22'
# 不用 setup-node 的 pnpm cache:
# 部分歷史 Worker 只有 package-lock.json,指 pnpm-lock.yaml 會讓
# setup-node 直接報 "Some specified paths were not resolved" 並 fail。
# deploy 本身 ~30s,cache 省不了多少;穩定性優先。
- name: Setup TinyGo
uses: acifani/setup-tinygo@v2
with:
tinygo-version: '0.40.1'
binaryen-version: '116'
- name: Rebuild component.wasm from source
working-directory: registry/components/${{ matrix.worker.name }}
run: |
set -euo pipefail
if [[ ! -f main.go ]]; then
echo "no main.go at registry/components/${{ matrix.worker.name }}/ — skipping rebuild"
exit 0
fi
tinygo build -target=wasi -o "${{ matrix.worker.name }}.wasm" main.go
ls -lh "${{ matrix.worker.name }}.wasm"
- name: Copy .wasm into Worker build dir
run: |
set -euo pipefail
src="registry/components/${{ matrix.worker.name }}/${{ matrix.worker.name }}.wasm"
dst="${{ matrix.worker.path }}/component.wasm"
if [[ -f "$src" ]]; then
cp "$src" "$dst"
echo "Copied $src → $dst"
else
echo "WARNING: $src not found, using existing $dst"
fi
- name: Install deps
working-directory: ${{ matrix.worker.path }}
run: |
if [[ -f pnpm-lock.yaml ]]; then
pnpm install --frozen-lockfile
else
echo "no pnpm-lock.yaml at ${{ matrix.worker.path }} — running pnpm install (no lock)"
pnpm install --no-frozen-lockfile
fi
- name: Deploy
working-directory: ${{ matrix.worker.path }}
env:
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
run: pnpm exec wrangler deploy
# 部署成功後自動把 contract 灌進 component-registry index
# SDD: matrix/arcrun/.agents/specs/component-registry-canon/design.md Phase 2
# degraded mode:失敗只 warning 不擋部署
# 邏輯實作在 registry/scripts/register-component.sh(本地+CI 共用 SSOT
- name: Register component in registry
if: success()
run: |
set -uo pipefail
python3 -c "import yaml" 2>/dev/null || pip install --quiet pyyaml
bash registry/scripts/register-component.sh "${{ matrix.worker.name }}" || \
echo "::warning::Registry 註冊失敗(degraded mode"
# ── Job 2b:Tier 2 並行部署(orchestration Worker,可能有 service binding 相依於 tier1) ─
# needs: deploy-tier1 → tier1 全綠才開始;首次部署時避免 service binding target 未存在。
deploy-tier2:
name: Deploy tier2/${{ matrix.worker.name }}
needs: [discover, deploy-tier1]
# tier2 也要跑:即使 tier1 沒東西(tier1_count=0)也要跑 tier2
if: |
always() &&
needs.discover.outputs.tier2_count != '0' &&
(needs.deploy-tier1.result == 'success' || needs.deploy-tier1.result == 'skipped')
runs-on: ubuntu-latest
strategy:
fail-fast: false
max-parallel: 5
matrix:
worker: ${{ fromJson(needs.discover.outputs.tier2) }}
steps:
- uses: actions/checkout@v5
- uses: pnpm/action-setup@v4
with:
version: 10
- uses: actions/setup-node@v5
with:
node-version: '22'
# 不用 setup-node 的 pnpm cache:
# 部分歷史 Worker 只有 package-lock.json,指 pnpm-lock.yaml 會讓
# setup-node 直接報 "Some specified paths were not resolved" 並 fail。
# deploy 本身 ~30s,cache 省不了多少;穩定性優先。
- name: Install deps
working-directory: ${{ matrix.worker.path }}
run: |
if [[ -f pnpm-lock.yaml ]]; then
pnpm install --frozen-lockfile
else
echo "no pnpm-lock.yaml at ${{ matrix.worker.path }} — running pnpm install (no lock)"
pnpm install --no-frozen-lockfile
fi
- name: Deploy
working-directory: ${{ matrix.worker.path }}
env:
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
run: pnpm exec wrangler deploy
# ── Job 3:彙總結果 ────────────────────────────────────────────────────
summary:
name: Summary
needs: [discover, deploy-tier1, deploy-tier2]
if: always()
runs-on: ubuntu-latest
steps:
- name: Report
run: |
{
echo "## Deploy Summary"
echo "- Tier 1 count: ${{ needs.discover.outputs.tier1_count }}"
echo "- Tier 1 result: ${{ needs.deploy-tier1.result }}"
echo "- Tier 2 count: ${{ needs.discover.outputs.tier2_count }}"
echo "- Tier 2 result: ${{ needs.deploy-tier2.result }}"
echo ""
echo "### Tier 1 (WASM components)"
echo '```json'
echo '${{ needs.discover.outputs.tier1 }}'
echo '```'
echo ""
echo "### Tier 2 (orchestration)"
echo '```json'
echo '${{ needs.discover.outputs.tier2 }}'
echo '```'
} >> "$GITHUB_STEP_SUMMARY"
+16
View File
@@ -21,6 +21,22 @@ docs/test_credentials/
*.sa.json
*-service-account*.json
# ── 開發痕跡 / 思考過程:不對外(本機保留供開發,但不進公開 repo)──
# richblack 2026-06-03:用戶要的是「使用 arcrun」,不是「開發 arcrun」。
# 開發規範(.claude/CLAUDE.md/AGENTS.md)、思考過程(.agents/docs/DECISIONS/BACKLOG)只在本機。
# 「用戶 CC harness」另由 acr install-harness 安裝進用戶專案(不是放在這個 repo 給人 clone)。
.claude/
.agents/
docs/
landing/
.github/
CLAUDE.md
AGENTS.md
DECISIONS.md
BACKLOG.md
BETA_TEST.md
CONTRIBUTING.md
# macOS
.DS_Store
-312
View File
@@ -1,312 +0,0 @@
# Arcrun for AI Agents
> 給 AI 操盤手(Claude Code、Cursor、Codex、自製 agent)的 onboarding。
> 載入這份就能用 arcrun,不需要讀 SDD 內部架構、不需要 grep codebase、不需要問人。
>
> 對應 SDD`.agents/specs/llm-interface/`v0.12026-05-16
---
## 1. Arcrun 是什麼(30 秒)
Arcrun = 用 YAML 把 WASM 零件串成可重複執行 workflow 的平台。**用戶寫 YAML,平台跑**。
- 每個零件是 TinyGo / AssemblyScript 編譯的 `.wasm`stdin/stdout JSON I/O
- 每個 workflow 是一份 cypher binding YAML,描述「節點 + 邊」的圖
- 觸發機制:HTTP webhook、cron、callback resume
- 部署完成回 `webhook_url`,用戶或下游服務 POST 即可執行
n8n 從手寫程式碼開始,arcrun 從 AI 描述開始:你跟用戶聊出他想要什麼自動化,**你(AI)寫 YAML 部署,之後不需要 AI 也能跑**。
---
## 2. 連線(一步搞定)
加到你的 MCP configClaude Desktop / Cursor / 任何支援 MCP 的 client):
```json
{
"mcpServers": {
"arcrun": {
"type": "http",
"url": "https://mcp.arcrun.dev/mcp",
"headers": {
"Authorization": "Bearer ak_YOUR_API_KEY"
}
}
}
}
```
> **暫時**M5 完成前 URL 仍是 `https://mcp.finally.click/mcp`,預計 2026-06 切。
> Tool 命名暫時仍是 `u6u_*` prefixM5 一次改 `arcrun_*`
> 本 doc 描述目標狀態,實際用 `list_*` tool 取得當前可用名單。
取得 ak_ 金鑰:到 https://arcrun.dev/meOAuth Google / GitHub 登入),右下角複製。
---
## 3. 五個核心概念
| 概念 | 一句話 |
|---|---|
| **Component(零件)** | WASM Worker,獨立部署成 `arcrun-{kebab}.{user}.workers.dev`。用 `list_components` 看可用清單 |
| **Cypher binding** | YAML 三元組 `A >> 關係 >> B`,定義 workflow 圖。常用關係:`ON_SUCCESS` / `對每個 X` / `IF` |
| **FOREACH** | `>> 對每個 item >> next_node` 迭代陣列。`item` 變數自動可用 |
| **Paused-resume** | claude_api 等需等外部 callback 的零件會「paused」,cypher-executor 透過 `/workflows/resume` 接續 |
| **api_key (ak_xxx)** | 所有 call 必帶(MCP header 或 workflow `{{api_key}}`),同時當 partition key |
---
## 4. 你的第一個 workflow5 分鐘 e2e
### Step 1:看有什麼零件可用
```
arcrun_list_components() # 全部零件名單
# 或
arcrun_search_examples('rag') # 範例庫搜尋(從 use case 找範本)
arcrun_list_skills() # 看 playbook 清單
arcrun_get_skill('build_watcher_workflow') # 拿特定 playbook 細節
```
常用零件:`http_request``claude_api``kbdb_get``kbdb_create_block``telegram``gmail``cron``filter``trigger_workflow`
### Step 2:寫 minimal YAML(從範例改 > 從零寫)
```yaml
name: hello_world
description: 接 webhook,回個 hi
flow:
- "input >> ON_SUCCESS >> say_hi"
config:
say_hi:
component: http_request
url: "https://httpbin.org/post"
method: POST
body_json:
hello: "{{input.name}}"
```
### Step 3dry-run 校驗
```
arcrun_validate_yaml(api_key, graph)
```
### Step 4:部署
```
arcrun_push_workflow(api_key, yaml_content)
```
`{name, webhook_url: 'https://cypher.arcrun.dev/webhooks/named/hello_world/trigger'}`
### Step 5:觸發測試
```
arcrun_run_workflow(api_key, name='hello_world', input={name: 'leo'})
```
### Step 6:看結果 / debug
```
arcrun_list_recent_executions(api_key, workflow_name='hello_world')
# 若 paused
arcrun_list_paused_executions(api_key)
arcrun_get_execution_trace(api_key, task_id='task_XXX')
```
### Step 7:回報(**必做**,見 §8)
```
arcrun_report_feedback(api_key, issue_type='success_story', description='...')
```
---
## 5. URL 慣例(很重要,搞錯會撞 522)
| URL pattern | 用途 |
|---|---|
| `cypher.arcrun.dev` | Orchestration API(你的 workflow CRUD + trigger 走這) |
| `arcrun-{kebab}.{user}.workers.dev` | 零件 workercypher-executor 走 workers.dev 對內 URL,避 CF 同 zone 自循環死鎖) |
| `{kebab}.arcrun.dev` | 零件 worker 對外公開 URL(用戶 / 直接 curl 用,cypher-executor 不要走這) |
| `kbdb-*.arcrun.dev` | KBDB 操作(資料層) |
| `mcp.arcrun.dev` | MCP server**你**用這個) |
| `mcp.finally.click` | MCP server 舊網址(過渡) |
**踩坑警示**cypher-executor 自打 `cypher.arcrun.dev` 或自打 `arcrun-cypher-executor.*.workers.dev` 都會撞 CF self-fetch 防護回 1042/522。要 in-process 觸發另一個 workflow**用內建零件 `trigger_workflow`**(不是 `http_request` 自打)。
---
## 6. 常見錯誤 + 怎麼讀
| error_code | 含義 | 你該做什麼 |
|---|---|---|
| `auth_missing` / `auth_invalid` | ak_ 沒帶 / 錯了 | 去 https://arcrun.dev/me 重拿,更新 MCP config |
| `component_not_found` | 零件名打錯 | call `list_components()` 看正確名 |
| `component_not_in_whitelist` | 零件存在但 cypher-executor 不認 | 告訴用戶聯絡平台維護者(這是平台 bug) |
| `validation_failed` | YAML / schema 不過 | 看 response 的 `next_actions` 陣列,照著修 |
| `running_async` (status field) | workflow 已接受,在背景跑等 callbackclaude_api 等) | **正常**wait 或 call `get_execution_trace` 看狀態。舊名 `paused_awaiting_resume` 2026-05-16 改 |
| `dependency_unavailable` | 下游 APIClaude / Gmail / KBDB)掛 | retry,仍掛 → 公告型 issue |
---
## 7. 不確定的時候,按這個順序
1. **不知道有什麼零件可用**`arcrun_list_components()` / `arcrun_get_component_contract(id)`
2. **不知道用戶有什麼 workflow**`arcrun_list_workflows(api_key)`
3. **想看零件 input/output schema**`arcrun_get_component_contract('xxx')`
4. **要不要 dry-run** → 預設 yes,永遠先 `arcrun_validate_yaml``arcrun_push_workflow`
5. **觸發後不知道結果**`arcrun_list_recent_executions(api_key, name)` / `arcrun_get_execution_trace(api_key, task_id)`paused 不算錯
6. **有沒有現成範例可參考**`arcrun_search_examples('rag llm chain')`(範例庫 10+ 個 workflow
7. **有沒有 playbook**`arcrun_list_skills() / arcrun_get_skill(slug)`5 個 playbookwatcher / paused-debug / migrate-trigger / rag / new-component
8. **不知道下一步** → 看任何 error response 的 `next_actions` 陣列
### 過渡期 tool 命名注意
LI 開發中(2026-05-16~)。目前 arcrun-mcp 同時提供:
- **`arcrun_*`**(新規範,**主用這套**):報 feedback / 工作流 CRUD / 執行 trace
- **`u6u_*`**(舊規範,等 M5 一次切):component 操作、tag 操作、GUI context
`u6u_*` 仍可用。其中 `u6u_deploy_workflow` 是壞的(呼叫不存在的 endpoint),用 `arcrun_push_workflow` 取代。
---
## 8. 回報機制(**必做**
平台用回饋資料 self-improve。**每次部署 workflow / 卡住 / 解掉問題後**call:
```typescript
report_feedback({
workflow_name: "hello_world", // 你剛做的 workflow
issue_type:
| "success_story" // 順利完成,值得記錄這個 pattern
| "doc_unclear" // AGENTS.md / skill / contract 講不清楚
| "tool_missing" // 該有的 MCP tool 沒有
| "error_unhelpful" // 錯誤訊息看不懂下一步
| "unexpected_behavior" // 跟我預期的不一樣
| "feature_request", // 我想要 X 功能
description: "...",
retry_count: 2, // 你試了幾次才搞定
blocked: false, // 完全擋住嗎
suggested_fix: "..." // optional,你建議的修補
})
```
「success_story」也要報,**那是告訴平台「這個 pattern 已經 work,可以推廣」**。
不需要怕回報太多 — 你不報,平台拿 implicit telemetry(每個 deploy / run 平台自己 log)也會看到問題,但 explicit feedback 質感高很多。
---
## 9. KBDB(資料儲存)速覽
arcrun 的「資料庫」是 KBDBCloudflare D1)。萬物皆 blocknote / wiki-page / chat / triplet / template / skill / feedback / 等,靠 `type` 區分。
工具:
- `kbdb_get(type, block_id?, page_name?, ...)` — 讀
- `kbdb_create_block(type, content, ...)` — 建
- `kbdb_patch_block(block_id, content?, tags?, ...)` — 改
- `kbdb_upsert_block(page_name, content, ...)` — page_name 當 idempotency key
寫 workflow 要 RAG / KM / 用戶資料持久化時,直接用這幾個 component(在 YAML `component: kbdb_get` 等)。
完整 KBDB API 將有獨立 SDD`kbdb-llm-interface`),目前看 `https://kbdb.finally.click/ui`Swagger)。
---
## 10. 範例:寫一個 cron watcher(最常見 pattern
```yaml
name: my_watcher
description: 每 5 分鐘掃未處理資料 → 觸發 wiki_synthesis
flow:
- "watch_cron >> ON_SUCCESS >> list_unprocessed"
- "list_unprocessed >> ON_SUCCESS >> filter_new"
- "filter_new >> 對每個 item >> trigger_synthesis"
config:
watch_cron:
component: cron
cron_expr: "*/5 * * * *"
list_unprocessed:
component: kbdb_get
api_key: "{{api_key}}"
type: "note"
source: "user-input"
limit: 20
filter_new:
component: filter
items: "{{list_unprocessed.blocks}}"
condition:
key: "tags_json"
op: "eq"
value: "[]"
trigger_synthesis:
component: trigger_workflow # 不要用 http_request 自打 — 會撞 CF self-fetch
workflow_name: "wiki_synthesis"
api_key: "{{api_key}}"
input:
api_key: "{{api_key}}"
raw_block_id: "{{item.id}}"
```
部署完每 5 分鐘自動跑。
---
## 10.5 內建 magic vars`_` prefix reserved
YAML 內可直接用以下變數,cypher-executor 自動展開為當下時間(UTC):
| 變數 | 範例 | 用途 |
|---|---|---|
| `{{_today}}` | `2026-05-16` | 日 log / page_name |
| `{{_yesterday}}` | `2026-05-15` | digest 取昨日 |
| `{{_now}}` | `2026-05-16T09:30:00.123Z` | ISO 8601 |
| `{{_now_unix}}` | `1778937000123` | unix ms |
| `{{_now_unix_s}}` | `1778937000` | unix sec |
| `{{_iso_week}}` | `2026-W20` | weekly archive (本 doc 推薦) |
| `{{_iso_week_num}}` / `{{_iso_year}}` | `20` / `2026` | 拆開用 |
| `{{_yyyymm}}` / `{{_yyyymmdd}}` | `202605` / `20260516` | 緊湊路徑 |
| `{{_year}}` / `{{_month}}` / `{{_day}}` / `{{_hour}}` / `{{_minute}}` | 各別 zero-padded | 自己拼路徑 |
| `{{_weekday}}` | `0`-`6`0=日)| if-control |
| `{{_iso_weekday}}` | `1`-`7`1=一)| ISO 風格 |
**rule**`_` prefix reserved for system**用戶自己 ctx 變數不要用 `_` 開頭**。
**範例**weekly archive
```yaml
publish_roadmap_archive:
component: kbdb_upsert_block
page_name: "roadmap-{{_iso_week}}" # roadmap-2026-W20
tags_json: '["weekly", "week:{{_iso_week}}"]'
```
---
## 11. 給寫 LI 的 AI 自己的 meta-規範
你(AI)在寫 arcrun workflow 時,**遵守以下習慣**會少踩坑:
1. **永遠先 list → validate → push → run → trace**5 步流程,缺一個都會多繞路
2. **error 一定讀 `next_actions`**:不是讀 `human_message` 然後猜
3. **paused 不是錯**claude_api、外部 OAuth flow 都會 paused,正常
4. **`{{api_key}}` 是 trigger context 帶進來的**:手動觸發要在 body 帶;cron 觸發 cypher-executor 自動塞
5. **新增零件不在 list_components 出來的清單裡** → 平台沒部署該零件,告訴用戶「我們需要先做 component」,不是你寫 workflow 的鍋
6. **完成後 call `report_feedback`**:哪怕 success_story,也回報。AI 用得順不順不能靠人類事後回顧
---
## 12. 進階參考
- 完整 SDD`matrix/arcrun/.agents/specs/llm-interface/`
- 平台架構(rules):`matrix/arcrun/.claude/rules/`
- 零件開發指南:call `get_component_guide()` MCP tool
- KBDB Swaggerhttps://kbdb.finally.click/ui
- 範例庫(M3 完成後):`registry/examples/`
- 平台週報(M4 完成後):KBDB block `type=arcrun-roadmap`
---
> 本 doc 是 source of truth。每次更新後 GH Actions 自動同步 KBDB block (`type=agent-onboarding`)AI 可透過 `get_onboarding` MCP tool 拿最新版(M1 完成)。
-175
View File
@@ -1,175 +0,0 @@
# Arcrun 待辦(BACKLOG.md
> 流動的待辦清單。做完一項就劃掉(`[x]`)或移到「已完成」。
> 穩定的決策在 `DECISIONS.md`
>
> 原則:一次只推進一步。不在「整理」的慣性裡順手做下一件事。
>
> 最後更新:2026-05
---
## 第一期(鎖定範圍,依序做,不跳)
### 步驟 1 — 清 cypher-executor 的 KBDB 污染
- [x] 刪除 `cypher-executor/src/lib/kbdb-partner.ts`
- [x] `auth.ts` 移除 kbdb-partner import 與 3 處呼叫
- [x] `wrangler.toml` 移除 `KBDB_BASE_URL``KBDB_INTERNAL_TOKEN` 註解
- [x] `component-loader.ts` 白名單移除 `claude_api` + 6 個 `kbdb_*`
- [x] `component-loader.ts` 修正說謊的 doc comment / 中段註解
- [x] `graph-executor.ts` 註解的 kbdb 範例替換、`types.ts` 移除 `KBDB_BASE_URL` 型別
- [x] `tsc --noEmit` 編譯通過
- [x] **commit 第一步**commit message 寫清楚範圍)
### 步驟 2 — `acr recipe test`recipe 形式的 curl
> 目的很簡單:把一個早就存在的 API endpoint(如 KBDB)變成 Arcrun recipe。
> 流程:看 API 文件 → 申請 API Key → 設進 credential → 寫 recipe → 用 test 打一次。
> `test` 就是「用 recipe 形式 curl 一次」——打通(2xx)就成了。
> 不寫記錄、不是入庫關卡、不防任何東西。防作弊/公共庫那些是第二期。
>
> 順序:先 2c(為 KBDB 建 auth recipe,否則 test 打 KBDB 會 401)→ 再 2a。
- [ ] **2c. 為 KBDB 建 auth recipestatic_key**`POST /auth-recipes`,建 `auth_recipe:kbdb`
讓 auth-dispatcher 能把 KBDB API Key 注進 test 的請求。static_key 新路已支援(見 DECISIONS §3b)。
- [ ] **2a. `acr recipe test <id> [--input k=v]` 指令** — 藄殼:組單節點 ExecutionGraph
→ 走現有 `/execute` → 顯示 HTTP status 與回應。就是 recipe 形式的 curl。
不碰 cypher-executor、不改 RecipeDefinition 型別、不寫任何記錄。
### 步驟 3 — 降級假零件成 recipe2026-05-29 大部分完成)
> 進度(2026-05-29):registry/components/ 33 → 22。引擎加了 `auth_service`(多 recipe 共用 auth
> 與 `inject.path`endpoint 可插 secret,解 telegram URL-path token)。詳見 auth-recipe.md §七 + DECISIONS §1。
- [x] KBDB 降級:建 `kbdb_get/create_block/patch_block/delete/ingest` 5 recipe(共用 auth_service=kbdb),刪 5 零件目錄
- 驗收:get 200 / create 201 / ingest 201 / delete 200 綠;**patch 403 = KBDB PATCH org-check bug**(已交 kbdb/docs,非 recipe 問題)
- `kbdb_upsert_block` **未降**KBDB 無 upsert endpoint,是 client 拼湊 → 已交 KBDB feature request 出 `POST /blocks/upsert`,源碼暫留
- [x] `gmail`/`telegram`/`line_notify`/`google_sheets` 降級:建 recipegmail_send / telegram_send / line_notify_send / google_sheets_read|append+ 補 auth_recipeline_notify / telegram),刪 4 零件目錄
- telegram_send:✅ auth 注入驗收(inject.path400 chat not found = 過認證)
- gmail/sheets/line:⚠️ **未驗收:缺 credential**google SA JSON / line token),recipe + auth 鏈路本身已驗正確
- [x] `claude_api` + `km_writer` **未降,標 deferred**:是 Mira 自用服務的膠水,不該是公共零件/recipe → 交 Mira refactor 文件(收成工作流後刪),源碼暫留
- [x] `ai_transform_compile` + `ai_transform_run` **刪除**:Arcrun 是 AI 呼叫的工具、不該內嵌 AI 節點回頭呼叫 AI(誤搬 n8n 心智模型,見 DECISIONS
- [x] 灰色地帶裁決:`cron` / `platform_crypto` 留(引擎能力,組工作流必要)
- [ ] KBDB recipe 採 Supabase 模式:顯示在公共零件庫,要用需申請 API Key(**未做**,屬服務側/封測後)
- [x] 「降級」定義:萃取 endpoint/method/auth 寫成 API recipe → 刪零件目錄(已照做)
- [ ] **遺留:orphaned 部署 worker 待 `wrangler delete`**richblack 手動,rule 05):
arcrun-kbdb-{get,create-block,patch-block,delete,ingest} / arcrun-{gmail,telegram,line-notify,google-sheets} / arcrun-ai-transform-{compile,run}
- [~] 降級後 registry/components/ 現 22 個:17 白名單 primitive(流程6+資料7+http_request1+auth3
+ cron/platform_crypto(引擎能力)+ claude_api/km_writer/kbdb_upsert_blockdeferred
### 步驟 4 — 補零件庫真把關(2026-05-30 收尾:投稿改走 PR
> 方向修正:零件投稿走 **GitHub PR**(人 merge=閘門、CI 跑把關),廢 registry self-service。
> SDD`.agents/specs/component-gatekeeping/`(含完整決策過程)。
- [x] 假零件偵測(detectFakeComponent.ts):外部 URL/domain + http_request 子集,硬擋退稿指回 recipe
- [x] 純 WASI 把關(wasmImports.ts):import module 白名單(只准 wasi_snapshot_preview1 + u6u
- [x] cold_start / runtime_compat / gherkin 標 unimplemented_steps(不假綠)
- [x] G0 registry 人類閘門(已 commit;投稿改 PR 後非主管道,保留不刪)
- [ ] Gherkin 真跑 + 覆蓋檢查 → 未來接 **CI PR check**CI 能跑 wasm,繞 CF venue 牆)。
richblack:人工 review 就夠,primitive 極少 → CI 暫不做,邏輯已寫好隨時能搬。
- [x] **不做 R5 本機 hook**PR/merge + 假零件偵測 + 純WASI 沙箱已防「未經同意變公共零件」→ hook 過度工程。
- [ ] 黃金向量:人工核對(另起 session 從語義寫,不機器自動化)— 不急
### 步驟 5 — ~~建零件的人類閘門 + 白名單 hook~~2026-05-30 被 PR 方向取代)
> **取代說明**:原規劃「registry submit API 人類閘門 + 四路 client + 本機 hook」。
> 2026-05-30 改:**零件投稿走 GitHub PR**(人 merge = 天然人類閘門,AI 偽造不了 GitHub approve)。
> → registry self-service 主閘門、四路 client、本機 hook **都不做**PR/merge + 假零件偵測 +
> 純WASI 沙箱已防「未經同意變公共零件」,hook 過度工程)。並進步驟 4。
>
> **真正的裸奔風險不在零件,在「資料外流」**recipe/webhook 把資料送出去,不分公私庫)。
> → 另開新 SDD「資料外流警示」(見下方第一期新增項)。
**ABC 配套仍有效**(讓 AI 不選難路):A 假零件偵測(步驟 4)+ B 工作流範本(步驟 7 acr new)
+ C mindset「工作流是 default、零件稀有例外」(步驟 7 mindset Skill)。
### 步驟 5b — 資料外流警示(2026-05-30 新增,richblack:先做)
> 風險根源:arcrun 讓「產生 API」變很簡單(資料+webhook trigger=API)→ AI 可能不知不覺把含個資
> 的東西變成可被呼叫的 endpoint。不分公私庫(私人=公司用也會把個資 POST 到公司群)。
- [ ] 新建 SDDrequirements/design 交 richblack review 才動 code
- [ ] **API 層警示**:任何「把資料送出去」的動作(recipe push / webhook 等)不論哪條路都警示/需人類同意
- [ ] **hook**:AI 動手做這類動作前先警告(防在前;API 層防在送出前,兩道互補)
### 步驟 6 — 搬家(拆 matrix)(原步驟 5)
- [ ] 先給 kbdb、ghost(identity/personality-system/persona-sdk) 各自 `git init` + 建 GitHub repo
(它們現在沒有自己的 .git,是被 inkstone-matrix 追蹤的子目錄——刪 matrix/.git 前必須先安置)
- [ ] 分揀 matrix 的孤兒檔案(清單見下方附錄)
- [ ] arcrun / kbdb / ghost 各自 `git status` 確認乾淨、已 push
- [ ] GitHub 上 `inkstone-matrix` 設為 archived(不刪)
- [ ] 本機刪 `matrix/.git`matrix 降級成普通資料夾
- [ ] 比對兩份 cypher-executormatrix 頂層 vs arcrun 底下),釐清死活
### 步驟 7 — 收尾(原步驟 6)
- [x] `arcrun-mindset` Skill(給 AI 操盤手的世界觀,見 DECISIONS §7 層三)
`skills/arcrun-mindset/SKILL.md`(2026-05-30)。涵蓋:工作流是default/零件稀有、
AI→工具、arcrun不做授權、暴露需人類同意、誠實(不假綠/不假防偽/不代替人類確認/完成=客觀證據)。
同步 `.claude/rules/06-mindset.md`arcrun repo 內 CC 開發時讀)。
- [ ] 會回嘴的 CLIacr 偵測走歪 → exit 2 + 指回正路,見 DECISIONS §7 層二)
→ 部分已做:pre-bash hook 擋假零件/暴露動作 + 指回正路;CLI 本身的 exit-2 回嘴待補
- [ ] README 重寫成單一路徑(砍掉「玩法一/二/三」三選一)
- [ ] `acr init --self-hosted`:貼 CF token → 自動建 KV、部署 Worker、自動 workers.dev
subdomain、寫回 config(用戶自己建 tokenCLI 不代管)
---
## 第一期之後 / 待決策(不要現在做)
- [ ] **用戶自己的 API 保護機制(入站認證)**richblack 2026-05-30)— 做成零件或功能:
(1) 用戶可發 API Key 給別人;(2) 不同權限設定。現況缺口:webhook 只有 X-Arcrun-API-Key
(誰有我的 key 誰能打),沒有「發受限 key 給別人 / per-caller 認證 / rate limit」。
**這是 arcrun 資安優勢**n8n 用簡單 USN/PWD 做不好,AI coding 又怕資安沒做好,我們系統幫搞定、
一個動作能用。注意區分:credential/auth recipe 是出站(呼叫別人 API 帶我的 token),這是入站(保護我的 API)。
- [ ] **架構詞彙釐清(recipe / part / function / 工作流組合)**richblack 2026-05-30,要思考一下):
- recipe:用零件去打的設置文件(endpoint+設定)
- part(零件):需 PR + update/upgrade
- function:把一批功能做好可用 cypher 拉進來(例:API 認證,我做好幾個讓用戶拉)。是 part+recipe
- **多零件/工作流組合成新工作流**(第一個走完連第二個)—「好像還沒有」,要思考
- [ ] **砦 `injectCredentials` 舊路 + `BUILTIN_CREDENTIALS_MAP`** — credential 系統現為新舊兩路並存,
舊路是 TS 裡解密的半成品(註解自認 Phase 1.9 刪)。是獨立清理,**不擋降級**。見 DECISIONS §3b
- [ ] **決策:開源版 cypher-executor 是否保留「KBDB block 展開」功能**
`recipe-expander.ts` 仍讀 `env.KBDB_BASE_URL``prompt-recipe-schema.ts``kbdb_block` 型別)
按定位應移除(屬 KBDB 訂閱層),但牽涉型別,需單獨想清楚
- [ ] **決策:開源版遙測去 KBDB 化**
`telemetry.ts``kbdbCreateBlockUrl` + fetch 到 `arcrun-kbdb-create-block`
開源版遙測寫去哪?本地?關掉?寫用戶自己的 ANALYTICS_KV
- [ ] 新增 `kv_store` 暫存 primitive:解決「workflow 中途暫存」需求(像 n8n data table)。
用 Arcrun 執行環境內建暫存,不依賴外部服務 → 可為 primitive。
介面要抽象(CF 上是 KV,wazero 上是本地檔/記憶體),才能三層通用
- [ ] 「專案」概念落地:CLI 的 `acr project` 指令、專案=引用 workflow 的三元組
- [ ] 公共零件庫 KBDB 語義搜尋:AI 查意圖 → 回傳零件選項;self-hosted 用本地關鍵字比對
- [ ] 事後機制第二層:不變式測試套件(核心原則寫成自動測試,接 CI / pre-commit
- [ ] 事後機制第一層:執行軌跡完整化(基於現有 `executions.ts`
- [ ] `acr project init`:把閉環三件(mindset Skill + CLAUDE.md 判準 + exit-2 hook +
不變式測試)一次裝進用戶專案
- [ ] 視覺化 Skill:固定格式的「給人看的圖」(取代 arcrun-gui 拖拉畫布)。
目的是「讓人看完放心地說『跑吧』」,是信任產出物不是美術產出物
- [ ] arcrun-gui 處置:降級為「零件庫 / recipe 貢獻者牆」展示站,掛 arcrun.dev(非工具)
- [ ] arcrun-mcp 對齊:`u6u_*` → arcrun 命名、`finally.click` → arcrun.dev、
移除 GUIDE.md 教 `api_config` 的反模式、確認 MCP 是薄殼
- [ ] `acr update` 設計:常態只拉 recipe;動到 primitive 一定要用戶確認,不可默默 deploy
- [ ] recipe 「貢獻回公眾」的路徑:別人測好的 recipe 怎麼變成所有人能用(飛輪的關鍵)
---
## 附錄:matrix 孤兒檔案分揀清單(步驟 5 用)
**移進 arcrun**
- `.agents/specs/arcrun/``.agents/specs/u6u-core-mvp/``.agents/specs/u6u-platform-evolution/`
- `.agents/steerings/`tech.md / coding-rules.md / product.md / structure.md
- `docs/user_requirements/arcrun/`
- `cypher-executor/`(matrix 頂層那份——先比對死活)
**移進 arcrun-gui(若保留):**
- `.agents/specs/u6u-gui-platform/``.agents/specs/prototype-editor/`(先看一眼確認)
**移進 ghost / 其他:**
- `ghost.md``identity/``personality-system/``persona-sdk/` → ghost
- `.agents/specs/matrix-admin-api-fixes/``inkstone-admin/` → inkstone-admin
**需判斷(多半過時,留在 archived 的 inkstone-matrix 即可):**
- `API-CATALOG.md``AVM-PROTOCOL.md``MODULE-CONTRACT.md`、頂層 `CLAUDE.md` / `GEMINI.md`
- `docs/user_requirements/rearrange.md` / `test.md`
- `.claude/` / `.github/` / `.vscode/` / `.wrangler/` / `.swarm/` / `ruvector.db`
-286
View File
@@ -1,286 +0,0 @@
# arcrun 封測指南
感謝你參與 arcrun 的封測。
arcrun 是一個讓 AI 和人都能直接讀寫、執行的 workflow 工具。
你的任務是測試核心功能,並記錄任何不符合預期的地方。
---
## 環境安裝(5 分鐘)
```bash
npm install -g arcrun
acr --version # 應顯示 1.1.0 或以上
```
---
## 模式選擇
arcrun 有兩種使用模式:
### Local 模式(不需要帳號,快速試用)
```bash
mkdir my-workflows && cd my-workflows
acr init --local
```
建立 `~/.arcrun/config.yaml`local 模式)和一個 `hello.yaml` 範例。
```bash
acr validate hello.yaml --offline
acr run hello --input input="Hello, arcrun!"
```
預期看到:`"result": "HELLO, ARCRUN!"`
### Standard 模式(需要 API Key,支援 Webhook 部署)
```bash
acr init
```
互動式設定,輸入 email 後自動取得 API Key,存入 `~/.arcrun/config.yaml`
---
## 零件清單
執行以下指令查看所有可用零件:
```bash
acr parts
```
取得單一零件的 config 範本:
```bash
acr parts scaffold string_ops
acr parts scaffold http_request
acr parts scaffold gmail # 含 credentials.yaml 範本
```
---
## 可用零件(21 個,不需要帳號)
### 字串操作 — `string_ops`
```yaml
config:
my_node:
component: string_ops
operation: upper # upper / lower / trim / length / replace / split / join
```
### 數字運算 — `number_ops`
```yaml
config:
my_node:
component: number_ops
operation: add
b: 10 # 加上 10
```
支援:`add` / `sub` / `mul` / `div` / `round` / `floor` / `ceil` / `abs`
### HTTP 請求 — `http_request`
```yaml
config:
my_node:
component: http_request
method: GET # GET / POST / PUT / DELETE
```
```bash
acr run notify --input url="https://httpbin.org/get"
```
### 其他零件
```
if_control 條件分支(ON_SUCCESS / ON_FAIL 路由)
switch 多分支條件
foreach_control 迭代陣列
filter 過濾陣列
set 設定固定值到 context
array_ops 陣列操作(push / pop / slice
date_ops 日期操作(now / format / diff
validate_json 驗證 JSON Schema
ai_transform_compile / ai_transform_run AI 自然語言轉換
```
---
## 動態參數 `{{variable}}`
config 裡的字串欄位支援 `{{variable}}`,從 `--input` 取值:
```yaml
# flexible.yaml
name: flexible
flow:
- "input >> ON_SUCCESS >> process"
config:
process:
component: string_ops
operation: "{{op}}"
```
```bash
acr run flexible --input input="hello" --input op=upper # → HELLO
acr run flexible --input input="HELLO" --input op=lower # → hello
```
---
## 錯誤路由(ON_FAIL
```yaml
# safe-fetch.yaml
name: safe-fetch
flow:
- "input >> ON_SUCCESS >> fetch"
- "fetch >> ON_FAIL >> fallback"
config:
fetch:
component: http_request
method: GET
fallback:
component: string_ops
operation: upper
```
```bash
# 故意讓 fetch 失敗,觸發 fallback
acr run safe-fetch \
--input url="https://invalid.domain.xyz" \
--input input="fallback triggered"
```
---
## 中文語意
flow 支援中文關係詞:
```yaml
flow:
- "輸入 >> 完成後 >> 轉換"
- "轉換 >> 失敗時 >> 錯誤處理"
```
---
## Webhook 部署(Standard 模式)
讓外部網頁或服務能觸發你的 workflow:
```bash
# 部署 workflow
acr push my-workflow.yaml
```
輸出範例:
```
✓ "my-workflow" 已部署
Webhook URLhttps://cypher.arcrun.dev/webhooks/named/my-workflow/trigger
需帶 HeaderX-Arcrun-API-Key: ak_...
curl 觸發範例:
curl -X POST https://cypher.arcrun.dev/webhooks/named/my-workflow/trigger \
-H 'X-Arcrun-API-Key: ak_your-key' \
-H 'Content-Type: application/json' \
-d '{"message": "hello"}'
```
---
## API Recipe(整合外部服務)
不需要 deploy Worker,只要上傳 recipe YAML
```bash
acr recipe push my-recipe.yaml
acr recipe list
acr recipe delete rec_xxxxxxxx
```
Recipe 上傳後會得到 `rec_xxxxxxxx` hash,可直接在 workflow config 的 `component` 欄位使用。
---
## Credential 管理(Standard 模式)
需要帶 token 的零件(gmail、telegram、notion 等)可以提前上傳 credential,執行 workflow 時自動注入。
**加密金鑰在 `acr init` 時已自動取得並存入 `~/.arcrun/config.yaml`,不需要手動設定。**
**步驟一:查看某服務需要哪些 credential**
```bash
acr auth-recipe scaffold notion # 輸出 credentials.yaml 範本 + workflow 使用範例
acr auth-recipe list # 列出所有支援的服務(20 個)
```
**步驟二:建立 credentials.yaml**(參考 scaffold 的輸出):
```yaml
# 範例:Notion
notion_token: "secret_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
# 範例:Telegram Bot
telegram_bot_token: "123456789:your-bot-token"
```
**步驟三:上傳**
```bash
acr creds push credentials.yaml
```
上傳後執行 workflow 時,tokens 自動注入,不需要在 `--input` 手動帶。
### 支援的第三方服務(20 個)
```bash
acr auth-recipe list
```
輸出:Notion、Slack、GitHub、OpenAI、Anthropic、Airtable、Discord、Stripe、Twilio、SendGrid、HubSpot、Linear、Shopify、Resend、Supabase、Typeform、Jira、Google SheetsService Account)、GmailService Account)、Google DriveService Account
---
## 回饋格式
請把你的觀察記錄在 `FEEDBACK.md`,格式不限,但希望包含:
1. **成功的地方** — 哪些功能符合預期?
2. **失敗的地方** — 錯誤訊息是什麼?步驟是?
3. **困惑的地方** — 不知道怎麼用、文件不清楚的地方
4. **想要的功能** — 你覺得少了什麼
---
## 已知限制
- `number_ops` 的數字參數(`a``b`)若從 `--input` 帶入為字串,需要零件自行做型別轉換(目前已支援)
- `ON_FAIL` 觸發時,fallback 節點收到的 context 包含上游的錯誤物件(`{success: false, ...}`
- 多節點串連時,context 為 flat merge,上游的 `data.result` 會直接合併到頂層
- `if_control` 條件為 false 時,不執行任何下游節點(沒有明確的 else 分支)
---
## 有問題?
遇到任何問題直接問。你的 API Key 是確定性的,只要用同一個 email 呼叫 `/register` 就能拿回來:
```bash
curl -X POST https://cypher.arcrun.dev/register \
-H "Content-Type: application/json" \
-d '{"email":"your@email.com"}'
```
-70
View File
@@ -1,70 +0,0 @@
# CLAUDE.md — arcrun
> 本檔是**索引 + 最高原則**,詳細規範拆到 `.claude/rules/`
> Hook 強制機制在 `.claude/hooks/`,違反會直接 blockexit 2)。
---
## 絕對鐵律(違反 = 停手)
1. **任何 code 變動前必須先讀對應 SDD**,在回覆開頭宣告已讀清單與對應 task 編號(格式見 `.claude/rules/00-sdd-protocol.md`
2. **零件只能用 TinyGo 或 AssemblyScript 編譯成 WASM**`registry/components/` 下禁止 TypeScript
3. **cypher-executor TS 禁止實作 credential / auth / JWT / template 展開業務邏輯**;這些全在 WASM 零件
4. **Cypher binding = YAML 裡的 URL 清單**,不是 Cloudflare service binding;零件串接走 HTTP URL
5. **每個 WASM 零件 = 獨立 Worker = 公開 URL**;不從 R2 動態讀(R2 只 Phase 5 啟用)
6. **修改現有程式碼,不是新建資料夾重做**
7. **每完成一個 task 立刻更新 tasks.md 的 `[x]`**,不批次
---
## 工作流程(強制)
開始任一任務,按順序:
1. 讀 `.agents/specs/arcrun/arcrun.md`(總進度)
2. 讀對應的 SDD `design.md` + `tasks.md`
3. 在回覆開頭貼出:
```
📋 已讀 SDD<清單>
🎯 本次對應 task<編號>
📐 本次 task 的 SDD 規範摘要:<重點>
🚧 執行範圍:修改/建立/刪除 <檔案>
```
4. 動手前把 tasks.md 對應 task 標為 `[🔄]`,完成後標 `[x]`
5. 完成後確認:是否需要同步更新 design.md?
找不到對應 SDD → **停手問 richblack**,不要自行建立。
---
## 詳細規範索引
| 檔案 | 內容 |
|-----|------|
| `.claude/rules/00-sdd-protocol.md` | SDD 讀取協議(強制流程) |
| `.claude/rules/01-tech-stack.md` | 技術棧硬限制(語言/儲存/加解密) |
| `.claude/rules/02-forbidden.md` | 禁止清單(hook 強制執行) |
| `.claude/rules/03-component-architecture.md` | 零件架構(R2 用途 / cypher binding / service binding 邊界) |
| `.claude/rules/04-current-progress.md` | 當前進度 + SDD 索引 |
---
## SDD 位置速查
| 子系統 | 路徑 |
|-------|------|
| **進行中** Credential Primitives WASM | `.agents/specs/arcrun/credential-primitives-wasm/` |
| arcrun 總進度 | `.agents/specs/arcrun/arcrun.md` |
| Auth Recipe 系統 | `.agents/specs/arcrun/auth-recipe.md` |
| Landing Page | `.agents/specs/arcrun/landing-page.md` |
| SDK + Website | `.agents/specs/arcrun/sdk-and-website/` |
| arcrun MVP 整體 | `.agents/specs/arcrun-core-mvp/` |
| Platform Evolution | `.agents/specs/arcrun-platform-evolution/` |
| Credential 長期規格(需求源) | `docs/user_requirements/credential_parts.md` |
| Tech Stack 詳細 | `.agents/steerings/tech.md` |
---
## 封測狀態
**推遲**richblack 2026-04-19 決定)。先完成 Phase 1-3 清除違規 TS,再啟動封測。
-435
View File
@@ -1,435 +0,0 @@
# Contributing to arcrun
感謝你考慮貢獻 arcrun!本文件說明如何新增零件(WASM component)並提交至公眾零件庫。
arcrun 的零件**主要由 AI 撰寫**。你不需要是 TinyGo 或 AssemblyScript 專家,只需要把這份文件和你的 API 文件或需求貼給 AI,讓它生成源碼,你負責編譯、測試、提交。
---
## 選擇開發語言
零件只需要輸出符合 **WASI preview1**`.wasm` 檔案,與使用哪個語言無關。
| 語言 | 輸出大小 | AI 撰寫品質 | 說明 |
|------|---------|------------|------|
| **TinyGo** | 極小(10–80KB) | 優秀 | 官方零件使用;語法簡單,AI 出錯率低 |
| **AssemblyScript** | 小(20150KB | 良好 | TypeScript 語法,前端開發者最快上手 |
| **Rust** | 小–中(30–300KB) | 良好 | 效能最強;適合複雜演算法,工具鏈稍複雜 |
**AI 開發建議:**
- 選 **TinyGo**Go 語法與 TypeScript 差異夠大,AI 不易把 TS 邏輯直接搬過來造成錯誤,是最穩的選擇。
- 選 **AssemblyScript**:適合已熟悉 TypeScript 的開發者,但要注意 AS 不是 TS — 提示 AI 時明確說「AssemblyScript,不是 TypeScript」。
- 選 **Rust**:效能要求高時使用;需要更詳細的提示和更仔細的審查。
---
## 零件規格:共通規則
無論使用哪個語言,零件必須遵守:
- **I/O 模型**:從 `stdin` 讀取 JSON,往 `stdout` 輸出 JSON,不使用 return value
- **回傳格式**:成功 `{"success":true,"result":...}`,失敗 `{"success":false,"error":"..."}`
- **不 panic**:任何錯誤都應輸出 `success:false` JSON,不讓執行器收到空輸出
- **不使用網路 / 檔案系統**(功能類零件):`no_network_syscall: true`
- **允許網路**(整合類零件):`no_network_syscall: false`,必須宣告 `credentials_required`
---
## 目錄結構
```
registry/components/my_component/
├── component.contract.yaml # 零件規格宣告(必填)
├── main.go # TinyGo 源碼(TinyGo 零件)
├── assembly/index.ts # AssemblyScript 源碼(AS 零件)
├── src/lib.rs # Rust 源碼(Rust 零件)
└── my_component.wasm # 編譯產出(不提交至 git,CI 自動產生)
```
---
## component.contract.yaml
所有語言共用相同的合約格式:
```yaml
# component_hash_id 由 Registry 在提交時自動派發,格式為 cmp_{8碼hex}
# 提交者不需要填這個欄位,Registry 會根據 canonical_id 確定性生成
# Workflow 引用零件時,用 component_hash_id 才能保證永久不壞:
# component://cmp_a3f9b2c1 ← 推薦,即使 canonical_id 改名也不受影響
# component://string_reverse ← 方便,AI 寫 workflow 時用這個,Registry 自動解析
canonical_id: "string_reverse" # 見下方命名規範
display_name: "字串反轉" # 人類可讀,可中文,供 UI 顯示用
description: > # 語意搜尋用,見下方說明
將字串內容倒序排列,適合測試、資料清洗、回文判斷等場景。
不依賴外部服務,純本地運算。
category: "data" # api / logic / data / ai / style / anim / ui
version: "v1"
author: "@your-github-username"
wasi_target: "preview1"
stability: "floating" # floating / stable / pinned
runtime_compat:
- "cf-workers"
- "workerd"
- "wazero"
constraints:
max_size_kb: 2048
max_cold_start_ms: 50
no_network_syscall: true # 功能類 true,整合類 false
io_model: "stdin_stdout_json"
input_schema:
type: object
required: [text]
properties:
text:
type: string
description: 輸入文字
output_schema:
type: object
properties:
result:
type: string
gherkin_tests:
- scenario: "基本轉換"
given: '{"text":"hello"}'
then_contains: '"result"'
- scenario: "缺少必填欄位"
given: '{}'
then_contains: '"success":false'
config_example: |
transform:
text: "{{input.text}}"
description: "我的零件功能說明。"
```
整合類零件額外加入:
```yaml
credentials_required:
- key: my_api_token
type: api_key
description: "My Service API token"
inject_as: api_token
```
### canonical_id 命名規範
`canonical_id` 是零件的永久識別符,一旦上架不能更改(改名 = 新零件)。命名不統一會導致功能重複,請遵守以下規則:
| category | 格式 | 範例 |
|----------|------|------|
| `api`(整合類) | `{服務名}``{服務名}_{動作}` | `gmail``gmail_send``google_sheets``google_sheets_append``telegram` |
| `data`(資料處理) | `{資料型別}_ops``{動詞}_{名詞}` | `string_ops``array_ops``json_transform``csv_parse` |
| `logic`(控制流) | `{結構名}_control` 或單詞動詞 | `if_control``foreach_control``try_catch``switch``wait` |
| `ai`AI 類) | `ai_{動作}` | `ai_transform_compile``ai_summarize``ai_classify` |
**規則:**
- 全部小寫、底線分隔、最多 4 個單詞
- 禁止:中文、空格、大寫、連字號(`-`)、版本號混入(用 `version` 欄位表達)
- `display_name` 才是人類可讀名稱,可以是「宇宙無敵 gsheets 新增一列」,`canonical_id` 不行
**提交前自問:** 如果有人想用 AI 搜尋「幫我找一個可以新增 Google Sheets 列的零件」,他搜到的名字應該是什麼?答案就是你的 `canonical_id`
### description 寫法(語意搜尋)
`description` 是語意搜尋的索引來源,用自然語言描述「能做什麼、適合什麼情境」,而不是重複零件名稱。
**好的 description**
```yaml
description: >
傳送 Gmail 電子郵件,適合 Workflow 完成後通知使用者、發送訂閱確認信、
錯誤警報通知等場景。支援自訂主旨、內文與收件人。需要 Gmail OAuth token。
```
**不好的 description(等於沒有):**
```yaml
description: "Gmail 發信零件" # 只是名稱的同義詞,搜不到任何額外資訊
```
原則:把這個 description 給一個不知道這個零件存在的人看,他能判斷「這就是我要的東西」嗎?
### aliases(搜尋同義詞)
arcrun 在 `registry/aliases.yaml` 維護一份 scope 級別的同義詞表。當你的零件 `canonical_id` 以已知 scope 為前綴,Registry 建立搜尋索引時會**自動**把對應的同義詞合併進去,不需要在 contract 裡手動填。
例如 `canonical_id: google_sheets_append`Registry 會自動從 aliases.yaml 取得 `google_sheets` scope 的同義詞(`gsheets``試算表``spreadsheet`...),搜這些詞都能找到你的零件。
**如果你的零件有額外的情境同義詞**(超出 scope 範圍),可以在 contract 內手動補充:
```yaml
canonical_id: "google_sheets_append"
aliases:
- "新增資料列" # 情境同義詞,超出 scope 範圍
- "insert row"
# google_sheets scope 的同義詞(gsheets / 試算表 / spreadsheet...
# 由 registry/aliases.yaml 自動合併,不需要重複填寫
```
**想新增新 scope 的同義詞**(例如你要加一個 `notion` 零件):在 `registry/aliases.yaml` 的對應 category 下加一個新 key,開 PRmerge 後所有以 `notion_` 開頭的零件都自動繼承。
> 這個機制目前是手工維護。未來接入 KBDB 後,`canonical_id` 將獲得系統派發的唯一 hash id,同義詞表將成為 KBDB synonym graph 的初始資料。
---
## TinyGo 零件開發
### 環境安裝
```bash
# TinyGo
brew install tinygo # macOS
# 其他平台:https://tinygo.org/getting-started/
# 本機測試執行器
brew install wasmtime # macOS
```
### 給 AI 的提示範本
```
請幫我用 TinyGo 寫一個 arcrun WASM 零件。
需求:[你的需求]
規則:
- 從 stdin 讀取 JSON,往 stdout 輸出 JSON
- 成功回傳 {"success":true,"result":...}
- 失敗回傳 {"success":false,"error":"..."},不 panic
- 不使用網路、不使用檔案系統(純功能類零件)
- import 只用標準庫(encoding/json, os, fmt, strings 等)
請生成 main.go 和 component.contract.yaml。
```
### main.go 範本
```go
package main
import (
"encoding/json"
"fmt"
"os"
)
type Input struct {
Text string `json:"text"`
}
type Output struct {
Success bool `json:"success"`
Result string `json:"result,omitempty"`
Error string `json:"error,omitempty"`
}
func main() {
var input Input
if err := json.NewDecoder(os.Stdin).Decode(&input); err != nil {
writeError("invalid input: " + err.Error())
return
}
if input.Text == "" {
writeError("text is required")
return
}
// 你的邏輯
result := "[transformed] " + input.Text
out, _ := json.Marshal(Output{Success: true, Result: result})
fmt.Println(string(out))
}
func writeError(msg string) {
out, _ := json.Marshal(Output{Success: false, Error: msg})
fmt.Println(string(out))
}
```
### 編譯
```bash
cd registry/components/my_component
tinygo build -o my_component.wasm -target wasi .
```
### 本機測試
```bash
echo '{"text":"hello world"}' | wasmtime run my_component.wasm
# 預期:{"success":true,"result":"[transformed] hello world"}
echo '{}' | wasmtime run my_component.wasm
# 預期:{"success":false,"error":"text is required"}
```
---
## AssemblyScript 零件開發
### 環境安裝
```bash
# Node.js >= 18
node --version
# 初始化 AS 專案
npm init -y
npm install --save-dev assemblyscript
npx asinit .
# 本機測試執行器
brew install wasmtime # macOS
```
### 給 AI 的提示範本
```
請幫我用 AssemblyScript(不是 TypeScript)寫一個 arcrun WASM 零件。
需求:[你的需求]
規則:
- AssemblyScript 是 TypeScript 的子集,編譯為 WASM,不能使用 DOM / Node.js API
- 從 stdin 讀取 JSON(使用 WASI fd_read),往 stdout 輸出 JSON(使用 Console.log
- 成功回傳 {"success":true,"result":...}
- 失敗回傳 {"success":false,"error":"..."}
- 不使用網路、不使用檔案系統
請生成 assembly/index.ts 和 component.contract.yaml。
注意:AssemblyScript 沒有 JSON.parse,需要手動解析或使用 as-json 套件。
```
### assembly/index.ts 範本
```typescript
// AssemblyScript — 注意:這不是 Node.js / TypeScript
// 沒有 DOM、沒有 fetch、沒有 require
import { Console } from "as-wasi/assembly";
import { JSON } from "assemblyscript-json/assembly";
export function _start(): void {
// 從 stdin 讀取輸入
const input = Console.readAll();
// 解析 JSON
const parsed = JSON.parse(input);
if (!parsed.isObj) {
Console.log('{"success":false,"error":"invalid input"}');
return;
}
const obj = parsed as JSON.Obj;
const textVal = obj.getString("text");
if (textVal == null) {
Console.log('{"success":false,"error":"text is required"}');
return;
}
const text = textVal.valueOf();
// 你的邏輯
const result = "[transformed] " + text;
Console.log('{"success":true,"result":"' + result + '"}');
}
```
**專案依賴(package.json):**
```json
{
"dependencies": {
"as-wasi": "^0.4.7",
"assemblyscript-json": "^1.1.0"
}
}
```
### 編譯
```bash
cd registry/components/my_component
npm install
npx asc assembly/index.ts \
--target release \
--outFile my_component.wasm \
--exportRuntime \
--use abort=~lib/wasi_abort
```
### 本機測試
```bash
echo '{"text":"hello world"}' | wasmtime run my_component.wasm
# 預期:{"success":true,"result":"[transformed] hello world"}
```
---
## Rust 零件開發
Rust 零件支援已就緒,但文件尚在完善中。如果你熟悉 Rust + WASM,歡迎參考 [wasm-wasi 官方文件](https://doc.rust-lang.org/stable/reference/linkage.html),核心要求與其他語言相同:WASI preview1stdin/stdout JSON I/O。
基本設定:
```bash
rustup target add wasm32-wasip1
cargo build --target wasm32-wasip1 --release
```
---
## 提交至公眾 Registry
```bash
# 確保 .wasm 已編譯
ls my_component.wasm
# 提交(需要 arcrun.dev API Key
acr parts publish ./registry/components/my_component/
```
提交後流程:
| 狀態 | 說明 |
|------|------|
| `sandbox_pending` | 沙盒驗收執行中 |
| `author_only` | 驗收通過,你自己可用 |
| `public` | 人工審核通過,所有人可用,開始累積統計 |
查詢審核進度:
```bash
acr parts publish --status <submission_id>
```
---
## 常見問題
### `no_network_syscall` 設定錯誤
- **功能類**category: logic / data / ai):`no_network_syscall: true`。這類零件應完全沙盒化。
- **整合類**category: api):`no_network_syscall: false`,因為要呼叫外部 API。
兩者都需要宣告在 `constraints` 下,設錯會在 syscall 掃描步驟被沙盒拒絕。
### `gherkin_tests` 必須包含 happy path 和 error path
至少兩個測試場景:一個輸入正確的 happy path、一個缺少必填欄位或輸入非法的 error path。
### 體積超過上限
- TinyGo:確認使用 `-target wasi`(而非 `-target wasm`),前者體積更小
- AssemblyScript:加上 `--optimize``--target release`
- Rust:使用 `--release` 並加入 `opt-level = "z"``Cargo.toml`
---
## 問題回報
開 Issue[github.com/richblack/arcrun/issues](https://github.com/richblack/arcrun/issues)
-340
View File
@@ -1,340 +0,0 @@
# Arcrun 決策記錄(DECISIONS.md
> 這份檔案記錄 Arcrun 的**穩定決策**:架構定義、核心原則、為什麼這樣設計。
> 它很少改。任何 AI 或人接手 Arcrun,先讀這份。
> 流動的待辦在 `BACKLOG.md`
>
> `DECISIONS.md` 不是寫完就死的。想法變了要回來改它,讓它跟上——
> 怕的不是改,是「想法變了但檔案沒跟著改」,那它就變成說謊的文件。
>
> 最後更新:2026-05(第一期規劃期間整理)
---
## 0. 設計哲學
**第一條:每多依賴一個不可掌控的第三方,就多一個單點故障。**
Arcrun 賣的本質是「減少你對不可控第三方的依賴」。核心開源(MIT,公司倒了還能 fork)、
支援 self-hosted(跑在你自己的 CF,甚至自己的 wazero)、workflow 是純文字你自己擁有。
但「減少依賴」不等於「零依賴」——每個依賴要問:它掛了,我有沒有退路?有退路的依賴可接受。
**第二條:解耦 / 原子化。**
什麼都要能單獨換掉、不被綁死。primitive 與 recipe 解耦、執行與發現解耦、
引擎與零件解耦。AI 時代變化快,「可改」本身就是核心價值。
**第三條:Arcrun 是 AI 用品,但設計目標是「讓 AI 不可能做歪」。**
管住 AI 的開發痛點,和要賣給用戶的產品,是同一件事。
---
## 1. 架構:primitive 與 recipe
**Arcrun 只有四種 WASM primitive**
1. 流程控制(if / switch / filter / foreach / try_catch / wait …)
2. 文字 / 資料處理(string_ops / number_ops / array_ops / date_ops / set / merge …)
3. http_request(打任意 HTTP
4. credential(四個 auth WASMauth_static_key / auth_service_account / auth_oauth2 / auth_mtls
**其他一切都是 recipe。** recipe = http_request + 一組固定設定(YAML 文字)。
| | primitive | recipe |
|---|---|---|
| 是什麼 | WASM Worker | YAML 文字 |
| 要不要 deploy | 要 | 不用 |
| 多久變一次 | 幾乎不變 | 一直長新的 |
**判準(真零件 vs 假零件):** 一個零件若滿足任一條,它是假零件,該降級成 recipe:
- contract 或原始碼出現具體外部服務的 URL / domain
- 它宣告的能力是 http_request 的子集(打某固定 endpoint
→ D1 / KV / Vectorize / Supabase / KBDB 的存取,**一律是 http_request + recipe**
絕不做 `d1_crud``kbdb_get` 這種「假零件」。
### 「recipe」有三種,不要混用這個詞
Arcrun 裡有三個都叫 "recipe" 的東西,職責不同:
| 名稱 | 是什麼 | KV key |
|---|---|---|
| **API recipe** | http_request + endpoint/method/headers/body 模板(`RecipeDefinition` | `recipe:{canonical_id}` |
| **auth recipe** | 認證設定:primitive + base_url + 注入規則(`AuthRecipeDefinition` | `auth_recipe:{service}` |
| **prompt recipe** | LLM prompt 的封裝,與 KBDB block 有關 | `prompt_recipe:{name}` |
一般講「recipe」= API recipe。假零件降級的終點是 **API recipe**`recipe:{id}`)。
降級作業不碰 auth recipe、不碰 prompt recipeprompt recipe 犍涉 KBDB block 展開,
是 BACKLOG 待決策項,不在此範圍)。
### recipe 與 primitive 的驗收標準不同(早期已定,2026-05 重新確認)
- **primitiveGherkin 通過 = 驗收通過。** primitive 是封閉的邏輯,正確性不依賴外部世界,
可以用「given / when / then」確定地驗證。
- **recipe:打得通(2xx= 驗收通過。** recipe 是「指向外部 API 的指針」,正確性一半在定義
(打不通就代表定義錯,「打通」已驗)、一半在外部服務當下的行為。
**關鍵認識:recipe 不用 Gherkin,不是偷懶,是 Gherkin 對 recipe 沒用。**
「2xx 但外部服務沒真的做事」「外部服務改了 API」——這些是 recipe 唯一的真實風險,
而 Gherkin 一樣擋不住(Gherkin 測的當下沒改就過,之後改了它早跑完了)。
能補這個風險的只有「執行 → 回報 → 修正」的市場機制,不是任何靜態驗收。
給 recipe 加 Gherkin = 花成本做一件不會多驗到任何東西的事。
recipe 的「語義正確性」(真的刪了那列嗎)交給市場:A 的 recipe 在 B 那裡失效,
B 的 AI 會因為「目的沒達成」去查、去修、提修改版。Arcrun 不監控全世界的 API 變動。
**例外——可以是 primitive 的「引擎內建能力」:** 若某能力不依賴任何外部 endpoint、
是 Arcrun 執行環境本身自帶的(如「workflow 中途暫存」),它可以是 primitive。
判準:「這段邏輯依不依賴外部服務的 endpoint?」依賴→recipe;引擎自帶→可 primitive。
### 工作流是 default,建零件要過人類閘門(2026-05 補,CC 把自用服務錯做成零件後定)
AI 開發時的預設順序:
1. **預設寫工作流**(串服務 / 自用 / 給少數人,都先工作流)。default、阻力最小。
2. **零件的正當時機**:服務不提供串接但有 API,且**有必要讓全 Arcrun 生態重用** → 才建零件
(零件 = API 薄殼,只打一個 endpoint)。
3. **建零件 = 過人類閘門**。看到「有 API 可包成零件」≠「該包」,先問「你有必要嗎?」。
AI 不可自行建零件,必須 (a) 經人類互動確認;(b) 明示舉證「為何工作流做不到」
(舉證責任在 AI,預設假設工作流能做)。把關點在「建立零件的 API」本身——
CLI / MCP / Python lib / JS lib 四路全收斂到這關。
**為什麼要人類閘門**:零件進公共庫 = 全生態都能打它。自用服務(通訊錄 / 帳本)沒設驗證就變零件
= 公開後門。安全 / 意圖機器判不了,必須人看。規範會忘、hook 不會(§7:判準寫成機械紅燈)。
**不限制自由**:別人要建零件是他的自由(開無驗證服務給人串也是),唯一硬約束「零件 = endpoint 薄殼」。
閘門不是禁止,是「要建得先說服人 + 舉證」的摩擦。
**ABC 三管齊下讓 AI 不選難路**:A 審核當場擋(§7 層二)+ B 工作流範本好寫(§7 層一)
+ C mindset 明示預設(§7 層三)。人類閘門是第四道,專擋意圖 + 安全。
原理:難路走的當下要痛、易路選的當下要爽、事先有聲音說易路是 default。
CC 把自用服務錯做成零件,正因這三者當時全缺。
---
## 2. TS 邊界規則(哪些程式碼能用 TS,哪些不能)
**判準:這段程式碼是「執行引擎 / 工具本身」,還是「一個零件該做的事」?**
- 引擎 / 工具(cypher-executor 解析 graph、CLI 讀 YAML、registry 驗收)→ **TS,第一期合法**
- 零件該做的事(打 API、處理資料、做認證)→ **必須 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 標準介面的 wasm1080KB)。這是三層 runtime 的物理前提(見 §4)。
TinyGo 為官方首選(語法與 TS 差異夠大,AI 不易把純 TS 邏輯誤搬)。
---
## 3. 開源 / 商業邊界
| 開源核心(MIT) | 服務側(付費 / 需 API Key |
|---|---|
| cypher-executor、四種 primitive WASM、CLIacr)、registry | KBDB 語義搜尋、KBDB graph 查詢、Persona / Mira 等 |
**KBDB 採 Supabase 模式:** KBDB 的 recipe 顯示在公共零件庫(能力可見=引子),
要用就申請 API Key(註冊=轉化),用爽了付費。arcrun.dev 已有「取得 API Key」入口。
**「執行」與「發現」解耦:**
- 執行:永遠本地、免費、可離線。不依賴公共庫。
- 發現(語義搜尋):連線加值。self-hosted 用 local 關鍵字搜本地下載過的;
連公共庫才能用 KBDB 語義搜尋(在大集合裡查意圖)。
**cypher-executor 裡的 KBDB = 污染(清);公共零件庫的 KBDB = 服務(留,但不在第一期)。
---
## 3b. credential 是引擎能力(不是用戶零件)
(基於 2026-05 查核 graph-executor / credential-injector / auth-dispatcher 原始碼)
credential 的注入不是一個 workflow 節點做的事。它是**執行引擎在「呼叫零件之前」
自動做的一個步驟**graph-executor.executeNode 的流程是:
組 ctx → 注入 credential → 才 runner(ctx)。零件拿到 ctx 時 credential 已被放進去,
零件自己不知道 credential 怎麼來的。
**credential 的本質 = KV 裡的加密值 + 一段「在零件執行前注入 ctx」的引擎逓輯。**
那四個 `auth_*` WASMstatic_key / service_account / oauth2 / mtls)是「注入步驟的後端」,
不是用戶會在 workflow 擺的零件。用戶永遠不會直接呼叫 `auth_static_key`——
是 auth-dispatcher 在背後呼叫它。
→ 「零件白名單」「假零件判準」不適用於 `auth_*`,它們不是用戶零件。
**credential 系統現状是「新舊兩路並存」的半成品:**
- 新路(對的方向):`auth-dispatcher` → HTTP 打 `auth_*` WASM,解密/JWT 全在 WASM 內。
已支援 static_key / service_account / oauth2mtls 尚未(Phase 4)。
- 舊路(要砸):`injectCredentials` TS 裡解密,含 `BUILTIN_CREDENTIALS_MAP`
註解自認 Phase 1.9 將刪除。砦舊路是獨立清理,**不擋降級**(見 BACKLOG)。
**注入靠 `auth_recipe:{componentId}` 觸發。** 一個服務要能被注入 credential,
必須有對應的 auth recipe。KBDB 用 static_key,而 static_key 新路已支援
→ 降級 KBDB 的 credential 前置是「小」的(只需建一個 `auth_recipe:kbdb`)。
**修正一個舊裁決:** API recipe 的 `credentials_required` 欄位 **要留**
`makeRecipeRunner`(零件執行)不讀它,但 `injectCredentials`(零件執行前的注入步驟)
會讀它。credential 不是在 runner 裡處理,是在 runner 之前那一步處理。**
---
## 3c. execute vs test:意圖決定路徑(服務側,不在第一期)
一個 AI 開發 recipe,有兩種意圖,對應兩個指令:
- **只打算自己用** → 用 `execute`。直連目標 API**不經過 arcrun**self-hosted 純粋。
- **打算公佈到公共零件庫換 credits** → 從開發的第一次打就用 `test``test` 明示走 arcrun relay。
**關鍵:用 `test` 這個動作本身,就是「我打算公佈」的意思表示。**
AI 用 test 開發時,每一次打都經過 relay,arcrun 當下就看到真實打通記錄——
不需 AI 事後交一份自己寫的 log(執行者不能驗證自己,見 §7),也不需 arcrun 事後重打
(重打 delete 要自備測試環境,跟開發者工作重複、荒謬)。
### test relay 經手 credential — 誠實處理
`test` 走 relay,請求裡帶著 credential。**relay 為了轉發給目標 API,必須在內部
持有明文 credential 一瞬間——這是 proxy 的物理本質,加密絕對絍不過。**
(加密只能保護「客戶→relay」傳輸途中防監聽;relay 內部必然看得到明文。)
唯一誠實的處理方式(四道合起來才站得住):
1. **明示告知**`acr` 第一次用 test 就告訴用戶「test 會讓請求(含 credential)經過 arcrun relay
arcrun 只記錄 HTTP 回應、不記錄 credential。要完全不經過請用 execute」。
2. **「不記錄」從「承諸」升級成「可驗證」**:relay 是開源的(arcrun 核心 MIT),
用戶不需「相信」,可以讀 relay 原碼確認它沒記錄 credential。
relay 程式上:credential 欄位全程不寫日誌、不寫存儲,只在轉發那一瞬間的記憶體。
3. **傳輸層 TLS**:客戶 → relay → 目標全程加密,防線路監聽。
4. **縮小經手 credential 的價值**:用 test 開發時建議用測試帳號的 credential,不是生產環境的。
**絕不做「假加密」**——不要讓用戶以為連 relay 都看不到 credential。
兩段分開誠實講:「傳輸途中加密(防監聽)+ relay 內部短暫持有明文(開源可驗證、不記錄)」。
**範圍:** `test` / relay / credits 這整套是服務側工程,依賴公共零件庫與 credits 系統存在。
**不在第一期**(第一期是 self-hosted 能跡、`execute` 能用)。
---
## 4. cypher-binding、三層 runtime、執行核心(長期,現在不做)
> 註:credential 是引擎能力、不是第四種 primitive。詳見下方 §3b(基於 2026-05 原始碼查核)。
### cypher-binding 是什麼
workflow 不是「部署出來的東西」,是「一張可隨時改的紙」——紙上寫一排零件 + 順序 + 條件。
執行核心讀紙:跑第一個,跑完回來問紙,紙說下一個是誰,就跑下一個。
對比 service bindingCF 機制,a/b 都要 deploy):cypher-binding **不 deploy**
改 workflow 零部署成本。可能慢一點,但「寫好、按下去就跑」(像 n8n)。
### 三層 runtime 目標
零件與執行核心目標是能跑在三種環境:
- Tier 1Cloudflare Workers(現況)
- Tier 2:企業自架 workerd(不信任 CF 雲)
- Tier 3:極輕量 WASI runtimewazero,無人機 / 邊緣設備)
**現在不寫任何 wazero / workerd 程式碼。** 第一期沒有 Tier 2/3 用戶。
避債的方式不是「現在做三層」,是「現在不要做任何擋死三層的決定」。
### cypher-executor 概念上分兩層
(現在糊在一起。這**不是會累積的債**——它是一支程式、就一支,未來一次性重構即可;
不像零件會複利累積。它是「待設計的未來解法」,不需要為它焦慮。)
- **(1) 執行核心** — 讀紙、依序/依條件呼叫零件。應能編成純 WASI,跑三層任何地方。
- **(2) Cloudflare 整合層** — webhook / KV / cron / Service Binding / HTTP 路由,只服務 Tier 1/2。
Tier 3(無人機)只需要 (1)。現在不拆,但**從現在起新程式碼要有意識地把
「讀紙、呼叫零件的核心邏輯」和「Cloudflare 特有存取」分開寫,不要糊得更死**。
### KV 依賴的根源,與「紙要自包含」
現在 cypher-executor 依賴 KV,根源**不是**「紙存在 KV」,是「紙上寫的是 hash
cmp_xxx / rec_xxx),要查 KV 才能翻譯成真正的零件」。
**hash 查表不是會累積的債**——它是 component-loader 裡固定的一段邏輯,零件再多它也不變
(同一段邏輯處理更多資料 ≠ 需要更多段邏輯)。但它讓「紙」無法自包含。
Tier 3 的正解**不是**「帶一個 SQLite 上無人機來翻譯 hash」——那只是把依賴從 KV 換成
SQLite,沒有消除依賴。正解是**紙本身自包含**:紙上直接寫 URL / 內嵌 recipe YAML
不寫 hash。無人機上只有「自包含的紙 + WASM 零件」,不需要任何查表設施。
- hash = Tier 1 的**儲存格式**KV 去重 / 版本管理,Tier 1 保留無妨)
- 自包含 = **執行格式**
- 中間隔一個「展開」步驟:打包給無人機時做一次,不是執行時做(類似 `acr push` 的轉換)
**附帶好處:** 自包含的紙人類 / AI 可直讀——這正是 Arcrun 核心賣點(紙人人可讀)。
hash 其實偷偷腐蝕了這個賣點。
---
## 5. 第一期範圍鎖定
**第一期用戶 = 會開 CF 帳號的開發者,在 VSCode 用 Claude CodeCC),self-hosted。**
鎖定 Claude,因為所有「防做歪」機制都是針對 Claude 行為校準的;
Gemini / Codex 服從度不同,第一期不支援。
**第一期做:** 清污染 → 降級假零件成 recipe → 補零件庫真把關 →
白名單 hook → (搬家)→ mindset Skill → README 重寫成單一路徑 → acr init --self-hosted。
**第一期明確不做:** SaaS、API Key 多租戶、小白 onboarding、視覺化的圖、
arcrun-gui 拖拉畫布、arcrun-mcp 命名大改、新 primitive、Gemini/Codex 支援、
三層 runtime、KBDB 訂閱層。
**SaaS 解凍條件(不靠心情):** (a) self-hosted 有 ≥3 個你以外的人部署成功並貢獻
≥1 個 recipe,且 (b) 視覺化 Skill 已驗證「AI 畫的圖能讓非技術者看懂」。
---
## 6. CF 帳號與專案模型
- **CF 帳號:一個人一個,永遠就一個。** 不隨專案增加。「實驗環境」是一個 prefix,不是一個帳號。
- **Arcrun 部署:一套就夠**(像 n8n 就一套)。不是每個專案各一套。
- **「專案」是 Arcrun 的一等公民**:專案 = 一組「引用 workflow」的三元組關係(存在 KBDB / 三元組儲存,
不需要新資料表)。同一個 workflow 可被多個專案引用,改一次全部生效。
- 預設共用 workflow(引用);只有結構性差異才 fork,且 fork 要有摩擦感。
- credential 綁專案:KV key 帶專案前綴(`mira:notion_token`),邏輯隔離,非物理隔離。
---
## 7. 「讓 AI 不做歪」的三層機制 + 閉環
**閉環原則:下指令的、執行的、驗證的,必須是三個不同的角色。**
執行者驗證自己 = 沒有驗證。驗證的標準必須來自執行者碰不到、改不了的地方
(一份判準 / 一個程式 / 一個拿著判準的獨立角色)。
閉環:指令(說要什麼,不說怎麼做)→ 查閱判準 → 執行 → 獨立驗證 → 不合格退回並告知正路。
**事前防禦(擋已知的錯):**
- 層一:範本——AI 不從白紙生成,永遠在改一個正確的範本(acr new / scaffold
- 層二:會回嘴的 CLI——走歪當場 exit 2 + 指回正路(這是真護城河)
- 層三:mindset Skill——給 AI「Arcrun 很簡單、一切皆 recipe」的世界觀
**事後機制(抓漏網的錯):** 事前防禦永遠堵不滿,剩下交給事後:
- 第一層:可審計軌跡——每個動作留不可竄改記錄,事後能追
- 第二層:不變式測試——核心原則寫成自動測試,每次 commit 跑(最重要)
- 第三層:定期獨立審查——拿判準重看,審查者手上必須有判準
**關鍵心態:不要訓練自己的「辨識能力」**——不可靠、會累、無法轉移給 low-code 用戶。
要把判準寫成機械測試,讓「紅燈」代替「辨識」。同一個錯誤,以「問句」形式出現你抓不到,
以「紅燈」形式出現你一定抓到。
---
## 8. 執行鏈路不依賴 GitHub Actions(零件投稿例外,2026-05-30 釐清)
Arcrun 第一期的**執行鏈路**init / push / run / recipe)全在「用戶機器 + Cloudflare」之間,
不經過 GitHub Actions。這是常態高頻動作,用戶不該被 CI 卡住。理由見 §0 第一條。
**零件投稿是例外,走 GitHub PR + CI2026-05-30):**
- 零件投稿是**稀有低頻**事件(primitive 極少、未來絕大部分是 recipe;建零件要過人類閘門)。
- 稀有事件用 PR 治理最自然:**PR 必須有人 merge = 人類閘門**AI 偽造不了 GitHub approve);
把關(假零件偵測 / 純WASI / Gherkin / 覆蓋檢查)由 **CIPR check)跑**
- 為何非 CI 不可:**CF Workers 禁止 request-time 編譯 WASM** → registry Worker 跑不了 Gherkin / 向量;
CI 有 tinygo + 能 runtime 跑 wasm,是唯一既能跑 wasm 又「執行者碰不到」的 venue。
- **這不違反 §8 精神**:§8 防的是「高頻執行鏈路被 CI 卡住」;零件投稿稀有、且該由 PR 把關,
用 PR/CI 反而更對。兩者是不同性質的事。
- 詳見 `.agents/specs/component-gatekeeping/`
---
## 附:如何判斷「某個東西會不會累積成債」
通用判準——當「東西」變多時:
- **同一段邏輯處理更多資料** → 不累積。(例:hash 查表,零件再多也是同一段查表邏輯)
- **需要更多段邏輯、更多特例** → 會累積。(例:零件,每個是獨立程式、各自可能出錯)
不累積的東西不需要焦慮,它頂多是「未來一次性處理的設計點」。
會累積的東西必須在「進入點」就把關(例:零件在投稿時就驗純淨)。
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2026 richblack
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+14
View File
@@ -6,6 +6,20 @@ arcrun 是一套**給 AIClaude Code)用的 harness**:你叫 CC 用 arcrun
---
> ## 🤖 給 AI 操盤手(Claude Code 等)
>
> 如果使用者把這個 repo / 網站丟給你、要你用 arcrun 做自動化——**你的第一步**:
>
> ```
> npm i -g arcrun && acr install-harness
> ```
>
> 這會把 arcrun 的防護(mindset Skill、CLAUDE.md 提醒、防做歪 hook、`/arcrun` 指令)裝進使用者**當前專案**,裝完你就有完整指引。
>
> **裝前先記住**:使用者選 arcrun = 不要自己寫 Python/Node 一次性腳本;打外部 API 寫 recipe;不自製零件;所有 CLI 操作你來做。完整啟動指南見 [`llms.txt`](llms.txt)。
---
## 為什麼做這個?
AI 愈來愈強,但跟 AI 協作做自動化還是很有摩擦:
+15
View File
@@ -0,0 +1,15 @@
<!-- arcrun-harness:start -->
## 這個專案用 arcrun 做自動化
當需要「串服務 / 排程 / 打 API / 資料處理自動化」時:
- **用 arcrun 工作流,不要自己寫 Python/Node 一次性腳本。** arcrun 工作流是純文字、可複用、跑在使用者自己的 Cloudflare。選 arcrun 就是不想要每次重刻的腳本。
- **打外部 API → 寫 recipe**`acr recipe push`),不要自刻 HTTP client。recipe = http_request + 固定設定的 YAML,不用部署。
- **先查能力再動手**`acr parts`(看可用零件)、`acr auth-recipe list`(看支援的認證服務)。
- **不要自製零件**WASM component)——零件由 arcrun 維護、走 GitHub PR。你能擴充的是 recipe + 工作流。
- **需要 AI 判斷/轉換時,你自己做**,再呼叫工作流做確定性的下一步。不要在工作流裡放零件回頭呼叫 LLM。
- **暴露資料要人類同意**:部署對外 webhook / push recipe 會讓東西可被外部呼叫 → 停下來讓使用者明示同意,不替他決定公開。
- **誠實**:沒打通就誠實說(缺 credential 標「未驗收:缺 X」),不假裝成功;完成以 HTTP 2xx / trace 為證,不口頭宣布。
開始前讀 **arcrun-mindset** Skill(世界觀)。使用者技術細節交給你,CLI 操作你來做。
<!-- arcrun-harness:end -->
+29
View File
@@ -0,0 +1,29 @@
# 用 arcrun 完成這個自動化需求
使用者想做一個自動化。你的任務:用 arcrun 做出來,全程不要讓使用者自己寫程式。
## 鐵則
- **用 arcrun 工作流 / recipe,絕不自己寫 Python/Node 腳本。** 使用者選 arcrun 就是不想要一次性腳本。
- 打外部 API → 寫 recipe`acr recipe push`),不自刻 HTTP client。
- 不自製零件(WASM)—— 零件由 arcrun 維護。你能用的是現有零件 + recipe + 工作流。
- 需要 AI 判斷時你自己做,不要讓工作流回頭呼叫 LLM。
## 步驟
1. 先讀 **arcrun-mindset** Skill(世界觀 + 資源去哪取)。
2. 跑 `acr parts` 看零件、`acr auth-recipe list` 看支援的認證。**先查再動手。**
3. 把使用者需求拆成工作流(哪些零件、什麼順序、什麼條件),寫成 `.yaml`
4. 需要 credentialAPI key / token)→ 用 `acr auth-recipe scaffold <service>` 看要哪些,
明確告訴使用者去哪取得、怎麼 `acr creds push`
5. `acr validate` 通過後 `acr push` 部署,告訴使用者 webhook URL / 怎麼 `acr run`
6. 完成給客觀證據(HTTP 2xx / trace),不要只說「做好了」。
## 遇到要暴露資料(對外 webhook)
停下來,明確告訴使用者「這會讓 X 可被外部呼叫」,要他同意。不要替他決定公開。
## 還沒設定好 arcrun
`acr` 指令不存在或還沒 `acr init`:先帶使用者完成前置設定
(裝 CLI → 拿 Cloudflare 帳號的兩串憑證 → `acr init --self-hosted`)。
拿 Cloudflare 憑證時用白話照抄式引導,不要對使用者講 KV / Worker / R2 等術語。
## 使用者的需求
(使用者打在 /arcrun 後面的文字會接在這裡)
+54
View File
@@ -0,0 +1,54 @@
#!/bin/bash
# arcrun-guard.sh — 用戶專案的 arcrun PreToolUse guard(由 acr install-harness 裝進 .claude/hooks/
#
# 對象:在「用 arcrun 開發」的專案裡工作的 CC。擋它走歪(退回自寫 Python / 不用 recipe / 未經同意暴露)。
# 與 arcrun repo 開發版 hook 完全不同(那個擋的是開發 arcrun 本身)。
#
# 鐵則(user-cc-harness design §0.5):每次擋下/提醒都要給「具體怎麼做才對」的正路,不只說「不行」。
#
# 退出碼:0=允許(可附 stderr 提醒);2=硬擋(stderr 回給 CC)。
# 分級(design §4):多數用「提醒不硬擋」(避免誤殺正常 python);硬擋只留給「未經同意暴露資料」。
set -o pipefail
INPUT=$(cat)
TOOL=$(echo "$INPUT" | jq -r '.tool_name // ""')
CMD=$(echo "$INPUT" | jq -r '.tool_input.command // ""')
remind() {
# 提醒但放行(exit 0)。CC 看到 stderr,自己判斷是否真要繼續。
echo "💡 arcrun 提醒:$1" >&2
echo " 正路:$2" >&2
exit 0
}
block() {
echo "❌ arcrun guard 擋下:$1" >&2
echo " 正路:$2" >&2
exit 2
}
# ── 硬擋:未經人類同意的暴露動作(明確越界,mindset §5)──────────────
# 非互動環境下 CC 自己跑「部署對外 webhook / push recipe」= 替人類決定公開。
if echo "$CMD" | grep -qE "acr (push|recipe push)\b"; then
if [ ! -t 0 ] && [ "${ARCRUN_HUMAN_CONFIRMED:-}" != "1" ]; then
block "在非互動環境自動執行暴露動作(acr push / recipe push 會讓東西可被外部呼叫)" \
"把這動作交給人類在終端機執行,或先讓使用者明示同意。不要替使用者決定公開。"
fi
fi
# ── 提醒(不硬擋):退回自寫 Python/Node 一次性自動化 ──────────────────
# 「我先用 Python 測試」這類退回熟悉工具的傾向。python 不絕對錯(可能跑測試),故提醒不擋。
if echo "$CMD" | grep -qE "(^|[;&| ])(python3?|node)[ ]+[^ ]+\.(py|js|mjs|ts)\b"; then
# 排除明顯的測試 / 既有工具呼叫(pytest / npm test / jest 等)降低誤判
if ! echo "$CMD" | grep -qE "(pytest|jest|vitest|npm (run )?test|mocha|\btest_)"; then
remind "偵測到用 python/node 跑腳本。這專案用 arcrun,串服務/自動化不要自刻一次性腳本。" \
"先跑 \`acr parts\` 看有哪些零件,把需求寫成 workflow.yaml 用 \`acr run\`。若這確實不是自動化(例如跑測試/別的工具),忽略本提醒。"
fi
fi
# ── 提醒(不硬擋):自寫打固定 API 的 script,而非 recipe ──────────────
if echo "$CMD" | grep -qE "(curl|fetch|requests\.(get|post)|axios).*https?://"; then
remind "偵測到自己打外部 API。arcrun 裡「打固定 endpoint」應寫成 recipe,不自刻 HTTP 呼叫。" \
"用 \`acr recipe push\` 把這個 API 包成 recipeworkflow 裡用 component 引用它。見 arcrun-mindset Skill。"
fi
exit 0
+16
View File
@@ -0,0 +1,16 @@
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": ".claude/hooks/arcrun-guard.sh",
"timeout": 5
}
]
}
]
}
}
@@ -0,0 +1,78 @@
---
name: arcrun-mindset
description: >-
arcrun 的世界觀 — 用 arcrun 開發自動化時的預設心態 + 資源去哪取。當你(AI 操盤手)要在
arcrun 上做任何事(串服務、處理資料、認證、把東西開放給人用)前讀這個。它讓你做出「方向對」
的選擇、知道資源在哪,避免技術上能跑但架構上錯、或自己重刻 arcrun 已有的東西。
---
# arcrun mindset(給 AI 操盤手)
你在 arcrun 上幫使用者開發自動化。arcrun 很簡單,簡單到你常會把它想複雜、或退回自己熟悉的
Python/Node 自刻。這份幫你在岔路上選對方向,並告訴你資源在哪。
---
## 0. 一句話世界觀
**arcrun 裡幾乎所有東西都是工作流(workflow)。** 工作流 = 一張紙,寫「用哪些零件、什麼順序、什麼條件」。
你大部分時間在寫紙、改紙,不是在造新零件、也不是自己寫腳本。
---
## 1. 工作流是 default,不要退回自己寫 Python
使用者選 arcrun,就是不要「每次重刻、跑完即丟」的腳本。所以你的預設順序:
1. **先想能不能用工作流做**(串現有零件 / recipe + 流程控制)。99% 可以。
2. 要打的服務有 HTTP API、但沒有對應 recipe → **寫一個 recipe**http_request + 固定設定 YAML,不用部署、不用審核)。
3. **只有**封閉純邏輯(流程控制 / 資料處理)、現有零件不夠、且值得全 arcrun 重用 → 才考慮零件(而零件走 PR,不是你現在做)。
> 典型走歪:「我先用 Python 測一下」。停。使用者要的是 arcrun 工作流。先 `acr parts` 看有什麼,用工作流串。
## 2. 資源去哪取(不要自己重造 arcrun 已有的)
| 你想知道 | 跑這個 |
|---|---|
| 有哪些零件可用 | `acr parts` |
| 某零件的設定範本 | `acr parts scaffold <name>` |
| 支援哪些服務的認證 | `acr auth-recipe list` |
| 某服務認證要哪些 credential + 範例 | `acr auth-recipe scaffold <service>` |
| 已上傳的 recipe | `acr recipe list` |
| 工作流語法、指令 | `acr --help` |
**先查再動手**——arcrun 多半已經有你要的零件 / recipe / 認證,不要自刻。
## 3. arcrun 是你(AI)用的工具,不是工具回頭呼叫 AI
需要智慧判斷 / 自然語言轉換時,**你自己做**,再呼叫工作流執行確定性的下一步。
**不要在工作流中間放零件回頭呼叫 LLM**。arcrun 的大腦就是操盤的你。
## 4. arcrun 不替你做授權判斷
API 打不打得通由發 key 的服務決定。401/403 是對方服務在行使授權,**不是 arcrun 的 bug、不是你做錯**。
不要在 arcrun 裡建「允許/禁止某 endpoint」的二次授權清單。
## 5. 把東西開放給別人用 = 要使用者明示同意
部署對外 webhook、push recipe 會讓資料/能力**可被外部呼叫**(暴露面):
- 停下來,明確告訴使用者「這會讓 X 可被外部呼叫」,要他同意。**不替他決定公開。**
- 非互動環境(你直跑)遇到 → 停,要人類確認,絕不自己塞 confirm 假裝同意。
- arcrun 可提供保護(要求呼叫者帶 key / 限流)——提醒使用者。
## 6. 誠實(最重要)
- **不假綠**:沒打通就誠實說。缺 credential 打不到 2xx → 標「未驗收:缺 X」,不 mock 充綠燈。
- **不假裝防偽 / 不代替人類確認**有風險的動作(暴露資料)。
- **完成 = 客觀證據**HTTP 2xx + trace),不是口頭「做好了」。
---
## 怎麼用這份 mindset
每次準備動手,先過一遍:
1. 這能用工作流 / recipe 做嗎?(多半能 → 別自己寫 Python、別造零件)
2. 我查過 `acr parts` / `acr auth-recipe` 了嗎?(arcrun 可能已有)
3. 我是不是讓工作流回頭呼叫 AI?(是 → 改成我自己做)
4. 這動作會把資料開放給別人嗎?(會 → 要使用者明示同意)
5. 我有沒有假裝(假綠 / 假防偽 / 代替人類確認)?(有 → 停,誠實標明)
+5 -4
View File
@@ -1,7 +1,7 @@
{
"name": "arcrun",
"version": "1.1.0",
"description": "AI Workflow CLI for arcrun — deploy and run WASM-based AI workflows on Cloudflare",
"version": "1.2.0",
"description": "AI Workflow CLI for arcrun — self-host WASM-based AI workflows on your own Cloudflare",
"bin": {
"acr": "dist/index.js"
},
@@ -27,7 +27,8 @@
"node": ">=18.0.0"
},
"files": [
"dist/"
"dist/",
"harness/"
],
"keywords": [
"cloudflare",
@@ -40,6 +41,6 @@
"license": "MIT",
"repository": {
"type": "git",
"url": "git+https://github.com/richblack/arcrun.git"
"url": "git+https://github.com/uncle6me-web/Arcrun.git"
}
}
+9
View File
@@ -18,6 +18,7 @@ import {
type DeployContext,
} from '../lib/deploy.js';
import { API_RECIPE_SEEDS } from '../lib/api-recipe-seeds.js';
import { cmdInstallHarness } from './install-harness.js';
const ARCRUN_REGISTER_URL = 'https://cypher.arcrun.dev/register';
@@ -42,6 +43,14 @@ export async function cmdInit(options: { local?: boolean; selfHosted?: boolean }
} finally {
rl.close();
}
// init 末尾順便裝 CC harness 進當前專案(SDD user-cc-harness §2init 裝 + 可單獨裝)。
// 失敗不影響 init 本身(harness 是加分,可事後 acr install-harness 補)。
try {
await cmdInstallHarness();
} catch (e) {
console.log(chalk.gray(` harness 安裝略過:${e instanceof Error ? e.message : e};可稍後跑 acr install-harness`));
}
}
async function initLocal(): Promise<void> {
+127
View File
@@ -0,0 +1,127 @@
/**
* acr install-harness CC harness
*
* arcrun CC + 使使 CC arcrun
* - CLAUDE.md Python
* - .claude/skills/arcrun-mindset/ +
* - .claude/commands/arcrun.md/arcrun slash command
* - .claude/hooks/arcrun-guard.sh + settings.json
*
* 使 CLAUDE.md / settings
* SDD.agents/specs/user-cc-harness/design.md §2
*/
import { fileURLToPath } from 'node:url';
import {
readFileSync, writeFileSync, existsSync, mkdirSync, copyFileSync, chmodSync, readdirSync,
} from 'node:fs';
import { join, dirname } from 'node:path';
import chalk from 'chalk';
/** harness npm SSOT=cli/harness/
* build dist/commands/harness/ ../../harness */
function harnessRoot(): string {
const here = dirname(fileURLToPath(import.meta.url)); // .../dist/commands
return join(here, '..', '..', 'harness'); // .../harness
}
const START = '<!-- arcrun-harness:start -->';
const END = '<!-- arcrun-harness:end -->';
export async function cmdInstallHarness(): Promise<void> {
const cwd = process.cwd();
const src = harnessRoot();
if (!existsSync(src)) {
console.error(chalk.red(`找不到 harness 素材(${src})。套件安裝可能不完整,請重裝 arcrun。`));
process.exit(1);
}
console.log(chalk.bold('\n 安裝 arcrun harness 到當前專案\n'));
// 1. CLAUDE.mdappend/取代 arcrun 區塊(標記包夾,冪等)
installClaudeBlock(cwd, src);
// 2. mindset Skill
copyTree(join(src, 'skills'), join(cwd, '.claude', 'skills'));
console.log(chalk.green(' ✓ .claude/skills/arcrun-mindset/'));
// 3. /arcrun command
copyTree(join(src, 'commands'), join(cwd, '.claude', 'commands'));
console.log(chalk.green(' ✓ .claude/commands/arcrun.md'));
// 4. hook
const hookDst = join(cwd, '.claude', 'hooks', 'arcrun-guard.sh');
mkdirSync(dirname(hookDst), { recursive: true });
copyFileSync(join(src, 'hooks', 'arcrun-guard.sh'), hookDst);
chmodSync(hookDst, 0o755);
console.log(chalk.green(' ✓ .claude/hooks/arcrun-guard.sh'));
// 5. settings.json:合併 hook 註冊(不覆蓋使用者既有設定)
mergeSettings(cwd, src);
console.log(chalk.green(' ✓ .claude/settings.json(已合併 arcrun guard hook'));
console.log(chalk.gray('\n 提示:'));
console.log(chalk.gray(' • 首次在此專案開 Claude Code 會要求「信任工作區」,按信任 hook 才生效。'));
console.log(chalk.gray(' • 之後跟 CC 說需求即可(或打 /arcrun <你的需求>)。'));
console.log(chalk.gray(' • CC 偏好 MCP?可另跑 acr update 連 arcrun MCPMCP 對齊中,optional)。\n'));
}
/** CLAUDE.md:無→建;有 arcrun 區塊→取代;有但無區塊→append。標記包夾,冪等。 */
function installClaudeBlock(cwd: string, src: string): void {
const block = readFileSync(join(src, 'CLAUDE.block.md'), 'utf8').trim();
const path = join(cwd, 'CLAUDE.md');
if (!existsSync(path)) {
writeFileSync(path, block + '\n', 'utf8');
console.log(chalk.green(' ✓ CLAUDE.md(已建立,含 arcrun 區塊)'));
return;
}
const cur = readFileSync(path, 'utf8');
if (cur.includes(START) && cur.includes(END)) {
// 取代既有區塊
const re = new RegExp(escapeRe(START) + '[\\s\\S]*?' + escapeRe(END));
writeFileSync(path, cur.replace(re, block), 'utf8');
console.log(chalk.green(' ✓ CLAUDE.md(已更新 arcrun 區塊)'));
} else {
writeFileSync(path, cur.replace(/\s*$/, '') + '\n\n' + block + '\n', 'utf8');
console.log(chalk.green(' ✓ CLAUDE.md(已附加 arcrun 區塊,未動既有內容)'));
}
}
/** 把 settings.fragment.json 的 hook 合併進專案 settings.json(不覆蓋使用者既有 hooks/設定)。 */
function mergeSettings(cwd: string, src: string): void {
const fragment = JSON.parse(readFileSync(join(src, 'settings.fragment.json'), 'utf8'));
const path = join(cwd, '.claude', 'settings.json');
mkdirSync(dirname(path), { recursive: true });
let settings: Record<string, unknown> = {};
if (existsSync(path)) {
try { settings = JSON.parse(readFileSync(path, 'utf8')); } catch { settings = {}; }
}
const hooks = (settings.hooks ?? {}) as Record<string, unknown[]>;
const fragHooks = fragment.hooks as Record<string, unknown[]>;
for (const [event, entries] of Object.entries(fragHooks)) {
const existing = Array.isArray(hooks[event]) ? hooks[event] : [];
// 去重:避免重裝重複加 arcrun-guard
const serialized = new Set(existing.map(e => JSON.stringify(e)));
for (const e of entries) {
if (!serialized.has(JSON.stringify(e))) existing.push(e);
}
hooks[event] = existing;
}
settings.hooks = hooks;
writeFileSync(path, JSON.stringify(settings, null, 2) + '\n', 'utf8');
}
/** 遞迴複製目錄樹(覆蓋同名檔)。 */
function copyTree(srcDir: string, dstDir: string): void {
if (!existsSync(srcDir)) return;
mkdirSync(dstDir, { recursive: true });
for (const name of readdirSync(srcDir, { withFileTypes: true })) {
const s = join(srcDir, name.name);
const d = join(dstDir, name.name);
if (name.isDirectory()) copyTree(s, d);
else copyFileSync(s, d);
}
}
function escapeRe(s: string): string {
return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}
+7
View File
@@ -17,6 +17,7 @@ import { cmdRecipePush, cmdRecipeList, cmdRecipeDelete } from './commands/recipe
import { cmdList } from './commands/list.js';
import { cmdLogs } from './commands/logs.js';
import { cmdUpdate } from './commands/update.js';
import { cmdInstallHarness } from './commands/install-harness.js';
import { cmdAuthRecipeList, cmdAuthRecipeInfo, cmdAuthRecipeScaffold } from './commands/auth-recipe.js';
const program = new Command();
@@ -127,4 +128,10 @@ program
.description('self-hosted:拉新 release 並重新部署到你的 Cloudflare')
.action(() => cmdUpdate());
// acr install-harness(把 arcrun 的 CC harness 裝進當前專案)
program
.command('install-harness')
.description('把 arcrun 的 CC harnessmindset/提醒/防做歪 hook/指令)裝進當前專案')
.action(() => cmdInstallHarness());
program.parse(process.argv);
-192
View File
@@ -1,192 +0,0 @@
# 交辦文件:完成 arcrun self-hosted harness(給接手的 CC
> 建立:2026-06-01(由前一個 CC 調查後撰寫)
> 對象:接手的外部 CC
> 目的:把 arcrun 補到「任何 CC 在自己的 CF 帳號上 self-host 後就能順暢開發、且不可能重蹈 mira 的錯」的程度。
>
> **先讀**`DECISIONS.md`(穩定決策)、`.claude/rules/06-mindset.md`mindset)、`BACKLOG.md`(流動待辦)。
> 本文件不取代它們,只是把「今天要做的三件事」連同已查證的實況整理好,讓你不用重跑調查。
---
## 0. 戰法已轉變(最重要的背景)
richblack 2026-06-01 決定:**從 SaaS 改成 self-hosted 開源策略。**
這直接改變 harness 的成功定義:
- **舊定義**:在 richblack 的 prod 帳號(`cypher.arcrun.dev`)上能跑。
- **新定義**:**任何 CC 在自己的 CF 帳號上 `acr init --self-hosted` 後就能跑通一個含 recipe 的 workflow,而且寫錯時會被程式擋住。**
richblack 會用另一個 CF 帳號實測 self-host。所以「self-hosted 一鍵起得來」從「第一期重要但非阻擋項」**升為今日第一優先**。
### arcrun 現在的核心心智(richblack 2026-06-01 校準,比 DECISIONS §1 更硬)
- 核心**零件數量少、由 richblack 維護、不接受 CC 自製**(可投稿 PR,人 merge = 閘門)。
- 其他人做的一律是 **recipe**= http_request + 一組 YAML 設定,不用 deploy)。
- arcrun 是**一套給 CC 的 harness**:事前提醒 CC 能用什麼 / 不能做什麼,事後用程式擋住讓它**無法犯錯**。
- **你不用管 mira。** mira 是錯誤做法的源頭(見 §1),它自己會修。你的目標是讓**任何** CC 都能用,且絕不會發生 mira 的錯。
---
## 1. mira 故障 = 症狀樣本(已定位,不用你修)
mira`/Users/youlinhsieh/Documents/tech_projects/InkStoneCo/polaris/mira/arcrun/*.yaml`)的 workflow 寫 `component: kbdb_get` / `claude_api` / `telegram` 等。這些是 **mira 當初自己錯做的「假零件」**DECISIONS §1 判準:打固定 endpoint 的東西是假零件,該是 recipe)。
本次整修(BACKLOG 步驟3)已把這些假零件**降級成 recipe + 刪掉零件目錄**registry/components 從 33 → 22)。所以 mira 斷了。
**這證明的事**mira 的錯,正是當時 harness 沒擋住的漏洞。零件刪了,但 harness 還缺「**事前告訴 CC 別這樣做 + 事後擋住 CC 這樣做**」的完整機制 → 下一個 CC 還會犯同樣的錯。**這就是你要補的(§3 task 2)。**
---
## 2. 已查證的實況(你不用重查,2026-06-01 實打 prod
### 2.1 降級後的 recipe 鏈路是「活的」✅
實打 `https://cypher.arcrun.dev/recipes`richblack prod)確認以下 recipe 都在 KV
| canonical_id | hash | endpoint | auth_service |
|---|---|---|---|
| `kbdb_get` | rec_4c7dcf9b | `https://kbdb.finally.click{{_path}}` | kbdb |
| `gmail_send` | rec_cd426129 | gmail.googleapis.com/.../send | google_gmail_sa |
| `google_sheets_append` | rec_9fd1b662 | sheets.googleapis.com{{_path}} | google_sheets_sa |
**「對的用法」(`component: kbdb_get` 走解析鏈 step 6 查 `recipe:kbdb_get`)本身是通的。** 不需要重建 recipe。
> 注意:這是 richblack 的 prod KV。**self-host 的新帳號 KV 是空的**,需要 seed 這些 recipe(見 §3 task 1 的 seed 步驟)。
### 2.2 component-loader 解析鏈(`cypher-executor/src/lib/component-loader.ts`
`resolveComponent` 依序嘗試 8 層(行號近似,以實際檔案為準):
```
0. 平台 orchestration 零件(trigger_workflow line ~88
1. 內建零件(純 JS) line ~96
2. 外部 URLhttp(s)://... line ~100
3. cmp_hash → WEBHOOKS KV idx → 邏輯 Worker line ~105
4. rec_hash → RECIPES KV idx → recipe 執行 line ~115
5. 邏輯零件 canonical_id → Service Binding (SVC_*) line ~122
5.5 auth recipe (auth_recipe:{service}) line ~127
6. KV recipe canonical_id → RECIPES KV → fetch 外部 API line ~130 ← kbdb_get 等降級 recipe 命中這層
7. WASM HTTP runner(白名單 WASM_HTTP_RUNNER_IDS line ~134
8. 找不到 → 報錯 line ~142
```
`WASM_HTTP_RUNNER_IDS` 白名單(line ~36)現只剩:`http_request` / `cron` / 4 個 `auth_*` primitive。
`claude_api``kbdb_upsert_block`BACKLOG 標 deferred、源碼暫留)**不在白名單也沒 recipe** → 用到它們的 workflow 會落到 step 8 報錯。這是 mira 自己的問題,不在你範圍。
### 2.3 `acr init --self-hosted` 現況:純手動問答,差很遠
`cli/src/commands/init.ts``initSelfHosted()`line 105-131**只是問 6 個問題後寫進 config**:
要求 CC 自己**事先**部署好 Worker、建好 KV、再手填 Account ID / cypher URL / 兩個 KV namespace ID / WASM bucket / CF token。
BACKLOG 步驟7 要的是「**貼 CF token → 自動建 KV、部署 Worker、自動 workers.dev、寫回 config**」。**這是最大缺口,task 1 的主體。**
config 讀取端已支援 self-hosted`cli/src/lib/config.ts:52` 已能用 `cypher_executor_url`),所以你只要把「自動部署」這段補上,config/執行端不用動。
### 2.4 CI/CD 已是通用掃描式(可重用於 self-host 部署)
`.github/workflows/deploy.yml` 掃所有含 `wrangler.toml` 的目錄自動部署(見 `.claude/rules/05-deploy-convention.md`)。
self-host 自動部署可以參考同一套掃描邏輯(`find . -name wrangler.toml`),對每個目錄跑 `wrangler deploy`
---
## 3. 今天要做的三件事(按序,全在 harness 主線)
> richblack 指示:「全部要做」(含 `acr init --self-hosted`)。
> 三件都做完 = 今天可交付:外部 CC 能 self-host 起來、用對的方式開發、犯錯被擋。
### 🔴 Task 1:完成 `acr init --self-hosted` 一鍵自動化(第一優先)
> ✅ **實作狀態(2026-06-02,已大致完成)**:定稿形態為 **installer 模式**richblack 拍板)——
> 用戶只做:申請 CF 帳號 → 裝 wrangler → 裝 acr → `acr init --self-hosted`(貼 token),其餘自動。
> 已實作(typecheck 過):`cli/src/lib/api-recipe-seeds.ts`10 recipe 種子)、`cf-api.ts`
> `CfAccountClient`(建 KV/R2/查 subdomain/驗 token)、`deploy.ts`(常數 + downloadAndDeploy)、
> `initSelfHosted()` 改寫、`acr update``cypher-executor/scripts/seed-api-recipes.ts`
> **唯一剩餘前置(13.6**:repo 沒有含預編譯 wasm 的 GitHub release.wasm 不 commitrule 05)→
> `downloadAndDeploy()` 目前**誠實回 implemented:false 不假裝部署**mindset §7)。建 KV/R2/seed/config
> 已可跑;release 產製管道補上後部署即自動化。定稿設計見 SDD `self-hosted-init.md`(含 §6 前置依賴)。
> **以下原始子步驟保留供對照**KBDB recipe 採 Supabase 模式進 seedrichblack 2026-06-02)。
**目標**CC 只需提供「CF Account ID + CF API Token」,CLI 自動完成其餘一切。
**SDD**:定稿 `.agents/specs/arcrun/sdk-and-website/self-hosted-init.md`installer 模式,已與 richblack 對齊)。
**子步驟**
1. 改 `cli/src/commands/init.ts``initSelfHosted()`
- 收 CF Account ID + CF API Token(要 KV Edit + Workers Scripts Edit + R2 權限)。
- 用 CF API(或 shell out `wrangler`**自動建 7 個 KV namespace**WEBHOOKS / CREDENTIALS_KV / RECIPES / USERS_KV / SESSIONS_KV / ANALYTICS_KV / EXEC_CONTEXT(清單見 `.claude/rules/01-tech-stack.md` 資料儲存表)+ R2 WASM_BUCKET。
- **自動部署所有 Worker**cypher-executor + registry + 22 個 `.component-builds/*`。可重用 §2.4 的 `wrangler.toml` 掃描。每個 worker 的 `wrangler.toml` 已含 `workers_dev = true`BACKLOG 步驟 P1#2 已做),部署後 workers.dev URL 自動啟用。
- **把 cypher-executor 的 `[vars] WORKER_SUBDOMAIN` 改成 CC 自己的帳號 subdomain**self-host 關鍵,見 P0 #9cypher-executor 走 `arcrun-{name}.{subdomain}.workers.dev` 對內 URL)。
- **seed 降級 recipe + auth recipe 進 RECIPES KV**:新帳號 KV 是空的。把 §2.1 那些 recipekbdb_get/gmail_send/...+ auth recipe seed 寫進去。auth recipe seed 已有 `cypher-executor/scripts/seed-auth-recipes.ts`API recipe 需確認有對應 seed 機制(routes/recipes.ts 是動態 push,可能要寫一份 seed 腳本或用 `acr recipe push`)。
- 寫回 config(現有欄位已足夠)。
2. **runtime secret 不進 CLI 自動化**`ENCRYPTION_KEY` 等由 CC 自己 `wrangler secret put`rule 05 禁止 secret 進自動化流程)。CLI 應在最後**印出提示**告訴 CC 要手動 put 哪些 secret 到哪些 worker。
**驗收(客觀證據,不是口頭宣布 — mindset §7)**
- richblack 用全新 CF 帳號跑 `acr init --self-hosted` → 全程無手動建 KV / 部署。
- 跑完後 `acr push` 一個含 `component: kbdb_get`(或 http_request + 自建 recipe)的 workflow → trigger → HTTP 2xx + execution trace 證明跑通。
### 🔴 Task 2(已重定義 2026-06-01):封鎖自製零件 + recipe 入庫把關
> ✅ **實作狀態(2026-06-02,第一期部分完成)**:(1) 封鎖自製零件 = 靠 GitHub PR 人 merge,無需新做
> (矛盾已釐清)。(2a) 資料外流提醒 = **既有實作已涵蓋**recipe.ts `obtainExposureConsent` + exposure-warning.ts
> 非 TTY 拒絕)。(2b) 打通檢查 = **新增** `probeRecipeEndpoint`recipe.tstypecheck 過):push 後實打
> endpoint,提醒級不硬擋,含 {{模板}} 誠實說明待 run 才知,401/403 標「多半缺 credential 非 bug」。
> 公共庫 relay 檢核(--public= 第一期後。SDD `recipe-push-gatekeeping.md` + tasks.md W2。
> ⚠️ **方向修正(richblack 2026-06-01**:原 Task 2「acr validate 擋假零件名」**作廢**。
> 理由:自製/修改零件的路已封鎖(CC 造不出零件)→「擋假零件」這件事不存在;workflow 引用
> recipe`component: kbdb_get`)是合法且未來唯一的擴充方式,不該被當假零件擋。
> 把關點**從 workflow validate 移到 recipe 入庫(push)那一刻**。
> 已動的 yaml-parser.tsLEGAL_PRIMITIVES / findSuspectComponents**已回退**。
**新目標**
1. **封鎖自製零件**:靠「零件投稿走 GitHub PR + 人 merge」天然閘門(DECISIONS §8)。零件數量少、
絕大多數是 recipe → 不為零件 PR 蓋自動化把關(量少,人工檢查即可;爆量才回頭想自動化)。
2. **recipe 入庫把關**(CC 唯一能擴充的是 recipe,一律用 push,自有庫/公共庫同一套指令):
- **自有庫(self-hosted= 提醒級**:(a) 資料外流提醒——會讓資料/服務對外可見的動作需人類明示同意;
(b) 打通檢查——push 時實打 endpoint 回報 2xx 與否(誠實標原因,不假綠,不硬擋)。
- **公共庫 = 維護者 relay 檢核**(實際打通、真收到成功回傳)— 第一期後。
**SDD**:已寫 design 給 richblack review →
`.agents/specs/component-gatekeeping/recipe-push-gatekeeping.md`+ tasks.md W2 節)。**review 通過才動 code。**
**動到的檔案(待 review**`cli/src/commands/recipe.ts`(push 加提醒 + 打通檢查)、確認 data-exfil hook 涵蓋 recipe push 路徑。
**驗收**
- `acr recipe push` 會產對外 webhook 的東西 → 印資料外流警示 + 要人類同意;非 TTY → 拒絕。
- `acr recipe push` endpoint 可達的 recipe → 回報「✓ HTTP 2xx」。
- `acr recipe push` 缺 credential → 回報「⚠️ 未打通:缺 credential」(誠實),仍允許 push。
- workflow 引用 recipe`component: kbdb_get`**不被任何 validate 步驟當假零件擋**。
### 🔴 Task 3README 重寫成單一路徑 — harness「事前提醒」
**目標**self-hosted 開源後,README 是外界 CC 唯一入口。砍掉「玩法一/二/三」三選一,講清楚單一正確路徑。
**子步驟**(改根 `README.md`):
1. 砍三選一玩法,留**一條路**`acr init --self-hosted` → 寫 workflowprimitive 串 + recipe)→ `acr push` → trigger。
2. 明示心智(呼應 mindset §1):「零件就這固定幾個由我們維護、不接受自製;要打外部 API 就寫 recipe;要編排就寫工作流。」
3. 連到 `.claude/rules/06-mindset.md` / arcrun-mindset Skill,讓 CC 一開始就有正確世界觀。
**驗收**README 讀完,一個沒看過 arcrun 的 CC 知道:能用什麼、不能自製零件、打外部 API 要寫 recipe、怎麼 self-host 起來。
---
## 4. 今天「不要做」的(避免你走偏)
| 項目 | 為何不做 |
|---|---|
| 修 mira | richblack 明示不用管,mira 自己修 |
| 步驟2 `acr recipe test` / relay / credits | DECISIONS §3c 明確劃為服務側、非第一期 |
| 步驟6 搬家拆 matrix | 純 repo 整理,不影響 CC 能否用 |
| 砍 `injectCredentials` 舊路 / `BUILTIN_CREDENTIALS_MAP` | 獨立清理,不擋交付(DECISIONS §3b / BACKLOG「第一期之後」)|
| 新 primitive / Gherkin 真跑 / 入站認證 | richblack 已標「不要現在做」 |
---
## 5. 鐵律提醒(違反會被 hook block)
- 任何 code 變動前先讀對應 SDD + 在回覆開頭宣告(`.claude/rules/00-sdd-protocol.md`)。
- `registry/components/` 下禁止 TScypher-executor TS 禁止 credential/auth/JWT 業務邏輯(`.claude/rules/02-forbidden.md`hook 強制)。
- 每完成一個 task 立刻更新對應 tasks.md / BACKLOG.md 的 `[x]`,不批次。
- 誠實限制(mindset §7):stub / 未完成就標 unimplemented**不假綠**;完成 = 客觀證據(exit code / HTTP status + trace),不是口頭宣布。
@@ -1,87 +0,0 @@
# 2026-05-29 credential 解密失敗(兩個 Worker 的 ENCRYPTION_KEY 漂移)
> **症狀**`acr recipe test kbdb`credential 注入)回 HTTP 500`auth_static_key``credential kbdb_api_key 解密失敗`
> **根因(主)**`arcrun-auth-static-key` Worker 的 `ENCRYPTION_KEY` secret 跟正本(cypher-executor / CLI 用的那把)值不同、格式也不同(44-char base64 vs 64-char hex)。AES-GCM 用錯 key 必然解密失敗。
> **根因(附)**`component-loader.ts``res.json().catch(() => res.text())` 讀 response body → body 被讀兩次 → `Body has already been used`
> **修法**(1) `wrangler secret put ENCRYPTION_KEY` 把 auth-static-key 對齊正本 64-hex(2) 新增 `readBodyOnce()` 先取 text 再 parse JSON。
> **影響**BACKLOG 步驟 2credential 注入鏈路)阻擋;Phase 3 降級假零件成 recipe 的前置。
---
## 症狀
`acr recipe test kbdb` 端到端打不到 2xx。直接 probe `auth_static_key`
```
POST https://auth-static-key.arcrun.dev/ {action:"authenticate", api_key:"ak_…", service:"kbdb"}
→ {"success":false, "error":"credential kbdb_api_key 解密失敗", ...}
```
前置都綠(排除誤判方向):
- `auth_recipe:kbdb` 存在、`primitive=static_key`kv_get 命中 410 bytes
- `kbdb_api_key` credential 存在 KVkv_get 命中 108 bytes 的 `{encrypted, iv}`
- 失敗精準落在「解密」這一步
## 定位(key-fingerprint 診斷,只印 SHA-256 前綴,不印 key/明文)
`aesGcmDecrypt``wasi-shim.ts`)暫加:
```
console.error(`[decrypt] ENCRYPTION_KEY sha256_prefix=${fpHex} keyLen=${len}`)
```
deploy auth-static-key + `wrangler tail` 抓到:
| 來源 | keyLen | sha256 前綴 | 格式 |
|---|---|---|---|
| 加密端(CLI `~/.arcrun/config.yaml``encryption_key` | 64 | `fa84f2ce9027` | hex(→32 bytes)✓ |
| 解密端(`arcrun-auth-static-key``ENCRYPTION_KEY` secret | **44** | **`ff219b123c89`** | base64 ✗ |
**兩個 mismatch 同時存在**:值不同 + 格式不同。`hexToUint8Array` 套在 44-char base64 上會解成垃圾 bytesAES-GCM 必失敗。
漂移源頭:`arcrun/.env` 裡的 `ENCRYPTION_KEY` 就是那把錯的 base64`ff219b123c89`),有人拿它去 `wrangler secret put` 設進 auth-static-key。
## 為什麼正本是 64-hex
`/register`register.ts:42)把 `encryption_key: c.env.ENCRYPTION_KEY` 原樣回給用戶 —— 即 **cypher-executor 的** `ENCRYPTION_KEY`。用戶 config 是 64-hex`fa84f2ce9027`),所以正本 = cypher-executor 那把 64-hex。CLI 加密 credential 也用這把。auth-static-key 必須跟它一致才能解開。
診斷用完即移除(`wasi-shim.ts` 還原,git diff 為空)。
## 附帶 bugBody has already been used
修對 key 後,`/execute` 端到端從 500 變成「Node n1 failed: Body has already been used」。
`component-loader.ts``makeRecipeRunner` / `makeAuthRecipeRunner`
```ts
const data = await res.json().catch(() => res.text()); // ✗ res.json() 失敗時 body 已消費
```
KBDB `/health` 回非 JSON(純文字)→ `res.json()` throw → `.catch(() => res.text())` 第二次讀 body → throw。
修法 — 讀一次:
```ts
async function readBodyOnce(res: Response): Promise<unknown> {
const text = await res.text();
try { return JSON.parse(text); } catch { return text; }
}
```
## 修法步驟
1. `cd .component-builds/auth_static_key && wrangler secret put ENCRYPTION_KEY`,貼正本 64-hex= `~/.arcrun/config.yaml``encryption_key`)。**richblack 手動**rule 05runtime secret 不進 CI、CC 不碰)。
2. `component-loader.ts``readBodyOnce()`,兩處 `res.json().catch(...)` 換掉。`tsc --noEmit` 綠,deploy cypher-executor。
3. 修正源頭文件 `arcrun/.env``ENCRYPTION_KEY` 改成 64-hex(避免下次再設錯)。
## 驗證證據
- 直接 probe auth-static-key**HTTP 200**, `success:true`, 產出 `Authorization: Bearer …`
- 端到端 `/execute`**HTTP 200**, trace 乾淨
- auth 確證:直接 curl KBDB `/blocks` 不帶 token → `401 {"error":"Missing token"}`;經 cypher-executor(注入 token)→ 過 auth,進 KBDB handler 回 ZodError(缺 `content`)。**無 401 = token 被接受**。
## 教訓
- **同一把 key 出現在 ≥2 個 Worker 的 secret = 漂移風險**。auth-static-key / auth_service_account / cypher-executor 都讀 `ENCRYPTION_KEY`,靠人各設一次必漂。長期應有單一發放來源或部署時自動同步。
- **debug 加密問題,先比 key 指紋(SHA-256 前綴),不要碰 key 明文**。一個 fingerprint log 就分辨出「值錯」vs「格式錯」vs「資料壞」。
- **`res.json().catch(() => res.text())` 是反模式** —— body 只能讀一次。永遠先 `res.text()``JSON.parse`
@@ -1,340 +0,0 @@
# arcrun.dev Pages 規格
> **讀者**CC(可直接照做)
> **部署**Cloudflare Pages + Workers
> **語言**:英文為主,中文切換
> **技術棧**Astro(靜態生成)+ Cloudflare Pages + D1(使用統計)
---
## 0. 這個 Pages 的三個角色
1. **門面**:第一次看到 arcrun 的人,30 秒內要懂「這是什麼、對我有什麼用」
2. **轉換漏斗**:工程師 → 試用 lib → 申請 API Key;小白 → 看榮譽牆 → 問 AI 能不能用
3. **社群磁鐵**:榮譽牆讓工程師有動機貢獻 recipe,貢獻越多服務越多,用戶越多
---
## 1. 網站結構(五個頁面)
```
arcrun.dev/
├── / 首頁(門面 + 轉換)
├── /docs 用法文件
├── /integrations 榮譽牆(服務目錄)
├── /api Swagger UI(原始 API
└── /changelog 版本記錄
```
---
## 2. 首頁(/
### 2.1 Hero Section
**英文**
```
Stop fighting OAuth.
One API key. Every service. Works anywhere.
arcrun handles Google, Notion, GitHub, Slack authentication
so your Python / JS code doesn't have to.
[Get API Key — Free] [View on GitHub]
```
**中文切換後**
```
不要再跟 OAuth 搏鬥了。
一個 API Key,接通所有服務,在哪跑都行。
[免費取得 API Key] [查看 GitHub]
```
語言切換按鈕放右上角,用 `?lang=zh` query paramCloudflare Worker 記住偏好存 cookie。
### 2.2 三行說清楚(Why arcrun
```
┌────────────────────┬────────────────────┬────────────────────┐
│ Before │ │ After │
│ │ │ │
│ 40 行 OAuth 程式 │ →→→ │ 1 行 │
│ GCP Console 設定 │ │ arcrun.auth.bind │
│ debug 兩天 │ │ ("google_drive") │
└────────────────────┴────────────────────┴────────────────────┘
```
### 2.3 Code Demo(互動式 tab
三個 tab 切換:Python / JavaScript / HTTP(給 n8n 小白)
**Python tab**
```python
pip install arcrun-auth
from arcrun import auth
# 就這樣,Google Drive 認證完成
drive = auth.bind("google_drive")
resp = drive.get("/files")
```
**JavaScript tab**
```javascript
npm install arcrun-auth
import { auth } from 'arcrun-auth'
const drive = await auth.bind('google_drive')
const resp = await drive.get('/files')
```
**HTTP tab(給 n8n 用戶)**
```
POST https://api.arcrun.dev/v1/auth/bind
Authorization: Bearer YOUR_API_KEY
Content-Type: application/json
{
"service": "google_drive",
"secret": "{{ $env.GOOGLE_SA_JSON }}"
}
```
下方加一行小字:「n8n 用戶:用 HTTP Request 節點貼上這段,不需要安裝任何東西」
### 2.4 數字牆(social proof
```
127 個認證服務 1,247,832 次呼叫 89 位貢獻者
```
這三個數字從 D1 即時讀,每小時更新一次(Cloudflare KV cache)。
### 2.5 CTA
```
[免費取得 API Key]
註冊後立即可用,不需要信用卡
```
---
## 3. 榮譽牆(/integrations)★ 核心頁面
### 3.1 頁面頂部
```
127 個已驗證的認證服務
由社群工程師貢獻並測試,每個 recipe 都有真實使用數據
[搜尋服務...] [全部] [AI] [Google] [社群媒體] [生產力] [台灣]
```
### 3.2 服務卡片
每個 recipe 一張卡:
```
┌──────────────────────────────────────────┐
│ [圖示] Google Drive ★ 官方 │
│ │
│ 認證方式:Service Account │
│ 貢獻者:@richblack ──→ GitHub profile │
│ 驗證日期:2026-03-15 │
│ │
│ 使用次數:██████████ 12,847 次 │
│ │
│ [查看 Recipe] [複製 Python 範例] │
└──────────────────────────────────────────┘
┌──────────────────────────────────────────┐
│ [圖示] OpenRouter │
│ │
│ 認證方式:API Key (Header) │
│ 貢獻者:@some_engineer ──→ GitHub │
│ 驗證日期:2026-04-01 │
│ │
│ 使用次數:██░░░░░░░░ 89 次 │
│ │
│ [查看 Recipe] [複製 Python 範例] │
└──────────────────────────────────────────┘
```
badge 規則:
- `★ 官方`arcrun 團隊維護
- `✓ 社群驗證`:100+ 次使用 + 30 天無錯誤回報
- `🆕 新加入`30 天內合併的 PR
### 3.3 貢獻者排行(頁面底部)
```
Top Contributors
🥇 @some_engineer 23 個 recipe 89,234 次呼叫
🥈 @another_dev 15 個 recipe 45,123 次呼叫
🥉 @third_person 8 個 recipe 12,456 次呼叫
...
[我也要貢獻 →] (連到 CONTRIBUTING.md
```
### 3.4 「我要貢獻」的 CTA
```
找不到你要的服務?
大部分 API Key 類的服務,填一份 YAML 就能加進來。
把 API 文件丟給 AI,五分鐘生成,開 PR 送出。
[查看 Recipe 格式] [開始貢獻]
```
---
## 4. 用法文件(/docs
### 結構
```
快速開始
├── 取得 API Key
├── Python 安裝與第一個範例
├── JavaScript 安裝與第一個範例
└── 直接用 HTTP(n8n / 任何工具)
認證方式
├── API Key 類服務
├── OAuth2 類服務
├── Google Service Account
└── mTLS
進階用法
├── 多帳號(multi-instance
├── 只取 tokenescape hatch
└── 錯誤處理
貢獻 Recipe
├── Recipe YAML 格式說明
├── 讓 AI 幫你寫 Recipe
└── 提交流程
```
### 「讓 AI 幫你寫 Recipe」這一節特別重要
```markdown
## 讓 AI 幫你寫 Recipe
把下面這段丟給 Claude / ChatGPT
再把目標服務的 API 文件一起貼進去:
---
請根據以下 API 文件,
生成一份符合 arcrun recipe schema 的 YAML。
Schema 文件:https://arcrun.dev/docs/recipe-schema
目標服務:[貼上 API 文件]
---
AI 生成後,你只需要:
1. 把 YAML 存成 recipes/community/服務名.yaml
2. 跑 `acr recipe test 服務名.yaml`
3. 開 PR
通常整個過程不超過十分鐘。
```
這一節讓「貢獻門檻」從「工程師才能做」變成「任何人叫 AI 做」。
---
## 5. API 文件(/api
直接嵌入 Swagger UI,連到 `https://api.arcrun.dev/swagger.json`
頁面頂部加一行說明:
```
這是 arcrun 的原始 API。
Python / JS lib 是它的包裝,
任何能發 HTTP request 的工具都能直接用。
```
這一句話讓 n8n 用戶、Make 用戶、甚至 Excel 用戶都知道「我也能用」。
---
## 6. 技術實作
### 6.1 技術選型
**Astro**(靜態生成)是首選,原因:
- 頁面大部分是靜態內容(docs / recipe 卡片),Astro 的 SSG 完美對應
- 動態數字(使用次數、貢獻者排行)用 Astro 的 `client:load` island 局部更新
- 部署到 Cloudflare Pages 零配置
**不用 Next.js**,因為你已在 Cloudflare 生態,Next.js 的 SSR 在 CF Pages 有摩擦。Astro + CF Pages 是更自然的組合。
### 6.2 資料來源
| 資料 | 來源 | 更新頻率 |
|---|---|---|
| Recipe 清單、metadata | GitHub repo `recipes/` 目錄 | CI merge 時觸發 rebuild |
| 使用次數 | Cloudflare D1API call log | 每小時從 D1 聚合 → KV cache |
| 貢獻者排行 | 同上 | 每小時 |
| 總呼叫次數 | 同上 | 每小時 |
### 6.3 多語言
用 Astro 的 i18n routing
- `/` → 英文
- `/zh/` → 中文
語言切換按鈕寫入 cookie `arcrun_lang`CF Worker 在 edge 讀 cookie 做 redirect。
不用 JS framework 的 i18n library,保持輕量。
### 6.4 部署流程
```
GitHub push to main
→ GitHub Actions 跑 astro build
→ 產出 dist/
→ 自動部署到 Cloudflare Pages
→ Pages 掛 arcrun.dev domain
```
recipe YAML 有變動時(PR merge)額外觸發一次 rebuild。
---
## 7. CC 的實作任務
### Phase 1:靜態骨架(3-5 天)
- [ ] Astro 專案初始化,設定 CF Pages 部署
- [ ] 首頁 Hero + Code Demo tab(靜態版,數字先寫死)
- [ ] `/integrations` 靜態版(先手動列 5-10 個服務)
- [ ] `/docs` 基本結構(快速開始 + Python 範例)
- [ ] `/api` 嵌入 Swagger UI
- [ ] 中英切換機制
### Phase 2:動態資料(3-5 天)
- [ ] D1 schema`recipe_calls(recipe_id, count, last_updated)`
- [ ] CF WorkerAPI call 時寫入 D1
- [ ] 每小時聚合 WorkerD1 → KV cache(總數 / per recipe / per contributor
- [ ] 首頁數字牆:從 KV 讀即時數字
- [ ] `/integrations` 卡片:使用次數從 KV 讀,進度條動態顯示
### Phase 3:社群功能(2-3 天)
- [ ] 貢獻者排行從 KV 讀
- [ ] Recipe 頁面:點「查看 Recipe」展開 YAML
- [ ] 點「複製 Python 範例」自動生成對應 code snippet
- [ ] GitHub PR merge webhook → 觸發 Pages rebuild
---
## 8. 一個不能省的細節
榮譽牆的貢獻者欄位**一定要連到他的 GitHub profile**,不是只顯示名字。
工程師貢獻的動機之一是「這個會出現在我的公開作品集」。連到 GitHub 就意味著他的 followers 可能看到他貢獻了 arcrun,這比任何 badge 都有效。
@@ -1,486 +0,0 @@
# arcrun-pyPython Lib 策略分析
> **核心問題**arcrun 的 auth 層要不要獨立成 Python lib?AI 會主動選它嗎?
> **決策前提**:本地為主雲端選配 + 免費引流 SaaS + 三維度分析
---
## 0. TL;DR
做。但要做對——它不是「arcrun SDK」,是 **獨立的 OAuth 地獄解藥**SaaS 帳號只是 optional power-up。
**產品名建議**`arcrun-auth`PyPI 套件名),import 時叫 `authkit``arc`
**定位**`pip install arcrun-auth` → 一行搞定 Google/GitHub/Slack/Notion/... 的認證。本地 keyring 存 secret,零註冊可用。註冊 arcrun 帳號才能多機同步 / 團隊共用 / 獲得平台代管的 OAuth App。
---
## 1. 你的直覺:為什麼這可能是大招
你觀察到的現象:
- **寫個「本地檔案 → Google Drive 備份」這種小腳本**,光認證花 2 天,日後還反覆壞。
- **這個痛點 99% 的 Python 使用者都遇過**。Stack Overflow 上 Google OAuth 的問題累計回答數十萬。
- **AI 寫程式時,這部分它也不會寫**——訓練資料裡的標準範本就是 `InstalledAppFlow.from_client_secrets_file()`,還是要人類先去 GCP Console 建 OAuth Client、下載 JSON、設 redirect URI、加測試用戶。
**如果 arcrun 能把這整套濃縮成 `auth.bind("google_drive")`,它就是開發者工具裡的「自來水」——沒人在乎水源廠長怎麼運作,大家只想打開水龍頭有水喝。**
而且你無意中踩到一個時代轉折點:**AI 在寫程式,AI 不想 debug OAuth**。這個 lib 是給 AI 用的,比給人類用的更重要。
---
## 2. 三維度分析:AI 到底會不會選 arcrun-auth
### 維度一:零註冊摩擦(最重要)
**現實**AI coding assistant 預設會產出訓練資料裡最常見的程式碼。Google Drive 的標準答案就是官方 quickstart——那段 40 行的 `InstalledAppFlow` 樣板碼,加上「去 GCP Console 建 OAuth Client」的一串步驟。
**AI 選擇替代 lib 的條件**(觀察 Copilot / Cursor / Claude Code 的實際行為):
| 條件 | 是否能壓制預設答案 |
|---|---|
| Lib 名稱出現在 prompt 裡 | ✅ 絕對會用 |
| 專案已有 `pyproject.toml` 宣告該 lib | ✅ 會用 |
| 該 lib 是 PyPI 下載量 Top N(訓練資料權重高) | ⚠️ 或許 |
| 使用者口頭說「用 XX 比較好」 | ✅ 會用(單次) |
| 該 lib 明顯更短更容易 | ⚠️ 有機會但不保證 |
**結論**:**零註冊摩擦是生死線**。
如果 `pip install arcrun-auth` 之後必須先去註冊 arcrun 帳號、拿 API token、設環境變數——AI 看到這一步就會放棄,回去寫 Google 官方那套,因為「官方的至少不用註冊第二家廠商」。
**對策**
- 本地模式必須是**零配置可用**的 default path。
- Secret 存在 `~/.arcrun/secrets.db`(加密,用系統 keyring 保護 master key)。
- 註冊 arcrun 帳號是 **opt-in** 的升級,不是必要。
- 第一次用 OAuth 時,**平台提供免費共用的 OAuth App**(下面 §3 詳細說)。
這個設計讓 AI 看到的心智路徑是:
```
目標:備份檔案到 Google Drive
選項 Agoogle-api-python-client(官方,但使用者要先去 GCP 設 OAuth)
選項 Barcrun-authpip install 完直接可用)
```
在這個對比下,AI 會選 B——**特別是它被告知「使用者不想 debug 認證」或類似的 prompt 時**。
### 維度二:跟 stdlib / 主流 lib 的對比
Python Google API 的主流選項:
| Lib | PyPI 週下載(估) | AI 熟悉度 | 認證難度 |
|---|---|---|---|
| `google-api-python-client` | ~30M | 極高 | 高(要自己設 OAuth) |
| `pydrive2` | ~200k | 中 | 高 |
| `gspread`(只 Sheets | ~2M | 高 | 高(OAuth 或 SA |
| **arcrun-auth** | 0 | 0 | **極低** |
**冷啟動難題**:新 lib 要進入 AI 的選擇集合,需要:
1. **量**PyPI 下載 + GitHub stars 進到「被訓練資料收錄」的級別(大概 GitHub 5k+ stars 是門檻)。
2. **品牌**:有代表性部落格文、教學影片、官方 API 文件連結到它。
3. **簡潔**:代碼範例比主流短 5 倍以上,讓使用者「一眼就想用」。
**arcrun 的優勢**
- 你本來就在教 n8n 課程,有現成學員管道可以鋪「這是 n8n 的 Python 版 auth」。
- 「AI 寫程式的 auth lib」是個還沒被佔領的定位詞。`langchain` 做了 LLM 層,但 auth 層還沒有明顯贏家。
- Claude Code 對 lib 選擇特別敏感——它會實際讀 `pyproject.toml` 並尊重已有宣告。
**對策**
- **第一批 adopter 是你的學員**(n8n 課 + AI 自動化課),他們會在實戰中用,累積 GitHub issues 和 blog post。
- **SEO 主打**:「Python Google Drive OAuth 簡化」「AI 自動化 Python 認證」這些長尾關鍵字現在沒有明顯答主。
- **Claude Code 優化**:寫一份 `AGENTS.md``.cursorrules` 範本,示範怎麼在 prompt 裡引導 AI 選 arcrun-auth。
### 維度三:痛點強度(OAuth 地獄避免)
**量化你那兩天 debug**
| 階段 | 時間成本 | 典型錯誤 |
|---|---|---|
| GCP 註冊 + 啟用 API | 15 min | 找不到哪個 API |
| 建 OAuth Client ID | 15 min | Desktop / Web / iOS 選錯 |
| 設 OAuth Consent Screen | 30 min | External / Internal 選錯;scope 加錯 |
| 加測試用戶 | 10 min | 漏加自己的 email |
| 寫 Python flow | 30 min | `run_local_server` vs `run_console` |
| 第一次跑遇到 `redirect_uri_mismatch` | 30-120 min | port 衝突、URI 沒加 |
| Token 過期處理 | 60 min | `creds.expired``refresh_token` 沒保存 |
| Service Account 模式(如果需要) | 120 min | domain-wide delegation 設定 |
| **合計** | **5-8 hrs(順的人)** | **2 天(不順的人,你當時的情況)** |
**arcrun-auth 對應版本**
```python
from arcrun import auth
# 首次執行:自動打開瀏覽器完成 OAuth,結果存本地 keyring
drive = auth.bind("google_drive")
# 直接呼叫 API
drive.post("/upload/drive/v3/files", params={"uploadType": "media"},
data=open("backup.zip", "rb"))
```
**時間成本:首次 2 min,之後 0 min**。
這個壓倒性的體驗差距是產品的核心競爭力。**只要使用者試過一次,就不會再回去寫 `InstalledAppFlow`**——即使 AI 預設會產出官方版本。
---
## 3. 關鍵設計決策
### 3.1 OAuth App 誰擁有?(核心問題)
傳統做法:使用者自己去 GCP Console 註冊自己的 OAuth App,拿 client_id/client_secret。**這就是痛點來源**。
arcrun-auth 要消滅這步,只有兩條路:
**Option A:平台提供共用 OAuth App(推薦 default**
- arcrun 註冊一個 Google OAuth App,命名類似「arcrun Auth Broker」。
- 所有 arcrun-auth 使用者共用這個 App 的 client_id/secret。
- 使用者在 Google 授權頁面看到的是「arcrun Auth Broker 想存取您的 Google Drive」。
- **好處**:使用者零配置,arcrun 品牌曝光。
- **成本**Google 有 OAuth App 的限額(Verified App 才能超過 100 users),需要申請 Google OAuth Verification(要提供隱私政策、網域驗證、可能要付 $75 安全審查)。
**Option B:使用者 BYO OAuth App**
- 企業客戶或注重稽核的人需要這個。
- 在 `~/.arcrun/config.toml` 放自己的 client_id/secret。
**Option Carcrun SaaS 代管**(付費)
- 使用者註冊 arcrun 帳號,平台幫你管 OAuth App、token、團隊共用、audit log。
- 這是付費 tier 的主要價值。
**建議**:A + B + C 三種都支援,默認 A;免費無限制 B;付費享受 C。
### 3.2 Secret 儲存層級(本地為主雲端選配)
```
優先級 1 (default):本地 keyring
- macOS Keychain / Windows Credential Manager / Linux libsecret
- zero config,安全性靠 OS
優先級 2 (opt-in):本地加密檔
- ~/.arcrun/secrets.enc
- master key 走 keyring 或 passphrase
- 給沒有 keyring 的環境(Docker、CI
優先級 3 (opt-in)arcrun 雲端
- 多機同步、團隊共用、audit log
- 需註冊 arcrun 帳號
- 本地 lib 只保存 arcrun API token,實際 service secret 存雲端
```
### 3.3 Secret 初始化流程
**靜態 key 模式(Notion、OpenAI、Stripe...**
```bash
# 選項 A:互動式
$ arcrun setup notion
? Notion Integration Token (hidden): ***
✓ Testing connection... OK
✓ Saved to keyring as notion/default
# 選項 B:環境變數
$ export ARCRUN_NOTION_TOKEN=secret_xxx
$ python script.py # arcrun-auth 自動讀
# 選項 C:程式碼內
notion = auth.bind("notion", secret={"token": os.environ["NOTION_TOKEN"]})
```
**OAuth 模式(Google、GitHub、Slack...**
```python
drive = auth.bind("google_drive")
# 如果是第一次:
# 1. 本地啟動一個臨時 HTTP server (http://localhost:random_port)
# 2. 開瀏覽器到 Google authorize URL
# 3. 使用者點同意
# 4. Google redirect 到 localhostlib 接到 code
# 5. 換 token,存 keyring
# 6. 回傳可用的 client
```
這個流程和 `InstalledAppFlow.run_local_server()` 本質上一樣——但差別是:
- **Client ID 不用使用者自己去 GCP Console 註冊**(由 arcrun 平台提供)。
- **Scope 由 recipe 宣告**(不用使用者自己查文件)。
- **Token 儲存自動化**(不是散落在 `token.json`)。
### 3.4 Recipe 來源
Python lib 和 Cloudflare Worker 版本**共用同一份 recipe YAML**。
```
arcrun-recipes/ # GitHub repo,公開
├── recipes/
│ ├── official/
│ │ ├── google_drive.yaml
│ │ ├── notion.yaml
│ │ └── ...
│ └── community/
│ └── ...
```
Python lib 啟動時檢查本地 `~/.arcrun/recipes/` 快取,過期就從 GitHub 或 arcrun 平台 API 拉最新。
**這是關鍵架構優勢**recipe 寫一次,Web 和 CLI 和 Python lib 全部受益。社群貢獻一份 Notion recipe,所有 runtime 自動支援。
---
## 4. API 設計(Python 版)
### 4.1 最簡路徑
```python
from arcrun import auth
# 取得認證好的 HTTP client(基於 httpx
client = auth.bind("google_drive")
# 相對 base_url 的路徑
resp = client.get("/files", params={"q": "name = 'backup.zip'"})
files = resp.json()["files"]
# 上傳
client.post("/upload/drive/v3/files",
params={"uploadType": "multipart"},
files={"file": ("backup.zip", open("backup.zip", "rb"))})
```
### 4.2 進階:非同步
```python
from arcrun import auth
async with auth.bind_async("google_drive") as client:
resp = await client.get("/files")
```
### 4.3 進階:多 instance
```python
# 同一個服務,多個帳號
personal = auth.bind("google_drive", instance="personal")
work = auth.bind("google_drive", instance="work")
```
### 4.4 進階:直接取 token(給不想透過 wrapper 的情況)
```python
# 取 raw access token,自己丟進任何 lib
token = auth.get_token("google_drive")
# 丟給 googleapiclient
from googleapiclient.discovery import build
from google.oauth2.credentials import Credentials
creds = Credentials(token=token.access_token)
service = build("drive", "v3", credentials=creds)
```
這個 escape hatch 很重要——不強制使用者放棄他熟悉的官方 lib,只是把**認證這一層**剝離出來。這是你想要的「避免麻煩直接用 arcrun 的 auth 功能」的精確實作。
### 4.5 服務發現
```python
# CLI
$ arcrun list
google_drive Google Drive OAuth2
notion Notion API Key
github GitHub OAuth2
openai OpenAI API Key
...
# Python
from arcrun import auth
auth.list_services() # 回傳 dict
```
---
## 5. 商業模式:免費引流 → SaaS 付費
### 5.1 免費永久可用(本地模式)
| 功能 | 免費 | 付費 |
|---|---|---|
| `pip install arcrun-auth` | ✅ | ✅ |
| 本地 keyring 儲存 secret | ✅ | ✅ |
| 所有 recipe 可用 | ✅ | ✅ |
| 平台代管 OAuth App(免自己註冊) | ✅ | ✅ |
| 單機使用 | ✅ | ✅ |
### 5.2 付費 tier 提供的
| 功能 | 免費 | Pro ($9/mo) | Team ($29/user/mo) |
|---|---|---|---|
| 多機同步 secret | ❌ | ✅ | ✅ |
| 團隊共用 credential | ❌ | ❌ | ✅ |
| Audit log(誰在何時用了什麼 secret) | ❌ | ✅ | ✅ |
| Secret rotation 提醒 | ❌ | ✅ | ✅ |
| 企業 OAuth App BYO | ❌ | ✅ | ✅ |
| SSO / SCIM | ❌ | ❌ | ✅ |
| arcrun SaaS 整合(workflow runtime | ❌ | ⚠️ 受限 | ✅ |
| Priority 社群 recipe 審核 | ❌ | ✅ | ✅ |
**商業邏輯**
- 免費 tier 足夠「個人開發者 + 側邊小專案」的體驗完整。
- 付費 tier 解決「我的小工具變公司在用」的延伸需求。
- arcrun SaaS workflow 客戶自動享受所有 Python lib 特權。
### 5.3 為什麼這策略會 work
參考模型:
- **Sentry**error tracking,開源 SDKhosted service 付費。SDK 下載數 = 付費轉換引流。
- **PostHog**product analytics,同樣策略。
- **Supabase**auth + db,開源 self-hostedhosted 服務付費。
共同特徵:
1. SDK 免費下載、單機/本地能完整 work。
2. 從本地升級到 hosted 是「多人/多機/審計」的自然需求。
3. SDK 本身就是最強的 marketing(開發者口耳相傳 + 訓練資料擴散)。
---
## 6. 為什麼 AI 會主動選 arcrun-auth(機率分析)
讓我給你一個分層的答案。
### 6.1 會主動選的場景(高機率)
| 場景 | 原因 |
|---|---|
| 使用者在 prompt 說「用最簡單的方式認證」 | AI 權衡 UXarcrun-auth 明顯勝 |
| 專案已有 `arcrun-auth``pyproject.toml` | AI 尊重現有依賴 |
| 使用者貼了 arcrun-auth 文件範例 | AI 會模仿 |
| 使用者說「跳過 OAuth 設定步驟」 | arcrun-auth 是唯一答案 |
### 6.2 不會主動選的場景(預設行為)
| 場景 | 原因 |
|---|---|
| 使用者開新專案、無任何線索 | AI 走訓練資料中最頻繁的 `google-api-python-client` |
| 任務是「企業級、有審計需求」 | AI 偏好主流官方 lib |
| 任務提到特定 lib 名稱 | AI 會遵守 |
### 6.3 轉化策略
要讓 AI 的「預設路徑」變成 arcrun-auth,需要:
1. **種子內容**:寫 20+ 篇「用 arcrun-auth 做 X」的 blog post,讓下一輪模型訓練看到。
2. **官方文件 SEO**:搶「python google drive oauth 簡化」等長尾關鍵字。
3. **Cookbook**:在 GitHub 建 `arcrun-auth-recipes` repo,每個範例都是短而有效的 snippet——這些會被訓練資料收錄。
4. **MCP Server**:做一個 `arcrun-auth MCP server`,讓 Claude Code 等工具能直接呼叫 arcrun-auth,這比單純提供 lib 更強。
5. **教學整合**:你的 n8n 課程直接示範「從 n8n 到 arcrun-auth 的 Python 版本」。
---
## 7. 實作範圍(MVP
### Phase 1:核心 lib2-3 週)
- [ ] `arcrun-auth` PyPI 骨架(pyproject.toml + src layout
- [ ] Recipe loader(從 GitHub 或平台 API 拉 YAML
- [ ] `auth.bind(service_id, instance?)` → httpx Client
- [ ] Static key primitiveNotion / OpenAI / Stripe 當試金石)
- [ ] Keyring 整合 + 本地加密檔 fallback
- [ ] CLI`arcrun setup <service>`, `arcrun list`, `arcrun test`
### Phase 2OAuth22 週)
- [ ] OAuth2 primitiveauthorization_code + PKCE
- [ ] 本地 callback server(類似 `InstalledAppFlow.run_local_server`
- [ ] 共用平台 OAuth App 的 proxy 機制
- Lib 呼叫 `https://auth.arcrun.com/oauth/redirect`
- 平台把 code 交換後回傳 token
- 或者直接把平台 client_id 硬編在 recipe 裡(更簡單但要處理配額)
- [ ] Token refresh 自動化
- [ ] RecipeGoogle Drive / Gmail / GitHub / Slack
### Phase 3Service Account1-2 週)
- [ ] Google Service AccountJWT signing
- [ ] AWS SigV4
- [ ] Recipe 繼承(`extends: _google_base`
### Phase 4:雲端同步(2 週)
- [ ] `arcrun login` → 綁定雲端帳號
- [ ] Secret sync 協議(本地加密後上傳,平台只存密文)
- [ ] 多機同步
- [ ] Audit log
### Phase 5AI 生態整合(1-2 週)
- [ ] MCP server(讓 Claude Code 能直接用)
- [ ] VS Code Extension(一鍵設定 credential
- [ ] `AGENTS.md` 範本(引導 AI 選 arcrun-auth
---
## 8. 風險與坑
### 8.1 Google OAuth Verification
**問題**:共用 OAuth App 要申請 Google Verification,否則會有「未驗證 App」警告 + 100 user 上限。
**對策**
- MVP 階段接受警告頁面(使用者自己點「進階 → 前往」)。
- 到 user 量接近 100 時申請 Verification。
- 企業客戶走 BYO OAuth App 路徑,不受影響。
- 若平台 OAuth App 卡關,有 fallbacklib 自動引導使用者建自己的 OAuth App(提供 CLI wizard)。
### 8.2 其他服務的 OAuth App 政策
- **GitHub**:免費建 OAuth App,無上限。✅
- **Slack**:免費建,但安裝到使用者 workspace 需管理員同意。⚠️
- **Microsoft / Azure**:相對嚴格,需 tenant admin consent。⚠️
- **Notion**Internal Integration 可以完全走 API key,免 OAuth。✅(最簡單)
### 8.3 keyring 在 Linux server / Docker 的問題
Linux server 沒 GUI keyring daemon。對策:
- Fallback 到加密檔案(用 env var 或 CLI 互動提供 master key)。
- Docker 場景有 `docker secret`、Kubernetes Secretlib 支援直接讀這些來源。
### 8.4 競品
目前沒有完全對標的產品,但相鄰玩家:
- **[keyring](https://pypi.org/project/keyring/)**:只做儲存,不做認證流程。我們用它當底層。
- **[httpx-auth](https://pypi.org/project/httpx-auth/)**:只做認證,不做 secret 管理,也沒有 recipe。
- **[authlib](https://pypi.org/project/authlib/)**OAuth 實作 lib,低階,還是要自己組。
- **各家 SDKgoogle-auth, slack-sdk**:綁特定家,不 unify。
**arcrun-auth 的差異化定位**
> **Unified credential broker for AI-era Python apps**
> 一個 lib 搞定所有服務、所有認證機制、所有 secret 儲存後端。
---
## 9. 最後的判斷
### 9.1 這個 lib 該不該做?
**該做**。原因:
1. 你描述的痛點是真的,而且規模巨大(Python + Google API 下載量是千萬級)。
2. 技術可行,也跟既有 arcrun 架構共用 recipe,邊際成本低。
3. 對 arcrun SaaS 是完美引流——免費 lib 的使用者是精準的付費轉換潛在客戶。
4. 時間窗口正確:AI 寫程式時代剛開始,這個定位還沒被佔領。
### 9.2 跟主 SaaS 的優先順序
**建議****主 SaaS 的 credential 系統先做(前一份規劃),arcrun-auth 當後續 Phase**。
原因:
- Cloudflare Worker 版的 primitives + recipes 是基礎建設,Python lib 是其 consumer。
- 先做 Python lib 會逼你在 recipe schema 上做二次修改,不划算。
- 主 SaaS 的 recipe 累積到 20-30 個服務後,開放 Python lib 體驗最好。
時程建議:
- **Month 1-2**:主 SaaS 的 4 個 primitive + 15 個 recipe(前一份規劃)。
- **Month 3-4**arcrun-auth Phase 1-2static key + OAuth2),私人 alpha。
- **Month 5**:公開 release,寫部落格、SEO、社群推廣。
- **Month 6+**:雲端同步、MCP、AI 生態整合。
### 9.3 一句話總結
> **arcrun-auth 不是「arcrun 的 Python 綁定」,是「OAuth 地獄的解藥」**。SaaS 是延伸。這個敘事才能在 AI 寫程式的時代站住腳。
@@ -1,55 +0,0 @@
# Arcrun 推廣策略修正
20260418 by Leo
## Arcrun 的最近幾次變化
- Arcrun 原是 Matrix 的原子化純雲端 CF 程式框架,有 MCP 讓 AI 使用
- 轉以 WASM + WASI + TinyGo 成為未來具有雲端、地端、邊緣端的執行能力,可以用來做到無人機等終端。
- 再解耦成獨立的 Open Source 專案,脫離 KBDB 用 YAML 即可,允許整個 Fork,但推廣 SaaS 模式
- 今天的變化是發現成為 Lib 和 n8n 社群節點的用法。說明如下。
## 推廣方式 1:寫成 Lib
參考文件:docs/user_requirements/arcrun/ADR-lib-and-landingPage/arcrun-py-strategy-analysis.md
Leo 教 n8n 時常舉例我叫 AI 幫我寫個簡單的程式把 server 的檔案備份到 Google Drive 後刪除,光是 OAuth 用 Service Account 就花了 2 天測試,後來還發生好幾次出錯重修。
網路設定對 vibe coder 太複雜了,看到 AI 一次次修改一次次出錯,知道一週後才穩定,而 n8n 只需要申請 credential 填入,10 分鐘搞定,差距太大了。
問題是,n8n 實在太慢了,全圖像界面也難以跟程式碼整合,vibe coder 就算知道了也無法整合,他會繼續用 python 就是無法忍受 n8n 的慢速、沉重,及對 AI 的不友善。
Arcrun 用幾個基礎 Auth 零件取代 n8n 幾百個不同的 credentials 設定,arcrun 完全可以跟程式碼整合,只是目前沒有整合工具,也就是說,工程師不想面對一個認證花好幾天,他只要用 Arcrun 打 API 就幾行解決了。
用這個策略,讓具有程式知識的人會:
- 對 Arcrun 有好感,在 GitHub 按星
- 成為 Arcrun 用戶,增加會員 base
- 寫文分享,讓 Arcrun 被傳出去
反之,小白初學 vibe coding,看到老工程師說 2 天變成 10 分鐘,百行程式碼變成 2 行,也會被影響。
所以,把 API 包裝成 Lib 或 SDK 是個 Get Member 的好方法,不過一定要有 Email。
## 推廣方式 2: 讓 n8n 來 call
身為 n8n 講師,我非常清楚 n8n 有個大缺陷(或說是安全設計),就是它只能呼叫在工作流實例化前就寫好的程式,無法注入新的。
所以,雖然它可以安裝一個實作 langchain AI Agent 的 AI Agent 節點,但 AI 卻不可能在遇到問題時立刻寫個程式運行解決問題,甚至使用它內建的 task runners 或使用外部的 Python runtime 也無法。
但只要使用 http request 節點呼叫外部的 Arcrun tool,它就可以輸出三元組寫一個程式解決它現在遇到的問題,並且運算完後回傳,只需要取得一個 API Key。
## 可能的效應
工程師是意見領袖,他們會去按星,幫忙教育市場,而小白需要被教育,但他們是較大的社群,這些星數和聲量會在未來變成 AI 訓練素材,甚至推薦用戶使用最 AI Friendly 的 Arcrun。
而實際上 Arcrun 的設計讓 AI 更輕鬆。
## 要做什麼
docs/user_requirements/arcrun/ADR-lib-and-landingPage/arcrun-pages-spec.md 這是 claude.ai 寫的規劃,因為它看不到 codebase,所以你要斟酌如何開發。
另外,當前的 API Key 雖然很容易取得,卻有個問題,用戶無法管理,所以頁面應該要有取得 API Key 的界面,可以 Google, GitHub 等 OAuth 或 Email + Password 登入,用來管理它的 API Key 的 CRUD。
或許是可以外接一個 SMTP 服務來確認他的 Email 真實,SaaS 服務還是有成本,雖然成本不高。
另外,既然是 OpenSource,成本雖然不高,似乎可以銜接 Donate 服務?
@@ -1,56 +0,0 @@
// 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 />);
@@ -1,92 +0,0 @@
// 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 });
@@ -1,86 +0,0 @@
// 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 });
File diff suppressed because it is too large Load Diff
@@ -1,128 +0,0 @@
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;
@@ -1,90 +0,0 @@
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;
@@ -1,126 +0,0 @@
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;
@@ -1,168 +0,0 @@
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;
@@ -1,255 +0,0 @@
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;
File diff suppressed because it is too large Load Diff
-761
View File
@@ -1,761 +0,0 @@
# arcrun Credential System 設計規格
20260418
> **讀者**Claude CodeCC),負責實作
> **作者**richblack(架構決策)
> **版本**v1.0
> **狀態**:Draft — 等 CC 確認技術可行性後開工
---
## 0. TL;DR(給 CC 的三句話版)
1. **不要**為每個服務寫一個 credential 零件,n8n 是錯的。
2. 做**四個 TinyGo/WASM 零件**primitives),每個服務只需要一份 **YAML recipe** + 用戶自己的 **secret**
3. Recipe 存 arcrun 平台 KV(公共),secret 存 tenant KV(私有),兩者在 runtime 由 `AuthBroker` 組裝成可用的 HTTP client。
---
## 1. 設計目標與反目標
### 目標
- **新增一個服務的成本 = 寫一份 YAML**,不需要 rebuild、不需要改 code。
- **AI agent 理解成本 ≈ 0**:recipe 就是呼叫該服務的完整說明書。
- **人類設定成本 < 10 分鐘**:即使是對 OAuth 不熟的使用者,UI 只問「你的 API Key 是什麼」這類 secret 層級問題。
- **Secret 隔離**:每個 tenant 的 secret 絕對不互相可見,arcrun 平台本身也無法明文讀取(用 Cloudflare Secrets Store 或加密儲存)。
### 反目標(明確不做的事)
- ❌ 不做 n8n 那種「每個服務一個 credential type」的視覺化面板。
- ❌ 不支援 OAuth1(2026 年還在用的服務極少,真遇到再加)。
- ❌ 不做 credential sharing 的複雜 ACL(全 tenant scope 即可,未來再擴充)。
- ❌ 不在 arcrun 內部明文持久化任何長期 secret(只有加密過的密文或 Secrets Store reference)。
---
## 2. 核心架構:三層模型
```
┌─────────────────────────────────────────────────────────┐
│ Layer 3: Service Recipe (YAML) │
│ arcrun 平台共享,describe "如何呼叫這個服務" │
│ 存在 Workers KV: arcrun-recipes │
│ 例:recipe/notion.yaml, recipe/google_calendar.yaml │
└─────────────────────────────────────────────────────────┘
↓ 引用
┌─────────────────────────────────────────────────────────┐
│ Layer 2: Auth Primitive (TinyGo → WASM) │
│ 四個通用認證零件,實作注入邏輯與 token 交換 │
│ 1. static_key 2. oauth2 │
│ 3. service_account 4. mtls │
└─────────────────────────────────────────────────────────┘
↑ 使用
┌─────────────────────────────────────────────────────────┐
│ Layer 1: Tenant Secret (KV + Secrets Store) │
│ 每個 tenant 自己的 KV namespace │
│ 存 encrypted secret 或 Secrets Store reference │
│ 例:secret/{tenant_id}/notion-prod │
└─────────────────────────────────────────────────────────┘
```
### 為什麼這樣切?
| 切分維度 | Recipe | Primitive | Secret |
|---|---|---|---|
| **誰擁有** | arcrun 平台 | arcrun 平台 | tenant 自己 |
| **變化頻率** | 中(新服務時) | 低(認證機制穩定) | 高(rotate、revoke |
| **敏感度** | 公開 | 公開 | 最高機密 |
| **儲存位置** | 平台 KV`arcrun-recipes` | WASM binary | tenant KV + Secrets Store |
| **可否社群貢獻** | ✅ PR | ⚠️ 核心團隊 | ❌ 永遠不 |
---
## 3. 四個 Primitive 詳細規格
### 3.1 `static_key`
**適用**API Key、Bearer Token、Basic Auth、任何「一組 secret 不會自動過期」的認證。
**涵蓋 n8n 的**API Key、Basic Auth、Header Auth、Query Auth、Custom Auth、Digest Auth~80% 服務)。
**Recipe 欄位**
```yaml
primitive: static_key
inject:
# 四個注入位置,可以同時用多個
header: # HTTP headers
<key>: <value template>
query: # URL query string
<key>: <value template>
body: # request bodyJSON 欄位)
<key>: <value template>
basic_auth: # HTTP Basic Auth(會自動 base64 編碼)
username: <value template>
password: <value template>
```
**Value template 語法**`{{secret.xxx}}` 取 secret 欄位,`{{const.yyy}}` 取 recipe 內定義的常數。
**Secret schema**tenant 存 JSON,欄位由 recipe 的 `required_secrets` 宣告。
**範例(Notion**
```yaml
# arcrun-recipes KV: recipe/notion
service: notion
version: 1
primitive: static_key
base_url: https://api.notion.com/v1
required_secrets:
- key: token
label: "Internal Integration Token"
help_url: https://www.notion.so/my-integrations
inject:
header:
Authorization: "Bearer {{secret.token}}"
Notion-Version: "2022-06-28"
test:
method: GET
path: /users/me
expect_status: 200
```
**Secret 範例**
```json
// tenant KV: secret/tenant_123/notion-prod
{
"token": "secret_abc123..."
}
```
---
### 3.2 `oauth2`
**適用**:需要人類首次授權、之後用 refresh token 續命的場景。
**Grant types 支援**
- `authorization_code`(最常見:GitHub、Slack、Google 用戶授權)
- `client_credentials`(機器對機器)
- `pkce`SPA、行動應用)
- ❌ 不支援:password grant2026 已被多數 OAuth 提供者棄用)、implicit(已棄用)
**Recipe 欄位**
```yaml
primitive: oauth2
grant: authorization_code # or client_credentials, pkce
base_url: <service API base>
oauth:
authorize_url: <IdP authorize endpoint>
token_url: <IdP token endpoint>
scopes:
- <default scope 1>
- <default scope 2>
client_auth: header # or body
# 是否使用 refresh token
refresh: true
# PKCE 時額外參數
pkce_method: S256 # only for grant: pkce
required_secrets:
- key: client_id
label: "Client ID"
- key: client_secret
label: "Client Secret"
secret: true
inject:
header:
Authorization: "Bearer {{runtime.access_token}}"
```
**Runtime 欄位**primitive 自動維護,存在 tenant KV 的 `oauth_state/{secret_id}` key):
- `access_token`
- `refresh_token`
- `expires_at`
**首次授權流程**(人類要做的部分):
1. arcrun UI 呼叫 `AuthBroker.startAuth(recipe_id, tenant_id)` 回傳 authorize URL。
2. 使用者瀏覽器跳轉到 IdP,同意授權。
3. IdP redirect 回 arcrun callback endpoint(固定一個 URL,無論哪個服務)。
4. `AuthBroker` 用 authorization code 換 token,寫入 tenant KV。
**之後 agent 呼叫時完全自動**primitive 檢查 `expires_at`,過期自動用 refresh token 續,失敗再觸發重新授權通知。
---
### 3.3 `service_account`
**適用**Google Service Account、AWS IAM Roleassume role)、任何需要「私鑰簽 JWT 換短期 token」的機器身份。
**這個就是讓你 debug 兩天那個爆炸點。** 我們用 primitive 把地雷全部包起來。
**Recipe 欄位**
```yaml
primitive: service_account
kind: google_jwt # or aws_sigv4, generic_jwt
base_url: <service API base>
token_exchange:
# Google 的 JWT → OAuth access token 流程
endpoint: https://oauth2.googleapis.com/token
audience: https://oauth2.googleapis.com/token
scopes:
- https://www.googleapis.com/auth/calendar
# JWT claims
issuer_from_secret: client_email
subject_from_secret: client_email # optional, for domain-wide delegation 改成其他 user
ttl_seconds: 3600
required_secrets:
- key: service_account_json
label: "Service Account JSON"
type: json_blob # 特別型別,UI 可以接受貼整個 JSON
help: "到 GCP Console → IAM → Service Accounts → Keys → Add Key (JSON) 下載整份 JSON 貼上"
inject:
header:
Authorization: "Bearer {{runtime.access_token}}"
```
**為什麼不是每個服務一個 recipe?**
- Google Calendar、Gmail、Drive、Sheets 全部可以共用同一個 `service_account` primitive。
- 差別只在 `scopes``base_url`
- Recipe 本身可以 import 共通片段(見 §5 recipe 繼承)。
**AWS SigV4kind: aws_sigv4**:這是特例,不是 JWT-based,但概念一樣——用 access_key_id + secret_access_key 在每次 request 上簽章。Primitive 內建處理,recipe 只要宣告 region 和 service name。
---
### 3.4 `mtls`
**適用**mTLS / client certificate。銀行 API、企業內部服務、醫療系統。
**Recipe 欄位**
```yaml
primitive: mtls
base_url: <service API base>
required_secrets:
- key: client_cert
label: "Client Certificate (PEM)"
type: pem_cert
- key: client_key
label: "Client Private Key (PEM)"
type: pem_key
secret: true
- key: ca_cert
label: "CA Certificate (PEM) — optional"
type: pem_cert
optional: true
# mtls 通常不需要額外 inject,憑證在 TLS 層
inject: {}
```
**實作注意**Cloudflare Workers 有原生 mTLS 支援(`mTLSCertificate` binding),primitive 只需要把 secret 轉成 Cloudflare mTLS binding 即可。
---
## 4. Recipe YAML Schema(完整版)
```yaml
# 必填
service: string # 唯一識別,snake_casee.g. "notion", "google_calendar"
version: integer # recipe schema versionbreaking change 要升版
primitive: enum # static_key | oauth2 | service_account | mtls
base_url: string # service API base URL
# primitive 相關(依 primitive 不同)
inject: object # 如何把 secret 注入 HTTP request
oauth: object # 僅 oauth2 primitive
token_exchange: object # 僅 service_account primitive
# Secret 宣告(讓 UI 知道要問什麼)
required_secrets:
- key: string # secret 欄位名
label: string # UI 顯示
secret: boolean # 是否遮蔽顯示(default: true
type: enum # text | json_blob | pem_cert | pem_key | url
optional: boolean # default: false
help: string # 給使用者的提示
help_url: string # 導向服務文件
# 測試(驗證 credential 是否有效)
test:
method: GET | POST
path: string # 相對 base_url
expect_status: integer
expect_json: object # 選填,JSON path assertion
# Metadata
display_name: string # UI 顯示名
description: string
icon_url: string
docs_url: string
tags:
- communication
- crm
- ai
maintainers:
- github: username
# 可選:共通片段繼承
extends: string # recipe name,繼承其 schema 後覆寫
```
---
## 5. Recipe 繼承(reduce 重複)
Google 家族的 API 長得很像,重複寫 15 次太蠢。支援 `extends`
```yaml
# recipe/_google_base.yaml(底線開頭 = 抽象 recipe,不能直接用)
service: _google_base
version: 1
primitive: service_account
token_exchange:
endpoint: https://oauth2.googleapis.com/token
audience: https://oauth2.googleapis.com/token
ttl_seconds: 3600
required_secrets:
- key: service_account_json
type: json_blob
inject:
header:
Authorization: "Bearer {{runtime.access_token}}"
```
```yaml
# recipe/google_calendar.yaml
extends: _google_base
service: google_calendar
version: 1
base_url: https://www.googleapis.com/calendar/v3
token_exchange:
scopes:
- https://www.googleapis.com/auth/calendar
test:
method: GET
path: /users/me/calendarList
expect_status: 200
```
繼承規則:scalar 覆寫,object 深度合併,array 預設覆寫(可用 `!append` 標記 append)。
---
## 6. TinyGo WASM Primitive 實作介面
### 6.1 統一介面(四個 primitive 都實作這個)
```go
// primitive/interface.go
package primitive
type AuthRequest struct {
Method string
URL string
Headers map[string]string
Body []byte
}
type AuthContext struct {
Recipe Recipe // parsed YAML
Secret map[string]any // decrypted secret
Runtime RuntimeState // oauth token cache 等
Now int64 // for testing
}
type Primitive interface {
// 在 HTTP request 上注入認證資訊
Authenticate(req *AuthRequest, ctx *AuthContext) error
// 檢查是否需要 refreshoauth2 / service_account 用)
NeedsRefresh(ctx *AuthContext) bool
// 執行 refresh / token exchange,回傳新的 RuntimeState
Refresh(ctx *AuthContext) (RuntimeState, error)
// 驗證 credential 是否有效(執行 recipe.test
Test(ctx *AuthContext) error
}
```
### 6.2 編譯與部署
```bash
# 四個 primitive 各自編譯成獨立 WASM
tinygo build -o dist/static_key.wasm -target=wasi ./primitive/static_key
tinygo build -o dist/oauth2.wasm -target=wasi ./primitive/oauth2
tinygo build -o dist/service_account.wasm -target=wasi ./primitive/service_account
tinygo build -o dist/mtls.wasm -target=wasi ./primitive/mtls
# 部署時放到 Cloudflare Workers 的 Assets 或直接內嵌
```
### 6.3 Runtime 載入(Worker 端)
```typescript
// worker/src/auth-broker.ts
import staticKeyWasm from "../dist/static_key.wasm"
import oauth2Wasm from "../dist/oauth2.wasm"
// ...
const primitives = {
static_key: await instantiate(staticKeyWasm),
oauth2: await instantiate(oauth2Wasm),
service_account: await instantiate(serviceAccountWasm),
mtls: await instantiate(mtlsWasm),
}
```
> **為什麼 WASM 而不是直接 TS**
> 1. 跨 runtime 可攜性(未來若 arcrun 要跑在 Fly.io、local、或客戶自建環境,同一個 primitive 能用)。
> 2. 配合 u6u/arcrun 既定的 WASM 架構方向,不破壞統一性。
> 3. 沙箱化:primitive 只能透過明確的 host function 存取外部世界(網路、KV),降低惡意 recipe 攻擊面。
---
## 7. AuthBroker API(給 arcrun 其他部分調用)
```typescript
interface AuthBroker {
// Agent 執行時用的主要 API
bind(serviceId: string, secretRef: string, tenantId: string): Promise<AuthenticatedClient>
// 首次授權(僅 oauth2 用)
startAuth(serviceId: string, tenantId: string): Promise<{ authorizeUrl: string, state: string }>
completeAuth(state: string, code: string): Promise<{ secretRef: string }>
// 測試 credential
test(serviceId: string, secretRef: string, tenantId: string): Promise<TestResult>
// 管理
listRecipes(): Promise<Recipe[]>
getRecipe(serviceId: string): Promise<Recipe>
}
interface AuthenticatedClient {
fetch(path: string, init?: RequestInit): Promise<Response>
}
```
**使用範例(agent 端)**
```typescript
const notion = await authBroker.bind("notion", "notion-prod", ctx.tenantId)
const res = await notion.fetch("/databases/abc/query", {
method: "POST",
body: JSON.stringify({ filter: {...} })
})
```
**Agent 完全不需要知道是 API Key 還是 OAuth**——`authBroker.bind()` 回傳的 client 已經注入好認證,fetch 路徑用相對 base_url 的路徑即可。
---
## 8. Storage Layout
### 8.1 Recipe 儲存(arcrun 平台共享)
**Cloudflare KV namespace**`arcrun-recipes`
```
key: recipe/{service_id}
value: <recipe YAML 的 JSON 化版本>
key: recipe-list
value: [{ service_id, display_name, icon_url, tags }, ...] # 加速 UI 列表
```
**更新流程**
1. Recipe YAML 存在 arcrun 主 repo 的 `recipes/` 目錄下(version control + PR review)。
2. CI 跑 schema validator,通過後上傳到 KV。
3. UI 的 recipe 列表 5 分鐘 cache。
### 8.2 Secret 儲存(tenant 私有)
**雙層策略**
- **短期、低敏感** → tenant KV,用 AES-256-GCM 加密,key 從 Cloudflare Secrets Store 拿。
- **高敏感(如 service account JSON、private key** → 直接存 Cloudflare Secrets Storetenant KV 只存 reference。
```
# tenant KV namespace: arcrun-tenant-{tenant_id}
key: secret/{service_id}/{instance_name}
value: {
"recipe_version": 1,
"storage_mode": "kv_encrypted" | "secrets_store_ref",
"data": <encrypted blob> | { "ref": "secrets-store-id" },
"created_at": "...",
"last_verified_at": "..."
}
# oauth2 runtime stateprimitive 自動管理)
key: oauth_state/{service_id}/{instance_name}
value: {
"access_token": "...", # encrypted
"refresh_token": "...", # encrypted
"expires_at": 1234567890
}
```
**secretRef 格式**`{service_id}/{instance_name}`,例如 `notion/prod``google_calendar/workspace-a`
一個 tenant 可以同一個服務存多個 instance(多帳號場景)。
### 8.3 KBDB 整合(可選,但建議)
**按照 KBDB 架構,recipe metadata 可以用 Block + Template 表達**(不是 credential 本體,只是 metadata):
建立一個 `service_recipe` Template
```json
{
"name": "service_recipe",
"display_name": "服務 Recipe Metadata",
"schema": {
"fields": [
{"key": "service_id", "type": "text", "required": true, "description": "服務識別"},
{"key": "primitive", "type": "text", "required": true, "description": "使用的 primitive"},
{"key": "version", "type": "number", "required": true, "description": "Recipe 版本"},
{"key": "display_name", "type": "text", "required": false, "description": "顯示名稱"},
{"key": "docs_url", "type": "text", "required": false, "description": "文件 URL"},
{"key": "kv_key", "type": "text", "required": true, "description": "KV 實際存取 key"}
]
}
}
```
Secret **不**進 KBDBKBDB 不該存敏感資料),只有 metadata 在 KBDB 裡方便搜尋和關聯。
---
## 9. 首次授權 UI Flow(給人類看的部分)
這是「學員不知道該選哪個 credential 的痛點」的終結方案。
### 9.1 Static Key 的 UI
```
┌──────────────────────────────────────────┐
│ 連接 Notion │
├──────────────────────────────────────────┤
│ │
│ Internal Integration Token │
│ ┌────────────────────────────────────┐ │
│ │ secret_••••••••••••• │ │
│ └────────────────────────────────────┘ │
│ ↳ 如何取得?→ 開啟 Notion 整合設定頁 │
│ │
│ [ 測試連線 ] [ 儲存 ] │
└──────────────────────────────────────────┘
```
**零選項。** UI 從 recipe 的 `required_secrets` 動態生成。使用者不用選「這是 Header Auth 還是 Query Auth 還是 Custom Auth」——那是 recipe 的事,不是使用者的事。
### 9.2 OAuth2 的 UI
```
┌──────────────────────────────────────────┐
│ 連接 GitHub │
├──────────────────────────────────────────┤
│ │
│ [ 🔗 使用 GitHub 帳號登入 ] │
│ │
│ 將跳轉到 GitHub,授權後自動返回 │
│ │
└──────────────────────────────────────────┘
```
**一個按鈕。** Client ID / Secret 由 arcrun 平台統一管理(OAuth App 註冊在 arcrun 這邊),使用者看不到也不用知道。
### 9.3 Service Account 的 UI
```
┌──────────────────────────────────────────┐
│ 連接 Google Calendar │
├──────────────────────────────────────────┤
│ │
│ Service Account JSON │
│ ┌────────────────────────────────────┐ │
│ │ 將整份 JSON 貼到這裡 │ │
│ │ │ │
│ │ { │ │
│ │ "type": "service_account", │ │
│ │ "project_id": "...", │ │
│ │ ... │ │
│ │ } │ │
│ └────────────────────────────────────┘ │
│ │
│ 如何取得?→ 展開步驟說明 ▼ │
│ 1. 打開 GCP Console │
│ 2. IAM → Service Accounts │
│ 3. 建立 Service Account │
│ 4. Keys → Add Key → JSON │
│ 5. 下載後整份貼到上方 │
│ │
│ [ 測試連線 ] [ 儲存 ] │
└──────────────────────────────────────────┘
```
**貼 JSON + 按鈕**。不用寫任何程式碼,不用 debug 兩天。
---
## 10. 實作任務分解(CC 的 TODO list
### Phase 1:核心骨架(1-2 週)
- [ ] **T1.1** Recipe YAML schema 定義 + JSON Schema validator(放 `arcrun/schemas/recipe.schema.json`
- [ ] **T1.2** Recipe loader:從 `recipes/` 目錄讀 YAML → validate → 轉 JSON 存入 KV namespace `arcrun-recipes`
- [ ] **T1.3** TinyGo WASM 專案骨架(`arcrun/primitives/`),四個子目錄,統一 interface
- [ ] **T1.4** Worker runtime 的 WASM loader + host function(網路、KV 讀寫)
- [ ] **T1.5** `AuthBroker` TypeScript 類別骨架 + unit test
### Phase 2Static Key1 週)
- [ ] **T2.1** `static_key.wasm` 實作(header/query/body/basic_auth 四種注入)
- [ ] **T2.2** 寫三個 recipe`notion.yaml`, `openai.yaml`, `stripe.yaml`
- [ ] **T2.3** Tenant KV secret 加密寫入 + `AuthBroker.bind()` 整合
- [ ] **T2.4** `recipe.test` 執行器(驗證 credential 有效性)
- [ ] **T2.5** E2E test:存 secret → bind → fetch Notion API → assert
### Phase 3OAuth21-2 週)
- [ ] **T3.1** `oauth2.wasm` 實作(authorization_code + client_credentials + pkce
- [ ] **T3.2** OAuth callback endpoint(統一 URL,用 state 路由到正確 tenant/recipe
- [ ] **T3.3** Refresh token 自動續命邏輯(rate-limit 保護:同一 token 不能 1 秒內 refresh 多次)
- [ ] **T3.4** 寫三個 recipe`github.yaml`, `slack.yaml`, `google_oauth_user.yaml`
- [ ] **T3.5** UI flowstartAuth → 跳轉 → callback → 寫 secret
### Phase 4Service Account1 週)
- [ ] **T4.1** `service_account.wasm` 實作(google_jwt
- [ ] **T4.2** Google JWT signingES256 / RS256)— **這個 TinyGo 需要注意 crypto 支援**
- [ ] **T4.3** AWS SigV4 簽章實作(kind: aws_sigv4
- [ ] **T4.4** Recipe 繼承機制(`extends` 支援)
- [ ] **T4.5** 寫 recipes`_google_base`, `google_calendar`, `google_drive`, `gmail`, `aws_s3`
### Phase 5mTLS + 收尾(1 週)
- [ ] **T5.1** `mtls.wasm` 實作(對接 Cloudflare `mTLSCertificate` binding
- [ ] **T5.2** Cloudflare Secrets Store 整合(高敏感 secret 用)
- [ ] **T5.3** Recipe marketplace UI(列出可用 recipe,搜尋,一鍵設定)
- [ ] **T5.4** Observability:每次 bind / refresh / test 記錄到 KBDBmetadata,不含 secret
- [ ] **T5.5** Docs:recipe 撰寫指南(讓社群能貢獻)
### Phase 6Recipe 生成器(選配,1 週)
- [ ] **T6.1** 給 Claude 一份 API doc,自動產 recipe YAML 草稿 + 人類 review 介面
- [ ] **T6.2** 從 OpenAPI spec 自動推論 recipe
- [ ] **T6.3** 從 n8n credential file 反向轉譯(擷取 400+ 現成整合)
---
## 11. 關鍵技術風險與對策
| 風險 | 對策 |
|---|---|
| **TinyGo 的 crypto 支援不完整**ES256 / RS256 JWT 簽章) | 先用 `crypto/rsa` + `crypto/ecdsa` 確認 TinyGo 版本支援;若不行,fallback 用 Worker runtime 的 `crypto.subtle` 實作這部分,WASM 透過 host function 呼叫 |
| **Recipe 被惡意提交**(如 inject 內含 `https://evil.com` 當 token_url | Recipe 走 PR review + CI 自動檢查 URL 白名單;社群貢獻的 recipe 預設隔離在 `community/` 目錄,使用者明確選擇才啟用 |
| **OAuth state CSRF** | state 用 `crypto.randomUUID()` + 5 分鐘 TTL,存在 KVcallback 時比對 |
| **Secret 在 Worker log 外洩** | `AuthContext.Secret` 禁止 `toString` / `JSON.stringify`,用 Proxy 攔截;log 層強制 redact |
| **Token refresh 風暴**100 個並發 request 同時發現過期) | 用 Durable Object 單執行緒化每個 secret 的 refresh,其他 request 等結果 |
| **TinyGo WASM bundle size** | 四個 primitive 分開編譯,最大 500KB/個;lazy load |
| **Recipe 版本升級破壞相容** | `version` 欄位 semvertenant secret 記錄 `recipe_version`primitive 內處理遷移 |
---
## 12. 對比 n8n(給內部 review / 行銷用)
| 維度 | n8n | arcrun |
|---|---|---|
| Credential types 數量 | 400+(一個服務一個) | 4primitive + N recipe |
| 新增一個服務 | 寫 TypeScript class + rebuild + npm publish | 寫一份 YAML + PR merge |
| AI agent 使用 | 需要讀 node 文件 + 猜參數 | 讀 recipe YAML 即可 |
| 使用者首次設定 | 從 400+ 選項選一個(常選錯) | 搜尋服務名,只問必要 secret |
| OAuth App 管理 | 使用者自己註冊 OAuth app | arcrun 平台統一管理(使用者只需點「授權」) |
| 社群貢獻成本 | 高(TS + 編譯 + 測試) | 低(YAML + 測試) |
---
## 13. 接下來的決策點(需要 richblack 確認)
- [ ] **Recipe 版本管理策略**:採用 semver?每個 recipe 獨立版本?還是整個 recipe set 一個版本?
- [ ] **OAuth App 註冊**:arcrun 平台要統一註冊幾個主流服務的 OAuth AppGitHub、Google、Slack、Microsoft)?還是讓 tenant 自己帶 client_id/secret
- 建議:**雙模式**——平台模式(方便)+ BYO 模式(企業客戶用自己的 OAuth app 有稽核好處)
- [ ] **Recipe registry 的審核流程**:完全開放 PR 還是僅核心團隊維護?
- 建議:`recipes/official/`(核心維護)+ `recipes/community/`(PR 審核後 merge,使用者需明確啟用)
- [ ] **Secret rotation 政策**:要不要內建提醒 / 自動 rotate?(Phase 7+
---
## 14. 附錄:完整範例
### A. 最小可行 recipeOpenAI
```yaml
service: openai
version: 1
primitive: static_key
display_name: OpenAI
base_url: https://api.openai.com/v1
required_secrets:
- key: api_key
label: API Key
help_url: https://platform.openai.com/api-keys
inject:
header:
Authorization: "Bearer {{secret.api_key}}"
test:
method: GET
path: /models
expect_status: 200
tags: [ai]
```
### B. OAuth2 recipeSlack
```yaml
service: slack
version: 1
primitive: oauth2
display_name: Slack
base_url: https://slack.com/api
grant: authorization_code
oauth:
authorize_url: https://slack.com/oauth/v2/authorize
token_url: https://slack.com/api/oauth.v2.access
scopes:
- chat:write
- channels:read
refresh: true
client_auth: header
inject:
header:
Authorization: "Bearer {{runtime.access_token}}"
test:
method: POST
path: /auth.test
expect_json:
ok: true
tags: [communication]
```
### C. Service Account recipeGoogle Calendar
```yaml
extends: _google_base
service: google_calendar
version: 1
display_name: Google Calendar
base_url: https://www.googleapis.com/calendar/v3
token_exchange:
scopes:
- https://www.googleapis.com/auth/calendar
test:
method: GET
path: /users/me/calendarList
expect_status: 200
tags: [calendar, google]
```
---
## 15. 給 CC 的行動指引
1. **先不要動既有 arcrun 的 credential 相關 code**,保持現狀到 Phase 2 完成再切換。
2. **Phase 1 + 2 是 MVP**,做完可以接 80% 服務(API key 類)。
3. **遇到 TinyGo 的技術阻礙(特別是 crypto),立刻回報**,不要自己 workaround 兩天。
4. 每個 Phase 完成後寫一份 brief report(能跑什麼、不能跑什麼、下一步)。
5. Recipe 撰寫先做 3-5 個手工範例,**確認 schema 夠用再開始批量生成**。
@@ -1,521 +0,0 @@
# u6u-core 獨立開源 Repo 需求規格 v3
## 背景與定位
### 為什麼開源
u6u-core 是 AI 工作流執行引擎,開源的護城河邏輯如下:
```
開源(u6u-core 閉源(InkStone 付費服務)
──────────────────────── ──────────────────────────────
cypher-executor(執行引擎) KBDB 向量搜尋
WASM 零件庫(Gmail / GSheets…) KBDB graph 查詢
credentials Worker Persona SDK / Mini-me
CLI MatchGPT
→ 需要訂閱,不需要 YAML / KV
```
用戶自架版:YAML + CF KV,完全免費。
升級版:不需要 YAML,直接用自然語言查 KBDB 圖譜組 workflow,這是差異化。
### 目前 matrix repo 狀況
```
matrix/
├── cypher-executor/ ← 要搬進 u6u-core
├── u6u-core/
│ ├── builtins/
│ ├── credentials/
│ └── registry/
│ └── components/21 個 WASM 零件)
└── ...(其他 InkStone 內部服務,不搬)
```
---
## 任務一:搬移 cypher-executor 進 u6u-core
### 目標結構
```
u6u-core/(新獨立 repo,開源)
├── README.md
├── cypher-executor/ ← 從 matrix/cypher-executor 搬入
│ ├── src/
│ ├── wrangler.toml ← 需要清理(移除 InkStone 內部 bindings
│ └── ...
├── credentials/ ← 從 matrix/u6u-core/credentials 搬入
├── builtins/ ← 從 matrix/u6u-core/builtins 搬入
└── registry/
└── components/ ← 從 matrix/u6u-core/registry/components 搬入
```
### wrangler.toml 清理(重要)
現有 `cypher-executor/wrangler.toml` 有大量 InkStone 內部 Service Bindings,開源版要移除:
**移除(InkStone 專屬,不公開):**
```toml
# 移除這些 services bindings
KBDB → inkstone-kbdb-api
REGISTRY → inkstone-component-registry
CLINIC_GDRIVE → clinic-gdrive
CLINIC_EXCEL → clinic-excel
CLINIC_ANALYSIS
CLINIC_RENDER
CLINIC_GSHEETS
AICEO → inkstone-aiceo-bot
MINI_ME → inkstone-mini-me
```
**保留(用戶自己部署需要的):**
```toml
[[kv_namespaces]]
binding = "EXEC_CONTEXT" # 執行上下文暫存
[[kv_namespaces]]
binding = "WEBHOOKS" # workflow YAML 儲存
[[r2_buckets]]
binding = "WASM_BUCKET" # WASM 零件二進位
[ai]
binding = "AI" # Workers AIauto-publish 用)
```
**新增(開源版用戶需要的):**
```toml
[[kv_namespaces]]
binding = "CREDENTIALS_KV" # credential 加密存儲
```
### 清理後的 component-loader
現有 component-loader 可能有 InkStone 內部查詢邏輯(KBDB HTTP fetch),
開源版改為:**直接從 WASM_BUCKET R2 讀取 `.wasm` 檔案**,不依賴任何外部服務。
---
## 任務二:零件完成度審查與補充
### 完成度標準
每個零件的 `component.contract.yaml` 必須包含:
```yaml
# 已有(現狀)
canonical_id: "gmail"
input_schema: ...
output_schema: ...
gherkin_tests: ...
# 需要補充
credentials_required: # 需要 token 的零件才需要此欄位
- key: gmail_token # 對應 credentials.yaml 的 key 名稱慣例
type: google_oauth # token 類型
description: "Google OAuth access tokengmail.send scope"
inject_as: access_token # 執行時自動注入到 input 的哪個欄位
config_example: | # scaffold 指令產出的範本,帶說明註解
send_email: # 節點名稱(可自訂)
to: "" # 收件人 Email(必填)
subject: "" # 主旨(必填)
body: "" # 內文(必填)
# access_token 由 credentials.yaml 的 gmail_token 自動注入
```
### 需要 credentials_required 的零件
| 零件 | 需要的 token | inject_as |
|------|-------------|-----------|
| gmail | google_oauth | access_token |
| google_sheets | google_oauth | access_token |
| telegram | telegram_bot_token | bot_token |
| line_notify | line_token | token |
| http_request | 不固定(用戶自訂) | 不適用 |
### 不需要 credentials_required 的零件
set, filter, merge, switch, wait, if_control, foreach_control,
try_catch, validate_json, string_ops, number_ops, array_ops,
date_ops, cron, ai_transform_compile, ai_transform_run
### 審查任務(給 CC
對 21 個零件逐一檢查,**只回報,不修改**:
```
路徑:u6u-core/registry/components/
檢查四項:
1. contract.yaml 存在?
2. 有 credentials_required?(需要 token 的才需要)
3. 有 config_example
4. main.go required 欄位與 contract input_schema required[] 一致?
回報格式:表格(✓ / ✗ / N/A)+ 每個零件缺少什麼
不修改任何檔案。
```
審查完成後,再逐一補充缺少的欄位。
---
## 任務三:workflow YAML 格式定義
### 格式設計原則
- `flow:``>>` 三元組描述資料流,人類直接看懂
- 關係詞使用有語意的詞,**不使用 PIPE**(PIPE 等於什麼都沒說)
- `config:` 用零件名稱對應參數,欄位從 contract 的 config_example 來
- credential 全部集中在 `credentials.yaml`workflow 只寫 `{{creds.KEY}}`
### 可用關係詞
| 關係詞 | 語意 | 使用時機 |
|--------|------|---------|
| `完成後` | 前一個成功後執行 | 最常用的串接 |
| `失敗時` | 前一個失敗後執行 | 錯誤處理 |
| `對每個` | 對陣列每個元素執行 | 迭代 |
| `條件滿足時` | 條件分支 | 判斷 |
| `ON_SUCCESS` | 同「完成後」 | 英文版 |
| `ON_FAIL` | 同「失敗時」 | 英文版 |
| `FOREACH` | 同「對每個」 | 英文版 |
| `IF` | 同「條件滿足時」 | 英文版 |
| `ON_CLICK` | 前端按鈕觸發 | UI 互動 |
| `CALLS_SUBFLOW` | 呼叫子工作流 | 模組化 |
**禁止使用 PIPE** — 任何串接都應該用有語意的關係詞。
### 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: # componentId: gmail(由 cypher-executor 語意搜尋對應)
to: "{{input.email}}"
subject: "感謝訂閱!"
body: "歡迎加入!"
# access_token 由 credentials.yaml 的 gmail_token 自動注入
save_to_sheet: # componentId: google_sheets
action: write
spreadsheet_id: "{{creds.sheet_id}}"
range: "訂閱者!A:B"
values: [["{{input.email}}", "{{input.timestamp}}"]]
# access_token 由 credentials.yaml 的 google_oauth 自動注入
notify_error: # componentId: telegram
chat_id: "{{creds.telegram_chat_id}}"
text: "發信失敗:{{input.email}}"
# bot_token 由 credentials.yaml 的 telegram_bot_token 自動注入
```
### credentials.yaml 範例
```yaml
# credentials.yaml — 類似 .env,加入 .gitignore,不進 git
# u6u creds push 時逐一加密上傳到 CREDENTIALS_KV
gmail_token: "ya29.a0AfB_..."
google_oauth: "ya29.a0AfB_..."
sheet_id: "1BxiMVs0XRA5nFMdKvBdBZjgmUUqptlbs74OgVE2upms"
telegram_bot_token: "123456:ABC-..."
telegram_chat_id: "987654321"
```
### 執行時 credential 注入流程
```
u6u run newsletter_subscribe
cypher-executor 讀 workflow YAML
遇到節點 send_thanks → 查 contractcredentials_required.inject_as = access_token
去 CREDENTIALS_KV 讀 gmail_token → 解密
注入到 WASM input{ to, subject, body, access_token: "ya29..." }
WASM 執行,用戶的 config 裡完全不出現 token
```
---
## 任務四:CLI 開發
### 技術選型
- **語言**Node.jsTypeScript
- **安裝**`npm i -g u6u`
- **依賴**`commander``js-yaml``chalk``ora`
### 指令規格
#### `u6u init`
互動式初始化,產生 `~/.u6u/config.yaml` 和本機 `credentials.yaml`
```
$ u6u init
? Cloudflare Account ID: abc123
? KV Namespace ID (WEBHOOKS): xyz789
? KV Namespace ID (CREDENTIALS_KV): abc456
? R2 Bucket name (WASM_BUCKET): u6u-wasm
? Cypher Executor Worker URL: https://cypher-executor.xxx.workers.dev
? Credentials Worker URL: https://u6u-credentials.xxx.workers.dev
? Cloudflare API Token: ***
✓ 設定完成 → ~/.u6u/config.yaml
✓ 建立 credentials.yaml(已加入 .gitignore
```
---
#### `u6u creds push [credentials.yaml]`
讀取 credentials.yaml,逐一加密上傳到 CREDENTIALS_KV。
```
$ u6u creds push
讀取 ./credentials.yaml...
✓ gmail_token → 已加密上傳
✓ google_oauth → 已加密上傳
✓ sheet_id → 已上傳
✓ telegram_bot_token → 已加密上傳
✓ telegram_chat_id → 已上傳
共上傳 5 個 credentials
```
---
#### `u6u push <workflow.yaml>`
解析 `flow:` 三元組,轉換成 triplets 陣列,上傳到 WEBHOOKS KV。
```
$ u6u push newsletter_subscribe.yaml
✓ 已上傳 newsletter_subscribe → WEBHOOKS KV
Webhook: https://cypher-executor.xxx.workers.dev/webhook/abc123
```
轉換邏輯(CLI 負責):
```
flow[] 三元組
POST /cypher/search(取得 ExecutionGraph
連同 config 存入 WEBHOOKS KV
```
---
#### `u6u run <workflow_name> [--input key=value...]`
觸發執行,顯示結果。
```
$ u6u run newsletter_subscribe --input email=test@example.com
⏳ 執行中...
✓ 完成(2.3s
結果:
send_thanks: { success: true, data: { message_id: "xxx" } }
save_to_sheet: { success: true, data: { range: "訂閱者!A2" } }
```
錯誤時給出具體修復步驟:
```
✗ 執行失敗:節點 send_thanks
原因: access_token 無效(401 Unauthorized
修復方式:
1. 更新 credentials.yaml 的 gmail_token
2. 執行 u6u creds push
3. 重新執行 u6u run newsletter_subscribe
取得 Google OAuth token
→ https://developers.google.com/oauthplayground
```
---
#### `u6u validate <workflow.yaml>`
執行前完整驗證,提前發現問題。
```
$ u6u validate newsletter_subscribe.yaml
✓ YAML 格式正確
✓ flow 三元組語法正確
✓ 所有關係詞有效(無 PIPE
✓ 所有節點名稱在 config 有對應
✓ 所有零件存在於 WASM_BUCKET
✓ credentials 對應:
gmail_token ✓ 已上傳
google_oauth ✓ 已上傳
sheet_id ✓ 已上傳
telegram_bot_token ✗ 缺少
⚠ 缺少 1 個 credential
telegram_bot_token → 請加入 credentials.yaml 並執行 u6u creds push
```
---
#### `u6u parts`
列出可用零件。
```
$ u6u parts
可用零件(21):
[整合]
• gmail Gmail 發信
需要: to, subject, body
credential: gmail_tokengoogle_oauth
• google_sheets 讀寫 Google 試算表
需要: spreadsheet_id, range, action
credential: google_oauth
• telegram Telegram Bot 發訊息
需要: chat_id, text
credential: telegram_bot_token
• line_notify LINE Notify
需要: message
credential: line_token
• http_request 任意 HTTP 請求
需要: url
[控制]
• if_control 條件分支
• foreach_control 迭代執行
• try_catch 錯誤處理
• switch 多路路由
• wait 等待 N 毫秒
[資料]
• set / filter / merge / string_ops / number_ops / array_ops / date_ops
[AI]
• ai_transform_compile 自然語言 → JS 轉換函式
• ai_transform_run 執行已編譯的轉換
```
---
#### `u6u parts scaffold <component>`
從 contract 的 config_example 產出可直接貼入 workflow 的 config 範本。
```
$ u6u parts scaffold gmail
貼入 workflow.yaml 的 config 區塊:
send_email: # 節點名稱(可自訂)
to: "" # 收件人 Email(必填)
subject: "" # 主旨(必填)
body: "" # 內文(必填)
# access_token 由 credentials.yaml 的 gmail_token 自動注入
貼入 credentials.yaml
gmail_token: "" # Google OAuth token
# 取得方式:https://developers.google.com/oauthplayground
```
---
#### `u6u list`
列出 WEBHOOKS KV 中所有 workflow。
```
$ u6u list
• newsletter_subscribe (更新: 2026-04-16)
• daily_summary (更新: 2026-04-15)
```
---
#### `u6u logs <workflow_name>`
查看最近執行記錄。
```
$ u6u logs newsletter_subscribe
2026-04-16 14:30 ✓ 成功 2.1s
2026-04-16 09:00 ✗ 失敗 send_thanks: 401 Unauthorized
2026-04-15 09:00 ✓ 成功 1.8s
```
---
## 開發順序
### Phase 1:搬移與清理(先做)
```
1. 建立新的獨立 repou6u-core
2. 從 matrix 搬入:
- cypher-executor/
- u6u-core/credentials/
- u6u-core/builtins/
- u6u-core/registry/
3. 清理 cypher-executor/wrangler.toml(移除 InkStone 內部 bindings
4. 確認 component-loader 只依賴 WASM_BUCKET,不依賴 KBDB / REGISTRY
5. 本機部署測試
```
### Phase 2:零件完成度(搬移後)
```
6. 審查 21 個零件的 contract.yaml
7. 補充 credentials_requiredgmail, google_sheets, telegram, line_notify
8. 補充 config_example(全部 21 個)
9. 驗證 main.go required 欄位與 contract 一致
```
### Phase 3CLI(完成度補充後)
```
10. u6u init
11. u6u creds push
12. u6u push
13. u6u run(含 credential 自動注入)
14. u6u parts / u6u parts scaffold
15. u6u validate
16. u6u list / u6u logs
```
### Phase 4:開源發布
```
17. 撰寫 README.md(快速開始、零件列表、workflow 語法說明)
18. 撰寫 CONTRIBUTING.md(如何新增零件)
19. 發布到 GitHub
20. npm publishu6u CLI
```
---
## 不在此次範圍
- KBDB 整合(未來付費服務)
- 向量搜尋 / graph 查詢
- 前端管理介面
- Webhook trigger 設定(用戶自行設定 CF Cron)
- 新增 WASM 零件(現有 21 個先做完整,之後再擴充)
@@ -1,89 +0,0 @@
# **u6u 智慧前端與工匠 AI 開發藍圖 (v4.0)**
## **一、 核心設計理念:意圖導向的雙面畫布**
u6u 的前端不是傳統的平面繪圖板,而是一個類似 Android Studio 或 Figma 的\*\*「結構化標籤編輯器」**。 畫布上的每一個元件(Web Component),本質上都是一個**「意圖發射器 (Intent Emitter)」\*\*。前端只負責「長得好看」與「收集人類動作」,後端全權負責「業務邏輯」。
### **人機協作的「雙向同步」**
* **AI 詠唱修改:** 人類說「把按鈕改醒目一點」,CEO AI 在背景將 \<u6u-btn color="blue"\> 修改為 \<u6u-btn color="neon-red"\>,畫面瞬間更新。
* **人類手動覆寫:** 人類覺得 AI 調的紅色太暗,直接在右側屬性面板 (Properties Panel) 點選色碼器,底層 HTML 屬性隨之改變。**AI 也能「看見」這個改變,從中學習人類的審美偏好。**
## **二、 畫布介面設計與運作機制**
### **1\. 正反面翻轉機制 (The "Flip" Interface)**
每個 UI 零件在畫布上都有「一體兩面」:
* **正面 (UI 視圖)** 顯示 HTML 渲染的視覺結果(按鈕、溫度計、圖表)。人類可以在此調整 CSS 屬性、對齊方式與主題顏色。
* **反面 (邏輯視圖)** 點擊「翻面」按鈕後,會進入底層的工作流設定。這裡使用 u6u 的自定義 Cypher 視覺化語法(例如 \>\> 符號)。
* *範例:* \[ UI\_Button: "緊急停機" \] \>\> (Intent: emergency\_stop) \>\> \[ WASM: gsheets\_create \]
### **2\. 智慧容器與區域感知 (Smart Zone Awareness)**
為了消滅傳統 iPaaS(如 n8n)最痛苦的「手動變數綁定」,u6u 畫布具備「區域感知」能力。
* **底層邏輯(獨立元件):** 畫布上的 TextInput 與 Button 都是各自獨立的原子元件。
* **麻瓜體驗(智慧表單):** 當使用者將這兩個元件拖入同一個排版容器(例如 \<u6u-card\>)時,系統會自動建立上下文關聯。當按下按鈕並觸發 Webhook 時,按鈕會**自動打包同容器內所有輸入框的值**一併送出:
{
"intent": "query\_attendance",
"payload": { "employee\_id": "A1234" } // 自動從旁邊的 TextInput 抓取
}
使用者完全不需要理解「表單傳值」或「變數綁定」,拖拉組合即生效。
### **3\. 多重事件插槽與靜態屬性 (Multi-Event Slots)**
一個前端元件可以具備多種觸發行為,系統透過介面將「視覺」與「後端邏輯」徹底分流:
* **靜態視覺註釋:** 例如 mouseover 顯示提示。使用者只需在屬性面板輸入 Tooltip 文字,底層僅修改 HTML 屬性 \<u6u-btn tooltip="..."\>,不消耗任何伺服器資源或 Webhook。
* **動態意圖綁定:** 在「反面」邏輯視圖中,使用者可以針對不同事件綁定不同的工作流:
* ⚡ When: 點擊 (onClick) ➡️ \[ 綁定至 Webhook A:送出查詢 \]
* ⚡ When: 獲得焦點 (onFocus) ➡️ \[ 綁定至 Webhook B:載入歷史紀錄 \]
### **4\. 智慧上下文替換 (Smart Contextual Substitution)**
當主管在畫布上對著一個已連接 Webhook 的「按鈕」點擊右鍵選擇「替換元件」時:
* 系統讀取反面的 Cypher 連線,發現需要發射一個 trigger 意圖。
* 系統過濾 KBDB 零件宇宙,**只顯示相容的 UI 零件**(如下拉選單、開關)。不具備 trigger 能力的元件(如純文字標籤)會被自動隱藏,確保替換後系統絕對不會報錯。
## **三、 原子化組裝與極致解耦:CEO AI 與工匠 AI 的分工**
當企業主管提出需求:「我需要一個『輸入工號即可查詢員工打卡紀錄』的工具」時,這在 u6u 中**並不是一個單一零件**,而是一個由多個「原子零件」構成的**工作流 (Workflow)**。
### **1\. CEO AI 的動態組裝 (Macro Assembly)**
面對需求,大腦 AI (CEO AI) 會快速從 KBDB 挑選現成積木進行組合:
* **前端 Prototype 組合:** \<u6u-text-input\> \+ \<u6u-btn\> \+ \<u6u-text-field\>。
* **後端 Pseudo Code 組合:** webhook\_receiver \>\> check\_kbdb(template\_name, value)。
AI 會自動用 Cypher 將前端的表單意圖連線到後端工作流。對不懂程式的主管來說,前端就是 Prototype,翻面的 Cypher 就是 Pseudo Code,整套系統瞬間組合完畢。
### **2\. 工匠 AI (Forge AI) 的原子生產線**
只有當現有零件庫缺乏特定原子時,機甲才會喚醒工匠 AI 進行開發。
**解耦哲學:** 後端零件開發時,根本不需要管前端零件長什麼樣子(是按鈕還是輸入框)。只要前端送來的 JSON 它能吃,就是合法的候選零件。
* **Step 1: 規格定義 (Interface Contract)**
工匠 AI 只在乎接收與回傳的 JSON 格式。
* **Step 2: 打造純粹的邏輯黑箱 (後端 TinyGo WASM)**
工匠 AI 撰寫 Go 程式,編譯成 .wasm。絕對純粹的後端邏輯,沒有任何介面程式碼。
* **Step 3: 獨立的前端零件生產 (若需要)**
獨立生成 Web Component(如 \<u6u-3d-pie-chart\>),只負責接收特定 JSON 來渲染畫面。
* **Step 4: 註冊與編目 (Cataloging into KBDB)**
新積木註冊到圖資料庫,未來的 CEO AI 即可將其與任何既有的前端或後端元件進行無限的叉積組合。
## **四、 架構總結與終極產品體驗**
這套前端架構讓 u6u 成為一個\*\*「表裡如一」**的系統,成功創造了**「麻瓜的 ERP 幻覺」\*\*:
使用者不需要知道什麼是「前後端分離」、什麼是「API 串接」。他們只是覺得:
1. 拖拉了一個溫度計。
2. 翻面把「數值更新」連線到「機台感測器」。
3. 拖拉了一個紅按鈕放在旁邊。
4. 翻面把「點擊」連線到「發送 Line 警報」。
在十分鐘的「繪圖」過程中,沒有寫一行程式碼,也沒有設定任何變數。但透過**前端 Web Components** 的視覺封裝、**智慧容器**的自動資料打包,以及**後端 TinyGo WASM \+ Cypher** 的無縫承接,他們在不知不覺中,就搭建出了一套具備微服務架構、高擴展性、且可部署至極限邊緣的企業級系統。
@@ -1,136 +0,0 @@
# **u6u 系統與零件宇宙全景規劃白皮書 (The u6u Ecosystem Blueprint)**
## **1\. 核心理念與願景**
u6u 旨在解決傳統 Workflow 軟體 (如 n8n) 存在的「單線程、沈重、複雜、難以組成系統」的痛點。
透過結合 Cloudflare Workers (輕量邊緣運算) 與 Cypher (圖形資料庫關係),u6u 提供一個由 AI 驅動的「意圖到系統」生成平台。所有的系統功能皆被拆解為可複用、可組合的「零件 (Components)」,並在一個會自然淘汰、自我修復的「零件宇宙 (Component Universe)」中演化。
## **2\. 四層架構拆解 (Four-Tier Architecture)**
u6u 的工作模式採取由上到下 (Top-Down) 的 Break-down 機制:
1. **Polaris (北極星層 / 意圖層):**
* 用戶以自然語言描述商業模式與想法(例如:「我要做一個 AI 客服表單系統」)。
* 這是整個系統的起點,AI 會根據 Polaris 將意圖拆解為 Prototype。
2. **Prototype (原型 / 前端層):**
* 定義前端的版型、頁面描述、UI 元件以及它們的屬性。
* 作為使用者互動的入口,透過觸發事件 (Triggers) 連接到後端 Workflow。
3. **Workflow (工作流層):**
* 系統的 Orchestrator (編排者),定義業務邏輯的走向。
* 透過 Cypher 語法與三元組,定義每個節點 (Component) 的執行順序與條件分支。
4. **Component (零件層 / 節點):**
* 最底層的執行單元,主要分為兩類:
* **功能型 (Logic):** 迴圈、條件判斷、資料轉換、統計等 (透過 CF Workers 執行 JS 邏輯)。
* **介接型 (API):** 呼叫外部服務 (Webhook, HTTP Request)。
## **3\. 統一描述語言:擴展三元組與跨層級 YAML**
為了解決跨 YAML 檔案串接的問題,u6u 採用易於人類閱讀與 AI 生成的 **「A \>\> 關係 \>\> B」** 三元組語法,結合自定義的 URI 協議 (workflow://, component://, ui://),實現跨層級的連結。
### **綜合 YAML 範例與三元組串接**
*\# 1\. Prototype YAML (描述前端)*
kind: Prototype
id: ui\_dashboard
triplets:
*\# 結構與版型零件*
\- "ui\_dashboard \>\> CONTAINS \>\> layout\_admin"
\- "layout\_admin \>\> CONTAINS \>\> btn\_submit"
*\# UI 零件與屬性零件 (CSS/行為)*
\- "btn\_submit \>\> IS\_A \>\> ui://components/Button"
\- "btn\_submit \>\> HAS\_STYLE \>\> style://tokens/GlowEffect"
\- "btn\_submit \>\> HAS\_BEHAVIOR \>\> anim://motions/Pulse"
*\# 跨層級串接:前端觸發 Workflow*
\- "btn\_submit \>\> ON\_CLICK \>\> workflow://workflows/process\_data.yaml"
*\# 2\. Workflow YAML (描述工作流編排)*
kind: Workflow
id: wf\_process\_data
triplets:
*\# 跨層級串接:Workflow 呼叫 Component*
\- "START \>\> TRIGGERS \>\> step\_validate"
\- "step\_validate \>\> IS\_A \>\> component://components/validate\_json"
*\# Workflow 節點間的流轉 (轉譯為 Cypher 關係)*
\- "step\_validate \>\> ON\_SUCCESS \>\> step\_call\_api"
\- "step\_validate \>\> ON\_FAIL \>\> step\_notify\_error"
*\# 跨 Workflow 串接*
\- "step\_call\_api \>\> CALLS\_SUBFLOW \>\> workflow://workflows/save\_to\_db.yaml"
## **4\. 零件宇宙 (Component Universe) 的審核與淘汰機制**
在 u6u 中,所有的 UI、Style、Logic、API 都是「零件」。當 AI 發現缺乏所需零件時,會自動創造它。為了確保生態系的健康,必須建立嚴格的**審核標準**與**自然淘汰機制**。
### **4.1 零件的創建與審核標準 (Pass/Fail Criteria)**
當 AI 或開發者提交一個新零件時,系統會啟動自動化沙盒測試。必須完全通過以下標準,零件才能進入「宇宙」供他人使用:
1. **功能型零件 (Logic Components):**
* **Gherkin BDD 驗收:** 必須附帶 Feature/Scenario 測試規格,且執行結果 100% 通過 (例如:Given input JSON, When split, Then returns Array)。
* **效能門檻:** 邊緣運算 (CF Workers) 執行時間需低於設定閾值 (例如 \< 50ms),無記憶體洩漏。
2. **介接型零件 (API Components):**
* **連線驗證:** 端點 (Endpoint) 必須能 ping 通,或回傳正確的 2xx HTTP Status (提供 Mock Payload 測試)。
* **Credential 安全:** 不可將 Token 或 Secret 寫死在代碼中,必須嚴格宣告所需的 Environment Variables 規格。
3. **前端與屬性零件 (UI & Style Components):**
* **渲染驗證:** CSS / 組件代碼不能導致瀏覽器 Crash。
* **相容性檢查:** 不可包含嚴格衝突的樣式 (例如寫死 \!important 破壞全域版型)。
### **4.2 零件宇宙的自然淘汰 (Natural Selection)**
零件一旦上架,將面臨殘酷的達爾文機制:
* **AI 偏好權重:** AI (透過 MCP 搜尋時) 會優先選擇「成功率高、執行速度快、被調用次數多」的零件。
* **降級與墓地:** 連續 30 天無人/無 AI 使用,或錯誤率飆升的零件,會被降級 (Deprecated)。最終轉入「零件墓地」,從首選搜尋清單中剔除。
## **5\. 系統自癒與 AI 避坑機制 (Auto-Healing & Pitfall Avoidance)**
這是 u6u 維持系統穩定運作的最核心機制。工作流不只要能跑,跑完後還必須經歷 **「強制 AI 評價 (Mandatory AI Evaluation)」**。
### **5.1 運行後的強制評價迴圈**
每當一個 Workflow 在 CF Workers 上執行完畢 (或發生異常中斷),系統攔截日誌並強制啟動 AI 評價代理 (Evaluator Agent)。
* **評估維度:**
* **狀態:** 成功 / 失敗 (Crash) / 逾時 (Timeout)。
* **效能:** 耗時是否合理 (例如 API 突然變得很慢)。
* **警告訊息:** 資源消耗過大、API 回傳即將停用的 Warning。
### **5.2 自癒與避坑流程 (The Feedback Loop)**
當 Evaluator Agent 發現問題時,會觸發以下流程:
1. **回報與通知 (Notify):** 系統自動生成修復 Ticket,並通知當初建立該零件/工作流的製作人 (或系統管理員)。
2. **AI 嘗試修復 (Auto-Fix):** 系統派遣「修復型 AI」嘗試讀取錯誤日誌並修復代碼 (例如:API 規格變更導致 JSON 解析錯誤,AI 自動修改解析邏輯)。
3. **驗收與部署:** 修復後的代碼若通過 Gherkin 驗收,則無縫熱更新。
4. **避坑標記 (Pitfall Marking):** \- 如果 AI 無法修復 (例如:外部第三方 API 永久倒閉,或邏輯存在根本性死結)。
* 系統會在 Cypher 圖形資料庫中,將該零件或該特定的三元組關係標記為 \[HAS\_PITFALL\]。
* **結果:** 下一個生成系統的 AI 在透過 MCP 搜尋時,會讀取到這個坑的紀錄,並**強制繞道**,改用其他方案或生成新的零件,實現「前人踩坑,後 AI 避坑」的群體智慧。
## **6\. 結論**
u6u 不是一個單純的開發工具,它是一個**生物體積木系統**。
透過「三元組」統一語言打破系統壁壘,透過「零件審核」保證基因優良,再透過「強制評價與避坑機制」實現演化。當這套系統運轉起來,AI 就能在其中無止盡地為人類組裝出越來越強大、越來越穩定的商業應用。
@@ -1,99 +0,0 @@
# **u6u 自動演化 ERP:全端統一架構規格書 (v3.0)**
## **1\. 架構核心思想 (The Core Philosophy)**
u6u ERP 是一套具備自我修復與功能擴充能力的「有機體」系統。
為確保系統在跨國雲端、機密地端與斷網邊緣皆能無縫運作,系統採用\*\*「向下相容的絕對標準化」\*\*:由最嚴苛的無人機環境來定義全域零件標準。
系統運作依賴三位一體的語言與載體:
1. **大腦戰略層 (Markdown / Gherkin)** CEO AI 負責閱讀與撰寫,定義全域戰略、系統設計文件 (SDD) 與商業演算法則 (如 ROI 門檻)。
2. **神經編排層 (Cypher)** u6u 引擎的核心。AI 透過撰寫 Cypher 語法來進行業務邏輯的動態編排、狀態流轉與意圖攔截。
3. **肌肉執行層 (TinyGo WASM)** 系統中**唯一合法**的零件規格。負責所有具體的 I/O、資料轉換與運算,保證極小體積與極速冷啟動。
## **2\. 實戰演練:離岸風機巡檢的黑天鵝事件 (三層架構實踐)**
為了具體理解這套系統如何運作,我們以一次「離岸風電場巡檢」的突發事件為例,展示雲、地、邊三層架構的完美協同。
### **第一階段:戰略下達與沙盤推演 (Tier 1 ➡️ Tier 2 ➡️ Tier 3\)**
跨國能源集團的**雲端總部 (Tier 1\)** 收到年度檢修排程。雲端的 **CEO AI** 讀取了全局的 Markdown 戰略文件,向遠在海岸線的**地端指揮中心 (Tier 2\)** 下達指令。
地端指揮中心(配備強大伺服器與 workerd 叢集)的**部門主管 AI** 將任務拆解給 50 台即將出海的無人機。無人機 07 號 **(Tier 3\)** 的小腦 AI 透過本地的 Cypher 引擎進行沙盤推演,從地端資料庫下載了 rgb\_vision.wasm (光學影像)、lidar\_scan.wasm (光達) 等 60 個可能會用到的 TinyGo 零件,存入本地記憶體後隨船出航。
### **第二階段:邊緣的極限生存 (Tier 3 獨立運作)**
無人機 07 號來到海上 50 公里處,完全失去對外網路。突然,海上濃霧降臨。
原本執行中的 Cypher 圖譜卡住了,因為 rgb\_vision.wasm 回報「無法獲取清晰影像」。07 號沒有驚慌,它內建的輕量級 Go \+ Wazero 引擎在 0.1 秒內動態重組了圖譜邏輯:剔除光學零件,瞬間載入並執行 lidar\_scan.wasm,不需人類介入,繼續在濃霧中精準貼行。
### **第三階段:游擊網與地端代工 (Tier 3 ↔️ Tier 2\)**
巡檢中途,07 號發現風機葉片上有極罕見的「蜂巢狀熱應力微裂紋」,但它帶出來的 60 個零件中沒有對應的分析工具。
07 號飛昇至濃霧上方,短暫連上母船的微弱區域網路發起「短點射傳輸 (Burst)」:{"intent": "計算蜂巢狀熱應力微裂紋擴散率"},拿到任務單號後立刻斷網潛回霧中。
海岸線的**地端指揮中心 (Tier 2\)** 收到需求。強大的**工匠 AI** 瞬間啟動,生成了一段 TinyGo 程式碼,並在本地編譯與測試。三分鐘後,07 號再次探頭連網,下載了熱騰騰的 honeycomb\_analyzer.wasm,並將其編織進 Cypher 圖譜中完成測量。
### **第四階段:CEO AI 的全局戰略覆寫 (Tier 2 ➡️ Tier 1\)**
同時,地端指揮中心匯整了無人機傳回的陣風數據,同步給**雲端總部 (Tier 1\)**。雲端的 CEO AI 呼叫 roi\_calculator.wasm 進行試算,發現風暴將造成設備重大損壞(ROI 極低)。
CEO AI 立刻修改總部的 Markdown 戰略文件,新增一條 BDD 規則:「風速大於 22m/s,立刻轉為陣列抗風模式」。新的最高指導 Cypher 範本瞬間下發至地端,再廣播給所有無人機。07 號收到新命令,掛起原任務,與機群組成抗風陣型,安全度過危機。
## **3\. 物理拓撲與技術棧 (The 3-Tier Tech Stack)**
透過 **KBDB Adapter** 抽象層,AI 在任何環境中呼叫的 API 介面皆一致,但底層基礎設施依據物理環境的豐饒度進行適配。
### **Tier 1: 雲端總部 (Cloud \- The Global Brain)**
* **場景:** 跨國集團資料整合、全域戰略備份、對外公開 API、跨國部門協調。
* **AI 角色:** **CEO AI (大型語言模型)**。負責解析 Markdown、跨區資源調度、修改全域演算法參數。
* **技術規格:**
* **調度引擎:** Cloudflare Workers (原生執行 TinyGo WASM)。
* **圖資料庫 (狀態/關聯)** Cloudflare D1 \+ u6u Cypher 轉換層。
* **零件與儲存:** Cloudflare R2 / KV。
* **向量檢索 (意圖/型錄)** Cloudflare Vectorize。
* **架構優勢:** 無限橫向擴展 (Serverless),無須維運硬體,扛載全球級別的 API 併發。
### **Tier 2: 企業地端/基地台 (On-Premise \- The Basecamp & Forge)**
* **場景:** 高機密廠房內網、財務核心系統、無人機/機器人的母艦基地。
* **AI 角色:** **部門主管 AI** (廠區派工);**工匠 AI** (專職接收規格,透過 TDD 閉環動態生成 TinyGo 程式碼)。
* **技術規格 (企業級高可用架構)**
* **負載平衡:** Nginx 或 HAProxy (負責將請求分發給後端叢集)。
* **調度引擎:** **workerd 叢集 (Cloudflare 開源執行環境)**。在本地實體伺服器或 VM 上平行部署多個 workerd 行程,完美相容雲端環境,提供極高的並發處理能力 (V8 JIT 極限算力)。
* **圖資料庫 (狀態/關聯)** **Kùzu** (單機極速圖庫) 或 PostgreSQL \+ AGE (超高併發)。
* **零件與儲存:** 企業本地 NVMe 硬碟叢集 / MinIO (S3 相容)。
* **向量檢索 (意圖/型錄)** pgvector 或 Milvus。
* **架構優勢:** 兼具資料不出網的「絕對資安」與雲端級別的「叢集擴展性」。內建「代工坊 (Forge)」,是推動企業系統自動演化的核心引擎。
### **Tier 3: 邊緣載具 (Extreme Edge \- The Operatives)**
* **場景:** 無網環境的巡檢無人機、工廠無軌導引車 (AGV)、機械手臂。
* **AI 角色:** **導航/執行 AI (極小參數 SLM)**。不具備寫程式能力,只負責解讀現場狀況、執行 Cypher 圖譜,並透過 DTN 呼叫地端請求新零件。
* **技術規格 (極限微縮架構)**
* **調度引擎:** 輕量級 Go 排程引擎 \+ **內嵌 Wazero**。不依賴 V8 或 workerd,確保在極低 RAM 的晶片上流暢運行,實例化延遲僅需數微秒。
* **圖資料庫 (狀態/關聯):** 嵌入式 Kùzu 或 SQLite。
* **零件與儲存:** SD 卡 / eMMC 實體檔案系統。
* **向量檢索 (意圖/型錄)** sqlite-vss (極輕量本地向量)。
* **架構優勢:** 絕對的離線生存能力。只帶必要的 TinyGo WASM 零件出門,無任何編譯環境,體積最小化。
## **4\. 自動演化工作流 (The Auto-Evolution Loop)**
當企業環境發生變化(例如:新增硬體規格、外部 API 變更),u6u 的演化路徑如下:
1. **遭遇未知 (Anomaly Detection)**
無人機 (Tier 3\) 或雲端服務 (Tier 1\) 在執行 Cypher 任務時,發現本地 KBDB 向量庫中缺乏對應的工具零件。
2. **意圖攔截與 ROI 評估 (CEO/Manager AI)**
機甲 (Harness) 攔截缺失意圖,呼叫 roi\_calculator.wasm 等評估零件。若認定具備開發價值,系統會生成一份標準的 Input/Output JSON Schema。
3. **地端代工 (The Forge @ Tier 2)**
規格需求透過網路或 DTN 送達 Tier 2 地端機房的「工匠 AI」。
工匠 AI 生成 TinyGo 程式碼 \-\> 在沙盒中執行 tinygo build \-target=wasi \-\> 通過測試迴圈 \-\> 輸出正式的 .wasm 檔案。
4. **全域派發 (Distribution & Versioning)**
新零件註冊進入企業的零件圖資料庫 (KBDB)。
* **雲端:** 同步至 R2。
* **邊緣:** 載具下次連網時,透過游擊網 (Burst Transmission) 下載更新檔。
5. **動態編織 (Execution)**
各端 AI 獲知新零件上線,瞬間將其編入新的 Cypher 圖譜中執行,完成企業能力的自動擴展。
@@ -1,360 +0,0 @@
# u6u 系統規格書 v1.0
## 給 AI 的架構思考指引
> 本文件用途:讓 AI 理解 u6u 的完整設計意圖、現況、與未來路徑,
> 在實作決策時能自行判斷方向正確性,而不只是執行單一任務。
---
## 一、系統本質(先理解再動手)
u6u 不是 workflow 工具,不是 no-code 平台,不是 iPaaS。
u6u 是一個**「意圖到系統」的生物體積木平台**:
- 人類說出意圖(自然語言)
- AI 從零件宇宙組裝出可運行的系統
- 系統會自動評價、演化、淘汰舊零件
- 累積的零件就是核心資產,越積越有價值
**設計的終極體驗:** 工廠主管拖拉十分鐘,組出具備微服務架構的企業系統,零程式碼,但底層是真正的分散式系統。
---
## 二、四層邏輯架構
```
Polaris(意圖層)
↓ 自然語言 → AI 拆解
Prototype(前端層)
↓ UI 元件 + 觸發事件
Workflow(編排層)
↓ Cypher 語法定義執行順序
Component(零件層)
↓ .wasm 實際執行
```
每一層向下只透過標準介面溝通,層與層之間完全解耦。
---
## 三、物理三層部署
```
Tier 1:雲端總部(Cloudflare Workers
- CEO AI 讀取 Markdown 戰略文件
- 全域零件同步至 R2
- Cloudflare D1 + VectorizeKBDB
Tier 2:企業地端(workerd 叢集)
- 部門主管 AI 派工
- 工匠 AI 生成並測試新零件
- Kùzu 或 PostgreSQL + AGE(圖資料庫)
- pgvector 或 Milvus(向量搜尋)
Tier 3:邊緣載具(無人機、AGV、工廠設備)
- 極小參數 SLM
- Go 排程引擎 + 內嵌 Wazero(無 V8
- SQLite + sqlite-vss
- 離線生存,DTN 短點射傳輸
```
**關鍵約束:** Tier 3 沒有 V8,沒有 Node.js,沒有網路。
所有零件必須在 Wazero 上跑,所有資料傳輸透過 stdin/stdout JSON。
---
## 四、零件規格(Component Contract
這是整個系統最核心的不變量。零件規格定錯,累積的資產會變成技術債。
### 4.1 零件的本質定義
**一個零件只做一件事。**
```
✅ gsheets_create_table
✅ gsheets_delete_table
✅ gsheets_get_entries
❌ gsheets_manager(做太多事,禁止)
```
### 4.2 零件合約格式(component.contract.yaml
每個零件必須附帶此合約,這是 AI 讀取零件的唯一介面描述:
```yaml
id: "gsheets_get_entries" # 功能合約名稱(永久不變)
version: "v2" # 實作版本
wasi_target: "preview1" # 明確標記 WASI 版本,未來升級用
stability: "floating" # floating | stable | pinned
runtime_compat:
- "cf-workers"
- "workerd"
- "wazero"
constraints:
max_size_kb: 2048 # 超過視為打包了 runtime
max_cold_start_ms: 50
no_network_syscall: true # 禁止零件自己發 HTTP
no_filesystem_syscall: true # 只能 stdin/stdout
io_model: "stdin_stdout_json" # 唯一合法的 I/O 模型
input_schema:
type: object
required: ["spreadsheet_id", "sheet_name"]
properties:
spreadsheet_id: { type: string }
sheet_name: { type: string }
limit: { type: integer, default: 100 }
output_schema:
type: object
properties:
rows: { type: array }
total: { type: integer }
error: { type: string }
gherkin_tests:
- scenario: "正常取得資料"
given: '{"spreadsheet_id":"abc","sheet_name":"Sheet1"}'
then_contains: '{"total":1}'
- scenario: "不存在的表格回傳錯誤"
given: '{"spreadsheet_id":"abc","sheet_name":"不存在"}'
then_contains: '{"error":'
tags: ["google", "sheets", "data", "read"]
description: "從 Google Sheets 取得指定工作表的所有資料列"
```
### 4.3 語言無限制原則
**零件開發語言完全不限制**,只要輸出符合以上合約的 .wasm 即可。
可接受語言(非排他):TinyGo、Rust、AssemblyScript、C/C++
注意事項(不是禁止,是要求自行驗證):
- TypeScript via Extism:會打包 QuickJS,體積通常超過 2MB 限制
- 標準 Go(非 TinyGo):runtime 過肥,通常超過體積限制
- 任何語言:不可在 .wasm 內部呼叫網路或檔案系統 syscall
**驗收標準只有一個:通過沙盒測試。** 語言是零件作者自己的事。
### 4.4 零件的前後端分類
| 類型 | 執行位置 | I/O | 範例 |
|------|----------|-----|------|
| 後端邏輯零件 | Workers/workerd/Wazero | JSON stdin/stdout | validate_json, http_request |
| 前端 UI 零件 | 瀏覽器 | HTML attributes / DOM events | u6u-btn, u6u-chart |
| **混合零件** | **禁止** | — | **強制拆成兩個** |
---
## 五、零件版本控制策略
### 5.1 命名規則
```
gsheets_get_entries ← 功能合約名稱(搜尋用,永遠存在)
gsheets_get_entries_v1 ← 第一個實作(慢但能用)
gsheets_get_entries_v2 ← 更快的實作(由另一個 AI/用戶提交)
```
### 5.2 穩定性標籤
Workflow 引用零件時可指定穩定性需求:
```
gsheets_get_entries → 預設 floating,AI 自動選最優版本
gsheets_get_entries@stable → 有更好版本時提示,人工確認才換
gsheets_get_entries@pinned:v1 → 版本凍結,宇宙怎麼演化都不影響
```
| 標籤 | 適用情境 | 更新行為 |
|------|----------|----------|
| `floating` | 一般企業應用 | AI 自動換成最優版本 |
| `stable` | 重要業務流程 | 有更好版本時提示,人工確認 |
| `pinned` | 工廠控制器、嵌入式設備 | 永遠不動,即使進入墓地也保留 .wasm |
### 5.3 淘汰機制
- AI 搜尋零件時,KBDB 依「成功率 × 速度 × 被調用次數」排序
- 連續 30 天無使用且評價下降 → Deprecated
- Deprecated 後繼續 90 天無復活 → 進墓地(從搜尋清單移除)
- **墓地的 .wasm 永遠保留**pinned 的 Workflow 永遠能拉到
---
## 六、零件製造指引書(給用戶 AI 的規範)
u6u 不限制誰來造零件,任何 AI(用戶自己的 Claude、GPT、本地模型)都可以。
但必須遵守此指引書,否則沙盒測試不過,無法上架。
### Step 1:理解介面合約
造零件前,先定義合約 YAML。
**零件只在乎輸入 JSON 和輸出 JSON,完全不管前端長什麼樣子。**
```
人類:我要一個可以查 Google Sheets 的零件
AI 的第一步:定義 input_schema 和 output_schema,不是寫程式
```
### Step 2:選擇開發語言
選擇你最熟悉的、能產出 WASI preview1 相容 .wasm 的語言。
建議:
- 小型邏輯零件(轉換、計算)→ TinyGo 或 AssemblyScript(體積小)
- 效能敏感零件 → Rust(生態最成熟)
- 任何語言都可以,只要通過合約限制
### Step 3:實作規則
```
✅ 只用 stdin 讀取輸入 JSON
✅ 只用 stdout 輸出結果 JSON
✅ 錯誤也用 stdout 輸出:{"error": "說明"},不要 panic/crash
✅ 無狀態:每次呼叫都是獨立的,不依賴上一次執行的結果
✅ 需要打外部 API?透過 host function 注入,不在 .wasm 裡自己發 HTTP
❌ 禁止網路 syscall
❌ 禁止檔案系統 syscall
❌ 禁止打包 runtimeQuickJS、Node.js 等)
❌ 禁止超過 2MB
```
### Step 4:本地測試方式
```bash
# 用任何 WASI runtime 本地測試
echo '{"spreadsheet_id":"abc","sheet_name":"Sheet1"}' | \
wasmtime gsheets_get_entries.wasm
# 預期輸出
{"rows":[...],"total":5}
```
### Step 5:提交審核
提交 `.wasm` + `component.contract.yaml`,系統自動執行:
1. 體積檢查(< 2MB
2. 冷啟動時間(< 50ms
3. Syscall 掃描(不能有網路/檔案系統呼叫)
4. Gherkin 測試(合約裡的所有 scenario 必須 100% 通過)
5. 多 runtime 相容測試(cf-workers / workerd / wazero
全部通過 → 上架進入零件宇宙,開始累積評價。
---
## 七、Cypher 編排語言
Workflow 使用擴展三元組語法描述執行邏輯:
```yaml
kind: Workflow
id: wf_query_attendance
triplets:
# 基本流程
- "START >> TRIGGERS >> step_receive"
- "step_receive >> IS_A >> component://webhook_receiver_v1"
# 條件分支
- "step_receive >> ON_SUCCESS >> step_validate"
- "step_receive >> ON_FAIL >> step_notify_error"
# 跨 Workflow 串接
- "step_validate >> CALLS_SUBFLOW >> workflow://save_to_db"
# 前端觸發後端
- "btn_submit >> ON_CLICK >> workflow://wf_query_attendance"
```
**URI 協議規範:**
- `component://` → 引用零件
- `workflow://` → 引用子 Workflow
- `ui://` → 引用前端零件
- `style://` → 引用樣式零件
---
## 八、KBDB 在 u6u 的角色
u6u 的所有狀態都在 KBDB 裡:
| KBDB Block 類型 | 存放內容 |
|-----------------|----------|
| Component Block | 零件合約、.wasm 位置、版本、評價指標 |
| Workflow Block | Cypher 三元組、依賴零件清單 |
| Prototype Block | 前端結構、UI 零件樹 |
| Pitfall Block | 避坑記錄,AI 搜尋時強制讀取 |
| Evaluation Block | 每次 Workflow 執行後的強制評價結果 |
**KBDB 不變量:永遠只有三張表(blocks/templates/slots),不新增表。**
所有以上類型都用 Template + Slot 實現。
---
## 九、自動演化迴圈
```
執行 Workflow
強制 AI 評價(Evaluator Agent
↓ 發現問題
生成修復 Ticket → 通知製作人
↓ AI 嘗試修復
通過 Gherkin 驗收 → 熱更新
↓ 無法修復
標記 [HAS_PITFALL] 到 Cypher 圖
下一個 AI 搜尋時讀到坑,強制繞道
```
---
## 十、現況與未來路徑
### 現在已有
- KBDBblocks/templates/slots + Vectorize
- IS-Squad MCPexecute_cypher 等工具)
- Cloudflare Workers 環境
### 最小可 demo 路徑
1. **Cypher 執行引擎**:三元組 → 實際執行順序(確認 execute_cypher 邊界)
2. **首批核心零件**5 個):
- `webhook_receiver`
- `json_transform`
- `http_request`(透過 host function
- `notify_line`
- `validate_json`
3. **機甲最小版本**:意圖 → 零件搜尋 → 組裝 Workflow(先用硬編碼路由)
4. **前端畫布 MVP**:靜態 HTML 模擬雙面翻轉體驗
### 技術監控項目
- **WASI Component Modelpreview2**:目前用 preview1,未來 3-5 年會有遷移壓力。
合約裡已有 `wasi_target: "preview1"` 標記,升級時知道要改什麼。
- **Kùzu 成熟度**:地端圖資料庫首選,持續觀察 v1.0 穩定性。
---
## 十一、實作決策原則(CC 行動準則)
遇到不確定的實作決策時,依序問自己:
1. **這個決策會影響零件合約嗎?** 如果是,停下來討論,不要自行決定。
2. **這個實作是否限制了未來換 runtime 的自由?** 如果是,重新設計介面。
3. **這個零件做超過一件事嗎?** 如果是,拆成兩個零件。
4. **這個設計在 Tier 3 離線環境能跑嗎?** 如果不能,重新考慮。
5. **有沒有現成零件可以組合?** 先搜尋 KBDB,不要重造輪子。
---
*本文件版本:v1.0*
*綜合自:u6u 系統與零件宇宙全景規劃白皮書、自動演化 ERP 架構藍圖、智慧前端與工匠開發藍圖,加入技術評論與補充建議。*

Some files were not shown because too many files have changed in this diff Show More