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:
@@ -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.wasm(stdin = 含 access_token 的完整 input)
|
|
||||||
↓
|
|
||||||
回傳結果
|
|
||||||
```
|
|
||||||
|
|
||||||
### credential-injector 實作位置
|
|
||||||
|
|
||||||
放在 `cypher-executor/src/actions/credential-injector.ts`(新增),由 `graph-executor.ts` 在每個節點執行前呼叫。
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
async function injectCredentials(
|
|
||||||
componentId: string,
|
|
||||||
input: Record<string, unknown>,
|
|
||||||
env: Env
|
|
||||||
): Promise<Record<string, unknown>> {
|
|
||||||
const contract = await loadContract(componentId) // 從 WASM_BUCKET 或本地讀取
|
|
||||||
if (!contract.credentials_required) return input
|
|
||||||
|
|
||||||
const enriched = { ...input }
|
|
||||||
for (const cred of contract.credentials_required) {
|
|
||||||
const record = await env.CREDENTIALS_KV.get(cred.key)
|
|
||||||
if (!record) {
|
|
||||||
throw new Error(
|
|
||||||
`缺少 credential: ${cred.key}\n修復:在 credentials.yaml 加入 ${cred.key} 後執行 acr creds push`
|
|
||||||
)
|
|
||||||
}
|
|
||||||
const { encrypted, iv } = JSON.parse(record)
|
|
||||||
enriched[cred.inject_as] = await decrypt(encrypted, iv, env.ENCRYPTION_KEY)
|
|
||||||
}
|
|
||||||
return enriched
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## workflow.yaml 解析設計
|
|
||||||
|
|
||||||
### CLI push 流程
|
|
||||||
|
|
||||||
```
|
|
||||||
acr push newsletter_subscribe.yaml
|
|
||||||
↓
|
|
||||||
yaml-parser.ts 讀取 workflow.yaml
|
|
||||||
↓
|
|
||||||
解析 flow[] 三元組 → triplets: [{subject, relation, object}]
|
|
||||||
驗證關係詞(拒絕 PIPE)
|
|
||||||
↓
|
|
||||||
POST cypher-executor /cypher/search → ExecutionGraph(節點 + 邊)
|
|
||||||
↓
|
|
||||||
合併 config: + ExecutionGraph → WorkflowDefinition
|
|
||||||
↓
|
|
||||||
PUT WEBHOOKS KV[workflow_name] = JSON.stringify(WorkflowDefinition)
|
|
||||||
↓
|
|
||||||
輸出 webhook URL
|
|
||||||
```
|
|
||||||
|
|
||||||
### workflow.yaml 格式(確認版)
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
name: newsletter_subscribe
|
|
||||||
description: 訂閱電子報,發感謝信並記錄到 GSheets
|
|
||||||
|
|
||||||
flow:
|
|
||||||
- "input >> 完成後 >> send_thanks"
|
|
||||||
- "input >> 完成後 >> save_to_sheet"
|
|
||||||
- "send_thanks >> 完成後 >> output"
|
|
||||||
- "send_thanks >> 失敗時 >> notify_error"
|
|
||||||
- "save_to_sheet >> 完成後 >> output"
|
|
||||||
|
|
||||||
config:
|
|
||||||
send_thanks:
|
|
||||||
to: "{{input.email}}"
|
|
||||||
subject: "感謝訂閱!"
|
|
||||||
body: "歡迎加入!"
|
|
||||||
# access_token 由 credentials.yaml 的 gmail_token 自動注入
|
|
||||||
|
|
||||||
save_to_sheet:
|
|
||||||
action: write
|
|
||||||
spreadsheet_id: "{{creds.sheet_id}}"
|
|
||||||
range: "訂閱者!A:B"
|
|
||||||
values: [["{{input.email}}", "{{input.timestamp}}"]]
|
|
||||||
|
|
||||||
notify_error:
|
|
||||||
chat_id: "{{creds.telegram_chat_id}}"
|
|
||||||
text: "發信失敗:{{input.email}}"
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## contract.yaml 補充欄位格式
|
|
||||||
|
|
||||||
### credentials_required(gmail 範例)
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
credentials_required:
|
|
||||||
- key: gmail_token
|
|
||||||
type: google_oauth
|
|
||||||
description: "Google OAuth access token(gmail.send scope)"
|
|
||||||
inject_as: access_token
|
|
||||||
```
|
|
||||||
|
|
||||||
### config_example(gmail 範例)
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
config_example: |
|
|
||||||
send_email: # 節點名稱(可自訂)
|
|
||||||
to: "" # 收件人 Email(必填)
|
|
||||||
subject: "" # 主旨(必填)
|
|
||||||
body: "" # 內文(必填)
|
|
||||||
# access_token 由 credentials.yaml 的 gmail_token 自動注入
|
|
||||||
```
|
|
||||||
|
|
||||||
### 各零件 credentials_required 對照表
|
|
||||||
|
|
||||||
| 零件 | key | type | inject_as |
|
|
||||||
|------|-----|------|-----------|
|
|
||||||
| gmail | gmail_token | google_oauth | access_token |
|
|
||||||
| google_sheets | google_oauth | google_oauth | access_token |
|
|
||||||
| telegram | telegram_bot_token | telegram_bot_token | bot_token |
|
|
||||||
| line_notify | line_token | line_token | token |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## CLI 技術設計
|
|
||||||
|
|
||||||
### 依賴
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"dependencies": {
|
|
||||||
"commander": "^12.0.0",
|
|
||||||
"js-yaml": "^4.1.0",
|
|
||||||
"chalk": "^5.3.0",
|
|
||||||
"ora": "^8.0.1"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### config.yaml 格式(~/.arcrun/config.yaml)
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
cloudflare_account_id: abc123
|
|
||||||
webhooks_kv_id: xyz789
|
|
||||||
credentials_kv_id: abc456
|
|
||||||
wasm_bucket: arcrun-wasm
|
|
||||||
cypher_executor_url: https://cypher-executor.xxx.workers.dev
|
|
||||||
credentials_worker_url: https://arcrun-credentials.xxx.workers.dev
|
|
||||||
api_token: ***(加密存本機)
|
|
||||||
```
|
|
||||||
|
|
||||||
### Cloudflare API 操作
|
|
||||||
|
|
||||||
CLI 使用 Cloudflare REST API(不依賴 Wrangler CLI):
|
|
||||||
- KV 寫入:`PUT /client/v4/accounts/{id}/storage/kv/namespaces/{ns_id}/values/{key}`
|
|
||||||
- KV 讀取:`GET /client/v4/accounts/{id}/storage/kv/namespaces/{ns_id}/values/{key}`
|
|
||||||
- KV 列出:`GET /client/v4/accounts/{id}/storage/kv/namespaces/{ns_id}/keys`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## wrangler.toml 變更對照
|
|
||||||
|
|
||||||
### 移除(InkStone 專屬)
|
|
||||||
|
|
||||||
```toml
|
|
||||||
# 全部移除:
|
|
||||||
[[services]]
|
|
||||||
binding = "KBDB"
|
|
||||||
service = "inkstone-kbdb-api"
|
|
||||||
|
|
||||||
[[services]]
|
|
||||||
binding = "REGISTRY"
|
|
||||||
service = "inkstone-component-registry"
|
|
||||||
|
|
||||||
[[services]]
|
|
||||||
binding = "CLINIC_GDRIVE"
|
|
||||||
service = "clinic-gdrive"
|
|
||||||
|
|
||||||
# ... CLINIC_EXCEL, CLINIC_ANALYSIS, CLINIC_RENDER, CLINIC_GSHEETS
|
|
||||||
|
|
||||||
[[services]]
|
|
||||||
binding = "AICEO"
|
|
||||||
service = "inkstone-aiceo-bot"
|
|
||||||
|
|
||||||
[[services]]
|
|
||||||
binding = "MINI_ME"
|
|
||||||
service = "inkstone-mini-me"
|
|
||||||
```
|
|
||||||
|
|
||||||
### 保留
|
|
||||||
|
|
||||||
```toml
|
|
||||||
[[kv_namespaces]]
|
|
||||||
binding = "EXEC_CONTEXT"
|
|
||||||
|
|
||||||
[[kv_namespaces]]
|
|
||||||
binding = "WEBHOOKS"
|
|
||||||
|
|
||||||
[[r2_buckets]]
|
|
||||||
binding = "WASM_BUCKET"
|
|
||||||
|
|
||||||
[ai]
|
|
||||||
binding = "AI"
|
|
||||||
```
|
|
||||||
|
|
||||||
### 新增
|
|
||||||
|
|
||||||
```toml
|
|
||||||
[[kv_namespaces]]
|
|
||||||
binding = "CREDENTIALS_KV"
|
|
||||||
id = "" # 用戶自行填入
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Standard 模式架構(用戶自己的 KV,arcrun.dev 的引擎)
|
|
||||||
|
|
||||||
### 儲存責任分界
|
|
||||||
|
|
||||||
```
|
|
||||||
arcrun.dev 負責:
|
|
||||||
WASM_BUCKET 公眾零件庫(.wasm 二進位)
|
|
||||||
ANALYTICS_KV 零件執行統計
|
|
||||||
ACCOUNTS_KV API Key → tenant_id + CF API Token 對應
|
|
||||||
SUBMISSIONS_KV 零件提交審核狀態
|
|
||||||
|
|
||||||
用戶自己負責(一個 CF KV,arcrun.dev 不存取明文):
|
|
||||||
USER_KV
|
|
||||||
workflow:{name} → workflow 執行圖(JSON)
|
|
||||||
cred:{key} → AES-GCM 加密 credential
|
|
||||||
```
|
|
||||||
|
|
||||||
### 完整系統圖
|
|
||||||
|
|
||||||
```
|
|
||||||
┌──────────────────────────────────────────────────────────────┐
|
|
||||||
│ arcrun.dev(你的 Cloudflare 帳號) │
|
|
||||||
│ │
|
|
||||||
│ auth-worker(api.arcrun.dev) │
|
|
||||||
│ POST /register → { api_key, tenant_id } │
|
|
||||||
│ ACCOUNTS_KV: { tenant_id, cf_api_token, api_key_hash } │
|
|
||||||
│ ※ 不儲存用戶 credential 或 workflow 內容 │
|
|
||||||
│ │
|
|
||||||
│ cypher-executor(cypher.arcrun.dev,共享) │
|
|
||||||
│ X-Arcrun-API-Key → tenant_id → cf_api_token │
|
|
||||||
│ 用 cf_api_token 呼叫 CF KV API → 讀用戶自己的 USER_KV │
|
|
||||||
│ WASM_BUCKET: gmail.wasm / telegram.wasm / ...(共享) │
|
|
||||||
│ │
|
|
||||||
│ public registry(registry.arcrun.dev) │
|
|
||||||
│ GET /components → 零件清單 + 統計 + author + visibility │
|
|
||||||
│ POST /submit → 接收零件,沙盒驗收後設 author_only │
|
|
||||||
│ POST /analytics/record → 執行統計(非同步) │
|
|
||||||
└──────────────────────────────────────────────────────────────┘
|
|
||||||
|
|
||||||
↕ CF KV API(用戶的 cf_api_token,KV Edit 權限)
|
|
||||||
|
|
||||||
┌──────────────────────────────────────────────────────────────┐
|
|
||||||
│ 用戶自己的 CF 帳號 │
|
|
||||||
│ USER_KV │
|
|
||||||
│ workflow:newsletter → { triplets, config } │
|
|
||||||
│ cred:gmail_token → { encrypted, iv } │
|
|
||||||
│ cred:telegram_bot → { encrypted, iv } │
|
|
||||||
└──────────────────────────────────────────────────────────────┘
|
|
||||||
```
|
|
||||||
|
|
||||||
### acr init 互動流程
|
|
||||||
|
|
||||||
```
|
|
||||||
$ acr init
|
|
||||||
|
|
||||||
? 你的 Cloudflare Account ID: abc123
|
|
||||||
? USER_KV Namespace ID(在 CF Dashboard 建立一個 KV 後貼上): kv_xyz
|
|
||||||
? CF API Token(只需 KV Edit 權限,arcrun 用此存取你的 KV): ***
|
|
||||||
? Email(取得 arcrun.dev API Key): you@example.com
|
|
||||||
|
|
||||||
→ 呼叫 POST https://api.arcrun.dev/register { email, cf_api_token_hash }
|
|
||||||
→ 取得 api_key: ak_xxxxxxxx
|
|
||||||
|
|
||||||
✓ 設定完成 → ~/.arcrun/config.yaml
|
|
||||||
✓ 建立 credentials.yaml(已加入 .gitignore)
|
|
||||||
|
|
||||||
你的 credential 與 workflow 存在你自己的 CF KV,arcrun 不會儲存它們。
|
|
||||||
```
|
|
||||||
|
|
||||||
### API Key 驗證與 KV 存取 Middleware
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// cypher-executor/src/lib/tenant.ts(新增)
|
|
||||||
export async function resolveUserKv(request: Request, env: Env) {
|
|
||||||
if (env.MULTI_TENANT === 'false') {
|
|
||||||
// Self-hosted:直接用本地 KV binding
|
|
||||||
return { kv: env.LOCAL_KV, prefix: '' }
|
|
||||||
}
|
|
||||||
|
|
||||||
const apiKey = request.headers.get('X-Arcrun-API-Key')
|
|
||||||
if (!apiKey) throw new Response('Missing API Key', { status: 401 })
|
|
||||||
|
|
||||||
const hash = await sha256(apiKey)
|
|
||||||
const account = await env.ACCOUNTS_KV.get(`hash:${hash}`)
|
|
||||||
if (!account) throw new Response('Invalid API Key', { status: 401 })
|
|
||||||
|
|
||||||
const { cf_api_token, account_id, kv_namespace_id } = JSON.parse(account)
|
|
||||||
|
|
||||||
// 回傳 CF KV API wrapper,用用戶自己的 token 存取
|
|
||||||
return {
|
|
||||||
kv: new CfKvClient({ cf_api_token, account_id, kv_namespace_id }),
|
|
||||||
prefix: ''
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### USER_KV Key Schema
|
|
||||||
|
|
||||||
```
|
|
||||||
Standard 模式(用戶自己的 KV):
|
|
||||||
workflow:{name} → WorkflowDefinition JSON
|
|
||||||
cred:{key} → { encrypted (base64), iv (base64) }
|
|
||||||
|
|
||||||
Self-hosted(本地 KV binding):
|
|
||||||
維持現有 key 格式,無 prefix
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 執行統計設計
|
|
||||||
|
|
||||||
### Analytics Record(非同步,不阻擋執行)
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// cypher-executor/src/actions/analytics.ts(新增)
|
|
||||||
export function recordExecution(
|
|
||||||
componentId: string,
|
|
||||||
version: string,
|
|
||||||
success: boolean,
|
|
||||||
durationMs: number
|
|
||||||
): void {
|
|
||||||
// fire-and-forget,不 await
|
|
||||||
fetch('https://registry.arcrun.dev/analytics/record', {
|
|
||||||
method: 'POST',
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
body: JSON.stringify({ canonical_id: componentId, version, success, duration_ms: durationMs })
|
|
||||||
}).catch(() => {}) // 統計失敗不影響執行
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 統計聚合(registry Worker)
|
|
||||||
|
|
||||||
```
|
|
||||||
ANALYTICS_KV 結構:
|
|
||||||
"stats:gmail:v1" → { total_runs: 140382, success_runs: 139444, total_ms: 16845840 }
|
|
||||||
|
|
||||||
每次 POST /analytics/record:
|
|
||||||
原子更新(KV 樂觀鎖)→ total_runs++, success_runs += success, total_ms += duration_ms
|
|
||||||
|
|
||||||
GET /components 回傳:
|
|
||||||
success_rate = success_runs / total_runs * 100
|
|
||||||
avg_duration_ms = total_ms / total_runs
|
|
||||||
排序:total_runs × success_rate(DESC)
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 零件貢獻流程設計
|
|
||||||
|
|
||||||
### `acr parts publish` 流程
|
|
||||||
|
|
||||||
```
|
|
||||||
$ acr parts publish gmail-v2
|
|
||||||
|
|
||||||
1. CLI 讀取 registry/components/gmail-v2/ 目錄
|
|
||||||
- component.contract.yaml(必須有 author 欄位)
|
|
||||||
- main.go
|
|
||||||
- gmail-v2.wasm
|
|
||||||
|
|
||||||
2. POST https://registry.arcrun.dev/submit
|
|
||||||
multipart form:
|
|
||||||
contract: <yaml content>
|
|
||||||
source: <main.go content>
|
|
||||||
wasm: <binary>
|
|
||||||
Header: X-Arcrun-API-Key: ak_xxxxx
|
|
||||||
|
|
||||||
3. registry 回應:
|
|
||||||
{ submission_id: "sub_abc123", status: "pending_review" }
|
|
||||||
|
|
||||||
4. CLI 輸出:
|
|
||||||
✓ 提交成功(submission_id: sub_abc123)
|
|
||||||
查詢進度:acr parts publish --status sub_abc123
|
|
||||||
```
|
|
||||||
|
|
||||||
### Registry 沙盒驗收與 visibility 狀態機
|
|
||||||
|
|
||||||
```
|
|
||||||
POST /submit 觸發(非同步執行):
|
|
||||||
|
|
||||||
[整合類零件:gmail、telegram、google_sheets、line_notify、http_request]
|
|
||||||
Step 1: 體積檢查(< 2048KB)
|
|
||||||
Step 2: syscall 掃描(無 filesystem syscall;網路 syscall 允許,因需呼叫外部 API)
|
|
||||||
通過 → visibility: author_only(作者立即可用,等人工審核)
|
|
||||||
|
|
||||||
[功能類零件:所有其他零件]
|
|
||||||
Step 1: 體積檢查(< 2048KB)
|
|
||||||
Step 2: 冷啟動時間(< 50ms)
|
|
||||||
Step 3: syscall 掃描(無網路 / 無 filesystem)
|
|
||||||
Step 4: Gherkin 測試(contract 中所有 scenario 100% 通過)
|
|
||||||
通過 → visibility: author_only(作者立即可用,等人工審核)
|
|
||||||
|
|
||||||
任一步驟失敗 → status: rejected(回傳 failed_step + reason)
|
|
||||||
|
|
||||||
人工審核通過 → visibility: public
|
|
||||||
- 零件出現在所有人的 GET /components
|
|
||||||
- 開始累積公開執行統計
|
|
||||||
|
|
||||||
人工審核拒絕 → visibility 維持 author_only
|
|
||||||
- 作者仍可使用,但收到拒絕原因
|
|
||||||
- 作者修改後可重新提交
|
|
||||||
|
|
||||||
acr parts 顯示規則:
|
|
||||||
visibility: author_only → [待審核] 只有你可用(不顯示統計)
|
|
||||||
visibility: public → ★ 成功率 | N 次執行 | by @author
|
|
||||||
|
|
||||||
任一失敗 → status: rejected
|
|
||||||
- 回傳 { failed_step, reason },格式與 Requirement 2 相同
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 開發順序(Phase 對齊 requirements)
|
|
||||||
|
|
||||||
```
|
|
||||||
Phase 1:搬移與清理(Requirement 1)
|
|
||||||
1.1 建立 arcrun repo,搬移四個目錄
|
|
||||||
1.2 清理 cypher-executor/wrangler.toml
|
|
||||||
1.3 改寫 component-loader(移除 KBDB/REGISTRY/MINI_ME 路徑)
|
|
||||||
1.4 移除 autoPublishMissing.ts(依賴 REGISTRY binding)
|
|
||||||
1.5 本機 wrangler dev 測試 /health
|
|
||||||
|
|
||||||
Phase 2:零件完整度(Requirement 2)
|
|
||||||
2.1 審查 21 個零件 contract.yaml(表格回報)
|
|
||||||
2.2 補充 credentials_required(4 個零件)
|
|
||||||
2.3 補充 config_example(全部 21 個)
|
|
||||||
2.4 驗證 main.go required 與 contract 一致
|
|
||||||
|
|
||||||
Phase 3:credential 注入(Requirement 3)
|
|
||||||
3.1 新增 credential-injector.ts
|
|
||||||
3.2 整合進 graph-executor 節點執行前
|
|
||||||
3.3 測試 gmail 零件端對端(credentials.yaml → push → run)
|
|
||||||
|
|
||||||
Phase 4:CLI(Requirement 4)
|
|
||||||
4.1 acr init(--hosted / --self-hosted 分支)
|
|
||||||
4.2 acr creds push(Hosted 走 API,Self-hosted 走 KV)
|
|
||||||
4.3 acr push
|
|
||||||
4.4 arcrun run
|
|
||||||
4.5 acr validate
|
|
||||||
4.6 acr parts / acr parts scaffold / acr parts publish
|
|
||||||
4.7 acr list / acr logs
|
|
||||||
|
|
||||||
Phase 5:開源發布(Requirement 5)
|
|
||||||
5.1 撰寫 README.md(含 --hosted 快速開始)
|
|
||||||
5.2 撰寫 CONTRIBUTING.md
|
|
||||||
5.3 確認無 InkStone 內部資訊殘留
|
|
||||||
5.4 GitHub 發布 + npm publish
|
|
||||||
|
|
||||||
Phase 6:Hosted SaaS(Requirement 6)
|
|
||||||
6.1 建立 auth-worker(api.arcrun.dev)
|
|
||||||
6.2 cypher-executor 加入 tenant middleware
|
|
||||||
6.3 CREDENTIALS_KV key schema 加 tenant prefix
|
|
||||||
6.4 部署至 arcrun.dev
|
|
||||||
|
|
||||||
Phase 7:統計與貢獻(Requirement 7 + 8)
|
|
||||||
7.1 analytics.ts(執行後 fire-and-forget)
|
|
||||||
7.2 registry /analytics/record 端點
|
|
||||||
7.3 ANALYTICS_KV 聚合邏輯
|
|
||||||
7.4 GET /components 加入統計排序
|
|
||||||
7.5 POST /submit 沙盒驗收 + author 寫入
|
|
||||||
7.6 acr parts publish 指令
|
|
||||||
```
|
|
||||||
@@ -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 2:component.contract.yaml 完整度補充
|
|
||||||
|
|
||||||
**User Story:** As a 零件使用者, I want 每個零件的 contract.yaml 都有 `credentials_required` 與 `config_example`, so that CLI 能自動注入 credential,用戶也能快速知道如何設定節點。
|
|
||||||
|
|
||||||
#### Acceptance Criteria
|
|
||||||
|
|
||||||
1. THE `credentials_required` 欄位 SHALL 出現在以下 4 個零件的 contract.yaml 中:`gmail`、`google_sheets`、`telegram`、`line_notify`。
|
|
||||||
2. WHEN `credentials_required` 存在,THE 欄位 SHALL 包含以下子欄位:`key`(對應 credentials.yaml 的 key 名稱)、`type`(token 類型,如 `google_oauth`、`telegram_bot_token`)、`description`(說明)、`inject_as`(執行時注入到 input 的哪個欄位名稱)。
|
|
||||||
3. THE `config_example` 欄位 SHALL 出現在所有 21 個零件的 contract.yaml 中。
|
|
||||||
4. WHEN `config_example` 存在,THE 欄位 SHALL 為 YAML 字串,內容為可直接貼入 workflow.yaml `config:` 區塊的範本,需有人類可讀的說明註解。
|
|
||||||
5. FOR 需要 credential 的零件,THE `config_example` SHALL 包含一行註解,說明哪個 credential key 會被自動注入到哪個欄位(如 `# access_token 由 credentials.yaml 的 gmail_token 自動注入`)。
|
|
||||||
6. THE main.go 的 `required` 欄位 SHALL 與 contract 的 `input_schema.required[]` 保持一致,不得有欄位名稱不符。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Requirement 3:workflow YAML 格式與執行時 credential 注入
|
|
||||||
|
|
||||||
**User Story:** As a 工作流設計者, I want 用有語意的關係詞撰寫 workflow.yaml,且 credential 自動注入, so that workflow 定義中完全不出現明文 token。
|
|
||||||
|
|
||||||
#### Acceptance Criteria
|
|
||||||
|
|
||||||
1. THE workflow.yaml `flow:` 欄位 SHALL 以 `"A >> 關係詞 >> B"` 三元組陣列描述資料流。
|
|
||||||
2. THE cypher-executor SHALL 支援以下關係詞:`完成後`、`失敗時`、`對每個`、`條件滿足時`、`ON_SUCCESS`、`ON_FAIL`、`FOREACH`、`IF`、`ON_CLICK`、`CALLS_SUBFLOW`。
|
|
||||||
3. THE cypher-executor SHALL 拒絕使用 `PIPE` 關係詞,並回傳明確錯誤訊息。
|
|
||||||
4. WHEN cypher-executor 執行一個節點,THE cypher-executor SHALL 查詢該節點對應零件的 `credentials_required`,若存在則從 `CREDENTIALS_KV` 解密對應 credential,並注入到 input 的 `inject_as` 欄位。
|
|
||||||
5. THE credential 注入 SHALL 發生在 WASM 執行前,用戶的 workflow `config:` 中不需也不應包含 token 值。
|
|
||||||
6. IF `credentials_required` 宣告的 credential key 在 `CREDENTIALS_KV` 中不存在,THE cypher-executor SHALL 回傳結構化錯誤,包含缺少的 key 名稱與修復步驟說明。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Requirement 4:CLI(arcrun,指令 acr)核心指令
|
|
||||||
|
|
||||||
**User Story:** As a 開發者, I want 透過 `acr` CLI 管理 workflow 與 credentials, so that 不需要直接操作 Cloudflare KV / R2 API 就能完成部署與執行。
|
|
||||||
|
|
||||||
#### Acceptance Criteria
|
|
||||||
|
|
||||||
1. THE CLI SHALL 以 Node.js/TypeScript 實作,套件名 `arcrun`,bin 名 `acr`,可透過 `npm i -g arcrun` 安裝,依賴只使用 `commander`、`js-yaml`、`chalk`、`ora`。
|
|
||||||
2. THE `acr init` 指令 SHALL 以互動式問答產生 `~/.arcrun/config.yaml`,問答內容為:CF Account ID、USER_KV namespace ID、CF API Token(用於 cypher-executor 代存取用戶 KV)、email(取得 arcrun.dev API Key);並建立空白本機 `credentials.yaml`。
|
|
||||||
3. THE `acr creds push [credentials.yaml]` 指令 SHALL 讀取 credentials.yaml,逐一加密上傳至用戶自己的 USER_KV,並顯示每個 key 的上傳結果。
|
|
||||||
4. THE `acr push <workflow.yaml>` 指令 SHALL 解析 `flow:` 三元組,轉換成執行圖,連同 `config:` 存入 `WEBHOOKS KV`,並輸出 webhook URL。
|
|
||||||
5. THE `acr run <workflow_name> [--input key=value...]` 指令 SHALL 觸發 cypher-executor 執行指定 workflow,顯示各節點執行結果;失敗時顯示具體節點、原因與修復步驟。
|
|
||||||
6. THE `acr validate <workflow.yaml>` 指令 SHALL 在執行前驗證:YAML 格式、關係詞合法性(無 PIPE)、所有節點在 config 中有對應、所有零件存在於 WASM_BUCKET、所有 credentials 已上傳至 CREDENTIALS_KV。
|
|
||||||
7. THE `acr parts` 指令 SHALL 列出所有可用零件(按類型分組),顯示每個零件的必填欄位與所需 credential。
|
|
||||||
8. THE `acr parts scaffold <component>` 指令 SHALL 從 contract 的 `config_example` 輸出可直接貼入 workflow.yaml 的 config 範本,以及對應的 credentials.yaml 欄位範本。
|
|
||||||
9. THE `acr list` 指令 SHALL 列出 WEBHOOKS KV 中所有已上傳的 workflow,顯示名稱與更新時間。
|
|
||||||
10. THE `acr logs <workflow_name>` 指令 SHALL 顯示最近執行記錄,包含時間、成功/失敗狀態、執行時間,失敗時顯示失敗節點與原因。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Requirement 5:README 與開源發布準備
|
|
||||||
|
|
||||||
**User Story:** As a 外部開發者, I want 看到清楚的 README,5 分鐘內能完成部署, so that 降低試用門檻,吸引社群貢獻。
|
|
||||||
|
|
||||||
#### Acceptance Criteria
|
|
||||||
|
|
||||||
1. THE README.md SHALL 包含以下章節:專案定位(開源核心 vs 閉源付費服務說明)、快速開始(`acr init` → `acr creds push` → `acr push` → `acr run` 四步驟)、零件列表(21 個零件分類說明)、workflow YAML 語法說明(三元組 + 關係詞表格)、自行部署說明(Cloudflare Workers 部署步驟)。
|
|
||||||
2. THE README.md 快速開始 SHALL 以 `newsletter_subscribe` 為範例 workflow,展示 gmail + google_sheets + telegram 的完整串接。
|
|
||||||
3. THE repo SHALL 包含 `CONTRIBUTING.md`,說明如何新增零件(TinyGo 開發環境、contract.yaml 格式、本機測試指令)。
|
|
||||||
4. THE repo SHALL 確保所有 InkStone 內部資訊(Worker URL、KV namespace ID、帳號資訊)不出現在任何已提交的檔案中。
|
|
||||||
5. WHEN cypher-executor 部署後第一次被呼叫,THE cypher-executor SHALL 能正常回應 health check(`GET /health` 回傳 `{ ok: true }`),不需要任何 InkStone 服務可用。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Requirement 6:Standard 模式 — API Key 註冊與用戶 KV 存取
|
|
||||||
|
|
||||||
**User Story:** As a 新用戶, I want 只需開一個 CF KV 就能開始使用 Arcrun,不需要部署任何 Worker, so that 最低門檻試用整個平台,且我的 credential 永遠在我自己的環境。
|
|
||||||
|
|
||||||
#### Acceptance Criteria
|
|
||||||
|
|
||||||
1. THE auth-worker SHALL 提供 `POST /register` 端點,接受 `{ email }` 後自動生成 API Key(格式:`ak_` 前綴 + 32 字元隨機字串),無需人工審核,立即回傳 `{ api_key, tenant_id }`。
|
|
||||||
2. THE auth-worker SHALL 將 `{ tenant_id, email, created_at, api_key_hash }` 存入 `ACCOUNTS_KV`,只存 hash 不存明文 API Key。arcrun.dev 不儲存任何用戶 credential 或 workflow 內容。
|
|
||||||
3. WHEN `acr init` 執行,THE CLI SHALL 互動式詢問以下資料並寫入 `~/.arcrun/config.yaml`:
|
|
||||||
- CF Account ID(用戶自己的)
|
|
||||||
- USER_KV namespace ID(用戶自己開的,存 credential + workflow)
|
|
||||||
- CF API Token(供 cypher-executor 用 CF API 存取用戶 KV,只需 KV Edit 權限)
|
|
||||||
- email(呼叫 `POST https://api.arcrun.dev/register` 取得 API Key)
|
|
||||||
4. THE cypher-executor SHALL 在每個 request 的 header 讀取 `X-Arcrun-API-Key`,驗證後取得該 tenant 的 CF API Token,用 Cloudflare API 存取用戶自己的 USER_KV;缺少或無效的 API Key 回傳 `401 Unauthorized`。
|
|
||||||
5. THE `acr creds push` 指令 SHALL 使用用戶的 CF API Token,直接呼叫 Cloudflare KV API 將加密 credential 寫入用戶自己的 USER_KV,不經過 arcrun.dev。
|
|
||||||
6. THE `acr push <workflow.yaml>` 指令 SHALL 同樣直接寫入用戶自己的 USER_KV,不經過 arcrun.dev。
|
|
||||||
7. WHEN Self-hosted 模式,THE cypher-executor SHALL 可透過環境變數 `MULTI_TENANT=false` 停用 API Key 驗證,直接使用本地 KV binding,與現有行為相容。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Requirement 7:公眾零件庫執行統計與貢獻榮譽
|
|
||||||
|
|
||||||
**User Story:** As a 零件使用者, I want 在 `acr parts` 看到每個零件的真實執行統計與作者資訊, so that 我能選擇最可靠的零件;As a 零件貢獻者, I want 我的名字和統計數字公開顯示, so that 我有動機將好零件推入公眾庫而非留在私庫。
|
|
||||||
|
|
||||||
#### Acceptance Criteria
|
|
||||||
|
|
||||||
1. THE contract.yaml SHALL 新增可選欄位 `author`(GitHub username,如 `@alice`),在 `acr parts` 顯示時一起展示。
|
|
||||||
2. WHEN cypher-executor 執行完一個零件節點,THE cypher-executor SHALL 非同步 POST 以下資料至 `https://registry.arcrun.dev/analytics/record`,不阻擋主流程:
|
|
||||||
```json
|
|
||||||
{ "canonical_id": "gmail", "version": "v1", "success": true, "duration_ms": 120 }
|
|
||||||
```
|
|
||||||
不含任何用戶資料或 tenant_id。
|
|
||||||
3. THE public registry SHALL 聚合每個零件的執行統計:`total_runs`(總執行次數)、`success_rate`(成功率,百分比)、`avg_duration_ms`(平均執行時間)。
|
|
||||||
4. THE `acr parts` 指令 SHALL 顯示每個零件的統計資料,格式為:
|
|
||||||
```
|
|
||||||
• gmail Gmail 發信 by @alice
|
|
||||||
★ 99.2% 成功 | 140,382 次執行 | 平均 120ms
|
|
||||||
```
|
|
||||||
5. IF 零件存在於用戶自架的私有 WASM_BUCKET 而非公眾庫,THE `acr parts` SHALL 顯示該零件但標註 `[私有]`,不顯示統計數字與 author。
|
|
||||||
6. THE public registry SHALL 在 `GET /components` 回傳的零件清單中,依 `total_runs × success_rate` 排序,讓高品質高使用量的零件排在前面。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Requirement 8:零件貢獻流程與 visibility 狀態
|
|
||||||
|
|
||||||
**User Story:** As a 零件開發者, I want 提交零件後立即能自己使用,等審核通過後公開給所有人, so that 不用等待審核就能驗證自己的零件是否有用。
|
|
||||||
|
|
||||||
#### Acceptance Criteria
|
|
||||||
|
|
||||||
1. THE contract.yaml SHALL 包含 `visibility` 欄位,值為 `author_only`(沙盒通過後作者立即可用)或 `public`(人工審核通過後所有人可用)。
|
|
||||||
2. THE `acr parts publish <component>` 指令 SHALL 打包指定零件的原始碼、`component.contract.yaml`、`.wasm`,POST 至 `https://registry.arcrun.dev/submit`(帶 `X-Arcrun-API-Key` header)。原始碼語言不限,但編譯產出必須為 WASM + WASI preview1。
|
|
||||||
3. WHEN 零件提交後,THE registry SHALL 依零件類型執行不同層級的沙盒驗收:
|
|
||||||
- **整合類**(需呼叫外部 API,如 gmail、telegram):體積 / syscall 掃描通過 → `author_only`
|
|
||||||
- **功能類**(純邏輯,如 string_ops、if_control):體積 / syscall 掃描 / Gherkin 測試全通過 → `author_only`
|
|
||||||
- 任一必要步驟失敗 → `rejected`(回傳具體失敗步驟與原因)
|
|
||||||
4. WHEN 零件 visibility 為 `author_only`,THE registry SHALL 讓該零件只對提交者的 API Key 可見,`acr parts` 顯示時標註 `[待審核]`,其他用戶看不到。
|
|
||||||
5. WHEN 人工審核通過,THE registry SHALL 將 visibility 改為 `public`,零件立即出現在所有人的 `acr parts` 清單,並開始累積公開執行統計。
|
|
||||||
6. WHEN 審核拒絕,THE registry SHALL 回傳具體失敗原因,零件保留 `author_only` 狀態讓作者繼續修改後重新提交。
|
|
||||||
7. THE `acr parts publish` 指令 SHALL 在提交後顯示 `submission_id`、目前 visibility 狀態,以及查詢審核進度的指令提示。
|
|
||||||
8. THE `acr parts` 指令 SHALL 對 `author_only` 零件顯示「[待審核] 只有你可用」,對 `public` 零件顯示執行統計與 author,讓貢獻者清楚知道零件的可用範圍。
|
|
||||||
@@ -1,206 +0,0 @@
|
|||||||
# Implementation Plan: arcrun MVP
|
|
||||||
|
|
||||||
## Overview
|
|
||||||
|
|
||||||
依照 Design 的七個 Phase 實作。原則:最小異動,不重寫現有邏輯,只 cherry-pick + carve-out + supplement。
|
|
||||||
所有 Phase 1–3 工作在 `matrix` repo 對應目錄驗證後再搬到新 repo。
|
|
||||||
|
|
||||||
**PR #2(claude/review-mvp-specs-8Bvdu)狀態:** 初始實作已提交,已修復以下問題後準備 merge:
|
|
||||||
- CF API Token 傳至 arcrun.dev 安全問題(已修復)
|
|
||||||
- 加密 fallback 格式不相容(已修復)
|
|
||||||
- submitComponent KBDB 依賴(已修復,改用 SUBMISSIONS_KV)
|
|
||||||
- Webhook 路由缺 analytics(已修復)
|
|
||||||
- `require()` 在 ES module 中(已修復)
|
|
||||||
- api 類零件 `no_network_syscall: true` 錯誤(已修復)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Phase 1:搬移與清理
|
|
||||||
|
|
||||||
- [x] 1. 建立 `arcrun` 獨立 repo 並初始化
|
|
||||||
- [x] 1.1 在 GitHub 建立新的 public repo(使用 matrix monorepo 的 `arcrun/` 子目錄代替,PR #2)
|
|
||||||
- [x] 1.2 設定 `.gitignore`(排除 `node_modules/`、`.wrangler/`、`credentials.yaml`、`~/.arcrun/`)
|
|
||||||
- [x] 1.3 從 `matrix` cherry-pick 四個目錄:
|
|
||||||
- `matrix/cypher-executor/` → `arcrun/cypher-executor/`
|
|
||||||
- `matrix/u6u-core/credentials/` → `arcrun/credentials/`
|
|
||||||
- `matrix/u6u-core/registry/components/` → `arcrun/registry/components/`
|
|
||||||
- _Requirements: 1.6_
|
|
||||||
|
|
||||||
- [x] 2. 清理 `cypher-executor/wrangler.toml`
|
|
||||||
- [x] 2.1 移除 9 個 InkStone Service Bindings(KBDB、REGISTRY、CLINIC_*、AICEO、MINI_ME)
|
|
||||||
- [x] 2.2 確認保留:`EXEC_CONTEXT`、`WEBHOOKS`、`WASM_BUCKET`、`AI`
|
|
||||||
- [x] 2.3 新增 `CREDENTIALS_KV` 與 `ANALYTICS_KV` KV namespace binding
|
|
||||||
- [x] 2.4 更新 `name` 為 `arcrun-cypher-executor`
|
|
||||||
- _Requirements: 1.1, 1.2, 1.3_
|
|
||||||
|
|
||||||
- [x] 3. 改寫 `cypher-executor/src/lib/component-loader.ts`
|
|
||||||
- [x] 3.1 移除對 MINI_ME、KBDB、InkStone bindings 的 hardcode
|
|
||||||
- [x] 3.2 實作三層邏輯:builtin Map → WASM_BUCKET R2 直讀 → 結構化錯誤
|
|
||||||
- _Requirements: 1.4, 1.5_
|
|
||||||
|
|
||||||
- [x] 4. 移除對 InkStone bindings 的依賴程式碼
|
|
||||||
- [x] 4.1 刪除 `autoPublishMissing.ts`(依賴 REGISTRY binding)
|
|
||||||
- [x] 4.2 移除所有 `env.KBDB`、`env.REGISTRY`、`env.MINI_ME`、`env.AICEO`、`env.CLINIC_*` 引用
|
|
||||||
- _Requirements: 1.1, 1.5_
|
|
||||||
|
|
||||||
- [ ] 5. 本機驗證
|
|
||||||
- [ ] 5.1 `cd arcrun/cypher-executor && wrangler dev` 能啟動(無 binding 錯誤)
|
|
||||||
- [ ] 5.2 `GET /health` 回傳 `{ ok: true }`
|
|
||||||
- [ ] 5.3 上傳 `validate_json.wasm` 到 WASM_BUCKET,執行 `POST /execute` 能正常回傳結果
|
|
||||||
- _Requirements: 1.5, 5.5_
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Phase 2:零件完整度補充
|
|
||||||
|
|
||||||
- [x] 6. api 類零件 `no_network_syscall` 修正
|
|
||||||
- [x] 6.1 gmail、telegram、google_sheets、line_notify、http_request 改為 `no_network_syscall: false`
|
|
||||||
- _Requirements: 2.1_
|
|
||||||
|
|
||||||
- [ ] 7. 審查 21 個零件 contract.yaml 並補充 `credentials_required`
|
|
||||||
- [ ] 7.1 確認 gmail、google_sheets、telegram、line_notify 有 `credentials_required`(PR #2 已加入,需驗證格式正確)
|
|
||||||
- [ ] 7.2 確認所有 21 個零件有 `config_example` 欄位
|
|
||||||
- [ ] 7.3 驗證 `main.go` required 欄位與 `contract.yaml` input_schema.required[] 一致
|
|
||||||
- _Requirements: 2.1, 2.2, 2.3_
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Phase 3:Credential 注入整合
|
|
||||||
|
|
||||||
- [x] 10. `credential-injector.ts` 已實作(`arcrun/cypher-executor/src/actions/credential-injector.ts`)
|
|
||||||
- [x] 10.1 讀取 contract.yaml from R2,解析 `credentials_required`
|
|
||||||
- [x] 10.2 從 `CREDENTIALS_KV` 讀取 AES-GCM 加密 token,注入到 input 對應欄位(inject_as)
|
|
||||||
- [x] 10.3 credential 不存在時拋出結構化錯誤(含 key 名稱與修復步驟)
|
|
||||||
- _Requirements: 3.4, 3.5, 3.6_
|
|
||||||
|
|
||||||
- [ ] 11. 驗證 credential 注入整合進 graph-executor
|
|
||||||
- [ ] 11.1 確認 `graph-executor.ts` 在節點執行前正確呼叫 `injectCredentials`
|
|
||||||
- [ ] 11.2 確認注入只影響 WASM input,不修改 WEBHOOKS KV 中儲存的 workflow 定義
|
|
||||||
- _Requirements: 3.4, 3.5_
|
|
||||||
|
|
||||||
- [ ] 12. 端對端測試(手動)
|
|
||||||
- [ ] 12.1 建立 `credentials.yaml`,加入測試 token
|
|
||||||
- [ ] 12.2 執行 `acr creds push`,確認寫入 CREDENTIALS_KV 格式為 `{ encrypted, iv }`(無 `mode: 'base64'`)
|
|
||||||
- [ ] 12.3 執行含 credential 的 workflow,確認 inject_as 欄位正確注入
|
|
||||||
- _Requirements: 3.4, 3.5_
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Phase 4:CLI 開發
|
|
||||||
|
|
||||||
- [x] 13. CLI 專案骨架已建立(`arcrun/cli/`)
|
|
||||||
- [x] 13.1 `package.json`(name: `arcrun`,bin: `acr`)
|
|
||||||
- [x] 13.2 `tsconfig.json`(module: NodeNext)
|
|
||||||
- [x] 13.3 所有 10 個指令已實作骨架
|
|
||||||
- _Requirements: 4.1_
|
|
||||||
|
|
||||||
- [x] 14. `acr init` 已實作,修正項:
|
|
||||||
- [x] 14.1 Standard 模式不再傳送 `cf_api_token` 至 arcrun.dev(只傳 `email`)
|
|
||||||
- [x] 14.2 `require()` 改用 `await import()` 修正 ES module 相容
|
|
||||||
- [ ] 14.3 **待補**:`acr init` 需詢問 `ARCRUN_ENCRYPTION_KEY` 並寫入 config(目前加密 key 需手動設定)
|
|
||||||
- _Requirements: 4.2, 6.3_
|
|
||||||
|
|
||||||
- [x] 15. `acr creds push` 已實作
|
|
||||||
- [x] 15.1 讀取 `credentials.yaml`,AES-GCM 加密後寫入用戶 CF KV(`cred:{name}`)
|
|
||||||
- [x] 15.2 加密 fallback(base64)已移除,key 不足時直接拋錯提示生成指令
|
|
||||||
- _Requirements: 4.3, 6.5_
|
|
||||||
|
|
||||||
- [x] 16. `acr push` 已實作
|
|
||||||
- _Requirements: 4.4_
|
|
||||||
|
|
||||||
- [x] 17. `acr run` 已實作
|
|
||||||
- _Requirements: 4.5_
|
|
||||||
|
|
||||||
- [ ] 18. `acr validate` credential 檢測邏輯有誤,需修復
|
|
||||||
- [ ] 18.1 `extractCredentialRefs()` 目前掃描 `{{creds.xxx}}` 語法,但 injection 使用 `inject_as` key
|
|
||||||
- [ ] 18.2 改為讀取 contract.yaml 的 `credentials_required[].key`,與 `cred:{key}` KV 存在性比對
|
|
||||||
- _Requirements: 4.6_
|
|
||||||
|
|
||||||
- [x] 19. `acr parts`、`acr parts scaffold`、`acr parts publish` 已實作
|
|
||||||
- [ ] 19.1 `acr parts` 中 YAML 解析改用 `js-yaml`(目前用 regex,可能解析失敗)
|
|
||||||
- _Requirements: 4.7, 4.8_
|
|
||||||
|
|
||||||
- [x] 20. `acr list` 與 `acr logs` 已實作
|
|
||||||
- _Requirements: 4.9, 4.10_
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Phase 5:開源發布準備
|
|
||||||
|
|
||||||
- [x] 21. README.md 已撰寫(`arcrun/README.md`)
|
|
||||||
- [x] 22. CONTRIBUTING.md 已撰寫(`arcrun/CONTRIBUTING.md`)
|
|
||||||
- [ ] 23. 安全審查(PR merge 前執行)
|
|
||||||
- [ ] 23.1 搜尋 `.workers.dev` InkStone 網域
|
|
||||||
- [ ] 23.2 確認 wrangler.toml 所有 KV id 欄位留空
|
|
||||||
- [ ] 23.3 確認 `credentials.yaml` 在 `.gitignore` 中
|
|
||||||
- _Requirements: 5.4_
|
|
||||||
|
|
||||||
- [ ] 24. 發布(安全審查後)
|
|
||||||
- [ ] 24.1 `npm publish`(CLI package `arcrun`)
|
|
||||||
- _Requirements: 5.1_
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Phase 6:Standard 模式 — auth-worker 與用戶 KV 代存取
|
|
||||||
|
|
||||||
- [ ] 25. 建立 `auth-worker`(新 Worker,部署至 `api.arcrun.dev`)
|
|
||||||
- [ ] 25.1 建立 `auth-worker/` 目錄,初始化 Hono + wrangler.toml
|
|
||||||
- [ ] 25.2 實作 `POST /register`:接收 `{ email, account_id, kv_namespace_id }` + CF API Token 透過 header 傳入
|
|
||||||
- **不在 request body 中接收 CF API Token**(Token 透過 header `CF-Api-Token` 傳入,減少 TLS 以外的洩漏面)
|
|
||||||
- 生成 `tenant_id` 與 `api_key`,存入 `ACCOUNTS_KV`
|
|
||||||
- [ ] 25.3 Bindings:`ACCOUNTS_KV`
|
|
||||||
- _Requirements: 6.1, 6.2_
|
|
||||||
|
|
||||||
- [ ] 26. 改造 `cypher-executor` 支援 multi-tenant 用戶 KV 代存取
|
|
||||||
- [ ] 26.1 讀取 `MULTI_TENANT` env var(目前已宣告但未讀取),實作 tenant middleware
|
|
||||||
- [ ] 26.2 `X-Arcrun-API-Key` → 查 `ACCOUNTS_KV` → 取得用戶 cf_api_token + kv_namespace_id → 建立 `CfKvClient`
|
|
||||||
- [ ] 26.3 `CfKvClient` 已實作(`arcrun/cli/src/lib/cf-api.ts`),需移植到 `cypher-executor/src/lib/`
|
|
||||||
- [ ] 26.4 `credential-injector.ts` 改用 userKv 取得加密 credential
|
|
||||||
- [ ] 26.5 webhook 路由注入 userKv
|
|
||||||
- _Requirements: 6.4, 6.5, 6.6_
|
|
||||||
|
|
||||||
- [ ] 27. 端對端測試(用戶 KV 隔離)
|
|
||||||
- _Requirements: 6.4, 6.5_
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Phase 7:公眾零件統計與貢獻審核
|
|
||||||
|
|
||||||
- [x] 28. Analytics 基礎設施已建立
|
|
||||||
- [x] 28.1 `execution-logger.ts` 建立,`writeExecutionVerdict` 寫入 `ANALYTICS_KV`(fire-and-forget)
|
|
||||||
- [x] 28.2 `/execute` 路由已整合 `waitUntil(writeExecutionVerdict(...))`
|
|
||||||
- [x] 28.3 `/webhooks/:token/trigger` 路由已補上 `waitUntil(writeExecutionVerdict(...))`
|
|
||||||
- _Requirements: 7.2_
|
|
||||||
|
|
||||||
- [ ] 29. registry Worker analytics 端點
|
|
||||||
- [ ] 29.1 新增 `POST /analytics/record` 路由,原子更新 `ANALYTICS_KV`
|
|
||||||
- [ ] 29.2 `GET /components` 回傳加入 `total_runs`、`success_rate`、`avg_duration_ms`
|
|
||||||
- _Requirements: 7.3, 7.6_
|
|
||||||
|
|
||||||
- [x] 30. `author` 欄位已加入 contract.yaml 規格
|
|
||||||
- _Requirements: 7.1_
|
|
||||||
|
|
||||||
- [x] 31. 零件提交審核流程已實作(`arcrun/registry/src/actions/submitComponent.ts`)
|
|
||||||
- [x] 31.1 沙盒驗收流程(sandboxAcceptance.ts):size_check + syscall_scan 已實作;cold_start + gherkin_tests 為 Phase 0 mock
|
|
||||||
- [x] 31.2 `SUBMISSIONS_KV` 儲存元數據,預設 `visibility: author_only`
|
|
||||||
- [ ] 31.3 `PATCH /submit/:id/approve` → 將 visibility 改為 `public`(待實作)
|
|
||||||
- [ ] 31.4 Gherkin 測試執行(取代 mock)
|
|
||||||
- _Requirements: 8.2, 8.3, 8.4, 8.5_
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 待辦(無相依順序,可平行處理)
|
|
||||||
|
|
||||||
- [ ] A. `builtins/` 清理:`initComponents.ts` 仍用舊的 HTTP endpoint 模式上架零件(`buildComponentDefs` 含 URL),應改為呼叫 `POST /submit` 送 WASM binary + contract,或直接移除 builtins(功能已整合到 registry)
|
|
||||||
- [ ] B. `validate` 指令 credential 檢測邏輯修復(見 Phase 4 Task 18)
|
|
||||||
- [ ] C. `acr init` 加入 `ARCRUN_ENCRYPTION_KEY` 設定步驟
|
|
||||||
- [ ] D. `acr parts` YAML 解析改用 `js-yaml`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Notes
|
|
||||||
|
|
||||||
- 標記 `*` 的子任務為選填,可跳過以加速 MVP 交付
|
|
||||||
- Gherkin 測試執行(sandbox 步驟 d)為 Phase 0 mock,Phase 7 補充
|
|
||||||
- cold-start 測量(sandbox 步驟 b)為 Phase 0 mock,Phase 2 補充
|
|
||||||
- CF API Token 永遠不離開用戶本機,arcrun.dev 只收 email + account_id + kv_namespace_id
|
|
||||||
@@ -1,822 +0,0 @@
|
|||||||
# Design Document: u6u Platform Evolution
|
|
||||||
|
|
||||||
## Overview
|
|
||||||
|
|
||||||
u6u 平台演進的核心目標是將現有的「HTTP endpoint 零件 + 單一 Cloudflare 部署」架構,演進為「WASM 零件模型 + 三層物理部署 + 雙面翻轉畫布」的完整平台。
|
|
||||||
|
|
||||||
設計的最高原則是 **Dogfooding**:每一層都是下一層的第一個用戶。底層先建立最小可運行的能力,再用自己的方式往上蓋。這確保每個設計決策都被真實使用場景驗證,而非紙上談兵。
|
|
||||||
|
|
||||||
### Bootstrap 順序(不可跳過)
|
|
||||||
|
|
||||||
```
|
|
||||||
Phase 0:最小 WASM 執行核心
|
|
||||||
→ Component Dispatcher 能在 CF Workers 執行一個 .wasm(stdin/stdout JSON)
|
|
||||||
→ validate_json.wasm 作為第一個真實零件(TinyGo,< 50KB,驗證整個 pipeline)
|
|
||||||
→ Component Registry API(/guide、/validate-contract、/components)
|
|
||||||
|
|
||||||
Phase 1:遷移現有零件
|
|
||||||
→ 將 u6u-builtins 的 20 個 HTTP endpoint 逐一遷移為 .wasm
|
|
||||||
→ 遷移期間 Component Dispatcher 雙模式並存(HTTP fallback)
|
|
||||||
→ 每個零件附帶 component.contract.yaml
|
|
||||||
|
|
||||||
Phase 2:Cypher 語意擴展 + Multi-Tier Dispatcher
|
|
||||||
→ 支援 IS_A、ON_SUCCESS、ON_FAIL、CALLS_SUBFLOW、ON_CLICK
|
|
||||||
→ Component Dispatcher 路由層(Tier 1 CF / Tier 2 workerd / Tier 3 Wazero)
|
|
||||||
|
|
||||||
Phase 3:前端畫布(用自己的 Web Components 開發)
|
|
||||||
→ 先建立 Web Components 零件庫(u6u-btn、u6u-card 等)
|
|
||||||
→ 畫布本身用這些 Web Components 組裝
|
|
||||||
→ 雙面翻轉介面
|
|
||||||
```
|
|
||||||
|
|
||||||
### 關鍵設計約束
|
|
||||||
|
|
||||||
- **KBDB 不變量**:永遠只有三張表(blocks / templates / slots),不新增表
|
|
||||||
- **API-First 鐵律**:所有跨服務通訊只透過 HTTP API,禁止相對路徑引用
|
|
||||||
- **零件 I/O 不變量**:唯一合法的 I/O 模型是 `stdin_stdout_json`
|
|
||||||
- **Tier 3 約束**:無 V8、無 Node.js、無網路,所有零件必須在 Wazero 上跑
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Architecture
|
|
||||||
|
|
||||||
### 系統全景圖
|
|
||||||
|
|
||||||
```mermaid
|
|
||||||
graph TB
|
|
||||||
subgraph "Tier 1 — Cloudflare Workers(雲端)"
|
|
||||||
CE[Cypher Executor<br/>GraphExecutor]
|
|
||||||
CD[Component Dispatcher<br/>路由層]
|
|
||||||
CR[Component Registry<br/>KBDB HTTP API]
|
|
||||||
KBDB[(KBDB<br/>blocks/templates/slots<br/>+ Vectorize)]
|
|
||||||
R2[(R2<br/>.wasm 二進位)]
|
|
||||||
CE --> CD
|
|
||||||
CD --> CR
|
|
||||||
CR --> KBDB
|
|
||||||
CR --> R2
|
|
||||||
end
|
|
||||||
|
|
||||||
subgraph "Tier 2 — workerd self-hosted(企業地端)"
|
|
||||||
T2D[Tier 2 Dispatcher<br/>同 wasi-shim,不同部署]
|
|
||||||
T2R[本地 Registry 快取]
|
|
||||||
T2D --> T2R
|
|
||||||
end
|
|
||||||
|
|
||||||
subgraph "Tier 3 — 邊緣載具"
|
|
||||||
T3E[Go 排程引擎]
|
|
||||||
Wazero[Wazero Runtime]
|
|
||||||
SQLite[(SQLite<br/>本地 KBDB)]
|
|
||||||
DTN[DTN 佇列]
|
|
||||||
T3E --> Wazero
|
|
||||||
T3E --> SQLite
|
|
||||||
T3E --> DTN
|
|
||||||
end
|
|
||||||
|
|
||||||
subgraph "前端"
|
|
||||||
Canvas[雙面翻轉畫布<br/>React 19 + Web Components]
|
|
||||||
WC[Web Components 零件庫<br/>u6u-btn / u6u-card / ...]
|
|
||||||
Canvas --> WC
|
|
||||||
end
|
|
||||||
|
|
||||||
CD -->|WASM 執行| Tier1WASM[.wasm 執行]
|
|
||||||
CD -->|Cypher binding| ExtSvc[外部服務<br/>MCP / n8n / 任意 URL]
|
|
||||||
CD -->|HTTP| T2D
|
|
||||||
T2D -->|Wazero IPC| Wazero
|
|
||||||
DTN -->|Burst 傳輸| T2D
|
|
||||||
Canvas -->|u6u:trigger event| CE
|
|
||||||
```
|
|
||||||
|
|
||||||
### Cypher Binding 的正確定義
|
|
||||||
|
|
||||||
**Cypher binding** 是 u6u 的核心執行機制,指「用 Cypher 三元組語法把零件串接成工作流,串接關係儲存在 KBDB,不寫死在程式碼裡」。
|
|
||||||
|
|
||||||
這個概念相對於 Cloudflare Workers 原生的 **Service Binding**(需要 deploy、串接關係寫死在 wrangler.toml)。
|
|
||||||
|
|
||||||
`cypher-executor` 就是執行 Cypher binding 的引擎。
|
|
||||||
|
|
||||||
**零件本身只有兩種 component_type:**
|
|
||||||
|
|
||||||
| component_type | 說明 | 需要 deploy? |
|
|
||||||
|---|---|---|
|
|
||||||
| `wasm` | 所有後端零件(內建或用戶自建),本地 WASM 執行 | 否 |
|
|
||||||
| `service_binding` | 多個零件預組合成單一高頻零件的效能最佳化(如 OAuth + GSheets 常用組合) | 是 |
|
|
||||||
|
|
||||||
> **重要:`cypher_binding` 不是 component_type。** 它是整個執行引擎的名字,描述「零件如何被串接」,而不是「零件如何被執行」。所有零件(不管是內建還是用戶自建、不管是打外部 API 還是純邏輯)都是 `.wasm`,透過 Cypher 三元組串接。
|
|
||||||
|
|
||||||
> **所有後端零件都是 `.wasm`。** 需要呼叫外部 HTTP API 的零件(如 google-sheets、http-request),透過 WASI shim 注入的 **host function** 發出網路請求,不在 .wasm 內部直接呼叫網路 syscall。
|
|
||||||
|
|
||||||
### Component Dispatcher 路由決策樹
|
|
||||||
|
|
||||||
```mermaid
|
|
||||||
flowchart TD
|
|
||||||
A[Cypher Executor 呼叫零件 id] --> B{查 Component Registry<br/>取得合約}
|
|
||||||
B --> C{component_type?}
|
|
||||||
C -->|wasm| E{當前 Tier?}
|
|
||||||
C -->|service_binding| SB{有 CF Service Binding?}
|
|
||||||
SB -->|是| SBExec[CF Service Binding 執行<br/>需 deploy,效能最佳]
|
|
||||||
SB -->|否| SBErr[回傳錯誤:binding 未宣告]
|
|
||||||
E -->|Tier 1 / Tier 2| I[workerd WASM<br/>WebAssembly.instantiate<br/>+ WASI shim(兩者相同)]
|
|
||||||
E -->|Tier 3| L[Wazero IPC<br/>stdin/stdout,完全離線]
|
|
||||||
I --> RC{runtime_compat<br/>包含 cf-workers?}
|
|
||||||
RC -->|否| J[回傳 RUNTIME_INCOMPATIBLE 錯誤]
|
|
||||||
RC -->|是| Exec[執行 .wasm]
|
|
||||||
```
|
|
||||||
|
|
||||||
### KBDB 資料模型(tpl-component)
|
|
||||||
|
|
||||||
```mermaid
|
|
||||||
erDiagram
|
|
||||||
TEMPLATES {
|
|
||||||
string template_id "tpl-component"
|
|
||||||
string name
|
|
||||||
string description
|
|
||||||
}
|
|
||||||
BLOCKS {
|
|
||||||
string block_id "comp-{id}-{version}"
|
|
||||||
string template_id
|
|
||||||
string user_id
|
|
||||||
string page_name
|
|
||||||
}
|
|
||||||
SLOTS {
|
|
||||||
string slot_id
|
|
||||||
string block_id
|
|
||||||
string key
|
|
||||||
string value
|
|
||||||
}
|
|
||||||
TEMPLATES ||--o{ BLOCKS : "defines"
|
|
||||||
BLOCKS ||--o{ SLOTS : "has"
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Components and Interfaces
|
|
||||||
|
|
||||||
### 1. Component Registry(`u6u-core/registry/`)
|
|
||||||
|
|
||||||
Component Registry 是 KBDB 的薄包裝層,透過 HTTP API 管理零件合約。
|
|
||||||
|
|
||||||
#### 零件命名機制
|
|
||||||
|
|
||||||
零件有兩個名稱,職責完全不同:
|
|
||||||
|
|
||||||
| 欄位 | 由誰決定 | 用途 | 範例 |
|
|
||||||
|---|---|---|---|
|
|
||||||
| `display_name` | 建立者自由取 | 顯示用,不影響任何邏輯 | `宇宙無敵 GSheets 超級寫入器` |
|
|
||||||
| `canonical_id` | Registry AI 正規化後確認 | 搜尋、版本控制、Cypher 引用的唯一鍵 | `gsheets_create_table` |
|
|
||||||
|
|
||||||
**canonical_id 正規化流程:**
|
|
||||||
|
|
||||||
```
|
|
||||||
提交者輸入 display_name
|
|
||||||
↓
|
|
||||||
Registry 用 Workers AI 建議 canonical_id
|
|
||||||
(格式:{service}_{verb}_{object},全小寫底線)
|
|
||||||
↓
|
|
||||||
同時搜尋 Vectorize,若相似度 > 0.9 的 canonical_id 已存在
|
|
||||||
→ 提示「可能與 gsheets_create_table 重複,是否作為新版本提交?」
|
|
||||||
↓
|
|
||||||
提交者確認或修改 canonical_id
|
|
||||||
↓
|
|
||||||
上架,canonical_id 永久不變
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 零件分類機制
|
|
||||||
|
|
||||||
採用「強制 category + 自由 tags」雙層分類:
|
|
||||||
|
|
||||||
- **`category`**:強制填,有限集合,定義前後端邊界
|
|
||||||
- `logic`:後端邏輯零件(.wasm,純計算/轉換)
|
|
||||||
- `api`:後端 API 零件(.wasm + cypher_binding,呼叫外部服務)
|
|
||||||
- `ui`:前端 UI 元件(Web Component,瀏覽器執行)
|
|
||||||
- `style`:前端樣式零件(CSS tokens)
|
|
||||||
- `anim`:前端動畫零件
|
|
||||||
|
|
||||||
- **`tags`**:自由增加,跨零件共享語意
|
|
||||||
- 例:`gsheets_create_table` 有 `["google", "sheets", "spreadsheet", "storage", "write"]`
|
|
||||||
- 例:`excel_write_row` 有 `["microsoft", "excel", "spreadsheet", "storage", "write"]`
|
|
||||||
- 搜尋「外部存儲」時,兩個都能透過 Vectorize 語意搜尋找到
|
|
||||||
|
|
||||||
**HTTP 端點:**
|
|
||||||
|
|
||||||
```
|
|
||||||
GET /components/guide → 機器可讀開發指引(Markdown)
|
|
||||||
POST /components/validate-contract → 驗證 component.contract.yaml 格式
|
|
||||||
POST /components → 提交零件(.wasm + contract)觸發沙盒驗收
|
|
||||||
GET /components/:id → 取得零件合約(最優版本)
|
|
||||||
GET /components/:id/versions → 取得所有版本清單(含評分)
|
|
||||||
GET /components/search?q=... → 語意搜尋零件
|
|
||||||
```
|
|
||||||
|
|
||||||
**KBDB 整合:**
|
|
||||||
- 每個零件版本 = 一個 Block,`block_id = comp-{id}-{version}`
|
|
||||||
- Template = `tpl-component`(預先建立,不新增表)
|
|
||||||
- `.wasm` 二進位存 R2,KBDB slot 只存 `wasm_r2_key`
|
|
||||||
- `description` + `tags` 欄位寫入 Vectorize 索引,支援語意搜尋
|
|
||||||
|
|
||||||
**Slot 欄位對應(tpl-component):**
|
|
||||||
|
|
||||||
| Slot key | 說明 | 範例值 |
|
|
||||||
|---|---|---|
|
|
||||||
| `canonical_id` | 正規化功能名稱(永久不變,搜尋/版本控制用) | `gsheets_create_table` |
|
|
||||||
| `display_name` | 建立者自取的顯示名稱 | `宇宙無敵 GSheets 超級寫入器` |
|
|
||||||
| `category` | 零件分類(有限集合) | `logic` / `api` / `ui` / `style` / `anim` |
|
|
||||||
| `version` | 實作版本 | `v1` |
|
|
||||||
| `wasi_target` | WASM 目標 | `preview1` |
|
|
||||||
| `stability` | 穩定性標籤 | `floating` |
|
|
||||||
| `runtime_compat` | 相容 runtime(JSON 陣列) | `["cf-workers","wazero"]` |
|
|
||||||
| `component_type` | 零件類型 | `wasm` / `service_binding` |
|
|
||||||
| `max_size_kb` | 體積上限 | `2048` |
|
|
||||||
| `max_cold_start_ms` | 冷啟動上限 | `50` |
|
|
||||||
| `no_network_syscall` | 禁止網路 syscall | `true` |
|
|
||||||
| `input_schema` | JSON Schema(JSON 字串) | `{"type":"object",...}` |
|
|
||||||
| `output_schema` | JSON Schema(JSON 字串) | `{"type":"object",...}` |
|
|
||||||
| `gherkin_tests` | 測試案例(JSON 字串) | `[{"scenario":"..."}]` |
|
|
||||||
| `wasm_r2_key` | R2 物件鍵(wasm 模式) | `components/validate_json/v1.wasm` |
|
|
||||||
| `service_binding_key` | CF binding key(service_binding 模式) | `CLINIC_GSHEETS` |
|
|
||||||
| `description` | 自然語言描述(寫入 Vectorize) | `在 Google Sheets 建立新工作表` |
|
|
||||||
| `tags` | 自由標籤(JSON 陣列,跨零件共享語意) | `["google","sheets","storage","write"]` |
|
|
||||||
| `success_rate` | 成功率(0-1) | `0.98` |
|
|
||||||
| `avg_duration_ms` | 平均執行時間 | `12` |
|
|
||||||
| `call_count` | 被調用次數 | `1024` |
|
|
||||||
| `status` | 狀態 | `active` / `deprecated` / `tombstone` |
|
|
||||||
| `deprecated_at` | 棄用時間戳記 | `1700000000000` |
|
|
||||||
|
|
||||||
### 2. Component Dispatcher(`cypher-executor/src/lib/component-loader.ts` 擴展)
|
|
||||||
|
|
||||||
Component Dispatcher 是 `createComponentLoader` 的升級版,新增 WASM 執行路徑。
|
|
||||||
|
|
||||||
**介面定義:**
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// 零件類型(只有兩種)
|
|
||||||
type ComponentType =
|
|
||||||
| 'wasm' // 所有後端零件,透過 Cypher binding 串接,本地 WASM 執行
|
|
||||||
| 'service_binding'; // 效能最佳化:CF Service Binding,需 deploy,用於高頻預組合零件
|
|
||||||
|
|
||||||
// 新版 ComponentDescriptor
|
|
||||||
type ComponentDescriptor = {
|
|
||||||
component_type: ComponentType;
|
|
||||||
// WASM 模式
|
|
||||||
wasm_r2_key?: string;
|
|
||||||
runtime_compat?: string[];
|
|
||||||
max_cold_start_ms?: number;
|
|
||||||
// Service Binding 模式(CF Worker 間高效呼叫,需 deploy)
|
|
||||||
binding?: string; // wrangler.toml 中宣告的 binding key
|
|
||||||
path?: string;
|
|
||||||
};
|
|
||||||
```
|
|
||||||
|
|
||||||
**Tier 1 WASM 執行(CF Workers 原生):**
|
|
||||||
|
|
||||||
Cloudflare Workers 原生支援 `WebAssembly.instantiate`,但 WASI preview1 需要手動實作 WASI imports。設計採用輕量 WASI shim 方案:
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// WASI preview1 shim(只實作 stdin/stdout/stderr,其餘 syscall 回傳 ENOSYS)
|
|
||||||
function createWasiImports(stdin: Uint8Array): {
|
|
||||||
imports: WebAssembly.Imports;
|
|
||||||
getStdout: () => Uint8Array;
|
|
||||||
} {
|
|
||||||
const stdoutChunks: Uint8Array[] = [];
|
|
||||||
let stdinOffset = 0;
|
|
||||||
|
|
||||||
return {
|
|
||||||
imports: {
|
|
||||||
wasi_snapshot_preview1: {
|
|
||||||
fd_write: (fd: number, iovs: number, iovs_len: number, nwritten: number) => { /* ... */ },
|
|
||||||
fd_read: (fd: number, iovs: number, iovs_len: number, nread: number) => { /* ... */ },
|
|
||||||
proc_exit: (code: number) => { throw new Error(`wasm exit: ${code}`); },
|
|
||||||
// 其餘 syscall 回傳 ENOSYS(76)
|
|
||||||
fd_seek: () => 76,
|
|
||||||
fd_close: () => 0,
|
|
||||||
environ_get: () => 0,
|
|
||||||
environ_sizes_get: () => 0,
|
|
||||||
args_get: () => 0,
|
|
||||||
args_sizes_get: () => 0,
|
|
||||||
clock_time_get: () => 0,
|
|
||||||
random_get: (buf: number, buf_len: number) => { /* crypto.getRandomValues */ return 0; },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
getStdout: () => { /* 合併 stdoutChunks */ },
|
|
||||||
};
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
> **設計決策**:不使用 `@cloudflare/workers-wasi`(已停止維護)。改用自製輕量 WASI shim,只實作 `fd_read`/`fd_write`/`proc_exit`/`random_get`,其餘 syscall 回傳 `ENOSYS`。這足以支援 TinyGo/Rust/AssemblyScript 的 stdin/stdout 零件,且不引入外部依賴。
|
|
||||||
|
|
||||||
**執行流程:**
|
|
||||||
|
|
||||||
```
|
|
||||||
1. 從 R2 取得 .wasm 二進位(ArrayBuffer)
|
|
||||||
2. WebAssembly.compile(buffer) → WebAssembly.Module
|
|
||||||
3. 建立 WASI imports shim(注入 stdin = JSON.stringify(input))
|
|
||||||
4. WebAssembly.instantiate(module, imports)
|
|
||||||
5. 呼叫 _start() 或 main()
|
|
||||||
6. 從 stdout buffer 讀取輸出
|
|
||||||
7. JSON.parse(stdout) → output
|
|
||||||
```
|
|
||||||
|
|
||||||
**R2 快取策略:**
|
|
||||||
- 第一次呼叫:從 R2 fetch `.wasm`,`WebAssembly.compile` 後快取 `WebAssembly.Module`(Worker 記憶體,跨請求共享)
|
|
||||||
- 後續呼叫:直接用快取的 Module,只重新 instantiate(避免重複編譯)
|
|
||||||
|
|
||||||
### 3. Cypher Triplet Parser 擴展(`cypher-executor/src/actions/triplet-parser.ts`)
|
|
||||||
|
|
||||||
現有 parser 只支援 `PIPE / IF / FOREACH / CONTINUE`。需擴展支援新語意關係。
|
|
||||||
|
|
||||||
**新增 EdgeType:**
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
export type EdgeType =
|
|
||||||
| 'PIPE' | 'IF' | 'FOREACH' | 'CONTINUE' // 現有
|
|
||||||
| 'IS_A' | 'ON_SUCCESS' | 'ON_FAIL' // 新增:執行語意
|
|
||||||
| 'ON_CLICK' | 'CALLS_SUBFLOW' // 新增:觸發語意
|
|
||||||
| 'CONTAINS' | 'HAS_STYLE' | 'HAS_BEHAVIOR'; // 新增:結構語意
|
|
||||||
```
|
|
||||||
|
|
||||||
**URI 協議解析:**
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// 節點 componentId 解析
|
|
||||||
function resolveComponentId(uri: string): {
|
|
||||||
type: 'component' | 'workflow' | 'ui' | 'style';
|
|
||||||
canonicalId: string;
|
|
||||||
stability: 'floating' | 'stable' | 'pinned';
|
|
||||||
pinnedVersion?: string;
|
|
||||||
} {
|
|
||||||
// component://validate_json@stable → { type: 'component', canonicalId: 'validate_json', stability: 'stable' }
|
|
||||||
// component://validate_json@pinned:v1 → { type: 'component', canonicalId: 'validate_json', stability: 'pinned', pinnedVersion: 'v1' }
|
|
||||||
// workflow://wf_save_to_db → { type: 'workflow', canonicalId: 'wf_save_to_db', stability: 'floating' }
|
|
||||||
// ui://u6u-btn → { type: 'ui', canonicalId: 'u6u-btn', stability: 'floating' }
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**ON_SUCCESS / ON_FAIL 執行語意:**
|
|
||||||
|
|
||||||
GraphExecutor 需要區分「節點執行成功」vs「節點執行失敗」,而非依賴 context 欄位:
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// 在 executeNode 中,捕捉 try/catch 後分別走 ON_SUCCESS / ON_FAIL 邊
|
|
||||||
case 'ON_SUCCESS':
|
|
||||||
// 只在上游節點成功時執行
|
|
||||||
if (!nodeError) {
|
|
||||||
result = await this.executeNode(nextNode, ...);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case 'ON_FAIL':
|
|
||||||
// 只在上游節點失敗時執行(接收 error context)
|
|
||||||
if (nodeError) {
|
|
||||||
result = await this.executeNode(nextNode, graph, { ...context, error: nodeError }, ...);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
```
|
|
||||||
|
|
||||||
**CALLS_SUBFLOW 執行語意:**
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
case 'CALLS_SUBFLOW': {
|
|
||||||
// 從 KBDB 載入子 Workflow 定義
|
|
||||||
const subWorkflowId = nextNode.componentId!.replace('workflow://', '');
|
|
||||||
const subGraph = await loadWorkflowFromKBDB(subWorkflowId, env);
|
|
||||||
const subExecutor = new GraphExecutor(loader);
|
|
||||||
const subResult = await subExecutor.execute(subGraph, result as Record<string, unknown>, kvNamespace);
|
|
||||||
result = { ...(result as Record<string, unknown>), ...subResult.data as Record<string, unknown> };
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 4. Web Components 零件庫(`u6u-core/web-components/`)
|
|
||||||
|
|
||||||
Web Components 以原生 Custom Elements API 實作,不依賴任何框架。
|
|
||||||
|
|
||||||
**`<u6u-btn>` 介面:**
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// HTML attributes
|
|
||||||
interface U6uBtnAttributes {
|
|
||||||
label: string; // 顯示文字
|
|
||||||
color?: string; // 主題色(CSS custom property)
|
|
||||||
tooltip?: string; // 滑鼠懸停提示(純靜態)
|
|
||||||
workflow?: string; // workflow://id
|
|
||||||
disabled?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 發出的自訂事件
|
|
||||||
interface U6uTriggerEvent extends CustomEvent {
|
|
||||||
detail: {
|
|
||||||
workflowId: string;
|
|
||||||
payload: Record<string, unknown>;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**`<u6u-card>` Smart Container 邏輯:**
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// u6u-card 攔截子元件的 u6u:trigger 事件
|
|
||||||
// 收集同容器內所有 u6u-text-input / u6u-text-field 的值
|
|
||||||
// 合併至 payload 後再向上冒泡
|
|
||||||
connectedCallback() {
|
|
||||||
this.addEventListener('u6u:trigger', (e: Event) => {
|
|
||||||
const trigger = e as CustomEvent;
|
|
||||||
e.stopPropagation();
|
|
||||||
|
|
||||||
const inputs = this.querySelectorAll('u6u-text-input, u6u-text-field');
|
|
||||||
const collected: Record<string, unknown> = {};
|
|
||||||
inputs.forEach(input => {
|
|
||||||
const name = input.getAttribute('name');
|
|
||||||
const value = (input as any).value;
|
|
||||||
if (name) collected[name] = value;
|
|
||||||
});
|
|
||||||
|
|
||||||
this.dispatchEvent(new CustomEvent('u6u:trigger', {
|
|
||||||
bubbles: true,
|
|
||||||
composed: true,
|
|
||||||
detail: {
|
|
||||||
...trigger.detail,
|
|
||||||
payload: { ...trigger.detail.payload, ...collected },
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 5. 雙面翻轉畫布(`inkstone-admin/frontend/web/`)
|
|
||||||
|
|
||||||
畫布本身用 React 19 + Web Components 組裝,體現 dogfooding 原則。
|
|
||||||
|
|
||||||
**翻轉狀態機:**
|
|
||||||
|
|
||||||
```mermaid
|
|
||||||
stateDiagram-v2
|
|
||||||
[*] --> UIView: 初始狀態
|
|
||||||
UIView --> LogicView: 點擊翻面按鈕
|
|
||||||
LogicView --> UIView: 點擊翻面按鈕
|
|
||||||
LogicView --> Editing: 修改三元組
|
|
||||||
Editing --> Saving: 確認儲存
|
|
||||||
Saving --> LogicView: KBDB 寫入成功
|
|
||||||
Saving --> LogicView: KBDB 寫入失敗(顯示錯誤)
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Data Models
|
|
||||||
|
|
||||||
### Component Contract YAML(完整規格)
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
canonical_id: "validate_json" # 正規化功能名稱(永久不變,Registry AI 正規化後確認)
|
|
||||||
display_name: "JSON 格式驗證器" # 建立者自取,顯示用
|
|
||||||
category: "logic" # logic | api | ui | style | anim
|
|
||||||
version: "v1" # 實作版本
|
|
||||||
wasi_target: "preview1" # WASM 目標格式
|
|
||||||
stability: "floating" # floating | stable | pinned
|
|
||||||
|
|
||||||
runtime_compat:
|
|
||||||
- "cf-workers"
|
|
||||||
- "workerd"
|
|
||||||
- "wazero"
|
|
||||||
|
|
||||||
constraints:
|
|
||||||
max_size_kb: 2048
|
|
||||||
max_cold_start_ms: 50
|
|
||||||
no_network_syscall: true
|
|
||||||
no_filesystem_syscall: true
|
|
||||||
io_model: "stdin_stdout_json" # 唯一合法值
|
|
||||||
|
|
||||||
input_schema:
|
|
||||||
type: object
|
|
||||||
required: ["json_string"]
|
|
||||||
properties:
|
|
||||||
json_string:
|
|
||||||
type: string
|
|
||||||
description: "待驗證的 JSON 字串"
|
|
||||||
|
|
||||||
output_schema:
|
|
||||||
type: object
|
|
||||||
properties:
|
|
||||||
valid:
|
|
||||||
type: boolean
|
|
||||||
error:
|
|
||||||
type: string
|
|
||||||
description: "驗證失敗時的錯誤訊息"
|
|
||||||
|
|
||||||
gherkin_tests:
|
|
||||||
- scenario: "合法 JSON 通過驗證"
|
|
||||||
given: '{"json_string":"{\"key\":\"value\"}"}'
|
|
||||||
then_contains: '{"valid":true}'
|
|
||||||
- scenario: "非法 JSON 回傳錯誤"
|
|
||||||
given: '{"json_string":"not-json"}'
|
|
||||||
then_contains: '{"valid":false,"error":'
|
|
||||||
|
|
||||||
tags: ["validation", "json", "utility"]
|
|
||||||
description: "驗證輸入字串是否為合法 JSON 格式"
|
|
||||||
```
|
|
||||||
|
|
||||||
### 零件開發語言決策
|
|
||||||
|
|
||||||
**內建零件使用 TinyGo**(純邏輯零件)和 TinyGo + `json.RawMessage`(需要任意 HTTP body 的零件)。不引入 Rust 作為內建零件語言。
|
|
||||||
|
|
||||||
**用戶自建零件支援三種語言,按難度分層:**
|
|
||||||
|
|
||||||
| 語言 | 目標用戶 | JSON 能力 | 備註 |
|
|
||||||
|---|---|---|---|
|
|
||||||
| **AssemblyScript** | 一般用戶(TS 背景) | 社群套件 `assemblyscript-json`,支援動態 JSON | 語法最接近 TS,門檻最低;靜默錯誤風險,沙盒驗收必須通過 |
|
|
||||||
| **TinyGo** | 技術較強用戶(Go 背景) | 靜態 struct 完整支援;`json.RawMessage` 處理任意 body | 編譯期報錯,AI 生成安全性較高 |
|
|
||||||
| **Rust** | 進階用戶 | `serde_json::Value` 完整動態 JSON | 生態最成熟,體積最小;學習曲線陡 |
|
|
||||||
|
|
||||||
**`/components/guide` 端點提供三份語言範例**,用戶根據自身背景選擇。
|
|
||||||
|
|
||||||
**內建零件 JSON 策略(TinyGo):**
|
|
||||||
|
|
||||||
```go
|
|
||||||
// 固定 schema 零件(google-sheets、gmail 等)→ 靜態 struct
|
|
||||||
type Input struct {
|
|
||||||
SpreadsheetId string `json:"spreadsheet_id"`
|
|
||||||
Range string `json:"range"`
|
|
||||||
AccessToken string `json:"access_token"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// 任意 body 零件(http-request)→ json.RawMessage 傳遞 raw bytes,不解析
|
|
||||||
type Input struct {
|
|
||||||
URL string `json:"url"`
|
|
||||||
Method string `json:"method"`
|
|
||||||
Body json.RawMessage `json:"body"` // 任意 JSON,不解析
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Workflow Cypher 三元組(完整語法)
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
kind: Workflow
|
|
||||||
id: wf_submit_form
|
|
||||||
|
|
||||||
triplets:
|
|
||||||
# 節點類型宣告
|
|
||||||
- "btn_submit >> IS_A >> ui://u6u-btn"
|
|
||||||
- "step_validate >> IS_A >> component://validate_json"
|
|
||||||
- "step_save >> IS_A >> component://kbdb_write"
|
|
||||||
|
|
||||||
# 前端觸發後端
|
|
||||||
- "btn_submit >> ON_CLICK >> step_validate"
|
|
||||||
|
|
||||||
# 成功/失敗分支
|
|
||||||
- "step_validate >> ON_SUCCESS >> step_save"
|
|
||||||
- "step_validate >> ON_FAIL >> step_notify_error"
|
|
||||||
|
|
||||||
# 子流程呼叫
|
|
||||||
- "step_save >> ON_SUCCESS >> CALLS_SUBFLOW >> workflow://wf_notify_user"
|
|
||||||
|
|
||||||
# 容器結構
|
|
||||||
- "card_main >> CONTAINS >> btn_submit"
|
|
||||||
- "card_main >> CONTAINS >> input_name"
|
|
||||||
```
|
|
||||||
|
|
||||||
### Evaluation Block(KBDB tpl-evaluation)
|
|
||||||
|
|
||||||
每次 Workflow 執行後,Evaluator Agent 寫入一個 Evaluation Block:
|
|
||||||
|
|
||||||
| Slot key | 說明 |
|
|
||||||
|---|---|
|
|
||||||
| `run_id` | 執行唯一 ID |
|
|
||||||
| `workflow_id` | Workflow ID |
|
|
||||||
| `component_id` | 被評價的零件 ID |
|
|
||||||
| `verdict` | `success` / `failed` / `timeout` |
|
|
||||||
| `duration_ms` | 執行時間 |
|
|
||||||
| `error_message` | 失敗訊息(可選) |
|
|
||||||
| `evaluated_at` | 評價時間戳記 |
|
|
||||||
|
|
||||||
### Pitfall Block(KBDB tpl-pitfall)
|
|
||||||
|
|
||||||
| Slot key | 說明 |
|
|
||||||
|---|---|
|
|
||||||
| `component_id` | 問題零件 ID |
|
|
||||||
| `failure_pattern` | 失敗模式描述 |
|
|
||||||
| `first_seen_at` | 首次發現時間戳記 |
|
|
||||||
| `occurrence_count` | 發生次數 |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Correctness Properties
|
|
||||||
|
|
||||||
*A property is a characteristic or behavior that should hold true across all valid executions of a system — essentially, a formal statement about what the system should do. Properties serve as the bridge between human-readable specifications and machine-verifiable correctness guarantees.*
|
|
||||||
|
|
||||||
|
|
||||||
### Property Reflection(去重分析)
|
|
||||||
|
|
||||||
在寫出最終屬性前,先做冗餘分析:
|
|
||||||
|
|
||||||
- **1.1 + 1.2**:都是合約欄位完整性驗證,合併為 Property 1「合約格式完整性」
|
|
||||||
- **2.1 + 2.2**:驗收流程執行 + 失敗回報,合併為 Property 2「沙盒驗收流程正確性」
|
|
||||||
- **2.3 + 2.4**:提交後可讀取 + 冪等提交,合併為 Property 3「零件提交冪等性與持久性」
|
|
||||||
- **3.1 + 3.5**:WASM 執行 + 雙模式路由,合併為 Property 4「Component Dispatcher 路由正確性」
|
|
||||||
- **3.4 + 6.4**:不相容 Tier 回傳錯誤 + 結構化錯誤,合併為 Property 5「Dispatcher 錯誤結構完整性」
|
|
||||||
- **4.1**:URI 解析 round-trip,獨立為 Property 6
|
|
||||||
- **4.2 + 4.4**:版本選擇算法(floating 最高分 + pinned 固定版本),合併為 Property 7「版本選擇策略正確性」
|
|
||||||
- **4.5**:版本保留不變量,獨立為 Property 8
|
|
||||||
- **5.7**:Confluence 屬性,獨立為 Property 9
|
|
||||||
- **8.3 + 8.4 + 8.5**:Web Components 事件與渲染,合併為 Property 10「Web Components 事件與渲染冪等性」
|
|
||||||
- **10.6 + 12.5**:評價冪等性 + 查詢冪等性,合併為 Property 11「系統操作冪等性」
|
|
||||||
|
|
||||||
最終保留 11 個屬性,每個提供獨立驗證價值。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Property 1: 合約格式完整性
|
|
||||||
|
|
||||||
*For any* component contract object,若缺少任何必填欄位(`id`、`version`、`wasi_target`、`stability`、`runtime_compat`、`constraints.max_size_kb`、`constraints.max_cold_start_ms`、`constraints.no_network_syscall`、`constraints.io_model`、`input_schema`、`output_schema`、`gherkin_tests`),合約驗證器 SHALL 拒絕該合約並回傳包含缺失欄位名稱的錯誤;反之,包含所有必填欄位的合約 SHALL 通過格式驗證。
|
|
||||||
|
|
||||||
**Validates: Requirements 1.1, 1.2, 1.4**
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Property 2: 沙盒驗收流程正確性
|
|
||||||
|
|
||||||
*For any* 提交的零件(.wasm + contract),若零件在驗收步驟 N 失敗,Component_Registry 的回應 SHALL 包含步驟 N 的名稱與具體失敗原因,且不執行步驟 N+1 之後的步驟。
|
|
||||||
|
|
||||||
**Validates: Requirements 2.1, 2.2**
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Property 3: 零件提交冪等性與持久性
|
|
||||||
|
|
||||||
*For any* 通過驗收的零件(id, version),提交後從 Component_Registry 讀取該零件的合約,所有欄位值 SHALL 與提交時的合約完全一致(序列化 round-trip);對相同 (id, version) 重複提交 N 次,KBDB 中 SHALL 只存在一個對應的 Block。
|
|
||||||
|
|
||||||
**Validates: Requirements 2.3, 2.4**
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Property 4: Component Dispatcher 路由正確性
|
|
||||||
|
|
||||||
*For any* 零件合約,若 `io_model = "stdin_stdout_json"`,Component_Dispatcher SHALL 使用 WASM 執行路徑,將 input JSON 寫入 stdin,從 stdout 讀取 output JSON;若 `io_model = "http_endpoint"`,SHALL 使用 HTTP 路徑。對任意合法 JSON input,WASM 執行路徑的輸出 SHALL 與 HTTP 執行路徑的輸出語意等效。
|
|
||||||
|
|
||||||
**Validates: Requirements 3.1, 3.5, 3.6**
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Property 5: Dispatcher 錯誤結構完整性
|
|
||||||
|
|
||||||
*For any* (component_id, tier) 組合,若該零件的 `runtime_compat` 不包含當前 tier,Component_Dispatcher 的錯誤回應 SHALL 同時包含:零件 id、當前 tier 名稱、已嘗試的呼叫路徑清單,三個欄位缺一不可。
|
|
||||||
|
|
||||||
**Validates: Requirements 3.4, 6.4**
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Property 6: 零件 URI 解析 Round-Trip
|
|
||||||
|
|
||||||
*For any* 合法的零件 URI 字串(格式為 `component://id`、`component://id@stable`、`component://id@pinned:vN`),解析後再重新序列化 SHALL 產生與原始 URI 語意等效的字串;解析出的 `id`、`stability`、`pinnedVersion` 欄位 SHALL 與原始 URI 中的對應部分完全一致。
|
|
||||||
|
|
||||||
**Validates: Requirements 4.1**
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Property 7: 版本選擇策略正確性
|
|
||||||
|
|
||||||
*For any* 零件 id 下的版本集合(每個版本有 success_rate、avg_duration_ms、call_count 評分),當 stability = `floating` 時,Component_Dispatcher SHALL 選取「成功率 × 速度評分 × 被調用次數」最高的版本;當 stability = `pinned:vN` 時,無論版本集合中其他版本的評分如何,SHALL 永遠選取版本 vN。
|
|
||||||
|
|
||||||
**Validates: Requirements 4.2, 4.4**
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Property 8: 歷史版本永久保留不變量
|
|
||||||
|
|
||||||
*For any* 已上架的零件版本(id, version),無論該版本後來被標記為 `deprecated` 或 `tombstone`,其 `.wasm` 二進位 SHALL 永遠可從 R2 讀取,且 `pinned` 引用 SHALL 永遠能透過 Component_Dispatcher 執行該版本。
|
|
||||||
|
|
||||||
**Validates: Requirements 4.5, 10.5**
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Property 9: Cypher 三元組解析 Confluence(順序無關性)
|
|
||||||
|
|
||||||
*For any* 合法的 Cypher 三元組集合,無論三元組在輸入陣列中的排列順序如何,`parseTriplets` 產生的執行圖(節點集合、邊集合、拓撲結構)SHALL 語意等效——即相同的節點 id 集合、相同的 (from, to, type) 邊集合。
|
|
||||||
|
|
||||||
**Validates: Requirements 5.7**
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Property 10: Web Components 事件與渲染冪等性
|
|
||||||
|
|
||||||
*For any* workflow URI 設定於 `<u6u-btn>` 的 `workflow` attribute,使用者點擊後發出的 `u6u:trigger` 事件 detail 中的 `workflowId` SHALL 與 URI 中的 id 完全一致;*For any* 一組具名 `<u6u-text-input>` 元件置於 `<u6u-card>` 內,觸發事件後收集到的 payload SHALL 包含所有輸入元件的 name-value 對;*For any* attribute 值,對同一 Web Component 設定相同 attribute 值 N 次,渲染結果 SHALL 與設定一次相同(冪等渲染)。
|
|
||||||
|
|
||||||
**Validates: Requirements 8.3, 8.4, 8.5**
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Property 11: 系統操作冪等性
|
|
||||||
|
|
||||||
*For any* Workflow 執行日誌(run_id),Evaluator_Agent 對相同 run_id 處理 N 次,KBDB 中 SHALL 只存在一個對應的 Evaluation Block,不產生重複記錄;*For any* Component_Registry 讀取操作的查詢參數,在 KBDB 資料不變的前提下,對相同參數呼叫 N 次 SHALL 回傳完全相同的結果。
|
|
||||||
|
|
||||||
**Validates: Requirements 10.6, 12.5**
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Error Handling
|
|
||||||
|
|
||||||
### Component Dispatcher 錯誤分類
|
|
||||||
|
|
||||||
| 錯誤類型 | 觸發條件 | 回應格式 |
|
|
||||||
|---|---|---|
|
|
||||||
| `COMPONENT_NOT_FOUND` | KBDB 中找不到零件 | `{ error: "COMPONENT_NOT_FOUND", component_id, tier }` |
|
|
||||||
| `RUNTIME_INCOMPATIBLE` | runtime_compat 不含當前 Tier | `{ error: "RUNTIME_INCOMPATIBLE", component_id, tier, attempted_paths: [] }` |
|
|
||||||
| `WASM_EXECUTION_TIMEOUT` | 超過 max_cold_start_ms | `{ error: "WASM_EXECUTION_TIMEOUT", component_id, timeout_ms }` |
|
|
||||||
| `WASM_INVALID_OUTPUT` | stdout 不是合法 JSON | `{ error: "WASM_INVALID_OUTPUT", component_id, raw_output }` |
|
|
||||||
| `WASM_SYSCALL_VIOLATION` | .wasm 嘗試網路/檔案 syscall | `{ error: "WASM_SYSCALL_VIOLATION", component_id, syscall_name }` |
|
|
||||||
| `CONTRACT_VALIDATION_FAILED` | 合約格式不合規 | `{ error: "CONTRACT_VALIDATION_FAILED", missing_fields: [] }` |
|
|
||||||
|
|
||||||
### 沙盒驗收失敗回應格式
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"success": false,
|
|
||||||
"failed_step": "syscall_scan",
|
|
||||||
"reason": "發現禁止的 syscall:sock_connect",
|
|
||||||
"guide_anchor": "#syscall-constraints",
|
|
||||||
"component_id": "my_component",
|
|
||||||
"version": "v1"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Tier 3 離線錯誤處理
|
|
||||||
|
|
||||||
Tier 3 在離線環境中,所有無法執行的操作都寫入 DTN 佇列,不拋出錯誤:
|
|
||||||
|
|
||||||
```go
|
|
||||||
// Go 排程引擎的錯誤處理策略
|
|
||||||
type DTNEntry struct {
|
|
||||||
Type string // "missing_component" | "sync_log" | "request_wasm"
|
|
||||||
Payload json.RawMessage
|
|
||||||
CreatedAt time.Time
|
|
||||||
RetryCount int
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Web Components 錯誤邊界
|
|
||||||
|
|
||||||
`<u6u-btn>` 在 `workflow` attribute 未設定時,點擊不發出事件,僅在 console 輸出警告:
|
|
||||||
|
|
||||||
```
|
|
||||||
[u6u-btn] workflow attribute is not set, click event ignored
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Testing Strategy
|
|
||||||
|
|
||||||
### 雙軌測試策略
|
|
||||||
|
|
||||||
本功能採用「單元測試 + 屬性測試」雙軌策略:
|
|
||||||
|
|
||||||
- **單元測試(Vitest)**:驗證具體範例、邊界條件、錯誤情境
|
|
||||||
- **屬性測試(fast-check)**:驗證上述 11 個 Correctness Properties,每個屬性最少執行 100 次迭代
|
|
||||||
|
|
||||||
### 屬性測試配置
|
|
||||||
|
|
||||||
使用 `fast-check`(已在 tech stack 中),每個屬性測試標記格式:
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// Feature: arcrun-platform-evolution, Property N: {property_text}
|
|
||||||
it.prop([fc.record({ id: fc.string(), version: fc.string(), ... })])(
|
|
||||||
'Property 1: 合約格式完整性',
|
|
||||||
(contract) => {
|
|
||||||
// ...
|
|
||||||
},
|
|
||||||
{ numRuns: 100 }
|
|
||||||
);
|
|
||||||
```
|
|
||||||
|
|
||||||
### 各 Phase 測試重點
|
|
||||||
|
|
||||||
**Phase 0(WASM 執行核心):**
|
|
||||||
- Property 4:WASM 執行路徑 round-trip(`validate_json.wasm` 作為 ground truth)
|
|
||||||
- Property 1:合約格式驗證
|
|
||||||
- 單元測試:WASI shim 的 `fd_read`/`fd_write` 正確性
|
|
||||||
|
|
||||||
**Phase 1(零件遷移):**
|
|
||||||
- Property 3:提交冪等性(20 個零件各提交兩次,驗證無重複)
|
|
||||||
- Property 2:沙盒驗收流程(各步驟失敗案例)
|
|
||||||
- Property 8:歷史版本保留(deprecate 後仍可讀取)
|
|
||||||
|
|
||||||
**Phase 2(Cypher 擴展):**
|
|
||||||
- Property 9:Confluence(三元組順序無關性,fast-check shuffle)
|
|
||||||
- Property 6:URI 解析 round-trip
|
|
||||||
- Property 7:版本選擇策略(floating 最高分、pinned 固定版本)
|
|
||||||
- Property 5:錯誤結構完整性
|
|
||||||
|
|
||||||
**Phase 3(前端畫布):**
|
|
||||||
- Property 10:Web Components 事件與渲染冪等性(`@testing-library/react` + fast-check)
|
|
||||||
- Property 11:評價冪等性(Evaluator Agent 重複處理)
|
|
||||||
|
|
||||||
### 整合測試
|
|
||||||
|
|
||||||
以下場景使用整合測試(1-3 個具體範例,不用 PBT):
|
|
||||||
|
|
||||||
- Tier 1 CF Workers 環境中實際執行 `validate_json.wasm`(驗證 WASM 在 Workers 環境可運行)
|
|
||||||
- KBDB tpl-component Template 建立與 Slot 讀寫(驗證 KBDB 整合)
|
|
||||||
- R2 `.wasm` 上傳與讀取(驗證 R2 整合)
|
|
||||||
- Vectorize 語意搜尋(驗證「查詢 Google Sheets 資料」能找到 `gsheets_get_entries`)
|
|
||||||
|
|
||||||
### 單元測試重點(非 PBT)
|
|
||||||
|
|
||||||
- WASI shim:`fd_read` 正確讀取 stdin、`fd_write` 正確寫入 stdout
|
|
||||||
- `evaluateCondition`:現有條件評估函數的邊界案例
|
|
||||||
- `resolveComponentId`:URI 解析的邊界案例(空字串、特殊字元)
|
|
||||||
- `<u6u-card>` Smart Container:巢狀容器的事件冒泡行為
|
|
||||||
@@ -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 1(Cloudflare Workers)、Tier 2(workerd 地端叢集)、Tier 3(Go + Wazero 邊緣載具)呼叫零件。
|
|
||||||
3. **前端雙面畫布**:建立以 Web Components 為基礎的視覺化畫布,正面為 UI 視圖,反面為 Cypher 邏輯視圖,智慧容器自動打包表單值。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Glossary
|
|
||||||
|
|
||||||
- **Component(零件)**:系統最小執行單元,一個零件只做一件事,以 `.wasm`(WASI preview1)或 Web Component 形式存在。
|
|
||||||
- **Component_Contract**:每個零件附帶的 `component.contract.yaml`,定義 id、version、wasi_target、stability、runtime_compat、constraints、input_schema、output_schema、gherkin_tests。
|
|
||||||
- **Component_Registry**:KBDB 中儲存所有零件合約與 `.wasm` 位置的索引,以 `tpl-component` Template Block 實作。
|
|
||||||
- **Component_Dispatcher**:Cypher Executor 內部的路由層,根據零件的 `runtime_compat` 與目標 Tier 決定呼叫路徑(Service Binding / workerd HTTP / Wazero IPC)。
|
|
||||||
- **Cypher_Executor**:Workflow 執行引擎,解析三元組語法,透過 GraphExecutor 執行節點,現部署於 Cloudflare Workers。
|
|
||||||
- **Cypher_Triplet**:`"A >> 關係 >> B"` 格式的三元組,描述節點間的語意關係。
|
|
||||||
- **KBDB**:三位一體記憶庫(blocks / templates / slots),搭配 Cloudflare Vectorize,是平台唯一的持久化狀態來源。
|
|
||||||
- **Tier_1**:雲端層,Cloudflare Workers + D1 + Vectorize + R2,全球無伺服器部署。
|
|
||||||
- **Tier_2**:企業地端層,workerd 叢集 + Kùzu 或 PostgreSQL + AGE,高機密內網環境。
|
|
||||||
- **Tier_3**:邊緣載具層,Go 排程引擎 + 內嵌 Wazero + SQLite,無 V8、無網路的極限環境(無人機、AGV)。
|
|
||||||
- **WASI_Preview1**:WebAssembly System Interface preview1 規格,零件唯一合法的 WASM 目標格式。
|
|
||||||
- **Canvas(畫布)**:前端雙面翻轉介面,正面為 UI 視圖,反面為 Cypher 邏輯視圖。
|
|
||||||
- **Smart_Container**:畫布上的排版容器(如 `<u6u-card>`),自動打包同容器內所有輸入元件的值並附加至觸發事件的 payload。
|
|
||||||
- **Forge_AI**:工匠 AI,負責在 Tier 2 地端接收零件規格、生成 TinyGo 程式碼、編譯並測試 `.wasm`。
|
|
||||||
- **Evaluator_Agent**:強制評價代理,每次 Workflow 執行後自動評估成功率、效能、警告訊息。
|
|
||||||
- **Pitfall_Block**:KBDB 中記錄已知問題的 Block,AI 搜尋時強制讀取以繞道。
|
|
||||||
- **Stability_Tag**:零件版本穩定性標籤,值為 `floating`(AI 自動選最優)、`stable`(人工確認才換)、`pinned`(版本凍結)。
|
|
||||||
- **DTN**:Delay-Tolerant Networking,Tier 3 邊緣載具在間歇性網路下的短點射傳輸協議。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Requirements
|
|
||||||
|
|
||||||
### Requirement 1:零件合約規格標準化
|
|
||||||
|
|
||||||
**User Story:** As a 平台架構師, I want 每個零件都有標準化的 `component.contract.yaml` 合約, so that AI 能透過統一介面讀取零件能力,並在任何 Tier 上驗證相容性。
|
|
||||||
|
|
||||||
#### Acceptance Criteria
|
|
||||||
|
|
||||||
1. THE Component_Contract SHALL 包含以下必填欄位:`canonical_id`(功能合約名稱,永久不變)、`display_name`(人類可讀名稱,可自由撰寫)、`description`(語意搜尋用途,需精確描述零件能做什麼、適用情境,至少 20 字)、`version`(實作版本)、`wasi_target`(值為 `"preview1"`)、`stability`(值為 `floating`、`stable` 或 `pinned` 之一)、`runtime_compat`(陣列,值為 `cf-workers`、`workerd`、`wazero` 的子集)、`constraints`、`input_schema`、`output_schema`、`gherkin_tests`。
|
|
||||||
1a. THE `canonical_id` SHALL 遵循以下命名規範,以確保全庫一致性:
|
|
||||||
- 格式:`{scope}_{action}` 或 `{scope}_{object}` 或 `{scope}`(單詞),全部小寫底線,不超過 4 個單詞
|
|
||||||
- **整合類**(category: api):以服務名稱為 scope,可加動作;範例:`gmail`、`gmail_send`、`google_sheets`、`google_sheets_append`、`telegram`、`telegram_send`
|
|
||||||
- **資料處理類**(category: data):以資料型別或操作為 scope;範例:`string_ops`、`array_ops`、`date_ops`、`json_transform`
|
|
||||||
- **控制流類**(category: logic):以控制結構名稱命名;範例:`if_control`、`foreach_control`、`try_catch`、`switch`、`wait`
|
|
||||||
- **AI 類**(category: ai):以 `ai_` 為前綴;範例:`ai_transform_compile`、`ai_summarize`、`ai_classify`
|
|
||||||
- 禁止:中文、空格、大寫、連字號(`-`)、版本號混入 id(`gmail_v2` 用 `version: v2` 表達)
|
|
||||||
1b. THE `display_name` SHALL 為人類可讀的自由格式名稱(可中文、可含空格);此欄位供 UI 顯示用,不作為系統識別符。範例:`canonical_id: google_sheets_append` 配 `display_name: "Google Sheets — 新增一列"`。
|
|
||||||
1c. THE `description` SHALL 用於 Vectorize 語意搜尋索引;撰寫時應以「能做什麼、適合什麼情境」為核心,避免只寫零件名稱的同義詞。範例:`"傳送 Gmail 電子郵件,適合 Workflow 完成時通知使用者、訂閱確認信、錯誤警報等場景。需要 Gmail OAuth token。"` 而非 `"Gmail 發信零件"`。
|
|
||||||
2. THE Component_Contract SHALL 在 `constraints` 中包含以下欄位:`max_size_kb`(上限 2048)、`max_cold_start_ms`(上限 50)、`no_network_syscall`(布林值)、`io_model`(值為 `"stdin_stdout_json"`)。
|
|
||||||
3. WHEN 一個零件的 `id` 已存在於 Component_Registry,THE Component_Registry SHALL 允許以新 `version` 值新增該零件的新實作,而不覆蓋舊版本。
|
|
||||||
4. THE Component_Contract SHALL 在 `gherkin_tests` 中至少包含一個正常情境(happy path)與一個錯誤情境(error path)的測試案例。
|
|
||||||
5. IF 一個零件的 `input_schema` 或 `output_schema` 涉及序列化或反序列化操作,THEN THE Component_Contract SHALL 包含一個 round-trip 測試案例,驗證 `parse(format(x)) == x`。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Requirement 2:零件沙盒驗收流程
|
|
||||||
|
|
||||||
**User Story:** As a 零件提交者(AI 或開發者), I want 提交的零件自動通過沙盒驗收, so that 只有符合品質標準的零件才能進入零件宇宙。
|
|
||||||
|
|
||||||
#### Acceptance Criteria
|
|
||||||
|
|
||||||
1. WHEN 一個零件被提交至 Component_Registry,THE Component_Registry SHALL 依序執行以下驗收步驟:(a)體積檢查(`.wasm` 小於 `max_size_kb`)、(b)冷啟動時間測量(小於 `max_cold_start_ms`)、(c)syscall 掃描(不含網路或檔案系統 syscall)、(d)Gherkin 測試執行(所有 scenario 100% 通過)、(e)多 runtime 相容測試(`runtime_compat` 列出的所有 runtime)。
|
|
||||||
2. IF 任一驗收步驟失敗,THEN THE Component_Registry SHALL 拒絕該零件上架,並回傳包含失敗步驟名稱與具體原因的錯誤訊息。
|
|
||||||
3. WHEN 所有驗收步驟通過,THE Component_Registry SHALL 將零件合約存入 KBDB 的 `tpl-component` Template Block,並記錄上架時間戳記。
|
|
||||||
4. THE Component_Registry SHALL 以冪等方式執行驗收流程,對相同 `id` 與 `version` 的重複提交回傳相同結果而不重複執行測試。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Requirement 3:現有 HTTP 零件遷移至 WASM
|
|
||||||
|
|
||||||
**User Story:** As a 平台開發者, I want 將現有 20 個 HTTP endpoint 零件遷移為 WASI preview1 `.wasm` 格式, so that 零件能在 Tier 3 邊緣載具(無 V8、無網路)上執行,消除技術債。
|
|
||||||
|
|
||||||
#### Acceptance Criteria
|
|
||||||
|
|
||||||
1. THE Component_Dispatcher SHALL 支援以 WASM 模式呼叫零件:讀取 `.wasm` 二進位、透過 WASI preview1 runtime 執行、將 input JSON 寫入 stdin、從 stdout 讀取 output JSON。
|
|
||||||
2. WHEN 一個 WASM 零件需要呼叫外部 HTTP API(如 Google Sheets),THE Component_Dispatcher SHALL 透過 host function 注入方式提供網路能力,而非允許 `.wasm` 內部直接發出網路 syscall。
|
|
||||||
3. THE Component_Dispatcher SHALL 在 Tier 1(Cloudflare Workers)環境中,以 `workerd` 內建的 WASM 執行能力執行 WASI preview1 零件。
|
|
||||||
4. WHEN 一個零件的 `runtime_compat` 不包含當前執行環境的 Tier,THE Component_Dispatcher SHALL 回傳錯誤,說明該零件不相容於當前 Tier,而非嘗試執行。
|
|
||||||
5. THE Component_Dispatcher SHALL 在遷移期間同時支援舊有 HTTP endpoint 模式(Service Binding 或外部 URL)與新 WASM 模式,以 Component_Contract 的 `io_model` 欄位區分呼叫路徑。
|
|
||||||
6. FOR ALL 現有 20 個內建零件,遷移後的 WASM 版本 SHALL 通過與原 HTTP 版本相同的 Gherkin 測試案例(round-trip 等效性)。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Requirement 4:零件版本控制與穩定性標籤
|
|
||||||
|
|
||||||
**User Story:** As a Workflow 設計者, I want 在 Cypher 三元組中指定零件的穩定性需求, so that 關鍵業務流程不會因 AI 自動升級零件而中斷。
|
|
||||||
|
|
||||||
#### Acceptance Criteria
|
|
||||||
|
|
||||||
1. THE Cypher_Executor SHALL 支援以下三種零件引用語法:`component://id`(預設 floating)、`component://id@stable`、`component://id@pinned:vN`。
|
|
||||||
2. WHEN 一個 Workflow 引用 `component://id`(floating),THE Component_Dispatcher SHALL 從 Component_Registry 選取該 `id` 下「成功率 × 速度 × 被調用次數」評分最高的版本執行。
|
|
||||||
3. WHEN 一個 Workflow 引用 `component://id@stable`,THE Component_Dispatcher SHALL 使用當前標記為 stable 的版本,並在有更優版本時記錄提示至 KBDB,但不自動切換。
|
|
||||||
4. WHEN 一個 Workflow 引用 `component://id@pinned:vN`,THE Component_Dispatcher SHALL 永遠使用版本 `vN`,即使該版本已被標記為 Deprecated。
|
|
||||||
5. THE Component_Registry SHALL 保留所有歷史版本的 `.wasm` 二進位,不因版本淘汰而刪除檔案。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Requirement 5:Cypher 語意關係擴展
|
|
||||||
|
|
||||||
**User Story:** As a Workflow 設計者, I want Cypher 三元組支援完整的語意關係集合, so that 能描述條件分支、子流程呼叫、前端觸發等複雜業務邏輯。
|
|
||||||
|
|
||||||
#### Acceptance Criteria
|
|
||||||
|
|
||||||
1. THE Cypher_Executor SHALL 解析並執行以下語意關係:`IS_A`(節點類型宣告)、`ON_SUCCESS`(成功後繼)、`ON_FAIL`(失敗後繼)、`ON_CLICK`(前端點擊觸發)、`CALLS_SUBFLOW`(呼叫子 Workflow)、`CONTAINS`(容器包含關係)、`HAS_STYLE`(樣式關聯)、`HAS_BEHAVIOR`(行為關聯)。
|
|
||||||
2. WHEN 解析 `IS_A` 關係,THE Cypher_Executor SHALL 從 Component_Registry 載入對應的零件合約,並以合約的 `input_schema` 驗證節點的輸入 context。
|
|
||||||
3. WHEN 解析 `ON_SUCCESS` 或 `ON_FAIL` 關係,THE Cypher_Executor SHALL 根據上游節點的執行結果(成功或拋出錯誤)決定走向,而非依賴 context 中的特定欄位。
|
|
||||||
4. WHEN 解析 `CALLS_SUBFLOW` 關係,THE Cypher_Executor SHALL 以當前 context 作為子 Workflow 的 initialContext 執行,並將子 Workflow 的輸出合併回主流程 context。
|
|
||||||
5. WHEN 解析 `ON_CLICK` 關係,THE Cypher_Executor SHALL 接受來自前端 Smart_Container 打包的 payload,並以該 payload 作為 Workflow 的 initialContext。
|
|
||||||
6. THE Cypher_Executor SHALL 支援 URI 協議前綴:`component://`(零件引用)、`workflow://`(Workflow 引用)、`ui://`(前端零件引用)、`style://`(樣式零件引用)。
|
|
||||||
7. FOR ALL 合法的 Cypher 三元組序列,THE Cypher_Executor SHALL 保證解析結果的冪等性:對相同輸入三元組集合,無論排列順序,產生語意等效的執行圖(Confluence 屬性)。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Requirement 6:Component Dispatcher 多 Tier 路由
|
|
||||||
|
|
||||||
**User Story:** As a 平台架構師, I want Cypher Executor 透過統一的 Component Dispatcher 介面呼叫跨 Tier 零件, so that Workflow 設計者不需要知道零件部署在哪個 Tier。
|
|
||||||
|
|
||||||
#### Acceptance Criteria
|
|
||||||
|
|
||||||
1. THE Component_Dispatcher SHALL 根據以下優先序決定呼叫路徑:(1)Tier 1:Cloudflare Service Binding(若零件部署為 Worker)或 WASM 直接執行;(2)Tier 2:workerd 叢集 HTTP endpoint;(3)Tier 3:Wazero IPC(stdin/stdout)。
|
|
||||||
2. WHEN Component_Dispatcher 在 Tier 1 環境中呼叫一個 `runtime_compat` 包含 `cf-workers` 的零件,THE Component_Dispatcher SHALL 優先使用 Cloudflare Service Binding,若 binding 不存在則退回 WASM 執行模式。
|
|
||||||
3. WHEN Component_Dispatcher 在 Tier 3 環境中呼叫零件,THE Component_Dispatcher SHALL 只使用 Wazero 執行本地 `.wasm` 檔案,不發出任何網路請求。
|
|
||||||
4. IF Component_Dispatcher 無法在當前 Tier 找到可用的呼叫路徑,THEN THE Component_Dispatcher SHALL 回傳結構化錯誤,包含:零件 id、當前 Tier、嘗試的呼叫路徑清單。
|
|
||||||
5. THE Component_Dispatcher SHALL 對每次零件呼叫記錄執行時間(ms)、成功或失敗狀態,並非同步寫入 KBDB 的 Evaluation Block,不阻擋主流程。
|
|
||||||
6. WHILE Component_Dispatcher 執行零件呼叫,THE Component_Dispatcher SHALL 強制套用 Component_Contract 中的 `max_cold_start_ms` 作為逾時上限,超時後回傳逾時錯誤。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Requirement 7:Tier 3 邊緣離線生存能力
|
|
||||||
|
|
||||||
**User Story:** As a 邊緣載具操作者(無人機、AGV), I want 載具在完全離線環境中仍能執行預載的 Workflow, so that 業務不因網路中斷而停擺。
|
|
||||||
|
|
||||||
#### Acceptance Criteria
|
|
||||||
|
|
||||||
1. THE Tier_3 執行引擎 SHALL 在無網路連線的環境中,使用本地 SQLite 作為 KBDB 替代儲存,執行預先下載的 Cypher Workflow 與 `.wasm` 零件。
|
|
||||||
2. WHEN Tier_3 執行引擎在執行中發現缺少所需零件,THE Tier_3 執行引擎 SHALL 記錄缺失零件的 `id` 與 `input_schema` 至本地 DTN 佇列,待下次連網時以 Burst 傳輸方式送至 Tier_2 請求代工。
|
|
||||||
3. WHEN Tier_3 執行引擎收到來自 Tier_2 的新 `.wasm` 零件,THE Tier_3 執行引擎 SHALL 在執行前對該零件進行 syscall 掃描,確認不含網路或檔案系統 syscall,通過後才載入執行。
|
|
||||||
4. THE Tier_3 執行引擎 SHALL 在 Cypher 圖譜執行中途動態替換失敗零件(如感測器零件因環境變化失效),以 Component_Registry 中相同 `input_schema` 的備用零件繼續執行,不中斷整體 Workflow。
|
|
||||||
5. WHEN Tier_3 執行引擎重新連線至 Tier_2,THE Tier_3 執行引擎 SHALL 將本地執行日誌(包含 trace、評價結果、Pitfall 記錄)同步至 Tier_2 的 KBDB,確保全局狀態一致。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Requirement 8:前端 Web Components 零件庫
|
|
||||||
|
|
||||||
**User Story:** As a 前端開發者, I want 一套以 Web Components 標準實作的 u6u UI 零件庫, so that 畫布上的 UI 元件能在任何現代瀏覽器中獨立運作,不依賴特定前端框架。
|
|
||||||
|
|
||||||
#### Acceptance Criteria
|
|
||||||
|
|
||||||
1. THE Canvas SHALL 提供以下核心 Web Components:`<u6u-btn>`(按鈕)、`<u6u-text-input>`(文字輸入)、`<u6u-text-field>`(多行文字)、`<u6u-chart>`(圖表)、`<u6u-card>`(智慧容器)。
|
|
||||||
2. THE `<u6u-btn>` SHALL 支援以下 HTML attributes:`label`(顯示文字)、`color`(主題色)、`tooltip`(滑鼠懸停提示,純靜態,不觸發 Webhook)、`workflow`(綁定的 Workflow URI,格式為 `workflow://id`)。
|
|
||||||
3. WHEN `<u6u-btn>` 的 `workflow` attribute 被設定且使用者點擊按鈕,THE `<u6u-btn>` SHALL 發出 `u6u:trigger` 自訂事件,事件 detail 包含 `{ workflowId, payload }`。
|
|
||||||
4. THE `<u6u-card>` SHALL 在接收到子元件的 `u6u:trigger` 事件時,自動收集同容器內所有 `<u6u-text-input>` 與 `<u6u-text-field>` 的當前值,合併至事件的 `payload` 後再向上冒泡。
|
|
||||||
5. FOR ALL Web Components,THE Canvas SHALL 保證元件的 HTML attribute 變更能即時反映至視覺渲染,且渲染結果與 attribute 值之間的對應關係具有冪等性(相同 attribute 值永遠產生相同渲染結果)。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Requirement 9:雙面翻轉畫布介面
|
|
||||||
|
|
||||||
**User Story:** As a 業務使用者(非工程師), I want 畫布上每個 UI 元件都能翻面查看並編輯其 Cypher 邏輯連線, so that 不需要寫程式就能理解並修改業務邏輯。
|
|
||||||
|
|
||||||
#### Acceptance Criteria
|
|
||||||
|
|
||||||
1. THE Canvas SHALL 為每個 UI 零件提供「翻面」操作,切換至邏輯視圖後,顯示該零件關聯的 Cypher 三元組(以視覺化節點連線方式呈現)。
|
|
||||||
2. WHEN 使用者在邏輯視圖中修改 Cypher 連線(新增、刪除或修改三元組),THE Canvas SHALL 即時更新對應 Workflow 的 KBDB Block,並在正面 UI 視圖中反映連線狀態變更(如按鈕顏色或 badge 提示)。
|
|
||||||
3. THE Canvas SHALL 在邏輯視圖中提供 Workflow URI 選擇器,列出 KBDB 中所有可用的 Workflow,讓使用者透過下拉選單完成 `ON_CLICK >> workflow://id` 的綁定,不需手動輸入 URI。
|
|
||||||
4. WHEN 使用者在畫布上將兩個 UI 零件拖入同一個 `<u6u-card>` 容器,THE Canvas SHALL 自動在邏輯視圖中顯示 Smart_Container 的自動打包關係,說明哪些輸入值會被自動收集。
|
|
||||||
5. THE Canvas SHALL 在使用者嘗試替換一個已綁定 Workflow 的 UI 零件時,只顯示 Component_Registry 中具備相同觸發能力(即 `u6u:trigger` 事件)的候選零件,過濾掉不相容的零件。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Requirement 10:自動演化評價迴圈
|
|
||||||
|
|
||||||
**User Story:** As a 平台維運者, I want 每次 Workflow 執行後自動觸發 AI 評價, so that 系統能持續識別問題零件並累積避坑知識。
|
|
||||||
|
|
||||||
#### Acceptance Criteria
|
|
||||||
|
|
||||||
1. WHEN 一個 Workflow 執行完畢(無論成功或失敗),THE Evaluator_Agent SHALL 在執行結束後非同步評估以下維度:執行狀態(成功 / 失敗 / 逾時)、各節點執行時間、零件錯誤率趨勢。
|
|
||||||
2. WHEN Evaluator_Agent 發現某零件的錯誤率在連續 5 次執行中超過 50%,THE Evaluator_Agent SHALL 在 KBDB 中為該零件建立 Pitfall_Block,記錄:零件 id、失敗模式描述、首次發現時間戳記。
|
|
||||||
3. WHEN Component_Dispatcher 在 Component_Registry 搜尋零件時,THE Component_Dispatcher SHALL 讀取目標零件的所有關聯 Pitfall_Block,並在選擇版本時降低有 Pitfall 記錄的版本的評分權重。
|
|
||||||
4. WHEN 一個零件連續 30 天無任何 Workflow 引用,THE Component_Registry SHALL 將該零件標記為 `Deprecated`,並從預設搜尋結果中移除,但保留 `.wasm` 二進位與合約。
|
|
||||||
5. WHEN 一個 `Deprecated` 零件再經過 90 天仍無引用,THE Component_Registry SHALL 將該零件移入墓地(tombstone 狀態),從所有搜尋結果中移除,但 `pinned` 版本的 `.wasm` 永遠保留且可被 Component_Dispatcher 存取。
|
|
||||||
6. THE Evaluator_Agent SHALL 以冪等方式處理重複的執行日誌,對相同 `run_id` 的重複評價請求回傳相同結果而不重複建立 Pitfall_Block。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Requirement 11:零件開發指引(Component Authoring Guide)
|
|
||||||
|
|
||||||
**User Story:** As a 零件開發者(使用自己的 AI 工具,如 Claude、GPT、本地模型), I want 平台提供完整的零件開發指引, so that 我的 AI 能根據指引生成符合合約規格的 `.wasm` 零件,並一次通過沙盒驗收。
|
|
||||||
|
|
||||||
#### Acceptance Criteria
|
|
||||||
|
|
||||||
1. THE Component_Registry SHALL 在 `GET /components/guide` 端點提供機器可讀的開發指引文件(Markdown 格式),內容包含:零件合約 YAML 完整範例、I/O 模型說明(stdin/stdout JSON)、各語言(TinyGo、Rust、AssemblyScript)的最小可運行範例程式碼、本地測試指令(`wasmtime` 執行方式)、常見錯誤與解法。
|
|
||||||
2. THE Component_Registry SHALL 在開發指引中明確列出所有禁止行為:網路 syscall、檔案系統 syscall、打包 runtime(QuickJS、Node.js 等)、超過 2MB、混合前後端邏輯於同一零件。
|
|
||||||
3. THE Component_Registry SHALL 在開發指引中提供 `component.contract.yaml` 的 JSON Schema 定義,讓開發者的 AI 能在提交前自行驗證合約格式正確性。
|
|
||||||
4. WHEN 一個零件提交驗收失敗,THE Component_Registry SHALL 在錯誤回應中附上指向開發指引對應章節的錨點連結(如 `#syscall-constraints`),讓開發者的 AI 能直接定位修復方向。
|
|
||||||
5. THE Component_Registry SHALL 提供 `POST /components/validate-contract` 端點,接受 `component.contract.yaml` 內容,回傳格式驗證結果(欄位完整性、schema 合法性、gherkin_tests 最低數量),讓開發者在提交 `.wasm` 前先驗證合約。
|
|
||||||
6. FOR ALL 開發指引中的程式碼範例,THE Component_Registry SHALL 保證範例能通過 Requirement 2 定義的沙盒驗收流程(指引本身是可執行的 ground truth)。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Requirement 12:KBDB Component Registry 整合
|
|
||||||
|
|
||||||
**User Story:** As a 系統開發者, I want Component Registry 完全以 KBDB 的 Template/Block/Slot 機制實作, so that 零件狀態與平台其他知識共享同一個持久化層,不引入新的資料庫。
|
|
||||||
|
|
||||||
#### Acceptance Criteria
|
|
||||||
|
|
||||||
1. THE Component_Registry SHALL 以 KBDB 的 `tpl-component` Template 儲存零件合約,每個零件版本對應一個 Block,Block 的 slots 對應合約的各欄位(id、version、wasi_target、stability、runtime_compat、constraints 等)。
|
|
||||||
2. THE Component_Registry SHALL 以 KBDB 的 Vectorize 索引零件的 `description` 與 `tags` 欄位,支援語意搜尋(如「查詢 Google Sheets 資料」能找到 `gsheets_get_entries`)。
|
|
||||||
3. WHEN Component_Dispatcher 搜尋零件時,THE Component_Registry SHALL 回傳按「成功率 × 速度評分 × 被調用次數」排序的版本清單,最多回傳 10 個候選版本。
|
|
||||||
4. THE Component_Registry SHALL 透過 KBDB 的 HTTP API 存取所有資料,不直接操作 D1 SQL,符合平台的 API-First 通訊鐵律。
|
|
||||||
5. FOR ALL Component_Registry 的讀取操作,THE Component_Registry SHALL 保證在 KBDB 資料不變的情況下,對相同查詢參數回傳相同結果(查詢冪等性)。
|
|
||||||
@@ -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 keys(canonical_id、display_name、category、version、wasi_target、stability、runtime_compat、constraints、input_schema、output_schema、gherkin_tests、wasm_r2_key、cypher_binding_url、service_binding_key、description、tags、success_rate、avg_duration_ms、call_count、status、deprecated_at)
|
|
||||||
- _Requirements: 12.1_
|
|
||||||
|
|
||||||
- [x] 1.2 實作 `POST /components/validate-contract` 端點
|
|
||||||
- 以 Zod schema 驗證 component.contract.yaml 所有必填欄位
|
|
||||||
- 回傳缺失欄位清單(`missing_fields: string[]`)
|
|
||||||
- _Requirements: 1.1, 1.2, 11.5_
|
|
||||||
|
|
||||||
- [ ]* 1.3 寫 property test for 合約格式完整性
|
|
||||||
- **Property 1: 合約格式完整性**
|
|
||||||
- **Validates: Requirements 1.1, 1.2, 1.4**
|
|
||||||
- 用 fast-check 生成隨機缺少任意必填欄位的合約物件,驗證 validator 必定拒絕並回傳該欄位名稱
|
|
||||||
- 用 fast-check 生成包含所有必填欄位的合約物件,驗證 validator 必定通過
|
|
||||||
|
|
||||||
- [x] 1.4 實作 `GET /components/guide` 端點
|
|
||||||
- 回傳 Markdown 格式開發指引(TinyGo 白名單、禁止行為、contract YAML 範例、wasmtime 測試指令)
|
|
||||||
- _Requirements: 11.1, 11.2, 11.3_
|
|
||||||
|
|
||||||
- [x] 1.5 實作 `POST /components` 零件提交端點(沙盒驗收流程)
|
|
||||||
- 依序執行五個驗收步驟:(a) 體積檢查、(b) 冷啟動時間測量、(c) syscall 掃描、(d) Gherkin 測試執行、(e) runtime 相容測試
|
|
||||||
- 任一步驟失敗立即停止,回傳 `{ success: false, failed_step, reason, guide_anchor, component_id, version }`
|
|
||||||
- 通過後以 KBDB HTTP API 寫入 Block(`block_id = comp-{id}-{version}`)
|
|
||||||
- 同時上傳 `.wasm` 至 R2,slot `wasm_r2_key` 記錄 R2 key
|
|
||||||
- _Requirements: 2.1, 2.2, 2.3_
|
|
||||||
|
|
||||||
- [ ]* 1.6 寫 property test for 沙盒驗收流程正確性
|
|
||||||
- **Property 2: 沙盒驗收流程正確性**
|
|
||||||
- **Validates: Requirements 2.1, 2.2**
|
|
||||||
- 用 fast-check 生成在步驟 N 失敗的零件,驗證回應包含步驟 N 名稱與原因,且不執行步驟 N+1
|
|
||||||
|
|
||||||
- [ ]* 1.7 寫 property test for 零件提交冪等性與持久性
|
|
||||||
- **Property 3: 零件提交冪等性與持久性**
|
|
||||||
- **Validates: Requirements 2.3, 2.4**
|
|
||||||
- 用 fast-check 生成通過驗收的零件,提交後讀取合約驗證所有欄位 round-trip 一致
|
|
||||||
- 對相同 (id, version) 重複提交 N 次,驗證 KBDB 只存在一個 Block
|
|
||||||
|
|
||||||
- [x] 2. 實作 WASI preview1 shim 與 WASM 執行核心(`cypher-executor/src/lib/`)
|
|
||||||
- [x] 2.1 實作輕量 WASI preview1 shim(`wasi-shim.ts`)
|
|
||||||
- 實作 `fd_read`(從 stdin buffer 讀取)、`fd_write`(寫入 stdout/stderr buffer)、`proc_exit`(拋出 Error)、`random_get`(`crypto.getRandomValues`)
|
|
||||||
- 其餘 syscall 一律回傳 ENOSYS(76)
|
|
||||||
- 不引入任何外部依賴(不使用 `@cloudflare/workers-wasi`)
|
|
||||||
- _Requirements: 3.1, 3.3_
|
|
||||||
|
|
||||||
- [x]* 2.2 寫單元測試 for WASI shim
|
|
||||||
- 測試 `fd_read` 正確讀取 stdin buffer(含多次讀取、邊界條件)
|
|
||||||
- 測試 `fd_write` 正確寫入 stdout buffer(fd=1)與 stderr buffer(fd=2)
|
|
||||||
- 測試 `proc_exit` 拋出 Error
|
|
||||||
|
|
||||||
- [x] 2.3 實作 Tier 1 WASM 執行器(`wasm-executor.ts`)
|
|
||||||
- 從 R2 fetch `.wasm` ArrayBuffer
|
|
||||||
- `WebAssembly.compile` 後快取 `WebAssembly.Module`(Worker 記憶體,跨請求共享)
|
|
||||||
- 建立 WASI shim,注入 stdin = `JSON.stringify(input)`
|
|
||||||
- `WebAssembly.instantiate(module, imports)` → 呼叫 `_start()` 或 `main()`
|
|
||||||
- 從 stdout buffer 讀取輸出,`JSON.parse` 後回傳
|
|
||||||
- 套用 `max_cold_start_ms` 逾時(`Promise.race`)
|
|
||||||
- _Requirements: 3.1, 3.3, 6.6_
|
|
||||||
|
|
||||||
- [ ]* 2.4 寫 property test for Component Dispatcher 路由正確性
|
|
||||||
- **Property 4: Component Dispatcher 路由正確性**
|
|
||||||
- **Validates: Requirements 3.1, 3.5, 3.6**
|
|
||||||
- 用 fast-check 生成合法 JSON input,驗證 WASM 執行路徑輸出與預期語意等效
|
|
||||||
|
|
||||||
- [x] 3. 建立 `validate_json.wasm` 第一個真實零件(TinyGo)
|
|
||||||
- [x] 3.1 撰寫 `validate_json` TinyGo 原始碼(`u6u-core/registry/components/validate_json/main.go`)
|
|
||||||
- 只使用白名單 import:`os`、`io`、`encoding/json`
|
|
||||||
- 讀取 stdin JSON,解析 `json_string` 欄位,嘗試 `json.Unmarshal`
|
|
||||||
- 成功輸出 `{"valid":true}`,失敗輸出 `{"valid":false,"error":"..."}`
|
|
||||||
- _Requirements: 3.1, 11.6_
|
|
||||||
|
|
||||||
- [x] 3.2 撰寫 `validate_json` component.contract.yaml
|
|
||||||
- 包含所有必填欄位、gherkin_tests(happy path + error path)
|
|
||||||
- `runtime_compat: ["cf-workers","workerd","wazero"]`
|
|
||||||
- _Requirements: 1.1, 1.2, 1.4_
|
|
||||||
|
|
||||||
- [ ]* 3.3 寫單元測試 for validate_json(Gherkin 場景驗證)
|
|
||||||
- 測試合法 JSON 輸入回傳 `{"valid":true}`
|
|
||||||
- 測試非法 JSON 輸入回傳 `{"valid":false,"error":...}`
|
|
||||||
|
|
||||||
- [x] 4. Checkpoint — Phase 0 驗收
|
|
||||||
- 確認 `validate_json.wasm` 能在 CF Workers 環境中透過 WASM 執行器執行
|
|
||||||
- 確認 Component Registry `/guide`、`/validate-contract`、`/components` 端點可用
|
|
||||||
- 確認所有 Phase 0 測試通過,向使用者確認是否繼續 Phase 1
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Phase 1:遷移現有零件(20 個 HTTP → WASM)
|
|
||||||
|
|
||||||
- [x] 5. 升級 Component Dispatcher 支援雙模式(`cypher-executor/src/lib/component-loader.ts`)
|
|
||||||
- [x] 5.1 重構 `ComponentDescriptor` 型別(移除舊 `http_endpoint`,新增 `component_type`)
|
|
||||||
- 定義 `ComponentType = 'wasm' | 'cypher_binding' | 'service_binding'`
|
|
||||||
- 新版 `ComponentDescriptor` 欄位:`component_type`、`wasm_r2_key`、`runtime_compat`、`max_cold_start_ms`、`url`(cypher_binding)、`method`、`binding`(service_binding)、`path`
|
|
||||||
- _Requirements: 3.5_
|
|
||||||
|
|
||||||
- [x] 5.2 實作路由決策邏輯(`component-dispatcher.ts`)
|
|
||||||
- 查 Component Registry 取得合約
|
|
||||||
- 依 `component_type` 分流:`wasm` → WASM 執行器;`cypher_binding` → HTTP POST 到外部 URL;`service_binding` → CF Service Binding
|
|
||||||
- 檢查 `runtime_compat` 是否包含當前 Tier,不包含則回傳 `RUNTIME_INCOMPATIBLE` 錯誤
|
|
||||||
- _Requirements: 3.4, 6.1, 6.2_
|
|
||||||
|
|
||||||
- [ ]* 5.3 寫 property test for Dispatcher 錯誤結構完整性
|
|
||||||
- **Property 5: Dispatcher 錯誤結構完整性**
|
|
||||||
- **Validates: Requirements 3.4, 6.4**
|
|
||||||
- 用 fast-check 生成 (component_id, tier) 組合,當 runtime_compat 不含當前 tier,驗證錯誤回應同時包含 component_id、tier、attempted_paths 三個欄位
|
|
||||||
|
|
||||||
- [x] 6. 遷移 20 個內建零件(`u6u-core/builtins/` → `u6u-core/registry/components/`)
|
|
||||||
- [x] 6.1 為每個零件撰寫 TinyGo 原始碼與 component.contract.yaml(批次作業)
|
|
||||||
- 每個零件:只用白名單 import、stdin/stdout JSON I/O、附帶 gherkin_tests
|
|
||||||
- 需要外部 API 的零件(如 gsheets):改用 `cypher_binding` 模式,contract 中記錄 `cypher_binding_url`
|
|
||||||
- _Requirements: 3.6, 11.6_
|
|
||||||
|
|
||||||
- [x] 6.2 透過 `POST /components` 批次提交 20 個零件至 Component Registry
|
|
||||||
- 每個零件通過沙盒驗收後自動寫入 KBDB
|
|
||||||
- 驗證 20 個零件的 Gherkin 測試全部通過
|
|
||||||
- _Requirements: 2.1, 3.6_
|
|
||||||
|
|
||||||
- [ ]* 6.3 寫 property test for 歷史版本永久保留不變量
|
|
||||||
- **Property 8: 歷史版本永久保留不變量**
|
|
||||||
- **Validates: Requirements 4.5, 10.5**
|
|
||||||
- 用 fast-check 生成已上架零件,標記為 deprecated 後,驗證 `.wasm` 仍可從 R2 讀取,pinned 引用仍可執行
|
|
||||||
|
|
||||||
- [x] 7. 實作 Component Registry 查詢端點
|
|
||||||
- [x] 7.1 實作 `GET /components/:id` 與 `GET /components/:id/versions`
|
|
||||||
- 透過 KBDB HTTP API 查詢 `tpl-component` blocks
|
|
||||||
- `/versions` 回傳按「成功率 × 速度評分 × 被調用次數」排序的版本清單(最多 10 個)
|
|
||||||
- _Requirements: 12.3_
|
|
||||||
|
|
||||||
- [x] 7.2 實作 `GET /components/search?q=...` 語意搜尋
|
|
||||||
- 呼叫 KBDB Vectorize API,以 `description` + `tags` 欄位做語意搜尋
|
|
||||||
- _Requirements: 12.2_
|
|
||||||
|
|
||||||
- [ ]* 7.3 寫單元測試 for 查詢冪等性
|
|
||||||
- 驗證相同查詢參數在 KBDB 資料不變時回傳相同結果
|
|
||||||
|
|
||||||
- [x] 8. Checkpoint — Phase 1 驗收
|
|
||||||
- 確認 20 個零件全部通過沙盒驗收並存入 KBDB
|
|
||||||
- 確認 Component Dispatcher 雙模式路由正確(WASM + cypher_binding)
|
|
||||||
- 確認所有 Phase 1 測試通過,向使用者確認是否繼續 Phase 2
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Phase 2:Cypher 語意擴展 + Multi-Tier Dispatcher
|
|
||||||
|
|
||||||
- [x] 9. 擴展 Cypher Triplet Parser(`cypher-executor/src/actions/triplet-parser.ts`)
|
|
||||||
- [x] 9.1 新增 EdgeType 定義
|
|
||||||
- 在現有 `PIPE | IF | FOREACH | CONTINUE` 基礎上新增:`IS_A | ON_SUCCESS | ON_FAIL | ON_CLICK | CALLS_SUBFLOW | CONTAINS | HAS_STYLE | HAS_BEHAVIOR`
|
|
||||||
- _Requirements: 5.1_
|
|
||||||
|
|
||||||
- [x] 9.2 實作 URI 協議解析函數(`resolveComponentId`)
|
|
||||||
- 解析 `component://id`、`component://id@stable`、`component://id@pinned:vN`、`workflow://id`、`ui://id`、`style://id`
|
|
||||||
- 回傳 `{ type, canonicalId, stability, pinnedVersion? }`
|
|
||||||
- _Requirements: 4.1, 5.6_
|
|
||||||
|
|
||||||
- [ ]* 9.3 寫 property test for 零件 URI 解析 Round-Trip
|
|
||||||
- **Property 6: 零件 URI 解析 Round-Trip**
|
|
||||||
- **Validates: Requirements 4.1**
|
|
||||||
- 用 fast-check 生成合法 URI 字串,解析後再序列化,驗證語意等效;驗證解析出的 id、stability、pinnedVersion 與原始 URI 完全一致
|
|
||||||
|
|
||||||
- [ ]* 9.4 寫 property test for Cypher 三元組解析 Confluence
|
|
||||||
- **Property 9: Cypher 三元組解析 Confluence(順序無關性)**
|
|
||||||
- **Validates: Requirements 5.7**
|
|
||||||
- 用 fast-check 生成合法三元組集合,用 `fc.shuffledSubarray` 打亂順序,驗證 `parseTriplets` 產生相同節點集合與邊集合
|
|
||||||
|
|
||||||
- [x] 10. 擴展 GraphExecutor 執行語意(`cypher-executor/src/graph-executor.ts`)
|
|
||||||
- [x] 10.1 實作 `IS_A` 關係處理
|
|
||||||
- 從 Component Registry 載入零件合約,以 `input_schema` 驗證節點輸入 context
|
|
||||||
- _Requirements: 5.2_
|
|
||||||
|
|
||||||
- [x] 10.2 實作 `ON_SUCCESS` / `ON_FAIL` 分支執行
|
|
||||||
- 在 `executeNode` 的 try/catch 中,成功走 `ON_SUCCESS` 邊,失敗走 `ON_FAIL` 邊(傳遞 error context)
|
|
||||||
- _Requirements: 5.3_
|
|
||||||
|
|
||||||
- [x] 10.3 實作 `CALLS_SUBFLOW` 子流程呼叫
|
|
||||||
- 從 KBDB 載入子 Workflow 定義,建立子 GraphExecutor 執行,將輸出合併回主流程 context
|
|
||||||
- _Requirements: 5.4_
|
|
||||||
|
|
||||||
- [x] 10.4 實作 `ON_CLICK` 前端觸發處理
|
|
||||||
- 接受來自前端 Smart Container 打包的 payload,作為 Workflow initialContext
|
|
||||||
- _Requirements: 5.5_
|
|
||||||
|
|
||||||
- [x] 10.5 實作 `CONTAINS` / `HAS_STYLE` / `HAS_BEHAVIOR` 結構語意解析(不執行,僅記錄圖結構)
|
|
||||||
- _Requirements: 5.1_
|
|
||||||
|
|
||||||
- [x] 11. 實作版本選擇策略(Component Dispatcher 升級)
|
|
||||||
- [x] 11.1 實作 floating 版本選擇算法
|
|
||||||
- 從 KBDB 查詢該 id 下所有版本,計算「成功率 × 速度評分 × 被調用次數」,選取最高分版本
|
|
||||||
- _Requirements: 4.2_
|
|
||||||
|
|
||||||
- [x] 11.2 實作 stable / pinned 版本選擇
|
|
||||||
- `stable`:使用當前標記為 stable 的版本,有更優版本時記錄提示至 KBDB 但不切換
|
|
||||||
- `pinned:vN`:永遠使用版本 vN,即使已 deprecated
|
|
||||||
- _Requirements: 4.3, 4.4_
|
|
||||||
|
|
||||||
- [ ]* 11.3 寫 property test for 版本選擇策略正確性
|
|
||||||
- **Property 7: 版本選擇策略正確性**
|
|
||||||
- **Validates: Requirements 4.2, 4.4**
|
|
||||||
- 用 fast-check 生成版本集合(各有不同 success_rate、avg_duration_ms、call_count),驗證 floating 選最高分;驗證 pinned:vN 無論其他版本評分如何永遠選 vN
|
|
||||||
|
|
||||||
- [x] 12. 實作 Evaluator Agent 與評價迴圈(`cypher-executor/src/actions/`)
|
|
||||||
- [x] 12.1 實作 `execution-evaluator.ts`(擴展現有 `execution-logger.ts`)
|
|
||||||
- Workflow 執行完畢後非同步寫入 KBDB Evaluation Block(`tpl-evaluation`)
|
|
||||||
- 記錄:run_id、workflow_id、component_id、verdict、duration_ms、error_message、evaluated_at
|
|
||||||
- 冪等處理:相同 run_id 不重複建立 Block
|
|
||||||
- _Requirements: 10.1, 10.6_
|
|
||||||
|
|
||||||
- [x] 12.2 實作 Pitfall Block 建立邏輯
|
|
||||||
- 偵測某零件連續 5 次執行錯誤率 > 50%,建立 `tpl-pitfall` Block
|
|
||||||
- 版本選擇時降低有 Pitfall 記錄的版本評分權重
|
|
||||||
- _Requirements: 10.2, 10.3_
|
|
||||||
|
|
||||||
- [x] 12.3 實作零件自動 Deprecated / Tombstone 狀態轉換
|
|
||||||
- 連續 30 天無引用 → 標記 `deprecated`,從預設搜尋移除
|
|
||||||
- 再 90 天無引用 → 標記 `tombstone`,從所有搜尋移除(pinned `.wasm` 永久保留)
|
|
||||||
- _Requirements: 10.4, 10.5_
|
|
||||||
|
|
||||||
- [ ]* 12.4 寫 property test for 系統操作冪等性
|
|
||||||
- **Property 11: 系統操作冪等性**
|
|
||||||
- **Validates: Requirements 10.6, 12.5**
|
|
||||||
- 用 fast-check 生成 run_id,對相同 run_id 呼叫 Evaluator N 次,驗證 KBDB 只存在一個 Evaluation Block
|
|
||||||
- 驗證相同查詢參數在資料不變時回傳相同結果
|
|
||||||
|
|
||||||
- [x] 13. Checkpoint — Phase 2 驗收
|
|
||||||
- 確認新 EdgeType 全部可解析執行(IS_A、ON_SUCCESS、ON_FAIL、CALLS_SUBFLOW、ON_CLICK)
|
|
||||||
- 確認版本選擇策略(floating / stable / pinned)行為正確
|
|
||||||
- 確認 Evaluator Agent 冪等寫入 KBDB
|
|
||||||
- 確認所有 Phase 2 測試通過,向使用者確認是否繼續 Phase 3
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Phase 3:前端畫布(Web Components + 雙面翻轉)
|
|
||||||
|
|
||||||
- [x] 14. 建立 Web Components 零件庫(`u6u-core/web-components/`)
|
|
||||||
- [x] 14.1 實作 `<u6u-btn>` Custom Element
|
|
||||||
- 支援 attributes:`label`、`color`、`tooltip`、`workflow`、`disabled`
|
|
||||||
- `workflow` 設定且點擊時發出 `u6u:trigger` CustomEvent(`{ workflowId, payload }`)
|
|
||||||
- `workflow` 未設定時點擊不發出事件,console 輸出警告
|
|
||||||
- _Requirements: 8.2, 8.3_
|
|
||||||
|
|
||||||
- [x] 14.2 實作 `<u6u-text-input>` 與 `<u6u-text-field>` Custom Elements
|
|
||||||
- 支援 `name`、`placeholder`、`value` attributes
|
|
||||||
- `value` property 可被 `<u6u-card>` 讀取
|
|
||||||
- _Requirements: 8.1_
|
|
||||||
|
|
||||||
- [x] 14.3 實作 `<u6u-card>` Smart Container
|
|
||||||
- 攔截子元件的 `u6u:trigger` 事件(`stopPropagation`)
|
|
||||||
- 收集同容器內所有 `<u6u-text-input>` / `<u6u-text-field>` 的 name-value 對
|
|
||||||
- 合併至 payload 後重新發出 `u6u:trigger`(`bubbles: true, composed: true`)
|
|
||||||
- _Requirements: 8.4_
|
|
||||||
|
|
||||||
- [x] 14.4 實作 `<u6u-chart>` Custom Element(基礎版)
|
|
||||||
- 支援 `data` attribute(JSON 字串)、基本折線圖渲染
|
|
||||||
- _Requirements: 8.1_
|
|
||||||
|
|
||||||
- [ ]* 14.5 寫 property test for Web Components 事件與渲染冪等性
|
|
||||||
- **Property 10: Web Components 事件與渲染冪等性**
|
|
||||||
- **Validates: Requirements 8.3, 8.4, 8.5**
|
|
||||||
- 用 fast-check 生成 workflow URI,驗證點擊後 `u6u:trigger` detail.workflowId 與 URI id 完全一致
|
|
||||||
- 用 fast-check 生成具名 input 集合置於 u6u-card,驗證收集到的 payload 包含所有 name-value 對
|
|
||||||
- 用 fast-check 生成 attribute 值,對同一元件設定相同值 N 次,驗證渲染結果冪等
|
|
||||||
|
|
||||||
- [x] 15. 建立雙面翻轉畫布(`inkstone-admin/frontend/web/`)
|
|
||||||
- [x] 15.1 實作翻轉狀態機(`Canvas.tsx`)
|
|
||||||
- 狀態:`UIView` ↔ `LogicView`(點擊翻面按鈕切換)
|
|
||||||
- `LogicView` → `Editing`(修改三元組)→ `Saving`(確認儲存)→ `LogicView`
|
|
||||||
- _Requirements: 9.1_
|
|
||||||
|
|
||||||
- [x] 15.2 實作邏輯視圖(Cypher 三元組視覺化)
|
|
||||||
- 顯示零件關聯的 Cypher 三元組(節點連線方式)
|
|
||||||
- 修改三元組後即時更新 KBDB Workflow Block(透過 KBDB HTTP API)
|
|
||||||
- _Requirements: 9.2_
|
|
||||||
|
|
||||||
- [x] 15.3 實作 Workflow URI 選擇器
|
|
||||||
- 列出 KBDB 中所有可用 Workflow,下拉選單完成 `ON_CLICK >> workflow://id` 綁定
|
|
||||||
- _Requirements: 9.3_
|
|
||||||
|
|
||||||
- [x] 15.4 實作 Smart Container 拖放與自動打包關係顯示
|
|
||||||
- 拖入同一 `<u6u-card>` 時,邏輯視圖自動顯示 CONTAINS 關係與自動打包說明
|
|
||||||
- _Requirements: 9.4_
|
|
||||||
|
|
||||||
- [x] 15.5 實作零件替換過濾器
|
|
||||||
- 替換已綁定 Workflow 的 UI 零件時,只顯示具備 `u6u:trigger` 能力的候選零件
|
|
||||||
- _Requirements: 9.5_
|
|
||||||
|
|
||||||
- [x] 15.6 在畫布中整合 u6u Web Components(Dogfooding)
|
|
||||||
- 畫布 UI 本身使用 `<u6u-btn>`、`<u6u-card>`、`<u6u-text-input>` 組裝
|
|
||||||
- 驗證 Web Components 在 React 19 環境中正確運作
|
|
||||||
- _Requirements: 8.1, 9.1_
|
|
||||||
|
|
||||||
- [ ]* 15.7 寫整合測試 for 畫布翻轉流程
|
|
||||||
- 測試 UIView → LogicView → Editing → Saving → LogicView 完整狀態轉換
|
|
||||||
- 測試 KBDB 寫入成功與失敗兩種情境
|
|
||||||
|
|
||||||
- [x] 16. Final Checkpoint — 全平台驗收
|
|
||||||
- 確認四個 Phase 的所有測試通過(`pnpm test` in each service)
|
|
||||||
- 確認 Dogfooding:畫布本身用 u6u Web Components 組裝,每一層都是下一層的第一個用戶
|
|
||||||
- 確認 KBDB 不變量:仍只有三張表(blocks / templates / slots)
|
|
||||||
- 確認 API-First 鐵律:所有跨服務通訊只透過 HTTP API
|
|
||||||
- 向使用者確認所有任務完成
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Phase 4:邊緣基礎設施(Tier 3 支援)
|
|
||||||
|
|
||||||
> 這個 Phase 不在零件遷移範圍內,是獨立的基礎設施工作。
|
|
||||||
|
|
||||||
- [ ] 17. Credentials 邊緣支援評估與改寫(`u6u-core/credentials/`)
|
|
||||||
- [ ] 17.1 評估 Tier 3 是否需要 Credentials
|
|
||||||
- 場景:無人機在有網路時呼叫外部 API(如取得感測器資料),需要 access_token
|
|
||||||
- 結論:Tier 3 需要在連網時從 Tier 2 取得 Credential,離線時使用本地快取的加密 token
|
|
||||||
- _Requirements: 7.1, 7.2_
|
|
||||||
|
|
||||||
- [ ] 17.2 實作 Credential 本地快取機制(Tier 3 用)
|
|
||||||
- Tier 3 Go 排程引擎在連網時從 Tier 2 Credentials Worker 取得加密 token
|
|
||||||
- 存入本地 SQLite(AES-GCM 加密,key 存於設備安全儲存)
|
|
||||||
- 離線時從本地快取讀取,過期時加入 DTN 佇列等待更新
|
|
||||||
- _Requirements: 7.1, 7.3_
|
|
||||||
|
|
||||||
- [ ] 18. Go Cypher Executor(Tier 3 邊緣執行引擎)(`u6u-core/executor/`)
|
|
||||||
- [ ] 18.1 用 Go 實作 Cypher 三元組解析器
|
|
||||||
- 解析 `"A >> 關係 >> B"` 格式,建立執行圖(nodes + edges)
|
|
||||||
- 支援 IS_A、ON_SUCCESS、ON_FAIL、CALLS_SUBFLOW、ON_CLICK 語意關係
|
|
||||||
- 對應 `cypher-executor/src/actions/triplet-parser.ts` 的 Go 版本
|
|
||||||
- _Requirements: 5.1, 5.7_
|
|
||||||
|
|
||||||
- [ ] 18.2 用 Go + Wazero 實作 WASM 零件執行器
|
|
||||||
- 載入本地 `.wasm` 檔案,透過 Wazero 原生 WASI preview1 執行
|
|
||||||
- 注入 `u6u` host module(`http_request` host function,透過 DTN 或直接 HTTP)
|
|
||||||
- stdin/stdout JSON I/O,與 Tier 1/2 的 `wasm-executor.ts` 語意等效
|
|
||||||
- _Requirements: 7.1, 7.4_
|
|
||||||
|
|
||||||
- [ ] 18.3 實作 DTN 佇列(離線請求緩衝)
|
|
||||||
- 零件需要網路但當前離線時,寫入本地 SQLite DTN 佇列
|
|
||||||
- 連網時 Burst 傳輸:批次送出佇列中的請求,接收回應後繼續執行
|
|
||||||
- _Requirements: 7.2, 7.5_
|
|
||||||
|
|
||||||
- [ ]* 18.4 整合測試:validate_json.wasm 在 Wazero 執行
|
|
||||||
- 確認同一個 `.wasm` 在 Tier 1(wasi-shim.ts)和 Tier 3(Wazero)執行結果一致
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Notes
|
|
||||||
|
|
||||||
- 標記 `*` 的子任務為選填,可跳過以加速 MVP 交付
|
|
||||||
- 每個任務都引用具體的 Requirements 條款以確保可追溯性
|
|
||||||
- Checkpoint 任務確保每個 Phase 完成後有明確的驗收點
|
|
||||||
- Property tests 使用 fast-check,每個屬性最少執行 100 次迭代
|
|
||||||
- 所有跨服務呼叫只透過 KBDB HTTP API,不直接操作 D1 SQL
|
|
||||||
- TinyGo 零件只使用白名單 import(`os`、`io`、`encoding/json`)
|
|
||||||
|
|
||||||
## Phase 5:u6u-mcp 對齊新 Registry + u6u-gui 前端
|
|
||||||
|
|
||||||
> 壓測前必須完成,讓 AI(u6u-mcp)和人類(u6u-gui)都能操作新的 WASM 零件架構。
|
|
||||||
|
|
||||||
- [x] 19. 更新 u6u-mcp 對齊新 Component Registry(`u6u-mcp/src/tools/`)
|
|
||||||
- [x] 19.1 更新 `u6u_publish_component`
|
|
||||||
- 舊:呼叫 `/components/publish`,payload 為 `{ component_id, gherkin, api_config }`
|
|
||||||
- 新:呼叫 `POST /components`,payload 為 `{ contract: ComponentContract, wasm_base64: string }`
|
|
||||||
- 新增 `contract` 和 `wasm_base64` 參數,更新工具描述說明 TinyGo 零件提交流程
|
|
||||||
|
|
||||||
- [x] 19.2 更新 `u6u_search_components`
|
|
||||||
- 舊:呼叫 `/components/match`(不存在的端點)
|
|
||||||
- 新:呼叫 `GET /components/search?q={query}`(新 Registry 語意搜尋端點)
|
|
||||||
- 更新工具描述:AI 可用自然語言搜尋零件(如「查詢 Google Sheets 資料」)
|
|
||||||
|
|
||||||
- [x] 19.3 更新 `u6u_get_component`
|
|
||||||
- 舊:讀舊格式 slots(`component_id`、`name`、`published_at`)
|
|
||||||
- 新:對齊 `tpl-component` slot 欄位(`canonical_id`、`display_name`、`category`、`version`、`stability`、`wasm_r2_key` 等)
|
|
||||||
- 呼叫新 Registry `GET /components/:id` 端點
|
|
||||||
|
|
||||||
- [x] 19.4 新增 `u6u_get_component_guide` 工具
|
|
||||||
- 呼叫 `GET /components/guide`,回傳開發指引給 AI
|
|
||||||
- AI 在開發新零件前可先讀取指引,確保生成符合規範的 TinyGo 程式碼
|
|
||||||
|
|
||||||
- [x] 20. 建立 u6u-gui 前端(`u6u-gui/`)
|
|
||||||
- [x] 20.1 建立 Cloudflare Pages 專案結構
|
|
||||||
- React 19 + Vite + Tailwind CSS v4
|
|
||||||
- 整合 `@u6u/web-components`(alias 指向 `u6u-core/web-components/src`)
|
|
||||||
- wrangler.toml 設定 Pages 部署
|
|
||||||
|
|
||||||
- [x] 20.2 建立主畫布頁面(`/canvas`)
|
|
||||||
- 整合 `Canvas.tsx`(從 inkstone-admin 移植)
|
|
||||||
- 連接 Cypher Executor API(`POST /cypher/execute`)
|
|
||||||
- 連接 Component Registry API(搜尋、查詢零件)
|
|
||||||
- AI 操作後(透過 u6u-mcp 修改 KBDB)畫布即時反映變更
|
|
||||||
|
|
||||||
- [x] 20.3 建立零件庫頁面(`/components`)
|
|
||||||
- 列出所有已上架零件(呼叫 `GET /components/search`)
|
|
||||||
- 顯示零件合約、評分、版本歷史
|
|
||||||
- 提供「提交新零件」入口(連結到開發指引)
|
|
||||||
|
|
||||||
- [x] 20.4 建立 Workflow 管理頁面(`/workflows`)
|
|
||||||
- 列出所有 Workflow(從 KBDB 查詢 `tpl-workflow`)
|
|
||||||
- 點擊進入畫布編輯
|
|
||||||
- 顯示執行歷史(Evaluation Block)
|
|
||||||
|
|
||||||
- [x] 20.5 部署至 Cloudflare Pages
|
|
||||||
- `pnpm build && npx wrangler pages deploy dist`
|
|
||||||
- 設定環境變數(KBDB_URL、CYPHER_URL、REGISTRY_URL)
|
|
||||||
@@ -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 token(gmail / google_sheets 等)
|
|
||||||
5. `acr push` 部署 workflow,取得 Webhook URL
|
|
||||||
6. 網頁 POST /webhooks/named/{name}/trigger,結果存 Google Sheets
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 二、場景各步驟驗證狀態
|
|
||||||
|
|
||||||
### Step 1:acr init → api_key
|
|
||||||
- [x] `acr init` Standard 模式完成,api_key 存入 `~/.arcrun/config.yaml`
|
|
||||||
- [x] 已驗證:`mode: standard, api_key: ak_...` 正確
|
|
||||||
|
|
||||||
### Step 2:acr parts scaffold → AI 看到零件格式
|
|
||||||
- [x] `acr parts` 列出 21 個零件,完全內建,不依賴 registry.arcrun.dev
|
|
||||||
- [x] `acr parts scaffold google_sheets` 輸出 spreadsheet_id / range / operation / values 格式與 credentials.yaml 範本
|
|
||||||
- [x] 已驗證:輸出可直接貼入 YAML
|
|
||||||
|
|
||||||
### Step 3:acr recipe push → 打外部 API
|
|
||||||
- [x] `acr recipe push` 上傳成功,回傳 rec_hash
|
|
||||||
- [x] workflow 使用 `component: rec_xxxxxxxx`,acr push 後 trigger 能正確呼叫外部 API
|
|
||||||
- [x] 已驗證(2026-04-18):httpbin_post recipe → trigger → httpbin.org/post 回傳正確 ✅
|
|
||||||
|
|
||||||
### Step 4:acr creds push → 自動注入 token
|
|
||||||
- [x] `POST /credentials` API 完成,以 `{api_key}:cred:{name}` 存入 KV
|
|
||||||
- [x] Webhook trigger 時 injectCredentials 從 KV 取得 token 自動注入
|
|
||||||
- [x] `/register` 現在回傳 `encryption_key`,`acr init` 自動存入 config
|
|
||||||
- [x] `acr creds push` 從 config 讀 encryption_key,不再需要手動設定環境變數
|
|
||||||
- [x] 已驗證(2026-04-18):beta@arcrun.dev 帳號完整流程:init → creds push → trigger → credential 注入成功 ✅
|
|
||||||
|
|
||||||
### Step 5:acr push → Webhook URL
|
|
||||||
- [x] `acr push workflow.yaml` 部署成功,顯示 Webhook URL 和完整 curl 範例
|
|
||||||
- [x] config 中的 `component` / 參數在 push 時套入 graph 節點
|
|
||||||
- [x] 已驗證(2026-04-18):sheet-test workflow push 成功 ✅
|
|
||||||
|
|
||||||
### Step 6:網頁 POST → 執行 → 結果到 Google Sheets
|
|
||||||
- [x] `POST /webhooks/named/{name}/trigger -H 'X-Arcrun-API-Key: ...'` 觸發執行正常
|
|
||||||
- [x] google_sheets 零件有實作(append row 到 Sheets API)
|
|
||||||
- [x] 已驗證(2026-04-18):trigger sheet-test → 報「缺少 credential」(符合預期,credential 未上傳)✅
|
|
||||||
- [ ] **未驗證**:真實 google_oauth token + acr creds push → trigger → Google Sheets 實際寫入
|
|
||||||
- 需要真實 OAuth token 才能完整驗證
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 三、封測啟動阻擋項
|
|
||||||
|
|
||||||
P0 全部清除才啟動封測。
|
|
||||||
|
|
||||||
| # | 項目 | 狀態 | 說明 |
|
|
||||||
|---|------|------|------|
|
|
||||||
| 1 | acr parts scaffold 正確輸出 | ✅ 完成 | 21 個零件內建清單 |
|
|
||||||
| 2 | acr recipe push 端對端 | ✅ 完成 | httpbin_post 驗證通過 |
|
|
||||||
| 3 | acr creds push 代碼 | ✅ 完成 | 需 ARCRUN_ENCRYPTION_KEY |
|
|
||||||
| 4 | credential 注入端對端 | ✅ 完成 | 無 token 時錯誤訊息正確 |
|
|
||||||
| 5 | acr push + webhook trigger | ✅ 完成 | 端對端驗證通過 |
|
|
||||||
| 6 | acr creds push 實測 | ✅ 完成 | /register 回傳 encryption_key,acr init 自動存入 config(CLI 1.0.9)|
|
|
||||||
| 7 | Google Sheets 真實寫入 | ⚠️ 部分驗證 | credential 注入已驗證;實際 Sheets 寫入需真實 OAuth token |
|
|
||||||
| 8 | 第三方服務認證 recipe | ✅ 完成 | 20 個服務(Notion/Slack/GitHub/OpenAI 等),CLI 1.1.0 |
|
|
||||||
| **9** | **cypher-executor outbound HTTP fetch 全失效** | ✅ **已解決 2026-05-13**(CF 同 zone 自循環死鎖,改走 workers.dev)| 詳見下方專段 |
|
|
||||||
| **10** | **multi-node chain context propagation 漏失** | ✅ **已解決 2026-05-13**(ON_SUCCESS/ON_FAIL/IF/ON_CLICK 沒 spread baseCtx)| 詳見下方專段 |
|
|
||||||
|
|
||||||
**目前狀況**:P0 全部解決。
|
|
||||||
- #9 修復方式:component worker URL 從 `*.arcrun.dev`(同 cypher zone)改走 `arcrun-{name}.{WORKER_SUBDOMAIN}.workers.dev`(避開同 zone 自循環)
|
|
||||||
- #10 修復方式:4 個 edge type 補 `{...baseCtx, ...result}`,跟 PIPE/FOREACH 一致
|
|
||||||
|
|
||||||
兩個 P0 解完 mira 7 節點 workflow 端對端通(含真 Claude 16 秒呼叫)。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### ✅ P0 #9(2026-05-13 已解決):cypher-executor outbound fetch 全失效
|
|
||||||
|
|
||||||
**完整事件報告(含誤判路徑)**:[docs/incidents/2026-05-13-cypher-outbound-522.md](../../../docs/incidents/2026-05-13-cypher-outbound-522.md)
|
|
||||||
|
|
||||||
**修復方式**:cypher-executor fetch component worker 從 `*.arcrun.dev`(同 zone)改走 `arcrun-{name}.{WORKER_SUBDOMAIN}.workers.dev`。對外 `cypher.arcrun.dev` 不變,用戶 0 感知。
|
|
||||||
|
|
||||||
**改動檔案**(2026-05-13):
|
|
||||||
- `cypher-executor/src/lib/component-loader.ts`:`wasmWorkerUrl(canonicalId, subdomain)` 簽名加 subdomain 參數 + URL pattern 改 workers.dev
|
|
||||||
- `cypher-executor/src/actions/auth-dispatcher.ts`:同步新簽名
|
|
||||||
- `cypher-executor/src/types.ts`:`Bindings` 加 `WORKER_SUBDOMAIN: string`
|
|
||||||
- `cypher-executor/wrangler.toml`:`[vars]` 加 `WORKER_SUBDOMAIN = "uncle6-me"`
|
|
||||||
- 5 個 component worker 在 dashboard 啟用 workers.dev URL(kbdb-get / kbdb-ingest / kbdb-create-block / kbdb-patch-block / claude-api,**未來新 component 也都要開**)
|
|
||||||
|
|
||||||
**驗證**:cypher-executor → kbdb-get / claude-api 從 522 → 200。mira `acr run wiki_synthesis` 5 節點 workflow 跑通前 3 節點(kbdb_get chain)。
|
|
||||||
|
|
||||||
**Self-hosted fork 注意**:必須改 `wrangler.toml [vars] WORKER_SUBDOMAIN` 為自己的 CF 帳號 subdomain,並把所有 component worker 在 dashboard 啟用 workers.dev URL。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### ✅ P0 #10(2026-05-13 已解決):multi-node chain context propagation 漏失
|
|
||||||
|
|
||||||
**現象**:cypher binding workflow 從第 2 個節點開始,原始 input context(top-level `api_key` / `mira_token` 等)丟失,下游節點 `{{api_key}}` 模板原文未替換傳給零件 → 401 Unauthorized 或類似錯。
|
|
||||||
|
|
||||||
**測試重現**:
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
flow:
|
|
||||||
- "input >> ON_SUCCESS >> n1"
|
|
||||||
- "n1 >> ON_SUCCESS >> n2"
|
|
||||||
config:
|
|
||||||
n1: { component: kbdb_get, api_key: "{{api_key}}", block_id: "{{b1}}" }
|
|
||||||
n2: { component: kbdb_get, api_key: "{{api_key}}", block_id: "{{b2}}" }
|
|
||||||
context: { api_key: "ak_xxx", b1: "...", b2: "..." }
|
|
||||||
```
|
|
||||||
|
|
||||||
n1 收到 ctx 含 `api_key / b1 / b2` ✓ → 跑通。
|
|
||||||
n2 收到的 ctx 只有 `n1.output spread`(blocks/count/success/block_id),**`api_key / b1 / b2` 不見**,`{{api_key}}` 原文傳到零件回 401。
|
|
||||||
|
|
||||||
**根因**:`graph-executor.ts` 在 PIPE / FOREACH 邊類型已修「baseCtx ∪ result」,但 **ON_SUCCESS / ON_FAIL / IF / ON_CLICK 四個 edge type 沒套同模式**,直接把 `result` 當下游 ctx 傳,丟掉原始 context。
|
|
||||||
|
|
||||||
**修法**(`cypher-executor/src/graph-executor.ts` line 407 / 415 / 423 / 472):
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// 改前
|
|
||||||
result = await this.executeNode(nextNode, graph, result, ...);
|
|
||||||
|
|
||||||
// 改後(同 PIPE/FOREACH 模式)
|
|
||||||
const baseCtx = (typeof context === 'object' && context !== null) ? context as Record<string, unknown> : {};
|
|
||||||
const baseResult = (typeof result === 'object' && result !== null) ? result as Record<string, unknown> : {};
|
|
||||||
const mergedCtx = { ...baseCtx, ...baseResult };
|
|
||||||
result = await this.executeNode(nextNode, graph, mergedCtx, ...);
|
|
||||||
```
|
|
||||||
|
|
||||||
**驗證**:mira `acr run wiki_synthesis` 7 節點 workflow 端對端跑通(16 秒,含真 Claude 呼叫)。每個節點都拿到正確 `api_key` 不再 401。
|
|
||||||
|
|
||||||
**歷史脈絡**:類似問題 2026-05-07 commit e8fca33 在 FOREACH edge 已修一次("FOREACH preserves outer context"),但當時沒同步處理另外 4 個 edge type。本次補完。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### ✅ P0 #10 補完三個衍生問題(2026-05-13 晚 ~ 2026-05-14)
|
|
||||||
|
|
||||||
P0 #10 修完後 mira 嘗試做 wiki 多段結構,又踩出三個 cypher binding 設計缺陷。**都是同一天解掉**。
|
|
||||||
|
|
||||||
#### A. interpolateData() 不遞迴 nested object
|
|
||||||
|
|
||||||
**現象**:`set` / `kbdb_create_block` 的 `values: { text: "{{classify.data.text}}" }`、`tags_json: ["facet:{{paragraph.facet}}"]` 等 nested config 內的 `{{x}}` 不被替換,原文傳給零件。
|
|
||||||
|
|
||||||
**根因**:`interpolateData()` 只 iterate top-level,對非 string 值(object / array)直接 pass-through 不下沉。
|
|
||||||
|
|
||||||
**修法**:拆 `interpolateString` + `interpolateValue`(遞迴 object / array),`interpolateData` 改 call `interpolateValue`。
|
|
||||||
|
|
||||||
**測試**:`set values: { text: "hello {{name}}", arr: ["item {{name}}"] }` 帶 `name=world` → 全展開。
|
|
||||||
|
|
||||||
#### B. ctx 沒存上游 output 的 node id namespace
|
|
||||||
|
|
||||||
**現象**:`{{classify.data.text}}` 找不到上游 classify 的 output;只能用 `{{data.text}}`(直接 spread 取),但會被下個節點覆蓋,多節點 chain 用不了。
|
|
||||||
|
|
||||||
**根因**:`propagateCtx` 只把上游 result spread 進 ctx,沒額外存 `[node.id]: result`。
|
|
||||||
|
|
||||||
**修法**:`propagateCtx` 改回傳 `{ ...baseCtx, ...baseResult, [upstreamNodeId]: upstreamResult }`。讓下游能用 `{{node_id.data.text}}` 從 namespace 取,永不被覆蓋。
|
|
||||||
|
|
||||||
**測試**:5 節點 chain 用 `{{load_schema.blocks.0.content}}` / `{{classify.data.text}}` 全展開。
|
|
||||||
|
|
||||||
#### C. FOREACH 找 iterable 只看 result,不看 ctx + 不看 nested
|
|
||||||
|
|
||||||
**現象**:mira wiki_synthesis 雙重 FOREACH(外層 `對每個 paragraph`、內層 `對每個 triplet`),外層 OK,內層跑 0 次。
|
|
||||||
|
|
||||||
**根因 (C1)**:`getIterableFromContext(result, key)` 只看當前節點 output。`result` 是 `create_paragraph` output(`{data, success}`),不含 paragraphs。但 `paragraphs` 早就在 ctx 從 classify spread 來。
|
|
||||||
|
|
||||||
**根因 (C2)**:當外層 FOREACH 把 `paragraph` item 注入 ctx,內層 FOREACH 要找 `paragraph.triplets`。`getIterableFromContext` 只看 top-level,看不到 `paragraph` 物件裡的 `triplets`。
|
|
||||||
|
|
||||||
**修法**:
|
|
||||||
- (C1) FOREACH `result` 找不到 iterable → fallback 找 `context`
|
|
||||||
- (C2) `getIterableFromContext` 加一輪「掃 ctx 內每個 object 找 nested key」
|
|
||||||
|
|
||||||
**測試**:mira wiki_synthesis 3 層樹(wiki-page → paragraphs → triplets)端對端跑通,KBDB 內驗證 `物理 AI` wiki 有 2 段 paragraph + 4 個 triplet,parent_id 正確接到對應 paragraph。
|
|
||||||
|
|
||||||
#### Edge type 一致化
|
|
||||||
|
|
||||||
抽 `propagateCtx(context, result, upstreamNodeId)` helper,5 個 edge type(PIPE / ON_SUCCESS / ON_FAIL / IF / ON_CLICK / FOREACH)全部用同一 function 組下游 ctx。**未來新 edge type 必須用這 helper**,避免再漏。
|
|
||||||
|
|
||||||
#### CLI validator 同步
|
|
||||||
|
|
||||||
`cli/src/lib/yaml-parser.ts` validateRelations 加 regex 支援 `對每個 X` / `FOREACH X` 迭代器命名(之前 validator 字串完全比對擋住,但 graph-builder 執行端早已支援)。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 三-A、P1 待改進(不擋封測,但 mira 已踩到)
|
|
||||||
|
|
||||||
#### ✅ P1 #3:cypher-executor `scheduled()` handler(2026-05-14 完成)
|
|
||||||
|
|
||||||
**原痛點**:cron 零件只做 expression validation;cypher-executor 沒 `scheduled()` handler。寫了 cron 首節點的 workflow 不會真的跑。
|
|
||||||
|
|
||||||
**之前的 workaround**(已撤):mira 寫了個 `/mira/wiki-from-raw` route 從前端 fire-and-forget 觸發 wiki_synthesis。但這違反「一律 arcrun-native」原則,也讓 arcrun 永遠補不齊缺失。**已刪 route,回 arcrun-native 路線**。
|
|
||||||
|
|
||||||
**落地**:
|
|
||||||
1. `wrangler.toml`:`[triggers] crons = ["* * * * *"]`(每分鐘 tick)
|
|
||||||
2. `src/lib/cron-match.ts`:5 欄位 cron expression matcher(支援 `*` / `N` / `*/N` / `1-5` / `5,10` 組合)
|
|
||||||
3. `src/scheduled.ts`:scheduled handler 掃 KV `cron-idx:` prefix,比對 controller.scheduledTime,匹配就 `executeWebhookGraph` 背景跑
|
|
||||||
4. `routes/webhooks-named.ts`:acr push 偵測首節點是 cron 零件 → 抽 `cron_expr` 存進 record + 額外寫 `cron-idx:{api_key}:{name}` 輕量 index entry。DELETE 一併清理
|
|
||||||
5. `src/index.ts`:export default 改 `{ fetch, scheduled }`
|
|
||||||
6. cypher-executor 自己加 `workers_dev = true` 給未來 self-trigger 用(fork 用 path-based 子 trigger 也走 workers.dev 避同 zone)
|
|
||||||
|
|
||||||
**workflow YAML 慣例**:
|
|
||||||
```yaml
|
|
||||||
flow:
|
|
||||||
- "my_cron >> ON_SUCCESS >> downstream_node"
|
|
||||||
config:
|
|
||||||
my_cron:
|
|
||||||
component: cron
|
|
||||||
cron_expr: "*/5 * * * *" # 每 5 分鐘
|
|
||||||
```
|
|
||||||
acr push 就會自動建立 cron-idx 並開始定時觸發。
|
|
||||||
|
|
||||||
**測試**:`tests/arcrun-test/cron_heartbeat.yaml` — 每分鐘 fire 一次 + set 節點 log。
|
|
||||||
`wrangler tail arcrun-cypher-executor` 應看 `[scheduled] trigger cron_heartbeat ...`。
|
|
||||||
|
|
||||||
**對應 use case**:mira `mira_feed_watcher`(7B.3h,下一輪做)/ RSS 每日抓 / voice-stt 每小時掃 / 等所有 cron-driven source。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
#### P1 #1:workflow 缺 IF/branch 能力(2026-05-14 mira 7B.3f 提出)
|
|
||||||
|
|
||||||
**現象**:mira 想做「找有則 PATCH 沒則 CREATE」(index-entry upsert),arcrun 目前只有 `ON_SUCCESS` + `對每個 X`(FOREACH)+ 已存在但壞掉的 `if_control`(見已知限制 #1),沒有 `>> ON_TRUE >>` / `>> ON_FALSE >>` 條件路由。
|
|
||||||
|
|
||||||
**短期 workaround**(已採用,2026-05-14):建 `kbdb_upsert_block` 零件,把分支邏輯封進零件內部(GET by page_name → 找到 PATCH 沒找到 POST)。caller 看到的是單純的 upsert 介面。
|
|
||||||
|
|
||||||
**長期解**:升 `if_control` false branch 路由 / 加 `>> ON_TRUE >>` edge type,讓 workflow 層可表達分支。對未來所有「找則改否則建」/「條件分流」場景都會撞到,不只 mira。
|
|
||||||
|
|
||||||
**位置**:cypher-executor/src/graph-executor.ts edge type 處理(5 個 edge type 抽出 `propagateCtx` 後新增 IF 應該不難)+ cli/src/lib/yaml-parser.ts validator。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
#### P0 #11(2026-05-14 已解決):interpolateString stringify array 撐爆下游
|
|
||||||
|
|
||||||
**現象**:mira_feed_watcher 用 `items: "{{list_raws.blocks}}"` 把 kbdb_get 拿到的 blocks 陣列傳給 filter 零件。watcher 跑 264ms 完成、0 raw 處理。
|
|
||||||
|
|
||||||
**根因**:`interpolateString` 看到模板就用 `String.replace`,非 string 值(陣列)一律 `JSON.stringify`。filter 零件收到字串 `"[{...},{...}]"` 不是 array,items 被忽略 → 0 matches → FOREACH 跑 0 次。
|
|
||||||
|
|
||||||
**修法**:`interpolateString` 加 single-ref pass-through 規則:若整個值是純單一 `{{x}}` 引用,回 raw value(保留 array / object 型別)。多 ref / 混合文字仍 stringify 拼接字串。
|
|
||||||
|
|
||||||
**測試**:mira_feed_watcher 推到 prod 後下一個 cron tick 觀察 wiki-processed tag 是否在 raws 上出現。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 三-B、新零件加入紀錄
|
|
||||||
|
|
||||||
| 日期 | 零件 | 動機 | 對應 SDD |
|
|
||||||
|---|---|---|---|
|
|
||||||
| 2026-05-14 | `kbdb_upsert_block` | mira 7B.3f index-entry per-entity upsert,繞過 workflow 缺 IF/branch 能力(P1 #1)。內部 GET by page_name → 找到 PATCH 沒找到 POST。page_name 當 idempotency key。 | polaris/mira/.agents/specs/mira-app/design.md §3.5.12.4.1 |
|
|
||||||
|
|
||||||
**新零件 checklist(避免 P1 #1 重蹈 kbdb_upsert_block 漏白名單覆轍)**:
|
|
||||||
|
|
||||||
每加一個 API/data 零件(不是 logic primitive),都要:
|
|
||||||
- [ ] `registry/components/{name}/main.go` + `component.contract.yaml` + `go.mod`
|
|
||||||
- [ ] `tinygo build -target=wasi` 通
|
|
||||||
- [ ] `.component-builds/{name}/` 完整 4 檔(`wrangler.toml` 含 `workers_dev = true` + `pnpm-lock.yaml` + `tsconfig.json` + `src/index.ts`)
|
|
||||||
- [ ] **`cypher-executor/src/lib/component-loader.ts` 的 `WASM_HTTP_RUNNER_IDS` 加 canonical_id**(漏這條 cypher-executor 永遠拋「找不到零件」,端對端會死靜悄悄)
|
|
||||||
- [ ] `acr validate workflow.yaml` 通
|
|
||||||
- [ ] 直接 curl `https://{kebab}.arcrun.dev` + `https://arcrun-{kebab}.{WORKER_SUBDOMAIN}.workers.dev` 都 200
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 三-C、P1 #2:workers_dev = true 全 component 自動化(2026-05-14 已收)
|
|
||||||
|
|
||||||
**原痛點**:每新部署一個 component worker,要去 CF Dashboard 手動 Enable workers.dev URL,否則 cypher-executor fetch 該 worker 會 404。
|
|
||||||
|
|
||||||
**解**:32 個 `.component-builds/*/wrangler.toml` 全部加 `workers_dev = true`。CI 每次 deploy 自動啟用對應 workers.dev URL,零手動。
|
|
||||||
|
|
||||||
**未來新 component**:模板 (`component-worker-template/`) 應該預設帶 `workers_dev = true`,新人 fork 不會踩。已列入「新零件 checklist」第 3 條。
|
|
||||||
|
|
||||||
**為何不走 `*.acr-comp.uncle6.me` 自訂 zone**:CF Universal SSL 只發一層子域,sub-sub `*.acr-comp.uncle6.me` 不蓋;要 ACM ($10/月) 才能簽。違反 arcrun「fork 後 self-host 用 free tier 跑得起來」核心目標。workers_dev=true 走 CF 默認的 workers.dev cert,free tier OK,更乾淨。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 原 P0 #9 調查紀錄(保留作歷史參考)
|
|
||||||
|
|
||||||
**現象**:cypher-executor 的 `makeHttpRunner` (`cypher-executor/src/lib/component-loader.ts:142`) 對任何 outbound URL fetch 都回 CF **522 (origin timeout, ~1000ms)**。
|
|
||||||
|
|
||||||
**測試矩陣**(mira repo `polaris/mira/arcrun/wiki_synthesis.yaml` 端對端壓測時發現):
|
|
||||||
|
|
||||||
| 路徑 | 結果 | 證明 |
|
|
||||||
|---|---|---|
|
|
||||||
| 本機 curl → kbdb-get.arcrun.dev | 200 (22ms) | KBDB worker 本身健康 |
|
|
||||||
| cypher-executor → kbdb-get.arcrun.dev (HTTP) | **522** (1002ms) | outbound HTTP fetch 壞 |
|
|
||||||
| cypher-executor → claude-api.arcrun.dev (HTTP) | **522** | 同 zone 也壞 |
|
|
||||||
| cypher-executor → httpbin.org (外部) | **522** | 不只是 same-account loop |
|
|
||||||
| cypher-executor → string_ops (Service Binding) | 200 ✅ | SVC_* 路徑正常 |
|
|
||||||
| acr run hello (built-in via SB) | ✅ | hello.yaml 仍跑得通 |
|
|
||||||
|
|
||||||
**衍生小 bug**:`set` 零件 input schema 變了(要 `assignments` 陣列或 `values` 物件,不是 `value`)。tests/arcrun-test/hello.yaml 跑 string_ops 沒踩到。
|
|
||||||
|
|
||||||
**影響範圍(封測啟動阻擋)**:
|
|
||||||
|
|
||||||
- 任何用戶 workflow 含 outbound HTTP 都壞:
|
|
||||||
- 用戶 `acr recipe push` 後 trigger → 打外部 API 全 522(推翻 P0 #2 4/18 紀錄)
|
|
||||||
- 用戶要存資料進 arcrun 內建 KBDB → 522(mira 7B.3c 卡這裡)
|
|
||||||
- 任何 auth primitive 走獨立 Worker URL 路徑 → 522
|
|
||||||
- Google Sheets 寫入 → 522(推翻 P0 #7「待真實 OAuth 驗證」評估,根本還沒到 OAuth 步驟就壞)
|
|
||||||
- 只有「全內建邏輯零件 + 純 service binding」workflow 還能跑
|
|
||||||
|
|
||||||
**根因(2026-05-13 確認):5/8-5/9 9 次 manual `wrangler deploy` 把含 WIP bug 的 cypher-executor 推上 prod**
|
|
||||||
|
|
||||||
調查路徑(按時序):
|
|
||||||
|
|
||||||
1. 一開始懷疑 free tier CPU cap(10ms / invocation)→ 對照測試「5 節點 SB chain」跑了 2.2 秒卻通過,**推翻**
|
|
||||||
2. 換懷疑 CF zone 規則 / Bot Fight Mode → dashboard bindings 乾淨無攔截,**推翻**
|
|
||||||
3. 用戶補繳費恢復 Workers Paid → 重測仍全 522,**徹底排除付費假設**
|
|
||||||
4. 看 dashboard Version History:5/8 4 次 + 5/9 5 次 = **9 次 manual `wrangler deploy by uncle6.me`**
|
|
||||||
5. 對照 GitHub Actions:4/24 後完全沒 deploy(最後是 commit e222116 `fix(wasi-shim)`)
|
|
||||||
6. 對照 git:本機 main **領先 origin/main 3 commits 未 push**,含:
|
|
||||||
- `497f92a feat(arcrun): recipe system + resumable workflow + component registry canon`
|
|
||||||
- `e8fca33 feat(cypher): 3-node wiki workflow end-to-end (FOREACH + nested interp + unified parsing)`
|
|
||||||
- `519423c feat(arcrun): mira wiki page with tag filter + accumulated WIP`(自描含 `cypher-executor: auth-dispatcher / wasi-shim adjustments (WIP)`)
|
|
||||||
|
|
||||||
**結論**:那 9 次 manual deploy 把上面 3 個 unpushed commit 的 cypher-executor 改動推上 prod,其中至少一個改動破壞了 outbound fetch(最可能是 519423c WIP 內的 wasi-shim / auth-dispatcher 改動)。GitHub Actions 因為沒 push 沒跑,CI 沒 catch,4/18-4/24 那段 SDD「驗證通過」的紀錄是 truth,現在 prod 是壞的版本。
|
|
||||||
|
|
||||||
**為何 SB 路徑沒事 / HTTP 路徑全死**:SB 走 cypher-executor 內部 service binding API(`env.SVC_X.fetch()`),不經過 outbound HTTP code path。HTTP 路徑走 `makeHttpRunner` (component-loader.ts:142) 的 `fetch(url, ...)`,這條路被 WIP code 弄壞。具體壞在哪要 diff 那 3 個 commit 的 cypher-executor 改動才知道。
|
|
||||||
|
|
||||||
**驗證 wrangler tail 證據**:trigger 任何 outbound HTTP 的 graph,cypher-executor 自己 `wallTime: 497ms, cpuTime: 2ms, outcome: ok`、無 logs、無 exceptions。代表 cypher-executor 把「fetch 失敗的 522 response」當作 component 正常輸出包回 client,自己沒撞任何錯。
|
|
||||||
|
|
||||||
**解法(三選一)**:
|
|
||||||
|
|
||||||
- **A. Rollback prod 到 4/24 的 e222116** — CF dashboard → arcrun-cypher-executor → Deployments → 找 4/24 那筆 → Rollback。5 分鐘恢復 outbound fetch,丟失 wiki workflow / recipe / resumable 等 cypher 端 WIP 改動(但前端、registry components、KBDB blocks 都不丟,因為它們是別的 worker / 別的儲存)。**richblack 操作。**
|
|
||||||
- **B. Diff 3 個 unpushed commit 找出壞掉的改動修掉** — 不丟功能,但要動 src code 走 SDD 協議,30min - 數小時。
|
|
||||||
- **C. 架構切換**(mira 老闆 2026-05-13 提的):sub-workflow 自殺交棒模式,cypher-executor 不再做集中 graph executor。從根本繞開「cypher-executor 一個 invocation 跑長 graph」這條脆弱路徑。一勞永逸但是大改。
|
|
||||||
|
|
||||||
**衍生小 bug 仍要修**(跟付費無關):`set` 零件 input schema 變了(要 `assignments` 陣列或 `values` 物件,不是 `value`)。要嘛 update set 零件 contract 容錯,要嘛文件化新 schema。
|
|
||||||
|
|
||||||
**為什麼這直接擋封測**:
|
|
||||||
|
|
||||||
封測場景 Step 6「網頁 POST → 結果存 Google Sheets」走 google_sheets 零件 (HTTP outbound to googleapis.com)。如果 cypher-executor outbound 全壞,**封測者跑任何含外部 API 的 workflow 都會 522**,不是「Google Sheets 實際寫入未驗證」級別的小事。
|
|
||||||
|
|
||||||
**也直接擋 mira**:[polaris/mira/.agents/specs/mira-app/tasks.md] 7B.3c-f(wiki 合成 workflow)卡這裡。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 四、封測前 P3(啟動當天)
|
|
||||||
|
|
||||||
- [ ] 用封測者 email 呼叫 `/register`,取得 api_key
|
|
||||||
- [ ] 將 ARCRUN_ENCRYPTION_KEY 以安全方式提供給封測者
|
|
||||||
- [ ] 確認聯絡管道
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 五、已知限制(封測期間不修)
|
|
||||||
|
|
||||||
1. `if_control` false branch 不路由(條件 false 時後續節點不執行)→ 升級計畫見 P1 #1,2026-05-14 mira 用 `kbdb_upsert_block` workaround
|
|
||||||
2. 多節點 context 不自動解包(上游輸出 flat merge,下游需從 `data.result` 取值)
|
|
||||||
3. 用戶自製邏輯零件(Phase 5)封測後才實作
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 六、實作進度
|
|
||||||
|
|
||||||
| Phase | 內容 | 狀態 |
|
|
||||||
|-------|------|------|
|
|
||||||
| 0 | Workers 部署、CI/CD、DNS | ✅ |
|
|
||||||
| 1 | CLI 基礎(init / validate / run / parts) | ✅ |
|
|
||||||
| 2 | /register、/cypher/execute、21 個零件 | ✅ |
|
|
||||||
| 3 | Service Binding 架構、{{variable}} 插值、ON_FAIL 修正 | ✅ |
|
|
||||||
| 4 | 動態 Recipe KV(CRUD)、acr recipe 指令 | ✅ |
|
|
||||||
| 5 | 用戶自製邏輯零件(WASM push) | ⏸ 封測後 |
|
|
||||||
| 6 | Credential 多租戶({api_key}:cred:{name})、acr creds push | ✅ |
|
|
||||||
| 7 | acr parts 內建清單、acr parts scaffold | ✅ |
|
|
||||||
| 8 | /webhooks/named、acr push 改版、config 套入 graph | ✅ |
|
|
||||||
|
|
||||||
### CLI 版本
|
|
||||||
|
|
||||||
| 版本 | 變更 |
|
|
||||||
|------|------|
|
|
||||||
| 1.1.0 | auth recipe 系統:20 個服務預建(Notion/Slack/GitHub/OpenAI/Google SA 等);acr auth-recipe 指令 |
|
|
||||||
| 1.0.9 | /register 回傳 encryption_key;acr init 自動儲存;creds push 不需手動設環境變數 |
|
|
||||||
| 1.0.8 | acr push → webhooks/named;config 套入 graph;acr parts 內建清單 |
|
|
||||||
| 1.0.7 | acr creds push → POST /credentials |
|
|
||||||
| 1.0.6 | acr recipe push / list / delete |
|
|
||||||
| 1.0.5 | hello.yaml 改 string_ops,--version 修正 |
|
|
||||||
| 1.0.4 | config/context 分離 |
|
|
||||||
| 1.0.3 | 初始發布 |
|
|
||||||
@@ -1,274 +0,0 @@
|
|||||||
# Auth Recipe System — SDD
|
|
||||||
|
|
||||||
> 文件類型:SDD(Software Design Document)
|
|
||||||
> 建立:2026-04-19
|
|
||||||
> 狀態:實作中
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 一、目標
|
|
||||||
|
|
||||||
封測前完成,讓封測者碰到「我要連 X 服務」都有辦法,而不是「還沒做」。
|
|
||||||
|
|
||||||
**精神**:`http_request` 是容器零件,auth recipe 是「如何對這個服務認證」的設定層,兩者分離。新增一個服務 = 寫一份 YAML,不需要改程式碼、不需要重新部署 Worker。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 二、三層模型
|
|
||||||
|
|
||||||
```
|
|
||||||
Layer 3: Auth Recipe (YAML/JSON in RECIPES KV)
|
|
||||||
公共,描述「如何對某服務認證」
|
|
||||||
key: auth_recipe:{service}
|
|
||||||
例: auth_recipe:notion, auth_recipe:slack
|
|
||||||
↓ 引用
|
|
||||||
Layer 2: Auth Primitive (TypeScript in Worker)
|
|
||||||
四個通用認證邏輯:static_key | oauth2 | service_account | mtls
|
|
||||||
封測只做 static_key 和 service_account (Google JWT)
|
|
||||||
↑ 使用
|
|
||||||
Layer 1: Tenant Secret (CREDENTIALS_KV)
|
|
||||||
每個 tenant 自己的加密 credential
|
|
||||||
key: {api_key}:cred:{name}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 三、Auth Recipe Schema
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
interface AuthRecipeDefinition {
|
|
||||||
kind: 'auth_recipe'; // 區別 RecipeDefinition 用
|
|
||||||
service: string; // canonical_id, e.g. "notion"
|
|
||||||
version: number;
|
|
||||||
primitive: 'static_key' | 'oauth2' | 'service_account' | 'mtls';
|
|
||||||
base_url: string;
|
|
||||||
display_name?: string;
|
|
||||||
description?: string;
|
|
||||||
|
|
||||||
// service_account 用
|
|
||||||
service_account_kind?: 'google_jwt';
|
|
||||||
token_exchange?: {
|
|
||||||
endpoint: string; // e.g. https://oauth2.googleapis.com/token
|
|
||||||
scopes: string[];
|
|
||||||
};
|
|
||||||
|
|
||||||
required_secrets: Array<{
|
|
||||||
key: string; // CREDENTIALS_KV 的名稱
|
|
||||||
label: string; // UI/CLI 顯示
|
|
||||||
type?: 'string' | 'json_blob'; // default: string
|
|
||||||
help?: string;
|
|
||||||
help_url?: string;
|
|
||||||
}>;
|
|
||||||
|
|
||||||
inject: {
|
|
||||||
header?: Record<string, string>; // "Authorization": "Bearer {{secret.token}}"
|
|
||||||
query?: Record<string, string>;
|
|
||||||
body?: Record<string, string>;
|
|
||||||
// path:注入 endpoint URL path 的 secret(2026-05-29 加)。
|
|
||||||
// 解 telegram 類「token 在 URL path」(/bot{token}/)—— header/query/body 都不適用。
|
|
||||||
// key = 模板變數名,API recipe 的 endpoint 用 {{auth.K}} 引用。
|
|
||||||
// 例:auth_recipe:telegram inject.path = { bot_token: "{{secret.telegram_bot_token}}" }
|
|
||||||
// recipe:telegram_send endpoint = "https://api.telegram.org/bot{{auth.bot_token}}/sendMessage"
|
|
||||||
path?: Record<string, string>;
|
|
||||||
};
|
|
||||||
|
|
||||||
created_at: number;
|
|
||||||
updated_at: number;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Template 語法**:
|
|
||||||
- `{{secret.KEY}}` → 從 tenant 的 CREDENTIALS_KV 解密取值
|
|
||||||
- `{{runtime.access_token}}` → service_account JWT exchange 後取得的短期 token
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 四、KV 儲存
|
|
||||||
|
|
||||||
沿用現有 `RECIPES` KV namespace,不新增 binding。
|
|
||||||
|
|
||||||
```
|
|
||||||
auth_recipe:{service} → AuthRecipeDefinition JSON
|
|
||||||
```
|
|
||||||
|
|
||||||
與現有 `recipe:{id}` / `idx:{hash}` 的 key 不衝突。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 五、執行流程
|
|
||||||
|
|
||||||
### 5.1 static_key(涵蓋 ~80% 服務)
|
|
||||||
|
|
||||||
```
|
|
||||||
trigger → graph-executor
|
|
||||||
→ injectCredentials(componentId, input, env, apiKey)
|
|
||||||
→ resolveAuthRecipe("notion", RECIPES KV)
|
|
||||||
→ 取得 required_secrets: [{key: "notion_token", ...}]
|
|
||||||
→ 從 CREDENTIALS_KV 讀 "{api_key}:cred:notion_token"
|
|
||||||
→ AES-GCM 解密
|
|
||||||
→ 展開 inject.header templates ({{secret.notion_token}} → 實際值)
|
|
||||||
→ 注入 _auth_headers, _auth_query, _auth_body 到 input
|
|
||||||
→ makeAuthRecipeRunner(recipe)
|
|
||||||
→ 合併 _auth_headers 到 fetch headers
|
|
||||||
→ 呼叫 recipe.base_url + input._path
|
|
||||||
→ 回傳結果
|
|
||||||
```
|
|
||||||
|
|
||||||
### 5.2 service_account(Google 家族)
|
|
||||||
|
|
||||||
```
|
|
||||||
injectCredentials
|
|
||||||
→ resolveAuthRecipe("google_sheets_sa", RECIPES KV)
|
|
||||||
→ 解密 service_account_json (JSON blob)
|
|
||||||
→ signGoogleJwt(serviceAccountJson, scopes) via crypto.subtle (RSASSA-PKCS1-v1_5 + SHA-256)
|
|
||||||
→ POST token_exchange.endpoint → 取得 access_token
|
|
||||||
→ 展開 inject.header: { Authorization: "Bearer {{runtime.access_token}}" }
|
|
||||||
→ 注入 _auth_headers
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 六、Context key 慣例
|
|
||||||
|
|
||||||
注入後的認證資訊以 `_auth_` 前綴攜帶,不污染業務欄位:
|
|
||||||
|
|
||||||
| Key | 說明 |
|
|
||||||
|---|---|
|
|
||||||
| `_auth_headers` | `Record<string, string>` — 要合併進 fetch headers |
|
|
||||||
| `_auth_query` | `Record<string, string>` — 要附加到 URL query string |
|
|
||||||
| `_auth_body` | `Record<string, string>` — 要合併進 request body |
|
|
||||||
| `_auth_path` | `Record<string, string>` — endpoint URL path 用(2026-05-29 加)。`makeRecipeRunner` 的 endpoint interpolate 用 `{{auth.K}}` 從這裡取值 |
|
|
||||||
|
|
||||||
`makeAuthRecipeRunner` / `makeRecipeRunner` 在發出 fetch 前讀取這些 `_auth_*` 欄位,
|
|
||||||
之後從 auto-body 中剔除所有 `_` 前綴欄位(不洩漏給下游)。
|
|
||||||
|
|
||||||
## 七、API recipe 的 auth_service(多 recipe 共用一把 auth,2026-05-29 加)
|
|
||||||
|
|
||||||
`RecipeDefinition` 加 `auth_service?: string` 欄位:API recipe **自報它屬於哪個服務**,
|
|
||||||
auth-dispatcher 用它查 `auth_recipe:{auth_service}`,而非假設 componentId == service name。
|
|
||||||
|
|
||||||
- 讓多個 recipe 共用同一把 auth:`recipe:kbdb_get` / `kbdb_create_block` 都設 `auth_service: "kbdb"`
|
|
||||||
→ 共用唯一的 `auth_recipe:kbdb`,加新 action 不必複製 auth recipe。
|
|
||||||
- auth-dispatcher 解析順序:先查 `recipe:{componentId}` 拿 `auth_service`,有就用它;
|
|
||||||
沒有則 fallback 把 componentId 當 service name(向後相容舊行為)。
|
|
||||||
- 這是「服務身分標籤」非「許可清單」:auth_recipe 只定義「怎麼認證」,不含「誰准用」。
|
|
||||||
授權由發 API key 的服務裁決,arcrun 不做內部授權判斷(見 DECISIONS.md「arcrun 不做授權判斷」)。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 七、向後相容
|
|
||||||
|
|
||||||
- 現有 `BUILTIN_API_RECIPES`(gmail, google_sheets, telegram, line_notify)**不動**
|
|
||||||
- 現有 `BUILTIN_CREDENTIALS_MAP` **不動**
|
|
||||||
- auth recipe 解析在 component-loader step 5.5(新增),在 step 6 KV recipe 和 step 7 builtin 之前
|
|
||||||
- 若 `auth_recipe:{service}` 不存在 → 繼續往下走,行為與現在完全相同
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 八、新增/修改的檔案
|
|
||||||
|
|
||||||
| 檔案 | 類型 | 說明 |
|
|
||||||
|---|---|---|
|
|
||||||
| `cypher-executor/src/routes/recipes.ts` | 修改 | 加 `AuthRecipeDefinition` 型別、`resolveAuthRecipe`、`/auth-recipes` CRUD routes |
|
|
||||||
| `cypher-executor/src/actions/credential-injector.ts` | 修改 | 加 auth recipe 分支:static_key + service_account |
|
|
||||||
| `cypher-executor/src/lib/jwt-signer.ts` | 新增 | Google JWT signing via crypto.subtle |
|
|
||||||
| `cypher-executor/src/lib/component-loader.ts` | 修改 | step 5.5 auth recipe lookup + `makeAuthRecipeRunner` |
|
|
||||||
| `cypher-executor/src/lib/auth-recipe-seeds.ts` | 新增 | 20 個常用服務的 auth recipe 定義 |
|
|
||||||
| `cli/src/commands/auth-recipe.ts` | 新增 | `acr auth-recipe list/info/scaffold` |
|
|
||||||
| `cli/src/commands/parts.ts` | 修改 | `cmdPartsScaffold` fallback 到 auth recipe |
|
|
||||||
| `cli/src/index.ts` | 修改 | 註冊 auth-recipe 指令 |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 九、封測前預計的 Auth Recipe 清單(20 個)
|
|
||||||
|
|
||||||
### static_key 類(~80% 服務)
|
|
||||||
|
|
||||||
| service | 認證方式 | credential key |
|
|
||||||
|---|---|---|
|
|
||||||
| `notion` | Bearer token (header) | `notion_token` |
|
|
||||||
| `slack` | Bot Token (Bearer) | `slack_bot_token` |
|
|
||||||
| `github` | PAT (Bearer) | `github_token` |
|
|
||||||
| `openai` | API key (Bearer) | `openai_api_key` |
|
|
||||||
| `anthropic` | API key (x-api-key) | `anthropic_api_key` |
|
|
||||||
| `airtable` | PAT (Bearer) | `airtable_token` |
|
|
||||||
| `discord` | Bot token ("Bot TOKEN") | `discord_bot_token` |
|
|
||||||
| `stripe` | Secret key (Bearer) | `stripe_secret_key` |
|
|
||||||
| `twilio` | AccountSid + AuthToken (Basic Auth) | `twilio_account_sid`, `twilio_auth_token` |
|
|
||||||
| `sendgrid` | API key (Bearer) | `sendgrid_api_key` |
|
|
||||||
| `hubspot` | Private App token (Bearer) | `hubspot_token` |
|
|
||||||
| `linear` | API key (Bearer) | `linear_api_key` |
|
|
||||||
| `shopify` | Admin API token (X-Shopify-Access-Token) | `shopify_access_token` |
|
|
||||||
| `resend` | API key (Bearer) | `resend_api_key` |
|
|
||||||
| `supabase` | Service role key (Bearer + apikey) | `supabase_service_key` |
|
|
||||||
| `typeform` | PAT (Bearer) | `typeform_token` |
|
|
||||||
| `jira` | API token + email (Basic Auth) | `jira_api_token`, `jira_email` |
|
|
||||||
|
|
||||||
### service_account 類(Google 家族,JWT signing)
|
|
||||||
|
|
||||||
| service | scopes | credential key |
|
|
||||||
|---|---|---|
|
|
||||||
| `google_sheets_sa` | spreadsheets | `google_service_account` |
|
|
||||||
| `google_gmail_sa` | gmail.send | `google_service_account` |
|
|
||||||
| `google_drive_sa` | drive | `google_service_account` |
|
|
||||||
|
|
||||||
> 注意:三個 Google 服務可共用同一個 `google_service_account` credential,只是 scope 不同。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 十、實作進度
|
|
||||||
|
|
||||||
### Server (cypher-executor)
|
|
||||||
|
|
||||||
- [x] `AuthRecipeDefinition` 型別 + `resolveAuthRecipe`
|
|
||||||
- [x] `/auth-recipes` CRUD routes
|
|
||||||
- [x] `injectFromAuthRecipe` — static_key primitive
|
|
||||||
- [x] `lib/jwt-signer.ts` — Google JWT via crypto.subtle
|
|
||||||
- [x] `injectFromAuthRecipe` — service_account primitive
|
|
||||||
- [x] `makeAuthRecipeRunner` in component-loader
|
|
||||||
- [x] step 5.5 in createComponentLoader
|
|
||||||
- [x] auth-recipe-seeds.ts (20 services)
|
|
||||||
- [x] seed script / deploy seeds to KV(2026-04-19 全部 ✅)
|
|
||||||
|
|
||||||
### CLI (arcrun)
|
|
||||||
|
|
||||||
- [x] `commands/auth-recipe.ts` — list / info / scaffold
|
|
||||||
- [x] 更新 `commands/parts.ts` — scaffold fallback
|
|
||||||
- [x] 更新 `index.ts` — 註冊指令
|
|
||||||
- [x] 版本升 1.1.0
|
|
||||||
- [x] npm publish(arcrun@1.1.0)
|
|
||||||
|
|
||||||
### 驗證
|
|
||||||
|
|
||||||
- [ ] notion (static_key) 端對端
|
|
||||||
- [ ] google_sheets_sa (service_account) 端對端
|
|
||||||
- [ ] 舊有 google_sheets builtin 向後相容確認
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 十一、長期演進:TinyGo WASM Primitive(封測後)
|
|
||||||
|
|
||||||
> 參考:`docs/user_requirements/arcrun/credential_parts.md`
|
|
||||||
|
|
||||||
**目前封測版**:Layer 2 primitive 邏輯在 `cypher-executor` TypeScript 中實作(`credential-injector.ts`)。
|
|
||||||
|
|
||||||
**長期目標**:四個 primitive 各自編譯為獨立 TinyGo WASM,取代現有 TS 實作:
|
|
||||||
|
|
||||||
```
|
|
||||||
arcrun/registry/components/auth_static_key/ ← TinyGo WASM
|
|
||||||
arcrun/registry/components/auth_oauth2/ ← TinyGo WASM
|
|
||||||
arcrun/registry/components/auth_service_account/ ← TinyGo WASM
|
|
||||||
arcrun/registry/components/auth_mtls/ ← TinyGo WASM
|
|
||||||
```
|
|
||||||
|
|
||||||
每個 primitive 實作統一 interface(`Authenticate` / `NeedsRefresh` / `Refresh` / `Test`)。
|
|
||||||
切換時 `cypher-executor` 的 `injectFromAuthRecipe` 改為呼叫對應 WASM,邏輯不變。
|
|
||||||
|
|
||||||
**何時做**:封測驗證完成、TinyGo crypto 支援確認後(特別是 RS256/ES256 JWT signing)。
|
|
||||||
在此之前,**不建立任何 TypeScript SDK 或 Python SDK 來包裝 credential 邏輯**。
|
|
||||||
|
|
||||||
### 禁止的做法
|
|
||||||
|
|
||||||
- ❌ 建立 `js-sdk/`、`python-sdk/` 包裝 credential 加解密
|
|
||||||
- ❌ 在 client 端重實作 AES-GCM encrypt/decrypt
|
|
||||||
- ❌ 用 TypeScript 重寫已計劃用 TinyGo 實作的 primitive 邏輯
|
|
||||||
@@ -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 primitive(crypto.subtle / KV / fetch)。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 現有 TS 實作(要刪除的)
|
|
||||||
|
|
||||||
| 檔案 | 功能 | 對應 WASM Primitive |
|
|
||||||
|------|------|---------------------|
|
|
||||||
| `credential-injector.ts` — `injectFromAuthRecipe()` | static_key template 展開 | `auth_static_key` |
|
|
||||||
| `credential-injector.ts` — service_account 分支 | JWT signing + token exchange | `auth_service_account` |
|
|
||||||
| `credential-injector.ts` — `decryptCredential()` | AES-GCM 解密 | host function(所有 primitive 共用) |
|
|
||||||
| `credential-injector.ts` — `interpolateTemplate()` | `{{secret.KEY}}` 替換 | 內建在各 primitive |
|
|
||||||
| `jwt-signer.ts` — `exchangeGoogleJwt()` | PEM→PKCS8→RS256→token | `auth_service_account` |
|
|
||||||
| `component-loader.ts` — BUILTIN_API_RECIPES | gmail/telegram/line/gsheets 寫死邏輯 | 刪除,改用 auth recipe + `http_request` 零件 |
|
|
||||||
| `credential-injector.ts` — BUILTIN_CREDENTIALS_MAP | 舊路徑 flat injection | 刪除,統一走 auth recipe |
|
|
||||||
| `arcrun/credentials/` | 重複的 credentials Worker | 刪除,路由已在 cypher-executor |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 4 個 WASM Primitive 設計
|
|
||||||
|
|
||||||
### 統一 I/O 介面(stdin/stdout JSON)
|
|
||||||
|
|
||||||
```
|
|
||||||
stdin(Worker → WASM):
|
|
||||||
{
|
|
||||||
"action": "authenticate" | "needs_refresh" | "refresh" | "test",
|
|
||||||
"api_key": "ak_xxx", // 租戶識別,用來組 KV key
|
|
||||||
"service": "openai", // 對應 auth_recipe:{service}
|
|
||||||
"request": { "method": "GET", "url": "/path", "headers": {}, "body": null }
|
|
||||||
}
|
|
||||||
|
|
||||||
WASM 內部流程:
|
|
||||||
1. recipeJSON = kv_get("auth_recipe:" + service)
|
|
||||||
2. 依 recipe.required_secrets 逐一 kv_get("{api_key}:cred:{name}") → {encrypted, iv}
|
|
||||||
3. secrets[name] = crypto_decrypt(encrypted, iv)
|
|
||||||
4. (service_account)crypto_sign_rs256(jwt, pkcs8) + http_request 換 token
|
|
||||||
5. 展開 recipe.inject 的 {{secret.X}} / {{runtime.X}} 模板
|
|
||||||
|
|
||||||
stdout(WASM → Worker):
|
|
||||||
{
|
|
||||||
"success": true,
|
|
||||||
"auth_headers": { "Authorization": "Bearer xxx" },
|
|
||||||
"auth_query": {},
|
|
||||||
"auth_body": {},
|
|
||||||
"runtime": { ... updated runtime state,供下次 refresh 用 }
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### auth_static_key
|
|
||||||
|
|
||||||
**位置**:`arcrun/registry/components/auth_static_key/`
|
|
||||||
**語言**:TinyGo 或 AssemblyScript
|
|
||||||
|
|
||||||
功能:
|
|
||||||
1. 讀取 `recipe.inject.header/query/body` 模板
|
|
||||||
2. 用 `secrets` 展開 `{{secret.KEY}}` 模板
|
|
||||||
3. 回傳 `auth_headers` / `auth_query` / `auth_body`
|
|
||||||
|
|
||||||
涵蓋:~80% 服務(Bearer token, API Key, Basic Auth, custom header)
|
|
||||||
|
|
||||||
### auth_service_account
|
|
||||||
|
|
||||||
**位置**:`arcrun/registry/components/auth_service_account/`
|
|
||||||
**語言**:TinyGo 或 AssemblyScript
|
|
||||||
|
|
||||||
功能:
|
|
||||||
1. 從 `secrets.service_account_json` 解析 private key
|
|
||||||
2. JWT signing(RS256:PEM→PKCS8→sign)
|
|
||||||
3. POST token exchange endpoint → 取得 access_token
|
|
||||||
4. 展開 `{{runtime.access_token}}` 模板
|
|
||||||
|
|
||||||
**crypto 考量**:
|
|
||||||
- TinyGo 的 `crypto/rsa` + `crypto/x509` 支援有限
|
|
||||||
- 若 TinyGo 不支援 RS256:使用 host function 讓 Worker 的 `crypto.subtle` 代簽
|
|
||||||
- 或改用 AssemblyScript(有 as-crypto 套件)
|
|
||||||
|
|
||||||
### auth_oauth2(新建)
|
|
||||||
|
|
||||||
**位置**:`arcrun/registry/components/auth_oauth2/`
|
|
||||||
|
|
||||||
功能:
|
|
||||||
1. `needs_refresh`:檢查 `runtime.expires_at` 是否過期
|
|
||||||
2. `refresh`:用 `runtime.refresh_token` + `secrets.client_secret` 換新 token
|
|
||||||
3. `authenticate`:展開 `{{runtime.access_token}}` 到 headers
|
|
||||||
|
|
||||||
### auth_mtls(新建)
|
|
||||||
|
|
||||||
**位置**:`arcrun/registry/components/auth_mtls/`
|
|
||||||
|
|
||||||
功能:
|
|
||||||
1. 從 `secrets` 讀取 client cert + key
|
|
||||||
2. 回傳 TLS 設定(由 Worker runtime 執行實際 mTLS handshake)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## cypher-executor 改動
|
|
||||||
|
|
||||||
### 保留(TS routing 層)
|
|
||||||
|
|
||||||
- `routes/credentials.ts` — HTTP CRUD for credentials(接收加密的 payload)
|
|
||||||
- `routes/recipes.ts` — HTTP CRUD for auth recipes
|
|
||||||
- `routes/auth.ts` — OAuth flow routing
|
|
||||||
- `graph-executor.ts` — workflow 執行排程
|
|
||||||
- `lib/wasi-shim.ts` — WASM runtime + host functions(加解密 / KV / 簽章 / HTTP 實際由 `crypto.subtle` / env binding / fetch 執行,但**呼叫時機由 WASM 決定**)
|
|
||||||
|
|
||||||
### 修改
|
|
||||||
|
|
||||||
- `actions/credential-injector.ts` — **整檔刪除**,改為新檔 `actions/auth-dispatcher.ts`(約 30 行):
|
|
||||||
1. 查 `resolveAuthRecipe(componentId)` 取得 `primitive` 名稱(static_key / service_account / oauth2 / mtls)
|
|
||||||
2. 載入對應的 `auth_{primitive}.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 Functions(WASM ↔ Worker 的橋接)
|
|
||||||
|
|
||||||
auth primitive WASM 需要呼叫外部能力時,透過 host function。全部放 `u6u` namespace。**錯誤回傳非零 uint32;成功 = 0 且把結果寫入 `outPtr` 指向的 buffer**。
|
|
||||||
|
|
||||||
| Host Function | TinyGo 簽章 | 用途 |
|
|
||||||
|---|---|---|
|
|
||||||
| `http_request` | `(urlPtr/Len, methodPtr/Len, headersPtr/Len, bodyPtr/Len, outPtr, outLenPtr) uint32` | HTTP 請求(已實作) |
|
|
||||||
| `kv_get` | `(keyPtr, keyLen, outPtr, outLenPtr) uint32` | 讀 KV。Worker 依 key 前綴路由到 `CREDENTIALS_KV` / `RECIPES` |
|
|
||||||
| `crypto_decrypt` | `(encPtr, encLen, ivPtr, ivLen, outPtr, outLenPtr) uint32` | AES-GCM 解密。encryption key 由 Worker 從 `env.ENCRYPTION_KEY` 內部讀取,**永遠不暴露給 WASM** |
|
|
||||||
| `crypto_sign_rs256` | `(dataPtr, dataLen, pkcs8Ptr, pkcs8Len, outPtr, outLenPtr) uint32` | Worker 用 `crypto.subtle.sign('RSASSA-PKCS1-v1_5' + SHA-256)`;private key 以 PKCS8 bytes 傳入 |
|
|
||||||
|
|
||||||
這些 host function 在 `lib/wasi-shim.ts` 中以 WASI import 提供。
|
|
||||||
|
|
||||||
### 安全邊界
|
|
||||||
|
|
||||||
- `ENCRYPTION_KEY` 只在 `crypto_decrypt` host function 內部使用,**絕不**經 stdin / 回傳值 / 任何路徑傳給 WASM
|
|
||||||
- `api_key` 經 stdin 傳入 WASM(讓 WASM 自己組 `{api_key}:cred:{name}` KV key)
|
|
||||||
- `kv_get` 在 Worker 側檢查 key 前綴:
|
|
||||||
- `auth_recipe:*` → 讀 `RECIPES`
|
|
||||||
- `{api_key}:cred:*` → 讀 `CREDENTIALS_KV`,且 `{api_key}` 必須等於 stdin 傳入的 api_key(防越權)
|
|
||||||
- 其他前綴 → 回傳錯誤
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 關於解密位置
|
|
||||||
|
|
||||||
採用**方案 B(唯一方案)**:WASM 透過 host function `crypto_decrypt()` 自行解密。
|
|
||||||
|
|
||||||
- cypher-executor TS 完全不解密、不知道 plaintext
|
|
||||||
- `ENCRYPTION_KEY` 永遠留在 Worker host function 內
|
|
||||||
- WASM 知道要解哪份 ciphertext(經 `kv_get` 讀到的 `{encrypted, iv}`),但拿不到 encryption key
|
|
||||||
- 這樣 TS 層完全沒有零件業務邏輯,符合 CLAUDE.md §禁止行為 1/6
|
|
||||||
|
|
||||||
(歷史註記:曾規劃方案 A「TS 先解密再送 stdin」,已廢棄 — 違反「TS 不得實作零件邏輯」。)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 不做的事
|
|
||||||
|
|
||||||
- ❌ 不改 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_id,6 個 API 零件 + 4 個 auth primitive)+ `wasmWorkerUrl()` URL 慣例輔助函數;解析鏈新增為第 8 層(放在 `BUILTIN_API_RECIPES` fallback 之後,避免 Phase 3 尚未完成時 API 零件 Worker 未部署造成 404;Phase 3 刪除 `BUILTIN_API_RECIPES` 後,API 零件會自然落到此層)。auth primitive 從此層進入。`tsc --noEmit` 通過。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Phase 1:auth_static_key WASM(優先,涵蓋 80% 服務)
|
|
||||||
|
|
||||||
方案 B:WASM 自行讀 KV + 解密,TS 不碰 plaintext。
|
|
||||||
|
|
||||||
- [x] 1.1 建立 `arcrun/registry/components/auth_static_key/` 目錄
|
|
||||||
- [x] 1.2 寫 `component.contract.yaml`(input: `{action, api_key, service, request}` → output: `{success, auth_headers, auth_query, auth_body, runtime}`)
|
|
||||||
- [x] 1.3 實作 `main.go`(TinyGo):
|
|
||||||
- 宣告 host imports:`kv_get` / `crypto_decrypt`(static_key 不需要 http_request)
|
|
||||||
- 從 stdin 讀 `{action, api_key, service}`
|
|
||||||
- `kv_get("auth_recipe:" + service)` → recipe JSON → 驗證 `primitive == "static_key"`
|
|
||||||
- 對每個 non-optional `recipe.required_secrets`:`kv_get("{api_key}:cred:{name}")` → `{encrypted, iv}` → `crypto_decrypt` → plaintext
|
|
||||||
- 展開 `{{secret.X}}` / `{{runtime.X}}` 模板於 `inject.header/query/body`;未知 key 展空字串(與 TS parity);其他 namespace 的 `{{...}}` 原樣保留
|
|
||||||
- 輸出 stdout JSON `{success, auth_headers, auth_query, auth_body, runtime}`
|
|
||||||
- [x] 1.4 `tinygo build -o auth_static_key.wasm -target=wasi main.go` — 2026-04-19 編譯通過(1.1MB,在 contract 限制 2MB 內)
|
|
||||||
- [🔄] 1.5 建立 `.component-builds/auth_static_key/`(用 `component-worker-template`)並部署到 `auth-static-key.arcrun.dev`
|
|
||||||
- 2026-04-20 完成**建置**部分:`.component-builds/auth_static_key/{wrangler.toml, package.json, tsconfig.json, src/index.ts, component.wasm}` 全數到位
|
|
||||||
- 方案 A:`src/index.ts` 直接 import `../../../cypher-executor/src/lib/wasi-shim` 的 `createWasiShim` + `createArcrunHostFunctions`(以 `ArcrunHostEnv` 結構型別相容);AES 解密邏輯仍只存在於 wasi-shim.ts 一處(rule 02 §2.2)
|
|
||||||
- 綁同組 KV:CREDENTIALS_KV (e7f4320f88d343f187e35e3543dd74c9) / RECIPES (9cf9db905c6241f78503199e58b2ffe0);ENCRYPTION_KEY 走 `wrangler secret put`
|
|
||||||
- `wrangler deploy --dry-run` 通過(1192 KiB, 419 KiB gzip);實際 `wrangler deploy` + `secret put ENCRYPTION_KEY` 留給 richblack 執行
|
|
||||||
- [x] 1.6 建立 `auth-dispatcher.ts`(取代 `credential-injector.ts`):查 auth recipe → HTTP POST 到對應 auth primitive URL → 合併 `_auth_headers` 進 ctx
|
|
||||||
- 2026-04-20 完成:`cypher-executor/src/actions/auth-dispatcher.ts` 新建,export `tryAuthDispatch(componentId, input, env, apiKey)`
|
|
||||||
- 流程:查 `resolveAuthRecipe` → primitive 在 `SUPPORTED_PRIMITIVES`(目前只有 `static_key`)→ fetch `wasmWorkerUrl('auth_static_key')` → 合併 `_auth_headers/_auth_query/_auth_body`
|
|
||||||
- 自引用防護:`AUTH_PRIMITIVE_IDS` set 排除 4 個 `auth_*` componentId
|
|
||||||
- `wasmWorkerUrl` 從 `component-loader.ts` export 出來共用
|
|
||||||
- `graph-executor.ts` 改為:先試 `tryAuthDispatch`(新路徑),沒命中 fallback 到舊 `injectCredentials`(Phase 1.9 刪)
|
|
||||||
- 檢查過 auth-dispatcher.ts 無 `crypto.subtle` / `interpolate` / `{{secret.` / hard-code API URL,符合 rule 02 §2.2
|
|
||||||
- `tsc --noEmit` 通過
|
|
||||||
- [ ] 1.7 端對端測試:openai recipe → 成功注入 `Authorization: Bearer <openai_key>`
|
|
||||||
- [ ] 1.8 端對端測試:twilio recipe(Basic Auth)→ 成功注入
|
|
||||||
- [ ] 1.9 **刪除 `credential-injector.ts` 整檔**(`decryptCredential` / `decryptSecrets` / `interpolateTemplate` / `BUILTIN_CREDENTIALS_MAP` 全刪)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Phase 2:auth_service_account WASM
|
|
||||||
|
|
||||||
- [🔄] 2.1 建立 `arcrun/registry/components/auth_service_account/` 目錄
|
|
||||||
- [🔄] 2.2 寫 `component.contract.yaml`
|
|
||||||
- [🔄] 2.3 實作 `main.go`:
|
|
||||||
- 從 stdin 讀 `{api_key, service}` + `kv_get` 拿 recipe + 解密 SA JSON
|
|
||||||
- 解析 SA JSON 取 `client_email` / `private_key`(PEM)
|
|
||||||
- PEM → PKCS8 bytes(純 Go,base64 decode + 去 header/footer)
|
|
||||||
- 組 JWT header + payload(base64url),呼叫 `crypto_sign_rs256(signingInput, pkcs8)` 拿 signature
|
|
||||||
- 組完整 JWT → `http_request` POST `token_uri` → 拿 `access_token`
|
|
||||||
- 展開 `{{runtime.access_token}}` 模板
|
|
||||||
- [x] 2.4 `tinygo build -o auth_service_account.wasm -target=wasi main.go` — 2026-04-20 編譯通過(1.1MB,在 contract 限制 2MB 內)
|
|
||||||
- [x] 2.5 建立 `.component-builds/auth_service_account/` 並部署到 `auth-service-account.arcrun.dev`
|
|
||||||
- 2026-04-20 完成**建置**部分:`.component-builds/auth_service_account/{wrangler.toml, package.json, tsconfig.json, src/index.ts, component.wasm}` 全數到位
|
|
||||||
- 方案 A:`src/index.ts` 重用 `createArcrunHostFunctions` 提供 kv_get/crypto_decrypt/crypto_sign_rs256,**額外加 `http_request` host function**(token exchange 用,非 crypto 不受 §2.2 約束)。http_request 直接回 response body 原文(WASM 端 json.Unmarshal 找 access_token)
|
|
||||||
- 綁同組 KV:CREDENTIALS_KV / RECIPES;ENCRYPTION_KEY 走 `wrangler secret put`
|
|
||||||
- `wrangler deploy --dry-run` 通過(1248 KiB, 440 KiB gzip);實際 `wrangler deploy` + `secret put ENCRYPTION_KEY` 留給 richblack 執行
|
|
||||||
- `auth-dispatcher.ts` 的 `SUPPORTED_PRIMITIVES` 加入 `'service_account'`,workflow 用 google SA recipe 會自動走新 WASM 路徑
|
|
||||||
- [ ] 2.6 端對端測試:google_sheets_sa recipe → 成功取得 access_token → 注入 header
|
|
||||||
- [x] 2.7 **刪除 `lib/jwt-signer.ts` 整檔** — 2026-04-20 完成
|
|
||||||
- `cypher-executor/src/lib/jwt-signer.ts` 已刪除(RS256 JWT 邏輯移入 `auth_service_account.wasm`)
|
|
||||||
- `credential-injector.ts` 原 line 23 `import { exchangeGoogleJwt }` 移除
|
|
||||||
- `credential-injector.ts` 原 line 140-150 service_account 分支改為 throw(任何 service_account recipe 已被 auth-dispatcher 攔截;這條 TS fallback 若被觸發即表架構錯亂,直接爆錯比沈默解密更安全)
|
|
||||||
- `cypher-executor` tsc --noEmit 通過
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Phase 3:清理 component-loader 的 TS 實作(全刪)
|
|
||||||
|
|
||||||
目標:`BUILTIN_API_RECIPES` 整段刪除,所有服務走 WASM runner(HTTP URL 路徑)。
|
|
||||||
|
|
||||||
- [x] 3.1 確認 `http_request.wasm` / `gmail.wasm` / `telegram.wasm` / `line_notify.wasm` / `google_sheets.wasm` 都在 `registry/components/` 且可執行 — 2026-04-20 驗證 6 個(含 cron)全數存在,main.go + .wasm 齊備
|
|
||||||
- [x] 3.2 確認上述零件 Worker 都已部署(`{name}.arcrun.dev` 可用) — 2026-04-20 完成**建置**部分
|
|
||||||
- 6 個 Worker 建置到位:`.component-builds/{http_request, gmail, telegram, line_notify, google_sheets, cron}/{wrangler.toml, package.json, tsconfig.json, src/index.ts, component.wasm}`
|
|
||||||
- 方案 A:5 個需 http_request 的零件(http_request/gmail/telegram/line_notify/google_sheets)`src/index.ts` 共用模板;cron 是純計算不註冊 host function
|
|
||||||
- 全部透過 `createWasiShim` 複用 cypher-executor/src/lib/wasi-shim.ts(rule 02 §2.2 邊界)
|
|
||||||
- 6 個 `wrangler deploy --dry-run` 全通過(~1.17 MB / ~413 KB gzip 每個);實際 `wrangler deploy` 留給 richblack 執行
|
|
||||||
- [x] 3.3 `component-loader.ts` 的內建路徑改為查對應 Worker URL → HTTP POST — 2026-04-20 完成
|
|
||||||
- 原本第 7 層是 `BUILTIN_API_RECIPES` fallback、第 8 層是 `WASM_HTTP_RUNNER_IDS` (HTTP URL);兩層合併為第 7 層 `WASM_HTTP_RUNNER_IDS` 直接走 `makeHttpRunner(wasmWorkerUrl(id))`
|
|
||||||
- 解析鏈新編號 1-8,順序不變(外部 URL → recipe hash → component hash → R2 → Service Binding → auth recipe runner → WASM HTTP runner → 找不到)
|
|
||||||
- [x] 3.4 **刪除 `BUILTIN_API_RECIPES` 整個 Record**(`http_request` / `gmail` / `telegram` / `line_notify` / `google_sheets` / `cron` 的 TS 實作全刪) — 2026-04-20 完成
|
|
||||||
- `cypher-executor/src/lib/component-loader.ts` 原 line 253-326 `BUILTIN_API_RECIPES` 常數 + fallback lookup 全刪(約 80 行)
|
|
||||||
- 全域搜尋確認:`gmail.googleapis.com/...messages/send` / `api.telegram.org/bot.*sendMessage` / `sheets.googleapis.com/v4/spreadsheets` / `notify-api.line.me/api/notify` 在 cypher-executor TS 中已不存在(auth-recipe-seeds.ts 的 `base_url` 是 recipe 資料欄位,不是 hard-coded API call)
|
|
||||||
- `cypher-executor` tsc --noEmit 通過
|
|
||||||
- [ ] 3.5 端對端測試:workflow 用 gmail auth recipe + gmail.wasm Worker → 成功發信
|
|
||||||
- [ ] 3.6 端對端測試:workflow 用 http_request.wasm Worker + auth_static_key 注入 → 成功呼叫任意 API
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Phase 4:auth_oauth2 + auth_mtls WASM(封測後)
|
|
||||||
|
|
||||||
- [ ] 4.1 建立 `arcrun/registry/components/auth_oauth2/`
|
|
||||||
- [ ] 4.2 實作:`needs_refresh` / `refresh` / `authenticate` 三個 action
|
|
||||||
- [ ] 4.3 建立 `arcrun/registry/components/auth_mtls/`
|
|
||||||
- [ ] 4.4 實作:輸出 TLS cert/key(實際 mTLS handshake 由 Worker runtime 執行,WASM 無法做 socket)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Phase 5:封測啟動門檻 — 核心穩定驗證
|
|
||||||
|
|
||||||
**全部通過才能啟動封測**。
|
|
||||||
|
|
||||||
- [ ] 5.1 所有 20 個 auth recipe seed 可正常運作(static_key 17 個 + service_account 3 個)
|
|
||||||
- [ ] 5.2 `cypher-executor/src/actions/credential-injector.ts` **不存在**
|
|
||||||
- [ ] 5.3 `cypher-executor/src/lib/jwt-signer.ts` **不存在**
|
|
||||||
- [ ] 5.4 `cypher-executor/src/lib/component-loader.ts` 無 `BUILTIN_API_RECIPES` / `BUILTIN_CREDENTIALS_MAP`
|
|
||||||
- [ ] 5.5 `cypher-executor/src/` 全域搜尋 `crypto.subtle.decrypt` 只出現在 `wasi-shim.ts` 的 `crypto_decrypt` host function
|
|
||||||
- [ ] 5.6 `cypher-executor/src/` 全域搜尋 `crypto.subtle.sign` 只出現在 `wasi-shim.ts` 的 `crypto_sign_rs256` host function
|
|
||||||
- [ ] 5.7 `cypher-executor/src/` 全域搜尋 `interpolate` 回傳 0 筆(template 展開全在 WASM)
|
|
||||||
- [ ] 5.8 全域搜尋 `{{secret\.` / `{{runtime\.` 在 TS 檔案中回傳 0 筆
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Phase 6:通用 CI/CD deploy workflow
|
|
||||||
|
|
||||||
**背景**(2026-04-20 richblack 決定):現 `.github/workflows/deploy.yml` 只部署 cypher-executor + registry + 已刪除的 credentials,漏掉 Phase 1-3 產出的 8 個 Worker,且硬編碼每個 job 導致未來新增 Worker 都要改 CI。改為**通用掃描式 workflow**:任何含 `wrangler.toml` 的目錄 = 部署單位,改到該目錄下任何檔案 = 觸發重新 deploy。
|
|
||||||
|
|
||||||
**關鍵決策**:
|
|
||||||
- 零件 `.wasm` 由 CI build(不 commit):`registry/components/{name}/main.go` 改動時才重 build,用 timestamp / content hash 判斷
|
|
||||||
- `.component-builds/{name}/component.wasm` 由 CI 從 `registry/components/{name}/{name}.wasm` 複製產生(deploy 前一步)
|
|
||||||
- 統一用 pnpm(`.component-builds/*` 本來就是;順勢把 cypher-executor 的 `package-lock.json` 砍了)
|
|
||||||
- runtime secret(`ENCRYPTION_KEY`)不進 CI,由 richblack 一次性 `wrangler secret put`
|
|
||||||
- registry Worker 的 `wrangler.toml` 現階段不改(職責是合約管理,與封測無關;`sandboxAcceptance.ts` 的 rule 02 §2.2 審查留到 Phase 5 用戶自製零件啟動時)
|
|
||||||
|
|
||||||
### Tasks
|
|
||||||
|
|
||||||
- [x] 6.1 改寫 `.github/workflows/deploy.yml`:動態掃描所有含 `wrangler.toml` 的目錄(排除 `node_modules/` + Pages 專案),用 matrix job fanout 部署;分兩層(tier1=`.component-builds/*`,tier2=其他),tier1 全綠後才 tier2(避免 service binding target 未存在)
|
|
||||||
- [x] 6.2 加上 TinyGo build 步驟:tier1 matrix 一律 setup-tinygo + 從 `registry/components/{name}/main.go` rebuild `.wasm` → copy 到 `.component-builds/{name}/component.wasm`
|
|
||||||
- [x] 6.3 diff-aware:push 到 main 時比對 `github.event.before..github.sha`,只 deploy 有 diff 的 Worker(含 `registry/components/{name}/` 連動 `.component-builds/{name}/`);`workflow_dispatch` 提供 `force_all` + `only` 選項
|
|
||||||
- [x] 6.4 統一 pnpm:刪除 `cypher-executor/package-lock.json` + `registry/package-lock.json`;workflow 優先 `pnpm install --frozen-lockfile`,若該目錄無 `pnpm-lock.yaml` 則 fallback 到 `--no-frozen-lockfile`(混合期容錯)
|
|
||||||
- [x] 6.5 加 `max-parallel: 5` 控制 Workers API rate limit(tier1 和 tier2 各自)
|
|
||||||
- [x] 6.6 驗證:`workflow_dispatch` + `force_all=true` 手動跑一次,24 個 Worker 全綠 — 2026-04-20 完成
|
|
||||||
- 最終綠色 run 24668903627(28/28 jobs,含 discover + summary):tier1 24 個零件 Worker + tier2 2 個 orchestration Worker(cypher-executor / registry)全 success
|
|
||||||
- 過程中修兩輪:先修 `setup-node` 的 `cache: 'pnpm'` 對 legacy `package-lock.json` 目錄失效(改為不用 cache);再修 tier2 三個 package.json(cypher-executor/registry/builtins)遺漏 `wrangler` devDependency + regen pnpm-lock.yaml
|
|
||||||
- ENCRYPTION_KEY secret 已由 richblack 授權、CC 從 .env pipe 到三個 Worker:`arcrun-auth-static-key`、`arcrun-auth-service-account`、`arcrun-cypher-executor`(不顯示內容)
|
|
||||||
- [x] 6.7 文件:在 `.claude/rules/` 加一份 `05-deploy-convention.md`(「新增 Worker = 新目錄 + wrangler.toml,不用改 CI」)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Notes
|
|
||||||
|
|
||||||
- 方案 B 是唯一方案(方案 A 已廢棄,違反 CLAUDE.md §禁止行為)
|
|
||||||
- Phase 0.6(host functions)+ 0.7(WASM runner)是 Phase 1-3 的硬前置,必須先做
|
|
||||||
- 若 TinyGo `encoding/base64` 可用就直接用;若不可用則自行實作(見 gmail/main.go 的 `base64URLEncode`)
|
|
||||||
- `auth_mtls` 的 TLS handshake 無法在 WASM 內做(WASI preview1 沒 socket),只能輸出 cert/key 讓 Worker 在 fetch 時用
|
|
||||||
- **每個 auth primitive WASM 都是獨立部署的 Worker**(透過 `component-worker-template/`),不是從 R2 動態載入
|
|
||||||
- Cypher binding = workflow YAML 裡的 URL 清單,不是 Cloudflare service binding
|
|
||||||
@@ -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 ← 全站 layout:next/font + design tokens + 全域 CSS 匯入
|
|
||||||
│ ├── globals.css ← 匯入 design-tokens.css;Tailwind @import
|
|
||||||
│ ├── design-tokens.css ← 新增:從 design-source 抽出的 CSS variables(:root {...})
|
|
||||||
│ ├── page.tsx ← Landing(RSC)
|
|
||||||
│ ├── auth/
|
|
||||||
│ │ └── page.tsx ← Auth("use client")
|
|
||||||
│ ├── dashboard/
|
|
||||||
│ │ └── page.tsx ← Dashboard("use client",仍靠 middleware 保護)
|
|
||||||
│ ├── keys/
|
|
||||||
│ │ └── page.tsx ← API Keys("use client")
|
|
||||||
│ ├── workflows/
|
|
||||||
│ │ ├── page.tsx ← Workflows 清單(redirect 到 dashboard 的 table,本身極簡)
|
|
||||||
│ │ └── [name]/page.tsx ← Workflow Viewer("use client")
|
|
||||||
│ ├── integrations/page.tsx ← 保留現有
|
|
||||||
│ ├── api-docs/page.tsx ← 保留現有
|
|
||||||
│ └── login/page.tsx ← 保留現有(redirect /auth 同義;見 §9 遷移策略)
|
|
||||||
├── components/
|
|
||||||
│ ├── shell/
|
|
||||||
│ │ ├── Logo.tsx
|
|
||||||
│ │ ├── Icon.tsx
|
|
||||||
│ │ ├── TopNav.tsx
|
|
||||||
│ │ ├── Footer.tsx
|
|
||||||
│ │ └── Sidebar.tsx
|
|
||||||
│ ├── primitives/
|
|
||||||
│ │ ├── Button.tsx ← btn / btn-primary / btn-secondary / btn-ghost 對應 class
|
|
||||||
│ │ ├── Pill.tsx
|
|
||||||
│ │ ├── Toggle.tsx
|
|
||||||
│ │ ├── Terminal.tsx ← landing hero 右卡用
|
|
||||||
│ │ └── ChatPreview.tsx ← landing hero 右卡用
|
|
||||||
│ └── workflow/
|
|
||||||
│ ├── Canvas.tsx ← wf-viewer 本體(節點 + SVG edges)
|
|
||||||
│ ├── NodeCard.tsx
|
|
||||||
│ ├── DetailPanel.tsx
|
|
||||||
│ ├── Minimap.tsx
|
|
||||||
│ └── ZoomControls.tsx
|
|
||||||
├── lib/
|
|
||||||
│ ├── api.ts ← typed fetch wrapper(fetch ${API_BASE}${path}, credentials: 'include')
|
|
||||||
│ ├── workflows.ts ← listWorkflows / getWorkflow / getWorkflowYaml
|
|
||||||
│ ├── apiKeys.ts ← listKeys / createKey / patchKey / deleteKey
|
|
||||||
│ └── me.ts ← 已存在邏輯,集中到此
|
|
||||||
├── middleware.ts ← 擴展 matcher(加 /keys, /workflows/*)
|
|
||||||
└── ...(既有 package.json / wrangler.toml 不變)
|
|
||||||
```
|
|
||||||
|
|
||||||
**路由對照設計稿的 5 screen**:
|
|
||||||
|
|
||||||
| Screen | Route |
|
|
||||||
|---|---|
|
|
||||||
| Landing | `/` |
|
|
||||||
| Auth | `/auth`(新增;`/login` 保留並內部 `redirect('/auth')`) |
|
|
||||||
| Dashboard | `/dashboard` |
|
|
||||||
| API Keys | `/keys` |
|
|
||||||
| Workflow Viewer | `/workflows/[name]` |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 2. Design tokens 對應
|
|
||||||
|
|
||||||
設計稿所有 CSS 變數抄進 `app/design-tokens.css`,**不解析、不改名**:
|
|
||||||
|
|
||||||
```css
|
|
||||||
:root {
|
|
||||||
--bg: #0F0F0F;
|
|
||||||
--bg-1: #141414;
|
|
||||||
--card: #1A1A1A;
|
|
||||||
--card-2: #222222;
|
|
||||||
--line: #262626;
|
|
||||||
--line-2: #303030;
|
|
||||||
--text: #EDEDED;
|
|
||||||
--text-dim: #A0A0A0;
|
|
||||||
--text-mute: #6B6B6B;
|
|
||||||
--primary: #6366F1;
|
|
||||||
--primary-2: #8B5CF6;
|
|
||||||
--primary-soft: rgba(99, 102, 241, 0.12);
|
|
||||||
--primary-ring: rgba(99, 102, 241, 0.32);
|
|
||||||
--success: #22C55E;
|
|
||||||
--warn: #F59E0B;
|
|
||||||
--danger: #EF4444;
|
|
||||||
--gradient: linear-gradient(135deg, #6366F1 0%, #8B5CF6 100%);
|
|
||||||
--gradient-soft: linear-gradient(135deg, rgba(99,102,241,0.16) 0%, rgba(139,92,246,0.16) 100%);
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
並在 Tailwind v4 的 `@theme inline` block 內對應出:
|
|
||||||
|
|
||||||
```css
|
|
||||||
@theme inline {
|
|
||||||
--color-bg: var(--bg);
|
|
||||||
--color-card: var(--card);
|
|
||||||
--color-card-2: var(--card-2);
|
|
||||||
--color-line: var(--line);
|
|
||||||
--color-line-2: var(--line-2);
|
|
||||||
--color-text: var(--text);
|
|
||||||
--color-text-dim: var(--text-dim);
|
|
||||||
--color-text-mute: var(--text-mute);
|
|
||||||
--color-primary: var(--primary);
|
|
||||||
--color-primary-2: var(--primary-2);
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
這樣 JSX 裡可用 `bg-bg / text-text-dim / border-line`,又保留 CSS 變數語義。
|
|
||||||
|
|
||||||
**現有的 `--background: #0a0a0a` 要換成 `#0F0F0F`**(視覺 breaking change;受影響:所有沿用 `bg-[#0a0a0a]` 的 inline 值)。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 3. 字型
|
|
||||||
|
|
||||||
```tsx
|
|
||||||
// app/layout.tsx
|
|
||||||
import { Inter, JetBrains_Mono } from 'next/font/google';
|
|
||||||
|
|
||||||
const inter = Inter({
|
|
||||||
subsets: ['latin'],
|
|
||||||
variable: '--font-inter',
|
|
||||||
weight: ['300', '400', '500', '600', '700', '800'],
|
|
||||||
});
|
|
||||||
const mono = JetBrains_Mono({
|
|
||||||
subsets: ['latin'],
|
|
||||||
variable: '--font-mono',
|
|
||||||
weight: ['400', '500', '600'],
|
|
||||||
});
|
|
||||||
|
|
||||||
// body class = `${inter.variable} ${mono.variable}`
|
|
||||||
```
|
|
||||||
|
|
||||||
`globals.css` 中的 `body { font-family: var(--font-inter), -apple-system, sans-serif; }`,`.mono` class 用 `font-family: var(--font-mono)`。
|
|
||||||
|
|
||||||
**移除**:
|
|
||||||
- `design-source/index.html` 第 7-9 行的 `<link rel="preconnect"> / <link href="fonts.googleapis.com">`(不寫入 production)。
|
|
||||||
- React / Babel standalone script 標籤(prototype 專用,不進 production)。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 4. 元件 porting 規則
|
|
||||||
|
|
||||||
Claude Design 用了 `window.Icon / window.Logo / window.AppIcon / window.TopNav ...` 的 globals 風格 — 那是 prototype 專用。Port 到 Next.js 時:
|
|
||||||
|
|
||||||
1. 每個元件拆單檔、具名 export。
|
|
||||||
2. 用 Tailwind + `className` 模板字串;共用 variant(如 btn)用 `cva`-style helper 即可(自己寫 5 行的 `clsx`-alike 函式),**不引入 class-variance-authority / clsx 套件**(避免新依賴)。
|
|
||||||
3. Icon 的 `paths` 直接搬,但每個 icon 拆成自己的 functional component 或集中在一個 `<Icon name="..." />`(沿用 design source 的 pattern)。
|
|
||||||
4. SVG arc wordmark 的 logo 直接 port。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 5. 各 screen 實作細節
|
|
||||||
|
|
||||||
### 5.1 Landing — `app/page.tsx`
|
|
||||||
|
|
||||||
- 結構:`<TopNav />` + `<Hero />` + `<Paths />` + `<Strip />` + `<Footer />`。
|
|
||||||
- Hero:heading、eyebrow、CTA、radial grid bg(純 CSS)。
|
|
||||||
- Paths 左卡(Developer):install tabs (`npm` / `pip` / `bun`) + 兩個 terminal block;**code 範例用 dogfooding 範例**(`acr` CLI),不留 `Arcrun` SDK 假 API。
|
|
||||||
- Paths 右卡(Everyone):chat preview 結構保留;assistant 對話中的 tool call 用「arcrun · digest/weekly」不動。
|
|
||||||
- Strip:4 cell。
|
|
||||||
- `LandingClientTabs` 因為有 tabs state,需標 `"use client"`;外層保持 RSC。
|
|
||||||
|
|
||||||
### 5.2 Auth — `app/auth/page.tsx`
|
|
||||||
|
|
||||||
- `"use client"`。state:`mode: 'signin' | 'signup'`, `email`, `pw`, `remember`。
|
|
||||||
- Submit:`fetch(${API_BASE}/auth/password-login, { method: 'POST', credentials: 'include' })`(**若 cypher-executor 尚未支援 password auth,先顯示「Password 登入尚未開放,請用 OAuth」警示,不偽造成功流程**)。
|
|
||||||
- OAuth 按鈕:直接 `<a href={API_BASE}/auth/google/start?redirect=/dashboard>`,和現行 `/login` 同樣機制。
|
|
||||||
- 下標提示「By signing up, you agree to our Terms ...」保留 static 字串。
|
|
||||||
- 保留 `/login` 路由向後相容(RSC 裡 `redirect('/auth')`)。
|
|
||||||
|
|
||||||
### 5.3 Dashboard — `app/dashboard/page.tsx`
|
|
||||||
|
|
||||||
- `"use client"` 或 split(外層 RSC 抓 /me,內層 Client)。
|
|
||||||
- 由 `<Sidebar current="dashboard" />` + main。
|
|
||||||
- 主要區塊:
|
|
||||||
- Main head:breadcrumb「{email 的 domain} › Dashboard」、heading「Welcome back, {display_name}」、subtitle 顯示 app/workflow 總數(從 `/apps` + `/workflows` 計算;若 endpoint 404 → 顯示 `—`)。
|
|
||||||
- Apps Grid:`/apps` 的結果渲染;每列永遠有一個 `app-empty` 卡(新建 CTA)。
|
|
||||||
- Workflows Table:`/workflows` 的結果渲染;空時改為全寬「No workflows yet. Run `acr push` to add one.」內嵌指令框。
|
|
||||||
- 「Open app」「View」按鈕導向 `/workflows/[name]`。
|
|
||||||
- 「Edit in Claude」按鈕本次不做動作,僅保 UI(disabled + tooltip「Coming soon」)。
|
|
||||||
|
|
||||||
### 5.4 API Keys — `app/keys/page.tsx`
|
|
||||||
|
|
||||||
- `"use client"`。
|
|
||||||
- Fetch `/api-keys`:若回傳為空陣列但 `/me` 有 api_key,fallback 顯示 `/me.api_key` 為唯一一列(單 key 相容模式)。
|
|
||||||
- 頂部 new-key-box:只在「剛剛建立新 key」的一次性狀態顯示(`useState` + `sessionStorage` flag,reload 後消失)。
|
|
||||||
- 表格、toggle、trash:對應 PATCH / DELETE。
|
|
||||||
- 「Create new key」按鈕:呼叫 `POST /api-keys`,拿到後打 highlight box。
|
|
||||||
- Revoke 警告文字維持設計稿「within 60 seconds」。
|
|
||||||
|
|
||||||
### 5.5 Workflow Viewer — `app/workflows/[name]/page.tsx`
|
|
||||||
|
|
||||||
- `"use client"`,param `name` 來自動態路由。
|
|
||||||
- Mount 後呼叫 `GET /workflows/:name`:後端回傳 `{ name, nodes: Node[], edges: Edge[], yaml, last_run: {...} }`(若 endpoint 未實作 → 顯示「Workflow viewer 尚未啟用」empty state,不用假資料)。
|
|
||||||
- `<Canvas>` 內:
|
|
||||||
- SVG 的 `<marker>`, `<path>` 定義抄設計稿。
|
|
||||||
- Node 用絕對定位(x/y 直接用 API 資料;資料沒有 coord 時做自動 layout — 階段性做簡單 dagre-free 的「column by depth」排版,避免新依賴)。
|
|
||||||
- 點選節點 → 右側 detail panel 顯示 input/output schema;若 type 含 `ai.*`,顯示 triplet 編輯器(model / temp / prompt)— 編輯本次 **read-only**(disabled input + 「Edit via acr CLI」提示)。
|
|
||||||
- 「Export YAML」按 `GET /workflows/:name/yaml` → `download` blob。
|
|
||||||
- 「Edit in Claude」:本次只開新 tab 到 `https://claude.ai/new?q=...`(文案「coming soon」按鈕),避免偽裝已整合。
|
|
||||||
- Zoom controls、minimap:純 UI,`zoom` state 實際不套 transform(或簡單 `style={{ transform: scale(zoom/100) }}` 套在 `.wf-nodes` + svg)。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 6. API wrapper(`lib/api.ts`)
|
|
||||||
|
|
||||||
```ts
|
|
||||||
export const API_BASE = process.env.NEXT_PUBLIC_API_BASE ?? 'https://cypher.arcrun.dev';
|
|
||||||
|
|
||||||
export async function arcrunFetch<T>(path: string, init: RequestInit = {}): Promise<T> {
|
|
||||||
const res = await fetch(`${API_BASE}${path}`, {
|
|
||||||
credentials: 'include',
|
|
||||||
headers: { 'Accept': 'application/json', ...(init.headers ?? {}) },
|
|
||||||
...init,
|
|
||||||
});
|
|
||||||
if (res.status === 401 && typeof window !== 'undefined') {
|
|
||||||
window.location.href = `/auth?redirect=${encodeURIComponent(location.pathname)}`;
|
|
||||||
throw new Error('unauthenticated');
|
|
||||||
}
|
|
||||||
if (!res.ok) throw new Error(`arcrun ${path}: ${res.status}`);
|
|
||||||
return res.json() as Promise<T>;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
所有頁面透過這個 wrapper。**禁止在 page.tsx 裡 hard-code `fetch('https://...')`**(測試可以 grep)。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 7. Middleware
|
|
||||||
|
|
||||||
```ts
|
|
||||||
export const config = {
|
|
||||||
matcher: ['/dashboard/:path*', '/keys/:path*', '/workflows/:path*'],
|
|
||||||
};
|
|
||||||
```
|
|
||||||
|
|
||||||
現有邏輯(讀 `arcrun_session` cookie,沒有就 redirect `/login?redirect=...`)保留,`/login` 改為內部 redirect `/auth`。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 8. 不做的設計稿功能
|
|
||||||
|
|
||||||
| 設計元素 | 取捨 |
|
|
||||||
|---|---|
|
|
||||||
| 底部的 `proto-switch`(5 個 screen 切換 pill) | **刪**。那是 prototype 用的 demo 切換器,不進 production。 |
|
|
||||||
| Sidebar 的 `count` badge | 先保留;數字從 `/workflows` / `/apps` 的長度派生;無資料時藏起來。 |
|
|
||||||
| Sidebar bottom 的 avatar + "Maya Rivera / maya@northwind.co" | 換成 `{display_name} / {email}`(真資料)。 |
|
|
||||||
| Workflow Viewer 的 triplet 可編輯 | 本次 disabled,僅顯示。 |
|
|
||||||
| 「Edit in Claude」整合 | 按鈕保留,點擊開新 tab 到 claude.ai,不串 MCP/API。 |
|
|
||||||
| 多 workspace breadcrumb | 固定顯示用戶 email domain 或「Personal」。 |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 9. 既有頁面遷移
|
|
||||||
|
|
||||||
| 既有 | 處理 |
|
|
||||||
|---|---|
|
|
||||||
| `/page.tsx` | **rewrite**:沿用設計稿結構,code demo 字串改為 `acr` 實際指令(現有的 `auth.bind(...)` 寫法可保留在 Python tab) |
|
|
||||||
| `/login` | 改為 `redirect('/auth')`(Next.js RSC redirect),保留舊連結相容 |
|
|
||||||
| `/dashboard` | **rewrite**:舊 dashboard 變成 API Keys 獨立頁 + 新 Dashboard 總覽。原本 dashboard 裡的 Key 卡片搬到 `/keys`。 |
|
|
||||||
| `/api-docs` | 不動 |
|
|
||||||
| `/integrations` | 不動;在 Dashboard Apps Grid 旁提供 link |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 10. 開發順序(高度相依)
|
|
||||||
|
|
||||||
見 `tasks.md`。總則:
|
|
||||||
|
|
||||||
1. 先做 design tokens + shell(Logo / Icon / Button / Sidebar / TopNav / Footer) — 其他頁面都吃這些。
|
|
||||||
2. 然後 Landing(可直接驗證視覺基準)。
|
|
||||||
3. 然後 Auth(獨立)。
|
|
||||||
4. 然後 API Keys(後端依賴少)。
|
|
||||||
5. 然後 Dashboard(依賴 `/workflows` + `/apps`,若未實作先 empty state)。
|
|
||||||
6. 最後 Workflow Viewer(依賴最重,多 endpoint)。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 11. 風險與未解
|
|
||||||
|
|
||||||
| 風險 | 緩解 |
|
|
||||||
|---|---|
|
|
||||||
| cypher-executor 尚未有 `/workflows`, `/apps`, `/api-keys` CRUD | 前端先做,統一走 404 → empty state;另開 task 去 cypher-executor SDD 增補。本次 SDD 不負責後端實作。 |
|
|
||||||
| Password auth 沒實作 | Auth 頁 email/password form 在 submit 時顯示「OAuth only」提示 |
|
|
||||||
| `acr push` 未記錄 node 座標 | Canvas 自動排版(by topological depth),不強制 YAML 載入 layout |
|
|
||||||
| `next-on-pages` 對 `"use client"` 大量頁面的 edge runtime 支援 | 本來就用 `next-on-pages`,問題不大;必要時 per-page `export const runtime = 'edge'` |
|
|
||||||
| 舊 `/dashboard` 的 bookmark 使用者 | 現行 `/dashboard` 的 Key 管理被搬走;保留 Key 區塊 + 顯示提示「New page: /keys」引導 |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 12. 與封測的關係
|
|
||||||
|
|
||||||
此 SDD 的實作**不解除封測阻擋**(封測阻擋在 Credential Primitives WASM)。此重設計與 Phase 0.6 / 0.7 / 1-3 是並行軌道。richblack 可決定先後順序,但本 SDD 獨立可 ship。
|
|
||||||
|
|
||||||
@@ -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 看到我已部署的 App(packaged workflow)和 Workflow 清單。
|
|
||||||
- 驗收:
|
|
||||||
- Apps Grid:每個 app 卡片顯示名稱、描述、「Open app」「Edit in Claude」。
|
|
||||||
- Workflows Table:顯示 workflow 名稱、節點數、最後修改、執行次數、狀態。
|
|
||||||
- 若 app / workflow 為空,顯示新建 CTA(非硬編 mock)。
|
|
||||||
|
|
||||||
### US-4:管理 API Keys
|
|
||||||
- 作為已登入用戶,我要建立、檢視、停用、刪除 API Key。
|
|
||||||
- 驗收:
|
|
||||||
- 剛產生的 Key 顯示在頂部高亮卡片(含警示文字「不會再顯示」)。
|
|
||||||
- 全部 Keys 在下方表格顯示(名稱、遮蔽後 key、建立時間、最後使用、狀態、toggle、trash)。
|
|
||||||
- Rotate / Revoke 立即生效(呼叫 cypher-executor)。
|
|
||||||
|
|
||||||
### US-5:檢視 Workflow graph
|
|
||||||
- 作為已登入用戶,我要點 workflow 進到 viewer,看到節點 DAG 與每個節點的 I/O schema 與參數。
|
|
||||||
- 驗收:
|
|
||||||
- Canvas 顯示節點(含 icon / type badge),節點以 bezier 曲線相連。
|
|
||||||
- 選中節點後右側面板顯示 input / output schema、configuration(針對 ai.completion 等節點顯示 triplet 編輯器)。
|
|
||||||
- 工具列含 Share / Export YAML / Edit in Claude。
|
|
||||||
- Minimap + zoom 控制顯示可用。
|
|
||||||
- 「Export YAML」呼叫 cypher-executor 取得該 workflow 的原始 YAML。
|
|
||||||
|
|
||||||
### US-6:Dogfooding
|
|
||||||
- 作為 arcrun 核心維護者,我要前端所有與服務打交道的動作都透過 arcrun 自家 API(cypher-executor)完成,不依賴第三方 OAuth / workflow / backend 服務。
|
|
||||||
- 驗收(見 §6)。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 3. 非功能需求
|
|
||||||
|
|
||||||
| 項目 | 規範 |
|
|
||||||
|---|---|
|
|
||||||
| 技術棧 | Next.js 15 App Router、React 19、Tailwind v4、TypeScript(沿用 `landing/` 現有堆疊) |
|
|
||||||
| 部署 | Cloudflare Pages(`@cloudflare/next-on-pages`),沿用 `landing/wrangler.toml` |
|
|
||||||
| 字型 | Inter、JetBrains Mono — 用 `next/font/google`,不拉 unpkg / fonts.googleapis.com `<link>` |
|
|
||||||
| 依賴 | 僅 Next / React / Tailwind;禁止 tRPC、React Query、SWR、Auth.js/Clerk、ReactFlow/XYFlow、Radix、shadcn CLI install、animation library(framer-motion)等第三方 |
|
|
||||||
| 狀態管理 | React 內建(useState / useReducer / Context);持久化用 `localStorage` 或 server session cookie |
|
|
||||||
| 國際化 | 延後;本次一律英文(與設計稿一致)。現有 `?lang=zh` 不擴展 |
|
|
||||||
| 無障礙 | 按鈕 `aria-label`、Form 控件有 `<label>`;鍵盤可完成登入 / 複製 key 流程 |
|
|
||||||
| 效能 | Landing 首屏無阻塞 JS(RSC);Dashboard / Workflow Viewer 可為 Client Component |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 4. 視覺基準
|
|
||||||
|
|
||||||
| 項目 | 值(source of truth:`design-source/index.html` 的 CSS 變數) |
|
|
||||||
|---|---|
|
|
||||||
| 主背景 | `#0F0F0F`(現有是 `#0a0a0a` — 本次改為 `#0F0F0F`) |
|
|
||||||
| Card | `#1A1A1A` |
|
|
||||||
| Line | `#262626` / `#303030` |
|
|
||||||
| Primary | `#6366F1`(indigo) → `#8B5CF6`(violet)漸層 |
|
|
||||||
| Text | `#EDEDED` / dim `#A0A0A0` / mute `#6B6B6B` |
|
|
||||||
| 字型 | Inter 300/400/500/600/700/800;Mono 400/500/600 |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 5. 範圍界線
|
|
||||||
|
|
||||||
### 納入
|
|
||||||
- 5 screen(Landing、Auth、Dashboard、API Keys、Workflow Viewer)
|
|
||||||
- Logo(SVG arc wordmark)、Icon set(inline SVG,從 primitives.jsx port)
|
|
||||||
- TopNav / Footer / Sidebar 三個 shell 元件
|
|
||||||
|
|
||||||
### 不納入(本次 SDD)
|
|
||||||
- **Multi-tenant workspace 切換**(設計稿有 "Northwind" breadcrumb,本次純顯示用戶 email)
|
|
||||||
- **Multi-API-key CRUD 後端**(cypher-executor 現只支援每帳號一把 key,多 key table 先以「目前只支援一把」狀態呈現 — 見 §6.2)
|
|
||||||
- **Workflow 編輯**(只做 read-only viewer;編輯仍走 acr CLI / YAML)
|
|
||||||
- **即時執行狀態 stream**(minimap / zoom 僅 UI,不做真實 pan-zoom transform)
|
|
||||||
- **i18n 中英切換**、**Hall of Fame**、**Donate**
|
|
||||||
- **Swagger UI 頁(/api-docs)** — 保留現狀
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 6. API 依賴(全部打 `cypher.arcrun.dev`)
|
|
||||||
|
|
||||||
### 6.1 既有(已實作於 cypher-executor)
|
|
||||||
- `GET /me` — 取得登入用戶 + api_key(driver:dashboard 側欄 avatar / API Keys 頁的單把 key)
|
|
||||||
- `PUT /me/api-key/rotate`、`DELETE /me/api-key` — Rotate / Revoke
|
|
||||||
- `GET /auth/google/start`、`GET /auth/github/start` — OAuth 起點
|
|
||||||
- `POST /auth/logout`
|
|
||||||
- `POST /webhooks/named/{name}/trigger` — 觸發(給 Landing 文案展示)
|
|
||||||
|
|
||||||
### 6.2 需新增的 endpoint(阻擋項;**本 SDD 只定義契約,cypher-executor 實作歸屬於另一個 task**)
|
|
||||||
|
|
||||||
| Method | Path | 說明 | 用途 |
|
|
||||||
|---|---|---|---|
|
|
||||||
| GET | `/workflows` | 列出當前 api_key 名下的 workflow(名稱、nodes、modified、run_count、status) | Dashboard / Workflows Table |
|
|
||||||
| GET | `/workflows/:name` | 取得 workflow 詳細(含 graph 節點 + edges + YAML) | Workflow Viewer |
|
|
||||||
| GET | `/workflows/:name/yaml` | 下載 raw YAML | Workflow Viewer Export |
|
|
||||||
| GET | `/apps` | 列出 app(= workflow 標上 icon/tone/description metadata) | Dashboard / Apps Grid |
|
|
||||||
| GET | `/api-keys` | 列出多把 key(若後端仍是單把,回傳單元素 array) | API Keys 頁 |
|
|
||||||
| POST | `/api-keys` | 建立新 key | API Keys 頁「Create new key」 |
|
|
||||||
| PATCH | `/api-keys/:id` | `{ active: boolean }` 切換 | API Keys 頁 toggle |
|
|
||||||
| DELETE | `/api-keys/:id` | 刪除 | API Keys 頁 trash |
|
|
||||||
|
|
||||||
**在後端尚未實作前**:前端用型別化 fetch wrapper 封裝呼叫;遇到 404 顯示 empty state(而非假資料)。本 SDD 明確禁止 hard-code mock fixture。
|
|
||||||
|
|
||||||
### 6.3 登入前後可用的公開資訊
|
|
||||||
- Integrations 清單(20 個 recipe):現有 `/integrations` 頁已有靜態清單,繼續複用。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 7. Dogfooding 紅線
|
|
||||||
|
|
||||||
| 禁止 | 用哪個 arcrun 內部替代 |
|
|
||||||
|---|---|
|
|
||||||
| Auth0 / Clerk / Supabase Auth | cypher-executor `/auth/*` + session cookie(現行 `arcrun_session`) |
|
|
||||||
| Segment / PostHog | 不加,或用後續 arcrun `analytics` 零件 |
|
|
||||||
| Vercel KV / Upstash | Cloudflare KV(經 cypher-executor) |
|
|
||||||
| 直接寫第三方 API(Slack / Notion 等)作 dashboard demo | 透過 arcrun workflow + trigger 模擬 |
|
|
||||||
| ReactFlow / XYFlow | 純 SVG 手刻(設計稿本來就是手刻) |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 8. 驗收總清單
|
|
||||||
|
|
||||||
- [ ] 5 個 screen 全部在 `/`, `/auth`, `/dashboard`, `/keys`, `/workflows/[id]` 可達。
|
|
||||||
- [ ] 設計稿的 spacing / color / border-radius 100% 對得上(以 `design-source/index.html` CSS 變數為準)。
|
|
||||||
- [ ] middleware 保護 `/dashboard`, `/keys`, `/workflows/*`(未登入 → `/auth?redirect=...`)。
|
|
||||||
- [ ] 只打 `cypher.arcrun.dev`(可透過 `NEXT_PUBLIC_API_BASE` override),grep 結果不含其他外部 API host。
|
|
||||||
- [ ] `package.json` 新增依賴 = 0(本次不引入新 npm 套件)。
|
|
||||||
- [ ] 無 mock 資料:若後端未實作,顯示 loading 或 empty state,不編假陣列給 UI。
|
|
||||||
- [ ] `pnpm build` 通過,`next-on-pages` 輸出無 edge-runtime 錯誤。
|
|
||||||
@@ -1,140 +0,0 @@
|
|||||||
# Frontend Redesign — Tasks
|
|
||||||
|
|
||||||
> 進度來源:本檔。完成一項立刻 `[x]`,不批次。
|
|
||||||
> 本 SDD 建立於 2026-04-23;richblack 尚未下令動工,**所有 task 預設 `[ ]`**。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Phase 0 — SDD 建立(本次)
|
|
||||||
|
|
||||||
- [x] 取得 Claude Design bundle,歸檔至 `design-source/`
|
|
||||||
- [x] 撰寫 `requirements.md`
|
|
||||||
- [x] 撰寫 `design.md`
|
|
||||||
- [x] 撰寫 `tasks.md`(本檔)
|
|
||||||
- [ ] richblack review + 認可 → 開 Phase 1
|
|
||||||
|
|
||||||
**等 richblack 明確說「開始動工」之前,不觸 `landing/` 任何檔案。**
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Phase 1 — Tokens + Shell(無外部 API 依賴)
|
|
||||||
|
|
||||||
- [ ] 1.1 建 `landing/app/design-tokens.css`,抄 design-source CSS :root 變數
|
|
||||||
- [ ] 1.2 更新 `landing/app/globals.css`:`@import './design-tokens.css'`,`@theme inline` 對應 token → Tailwind color
|
|
||||||
- [ ] 1.3 `layout.tsx` 引入 `next/font/google` 的 Inter + JetBrains Mono;body bg 改 `var(--bg)`
|
|
||||||
- [ ] 1.4 建 `landing/components/shell/Icon.tsx`(港設計稿 primitives.jsx 的所有 icon)
|
|
||||||
- [ ] 1.5 建 `landing/components/shell/Logo.tsx`(SVG arc wordmark)
|
|
||||||
- [ ] 1.6 建 `landing/components/shell/TopNav.tsx`
|
|
||||||
- [ ] 1.7 建 `landing/components/shell/Footer.tsx`
|
|
||||||
- [ ] 1.8 建 `landing/components/shell/Sidebar.tsx`(含頭像、項目清單、登出按鈕)
|
|
||||||
- [ ] 1.9 建 `landing/components/primitives/Button.tsx`(variants: primary / secondary / ghost / danger-ghost / sm / lg)
|
|
||||||
- [ ] 1.10 建 `landing/components/primitives/Pill.tsx`、`Toggle.tsx`
|
|
||||||
- [ ] 1.11 Lint + build pass
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Phase 2 — Landing(`/`)
|
|
||||||
|
|
||||||
- [ ] 2.1 Rewrite `app/page.tsx`,結構照 design-source/screens/Landing.jsx
|
|
||||||
- [ ] 2.2 Hero:heading / eyebrow / CTA / radial-grid bg(CSS only)
|
|
||||||
- [ ] 2.3 Paths 左卡:install tabs 元件(Client Component)+ terminal code blocks(字串改 `acr` 實際指令)
|
|
||||||
- [ ] 2.4 Paths 右卡:chat preview(靜態)
|
|
||||||
- [ ] 2.5 Feature strip(4 cell)
|
|
||||||
- [ ] 2.6 TopNav / Footer 接上
|
|
||||||
- [ ] 2.7 Responsive(≤ 768px: paths 單欄、hero h1 縮 1 級)
|
|
||||||
- [ ] 2.8 視覺比對 design-source/index.html(截圖對比 / DOM spec 檢查)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Phase 3 — Auth(`/auth`,`/login` redirect)
|
|
||||||
|
|
||||||
- [ ] 3.1 建 `app/auth/page.tsx`(Client)
|
|
||||||
- [ ] 3.2 Sign in / Sign up tabs + OAuth buttons(Google / GitHub)接既有 `/auth/google/start` / `/auth/github/start`
|
|
||||||
- [ ] 3.3 Email / Password form — submit 顯示「OAuth only」提示(待後端支援)
|
|
||||||
- [ ] 3.4 `/login` 頁改為 `redirect('/auth')`
|
|
||||||
- [ ] 3.5 視覺比對 design-source/screens/Auth.jsx
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Phase 4 — API Keys(`/keys`)
|
|
||||||
|
|
||||||
- [ ] 4.1 建 `lib/api.ts` fetch wrapper
|
|
||||||
- [ ] 4.2 建 `lib/apiKeys.ts`(listKeys / createKey / patchKey / deleteKey)— 後端未實作時回 `[{ ...from /me }]` 的 fallback
|
|
||||||
- [ ] 4.3 建 `app/keys/page.tsx`:頂部 new-key-box(sessionStorage flag)、全表格
|
|
||||||
- [ ] 4.4 「Create new key」呼叫 `POST /api-keys`(後端未實作 → 顯示「coming soon」toast)
|
|
||||||
- [ ] 4.5 Toggle active / trash 接 PATCH / DELETE
|
|
||||||
- [ ] 4.6 middleware.ts matcher 加 `/keys/:path*`
|
|
||||||
- [ ] 4.7 原 `/dashboard` 頁的 Key 卡區塊移除,放提示「API Keys 已搬到 /keys」
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Phase 5 — Dashboard(`/dashboard`)
|
|
||||||
|
|
||||||
- [ ] 5.1 建 `lib/workflows.ts`、`lib/apps.ts`
|
|
||||||
- [ ] 5.2 Rewrite `app/dashboard/page.tsx`(保留 /me session 檢查)
|
|
||||||
- [ ] 5.3 Main head:breadcrumb / greeting / summary counters
|
|
||||||
- [ ] 5.4 Apps Grid + empty-state 卡(`/apps` 404 → 只顯示 empty-state)
|
|
||||||
- [ ] 5.5 Workflows Table(`/workflows` 404 → 顯示「no workflows — acr push」CTA)
|
|
||||||
- [ ] 5.6 Sidebar 接真實 user(email / display_name)、登出按鈕接 `/auth/logout`
|
|
||||||
- [ ] 5.7 視覺比對 design-source/screens/Dashboard.jsx
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Phase 6 — Workflow Viewer(`/workflows/[name]`)
|
|
||||||
|
|
||||||
- [ ] 6.1 建 `app/workflows/[name]/page.tsx`(Client)
|
|
||||||
- [ ] 6.2 Topbar:back / logo / breadcrumb / title / saved indicator / share / export / edit-in-claude
|
|
||||||
- [ ] 6.3 `<Canvas>`:SVG defs + 節點定位 + bezier edges;資料從 `GET /workflows/:name`
|
|
||||||
- [ ] 6.4 Auto-layout:topological depth → columns,同 depth 平均分配 y
|
|
||||||
- [ ] 6.5 NodeCard 點擊 → DetailPanel
|
|
||||||
- [ ] 6.6 DetailPanel:input / output schema、triplet(disabled)、last run stats(可選)
|
|
||||||
- [ ] 6.7 Export YAML:`GET /workflows/:name/yaml` → blob download
|
|
||||||
- [ ] 6.8 Minimap(純顯示)、ZoomControls(簡單 scale)
|
|
||||||
- [ ] 6.9 middleware.ts matcher 加 `/workflows/:path*`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Phase 7 — 清理 + 收尾
|
|
||||||
|
|
||||||
- [ ] 7.1 刪除舊 `/dashboard` 不再用的 code(rotate/revoke 若全搬到 /keys)
|
|
||||||
- [ ] 7.2 Grep 檢查:除 `cypher.arcrun.dev` 外無任何第三方 API host
|
|
||||||
- [ ] 7.3 Grep 檢查:無 `mock` / `fixture` / 硬編的假資料陣列(`app-empty` 的字串常數除外)
|
|
||||||
- [ ] 7.4 Grep 檢查:無新增 npm 依賴(`git diff landing/package.json` 應只改版本,不加條目)
|
|
||||||
- [ ] 7.5 `pnpm build` 通過;`next-on-pages` 通過
|
|
||||||
- [ ] 7.6 local `pnpm dev` 手工巡覽 5 screen,每個截圖比對 design-source
|
|
||||||
- [ ] 7.7 更新 `.agents/specs/arcrun/arcrun.md`,加一段「CLI 1.2.0 搭配新 landing」之類的進度註記
|
|
||||||
- [ ] 7.8 richblack 認可 → 合併 / deploy
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 需要 cypher-executor 搭配的 endpoint(不屬於本 SDD)
|
|
||||||
|
|
||||||
若 richblack 決定新 endpoint 要和前端同 PR 做:
|
|
||||||
|
|
||||||
- [ ] 後端:`GET /workflows`
|
|
||||||
- [ ] 後端:`GET /workflows/:name` + `/yaml`
|
|
||||||
- [ ] 後端:`GET /apps`
|
|
||||||
- [ ] 後端:`GET /api-keys`、`POST /api-keys`、`PATCH /api-keys/:id`、`DELETE /api-keys/:id`
|
|
||||||
|
|
||||||
否則:以 empty state 呈現,封測也能運作。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## KBDB 整合(配合 matrix/kbdb/.agents/specs/arcrun-key-auth/)
|
|
||||||
|
|
||||||
Arcrun 用戶的 `ak_xxx` Key 同時可用於 KBDB(捆綁服務,自動開通)。
|
|
||||||
cypher-executor 需在以下時機呼叫 KBDB:
|
|
||||||
|
|
||||||
- [ ] 後端:OAuth callback 成功 → `POST /partners`(建立 KBDB partner 記錄)
|
|
||||||
- [ ] 後端:`PUT /me/api-key/rotate` → 舊 partner revoke + 建新 partner 記錄
|
|
||||||
- [ ] 後端:`DELETE /me/api-key` → KBDB partner revoke
|
|
||||||
|
|
||||||
詳細設計見 `matrix/kbdb/.agents/specs/arcrun-key-auth/design.md`。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 目前狀態
|
|
||||||
|
|
||||||
- **進度**:Phase 0 已完成(4/5;最後一項等 richblack 認可)。
|
|
||||||
- **阻擋**:richblack 認可 + 「開始動工」指令。
|
|
||||||
- **未啟動**:Phase 1-7 全部 `[ ]`。
|
|
||||||
@@ -1,325 +0,0 @@
|
|||||||
# arcrun.dev Landing Page — SDD
|
|
||||||
|
|
||||||
> **目標**:給工程師一個門面,可以取得 API Key、管理 Key、探索 API(Swagger),同時藉此獲得會員 Email。
|
|
||||||
> **原則**:先快速可用,不追求功能完整。榮譽牆、Python Lib 是後期。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 0. 範圍(這份 SDD 涵蓋)
|
|
||||||
|
|
||||||
| 功能 | 說明 |
|
|
||||||
|---|---|
|
|
||||||
| 首頁 Hero | 說明 arcrun 是什麼,CTA 取得 API Key |
|
|
||||||
| OAuth 登入 | Google / GitHub(用自己的 auth recipe — dogfooding) |
|
|
||||||
| API Key 管理 | 查看、Rotate、Revoke |
|
|
||||||
| Swagger UI | 嵌入 `/api`,讓工程師直接試打 |
|
|
||||||
| 榮譽牆 `/integrations` | 靜態骨架,先列 20 個 recipe,無動態數字 |
|
|
||||||
| 中英切換 | `?lang=zh` |
|
|
||||||
|
|
||||||
**不在本次範圍**:Python lib、Donate 整合、Social Proof 即時數字、貢獻者排行。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 1. 技術選型
|
|
||||||
|
|
||||||
### 1.1 框架:Next.js(App Router)
|
|
||||||
|
|
||||||
**選 Next.js 而非 Astro 的原因**:
|
|
||||||
- `finally-click` 已有完整 Next.js + OAuth 回調實作,可直接複用模式
|
|
||||||
- API Key 管理頁有登入態保護需求,Next.js 的 middleware 最直接
|
|
||||||
- Cloudflare Pages 支援 Next.js(`@cloudflare/next-on-pages`)
|
|
||||||
- Astro 在動態路由保護上摩擦較多
|
|
||||||
|
|
||||||
### 1.2 部署:Cloudflare Pages
|
|
||||||
|
|
||||||
```
|
|
||||||
arcrun.dev → Cloudflare Pages(Next.js)
|
|
||||||
API calls → cypher.arcrun.dev(現有 Worker)
|
|
||||||
```
|
|
||||||
|
|
||||||
### 1.3 儲存:現有 cypher-executor CREDENTIALS_KV + 新增 USERS_KV
|
|
||||||
|
|
||||||
現有 cypher-executor Worker 已有:
|
|
||||||
- `CREDENTIALS_KV`:`{api_key}:cred:{name}` 存 tenant credentials
|
|
||||||
- `RECIPES`:auth recipes
|
|
||||||
|
|
||||||
新增需求:
|
|
||||||
- **USERS_KV**:存 user 帳號,key = `user:{provider}:{provider_user_id}`
|
|
||||||
- value: `{ email, display_name, api_key, created_at, provider }`
|
|
||||||
- **SESSIONS_KV**:存 login session,key = `sess:{session_id}`
|
|
||||||
- value: `{ api_key, email, expires_at }`
|
|
||||||
- TTL = 7 天
|
|
||||||
|
|
||||||
兩個 KV 都加到 cypher-executor `wrangler.toml`。
|
|
||||||
|
|
||||||
### 1.4 OAuth — Dogfooding 自己的 Auth Recipe
|
|
||||||
|
|
||||||
登入用 arcrun 自己的 auth recipe:
|
|
||||||
- 不是用 arcrun auth recipe 的 `http_request` runner 去打第三方
|
|
||||||
- 而是 **直接複用 recipe YAML 裡定義的 OAuth App 設定(client_id/secret)**
|
|
||||||
- Worker 端實作 standard OAuth2 authorization_code flow
|
|
||||||
|
|
||||||
**支援提供商(MVP)**:
|
|
||||||
- Google(google_drive recipe 的 OAuth App,或另建 arcrun-login Google App)
|
|
||||||
- GitHub(github recipe 的 OAuth App)
|
|
||||||
|
|
||||||
登入 OAuth App 與 auth recipe 的 OAuth App **可以是同一個**(只要 scopes 包含 `openid profile email`),但更乾淨的做法是登入用獨立的 Google/GitHub App(只要 email scope),auth recipe 用的是資源存取 App。
|
|
||||||
|
|
||||||
**決策:登入用獨立 OAuth App**
|
|
||||||
- `GOOGLE_CLIENT_ID`、`GOOGLE_CLIENT_SECRET` — 只申請 `openid profile email`
|
|
||||||
- `GITHUB_CLIENT_ID`、`GITHUB_CLIENT_SECRET` — 只申請 `read:user` + `user:email`
|
|
||||||
- 以 Worker Secret 方式存入 cypher-executor
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 2. 頁面結構
|
|
||||||
|
|
||||||
```
|
|
||||||
arcrun.dev/
|
|
||||||
├── / 首頁(Hero + Code snippet + CTA)
|
|
||||||
├── /login 登入頁(Google / GitHub 按鈕)
|
|
||||||
├── /auth/callback OAuth callback(Pages Function)
|
|
||||||
├── /dashboard API Key 管理(需登入)
|
|
||||||
├── /api Swagger UI(嵌入 swagger.json)
|
|
||||||
└── /integrations 服務目錄(靜態,20 個 recipe)
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 3. 登入 / OAuth 流程
|
|
||||||
|
|
||||||
### 3.1 流程圖
|
|
||||||
|
|
||||||
```
|
|
||||||
用戶點「Google 登入」
|
|
||||||
→ GET /auth/google/start(Worker 端)
|
|
||||||
→ redirect 到 Google OAuth(state = random, 存 SESSIONS_KV sess:state:{state} = {provider, redirect_back})
|
|
||||||
→ 用戶同意
|
|
||||||
→ GET /auth/callback?code=...&state=...(Worker 端)
|
|
||||||
→ 驗 state
|
|
||||||
→ 用 code 換 access_token(POST google token endpoint)
|
|
||||||
→ 用 token 取 userinfo(GET google userinfo)
|
|
||||||
→ upsert USERS_KV user:{provider}:{provider_id} = {email, display_name, api_key, ...}
|
|
||||||
→ 若新用戶:呼叫現有 /register?email=... 取得 arcrun API Key
|
|
||||||
→ 建立 session:SESSIONS_KV sess:{session_id} = {api_key, email, ...},TTL=7d
|
|
||||||
→ Set-Cookie: arcrun_session={session_id}; HttpOnly; Secure; SameSite=Lax
|
|
||||||
→ redirect 到 /dashboard
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3.2 「若新用戶取得 API Key」的邏輯
|
|
||||||
|
|
||||||
現有 `/register` endpoint 接受 `email` 回傳 `api_key`(HMAC 確定性)。
|
|
||||||
但 landing page 需要的是**真正綁定到用戶帳號的 key**,且用戶可以 rotate/revoke。
|
|
||||||
|
|
||||||
**方案:延伸現有 register endpoint**
|
|
||||||
|
|
||||||
`/register` 目前:`HMAC(secret, email)` → 確定性 api_key,存 CREDENTIALS_KV
|
|
||||||
|
|
||||||
新增邏輯:
|
|
||||||
1. 若 USERS_KV 已有此 user → 直接用記錄裡的 api_key
|
|
||||||
2. 若新 user → 呼叫現有 `/register`(保持 HMAC 確定性邏輯)→ 拿到 api_key → 存入 USERS_KV
|
|
||||||
|
|
||||||
**好處**:不破壞現有 register 邏輯;登入後的 dashboard 顯示的 key = 現有 key = 封測用的 key。
|
|
||||||
|
|
||||||
### 3.3 Rotate / Revoke
|
|
||||||
|
|
||||||
- **Rotate**:產生新 UUID v4 key → 更新 USERS_KV 記錄 → 舊 key 失效(透過把新 key 加到 CREDENTIALS_KV,舊 key 的資料都跟著 API Key 命名空間走,所以 credentials 會留在舊 namespace)
|
|
||||||
- 簡化版:Rotate 後顯示提示「您的 workflow credentials 已和舊 Key 分離,請重新設定」
|
|
||||||
- **Revoke**:USERS_KV 記錄 `revoked: true` → Worker middleware 拒絕此 key
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 4. API 端點(新增到 cypher-executor)
|
|
||||||
|
|
||||||
```
|
|
||||||
GET /auth/google/start → redirect 到 Google OAuth
|
|
||||||
GET /auth/github/start → redirect 到 GitHub OAuth
|
|
||||||
GET /auth/callback?code=&state= → 換 token、建立 session
|
|
||||||
POST /auth/logout → 清 session cookie
|
|
||||||
GET /me → 回傳當前登入用戶資訊(需 session cookie 或 API Key)
|
|
||||||
PUT /me/api-key/rotate → 產生新 key
|
|
||||||
DELETE /me/api-key → Revoke(標記撤銷)
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 5. 前端頁面設計
|
|
||||||
|
|
||||||
### 5.1 首頁(/)
|
|
||||||
|
|
||||||
```
|
|
||||||
Hero:
|
|
||||||
Stop fighting OAuth.
|
|
||||||
One API key. Every service. Works anywhere.
|
|
||||||
|
|
||||||
[Get API Key — Free] [View on GitHub]
|
|
||||||
|
|
||||||
Before/After:
|
|
||||||
40 行 OAuth 程式碼 → auth.bind("google_drive")
|
|
||||||
|
|
||||||
Code Demo(三個 tab):
|
|
||||||
Python / JavaScript / HTTP(n8n 用戶)
|
|
||||||
|
|
||||||
[Get Free API Key] 按鈕
|
|
||||||
```
|
|
||||||
|
|
||||||
### 5.2 登入頁(/login)
|
|
||||||
|
|
||||||
```
|
|
||||||
arcrun
|
|
||||||
|
|
||||||
登入或建立帳號
|
|
||||||
|
|
||||||
[Continue with Google]
|
|
||||||
[Continue with GitHub]
|
|
||||||
|
|
||||||
不需要信用卡。API Key 立即可用。
|
|
||||||
```
|
|
||||||
|
|
||||||
### 5.3 Dashboard(/dashboard)
|
|
||||||
|
|
||||||
```
|
|
||||||
歡迎,{display_name}
|
|
||||||
|
|
||||||
您的 API Key
|
|
||||||
┌────────────────────────────────┐
|
|
||||||
│ ak_xxxxxxxxxxxxxxxxxxxx [複製] │
|
|
||||||
└────────────────────────────────┘
|
|
||||||
|
|
||||||
[Rotate Key] [Revoke Key]
|
|
||||||
|
|
||||||
使用說明:
|
|
||||||
Authorization: Bearer {key}
|
|
||||||
或 X-Arcrun-API-Key: {key}
|
|
||||||
|
|
||||||
[登出]
|
|
||||||
```
|
|
||||||
|
|
||||||
### 5.4 Swagger UI(/api)
|
|
||||||
|
|
||||||
- 嵌入 `<SwaggerUIBundle>` JS(CDN)
|
|
||||||
- `url: 'https://cypher.arcrun.dev/swagger.json'`(現有 Worker 已有 `/docs` openapi endpoint)
|
|
||||||
- 頂部說明:「這是 arcrun 的原始 API。Python / JS lib 是它的包裝,任何能發 HTTP request 的工具都能直接用。」
|
|
||||||
|
|
||||||
### 5.5 服務目錄(/integrations)
|
|
||||||
|
|
||||||
- 靜態列出 20 個 auth recipe(從 seed data 產生)
|
|
||||||
- 每個 recipe:名稱、認證方式(static_key / service_account)、所需 credentials
|
|
||||||
- 「找不到你要的服務?開 PR 貢獻 Recipe」CTA
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 6. 檔案結構
|
|
||||||
|
|
||||||
```
|
|
||||||
arcrun/landing/ ← 新 Next.js 專案
|
|
||||||
├── app/
|
|
||||||
│ ├── layout.tsx
|
|
||||||
│ ├── page.tsx 首頁
|
|
||||||
│ ├── login/
|
|
||||||
│ │ └── page.tsx
|
|
||||||
│ ├── dashboard/
|
|
||||||
│ │ ├── page.tsx
|
|
||||||
│ │ └── middleware.ts (或 root middleware)
|
|
||||||
│ ├── api-docs/
|
|
||||||
│ │ └── page.tsx Swagger UI
|
|
||||||
│ └── integrations/
|
|
||||||
│ └── page.tsx
|
|
||||||
├── middleware.ts 保護 /dashboard(讀 cookie)
|
|
||||||
├── lib/
|
|
||||||
│ └── auth.ts session helpers
|
|
||||||
├── public/
|
|
||||||
├── next.config.ts
|
|
||||||
├── package.json
|
|
||||||
└── wrangler.toml CF Pages 設定
|
|
||||||
```
|
|
||||||
|
|
||||||
cypher-executor 新增:
|
|
||||||
```
|
|
||||||
arcrun/cypher-executor/src/routes/
|
|
||||||
├── auth.ts ← 新增(OAuth start/callback/logout/me)
|
|
||||||
```
|
|
||||||
|
|
||||||
cypher-executor wrangler.toml 新增:
|
|
||||||
```toml
|
|
||||||
[[kv_namespaces]]
|
|
||||||
binding = "USERS_KV"
|
|
||||||
id = "<to be created>"
|
|
||||||
|
|
||||||
[[kv_namespaces]]
|
|
||||||
binding = "SESSIONS_KV"
|
|
||||||
id = "455d0505c7534883a4d4985ab8295857"
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 7. 環境變數 / Secrets
|
|
||||||
|
|
||||||
### cypher-executor(Worker Secrets)
|
|
||||||
|
|
||||||
```
|
|
||||||
GOOGLE_CLIENT_ID Google OAuth App client_id(僅 openid profile email scope)
|
|
||||||
GOOGLE_CLIENT_SECRET Google OAuth App client_secret
|
|
||||||
GITHUB_CLIENT_ID GitHub OAuth App client_id(read:user + user:email scope)
|
|
||||||
GITHUB_CLIENT_SECRET GitHub OAuth App client_secret
|
|
||||||
SESSION_SECRET 隨機 32 bytes,用於 HMAC session ID(或直接用 UUID)
|
|
||||||
```
|
|
||||||
|
|
||||||
### landing(Pages Environment Variables)
|
|
||||||
|
|
||||||
```
|
|
||||||
NEXT_PUBLIC_API_BASE https://cypher.arcrun.dev
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 8. 實作步驟(Checklist)
|
|
||||||
|
|
||||||
### Phase 1:cypher-executor 後端擴充
|
|
||||||
|
|
||||||
- [x] `wrangler kv:namespace create USERS_KV` → 填入 wrangler.toml (id: 25bef01d079148919578894434d58c4d)
|
|
||||||
- [x] `wrangler kv:namespace create SESSIONS_KV` → 填入 wrangler.toml (id: 455d0505c7534883a4d4985ab8295857)
|
|
||||||
- [x] 建立 `arcrun/cypher-executor/src/routes/auth.ts`
|
|
||||||
- [x] GET `/auth/google/start`
|
|
||||||
- [x] GET `/auth/github/start`
|
|
||||||
- [x] GET `/auth/callback`(換 token → userinfo → upsert USERS_KV → 建 session → Set-Cookie → redirect)
|
|
||||||
- [x] POST `/auth/logout`
|
|
||||||
- [x] GET `/me`(讀 session cookie 或 API Key header)
|
|
||||||
- [x] PUT `/me/api-key/rotate`
|
|
||||||
- [x] DELETE `/me/api-key`(revoke)
|
|
||||||
- [x] 在 `src/index.ts` 掛載 `authRouter`
|
|
||||||
- [ ] `wrangler secret put GOOGLE_CLIENT_ID` 等 4 個 secrets ← **用戶需自建 Google/GitHub OAuth App**
|
|
||||||
- [x] `wrangler deploy` ← 已部署(Worker version 7877857b)
|
|
||||||
|
|
||||||
### Phase 2:Next.js Landing 專案
|
|
||||||
|
|
||||||
- [x] `npx create-next-app@latest arcrun/landing --typescript --tailwind --app`
|
|
||||||
- [x] 設定 `@cloudflare/next-on-pages`(Next.js 15 + .npmrc legacy-peer-deps)
|
|
||||||
- [ ] 建立 `middleware.ts`(保護 /dashboard,讀 `arcrun_session` cookie)← 待做
|
|
||||||
- [x] 首頁(`app/page.tsx`):Hero + Code Demo tab + CTA
|
|
||||||
- [x] 登入頁(`app/login/page.tsx`):Google / GitHub 按鈕(href 到 cypher.arcrun.dev/auth/google/start)
|
|
||||||
- [x] Dashboard(`app/dashboard/page.tsx`):顯示 API Key,Rotate / Revoke 按鈕
|
|
||||||
- [x] Swagger UI(`app/api-docs/page.tsx`):client component,動態 import swagger-ui CDN
|
|
||||||
- [x] 服務目錄(`app/integrations/page.tsx`):靜態,列 20 個 recipe
|
|
||||||
- [ ] 中英切換 ← 低優先,可延後
|
|
||||||
- [x] `wrangler pages deploy` → https://42a8d302.arcrun-landing.pages.dev
|
|
||||||
- [ ] Cloudflare dashboard 設定 arcrun.dev custom domain → arcrun-landing Pages project
|
|
||||||
|
|
||||||
### Phase 3:驗收(待 OAuth Secrets 填入後)
|
|
||||||
|
|
||||||
- [ ] Google / GitHub OAuth 完整流程(登入 → dashboard → 看到 key)
|
|
||||||
- [ ] Rotate:新 key 出現
|
|
||||||
- [ ] Revoke:舊 key 的 API 呼叫回傳 401
|
|
||||||
- [ ] Swagger UI 正常載入,可試打 `/health`
|
|
||||||
- [x] `/integrations` 正確列出 20 個服務
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 9. 待決事項(開始實作前確認)
|
|
||||||
|
|
||||||
| 問題 | 預設決策 |
|
|
||||||
|---|---|
|
|
||||||
| Google OAuth App 是否要另建(只有 email scope)? | 是,另建;auth recipe 的 App 不動 |
|
|
||||||
| Rotate 後舊 credentials 是否遷移? | 不遷移,顯示提示 |
|
|
||||||
| Domain arcrun.dev 是否已購入且在 Cloudflare? | 假設是(wrangler.toml 有設 zone_name) |
|
|
||||||
| 登入後 redirect 預設到 /dashboard | 是,可從 `?redirect=` 覆寫 |
|
|
||||||
@@ -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
|
|
||||||
|
|
||||||
### 系統關係圖
|
|
||||||
|
|
||||||
```
|
|
||||||
使用者程式碼
|
|
||||||
├── CLI(acr) → cypher.arcrun.dev(HTTP API)
|
|
||||||
├── Python SDK(arcrun) → cypher.arcrun.dev(HTTP API)
|
|
||||||
└── JS SDK(arcrun / @arcrun/sdk) → cypher.arcrun.dev(HTTP API)
|
|
||||||
|
|
||||||
arcrun.dev 網站(Next.js / Cloudflare Pages)
|
|
||||||
├── /login → /auth/google/start, /auth/github/start(cypher.arcrun.dev)
|
|
||||||
├── /dashboard → /me, /me/api-key/rotate(cypher.arcrun.dev)
|
|
||||||
├── /integrations → /auth-recipes(cypher.arcrun.dev)
|
|
||||||
└── /components → /recipes + 靜態零件清單(embedded)
|
|
||||||
|
|
||||||
cypher.arcrun.dev(Cloudflare Worker — cypher-executor,不改)
|
|
||||||
├── POST /credentials ← 接收 { name, encrypted, iv }
|
|
||||||
├── GET /credentials ← 列出 credential 名稱
|
|
||||||
├── DELETE /credentials/:name ← 刪除 credential
|
|
||||||
├── GET /auth-recipes ← 列出 20 個 auth recipe
|
|
||||||
├── GET /auth-recipes/:service ← 單一 recipe 詳情
|
|
||||||
├── POST /webhooks/named ← 部署 workflow
|
|
||||||
├── POST /webhooks/named/:name/trigger ← 觸發 workflow
|
|
||||||
├── GET /webhooks/named ← 列出 workflow
|
|
||||||
├── POST /register ← 註冊取得 API Key
|
|
||||||
├── GET /me ← 當前用戶資訊
|
|
||||||
└── /auth/* ← OAuth 流程
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Python SDK(`arcrun/python-sdk/`)
|
|
||||||
|
|
||||||
### 目錄結構
|
|
||||||
|
|
||||||
```
|
|
||||||
arcrun/python-sdk/
|
|
||||||
├── pyproject.toml ← hatchling build, name="arcrun", deps=[httpx>=0.27, cryptography>=42]
|
|
||||||
├── README.md
|
|
||||||
└── arcrun/
|
|
||||||
├── __init__.py ← from .client import Arcrun
|
|
||||||
├── client.py ← Arcrun class(主入口)
|
|
||||||
├── crypto.py ← AES-GCM 加密(client 端,用 cryptography 套件)
|
|
||||||
├── creds.py ← CredentialsClient(push/list/delete)
|
|
||||||
├── auth.py ← AuthClient(setup/bind/get_token/list_services)
|
|
||||||
└── workflows.py ← WorkflowClient(run/push/list/delete)
|
|
||||||
```
|
|
||||||
|
|
||||||
### API 設計
|
|
||||||
|
|
||||||
```python
|
|
||||||
from arcrun import Arcrun
|
|
||||||
|
|
||||||
# 建構 — api_key 從參數 > 環境變數 > ~/.arcrun/config.yaml 自動取得
|
|
||||||
client = Arcrun()
|
|
||||||
# 或明確指定
|
|
||||||
client = Arcrun(api_key="ak_xxx", encryption_key="hexstring")
|
|
||||||
|
|
||||||
# Auth:設定並綁定服務
|
|
||||||
client.auth.setup("openai", api_key="sk-xxx") # 加密 + 上傳
|
|
||||||
openai_client = client.auth.bind("openai") # 取回 pre-auth client
|
|
||||||
response = openai_client.get("/models") # httpx.Client
|
|
||||||
token = client.auth.get_token("openai") # raw token string
|
|
||||||
services = client.auth.list_services() # [{ service, display_name, ... }]
|
|
||||||
|
|
||||||
# Credentials:低階操作
|
|
||||||
client.creds.push("my_token", "value123")
|
|
||||||
names = client.creds.list()
|
|
||||||
client.creds.delete("my_token")
|
|
||||||
|
|
||||||
# Workflows
|
|
||||||
result = client.workflows.run("my-flow", {"email": "user@example.com"})
|
|
||||||
url = client.workflows.push("my-flow", graph_dict)
|
|
||||||
workflows = client.workflows.list()
|
|
||||||
```
|
|
||||||
|
|
||||||
### Credential 加密流程
|
|
||||||
|
|
||||||
```
|
|
||||||
setup("openai", api_key="sk-xxx")
|
|
||||||
1. GET /auth-recipes/openai → recipe(含 required_secrets, inject)
|
|
||||||
2. 對應 required_secrets[0].key = "openai_api_key"
|
|
||||||
3. crypto.py 用 encryption_key AES-GCM 加密 "sk-xxx"
|
|
||||||
4. POST /credentials → { name: "openai_api_key", encrypted, iv }
|
|
||||||
5. 本地 _cred_cache["openai_api_key"] = "sk-xxx"(供 bind() 用)
|
|
||||||
|
|
||||||
bind("openai")
|
|
||||||
1. GET /auth-recipes/openai → recipe.inject.header = { Authorization: "Bearer {{secret.openai_api_key}}" }
|
|
||||||
2. 用 _cred_cache["openai_api_key"] 替換 template → "Bearer sk-xxx"
|
|
||||||
3. 回傳 AuthenticatedClient(base_url="https://api.openai.com/v1", headers={"Authorization": "Bearer sk-xxx"})
|
|
||||||
```
|
|
||||||
|
|
||||||
**注意**:`bind()` 依賴 `setup()` 在同一 session 建立的 `_cred_cache`。跨 session 使用時(credential 已上傳但 cache 不存在),`bind()` 無法解析 template — 此時 `get_token()` 也無法返回值。**這是已知限制,封測期間先接受。** 長期解法是 server 提供 `/credentials/:name/secret` 解密端點(u6u-core/credentials 已有)。
|
|
||||||
|
|
||||||
### 關鍵差異:crypto.py 的定位
|
|
||||||
|
|
||||||
`crypto.py` 只做 **加密**(encrypt),不做解密。
|
|
||||||
功能等同 `u6u-core/credentials/src/actions/crypto.ts` 的 `encrypt()` 函數。
|
|
||||||
解密只在 server 端發生(cypher-executor 的 `credential-injector.ts` 或 `u6u-core/credentials/getCredentialSecret.ts`)。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## JS/TS SDK(`arcrun/js-sdk/`)
|
|
||||||
|
|
||||||
### 目錄結構
|
|
||||||
|
|
||||||
```
|
|
||||||
arcrun/js-sdk/
|
|
||||||
├── package.json ← name TBD(arcrun vs @arcrun/sdk),tsup build
|
|
||||||
├── tsconfig.json ← ES2020, NodeNext
|
|
||||||
└── src/
|
|
||||||
├── index.ts ← export class Arcrun
|
|
||||||
├── crypto.ts ← Web Crypto API AES-GCM encrypt(client 端)
|
|
||||||
├── creds.ts ← CredentialsClient(push/list/delete)
|
|
||||||
├── auth.ts ← AuthClient(setup/bind/getToken/listServices)
|
|
||||||
└── workflows.ts ← WorkflowClient(run/push/list/delete)
|
|
||||||
```
|
|
||||||
|
|
||||||
### API 與 Python SDK 對等
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
import { Arcrun } from 'arcrun' // or '@arcrun/sdk'
|
|
||||||
|
|
||||||
const client = new Arcrun() // reads ARCRUN_API_KEY from env
|
|
||||||
|
|
||||||
await client.auth.setup('openai', { api_key: 'sk-xxx' })
|
|
||||||
const oai = await client.auth.bind('openai')
|
|
||||||
const models = await (await oai.get('/models')).json()
|
|
||||||
|
|
||||||
const token = await client.auth.getToken('openai')
|
|
||||||
const services = await client.auth.listServices()
|
|
||||||
|
|
||||||
await client.creds.push('my_token', 'value')
|
|
||||||
const names = await client.creds.list()
|
|
||||||
|
|
||||||
const result = await client.workflows.run('my-flow', { email: 'user@example.com' })
|
|
||||||
```
|
|
||||||
|
|
||||||
### Build 產物
|
|
||||||
|
|
||||||
```
|
|
||||||
dist/
|
|
||||||
├── index.js ← ESM
|
|
||||||
├── index.cjs ← CJS
|
|
||||||
├── index.d.ts ← TypeScript 型別
|
|
||||||
└── index.d.cts
|
|
||||||
```
|
|
||||||
|
|
||||||
### Crypto 實作
|
|
||||||
|
|
||||||
使用 Web Crypto API(`crypto.subtle`),相容 Node 18+ / browsers / CF Workers / Deno:
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
async function encrypt(plaintext: string, hexKey: string): Promise<{ encrypted: string; iv: string }> {
|
|
||||||
const key = await crypto.subtle.importKey('raw', hexToBytes(hexKey), { name: 'AES-GCM' }, false, ['encrypt']);
|
|
||||||
const iv = crypto.getRandomValues(new Uint8Array(12));
|
|
||||||
const ciphertext = await crypto.subtle.encrypt({ name: 'AES-GCM', iv }, key, new TextEncoder().encode(plaintext));
|
|
||||||
return { encrypted: toBase64(ciphertext), iv: toBase64(iv.buffer) };
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## arcrun.dev 網站
|
|
||||||
|
|
||||||
### 現有狀態(`arcrun/landing/`)
|
|
||||||
|
|
||||||
已完成:
|
|
||||||
- [x] `/` — Hero + Code Demo(Python/JS/HTTP tabs)
|
|
||||||
- [x] `/login` — Google + GitHub OAuth 按鈕(前端 OK,需設 OAuth secrets)
|
|
||||||
- [x] `/dashboard` — API Key 查看/Copy/Rotate/Revoke(依賴 `/me` API)
|
|
||||||
- [x] `/integrations` — 20 個 recipe 靜態卡片
|
|
||||||
- [x] `/api-docs` — Swagger UI CDN 嵌入
|
|
||||||
- [x] `middleware.ts` — 保護 `/dashboard`(未登入 → `/login`)
|
|
||||||
- [x] Cloudflare Pages 部署
|
|
||||||
|
|
||||||
待完成:
|
|
||||||
- [ ] OAuth secrets 設定(`GOOGLE_CLIENT_ID` / `GOOGLE_CLIENT_SECRET` / `GITHUB_CLIENT_ID` / `GITHUB_CLIENT_SECRET`)
|
|
||||||
- [ ] `/components` 頁面(零件列表 — 21 個 WASM 零件的 input/output/config_example)
|
|
||||||
- [ ] 首頁 code demo 更新為三種使用方式(CLI / Python / JS)
|
|
||||||
- [ ] 登入流程真實驗證
|
|
||||||
|
|
||||||
### 新增頁面:`/components`
|
|
||||||
|
|
||||||
```
|
|
||||||
/components
|
|
||||||
├── 零件卡片(21 個)
|
|
||||||
│ ├── canonical_id
|
|
||||||
│ ├── display_name
|
|
||||||
│ ├── description
|
|
||||||
│ ├── input_schema(required / optional 欄位)
|
|
||||||
│ ├── output_schema
|
|
||||||
│ ├── credentials_required(if any)
|
|
||||||
│ └── config_example(YAML code block)
|
|
||||||
└── 分類篩選(邏輯 / API / 控制流)
|
|
||||||
```
|
|
||||||
|
|
||||||
資料來源:靜態嵌入(從 `registry/components/*/component.contract.yaml` 在 build 時讀取),不依賴 runtime API。
|
|
||||||
|
|
||||||
### OAuth 設定(待 richblack 操作)
|
|
||||||
|
|
||||||
需要在 Cloudflare Worker 設定以下 secrets:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
wrangler secret put GOOGLE_CLIENT_ID --name arcrun-cypher-executor
|
|
||||||
wrangler secret put GOOGLE_CLIENT_SECRET --name arcrun-cypher-executor
|
|
||||||
wrangler secret put GITHUB_CLIENT_ID --name arcrun-cypher-executor
|
|
||||||
wrangler secret put GITHUB_CLIENT_SECRET --name arcrun-cypher-executor
|
|
||||||
wrangler secret put SESSION_SIGNING_SECRET --name arcrun-cypher-executor
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## server 端需要的修改
|
|
||||||
|
|
||||||
### cypher-executor 修改(最小化)
|
|
||||||
|
|
||||||
目前 `POST /credentials` 端點(`routes/credentials.ts`)接收 `{ name, encrypted, iv }` 後直接存 KV。
|
|
||||||
|
|
||||||
SDK 需要的改動:
|
|
||||||
|
|
||||||
1. **`GET /auth-recipes` 回應格式**:目前 list 端點回 `{ recipes: [...] }` 但 recipe 的 `service` 欄位是 key — SDK 已在 list_services() 正確處理 ✅
|
|
||||||
|
|
||||||
2. **`GET /auth-recipes/:service` 回應格式**:目前回 `{ success: true, recipe: {...} }` — SDK 需讀 `body.recipe` 而非 body 本身 ✅
|
|
||||||
|
|
||||||
3. **`POST /credentials` 不需改動** — SDK 自己做 AES-GCM 加密後送 `{ name, encrypted, iv }` ✅
|
|
||||||
|
|
||||||
4. **未來**:新增 `GET /credentials/:name/secret` 端點(解密返回 plaintext),讓跨 session 的 `bind()` 能工作。但此端點在 `u6u-core/credentials/src/actions/getCredentialSecret.ts` 已有實作 — 需要在 cypher-executor 整合或 Service Binding 到 u6u-credentials Worker。**封測後再做。**
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 不做的事(明確排除)
|
|
||||||
|
|
||||||
- ❌ 不在 SDK 裡做 workflow 解析或 YAML 處理 — 那是 CLI 的職責
|
|
||||||
- ❌ 不在 SDK 裡做 server-side 解密 — 解密只在 server 端
|
|
||||||
- ❌ 不建新的 credentials Worker — 用現有的
|
|
||||||
- ❌ 不建新的 KV namespace — 用現有的 CREDENTIALS_KV
|
|
||||||
- ❌ 不改 cypher-executor 的 credential-injector.ts — 那已經完成且測試通過
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 實作順序
|
|
||||||
|
|
||||||
```
|
|
||||||
Phase 1:Python SDK 重建 + 測試
|
|
||||||
1.1 重建 arcrun/python-sdk/(按本 SDD 的結構)
|
|
||||||
1.2 修正上次的 bug:recipe 回應 wrapper、inject key "header" vs "headers"、secret key mapping
|
|
||||||
1.3 對 cypher.arcrun.dev live 測試全部 API
|
|
||||||
1.4 本地安裝測試(pip install -e .)
|
|
||||||
|
|
||||||
Phase 2:JS SDK 重建 + 測試
|
|
||||||
2.1 重建 arcrun/js-sdk/(按本 SDD 的結構)
|
|
||||||
2.2 同步修正 Python SDK 發現的所有 recipe 格式問題
|
|
||||||
2.3 build(tsup)+ 本地測試
|
|
||||||
|
|
||||||
Phase 3:arcrun.dev 網站補完
|
|
||||||
3.1 新增 /components 頁面
|
|
||||||
3.2 更新首頁 code demo(三種使用方式)
|
|
||||||
3.3 OAuth secrets 設定(需 richblack 操作 GCP / GitHub)
|
|
||||||
3.4 登入流程驗證
|
|
||||||
|
|
||||||
Phase 4:GitHub README + 發布
|
|
||||||
4.1 更新 arcrun/README.md — 三種 Quick Start
|
|
||||||
4.2 pip publish(arcrun)
|
|
||||||
4.3 npm publish(TBD 套件名)
|
|
||||||
4.4 最終驗證:從零開始 pip install / npm install / 打 API
|
|
||||||
```
|
|
||||||
@@ -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 Worker(workflow 執行、credential 管理、auth recipe、webhook)
|
|
||||||
- `u6u-core/credentials`:credential Worker(AES-GCM 加解密)— arcrun/credentials 是其 cherry-pick
|
|
||||||
- `arcrun/cli`:CLI 工具(已發布 npm `arcrun@1.1.0`)
|
|
||||||
- `arcrun/landing`:Next.js 前端(已部署 Cloudflare Pages,有 hero/login/dashboard/integrations 骨架)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Glossary
|
|
||||||
|
|
||||||
- **SDK lib**:Python / JS 套件,wrapping `cypher.arcrun.dev` HTTP API,安裝後可在程式碼中直接使用
|
|
||||||
- **auth.setup()**:上傳一個服務的 credential(如 Notion token、OpenAI API Key)到 arcrun
|
|
||||||
- **auth.bind()**:取回已設定服務的 pre-authenticated HTTP client
|
|
||||||
- **auth.get_token()**:取回某服務的 raw token(escape hatch,給官方 SDK 用)
|
|
||||||
- **workflows.run()**:觸發已部署的 workflow
|
|
||||||
- **workflows.push()**:上傳 workflow 定義
|
|
||||||
- **Recipe**:描述「如何對某服務認證」的 YAML 設定,存在 RECIPES KV
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Requirements
|
|
||||||
|
|
||||||
### Requirement 1:Python SDK(`pip install arcrun`)
|
|
||||||
|
|
||||||
**User Story:** As a Python 開發者, I want `pip install arcrun` 後在程式碼中使用 arcrun, so that 不用離開寫程式環境就能串接 20+ 服務。
|
|
||||||
|
|
||||||
#### Acceptance Criteria
|
|
||||||
|
|
||||||
1. THE Python SDK SHALL 以 `arcrun` 套件名發布到 PyPI,支援 Python 3.10+。
|
|
||||||
2. THE SDK SHALL 提供以下 API:
|
|
||||||
- `Arcrun(api_key=, base_url=)` — 建構 client,api_key 支援從環境變數 `ARCRUN_API_KEY` 或 `~/.arcrun/config.yaml` 自動讀取
|
|
||||||
- `client.health()` — 健康檢查
|
|
||||||
- `client.auth.list_services()` — 列出可用 auth recipe 服務
|
|
||||||
- `client.auth.setup(service, **kwargs)` — 上傳 credential
|
|
||||||
- `client.auth.bind(service)` — 取得 pre-authenticated HTTP client
|
|
||||||
- `client.auth.get_token(service)` — 取得 raw token
|
|
||||||
- `client.creds.push(name, value)` — 上傳加密 credential
|
|
||||||
- `client.creds.list()` — 列出 credential 名稱
|
|
||||||
- `client.creds.delete(name)` — 刪除 credential
|
|
||||||
- `client.workflows.run(name, input)` — 觸發 workflow
|
|
||||||
- `client.workflows.push(name, graph)` — 上傳 workflow
|
|
||||||
- `client.workflows.list()` — 列出已部署 workflow
|
|
||||||
3. THE SDK 的 credential 加密 SHALL 在 client 端完成(使用 `cryptography` 套件 AES-GCM),然後以 `POST /credentials` 上傳加密後的 `{ name, encrypted, iv }` 到 server。
|
|
||||||
4. THE `auth.bind()` SHALL 從 server 取得 auth recipe 的 inject template,在 client 端用 cache 的 plaintext 值填入,回傳 pre-configured `httpx.Client`。
|
|
||||||
5. THE SDK SHALL 使用 `httpx` 做 HTTP client(async 版使用 `httpx.AsyncClient`)。
|
|
||||||
6. THE SDK 位置 SHALL 為 `arcrun/python-sdk/`,build 系統用 `hatchling`(`pyproject.toml`)。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Requirement 2:JavaScript/TypeScript SDK(`npm install arcrun`)
|
|
||||||
|
|
||||||
**User Story:** As a JS/TS 開發者, I want `npm install arcrun` 後在程式碼中使用 arcrun, so that 可以嵌入現有 Node.js / Deno / Cloudflare Workers 專案。
|
|
||||||
|
|
||||||
#### Acceptance Criteria
|
|
||||||
|
|
||||||
1. THE JS SDK SHALL 以 `arcrun` 套件名發布到 npm,提供 ESM + CJS 雙格式 + TypeScript 型別定義。
|
|
||||||
2. THE SDK SHALL 提供與 Python SDK 對等的 API(camelCase 版):
|
|
||||||
- `new Arcrun({ apiKey?, baseUrl? })` — 讀 `process.env.ARCRUN_API_KEY`
|
|
||||||
- `client.health()` — 回傳 `Promise<unknown>`
|
|
||||||
- `client.auth.listServices()` / `setup()` / `bind()` / `getToken()`
|
|
||||||
- `client.creds.push()` / `list()` / `delete()`
|
|
||||||
- `client.workflows.run()` / `push()` / `list()` / `delete()`
|
|
||||||
3. THE SDK 的 credential 加密 SHALL 使用 Web Crypto API(`crypto.subtle` AES-GCM),相容 Node 18+、browsers、Cloudflare Workers、Deno。
|
|
||||||
4. THE `auth.bind()` SHALL 回傳一個有 `get/post/put/delete/patch` 方法的 `AuthenticatedClient`,base URL + auth headers 已配置。
|
|
||||||
5. THE SDK SHALL 使用原生 `fetch()` API,不依賴外部 HTTP client 套件。
|
|
||||||
6. THE SDK 位置 SHALL 為 `arcrun/js-sdk/`,build 用 `tsup`(ESM + CJS + DTS),`tsconfig.json` target ES2020 + NodeNext module。
|
|
||||||
7. THE JS SDK 套件名與 CLI 套件名衝突(都叫 `arcrun`),SHALL 使用 `@arcrun/sdk` 或由 richblack 決定套件名。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Requirement 3:arcrun.dev 網站完成
|
|
||||||
|
|
||||||
**User Story:** As a 潛在用戶, I want 在 arcrun.dev 上登入取得 API Key、瀏覽零件和 recipe 列表, so that 我可以評估 arcrun 是否符合需求並立即開始使用。
|
|
||||||
|
|
||||||
#### Acceptance Criteria
|
|
||||||
|
|
||||||
1. THE 網站 SHALL 在 `arcrun.dev` 提供以下頁面:
|
|
||||||
- `/` — 首頁 Hero + 三種使用方式(CLI / Python / JS)
|
|
||||||
- `/login` — Google + GitHub OAuth 登入
|
|
||||||
- `/dashboard` — 登入後顯示 API Key(查看/Copy/Rotate/Revoke)
|
|
||||||
- `/integrations` — 列出 20 個 auth recipe 服務,可按分類篩選
|
|
||||||
- `/components` — 列出所有零件(21 個 WASM 零件),顯示 input/output schema、config_example
|
|
||||||
- `/api-docs` — Swagger UI,可直接試打 API
|
|
||||||
2. THE 登入 SHALL 使用 Google + GitHub OAuth,流程走 `cypher.arcrun.dev` 的 `/auth/*` 端點。
|
|
||||||
3. THE 登入後 SHALL 自動對該 email 呼叫 `/register` 取得 API Key(若已有則取回現有 key)。
|
|
||||||
4. THE `/dashboard` SHALL 允許 Rotate(生成新 key)、Revoke(標記失效)、Copy to clipboard。
|
|
||||||
5. THE 網站 SHALL 部署在 Cloudflare Pages(現有 `arcrun/landing`),使用 Next.js App Router。
|
|
||||||
6. THE 首頁 code demo 區 SHALL 包含三個 tab:Python、JavaScript、HTTP/curl,展示三種使用方式。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Requirement 4:GitHub README 更新
|
|
||||||
|
|
||||||
**User Story:** As a GitHub 訪客, I want README 清楚說明三種使用方式, so that 我能選擇最適合的方式開始用 arcrun。
|
|
||||||
|
|
||||||
#### Acceptance Criteria
|
|
||||||
|
|
||||||
1. THE `arcrun/README.md` SHALL 包含三種 Quick Start:
|
|
||||||
- **CLI**:`npm i -g arcrun && acr init && acr push workflow.yaml && acr run`
|
|
||||||
- **Python**:`pip install arcrun && from arcrun import Arcrun && ...`
|
|
||||||
- **JavaScript**:`npm install arcrun && import { Arcrun } from 'arcrun' && ...`
|
|
||||||
2. THE README SHALL 包含完整零件列表(21 個)和 auth recipe 列表(20 個服務)。
|
|
||||||
3. THE README SHALL 連結到 `arcrun.dev`(取得 API Key)和 Swagger UI(API 文件)。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Requirement 5:SDK 發布
|
|
||||||
|
|
||||||
**User Story:** As a SDK 使用者, I want 公開安裝並直接使用, so that 不需要從原始碼 build。
|
|
||||||
|
|
||||||
#### Acceptance Criteria
|
|
||||||
|
|
||||||
1. THE Python SDK SHALL 發布到 PyPI,`pip install arcrun` 可安裝。
|
|
||||||
2. THE JS SDK SHALL 發布到 npm,`npm install arcrun`(或 `@arcrun/sdk`)可安裝。
|
|
||||||
3. THE 發布前 SHALL 完成以下測試(對 `cypher.arcrun.dev` live API):
|
|
||||||
- `health()` ✅
|
|
||||||
- `auth.list_services()` ✅
|
|
||||||
- `auth.setup()` + `auth.bind()` ✅(至少一個 static_key 服務如 openai)
|
|
||||||
- `creds.push()` + `creds.list()` ✅
|
|
||||||
- `workflows.list()` ✅
|
|
||||||
@@ -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 CLI;self-host 本來就有上傳能力。CLI 不自己重寫 CF Script Upload API。 |
|
|
||||||
| 源碼來源 | **GitHub release tarball**(含預編譯 wasm)| 版本明確、不需用戶有 git、`acr update` 拉新 release 同一條路。 |
|
|
||||||
| 為何不是 git clone | repo **沒 commit `.wasm`**(rule 05 build 產物不 commit)→ clone 拿不到 wasm | 必須走含 wasm 的 release artifact。 |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 2. 為什麼是 installer 而非「repo 內掃 wrangler.toml」(推翻初稿)
|
|
||||||
|
|
||||||
初稿假設「用戶在 repo 內跑、CLI 掃 wrangler.toml」。**推翻**,因為:
|
|
||||||
- npm 全域裝的 `acr` 手上**沒有** 24 個 Worker 源碼。
|
|
||||||
- repo 沒 commit `.wasm`(已查證 `git ls-files .component-builds | grep .wasm` = 0)→ 連 clone 都拿不到可部署的 wasm。
|
|
||||||
- 用戶不該需要懂 git / tinygo / repo 結構。
|
|
||||||
|
|
||||||
→ 正解:CLI 當 installer,從 **GitHub release(含預編譯 wasm)** 拉部署物到暫存目錄,在暫存目錄注入 KV id 後 `wrangler deploy`。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 3. 流程設計(`initSelfHosted` 改寫)
|
|
||||||
|
|
||||||
```
|
|
||||||
acr init --self-hosted
|
|
||||||
│
|
|
||||||
├─ 1. 問 2 輸入:CF Account ID + CF API Token
|
|
||||||
│ (wrangler 是否已裝?which wrangler;沒裝 → 提示先裝 CF CLI 再來)
|
|
||||||
│ 驗 token:CF API GET /accounts/{id}/tokens/verify + GET /accounts/{id}
|
|
||||||
│ 缺權限(Workers Scripts Edit / KV Edit / R2 Edit)→ exit 1 指出缺哪個 scope
|
|
||||||
│
|
|
||||||
├─ 2. 建資源(冪等:先 list 已存在就重用)
|
|
||||||
│ 7 KV:WEBHOOKS / CREDENTIALS_KV / RECIPES / USERS_KV /
|
|
||||||
│ SESSIONS_KV / ANALYTICS_KV / EXEC_CONTEXT(rule 01 資料儲存表)
|
|
||||||
│ 1 R2:WASM_BUCKET
|
|
||||||
│
|
|
||||||
├─ 3. 下載部署物:GitHub release tarball → 解壓到暫存目錄 (~/.arcrun/.deploy-<ver>/)
|
|
||||||
│ 內含:cypher-executor/ + registry/ + .component-builds/*(每個含預編譯 component.wasm + wrangler.toml)
|
|
||||||
│
|
|
||||||
├─ 4. 注入設定到暫存目錄的 wrangler.toml(不改用戶 repo,改暫存副本)
|
|
||||||
│ - 各 Worker 的 KV binding id ← step 2 建立的
|
|
||||||
│ - cypher-executor [vars] WORKER_SUBDOMAIN ← CF API GET /accounts/{id}/workers/subdomain
|
|
||||||
│
|
|
||||||
├─ 5. 部署:對暫存目錄每個含 wrangler.toml 的 dir,shell out
|
|
||||||
│ `wrangler deploy`(env CLOUDFLARE_API_TOKEN=<token>, CLOUDFLARE_ACCOUNT_ID=<id>)
|
|
||||||
│ 分兩層:tier1 = .component-builds/*(先)→ tier2 = cypher-executor / registry(後)
|
|
||||||
│ 每個 wrangler.toml 已含 workers_dev = true → workers.dev URL 自動啟用
|
|
||||||
│
|
|
||||||
├─ 6. seed recipe 進 RECIPES KV(部署後打新 cypher URL,或直接 CF KV API 寫)
|
|
||||||
│ - auth recipe:重用 AUTH_RECIPE_SEEDS(cypher-executor/src/lib/auth-recipe-seeds.ts)
|
|
||||||
│ - API recipe:新增 seed-api-recipes.ts(見 §5)
|
|
||||||
│
|
|
||||||
├─ 7. 寫回 config(mode: self-hosted + 所有 id + cypher_executor_url = 部署後 workers.dev URL)
|
|
||||||
│
|
|
||||||
└─ 8. 印手動 secret 提示:
|
|
||||||
wrangler secret put ENCRYPTION_KEY --name arcrun-cypher-executor
|
|
||||||
wrangler secret put ENCRYPTION_KEY --name arcrun-auth-static-key
|
|
||||||
wrangler secret put ENCRYPTION_KEY --name arcrun-auth-service-account
|
|
||||||
(三 Worker 共用同一把 key,見 memory: encryption-key-drift-trap)
|
|
||||||
```
|
|
||||||
|
|
||||||
### `acr update`(同一條路,未來新零件)
|
|
||||||
- 拉新 GitHub release → 解壓 → 注入既有 config 的 KV id → wrangler deploy 變動的 Worker。
|
|
||||||
- 第一期至少做到「重跑等效 init 的部署步驟」;diff-only 部署可後續優化。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 4. 動到的檔案
|
|
||||||
|
|
||||||
| 檔案 | 動作 |
|
|
||||||
|---|---|
|
|
||||||
| `cli/src/commands/init.ts` | 改寫 `initSelfHosted()`(line 105-131)為 installer 流程 |
|
|
||||||
| `cli/src/lib/cf-api.ts` | 擴充:KV namespace 建立/list、R2 bucket 建立、subdomain 查詢、token verify |
|
|
||||||
| 新增 `cli/src/lib/deploy.ts`(暫定)| 下載 release tarball + 解壓 + 注入 wrangler.toml + shell out wrangler deploy |
|
|
||||||
| 新增 `cli/src/commands/update.ts`(暫定)| `acr update`:拉新 release 重部署 |
|
|
||||||
| 新增 `cli/src/lib/api-recipe-seeds.ts` | API recipe **種子資料**(installer 用;放 CLI 端,**不放 cypher-executor/src**——rule 02 §2.2 hook 擋 cypher-executor TS hard-code endpoint,且 seed 資料本就屬 installer 職責)|
|
|
||||||
| 新增 `cypher-executor/scripts/seed-api-recipes.ts` | seed **腳本**(給 prod 補灌用,import CLI 的種子資料;`scripts/` 不受 §2.2 hook 管)|
|
|
||||||
| `cli/src/index.ts` | 註冊 `acr update` 指令 |
|
|
||||||
|
|
||||||
**不動**:cypher-executor 執行路徑、既有零件 wasm 源碼、config 讀取端(config.ts:52 已支援 self-hosted)。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 5. API recipe seed(新增 seed-api-recipes.ts,richblack 2026-06-02 定)
|
|
||||||
|
|
||||||
codebase 只有 auth recipe seed。新增 `seed-api-recipes.ts`,把現役 API recipe hard-code 成種子。
|
|
||||||
|
|
||||||
### 現役 API recipe(從 prod KV 查得,2026-06-01)
|
|
||||||
- `kbdb_get`(+ create_block / patch_block / delete / ingest)→ auth_service: kbdb
|
|
||||||
- `gmail_send` → google_gmail_sa
|
|
||||||
- `google_sheets_append` / `google_sheets_read` → google_sheets_sa
|
|
||||||
- `telegram_send` → telegram
|
|
||||||
- `line_notify_send` → line_notify
|
|
||||||
|
|
||||||
### KBDB recipe 採 Supabase 模式(richblack 2026-06-02)
|
|
||||||
- **KBDB 是 richblack 提供的服務**(跟 arcrun 一樣),採「基礎免費、大量收費」。
|
|
||||||
- KBDB recipe **進 seed**(展示能力 = 引子,Supabase 模式)。使用者要用 → 去 **arcrun 取統一 API Key**(已有 /register 入口),把 key 設成 credential。
|
|
||||||
- ⚠️ **FOLLOW-UP(交 KBDB 端)**:現役 endpoint 是 `kbdb.finally.click{{_path}}`。richblack:這是 KBDB 端要改的問題——KBDB 該用統一對外網址提供大家用,不是 finally.click。**seed 先照現況進;KBDB 端改網址後同步更新 seed。** 此事不擋 init 實作。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 6. 部署物產製:commit wasm 進 repo + codeload tarball(richblack 2026-06-02 定案)
|
|
||||||
|
|
||||||
> 此節**取代初稿的「GitHub release artifact」構想**。richblack 拍板更輕的做法:
|
|
||||||
> 直接把預編譯 wasm commit 進 repo,CLI 從 GitHub codeload tarball 拿。不需 release.yml 機制。
|
|
||||||
|
|
||||||
### 6.1 策略
|
|
||||||
|
|
||||||
- **repo 自帶可部署的 wasm**:刪 `.gitignore` 的 `*.wasm` 排除,commit 預編譯 wasm 進 repo。
|
|
||||||
→ repo 本身就是部署來源,CLI 直接拿、用戶用自己的 CF token deploy。
|
|
||||||
- **CLI 走 codeload tarball**:`https://codeload.github.com/richblack/arcrun/tar.gz/{ref}`
|
|
||||||
(ref = main 或 tag)。用戶不需 git、版本可控(tag)。`acr update` 拉新 ref。
|
|
||||||
- **理由**(richblack):「我在我的 CF 能用 = 我已擁有 wasm;用戶指向我的 GitHub 取得 wasm,
|
|
||||||
用他自己的 CF credential deploy。開源,看不看源碼不重要,體驗好最重要。」
|
|
||||||
|
|
||||||
### 6.2 ⚠️ 推翻既有鐵律(rule 05)— 需同步改規則
|
|
||||||
|
|
||||||
`.claude/rules/05-deploy-convention.md` 明文「`.component-builds/{name}/component.wasm` **不 commit 進 repo**
|
|
||||||
(build 產物)」「Phase 1-3 暫時 commit 過,**之後會加 .gitignore 清理**」。
|
|
||||||
**本決策反向**:commit wasm 進 repo(self-host 需 repo 自帶可部署 wasm)。
|
|
||||||
→ **實作時必須同步改 rule 05 + .gitignore**,否則 pre-write hook / 規則與實作打架。
|
|
||||||
→ deploy.yml 的 CI rebuild 步驟仍保留(CI 部署 prod 時用最新 source rebuild,與 commit 的 wasm 不衝突;
|
|
||||||
commit 的 wasm 是給「self-host 用戶 + acr init」用的部署來源)。
|
|
||||||
|
|
||||||
### 6.3 只 commit 部署所需的 wasm(省空間)
|
|
||||||
|
|
||||||
- 實況(2026-06-02 查):`registry/components/*.wasm` 23 個(build 中間產物)+
|
|
||||||
`.component-builds/*/component.wasm` 22 個(部署物),共 **~50MB**。
|
|
||||||
- **部署只需 `.component-builds/*/component.wasm`**(wrangler deploy 認這個)。
|
|
||||||
→ **只 commit `.component-builds/*/component.wasm`(22 個),不必 commit registry 那 23 個**(省一半)。
|
|
||||||
`.gitignore` 改成:保留排除 `registry/components/**/*.wasm`(中間產物),只放行 `.component-builds/**/component.wasm`。
|
|
||||||
- ⚠️ **誠實 trade-off**(mindset §7):commit wasm 進 repo → 每次 wasm rebuild 都在 git 歷史累積二進位,
|
|
||||||
**repo 長期會膨脹**。可接受(self-host 體驗優先),但記錄此代價;未來若膨脹過劇,再考慮 release artifact / git-lfs。
|
|
||||||
|
|
||||||
### 6.3.1 「錯做成零件」的 3 個不 commit(richblack 2026-06-02)
|
|
||||||
|
|
||||||
實際 commit 的是 **19 個正當零件**,不是 22。排除的 3 個:`claude_api` / `km_writer` / `kbdb_upsert_block`。
|
|
||||||
- **原因(richblack 修正「待刪」說法)**:它們**不是 endpoint 薄殼,是把工作流硬塞進零件**(違反 DECISIONS §1)。
|
|
||||||
例:`kbdb_upsert_block` 的 upsert 邏輯應在 KBDB API 那邊(API 提供 upsert endpoint),零件只該驅動它;
|
|
||||||
現在卻把「GET 找→有則 PATCH 無則 POST」整段工作流塞進零件。本質是工作流/recipe,被錯做成零件。
|
|
||||||
- **為何「現在就不 commit」而非「先 commit 之後刪」**:commit 二進位進 git 歷史後,即使日後 `git rm`,
|
|
||||||
歷史裡仍永久殘留(repo 體積已被佔),除非 rewrite history(很麻煩)。**錯誤的東西不灌進永久歷史。**
|
|
||||||
- **落地**:`.gitignore` 放行 22 個後**再排除這 3 個**(後出現規則勝出);`deploy.ts discoverWorkerDirs`
|
|
||||||
只部署「同時有 wrangler.toml + component.wasm」的目錄 → self-host 用戶 codeload 拿到的目錄缺這 3 個 wasm → 自然跳過。
|
|
||||||
- **後續**:這 3 個的降級(變回工作流/recipe)是 BACKLOG 既有待辦,本次不處理,但確保它們不進 self-host 部署來源。
|
|
||||||
|
|
||||||
### 6.4 CLI deploy 流程(deploy.ts downloadAndDeploy 補實作)
|
|
||||||
|
|
||||||
```
|
|
||||||
1. 下載 codeload tarball(ref 預設 main,acr update 可帶 tag)→ 解壓 ~/.arcrun/.deploy-<ref>/
|
|
||||||
2. 讀解壓出的 .component-builds/* + cypher-executor/ + registry/
|
|
||||||
3. 各 wrangler.toml 注入 ctx.kvNamespaceIds + cypher-executor WORKER_SUBDOMAIN
|
|
||||||
4. tier1=.component-builds/*(先)→ tier2=cypher-executor/registry(後)
|
|
||||||
每個 dir:pnpm install(若有 lock)→ CLOUDFLARE_API_TOKEN=<用戶> wrangler deploy
|
|
||||||
5. 回 cypherExecutorUrl = https://arcrun-cypher-executor.<subdomain>.workers.dev
|
|
||||||
```
|
|
||||||
|
|
||||||
注意:tier2(cypher-executor/registry)是 TS,wrangler deploy 會在用戶端用內建 esbuild bundle
|
|
||||||
(不需額外工具,richblack 確認源碼可見不重要、體驗優先 → artifact 含 TS 源碼即可)。
|
|
||||||
|
|
||||||
### 6.5 實作順序
|
|
||||||
|
|
||||||
1. 改 `.gitignore`(放行 `.component-builds/**/component.wasm`)+ commit 22 個 wasm。
|
|
||||||
2. 同步改 rule 05(記錄此決策推翻原慣例)。
|
|
||||||
3. 補實 `deploy.ts downloadAndDeploy`(codeload 下載 + 注入 + wrangler deploy)。
|
|
||||||
4. **在 1-2 完成前,downloadAndDeploy 維持誠實 unimplemented,不假裝(mindset §7)。**
|
|
||||||
|
|
||||||
### 6.6 未來方向:零件按需安裝(richblack 2026-06-02,現在不做)
|
|
||||||
|
|
||||||
- 現在 `acr init --self-hosted` **全裝基礎零件**(22 個一次部署)。簡單、夠用。
|
|
||||||
- **未來若零件數量真的變很多**,再思考「按需安裝」(只裝 workflow 實際用到的零件 / 用戶選裝)。
|
|
||||||
- **現在不做的理由**(DECISIONS 附錄「會不會累積成債」):零件目前少且未來絕大多數是 recipe
|
|
||||||
(不需 deploy)→ 為「零件爆量」做按需安裝基建 = 為不存在的規模做自動化 = 過度工程。
|
|
||||||
零件真的爆量再回頭做,屆時是「未來一次性處理的設計點」,現在不必焦慮。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 7. 驗收標準(客觀證據,mindset §7)
|
|
||||||
|
|
||||||
1. richblack 用**全新 CF 帳號** + wrangler 已裝 + 一個 CF API Token 跑 `acr init --self-hosted`
|
|
||||||
→ 全程無手動建 KV / 無手動 clone / 無 tinygo / 無手動填 namespace id。
|
|
||||||
2. 跑完印 secret 提示,richblack 手動 `wrangler secret put ENCRYPTION_KEY` ×3。
|
|
||||||
3. `acr push` 一個含 http_request + 自建 recipe 的 workflow → trigger → **HTTP 2xx + execution trace**。
|
|
||||||
4. 冪等:重跑 init 不重建已存在 KV / 不報錯。
|
|
||||||
5. `acr update` 拉新 codeload tarball(tag)→ 重部署成功。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 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 1:Python SDK
|
|
||||||
|
|
||||||
- [ ] 1. 建立 `arcrun/python-sdk/` 目錄
|
|
||||||
- [ ] 1.1 `pyproject.toml`:name=arcrun, deps=[httpx>=0.27, cryptography>=42], build-system=hatchling
|
|
||||||
- [ ] 1.2 `arcrun/__init__.py`:`from .client import Arcrun`
|
|
||||||
- [ ] 1.3 `arcrun/crypto.py`:AES-GCM encrypt only(使用 `cryptography` 套件)
|
|
||||||
- [ ] 1.4 `arcrun/creds.py`:CredentialsClient — push(加密 + POST /credentials)、list(GET /credentials)、delete
|
|
||||||
- [ ] 1.5 `arcrun/auth.py`:AuthClient — setup(fetch recipe → match secrets → encrypt → push)、bind(fetch recipe → resolve headers from cache → return AuthenticatedClient)、get_token、list_services
|
|
||||||
- [ ] 1.6 `arcrun/workflows.py`:WorkflowClient — run(POST /webhooks/named/{name}/trigger)、push(POST /webhooks/named)、list(GET /webhooks/named)、delete
|
|
||||||
- [ ] 1.7 `arcrun/client.py`:Arcrun class — 讀 api_key / encryption_key 從 param > env > config.yaml
|
|
||||||
|
|
||||||
- [ ] 2. 修正上次已知的 bug
|
|
||||||
- [ ] 2.1 `_fetch_recipe()` 回應是 `{ success: true, recipe: {...} }`,需讀 `.recipe` 欄位
|
|
||||||
- [ ] 2.2 `inject` 下的 key 是 `header`(singular),不是 `headers`
|
|
||||||
- [ ] 2.3 `required_secrets[].key` 是 prefixed(如 `openai_api_key`),setup() 的 kwargs alias 要能對應
|
|
||||||
- [ ] 2.4 `list_services()` 回應的 recipe 用 `service` 欄位(不是 `service_id`)
|
|
||||||
|
|
||||||
- [ ] 3. 測試(對 cypher.arcrun.dev live API)
|
|
||||||
- [ ] 3.1 `health()` → `{"ok": true}`
|
|
||||||
- [ ] 3.2 `auth.list_services()` → 20 個服務
|
|
||||||
- [ ] 3.3 `auth.setup("openai", api_key="sk-test-dummy")` → 成功
|
|
||||||
- [ ] 3.4 `auth.bind("openai")` → AuthenticatedClient with Authorization header
|
|
||||||
- [ ] 3.5 `auth.get_token("openai")` → "sk-test-dummy"
|
|
||||||
- [ ] 3.6 `creds.push("test_token", "value123")` → 成功
|
|
||||||
- [ ] 3.7 `creds.list()` → 含 "test_token"(注意 KV eventual consistency)
|
|
||||||
- [ ] 3.8 `workflows.list()` → []
|
|
||||||
- [ ] 3.9 cleanup: `creds.delete("test_token")`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Phase 2:JS/TS SDK
|
|
||||||
|
|
||||||
- [ ] 4. 建立 `arcrun/js-sdk/` 目錄
|
|
||||||
- [ ] 4.1 `package.json`:name TBD(arcrun vs @arcrun/sdk),deps=devDeps only(tsup, typescript, @types/node)
|
|
||||||
- [ ] 4.2 `tsconfig.json`:ES2020, NodeNext
|
|
||||||
- [ ] 4.3 `src/crypto.ts`:Web Crypto API AES-GCM encrypt only
|
|
||||||
- [ ] 4.4 `src/creds.ts`:CredentialsClient — push/list/delete via fetch
|
|
||||||
- [ ] 4.5 `src/auth.ts`:AuthClient — setup/bind/getToken/listServices
|
|
||||||
- [ ] 4.6 `src/workflows.ts`:WorkflowClient — run/push/list/delete
|
|
||||||
- [ ] 4.7 `src/index.ts`:export class Arcrun + re-exports
|
|
||||||
|
|
||||||
- [ ] 5. 同步修正(與 Python SDK 同樣的 recipe 格式問題)
|
|
||||||
- [ ] 5.1 `_fetchRecipe()` 讀 `body.recipe`
|
|
||||||
- [ ] 5.2 inject key: `header` not `headers`
|
|
||||||
- [ ] 5.3 setup() secret key alias matching
|
|
||||||
- [ ] 5.4 listServices() 用 `service` 欄位
|
|
||||||
|
|
||||||
- [ ] 6. Build + 測試
|
|
||||||
- [ ] 6.1 `tsup` build → dist/index.js + dist/index.cjs + dist/index.d.ts
|
|
||||||
- [ ] 6.2 Node.js 腳本對 live API 測試(同 Python 測試項目)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Phase 3:arcrun.dev 網站
|
|
||||||
|
|
||||||
- [ ] 7. 新增 `/components` 頁面
|
|
||||||
- [ ] 7.1 從 `registry/components/*/component.contract.yaml` 讀取 21 個零件資料
|
|
||||||
- [ ] 7.2 卡片顯示:canonical_id, display_name, description, input required/optional, credentials_required, config_example
|
|
||||||
- [ ] 7.3 分類篩選:邏輯類 / API 類 / 控制流類
|
|
||||||
|
|
||||||
- [ ] 8. 更新首頁
|
|
||||||
- [ ] 8.1 Code demo tabs 改為 CLI / Python / JS 三個
|
|
||||||
- [ ] 8.2 CLI tab 展示 `acr init → acr push → acr run`
|
|
||||||
- [ ] 8.3 Python tab 展示 `pip install arcrun → Arcrun() → auth.setup → auth.bind`
|
|
||||||
- [ ] 8.4 JS tab 展示 `npm install arcrun → new Arcrun() → auth.setup → auth.bind`
|
|
||||||
|
|
||||||
- [ ] 9. OAuth 流程補完
|
|
||||||
- [ ] 9.1 確認 cypher-executor 的 `/auth/google/start`、`/auth/github/start`、`/auth/callback` 路由正確
|
|
||||||
- [ ] 9.2 提供 richblack OAuth secrets 設定指令清單
|
|
||||||
- [ ] 9.3 richblack 設定 secrets 後驗證登入流程
|
|
||||||
|
|
||||||
- [ ] 10. 部署
|
|
||||||
- [ ] 10.1 Cloudflare Pages build + deploy
|
|
||||||
- [ ] 10.2 驗證所有頁面可存取
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Phase 4:README + 發布
|
|
||||||
|
|
||||||
- [ ] 11. 更新 `arcrun/README.md`
|
|
||||||
- [ ] 11.1 三種 Quick Start(CLI / Python / JS)
|
|
||||||
- [ ] 11.2 零件列表(21 個)
|
|
||||||
- [ ] 11.3 Auth Recipe 列表(20 個服務)
|
|
||||||
- [ ] 11.4 連結到 arcrun.dev 和 Swagger UI
|
|
||||||
|
|
||||||
- [ ] 12. 發布
|
|
||||||
- [ ] 12.1 Python SDK:`pip install build && python -m build && twine upload dist/*`
|
|
||||||
- [ ] 12.2 JS SDK:`npm run build && npm publish`
|
|
||||||
- [ ] 12.3 驗證:從零開始 `pip install arcrun` / `npm install arcrun` + hello world
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Phase 5:acr init --self-hosted installer(2026-06-02 新增)
|
|
||||||
|
|
||||||
> 定稿 design:`self-hosted-init.md`。CLI = installer:建 KV/R2 + 拉預編譯 wasm + wrangler deploy + seed。
|
|
||||||
> 用戶只做:申請 CF 帳號 → 裝 wrangler → 裝 acr → acr init --self-hosted。其餘自動。
|
|
||||||
> 背景:戰法轉 self-hosted 開源(docs/HANDOFF-self-host-harness.md)。
|
|
||||||
|
|
||||||
- [x] 13.1 API recipe 種子 — **位置修正**:種子資料放 `cli/src/lib/api-recipe-seeds.ts`(installer 用,避開 cypher §2.2 hook),seed 腳本 `cypher-executor/scripts/seed-api-recipes.ts`(import 種子,給 prod 補灌)。10 個現役 recipe(kbdb_*/gmail_send/google_sheets_*/telegram_send/line_notify_send)。KBDB Supabase 模式進 seed(finally.click 是 KBDB 端 follow-up,已註於 api-recipe-seeds.ts)
|
|
||||||
- [x] 13.2 `cli/src/lib/cf-api.ts` 新增 `CfAccountClient`:verifyAccess / listKvNamespaces / ensureKvNamespace(冪等)/ ensureR2Bucket(冪等)/ getWorkersSubdomain
|
|
||||||
- [x] 13.3 `cli/src/commands/init.ts` `initSelfHosted()` 改寫:驗 token → 建 7 KV + R2 → 查 subdomain → downloadAndDeploy → 寫 config → seed(部署完成時)→ 印 secret 提示。誠實:部署未自動化時明說,不假綠
|
|
||||||
- [x] 13.4 `cli/src/lib/deploy.ts`:REQUIRED_KV/R2/SECRET 常數 + wranglerAvailable() + **downloadAndDeploy 已補實**(codeload tarball 下載 + 解壓 + discoverWorkerDirs 分 tier + injectWranglerConfig 注入 KV id/subdomain + runWranglerDeploy;部分失敗誠實收集回報,不假綠)
|
|
||||||
- [x] 13.5 `cli/src/commands/update.ts` + index.ts 註冊 `acr update`(self-hosted 重部署,同走 downloadAndDeploy)
|
|
||||||
- [x] 13.6 部署物產製:**改用 commit wasm 進 repo + codeload**(取代 release artifact,richblack 2026-06-02,§6)
|
|
||||||
- `.gitignore` 否定規則放行 `.component-builds/**/component.wasm`(registry 中間產物仍排除)→ 已驗 git check-ignore
|
|
||||||
- rule 05 同步改(記錄推翻「wasm 不 commit」+ trade-off)
|
|
||||||
- commit 22 個 `.component-builds/*/component.wasm` 進 repo
|
|
||||||
- [ ] 13.7 驗收:全新 CF 帳號跑 acr init --self-hosted 全自動;acr push workflow → trigger 2xx + trace(**待 richblack 用第二帳號實測** + push 含 wasm 的 commit 到 GitHub 後 codeload 才拿得到)
|
|
||||||
- [x] 13.8 typecheck:cli `tsc --noEmit` exit 0
|
|
||||||
|
|
||||||
## 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 必須有人 merge(richblack approve);AI 偽造不了 GitHub approve |
|
|
||||||
| 舉證「為何不是工作流」 | PR description,review 時看 |
|
|
||||||
| G1 假零件 / G3 純WASI / G4 Gherkin / 覆蓋檢查 / 黃金向量 | **CI(PR check)跑** —— CI 有 tinygo + 能 runtime 跑 wasm,**繞開 CF Workers 不能 runtime 編譯 wasm 的 venue 牆** |
|
|
||||||
|
|
||||||
**§8 衝突釐清:** DECISIONS §8「不依賴 GitHub Actions」指**執行鏈路**(init/push/run/recipe,常態高頻,
|
|
||||||
用戶機器+CF)。**零件投稿是稀有低頻、該由 PR 治理**,用 PR/CI 不違反 §8——反而更對(CI 能跑 wasm,
|
|
||||||
registry Worker 不能)。需在 DECISIONS 補這個區別(待 richblack 確認改穩定文件)。
|
|
||||||
|
|
||||||
**哪些作廢 / 哪些保留:**
|
|
||||||
- ❌ 作廢:registry submit API 當主投稿管道、四路(CLI/MCP/py/js)self-service 投稿、平台端 sandbox 重跑、`acr parts publish` 加人類閘門(投稿不走 CLI 了)。
|
|
||||||
- ✅ 保留並搬 CI:G1 假零件偵測邏輯(detectFakeComponent.ts)、G3 純WASI(wasmImports.ts)、G4 Gherkin 真跑(CI 能跑 wasm)、B 覆蓋檢查、黃金向量人工核對。
|
|
||||||
- ✅ 已 commit 的 registry G0/G1/G3 程式碼**保留不刪**(無害,且 G1/G3 邏輯被 CI 複用),但 registry submit 不再是主管道。
|
|
||||||
- ✅ R5 本機 hook(擋 CC 直接造零件目錄)仍要 —— 它擋的是「繞過 PR 直接改 repo」,與 PR 管道互補。
|
|
||||||
|
|
||||||
**以下 §0-§9 為原 design,閱讀時套用上述修正。**
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 0. 架構總覽
|
|
||||||
|
|
||||||
### 0.0 範圍:把關跨公共庫 + self-hosted 私人庫(richblack 2026-05-29)
|
|
||||||
|
|
||||||
把關**不是公共庫專屬**。每個 self-hosted 部署有自己的零件庫(自己的 registry Worker)。
|
|
||||||
**加入公共庫或任何 self-hosted 私人庫,都跑同一套把關鏈(G0-G6 + 本機 hook)。**
|
|
||||||
- 實作上天然成立:把關邏輯在 registry Worker code 裡,self-hosted 跑同一份 registry → 把關跟著走,不需公私庫分兩套。
|
|
||||||
- G0 人類閘門在 self-hosted 下,「人類」= 部署擁有者本人(防的是「他的 AI 自作主張把東西做成零件」,不是防他本人;他自己確認 + 舉證即可過)。
|
|
||||||
- 理由:self-hosted 一樣有「自用服務沒驗證就變零件」的風險,且私人庫零件之後可能貢獻回公共。
|
|
||||||
|
|
||||||
### 0.1 把關鏈
|
|
||||||
|
|
||||||
投稿零件的唯一入口是 registry Worker 的 submit。把關鏈(依序,任一失敗即退稿):
|
|
||||||
|
|
||||||
```
|
|
||||||
submit 請求(帶 wasm + contract + 人類確認憑證 + 舉證)
|
|
||||||
│
|
|
||||||
├─ G0 人類閘門(R4) ← 最先擋:沒人類確認 + 舉證 → 403
|
|
||||||
├─ G1 假零件偵測(R2) ← contract/原碼有外部 URL 或 http 子集 → 退稿指回正路
|
|
||||||
├─ G2 size_check(已有)
|
|
||||||
├─ G3 syscall_scan + 純WASI(R3)← 擴充:只准 WASI preview1 + u6u host func 白名單
|
|
||||||
├─ G4 gherkin_tests(R1) ← 真跑 WASM,given→stdin→比對 then_contains
|
|
||||||
├─ G5 cold_start(mock,標未實作)
|
|
||||||
└─ G6 runtime_compat(mock,標未實作)
|
|
||||||
→ 全過 → 派 hash → 寫 KV
|
|
||||||
```
|
|
||||||
|
|
||||||
另一道獨立防線:**本機 hook**(R5),擋 CC 繞過 API 直接在 repo 造零件目錄。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 1. G0 人類閘門(R4)— registry submit endpoint
|
|
||||||
|
|
||||||
### 1.1 請求格式增欄
|
|
||||||
submit 請求 body 增兩個欄位:
|
|
||||||
```ts
|
|
||||||
interface SubmitRequest {
|
|
||||||
wasm_base64: string;
|
|
||||||
contract: ComponentContract;
|
|
||||||
human_confirmation?: {
|
|
||||||
confirmed_by_human: true; // 必須為 literal true
|
|
||||||
reason_why_not_workflow: string; // 非空,AI 舉證
|
|
||||||
confirmed_at: string; // ISO timestamp
|
|
||||||
};
|
|
||||||
skip_acceptance?: boolean; // 既有:backfill 用(仍保留)
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 1.2 閘門邏輯
|
|
||||||
```
|
|
||||||
若 skip_acceptance(backfill 既有零件)→ 跳過 G0(這些是已驗、已部署的存量,不是新投稿)
|
|
||||||
否則(新投稿):
|
|
||||||
若無 human_confirmation 或 reason_why_not_workflow 空 → 403:
|
|
||||||
"建零件需人類確認。請用 `acr parts publish`(會互動式問你),
|
|
||||||
並說明為何工作流做不到。預設假設工作流能做——先試工作流 / recipe。"
|
|
||||||
記錄 reason_why_not_workflow 進 KV metadata(軌跡可審)
|
|
||||||
```
|
|
||||||
|
|
||||||
### 1.3 四路收斂(CLI / MCP / Python / JS)
|
|
||||||
- 它們建零件都呼叫 registry submit endpoint → G0 在 endpoint,自動四路通管。
|
|
||||||
- **CLI `acr parts publish`**:強制互動式 prompt 問人類「(1) 工作流為何做不到?(2) 確認要建零件?」,把答案組成 `human_confirmation` 送出。非互動環境(AI 直跑)`acr` 偵測 stdin 非 TTY → 拒絕並提示「需人類互動」。
|
|
||||||
- MCP / Python / JS lib:傳 `human_confirmation` 才能成功;它們的 SDK 文件註明此欄位需人類提供。
|
|
||||||
- **誠實限制**(寫進 mindset + 文件):AI 技術上能偽造 `confirmed_by_human:true`。靠 reason 留記錄 + mindset 明示「絕不代替人類確認建零件」+ 軌跡可審,讓偽造成明確越界,不聲稱不可能繞過。
|
|
||||||
|
|
||||||
## 2. G1 假零件偵測(R2)
|
|
||||||
|
|
||||||
新增 `detectFakeComponent(contract, wasmBytes): string | null`:
|
|
||||||
- (a) **外部 URL/domain**:掃 contract 的 description / input_schema / output_schema 文字,及 wasm binary 文字,比對 URL pattern(`https?://`、常見 domain)。命中 → 退稿。
|
|
||||||
- (b) **http_request 子集**:若 contract 宣告只做「打固定 endpoint」(heuristic:description 含「打/呼叫 ... API/endpoint」且 input 有 url-like 欄位且無實質邏輯運算),標記疑似。
|
|
||||||
- 退稿訊息:「偵測到疑似假零件(寫死 endpoint / http 子集)。這該是 API recipe(http_request + 固定設定)或工作流,不是零件。見 DECISIONS §1。」
|
|
||||||
- 排除:`auth_*` primitive(credential 後端,DECISIONS §3b 不適用假零件判準)、`http_request` 自己。
|
|
||||||
|
|
||||||
## 3. G3 純 WASI 把關(R3)
|
|
||||||
|
|
||||||
擴充現有 `scanSyscalls`:
|
|
||||||
- 現況:掃 `FORBIDDEN_SYSCALLS` 黑名單。
|
|
||||||
- 擴充:改為「import 白名單」——解析 wasm import section,確認所有 import module 只屬 `wasi_snapshot_preview1` + `u6u`(host functions)。出現其他 module → 退稿(runtime 鎖定風險)。
|
|
||||||
- 實作:簡易 wasm import section 解析(不需完整 wasm parser,掃 import 段的 module name 字串)。
|
|
||||||
|
|
||||||
## 4. G4 Gherkin 真實作(R1)— **修訂(2026-05-29,richblack review)**
|
|
||||||
|
|
||||||
### 4.0 為何不能在 registry Worker 跑(原設計作廢)
|
|
||||||
原設計假設 registry Worker instantiate 投稿 wasm 跑 Gherkin。**此假設錯誤**:
|
|
||||||
- **Cloudflare Workers 禁止 request-time 編譯 WASM**(`new WebAssembly.Module(bytes)` / `WebAssembly.compile()` 只能 startup 用 bundle 的 module;workers-types 把 `Module` 標 abstract 正反映此限制)。registry 收到的是 runtime 投稿 bytes → 跑不了。
|
|
||||||
- DECISIONS §8:第一期**不依賴 GitHub Actions** → 也不能靠 CI 跑 Gherkin。
|
|
||||||
- 剩下唯一一致 venue = **投稿者本地機器**(有 tinygo + 能跑 wasm,與現有 build 流程同環境)。
|
|
||||||
|
|
||||||
### 4.1 正確設計:Gherkin 在投稿指令本地跑
|
|
||||||
零件投稿走一個**獨立 CLI 指令**(既有指令;「本地或公共都是投稿」):
|
|
||||||
1. 本地 `tinygo build`(或讀已 build 的 .wasm)。
|
|
||||||
2. **本地跑 Gherkin**:對每個 `gherkin_tests[]`,用 Node 的 WebAssembly + 同一份 wasi-shim instantiate wasm,given→stdin→run→比對 then_contains。Node 環境能 runtime 編譯 wasm(不像 CF Workers)。
|
|
||||||
3. 任一 scenario 失敗 → 投稿指令本地就擋下,不送出。
|
|
||||||
4. 通過 → 把**測試結果隨投稿上傳**(見 4.2)。
|
|
||||||
|
|
||||||
`runGherkin.ts`(已寫,用 createWasiShim)邏輯正確,只是**執行 venue 從 registry Worker 改成 CLI(Node)**。registry 端不再跑 Gherkin。
|
|
||||||
|
|
||||||
### 4.2 「平台看得到測試結果」(呼應 §3c:執行者不能驗證自己)
|
|
||||||
投稿 payload 帶 `gherkin_evidence`:每個 scenario 的 `{scenario, given, actual_stdout, passed}`。
|
|
||||||
- registry 存進 KV metadata(軌跡可審)。
|
|
||||||
- 平台看得到**原始 stdout**,不是只看投稿者宣稱的「passed」。
|
|
||||||
- **誠實限制**(同人類閘門):本地跑 + 自報結果,AI 技術上能偽造 actual_stdout。靠軌跡可審 + mindset 明示 + 未來 §3c 的 test/relay(投稿走 relay 讓平台當下親跑,第一期後)補強。第一期是「本地跑 + 上傳證據 + 可審」,不聲稱不可繞過。
|
|
||||||
|
|
||||||
### 4.3 公私庫分流(投稿指令旗標)
|
|
||||||
- 預設投稿 → **私人庫**(self-hosted 自己的 registry)。
|
|
||||||
- `-p` / `--public` → 推**公共庫**。
|
|
||||||
- 兩者都跑同一套把關(§0.0:跨公私庫同一套)。差別只在目標 registry。
|
|
||||||
|
|
||||||
### 4.4 registry 端對應
|
|
||||||
- registry submit 仍跑 G1(假零件)、G3(純WASI)——這兩個是**靜態掃描,不需執行 wasm,CF Worker 可跑**。
|
|
||||||
- G4 Gherkin 的執行移到 CLI;registry 收 `gherkin_evidence` 存證、可選做輕量一致性檢查(evidence 的 scenario 數與 contract.gherkin_tests 對得上、每個 passed=true),但**不重跑**(跑不了)。
|
|
||||||
- G5/G6(cold_start/runtime_compat)維持 unimplemented_steps 標記。
|
|
||||||
|
|
||||||
## 4.5 信任模型與發佈風險(richblack 2026-05-29 定,重要——勿誤讀為零風險)
|
|
||||||
|
|
||||||
**Gherkin 全綠 ≠ 零件安全可發佈。** 把關能驗到什麼、不能驗到什麼,要說清楚:
|
|
||||||
|
|
||||||
### 把關各層各防什麼
|
|
||||||
| 層 | 防得了 | 防不了 |
|
|
||||||
|---|---|---|
|
|
||||||
| G4 Gherkin | 「零件對宣稱的 given 不 work」 | 「Gherkin 寫得避重就輕、只測會過的 case」——投稿者可寫不完整的測試,沒測到的 input 仍可能出錯/惡意 |
|
|
||||||
| G1 假零件 / G3 純WASI | 連外 / 非 WASI / 寫死 endpoint | 沙箱內的邏輯錯誤 |
|
|
||||||
| **純 WASI 沙箱(真正的發佈底氣)** | **框死零件能做的事**:只能 stdin→stdout,無網路 syscall、無檔案系統、無 host 能力(除白名單 host func)→ 就算邏輯有問題或惡意,傷害被沙箱框死 | 沙箱允許範圍內的錯誤輸出 |
|
|
||||||
| 市場機制(§1) | 長尾:別人用了發現不對 → 回報 → 修 | 投稿當下測不到的 |
|
|
||||||
|
|
||||||
**結論:發佈非零風險,是風險可控。** 主要安全保證是**純 WASI 沙箱框死能力**(不是 Gherkin)。Gherkin 驗行為契約、沙箱框死破壞力、市場補長尾,三者合起來讓「發佈陌生人的零件」風險可控到能接受。
|
|
||||||
|
|
||||||
### 信任層級(誰跑 Gherkin 決定可信度,§3c:執行者不能驗證自己)
|
|
||||||
- **第一期**:投稿者**本地跑 + evidence 上傳可審**。AI 技術上能偽造 actual_stdout(它控制本地環境)。**誠實標明:第一期 evidence 可造假**,靠純 WASI 沙箱 + 軌跡可審 + mindset 撐著。
|
|
||||||
- **未來**:平台**自己重編源碼 + 重跑 Gherkin**(投稿者無法造假「跑的結果」)。需要能 runtime 跑 wasm 的 venue——**非 CF Worker(不能 runtime 編譯)、非 CI(§8 不依賴)**——可能是公共庫專屬的 sandbox 服務。列未來,可能擋公共庫「完全可信發佈」。
|
|
||||||
- 再往後:§3c 的 test/relay(投稿走 relay 讓平台當下親跑)。
|
|
||||||
|
|
||||||
## 5. G5/G6 mock 標未實作(R3 誠實)
|
|
||||||
|
|
||||||
- cold_start / runtime_compat 保留 mock,但 **SandboxResult 增 `unimplemented_steps: string[]`**,回傳時明列 `["cold_start","runtime_compat"]`,submit 回應與文件明示「這兩步未實作、未真正驗證」。不回 `success:true` 假裝全綠——回 success 但附 unimplemented 清單。
|
|
||||||
|
|
||||||
## 5.5 黃金向量:人工核對 + B 覆蓋檢查(richblack 2026-05-30 定)
|
|
||||||
|
|
||||||
Claude.ai 建議用「黃金向量 + 把關自己重跑」自動驗收,防放水的 Gherkin。**價值保留,實作降級**,理由:
|
|
||||||
|
|
||||||
- **primitive 極少、未來絕大部分是 recipe**(人類閘門 + 工作流優先把零件擋在源頭)。現役 17 白名單 + cron/platform_crypto,未來極少新增。
|
|
||||||
- 「把關自動重跑向量」是為「零件大量增加」做的規模化基建——但零件不會大量增加 → 為不存在的規模做自動化 = 過度工程(DECISIONS 附錄「會不會累積成債」判準)。
|
|
||||||
- 且「把關自己重跑 wasm」撞 venue 牆(CF 不能 runtime 編譯 wasm,同 §4.0)→ 需平台端 sandbox(第一期沒有)。
|
|
||||||
|
|
||||||
**降級後做法:**
|
|
||||||
- **A 黃金向量 conformance → 人工核對**:黃金向量當「人類閘門時用 CC 核對 primitive 的對照表」。新增 primitive(極稀有)時,人類閘門已要你親自確認,那一刻用 CC 本地跑向量核對(本地有 tinygo + 能跑 wasm,繞開 venue 牆)、人工凍結。**不做機器自動重跑。**
|
|
||||||
- **B 覆蓋檢查 hook → 現在做(純靜態、不可造假、成本低、價值與零件數無關)**:靜態 parse contract:`input_schema` 每個 required 欄位至少出現在一個 Gherkin given、`output_schema` 每個欄位至少被一個 then_contains 斷言。缺 → exit 2 指出漏哪個欄位。擋「只測 happy path、不碰宣告過的行為面」。
|
|
||||||
- **初始向量來源(信任根:寫向量≠寫實作)**:另起 session 從 contract 語義寫、不看實作原碼(primitive 語義客觀如 add(2,2)=4)。人工核對用,不急、新增 primitive 時逐個補,不必 21 個一次到位。
|
|
||||||
- 殘留(誠實):向量/覆蓋檢查擋不住「列出的 case 全對、沒列到的 case 錯」。交給「用」——出 bug 補進向量,永不 regress。是會長大的網,非設一次完美。
|
|
||||||
|
|
||||||
## 6. R5 白名單 + 本機 hook
|
|
||||||
|
|
||||||
- `registry/MVP_COMPONENTS.txt`:一行一個白名單 canonical_id(現役 22 個)。
|
|
||||||
- `pre-write-guard.sh` 增規則:寫入 `registry/components/{name}/...` 且 `{name}` 不在 MVP_COMPONENTS.txt → exit 2,訊息「新增零件需走 submit API 人類閘門,不可直接造 repo 目錄」。
|
|
||||||
- `pre-bash-guard.sh` 增規則:`mkdir .../registry/components/{白名單外}` → exit 2。
|
|
||||||
- `.ts` 偵測現有 hook 已做(rule 1.1)。
|
|
||||||
- **B 覆蓋檢查**(5.5):可放 registry submit 的靜態驗收(不需跑 wasm,CF 可跑)或 pre-write hook,擋宣告過的欄位沒被 Gherkin 測到。
|
|
||||||
|
|
||||||
## 7. 範圍邊界
|
|
||||||
|
|
||||||
- **動 registry TS**(sandboxAcceptance / submitComponent / routes / types)+ **CLI**(acr parts publish,既有指令)+ **hook**。
|
|
||||||
- 不動 cypher-executor 執行路徑、不動既有零件 wasm。
|
|
||||||
- backfill 路徑(skip_acceptance)保持可用,不被新閘門擋(存量零件不需人類閘門)。
|
|
||||||
- CLI/MCP/Python/JS 四路:本期至少做 CLI `acr parts publish` + registry endpoint 強制;MCP/Python/JS 補 `human_confirmation` 欄位支援(薄)。
|
|
||||||
|
|
||||||
## 8. 驗收標準
|
|
||||||
|
|
||||||
- 投一個寫死 endpoint 的假零件 → G1 退稿(終端輸出)。
|
|
||||||
- 投一個 `.ts` 進 registry/components → hook exit 2。
|
|
||||||
- 投一個白名單外的新零件目錄(本機造)→ hook exit 2。
|
|
||||||
- 無 human_confirmation 的 submit → 403。
|
|
||||||
- 帶 human_confirmation + 過 Gherkin 的真零件 → 通過、寫 KV、reason 留 metadata。
|
|
||||||
- Gherkin given/then 對真零件跑綠;故意改壞 then_contains → 退稿。
|
|
||||||
- cold_start/runtime_compat 在回應裡列入 unimplemented_steps(不假綠)。
|
|
||||||
|
|
||||||
## 9. 決議(richblack 2026-05-29 design review 定)
|
|
||||||
|
|
||||||
- **Q1 → 消解**:Gherkin 測的零件**永遠是封閉邏輯(框架),不連外**。任何要加外部 URL 的東西按定義就是 recipe,不是零件——這種「連外零件」根本不該存在(會被 G1 假零件偵測擋下、降成 recipe)。所以 G4 Gherkin 只跑不需 host function 的封閉邏輯零件,**不需要 mock host func、不需要 skip 機制**。零件用 `u6u.http_request` 連外 = G1 直接退稿。
|
|
||||||
- **Q2 → 兩者都硬擋**:(a) contract/原碼有具體外部 URL/domain → 硬退稿;(b) 宣告能力是 http_request 子集 → 也硬退稿。理由:與 Q1 一致——零件不該連外,這兩個 pattern 都是「該是 recipe 的東西偽裝成零件」,硬擋無誤殺顧慮(真的要連外就去做 recipe)。
|
|
||||||
- **Q3 → submit 過閘門後自動 append**:人類閘門通過 + 驗收綠的零件,submit 成功時自動把 canonical_id append 進 `MVP_COMPONENTS.txt`。白名單反映「已正當投稿的零件」,不需手動維護。本機 hook 讀此檔擋「白名單外的直接造目錄」。
|
|
||||||
|
|
||||||
### Q1 連帶結論(強化 G1)
|
|
||||||
既然「零件不連外、連外即 recipe」是硬規則,G1 假零件偵測 = G4 Gherkin 的前置守門:
|
|
||||||
G1 擋掉所有連外/http 子集的投稿 → 能進到 G4 的必然是封閉邏輯零件 → Gherkin 必然不需 host func。
|
|
||||||
兩道閘門邏輯自洽。
|
|
||||||
@@ -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 relay(DECISIONS §3c,第一期後)|
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 1. 自有庫 push 把關(self-hosted,第一期做)
|
|
||||||
|
|
||||||
`acr recipe push` 的兩個提醒。**提醒級 = 告知 + 需人類明示同意,不硬擋**(self-hosted 是用戶自己的庫,
|
|
||||||
他同意後就是他的責任 — mindset §6 / data-exfil-warning 既有原則)。
|
|
||||||
|
|
||||||
### 1.1 資料外流提醒(W2.2)
|
|
||||||
- **觸發**:push 的 recipe / 或部署的 workflow 會讓「資料對外可見」——主要是產生**對外可被呼叫的 webhook**
|
|
||||||
(`POST /webhooks/named/...` 對外 trigger URL),或 recipe 把本地資料 POST 到外部服務。
|
|
||||||
- **行為**:CLI 印明確警示「這個動作會讓 X 對外界可見/可呼叫,確認要繼續嗎?」→ 需人類明示同意(y/N)。
|
|
||||||
非 TTY(AI 直跑)→ 拒絕,提示「需人類確認」(mindset §7:絕不代替人類做暴露確認)。
|
|
||||||
- **與既有 data-exfil-warning 的關係**:已有 API 層 + pre-bash hook(commit 51d40ee 等)。
|
|
||||||
本項確認**涵蓋 recipe push 這條路徑**;若已涵蓋則只補文件,若沒涵蓋則補上 push 路徑的提醒。
|
|
||||||
- **誠實限制**:AI 技術上能偽造 exposure_consent。價值是法律歸責 + 軌跡可審,不聲稱不可繞過(mindset §7)。
|
|
||||||
|
|
||||||
### 1.2 打通檢查(W2.3)
|
|
||||||
- **目的**:recipe 是「指向外部 API 的指針」,正確性一半在「打不打得通」(DECISIONS §1 recipe 驗收標準 = 2xx)。
|
|
||||||
- **行為**:push 時(或 push 後)對 recipe 的 endpoint **實打一次**,回報 HTTP status。
|
|
||||||
- 2xx → 「✓ recipe 打通(HTTP 200)」
|
|
||||||
- 4xx/5xx → 「⚠️ recipe 未打通(HTTP 401/404/...)」+ 誠實標原因(如「缺 credential → 先 acr creds push」)
|
|
||||||
- 連不上 → 「⚠️ 無法連線」
|
|
||||||
- **self-hosted 是提醒級**:打不通**不硬擋 push**(用戶可能就是要先 push 再設 credential),只如實回報。
|
|
||||||
- **誠實**(mindset §7):缺 credential 打不到 2xx 就誠實標「未驗收:缺 X」,不 mock 充綠燈。
|
|
||||||
|
|
||||||
### 1.3 動到的檔案(待 review 後)
|
|
||||||
| 檔案 | 動作 |
|
|
||||||
|---|---|
|
|
||||||
| `cli/src/commands/recipe.ts` | push 流程加 (1) 資料外流提醒 prompt (2) 打通檢查(實打 endpoint 回報 status) |
|
|
||||||
| `.claude/hooks/*`(如需)| 確認 data-exfil pre-bash hook 涵蓋 recipe push;缺則補 |
|
|
||||||
|
|
||||||
**不動**:cypher-executor 執行路徑、零件、credential 解密邏輯。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 2. 公共庫 push 把關(第一期後)
|
|
||||||
|
|
||||||
- recipe 進公共庫 = 別人會用 → 需維護者機制檢核「**實際打通、真收到成功回傳**」(不是投稿者自報)。
|
|
||||||
- 機制:DECISIONS §3c 的 **test/relay**——push 公共庫走 relay,維護者當下親見真實打通記錄
|
|
||||||
(執行者不能驗證自己,§7 閉環)。
|
|
||||||
- **範圍**:依賴公共庫 + relay 基建,**第一期不做**(第一期是 self-hosted + 提醒級)。
|
|
||||||
- 本檔只記框架,第一期不實作。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 3. 同一套指令、不同把關強度的切分(W2.4)
|
|
||||||
|
|
||||||
- `acr recipe push`(自有庫,預設)→ §1 提醒級。
|
|
||||||
- `acr recipe push --public`(公共庫,未來)→ §2 relay 檢核級。
|
|
||||||
- 同一指令、旗標分流(呼應 design §4.3 公私庫分流:`-p`/`--public`)。
|
|
||||||
- 第一期只實作預設(自有庫)路徑。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 4. 驗收標準(客觀證據,mindset §7)
|
|
||||||
|
|
||||||
第一期(自有庫提醒級):
|
|
||||||
1. `acr recipe push` 一個會產對外 webhook 的東西 → CLI 印資料外流警示 + 要人類同意;非 TTY → 拒絕。
|
|
||||||
2. `acr recipe push` 一個 endpoint 可達的 recipe → 打通檢查回報「✓ HTTP 2xx」。
|
|
||||||
3. `acr recipe push` 一個缺 credential 的 recipe → 回報「⚠️ 未打通:缺 credential」(誠實,不假綠),但仍允許 push。
|
|
||||||
4. 確認 workflow 引用 recipe(`component: kbdb_get`)**不再被任何 validate 步驟當假零件擋**(W1 已作廢)。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 5. 與既有 SDD 的一致性確認(無新矛盾)
|
|
||||||
|
|
||||||
- 不動「零件投稿走 PR + 人工檢查」(§0.1/0.2,與 design 頂部方向修正、DECISIONS §8 一致)。
|
|
||||||
- 不重啟「零件 PR 自動化把關」(§0.2,與 BACKLOG 步驟5 真意一致)。
|
|
||||||
- 資料外流提醒延續既有 data-exfil-warning 原則(mindset §6),只確認涵蓋 recipe push 路徑。
|
|
||||||
- 打通檢查 = recipe 驗收標準 2xx 的落地(DECISIONS §1)。
|
|
||||||
@@ -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 不適用 Gherkin(DECISIONS §1:recipe 驗收 = 2xx);本需求只針對 WASM 零件投稿。
|
|
||||||
|
|
||||||
### R2 — 假零件偵測(退稿並指回正路)
|
|
||||||
依 DECISIONS §1 判準,投稿零件若滿足任一,退稿:
|
|
||||||
- (a) contract 或(可得的)原碼出現具體外部服務 URL / domain
|
|
||||||
- (b) 宣告能力是 http_request 子集(打某固定 endpoint)
|
|
||||||
退稿訊息要指回正路:「這該是 recipe / 工作流,不是零件」。
|
|
||||||
|
|
||||||
### R3 — 純 WASI preview1 / 零宿主依賴把關
|
|
||||||
- 擴充 syscall 掃描:確認零件只依賴 WASI preview1 + u6u host functions 白名單,無其他宿主特定依賴(避免 runtime 鎖定債,DECISIONS §4)。
|
|
||||||
- 冷啟動 / runtime 相容兩步可暫留 mock,但**明確標「未實作」**,不可假裝通過(§3c/§7 禁假綠)。
|
|
||||||
|
|
||||||
### R4 — 建零件的人類閘門(核心,BACKLOG 步驟 5 強化規格)
|
|
||||||
- **把關點 = 建立零件的 API 本身**(registry submit endpoint),不是寫檔案。CLI / MCP / Python lib / JS lib 四路全收斂到這關。
|
|
||||||
- submit 預設**拒絕**,除非請求帶:
|
|
||||||
- (a) 人類已確認憑證(CLI `acr component create` 強制互動式問人類,AI 非互動環境生不出)
|
|
||||||
- (b) 舉證 `reason_why_not_workflow`(AI 須證明工作流為何做不到;舉證責任在 AI,預設假設工作流能做)
|
|
||||||
- 缺 → 403 + 指回正路(先試工作流)。
|
|
||||||
- 誠實限制:沒技術能 100% 防 AI 假冒人類確認;靠閘門 + 舉證留記錄 + mindset 明示「絕不代替人類確認」+ 軌跡可審計,讓假冒成明確越界。
|
|
||||||
|
|
||||||
### R5 — 白名單 + 本機 hook 兜底
|
|
||||||
- `MVP_COMPONENTS.txt` 白名單檔。
|
|
||||||
- pre-write / pre-bash hook:CC 直接在 `registry/components/{白名單外新名}/` 造目錄或 mkdir → exit 2(擋繞過 API 直接改 repo 等 CI 部署的旁門)。
|
|
||||||
- pre-write hook:`registry/components/` 下出現 `.ts`(非 AssemblyScript)→ exit 2(現有 hook 已部分做)。
|
|
||||||
|
|
||||||
### R6 — 不限制自由
|
|
||||||
- 不做「audience 宣告 / 禁止自用進公共庫」式的限制(DECISIONS:別人要建零件是他的自由)。
|
|
||||||
- 唯一硬約束:零件 = 只打一個 endpoint 的薄殼。閘門是「要建得先說服人 + 舉證」的摩擦,非禁止。
|
|
||||||
- arcrun 不做授權判斷(能否打通由發 key 的服務裁決)——把關針對「是否該是零件」,不針對「誰能打哪個 API」。
|
|
||||||
|
|
||||||
## 非目標
|
|
||||||
- Phase 5 用戶自製零件 R2 上傳(未啟用)。
|
|
||||||
- registry KV schema 大改(用既有結構)。
|
|
||||||
- 完整的 audit trail 系統(DECISIONS §7 事後機制第一層,另議)。
|
|
||||||
@@ -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**:G1(detectFakeComponent)、G3(wasmImports)、G5/G6(unimplemented_steps)、
|
|
||||||
G0 registry 人類閘門(保留不刪)。測試 15 綠。
|
|
||||||
- **改去向**:G4 Gherkin / 覆蓋檢查 → 未來接 CI PR check(CI 能跑 wasm)。G1/G3 邏輯可複用。
|
|
||||||
- **不做**:CI PR check(richblack:人工 review 就夠,primitive 極少)、R5 本機 hook
|
|
||||||
(PR/merge + G1 + 沙箱已防「未經同意變公共零件」,hook 過度工程)、registry self-service、
|
|
||||||
acr parts publish 加閘門、平台 sandbox 重跑。
|
|
||||||
- **黃金向量**:人工核對(另起 session 從語義寫),不急、不機器自動化。
|
|
||||||
- **轉出範圍**:真正的裸奔風險在「資料外流」(recipe/webhook 把資料送出去),不分公私庫
|
|
||||||
→ 另開新 SDD「資料外流警示」(API 層警示 + AI 動手前 hook)。用戶 API 保護(入站認證)
|
|
||||||
+ recipe/part/function 架構釐清 → 記 BACKLOG 待決策。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## G1 假零件偵測(R2,Q2=兩者硬擋)
|
|
||||||
- [x] 1.1 `registry/src/actions/detectFakeComponent.ts`:(a) 外部 URL/domain 偵測(掃 contract 文字 + wasm binary 文字)硬擋;(b) http_request 子集偵測硬擋
|
|
||||||
- [x] 1.2 排除 `auth_*` primitive 與 `http_request` 自己
|
|
||||||
- [x] 1.3 接進 sandboxAcceptance 步驟鏈(fake_component_scan 為第一步)
|
|
||||||
- [x] 1.4 退稿訊息指回正路(「這該是 recipe/工作流」)
|
|
||||||
|
|
||||||
## G3 純 WASI 把關(R3)
|
|
||||||
- [x] 3.1 `wasmImports.ts` 解析 wasm import section,取出所有 import module name(已對真實零件驗證)
|
|
||||||
- [x] 3.2 白名單:只准 `wasi_snapshot_preview1` + `u6u`;其他 module → 退稿
|
|
||||||
- [x] 3.3 接進 scanSyscalls(白名單為主,黑名單為次)
|
|
||||||
|
|
||||||
## G4 Gherkin 真實作(R1)— venue 修訂:CLI 本地跑,非 registry(design §4 修訂)
|
|
||||||
> CF Worker 不能 runtime 編譯 wasm + §8 不依賴 CI → Gherkin 在投稿指令本地(Node)跑。
|
|
||||||
> 在第一段(tinygo build→wasm 之後)測,跟 worker 無關。registry 只存 evidence 不重跑。
|
|
||||||
- [x] 4.1 `runGherkin.ts`(createWasiShim 跑 wasm,邏輯正確)—— 但 venue 要從 registry 改 CLI
|
|
||||||
- [ ] 4.2 回退 sandboxAcceptance:registry 不跑 Gherkin(移除 await runGherkin),改回靜態步驟
|
|
||||||
- [ ] 4.3 Gherkin 邏輯搬到 CLI 投稿指令(Node 環境 instantiate wasm)
|
|
||||||
- [ ] 4.4 投稿 payload 帶 gherkin_evidence(scenario/given/actual_stdout/passed),registry 存 metadata 可審
|
|
||||||
- [ ] 4.5 誠實標明第一期 evidence 可造假(mindset + 文件);平台重跑列未來
|
|
||||||
- [ ] 4.6 投稿指令(暫名 acr component submit)+ 公私庫分流(-p 公共)— 新 CLI 工程
|
|
||||||
|
|
||||||
## G5/G6 誠實標未實作(R3 禁假綠)
|
|
||||||
- [x] 5.1 SandboxResult 增 `unimplemented_steps: string[]`
|
|
||||||
- [x] 5.2 cold_start / runtime_compat 列入 unimplemented_steps,submit 回應明示
|
|
||||||
|
|
||||||
## G0 人類閘門(R4,核心)
|
|
||||||
- [x] 0.1 submit 請求增 `human_confirmation`(SubmitOptions in submitComponent.ts)+ route 解析(multipart/JSON 皆支援)
|
|
||||||
- [x] 0.2 submit 邏輯:非 skip_acceptance 的新投稿,無 human_confirmation/空 reason → gateError(指回正路)
|
|
||||||
- [x] 0.3 human_confirmation + gherkin_evidence 寫進 KV metadata(軌跡可審)
|
|
||||||
- [ ] 0.4 CLI `acr parts publish`(既有指令):互動式問人類(工作流為何做不到 + 確認),非 TTY 拒絕
|
|
||||||
- [ ] 0.5 MCP / Python lib / JS lib 補 human_confirmation 欄位支援(薄)
|
|
||||||
- [ ] 0.6 誠實限制寫進 mindset Skill(步驟 7)+ SDK 文件
|
|
||||||
|
|
||||||
> 命名修正(2026-05-29):投稿走**既有** `acr parts publish`(cli/src/commands/parts.ts),
|
|
||||||
> 非另建 acr component create(符合「修改現有不重建」)。G0-CLI(0.4)與 G4-CLI 合併在此指令做。
|
|
||||||
|
|
||||||
## R5 白名單 + 本機 hook
|
|
||||||
- [ ] 5.3 `registry/MVP_COMPONENTS.txt`(現役 22 個 canonical_id)
|
|
||||||
- [ ] 5.4 submit 過閘門成功 → 自動 append canonical_id 進白名單(Q3)
|
|
||||||
- [ ] 5.5 pre-write-guard.sh:寫 `registry/components/{白名單外}/` → exit 2
|
|
||||||
- [ ] 5.6 pre-bash-guard.sh:mkdir `registry/components/{白名單外}` → exit 2
|
|
||||||
|
|
||||||
## W1 ~~CLI workflow validate 擋假零件式 component 名~~(2026-06-01 作廢,方向修正)
|
|
||||||
|
|
||||||
> **作廢原因(richblack 2026-06-01)**:「擋假零件」這件事不再存在——因為**自製/修改零件的路
|
|
||||||
> 已被封鎖**(CC 根本造不出零件),workflow 引用 recipe(如 component: kbdb_get)是**合法且
|
|
||||||
> 未來唯一的擴充方式**,不該被當「假零件」擋。把關點從「workflow validate」**移到 recipe 入庫
|
|
||||||
> (push)那一刻**。已動的 yaml-parser.ts `LEGAL_PRIMITIVES`/`findSuspectComponents` 已回退。
|
|
||||||
> 取而代之 → 見 W2。
|
|
||||||
|
|
||||||
## W2 封鎖自製零件 + recipe 入庫把關(2026-06-01 新方向)
|
|
||||||
|
|
||||||
> richblack 2026-06-01 定調:
|
|
||||||
> - 零件由維護者管理,**CC 不能自製/修改零件**(hook + CLI 拒絕)→ 不再有「假零件」。
|
|
||||||
> - CC 唯一能擴充的是 **recipe**。recipe 一律用「推(push)」,**自有庫與公共庫同一套指令**。
|
|
||||||
> - 把關依庫別分強度:
|
|
||||||
> - **自有庫(self-hosted)**:只能**提醒**(無法在別人機器強制)。兩個提醒:
|
|
||||||
> (1) 資料外流提醒——某動作會讓外界看到你的東西(如 workflow 產對外 webhook),同意後是他的責任;
|
|
||||||
> (2) 打通檢查——查他要打的 API 是否打得通(2xx)。
|
|
||||||
> - **公共庫**:由維護者機制檢核「實際打通、真收到成功回傳」(PR/CI relay,DECISIONS §3c,第一期後)。
|
|
||||||
> 屬 change,需先寫 design 給 richblack review 才動 code。本節先記框架。
|
|
||||||
|
|
||||||
- [x] W2.1 封鎖自製零件 — **釐清完成(richblack 2026-06-02)**:靠「零件投稿走 GitHub PR + 人 merge」
|
|
||||||
天然閘門(DECISIONS §8)。BACKLOG 步驟5「不做 hook」真意 = 零件少、不為零件 PR 蓋自動化把關
|
|
||||||
(量少人工檢查;爆量才回頭想),**不是**不阻止自製。無矛盾,不需新做 hook。
|
|
||||||
- [x] W2.2 `acr recipe push` 資料外流提醒 — **既有實作已涵蓋**:recipe.ts:70-79 `obtainExposureConsent`
|
|
||||||
(exposure-warning.ts:互動打資源名確認、非 TTY 拒絕、首次問記住)。data-exfil-warning SDD 已做,確認涵蓋 recipe push 路徑。
|
|
||||||
- [x] W2.3 `acr recipe push` 打通檢查 — **新增** `probeRecipeEndpoint`(recipe.ts):push 成功後實打 endpoint,
|
|
||||||
回報 2xx/⚠。提醒級不硬擋;endpoint 含 {{模板}} → 誠實說明待 run 才知;401/403 → 標「多半缺 credential,非 recipe bug」(不假綠,mindset §7)
|
|
||||||
- [ ] W2.4 公共庫 push(--public)= 維護者 relay 檢核(DECISIONS §3c)— 第一期後,本期只做自有庫提醒級
|
|
||||||
|
|
||||||
## 驗收(design §8)
|
|
||||||
- [ ] V1 投寫死 endpoint 假零件 → G1 退稿(終端輸出)
|
|
||||||
- [ ] V2 投 `.ts` 進 registry/components → hook exit 2
|
|
||||||
- [ ] V3 本機造白名單外零件目錄 → hook exit 2
|
|
||||||
- [ ] V4 無 human_confirmation 的 submit → 403
|
|
||||||
- [ ] V5 帶 human_confirmation + 過 Gherkin 真零件 → 通過 + reason 留 metadata + 白名單 append
|
|
||||||
- [ ] V6 故意改壞 then_contains → Gherkin 退稿
|
|
||||||
- [ ] V7 回應含 unimplemented_steps(cold_start/runtime_compat 不假綠)
|
|
||||||
@@ -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+ 個零件已經部署成獨立 Worker(kbdb_ingest, claude_api, kbdb_create_block, kbdb_patch_block, http_request, string_ops, ⋯),但**它們的 contract.yaml 沒有透過 `POST /components/submit` 進 registry index**。
|
|
||||||
|
|
||||||
部署路徑:
|
|
||||||
```
|
|
||||||
registry/components/{name}/main.go ← TinyGo 寫的零件
|
|
||||||
↓ tinygo build
|
|
||||||
.component-builds/{name}/component.wasm
|
|
||||||
↓ wrangler deploy
|
|
||||||
{name}.arcrun.dev (Worker) ← 零件可被 HTTP 呼叫了
|
|
||||||
|
|
||||||
registry index? ← 這步從來沒做
|
|
||||||
```
|
|
||||||
|
|
||||||
### 1.3 影響(吃狗糧的觀察)
|
|
||||||
- 新 AI(Claude / Gemini / Codex)進來不知道有什麼零件 → 自己寫 Python 直打 API
|
|
||||||
- arcrun 想推「AI-first 自服務」整個破功
|
|
||||||
- 文件寫得再好都救不了 — 因為 README 只能寫概念,零件清單必須是 API 動態查
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 2. 目標
|
|
||||||
|
|
||||||
**Registry 是零件 metadata 的 SSOT**:
|
|
||||||
|
|
||||||
- 零件 Worker 在跑 ⇔ registry 有對應 entry(雙向綁定)
|
|
||||||
- AI 透過 MCP `search_components` 永遠找得到所有現役零件
|
|
||||||
- README 不寫死數量,動態 badge 即時反映
|
|
||||||
- 第三方裝完 MCP 30 秒內能找到第一個可用零件
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 3. 三層設計
|
|
||||||
|
|
||||||
### Layer 1: 一次性 backfill(Phase 1)
|
|
||||||
|
|
||||||
掃 `matrix/arcrun/registry/components/*/component.contract.yaml`,把每個 contract POST 進 registry index。
|
|
||||||
|
|
||||||
工具:`matrix/arcrun/registry/scripts/backfill-index.ts`
|
|
||||||
- 讀檔 → 解析 YAML → 呼叫 registry submit endpoint
|
|
||||||
- idempotent:已存在不重複寫(registry 端要支援 upsert)
|
|
||||||
- 跳過沙盒驗收(這些零件已驗過、已部署,不用重跑 gherkin tests)
|
|
||||||
|
|
||||||
### Layer 2: 部署即註冊(Phase 2)
|
|
||||||
|
|
||||||
改 `.github/workflows/deploy.yml`:
|
|
||||||
- 通用掃描掃到 `.component-builds/{name}/wrangler.toml` 部署成功後
|
|
||||||
- post-deploy step 自動呼叫 registry submit(contract 從 `registry/components/{name}/component.contract.yaml` 讀)
|
|
||||||
|
|
||||||
零件 Worker 部署 ⇒ registry 自動更新。沒有「零件部署了但 registry 不知道」的可能。
|
|
||||||
|
|
||||||
### Layer 3: Discoverability(Phase 3)
|
|
||||||
|
|
||||||
- README 移除「21 個零件」這種寫死數字,改「跑 search 看當前清單」
|
|
||||||
- 加 badge endpoint `registry.arcrun.dev/badge/components.svg` 即時顯示數量
|
|
||||||
- MCP `get_component_guide` 開頭加鐵律:「動工前必須先 search,不是猜」
|
|
||||||
- onboarding kit GitHub template:CLAUDE.md / .cursor/rules / AGENTS.md 三件套,all 強制 search 優先
|
|
||||||
|
|
||||||
### Layer 4: u6u → arcrun Rebrand(Phase 4)
|
|
||||||
|
|
||||||
把 `matrix/u6u-mcp/` 跟所有 `u6u_*` tool 名搬到 arcrun 命名空間。
|
|
||||||
|
|
||||||
理由:
|
|
||||||
- u6u 是申請 arcrun.dev 之前的暫名,現在已過時
|
|
||||||
- 命名混亂阻礙推廣(「為什麼 arcrun 文件叫 u6u_*?」)
|
|
||||||
- 第三方看到 u6u 不知道是同一個產品
|
|
||||||
|
|
||||||
範圍:
|
|
||||||
1. 目錄:`matrix/u6u-mcp/` → `matrix/arcrun-mcp/`
|
|
||||||
2. Worker name:`u6u-mcp` → `arcrun-mcp`
|
|
||||||
3. Tool 前綴:`u6u_search_components` → `arcrun_search_components`(14 個 tool)
|
|
||||||
4. Hostname:`mcp.finally.click` → `mcp.arcrun.dev`(finally.click 保留 redirect 到 arcrun.dev 過渡期)
|
|
||||||
5. Repo / Worker 內部 ID:u6u-mcp-server → arcrun-mcp-server
|
|
||||||
6. README 全文:u6u → arcrun
|
|
||||||
7. user memory(CLAUDE.md / MEMORY.md)相關提及一併更新
|
|
||||||
8. inkstone-component-registry(舊 worker)廢止 → arcrun-registry 為唯一現役
|
|
||||||
|
|
||||||
**Rebrand 原則:**
|
|
||||||
- 用戶端 config(claude_desktop_config.json 等)給過渡期:兩個 URL 都活,舊的回 deprecation header 提示換新
|
|
||||||
- Tool 前綴 `u6u_*` → `arcrun_*` 沒有過渡期(一刀切,因為前綴是 AI 看的,不是用戶記憶肌肉)
|
|
||||||
- 文件 / repo 內所有 reference 立即改
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 4. 範圍邊界
|
|
||||||
|
|
||||||
**在本 SDD 範圍內:**
|
|
||||||
- ✅ Phase 1: backfill index
|
|
||||||
- ✅ Phase 2: 部署即註冊 hook
|
|
||||||
- ✅ Phase 3: README + badge + onboarding kit
|
|
||||||
- ✅ Phase 4: u6u → arcrun rebrand(含目錄 / worker / hostname / tool 前綴 / 文件)
|
|
||||||
|
|
||||||
**不在範圍內:**
|
|
||||||
- 新零件開發(這是 polaris 業務範圍)
|
|
||||||
- registry KV schema 改動(用既有結構)
|
|
||||||
- u6u-gui 的 rebrand(u6u-mcp 同 monorepo 但獨立 SDD)
|
|
||||||
- Phase 5(用戶自製零件 R2 上傳)— 等 Phase 4 完成後另開 SDD
|
|
||||||
|
|
||||||
**前置依賴(已完成):**
|
|
||||||
- ✅ u6u-mcp Zod 4 → Zod 3 修復(2026-05-07)
|
|
||||||
- ✅ u6u-mcp service binding 改指 arcrun-registry(2026-05-07)
|
|
||||||
- ✅ arcrun-registry Worker 部署在 registry.arcrun.dev
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 5. 驗收標準
|
|
||||||
|
|
||||||
### Phase 1 驗收
|
|
||||||
- `u6u_search_components("kbdb")` 回非空結果,含 `kbdb_ingest` / `kbdb_create_block` / `kbdb_patch_block`
|
|
||||||
- `acr parts list` CLI 端對端能列出
|
|
||||||
- registry KV 內至少 30 entries
|
|
||||||
|
|
||||||
### Phase 2 驗收
|
|
||||||
- 部署任一既有零件後,registry 30 秒內 reflect 更新
|
|
||||||
- 部署一個全新零件,無需手動 publish,registry 自動有
|
|
||||||
- CI workflow 不會因 registry 寫入失敗就擋部署(degraded mode:寫入失敗 log warning 但不 fail)
|
|
||||||
|
|
||||||
### Phase 3 驗收
|
|
||||||
- README 沒有「21 個零件」「30 個零件」這種寫死數字
|
|
||||||
- badge SVG 渲染正確、數字跟 KV 一致
|
|
||||||
- onboarding kit clone 下來,照 README 跑能 30 秒內 list 到零件
|
|
||||||
|
|
||||||
### Phase 4 驗收
|
|
||||||
- `mcp.arcrun.dev/mcp/mcp` 通,回的 tool 名都是 `arcrun_*`
|
|
||||||
- 舊 `mcp.finally.click/mcp/mcp` 仍可用但回 deprecation header
|
|
||||||
- README / docs / GUIDE 全部 u6u 字樣消除
|
|
||||||
- `matrix/u6u-mcp/` 目錄不存在,改為 `matrix/arcrun-mcp/`
|
|
||||||
- 用戶記憶(`~/.claude/.../MEMORY.md`)arcrun MCP 設定範例已更新
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 6. 風險與緩解
|
|
||||||
|
|
||||||
| 風險 | 緩解 |
|
|
||||||
|---|---|
|
|
||||||
| backfill 把 contract 灌進去後,沙盒驗收覆蓋既有資料 | registry submit 加 `skip_acceptance=true` flag,僅 backfill 用 |
|
|
||||||
| 部署 hook 寫入失敗擋掉部署 | hook degraded mode:失敗只 warning,不 fail 部署 |
|
|
||||||
| Rebrand 把現役 client 弄壞 | 過渡期:舊 hostname 跟 worker 並存 1 個月 |
|
|
||||||
| Tool 前綴改名 AI 適應期 | 不過渡,一刀切(前綴是 system instruction 範圍,AI 一個 prompt 就學會)|
|
|
||||||
| 既有用戶 config 寫死 finally.click | 提前公告 + 過渡期 + 舊 endpoint 自動 redirect / proxy |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 7. 變更紀錄
|
|
||||||
|
|
||||||
| 版本 | 日期 | 內容 |
|
|
||||||
|---|---|---|
|
|
||||||
| v1.0 | 2026-05-07 | 初版。吃狗糧發現 registry 空的,三層設計(backfill / auto-register / discoverability)+ u6u → arcrun rebrand 一併納入。 |
|
|
||||||
@@ -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` bug(2026-05-07)
|
|
||||||
- [x] 0.2 u6u-mcp service binding `inkstone-component-registry` → `arcrun-registry`(2026-05-07)
|
|
||||||
- [x] 0.3 確認 `mcp.finally.click/mcp/mcp` 端對端通,tools/list 回 14 個 tool(2026-05-07)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Phase 1:Backfill Index(半天,立即見效)
|
|
||||||
|
|
||||||
- [x] 1.1 探查 registry 既有 endpoint:發現
|
|
||||||
- 既有 `POST /components` 強制要 wasm bytes(multipart 或 base64),跑沙盒驗收 + 寫 R2 + 寫 KV
|
|
||||||
- cypher-executor 已不從 R2 動態載 wasm(line 32 標 R2 路徑作廢,零件用獨立 Worker URL)
|
|
||||||
- 結論:R2 是 legacy,registry 真正用途是 metadata 索引給 AI 搜尋
|
|
||||||
- 決策:**加新 endpoint `POST /components/index-only`** 接 contract(無 wasm、無沙盒),專供 backfill 跟「已部署但未索引」零件用
|
|
||||||
- [x] 1.1.1 加 `src/actions/indexOnlyComponent.ts`(metadata-only 寫 KV,冪等)
|
|
||||||
- [x] 1.1.2 加 `src/routes/components.ts` 的 `POST /index-only` route
|
|
||||||
- [x] 1.1.3 部署 + smoke test(contract 驗證 + 錯誤處理通過)
|
|
||||||
- [x] 1.2 寫 `matrix/arcrun/registry/scripts/backfill-index.mjs`(zero-build node script,用 js-yaml)
|
|
||||||
- [x] 1.3 dry-run 確認 30 個 component 全 parse 通
|
|
||||||
- [x] 1.4 跑真 backfill(過程中發現並修了兩個 schema 問題):
|
|
||||||
- schema enum `category` 補 `auth` / `ai` / `platform`(types.ts)
|
|
||||||
- `max_cold_start_ms` 上限放寬 50 → 500(auth/ai 含 crypto 需要)
|
|
||||||
- `no_network_syscall` / `no_filesystem_syscall` 改 optional
|
|
||||||
- `max_size_kb` 上限放寬 2048 → 8192
|
|
||||||
- index-only route 對缺 gherkin/description/tags 的零件補 placeholder(不擋索引)
|
|
||||||
- [x] 1.5 驗證:MCP `u6u_search_components("kbdb")` 回 3 個零件(kbdb_ingest / kbdb_create_block / kbdb_patch_block)
|
|
||||||
- [ ] 1.6 驗證:`acr parts list` CLI 端對端能列
|
|
||||||
- [x] 1.7 驗證:registry KV 30 entries(30 created + 30 idx 共 60 keys)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Phase 1.5:砍 R2 dead storage(先於 Phase 2,清架構斷層)
|
|
||||||
|
|
||||||
> 2026-05-07 加入。R2 wasm 路徑早已 dead(cypher-executor 不從 R2 讀),保留只會誤導 AI。
|
|
||||||
> SDD design.md 的「Phase 5 用戶自製零件 R2 上傳」一併廢止。
|
|
||||||
|
|
||||||
- [x] 1.5.1 改 `submitComponent.ts`:移除 R2 寫入段落,保留 KV 寫入
|
|
||||||
- [x] 1.5.2 移除 `wrangler.toml` 的 `[[r2_buckets]] WASM_BUCKET` binding
|
|
||||||
- [x] 1.5.3 移除 `types.ts` Bindings 的 `WASM_BUCKET: R2Bucket`
|
|
||||||
- [x] 1.5.4 既有 `wasm_r2_key` 欄位保留為 deprecated(queryComponents 仍會讀 legacy record)
|
|
||||||
- [ ] 1.5.5 廢止 `arcrun-wasm` R2 bucket(30 天觀察期後 → 2026-06-07 之後 `wrangler r2 bucket delete`)
|
|
||||||
- [x] 1.5.6 部署 + smoke test:search 端對端通過(kbdb 找到 3 個零件)
|
|
||||||
|
|
||||||
## Phase 2:部署即註冊(1-2 天)
|
|
||||||
|
|
||||||
- [x] 2.1 選擇方案:CI step(github actions)— 在 wrangler deploy 之後 curl `/index-only`
|
|
||||||
- [x] 2.2 寫 `registry/scripts/register-component.sh`(本地 + CI 共用 SSOT,python3 + pyyaml 解 YAML,curl POST registry)
|
|
||||||
- [x] 2.3 改 `.github/workflows/deploy.yml` tier1 deploy step 後加 "Register component in registry" step(degraded mode:失敗只 warning)
|
|
||||||
- [x] 2.4 本地驗 `bash scripts/register-component.sh kbdb_ingest` → 200 + already_indexed
|
|
||||||
- [ ] 2.5 真正 push 一個新零件驗 CI hook 端對端(需要等下次新增零件時驗)
|
|
||||||
- [ ] 2.6 文件化:`docs/contributing-components.md`「新增零件的標準流程」
|
|
||||||
- [ ] 2.7 廢止 `u6u_publish_component` tool 的「需手動 publish」假設(rebrand 一起做)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Phase 3:Discoverability(半天)
|
|
||||||
|
|
||||||
- [ ] 3.1 改 GitHub `richblack/arcrun` README
|
|
||||||
- 移除「21 個零件」這種寫死數字
|
|
||||||
- 加「跑 `acr parts list` 或 MCP search 看當前清單」
|
|
||||||
- 加 badge:``
|
|
||||||
- [ ] 3.2 加 `matrix/arcrun/registry/src/routes/badge.ts`
|
|
||||||
- GET `/badge/components.svg` 回 shields.io 格式 SVG
|
|
||||||
- count 從 KV 即時 query
|
|
||||||
- cache 1 分鐘(`Cache-Control: max-age=60`)
|
|
||||||
- [ ] 3.3 改 MCP `u6u_get_component_guide` tool(之後改名 `arcrun_*`)
|
|
||||||
- 開頭加「鐵律:動工前必須先 search_components,找不到才 publish」
|
|
||||||
- [ ] 3.4 onboarding kit GitHub template repo(建議名 `arcrun-quickstart`)
|
|
||||||
- 三件套:CLAUDE.md / `.cursor/rules/arcrun.mdc` / AGENTS.md
|
|
||||||
- 強制:「呼叫 Claude/任何 AI 前,先 list MCP tools;arcrun MCP 已連線時,**禁止用 Python 直打 HTTP API**」
|
|
||||||
- 內附範例 hello workflow 跟 component
|
|
||||||
- [ ] 3.5 寫 onboarding doc:`docs/onboarding-third-party-engineer.md`
|
|
||||||
- 第三方工程師如何 30 秒內讓 AI 學會用 arcrun
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Phase 4:u6u → arcrun Rebrand(1 天,最後做)
|
|
||||||
|
|
||||||
> 規劃做完 Phase 1-3 驗證 OK 才動 rebrand,避免邊改邊驗。
|
|
||||||
|
|
||||||
### 4.1 Repo & Worker
|
|
||||||
- [ ] 4.1.1 `git mv matrix/u6u-mcp matrix/arcrun-mcp`(或 cp + rm,視 git history 偏好)
|
|
||||||
- [ ] 4.1.2 改 `matrix/arcrun-mcp/wrangler.toml`:
|
|
||||||
- `name = "u6u-mcp"` → `name = "arcrun-mcp"`
|
|
||||||
- 加 route `mcp.arcrun.dev/*`,舊 `studio.finally.click/mcp*` 保留 1 個月
|
|
||||||
- [ ] 4.1.3 改 `package.json`:`@inkstone/u6u-mcp-worker` → `@arcrun/mcp-worker`
|
|
||||||
|
|
||||||
### 4.2 Tool 前綴改名
|
|
||||||
- [ ] 4.2.1 14 個 tool 檔案 rename:`u6u_*.ts` → `arcrun_*.ts`
|
|
||||||
- [ ] 4.2.2 每個 tool 內部 `server.tool("u6u_xxx", ...)` 改 `server.tool("arcrun_xxx", ...)`
|
|
||||||
- [ ] 4.2.3 `src/tools/registry.ts` import 路徑全改
|
|
||||||
- [ ] 4.2.4 `src/index.ts` `serverInfo.name` 從 `u6u-mcp-server` 改 `arcrun-mcp-server`
|
|
||||||
|
|
||||||
### 4.3 文件
|
|
||||||
- [ ] 4.3.1 README.md 全文 u6u → arcrun
|
|
||||||
- [ ] 4.3.2 GUIDE.md 同上
|
|
||||||
- [ ] 4.3.3 GitHub `richblack/arcrun` README 補 MCP 段落(之前沒提)
|
|
||||||
- [ ] 4.3.4 任何提到 `u6u-mcp` / `mcp.finally.click` 的 docs 更新
|
|
||||||
|
|
||||||
### 4.4 用戶記憶
|
|
||||||
- [ ] 4.4.1 `~/.claude/projects/.../memory/MEMORY.md` 加 arcrun MCP entry
|
|
||||||
- URL: `https://mcp.arcrun.dev/mcp/mcp`
|
|
||||||
- tool 前綴: `arcrun_*`
|
|
||||||
- finally.click 過渡期到何時
|
|
||||||
- [ ] 4.4.2 polaris/mira/CLAUDE.md 提到 daemon / arcrun / MCP 的部分對齊新命名
|
|
||||||
|
|
||||||
### 4.5 過渡期(舊 endpoint 不立刻砍)
|
|
||||||
- [ ] 4.5.1 舊 `mcp.finally.click/mcp/mcp` 加回應 header `Deprecation: true` + `Link: <https://mcp.arcrun.dev/mcp/mcp>; rel="successor-version"`
|
|
||||||
- [ ] 4.5.2 舊 worker 繼續服務 30 天(2026-06-07 為止)
|
|
||||||
- [ ] 4.5.3 廢止排程:2026-06-07 後舊 worker 改回 410 Gone + 提示換新 URL
|
|
||||||
|
|
||||||
### 4.6 驗證
|
|
||||||
- [ ] 4.6.1 `mcp.arcrun.dev/mcp/mcp` initialize + tools/list + 一個 tool call 全通
|
|
||||||
- [ ] 4.6.2 我的 Claude Code config 切到新 URL,用 `mcp__arcrun__search_components` 端對端測
|
|
||||||
- [ ] 4.6.3 grep `u6u` 在 `matrix/arcrun-mcp/` 結果為 0(除了 changelog 紀錄)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 風險追蹤
|
|
||||||
|
|
||||||
- 風險 1:backfill 跑進去發現某些 contract.yaml 格式跟 registry 期望不一樣 → 緩解:dry-run 先看,必要時補 contract 欄位
|
|
||||||
- 風險 2:Phase 4 rebrand 期間用戶 client 設定亂 → 緩解:過渡期 + Deprecation header
|
|
||||||
- 風險 3:自動註冊 hook 失敗導致部署被擋 → 緩解:degraded mode(warning 不 fail)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Known Issues(吃狗糧發現的,先記錄)
|
|
||||||
|
|
||||||
### KI-1:u6u-mcp README URL 寫錯
|
|
||||||
- README 寫 `mcp.finally.click/mcp`,實際是 `mcp.finally.click/mcp/mcp`(basePath + route)
|
|
||||||
- 影響:用戶照 README 裝完試打不通
|
|
||||||
- 解法:rebrand 時順便修
|
|
||||||
|
|
||||||
### KI-2:inkstone-component-registry 跟 arcrun-registry 並存
|
|
||||||
- 兩個 worker 都活著,u6u-mcp 之前指錯
|
|
||||||
- inkstone-component-registry 是舊版(2026-03-24)、arcrun-registry 是現役(2026-04-16)
|
|
||||||
- 解法:Phase 1 backfill 完成後,inkstone-component-registry worker 廢止
|
|
||||||
|
|
||||||
### KI-3:search 對自然語言不夠靈敏(吃狗糧第一個發現)
|
|
||||||
- 現象:
|
|
||||||
- `search("從 KBDB 讀取或查詢 block")` → 0 結果
|
|
||||||
- `search("kbdb")` → 3 結果(kbdb_ingest / kbdb_patch_block / kbdb_create_block)
|
|
||||||
- 根因:搜尋走 embedding(bge-m3)相似度,但既有零件清單少(30 個)+ description 寫得正式,自然語言整句的 embedding 跟 description 距離太遠
|
|
||||||
- 影響:**致命** — AI 第一句永遠是自然語言整句,回 0 就會放棄 search 改寫 Python
|
|
||||||
- 解法(Phase 3 處理):
|
|
||||||
1. embedding search 之外加 keyword fallback(split query → 對 canonical_id / display_name / tags 做 ILIKE)
|
|
||||||
2. 或 lower threshold(目前 SCORE_THRESHOLD = 0.5,可能過高)
|
|
||||||
3. MCP get_component_guide 教 AI 「找不到時拆關鍵字再 search」
|
|
||||||
- 優先級:P1(會擋推廣)
|
|
||||||
@@ -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 層(防在後,真的暴露前)
|
|
||||||
暴露動作打到 server(webhook 部署 endpoint / 未來公共貢獻 endpoint)時,server 要求帶
|
|
||||||
「人類已確認暴露」的明示旗標,沒帶 → 拒絕 + 提示。與 component-gatekeeping 的 human_confirmation 同模式:
|
|
||||||
```ts
|
|
||||||
exposure_confirmation?: {
|
|
||||||
confirmed_by_human: true;
|
|
||||||
understood_exposure: string; // 人類說明「我知道這會把什麼開放給誰」
|
|
||||||
confirmed_at: string;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
缺 → 拒絕,訊息:「部署 webhook = 開一個對外可呼叫的 endpoint。確認你知道這會暴露 [workflow 名]
|
|
||||||
的能力/輸出?用 `acr push --confirm-exposure` 並說明。」
|
|
||||||
|
|
||||||
### 為何兩道都要
|
|
||||||
- 只有 hook:AI 可繞過 CLI 直接打 server API → API 層補。
|
|
||||||
- 只有 API 層:AI 已經寫好暴露的東西才在送出時被擋,浪費 + 用戶較晚看到 → hook 提前。
|
|
||||||
- 同 component-gatekeeping 的雙層精神。
|
|
||||||
|
|
||||||
## 2. 警示要讓人看得懂(R1)
|
|
||||||
|
|
||||||
不是「確認嗎 Y/N」,是說清楚風險:
|
|
||||||
```
|
|
||||||
⚠️ 這個動作會把 workflow "contacts_lookup" 變成可被外部呼叫的 endpoint:
|
|
||||||
https://cypher.arcrun.dev/webhooks/named/contacts_lookup/trigger
|
|
||||||
|
|
||||||
任何持有觸發憑證的人都能呼叫它、取得它的輸出。
|
|
||||||
這個 workflow 讀取:[盤出 workflow 用到的資料源,若可得]
|
|
||||||
需要保護(要求呼叫者認證)嗎?目前的觸發認證是:[現況]
|
|
||||||
|
|
||||||
確認部署?(需人類明示)
|
|
||||||
```
|
|
||||||
- 「workflow 讀取什麼資料源」:盡力從 workflow 定義盤(用了哪些 recipe / endpoint),盤不出就標「無法自動判斷,請自行確認」。誠實。
|
|
||||||
|
|
||||||
## 3. known-destinations / 不重複問(R4 避免盲目按 yes)
|
|
||||||
|
|
||||||
觸發策略已經很窄(只暴露動作),但同一個 workflow 重複部署不該每次問。
|
|
||||||
- 首次部署某 workflow 為 webhook → 問。人類確認後記住(該 workflow 標記 exposure-confirmed)。
|
|
||||||
- 之後同 workflow 更新 → 不重問(除非暴露面變大,如新增對外觸發)。
|
|
||||||
- 記在哪:workflow metadata(WEBHOOKS KV 的 record 加 `exposure_confirmed_at`)。
|
|
||||||
|
|
||||||
## 4. 與既有一致(R5)
|
|
||||||
- 同 component-gatekeeping:AI 不可替人類決定有外洩風險的動作;誠實限制(AI 能偽造 confirmed_by_human,靠軌跡可審 + mindset)。
|
|
||||||
- 同「arcrun 不做授權判斷」:不判斷「該不該暴露」,只「攔下來讓人類明示同意」。不禁止暴露,要明示同意。
|
|
||||||
|
|
||||||
## 5. 範圍邊界
|
|
||||||
- **動**:webhook 部署路徑(webhooks-named.ts)的 exposure_confirmation + CLI `acr push --confirm-exposure` 互動 + hook。
|
|
||||||
- **不動**:用戶 API 入站保護機制(發 key/權限/限流,另列 BACKLOG);recipe 私人 push(不暴露,不擋);出站 http 節點(不擋)。
|
|
||||||
- recipe 公共貢獻路徑未實作 → 本系統只要求它**未來**內建 exposure 閘門(記進那條 BACKLOG 項)。
|
|
||||||
|
|
||||||
## 6. 決議(richblack 2026-05-30 design review)
|
|
||||||
|
|
||||||
- **Q1 → recipe push 也警示,公私一視同仁。** 不是因為 recipe 本身暴露,而是統一原則「凡有資料去向/暴露面的動作都警示」。用戶可選「以後不要警示」(記偏好)。理由見 §7 同意 log。
|
|
||||||
- **Q2 → webhook 部署要警示,但角度是「提醒 + 提供保護」不是「擋」。** 用不用認證是用戶決定(如美國氣象 API 本就無 key 公開)。我們警示時**順便提醒「可用 arcrun 提供的保護措施」**(接「用戶 API 保護機制」資安優勢,BACKLOG 待決策)。首次問、記住(§3)。
|
|
||||||
- **Q3 → hook 偵測 `acr push` 指令**(簡單版,pre-bash 攔指令)。
|
|
||||||
|
|
||||||
## 7. 同意 = 法律憑證(richblack 2026-05-30,重要)
|
|
||||||
|
|
||||||
每次人類同意「暴露/送資料」的動作,**留 log(誰、何時、同意了什麼)**。這不只是「軌跡可審」,是**法律保護**:
|
|
||||||
|
|
||||||
- 真發生資料外洩 / 糾紛時,有「用戶在 [時間] 明示知情同意把 [什麼] 暴露給 [誰]」的證據 → 避免訴訟風險(責任在明示同意的用戶,不在 arcrun)。
|
|
||||||
- 「以後不要警示」這個選擇**本身也要 log**(用戶在 [時間] 選擇了不再對 [X] 警示 = 他知道風險並接受)。
|
|
||||||
- 同意 log 存放:與動作關聯(webhook record / recipe record 的 `exposure_consent: { confirmed_by, understood, confirmed_at, suppress_future }`)。
|
|
||||||
- 誠實限制同前:AI 能偽造 confirmed_by_human。但**法律意義上,憑證存在 + 可審 = 用戶有機會知情**,這道防線的價值是法律歸責不是技術防偽。
|
|
||||||
|
|
||||||
→ 這把 §1b 的 `exposure_confirmation` 升級為**帶法律意義的同意憑證**,所有暴露/送資料動作(recipe push / webhook 部署)共用此機制。
|
|
||||||
|
|
||||||
## 8. 警示是「保護措施的入口」(不只是攔)
|
|
||||||
|
|
||||||
警示訊息除了說明風險,**主動提供 arcrun 的保護措施**(產品價值,非只防呆):
|
|
||||||
```
|
|
||||||
⚠️ 這個動作會把 [X] 開放/送出。
|
|
||||||
arcrun 可以幫你保護它:
|
|
||||||
- 要求呼叫者帶 API Key(你發給特定對象)
|
|
||||||
- 設定權限 / 限流
|
|
||||||
一個動作就能加上。要加保護嗎?還是確認公開(如公開資料 API)?
|
|
||||||
```
|
|
||||||
(具體保護機制是 BACKLOG「用戶 API 保護機制」待決策項——本系統先在警示處**提示它存在**,實作後接上。)
|
|
||||||
@@ -1,56 +0,0 @@
|
|||||||
# Requirements: 資料外流警示(Data Exfiltration Warning)
|
|
||||||
|
|
||||||
> 2026-05-30 建立(richblack 確認)。資安優先、及早做。
|
|
||||||
> 判準源:DECISIONS §7(讓 AI 不做歪 + 閉環)、§0(減少不可控依賴 / 風險);BACKLOG 步驟 5b。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 背景與風險
|
|
||||||
|
|
||||||
arcrun 讓「產生 API / 把資料送出去」變很簡單(一堆資料 + webhook trigger = API;recipe = 打某 endpoint)。
|
|
||||||
**這個「簡單」本身是風險**:簡單到 AI 可能在用戶不知情下,把含個資的東西變成可被呼叫的 endpoint,或把
|
|
||||||
敏感資料送到非預期對象。
|
|
||||||
|
|
||||||
**richblack 的核心情境**:用戶有個 Google Sheets 存所有朋友的個資。正確用法是「call 它查詢」(自用)。
|
|
||||||
但若 AI 把它變成一個 recipe / workflow,**送資料到非預期對象**(公開 webhook、公司群、外部 endpoint),
|
|
||||||
且**沒做認證** → 所有資料裸奔。
|
|
||||||
|
|
||||||
**關鍵原則(richblack 2026-05-30):**
|
|
||||||
1. **不分公私庫都要警示**。私人庫(公司用)一樣會出事(不小心把個資 POST 到公司群)。觸發點不是「推公共」,
|
|
||||||
是「這動作會讓資料流向某處」。
|
|
||||||
2. **不禁止**用戶公開 / 送資料(他要放什麼給誰是他的自由)——**但要確定他自己明示同意了**,不是 AI 替他決定。
|
|
||||||
3. **兩道防線**:(a) hook 在 AI 動手前警告(防在前);(b) API 層在資料真送出前攔(防在後)。不論哪條路都攔。
|
|
||||||
|
|
||||||
## 需求
|
|
||||||
|
|
||||||
### R1 — API 層警示(資料送出前需人類同意,不分公私庫)
|
|
||||||
任何「把資料送往某目的地」的 API 動作,在執行/儲存前需人類明示同意。涵蓋(待 design 盤準):
|
|
||||||
- `acr recipe push`:recipe 定義一個 endpoint(資料去向)。
|
|
||||||
- `acr push`(workflow):workflow 可能含「讀敏感源 → 送往某 endpoint」的節點。
|
|
||||||
- webhook trigger 部署:把 workflow 變成可被呼叫的 API。
|
|
||||||
- 未來「recipe 貢獻公共庫」路徑(BACKLOG 飛輪項)。
|
|
||||||
|
|
||||||
同意內容要讓人看得懂風險:**這個動作會把 [什麼資料] 送到 [哪個目的地],需不需要 credential 保護?確認?**
|
|
||||||
|
|
||||||
### R2 — hook 在 AI 動手前警告
|
|
||||||
AI 要做這類動作(寫含外部 endpoint 的 recipe/workflow、跑會送資料的指令)前,hook 先警告 + 要人類確認。
|
|
||||||
這是「防在前」——在 AI 本機動手當下就提醒,不等到 API 層才攔(省一趟、且更早讓人看到)。
|
|
||||||
|
|
||||||
### R3 — 誠實的偵測範圍(不假裝能全防)
|
|
||||||
「敏感資料送到非預期對象」機械上難完整偵測。本系統做**可機械判斷的部分**,誠實標明擋不住的:
|
|
||||||
- 能偵測:動作含「外部 endpoint」(recipe endpoint / workflow http 節點 / webhook 對外)。
|
|
||||||
- 難偵測:「這份資料是否敏感」「這個目的地是否非預期」——這需要語境,機器判不準。
|
|
||||||
- 做法:偵測到「資料 → 外部目的地」的動作就警示,由**人類**判斷該資料/目的地是否該放行(人類同意是判斷點,機器只負責「攔下來問」)。
|
|
||||||
|
|
||||||
### R4 — 不擋自用、不製造過度摩擦
|
|
||||||
- 純自用、不送資料出去的動作(acr run 本機、查詢類)不該被警示淹沒。
|
|
||||||
- 警示要精準在「資料外流」動作,不是每個動作都問(否則用戶會盲目按 yes,警示失效)。
|
|
||||||
|
|
||||||
### R5 — 與既有閘門一致
|
|
||||||
- 與「建零件人類閘門」(component-gatekeeping)同精神:AI 不可替人類決定有外洩風險的動作。
|
|
||||||
- 與「arcrun 不做授權判斷」一致:不判斷「該不該送」,只「攔下來讓人類明示同意」。
|
|
||||||
|
|
||||||
## 非目標
|
|
||||||
- 用戶自己的 API 保護機制(入站認證:發 key 給別人 / 權限 / 限流)—— 另列 BACKLOG 待決策。
|
|
||||||
- 完整 DLP(資料外洩防護)系統 / 內容掃描判斷敏感度 —— 機器判不準,不做。
|
|
||||||
- recipe 公共貢獻路徑本身 —— 未實作(飛輪項),本系統只要求它未來內建同意閘門。
|
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
# Tasks: 資料外流警示
|
|
||||||
|
|
||||||
> 對應 design.md(richblack 已 review,Q1-Q3 + 法律憑證 + 保護入口決議)。
|
|
||||||
> 每完成一個 task 立刻標 [x],不批次。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 共用:同意憑證機制(§7 法律憑證)
|
|
||||||
- [x] C1 定義 `exposure_consent { confirmed_by, understood, confirmed_at, suppress_future }` 型別
|
|
||||||
- [x] C2 同意 log 寫入動作關聯的 record(webhook record / recipe record),可審
|
|
||||||
- [x] C3 「以後不要警示」(suppress_future)本身也 log(用戶知風險並接受)
|
|
||||||
|
|
||||||
## API 層警示(R1,防在後)
|
|
||||||
- [x] A1 webhook 部署(webhooks-named.ts POST)要 exposure_consent,缺且未 suppress → 拒絕 + 提示
|
|
||||||
- [x] A2 recipe push(/recipes POST)同上(公私一視同仁)
|
|
||||||
- [x] A3 首次暴露某資源問、記住(exposure_confirmed / suppress_future)→ 之後不重問(§3)
|
|
||||||
- [x] A4 警示訊息說明風險 + 盤資料源(盡力,盤不出標「請自行確認」)+ **提示 arcrun 保護措施**(§8)
|
|
||||||
|
|
||||||
## CLI 警示(互動 + 旗標)
|
|
||||||
- [x] B1 `acr push`:部署前互動式警示(首次某 workflow),人類確認組 exposure_consent 送出;`--confirm-exposure` 跳過互動(CI/非 TTY);`--suppress-warning` 記偏好
|
|
||||||
- [x] B2 `acr recipe push`:同上
|
|
||||||
- [x] B3 非 TTY(AI 直跑)無 --confirm-exposure → 拒絕並提示「需人類確認暴露」
|
|
||||||
|
|
||||||
## hook(R2,防在前,Q3=偵測指令)
|
|
||||||
- [x] H1 pre-bash-guard:偵測 `acr push` / `acr recipe push` → 警示「這會把 X 變可被外部呼叫,需人類確認」
|
|
||||||
|
|
||||||
## mindset / 文件
|
|
||||||
- [x] M1 誠實限制(AI 能偽造 confirmed_by_human,靠憑證可審 + 法律歸責)寫進 mindset Skill(步驟 7)+ 文件
|
|
||||||
|
|
||||||
## 驗收
|
|
||||||
- [ ] V1 acr push 部署 webhook(首次)→ 互動警示 + 說明暴露 + 提示保護 的終端輸出
|
|
||||||
- [x] V2 非 TTY 跑 acr push 無 --confirm-exposure → 拒絕的輸出
|
|
||||||
- [x] V3 webhook 部署 API 無 exposure_consent → 拒絕的輸出
|
|
||||||
- [x] V4 同一 workflow 二次部署 → 不重問(已記住)
|
|
||||||
- [ ] V5 --suppress-warning 後 → 不再警示,但 suppress 選擇有 log
|
|
||||||
- [x] V6 同意後 → exposure_consent 寫進 record 可查(法律憑證)
|
|
||||||
|
|
||||||
## 範圍邊界
|
|
||||||
- 不動用戶 API 入站保護機制(發 key/權限/限流)—— BACKLOG 待決策,本系統只在警示處「提示它存在」。
|
|
||||||
- 不擋出站 http 節點(不暴露我的資料)、不擋 acr run(本機)。
|
|
||||||
@@ -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 tool(AI 直接 call) |
|
|
||||||
|
|
||||||
**原則**:
|
|
||||||
1. **discoverable** — AI 不靠 grep / 不靠人就能知道有什麼
|
|
||||||
2. **idempotent** — 同樣輸入兩次結果一樣,可預測
|
|
||||||
3. **dry-run by default** — preview 是預設、commit 是 explicit
|
|
||||||
4. **structured errors** — error 是 JSON 含 next_actions,不是字串
|
|
||||||
5. **closed loop** — AI 卡住的 data 自動回流,平台 self-improving
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 2. 系統架構
|
|
||||||
|
|
||||||
```
|
|
||||||
┌────────────────────────────────────────────────────────────────┐
|
|
||||||
│ AI agent (Claude / Cursor / ...) │
|
|
||||||
└──────────────────┬──────────────────────────────┬───────────────┘
|
|
||||||
│ │
|
|
||||||
│ ① Read AGENTS.md │ ② MCP tool calls
|
|
||||||
▼ ▼
|
|
||||||
┌────────────────────────┐ ┌──────────────────────────────────┐
|
|
||||||
│ AGENTS.md │ │ arcrun-mcp (擴張 arcrun-mcp) │
|
|
||||||
│ (repo + KBDB block) │ │ 25 tools, 5 categories │
|
|
||||||
└────────────────────────┘ └──────────────┬───────────────────┘
|
|
||||||
│
|
|
||||||
┌────────┬─────────┬────────┴────────┬──────────┐
|
|
||||||
▼ ▼ ▼ ▼ ▼
|
|
||||||
┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌──────────┐
|
|
||||||
│ cypher- │ │registry │ │ kbdb │ │ Skills │ │ Examples │
|
|
||||||
│executor │ │ │ │ │ │ (KBDB) │ │ (git+KV) │
|
|
||||||
│ 31 路由 │ │ │ │ 50 路由 │ │ │ │ │
|
|
||||||
└─────────┘ └─────────┘ └─────────┘ └─────────┘ └──────────┘
|
|
||||||
▲
|
|
||||||
│ ③ AI 部署完 / 卡住 call feedback
|
|
||||||
│
|
|
||||||
┌─────────────┴──────────────┐
|
|
||||||
│ report_feedback / agent-telemetry │
|
|
||||||
│ (寫進 KBDB) │
|
|
||||||
└─────────────┬──────────────┘
|
|
||||||
│ ④ 每週聚合
|
|
||||||
▼
|
|
||||||
┌──────────────────────────────┐
|
|
||||||
│ agent_feedback_weekly_review │
|
|
||||||
│ (arcrun workflow, dogfood) │
|
|
||||||
│ → arcrun-roadmap KBDB block │
|
|
||||||
└──────────────────────────────┘
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 3. 五層 LI 模型
|
|
||||||
|
|
||||||
### Layer 0:Onboarding(AGENTS.md)
|
|
||||||
|
|
||||||
**位置**:
|
|
||||||
- `arcrun/AGENTS.md`(repo 根,git 可讀)
|
|
||||||
- KBDB `type=agent-onboarding` block(MCP `get_onboarding_doc()` 拉)
|
|
||||||
|
|
||||||
**結構(控 5-8K tokens)**:
|
|
||||||
1. **30 字 What** — 「arcrun 是用 YAML 把零件串成 workflow 的平台」
|
|
||||||
2. **5 個核心概念** — component / cypher binding YAML / FOREACH / paused-resume / api_key
|
|
||||||
3. **連線**:「在你的 MCP config 加 `https://mcp.arcrun.dev/mcp`,bearer `ak_xxx`」
|
|
||||||
4. **第一個 workflow(minimal example)** — 10 行 YAML,含部署 + 觸發
|
|
||||||
5. **5 個 URL 慣例與為什麼**(workers.dev vs arcrun.dev / KBDB endpoints)
|
|
||||||
6. **常見錯誤 5 個 + 怎麼讀**
|
|
||||||
7. **不確定的時候**:明確列「先 call list_X 再下手」
|
|
||||||
8. **回報機制**:完成任何 workflow deploy / 卡住,**必須** call `report_feedback`
|
|
||||||
|
|
||||||
**部署機制**:
|
|
||||||
- repo `arcrun/AGENTS.md` 是 source of truth
|
|
||||||
- GH Actions 監聽該檔變動 → 自動 PATCH KBDB onboarding block
|
|
||||||
- MCP `get_onboarding_doc()` 從 KBDB 讀(最新)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Layer 1:arcrun-mcp(主戰場)
|
|
||||||
|
|
||||||
#### 1.1 命名與部署
|
|
||||||
|
|
||||||
- **Server URL**: `https://mcp.arcrun.dev/mcp`(唯一 URL)
|
|
||||||
- **舊 URL**:`mcp.finally.click` 直接退場(leo 2026-05-16 拍板,理由:「finally.click 是還沒申請新網址的暫用,那是一個服務,arcrun 是底層」)
|
|
||||||
- **Worker rename**:`u6u-mcp` → `arcrun-mcp`(wrangler name 改),repo 路徑 `matrix/u6u-mcp/` → `matrix/arcrun-mcp/`
|
|
||||||
- **Tool prefix**: 統一 `arcrun_*`(單一 rename,**不保留 `u6u_*` alias**,不留 deprecation 期。用戶量還很少,一次切換可接受。切換前在 mira / telegram 公告)
|
|
||||||
|
|
||||||
#### 1.2 Tool 完整清單(25 個,5 類)
|
|
||||||
|
|
||||||
##### A. Onboarding & Discovery(5 tools)
|
|
||||||
|
|
||||||
| Tool | 對應 HTTP | 用途 |
|
|
||||||
|---|---|---|
|
|
||||||
| `arcrun_get_onboarding` | KBDB GET `/blocks?type=agent-onboarding` | 拉 AGENTS.md 內容 |
|
|
||||||
| `arcrun_list_components` | registry GET `/components` | 所有可用零件 + 1 行 desc |
|
|
||||||
| `arcrun_get_component_contract` | registry GET `/components/:id` | input/output schema + gherkin tests |
|
|
||||||
| `arcrun_list_recipes` | cypher GET `/recipes` + `/auth-recipes` | API recipe 一覽 |
|
|
||||||
| `arcrun_search_examples` | KBDB POST `/search`(type=workflow-example)| 用 use case 搜範例 |
|
|
||||||
|
|
||||||
##### B. Workflow CRUD(7 tools)
|
|
||||||
|
|
||||||
| Tool | 對應 HTTP | 用途 |
|
|
||||||
|---|---|---|
|
|
||||||
| `arcrun_list_workflows` | cypher GET `/webhooks/named` | 用戶現有 workflow |
|
|
||||||
| `arcrun_get_workflow` | cypher GET `/webhooks/named/:name` | 拿 YAML / graph |
|
|
||||||
| `arcrun_validate_yaml` | cypher POST `/validate` | dry-run 校驗,不部署 |
|
|
||||||
| `arcrun_push_workflow` | cypher POST `/webhooks/named` | 部署(也含 validate) |
|
|
||||||
| `arcrun_delete_workflow` | cypher DELETE `/webhooks/named/:name` | 刪 |
|
|
||||||
| `arcrun_preview_workflow` | **新增** POST `/preview` | 不寫 KV、模擬 execute |
|
|
||||||
| `arcrun_diff_workflow` | **新增** POST `/webhooks/named/:name/diff` | 既有 vs 新 YAML 差異 |
|
|
||||||
|
|
||||||
##### C. Execution & Trace(5 tools)
|
|
||||||
|
|
||||||
| Tool | 對應 HTTP | 用途 |
|
|
||||||
|---|---|---|
|
|
||||||
| `arcrun_run_workflow` | cypher POST `/webhooks/named/:name/trigger` | 觸發 + 回 execution_id |
|
|
||||||
| `arcrun_get_execution_trace` | **新增** GET `/executions/:id` | 結構化 trace(per-node status) |
|
|
||||||
| `arcrun_list_recent_executions` | **新增** GET `/workflows/:name/executions?limit=10` | 最近 N 次 |
|
|
||||||
| `arcrun_list_paused_executions` | **新增** GET `/executions/paused` | 卡 callback 的 |
|
|
||||||
| `arcrun_resume_execution` | cypher POST `/workflows/resume` | 手動 resume |
|
|
||||||
|
|
||||||
##### D. Component & Recipe Management(4 tools)
|
|
||||||
|
|
||||||
| Tool | 對應 HTTP | 用途 |
|
|
||||||
|---|---|---|
|
|
||||||
| `arcrun_search_components` | registry POST search | 語意搜尋 |
|
|
||||||
| `arcrun_publish_component` | registry POST publish | 上 WASM |
|
|
||||||
| `arcrun_create_recipe` | cypher POST `/recipes` | 建 API recipe |
|
|
||||||
| `arcrun_create_auth_recipe` | cypher POST `/auth-recipes` | 建 auth recipe |
|
|
||||||
|
|
||||||
##### E. Feedback & Skills(4 tools)
|
|
||||||
|
|
||||||
| Tool | 對應 HTTP | 用途 |
|
|
||||||
|---|---|---|
|
|
||||||
| `arcrun_report_feedback` | KBDB POST `/blocks`(type=agent-feedback)| AI 主動回報 |
|
|
||||||
| `arcrun_list_skills` | KBDB GET `/blocks?type=agent-skill` | 列可用 playbook |
|
|
||||||
| `arcrun_get_skill` | KBDB GET `/blocks/:id` | 拿 skill 內容 |
|
|
||||||
| `arcrun_publish_skill` | KBDB POST `/blocks`(type=agent-skill)| AI 把學到的存回去 |
|
|
||||||
|
|
||||||
#### 1.3 Tool contract 統一規範
|
|
||||||
|
|
||||||
每個 tool 都遵守:
|
|
||||||
|
|
||||||
**Input**:
|
|
||||||
- Zod schema declarative
|
|
||||||
- 每個參數有 `.describe('...')`(給 AI 看的)
|
|
||||||
- optional 標 default
|
|
||||||
|
|
||||||
**Output(成功)**:
|
|
||||||
```typescript
|
|
||||||
{
|
|
||||||
ok: true,
|
|
||||||
data: T,
|
|
||||||
hints?: string[], // optional next-step suggestion,e.g.「你剛部署了,可 call run_workflow 測試」
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Output(失敗)**:
|
|
||||||
```typescript
|
|
||||||
{
|
|
||||||
ok: false,
|
|
||||||
error_code: 'enum_value',
|
|
||||||
human_message: string,
|
|
||||||
next_actions: string[], // 可程式化的下一步
|
|
||||||
detail?: unknown, // 原始錯誤(debug)
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 1.4 error_code enum(v1,可加不可刪)
|
|
||||||
|
|
||||||
| error_code | 對應狀況 |
|
|
||||||
|---|---|
|
|
||||||
| `auth_missing` | api_key 沒帶 |
|
|
||||||
| `auth_invalid` | api_key 無效 |
|
|
||||||
| `not_found` | workflow / block 不存在 |
|
|
||||||
| `validation_failed` | YAML / schema 不過 |
|
|
||||||
| `component_not_found` | 零件 ID 沒對應 |
|
|
||||||
| `component_not_in_whitelist` | 零件存在但 cypher-executor 不認 |
|
|
||||||
| `paused_awaiting_resume` | workflow 在等 callback |
|
|
||||||
| `rate_limited` | 太頻繁 |
|
|
||||||
| `internal_error` | 平台 bug(含 detail) |
|
|
||||||
| `dependency_unavailable` | 下游服務(claude / mira daemon / KBDB)掛 |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Layer 2:Skill blocks(KBDB-native playbook)
|
|
||||||
|
|
||||||
#### 2.1 Schema
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
type AgentSkill = {
|
|
||||||
id: string;
|
|
||||||
type: 'agent-skill';
|
|
||||||
page_name: `skill-${slug}`;
|
|
||||||
content: string; // markdown playbook
|
|
||||||
tags_json: string[]; // ['watcher', 'debug', 'rag', ...]
|
|
||||||
source: 'manual' | 'auto-extracted';
|
|
||||||
metadata_json: {
|
|
||||||
when_to_use: string;
|
|
||||||
example_use_case: string;
|
|
||||||
contributed_by?: string; // agent user_agent
|
|
||||||
success_count?: number;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 2.2 種子 skill(先建 5 個,以 mira 經驗為本)
|
|
||||||
|
|
||||||
| skill page_name | 用途 |
|
|
||||||
|---|---|
|
|
||||||
| `skill-build_watcher_workflow` | cron 掃資料 → 觸發處理(mira_feed_watcher pattern) |
|
|
||||||
| `skill-debug_paused_workflow` | claude_api / 任何 async callback paused 怎麼追 |
|
|
||||||
| `skill-migrate_http_to_trigger_workflow` | self-fetch 換 trigger_workflow(剛踩過的) |
|
|
||||||
| `skill-rag_with_arcrun` | 用 KBDB + claude_api 做 RAG |
|
|
||||||
| `skill-add_new_wasm_component` | 從 TinyGo 寫到 deploy 全流程 |
|
|
||||||
|
|
||||||
#### 2.3 自動萃取機制(後期)
|
|
||||||
|
|
||||||
`agent_feedback_weekly_review` 跑出來的 Top patterns,LLM 包成 skill draft,leo review 後 publish。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Layer 3:Examples(可搜尋的範例庫)
|
|
||||||
|
|
||||||
#### 3.1 存放
|
|
||||||
|
|
||||||
- `arcrun/registry/examples/{slug}/`
|
|
||||||
- `workflow.yaml`
|
|
||||||
- `description.md`(解決什麼問題、怎麼 trigger、預期結果)
|
|
||||||
- `tags.json`(`["webhook", "llm", "cron", ...]`)
|
|
||||||
- CI build 出 `examples-index.json` 推 KBDB(type=workflow-example,內容是 YAML + tags)
|
|
||||||
|
|
||||||
#### 3.2 搜尋
|
|
||||||
|
|
||||||
- `arcrun_search_examples(use_case)` 走 KBDB `/search`(既有 semantic)
|
|
||||||
- 命中 → 回 YAML + description + 「你可以基於這個改 X」hint
|
|
||||||
|
|
||||||
#### 3.3 第一批範例(10 個)
|
|
||||||
|
|
||||||
| slug | 用途 |
|
|
||||||
|---|---|
|
|
||||||
| `webhook-to-slack` | 簡單轉發 |
|
|
||||||
| `cron-watcher` | mira_feed_watcher 簡化版 |
|
|
||||||
| `llm-classify` | claude_api 分類 |
|
|
||||||
| `rag-search-answer` | 從 KBDB 找 context → 回答 |
|
|
||||||
| `email-summary` | gmail → claude → telegram |
|
|
||||||
| `pdf-to-blocks` | convert → ingest |
|
|
||||||
| `github-issue-bot` | webhook → claude → 留 comment |
|
|
||||||
| `daily-digest` | cron → 多源聚合 → 推送 |
|
|
||||||
| `parallel-fanout` | 一份輸入分發多 workflow |
|
|
||||||
| `error-retry` | try_catch + wait + retry |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Layer 4:Telemetry & Feedback Loop
|
|
||||||
|
|
||||||
#### 4.1 兩條線
|
|
||||||
|
|
||||||
```
|
|
||||||
explicit feedback (AI call) implicit telemetry (platform auto-log)
|
|
||||||
│ │
|
|
||||||
▼ ▼
|
|
||||||
KBDB block type= KBDB block type=
|
|
||||||
agent-feedback agent-telemetry
|
|
||||||
│ │
|
|
||||||
└──────────────┬───────────────────────┘
|
|
||||||
▼
|
|
||||||
agent_feedback_weekly_review
|
|
||||||
(arcrun workflow, 週一 9am cron)
|
|
||||||
▼
|
|
||||||
LLM 聚合 + 分類
|
|
||||||
▼
|
|
||||||
KBDB block type=arcrun-roadmap
|
|
||||||
(leo 收到 + 寫進 mira 河道)
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 4.2 agent-feedback schema
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
type AgentFeedback = {
|
|
||||||
type: 'agent-feedback';
|
|
||||||
content: string; // description 原文
|
|
||||||
source: 'mcp-tool-call';
|
|
||||||
user_id: string; // 用戶 namespace
|
|
||||||
metadata_json: {
|
|
||||||
workflow_name?: string;
|
|
||||||
issue_type: 'doc_unclear' | 'tool_missing' | 'error_unhelpful'
|
|
||||||
| 'unexpected_behavior' | 'feature_request' | 'success_story';
|
|
||||||
retry_count?: number;
|
|
||||||
blocked: boolean;
|
|
||||||
suggested_fix?: string;
|
|
||||||
agent_user_agent: string; // 'claude-code/1.x' etc
|
|
||||||
};
|
|
||||||
tags_json: ['agent-feedback', `issue:${issue_type}`];
|
|
||||||
};
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 4.3 agent-telemetry schema
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
type AgentTelemetry = {
|
|
||||||
type: 'agent-telemetry';
|
|
||||||
source: 'cypher-executor';
|
|
||||||
metadata_json: {
|
|
||||||
event_type: 'deploy_success' | 'deploy_fail' | 'run_success'
|
|
||||||
| 'run_fail' | 'validation_error' | 'mcp_tool_call';
|
|
||||||
workflow_name?: string;
|
|
||||||
component_id?: string;
|
|
||||||
error_code?: string;
|
|
||||||
duration_ms: number;
|
|
||||||
api_key_hash: string; // 雜湊,不存原值
|
|
||||||
agent_user_agent: string;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
```
|
|
||||||
|
|
||||||
寫入點:
|
|
||||||
- `webhook-handlers.executeWebhookGraph` 完成(成功 / 失敗都記)
|
|
||||||
- `routes/webhooks-named.ts` push(deploy 記)
|
|
||||||
- `routes/cypher.ts` validate 失敗(validation_error 記)
|
|
||||||
- arcrun-mcp 每個 tool call wrap 記(mcp_tool_call)
|
|
||||||
|
|
||||||
#### 4.4 隱私
|
|
||||||
|
|
||||||
- api_key 一律 SHA-256 截前 16 字元(不可逆,可聚合)
|
|
||||||
- workflow content 不 log(只記 name)
|
|
||||||
- 用戶看自己的 telemetry:MCP `arcrun_my_telemetry(limit)`,只回自己 hash
|
|
||||||
|
|
||||||
#### 4.5 weekly review workflow
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
name: agent_feedback_weekly_review
|
|
||||||
description: 每週一聚合 AI 回饋 + telemetry,產出 Top 痛點
|
|
||||||
|
|
||||||
flow:
|
|
||||||
- "weekly_cron >> ON_SUCCESS >> fetch_feedback"
|
|
||||||
- "fetch_feedback >> ON_SUCCESS >> fetch_telemetry"
|
|
||||||
- "fetch_telemetry >> ON_SUCCESS >> aggregate"
|
|
||||||
- "aggregate >> ON_SUCCESS >> llm_summarize"
|
|
||||||
- "llm_summarize >> ON_SUCCESS >> publish_roadmap_block"
|
|
||||||
- "publish_roadmap_block >> ON_SUCCESS >> notify_telegram"
|
|
||||||
|
|
||||||
config:
|
|
||||||
weekly_cron:
|
|
||||||
component: cron
|
|
||||||
cron_expr: "0 9 * * 1" # 週一 9 am UTC
|
|
||||||
fetch_feedback:
|
|
||||||
component: kbdb_get
|
|
||||||
type: 'agent-feedback'
|
|
||||||
created_after: "{{ now - 7d }}"
|
|
||||||
limit: 500
|
|
||||||
fetch_telemetry:
|
|
||||||
component: kbdb_get
|
|
||||||
type: 'agent-telemetry'
|
|
||||||
created_after: "{{ now - 7d }}"
|
|
||||||
limit: 5000
|
|
||||||
aggregate:
|
|
||||||
component: filter # 或寫個 `group_by` 邏輯零件
|
|
||||||
items: "{{fetch_feedback.blocks}}"
|
|
||||||
group_by: 'metadata_json.issue_type'
|
|
||||||
llm_summarize:
|
|
||||||
component: claude_api
|
|
||||||
prompt: |
|
|
||||||
你是 arcrun 平台的回饋分析師。下面是這週 AI agent 的所有回饋 +
|
|
||||||
telemetry 失敗事件。請:
|
|
||||||
1. 列 Top 5 痛點(含證據引用)
|
|
||||||
2. 為每個痛點建議具體改動(文件 / MCP tool / 錯誤訊息)
|
|
||||||
3. 評估嚴重程度(blocked AI vs 略不便)
|
|
||||||
|
|
||||||
Feedback:{{aggregate}}
|
|
||||||
Telemetry 失敗事件:{{fetch_telemetry.blocks}}
|
|
||||||
publish_roadmap_block:
|
|
||||||
component: kbdb_create_block
|
|
||||||
type: 'arcrun-roadmap'
|
|
||||||
page_name: 'roadmap-week-{{date.iso_week}}'
|
|
||||||
content: "{{llm_summarize.data.text}}"
|
|
||||||
notify_telegram:
|
|
||||||
component: telegram
|
|
||||||
chat_id: "{{ leo_chat_id }}"
|
|
||||||
text: "Arcrun 週報出爐:{{publish_roadmap_block.data.id}}"
|
|
||||||
```
|
|
||||||
|
|
||||||
**這個 workflow 是 dogfood inception**:arcrun 自己跑分析 arcrun 自己。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 4. Coverage Matrix(人類 vs AI)
|
|
||||||
|
|
||||||
> 這份表是 NFR-5「覆蓋率可量化」的具體實踐。每個 release 重新檢視。
|
|
||||||
|
|
||||||
### 4.1 人類 GUI 能做的事 × LI 對應
|
|
||||||
|
|
||||||
| 人類在 arcrun-gui 做的 | LI MCP 工具 | 對等? |
|
|
||||||
|---|---|---|
|
|
||||||
| 看現有 workflow 列表 | `arcrun_list_workflows` | ✅ |
|
|
||||||
| 點開 workflow 看 YAML | `arcrun_get_workflow` | ✅ |
|
|
||||||
| 編輯 workflow YAML | `arcrun_push_workflow`(含 update) | ✅ |
|
|
||||||
| 執行 workflow | `arcrun_run_workflow` | ✅ |
|
|
||||||
| 看執行結果 | `arcrun_get_execution_trace` | ✅(新增) |
|
|
||||||
| 翻零件庫 | `arcrun_list_components` + search | ✅ |
|
|
||||||
| 看零件 contract | `arcrun_get_component_contract` | ✅ |
|
|
||||||
| 上傳 WASM 零件 | `arcrun_publish_component` | ✅ |
|
|
||||||
| 設 credential | `arcrun_create_recipe` + (手動 push credential) | ⚠️ 部分(credential 提交需安全考量,AI 不全自動) |
|
|
||||||
| 翻歷史執行 | `arcrun_list_recent_executions` | ✅(新增) |
|
|
||||||
| 看 paused 工作流 | `arcrun_list_paused_executions` | ✅(新增) |
|
|
||||||
| 手動 resume | `arcrun_resume_execution` | ✅ |
|
|
||||||
| 視覺化 canvas 拖拉 | — | ❌(純視覺,LI 不複製) |
|
|
||||||
| 看排版 prototype | — | ❌(同上) |
|
|
||||||
| Flip UI/Logic view | — | ❌(同上) |
|
|
||||||
| Action log(操作軌跡) | `arcrun_my_telemetry` | ✅(含更多資料)|
|
|
||||||
|
|
||||||
**Gap**:3 個視覺類動作 LI 不需要對等;其他全等。
|
|
||||||
|
|
||||||
### 4.2 cypher-executor 31 路由 × LI 暴露
|
|
||||||
|
|
||||||
| 路由 | LI 暴露? | 工具 |
|
|
||||||
|---|---|---|
|
|
||||||
| `/health`、`/docs`、`/openapi.json` | ❌ infra | — |
|
|
||||||
| `/execute` | ✅ | `arcrun_run_workflow`(內部) |
|
|
||||||
| `/cypher/search`、`/cypher/execute` | ✅ | discovery 內部用 |
|
|
||||||
| `/workflows/resume` | ✅ | `arcrun_resume_execution` |
|
|
||||||
| `/webhooks/named*` | ✅ | CRUD 對應 5 個 tools |
|
|
||||||
| `/webhooks/*` (anonymous) | ❌ deprecated | — |
|
|
||||||
| `/credentials*` | ⚠️ | 只 list + delete,POST 走人類流程(安全考量) |
|
|
||||||
| `/recipes*`、`/auth-recipes*` | ✅ | 4 個 tools |
|
|
||||||
| `/validate` | ✅ | `arcrun_validate_yaml` |
|
|
||||||
| `/auth/*`、`/register`、`/me*` | ❌ admin | — |
|
|
||||||
|
|
||||||
**新增需建路由**:`/preview`、`/executions/:id`、`/workflows/:name/executions`、`/executions/paused`、`/webhooks/named/:name/diff`、`/my-telemetry`(共 6 個)
|
|
||||||
|
|
||||||
### 4.3 KBDB 50 路由 × LI 暴露
|
|
||||||
|
|
||||||
LI **不直接 expose KBDB 50 個路由**。AI 透過 arcrun-mcp 的 abstracted tool(`get_skill` / `list_skills` / `report_feedback` 等)間接用 KBDB。
|
|
||||||
|
|
||||||
例外(值得直接 expose 的):
|
|
||||||
- `arcrun_kbdb_search(query)` — 走 `/search` semantic
|
|
||||||
- `arcrun_kbdb_get_block(id)` — 看 block 內容(debug 用)
|
|
||||||
|
|
||||||
KBDB schema 設計 / triplets / records 這些屬於另一個 SDD(KBDB MCP),不在本 LI 範圍。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 5. AGENTS.md 模板
|
|
||||||
|
|
||||||
```markdown
|
|
||||||
# Arcrun for AI Agents
|
|
||||||
|
|
||||||
## What is Arcrun (30 sec)
|
|
||||||
Arcrun lets you compose Cloudflare Workers (WASM components) into workflows via YAML.
|
|
||||||
You write YAML, push to KV, trigger via webhook or cron. Each component is a
|
|
||||||
TinyGo/AssemblyScript WASM with stdin/stdout JSON I/O.
|
|
||||||
|
|
||||||
## Connect (one step)
|
|
||||||
Add to your MCP config:
|
|
||||||
\`\`\`json
|
|
||||||
{
|
|
||||||
"mcpServers": {
|
|
||||||
"arcrun": {
|
|
||||||
"url": "https://mcp.arcrun.dev/mcp",
|
|
||||||
"headers": {"Authorization": "Bearer $ARCRUN_API_KEY"}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
\`\`\`
|
|
||||||
Get your ak_ key at https://arcrun.dev/me.
|
|
||||||
|
|
||||||
## Your first workflow (5 min)
|
|
||||||
1. `arcrun_list_components()` → see what's available
|
|
||||||
2. Write 3-line YAML in chat (assistant does this)
|
|
||||||
3. `arcrun_validate_yaml(yaml)` → confirm
|
|
||||||
4. `arcrun_push_workflow(yaml)` → deploy
|
|
||||||
5. `arcrun_run_workflow(name, input)` → test
|
|
||||||
6. `arcrun_get_execution_trace(id)` → see results
|
|
||||||
|
|
||||||
## Core concepts (5 things)
|
|
||||||
- **Component**: WASM Worker. Use existing or `arcrun_publish_component`.
|
|
||||||
- **Cypher binding YAML**: triplets `A >> relation >> B` define graph.
|
|
||||||
- **FOREACH**: `>> 對每個 X >> next_node` iterates lists.
|
|
||||||
- **Paused-resume**: claude_api etc pause workflow; resumes via callback.
|
|
||||||
- **api_key**: ak_xxx, all calls need this in MCP auth.
|
|
||||||
|
|
||||||
## URL conventions (why this matters)
|
|
||||||
- `cypher.arcrun.dev` = orchestration API (you push here)
|
|
||||||
- `arcrun-{kebab}.{user}.workers.dev` = component workers (workers.dev avoids CF self-fetch)
|
|
||||||
- `kbdb-*.arcrun.dev` = KBDB component workers (your data lives here)
|
|
||||||
|
|
||||||
## Common errors
|
|
||||||
| error_code | What to do |
|
|
||||||
|---|---|
|
|
||||||
| `component_not_found` | call `list_components`, check spelling |
|
|
||||||
| `validation_failed` | read `next_actions` field, fix YAML |
|
|
||||||
| `paused_awaiting_resume` | normal for claude_api; wait or `get_execution_trace` |
|
|
||||||
| `auth_invalid` | ak_ wrong, re-fetch from /me |
|
|
||||||
|
|
||||||
## Unsure? Do this:
|
|
||||||
- Don't know what's available → `list_components` / `list_workflows` / `search_examples`
|
|
||||||
- Don't know syntax → `get_skill('skill-build_X')` for playbooks
|
|
||||||
- Hit weird behavior → **MUST** call `report_feedback(issue_type='unexpected_behavior', ...)`
|
|
||||||
|
|
||||||
## Feedback (required)
|
|
||||||
After every workflow deploy or debug session, call:
|
|
||||||
\`\`\`
|
|
||||||
arcrun_report_feedback({
|
|
||||||
workflow_name: '...',
|
|
||||||
issue_type: 'success_story' | 'doc_unclear' | 'tool_missing' | 'error_unhelpful' | 'feature_request',
|
|
||||||
description: '...',
|
|
||||||
blocked: false,
|
|
||||||
retry_count: N,
|
|
||||||
suggested_fix: '...' // optional
|
|
||||||
})
|
|
||||||
\`\`\`
|
|
||||||
|
|
||||||
Even "success" stories help — they tell us what's working.
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 6. Migration plan(u6u-mcp → arcrun-mcp 一次切換)
|
|
||||||
|
|
||||||
### 6.1 切換方式
|
|
||||||
|
|
||||||
leo 2026-05-16 拍板:u6u branding 整體退場,**單一 rename,不留 alias 也不留 deprecation 期**。
|
|
||||||
|
|
||||||
- Worker 部署 name 改:`u6u-mcp` → `arcrun-mcp`(wrangler.toml)
|
|
||||||
- 路由改:`mcp.finally.click/*` → `mcp.arcrun.dev/*`(DNS + worker route 同步切)
|
|
||||||
- 舊 URL 退場:`mcp.finally.click` 不保留(要嘛 410 Gone,要嘛 301 redirect 到 arcrun.dev landing 一個說明頁)
|
|
||||||
- Tool 名一次改:所有 `u6u_*` rename 成 `arcrun_*`,**舊名直接消失**
|
|
||||||
- repo 路徑改:`matrix/u6u-mcp/` → `matrix/arcrun-mcp/`
|
|
||||||
|
|
||||||
### 6.2 切換前須做
|
|
||||||
|
|
||||||
- [ ] 全 monorepo `grep u6u_` 確認所有 client(mira / 自家腳本 / leo 自己的 IDE 配置)
|
|
||||||
- [ ] 公告:在 mira 河道 + telegram 通知「ak_xxx 用戶請更新 MCP 配置:URL → mcp.arcrun.dev」
|
|
||||||
- [ ] 切換當天 worker 部署兩個 name(過渡 1 天可回滾),確認流量切完才把舊 worker disable
|
|
||||||
|
|
||||||
### 6.3 為什麼不做 deprecation
|
|
||||||
|
|
||||||
- 用戶量極少(dogfood 階段,主要是 leo 自己)
|
|
||||||
- 留 alias 會讓新 AI agent 學到舊名,違背「LI 一致性」原則
|
|
||||||
- 90 天監控成本 > 一次切換 + 公告
|
|
||||||
|
|
||||||
### 6.2 新增 tools(按 phase)
|
|
||||||
|
|
||||||
Phase 1(gap-fill):
|
|
||||||
- `arcrun_validate_yaml`(既有 `/validate` 已存,包 MCP 即可)
|
|
||||||
- `arcrun_get_execution_trace`(需 cypher-executor 新加 `/executions/:id`)
|
|
||||||
- `arcrun_list_recent_executions`(新 endpoint)
|
|
||||||
- `arcrun_list_paused_executions`(新 endpoint)
|
|
||||||
- `arcrun_report_feedback`(新 tool)
|
|
||||||
- `arcrun_get_onboarding`(KBDB read)
|
|
||||||
|
|
||||||
Phase 2(advanced):
|
|
||||||
- `arcrun_preview_workflow`(新 endpoint,沒 KV side-effect)
|
|
||||||
- `arcrun_diff_workflow`
|
|
||||||
- `arcrun_my_telemetry`
|
|
||||||
- `arcrun_search_examples`(KBDB write + search 已存)
|
|
||||||
- `arcrun_list_skills` / `arcrun_get_skill` / `arcrun_publish_skill`
|
|
||||||
|
|
||||||
Phase 3(auto-loop):
|
|
||||||
- `agent_feedback_weekly_review` workflow
|
|
||||||
- `arcrun-roadmap` block 生成
|
|
||||||
- LLM extract skill 自動化
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 7. 開發順序與里程碑
|
|
||||||
|
|
||||||
### Milestone 1:可量測(1 週)
|
|
||||||
- 寫 AGENTS.md v1
|
|
||||||
- 加 implicit telemetry 寫入點(cypher-executor)
|
|
||||||
- 加 `arcrun_report_feedback` MCP tool
|
|
||||||
- 建 `agent-feedback` / `agent-telemetry` KBDB template
|
|
||||||
- 開始收 data
|
|
||||||
|
|
||||||
### Milestone 2:gap-fill(1 週)
|
|
||||||
- arcrun-mcp 補上 6 個 Phase 1 tools
|
|
||||||
- 新 endpoints 在 cypher-executor 加
|
|
||||||
- 每個 tool 結構化 error contract
|
|
||||||
|
|
||||||
### Milestone 3:skill + example(1 週)
|
|
||||||
- 種子 5 個 skill blocks
|
|
||||||
- 種子 10 個 example workflows
|
|
||||||
- `search_examples` 跑通
|
|
||||||
|
|
||||||
### Milestone 4:closed loop(半週)
|
|
||||||
- weekly_review workflow 部署
|
|
||||||
- 第一份 arcrun-roadmap block 產出
|
|
||||||
- leo 收到第一份週報
|
|
||||||
|
|
||||||
### Milestone 5:rename + cleanup(1 週)
|
|
||||||
- arcrun-mcp → arcrun-mcp 公開
|
|
||||||
- 舊 tool 加 deprecation warning
|
|
||||||
- AGENTS.md 同步 KBDB
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 8. 決策紀錄
|
|
||||||
|
|
||||||
### 8.1 已拍板(2026-05-16 leo)
|
|
||||||
|
|
||||||
| 決策 | 結果 | 理由 |
|
|
||||||
|---|---|---|
|
|
||||||
| MCP server URL | `mcp.arcrun.dev` 單一 URL,舊 `mcp.finally.click` 直接退場 | finally.click 是還沒申請新網址的暫用,那是一個服務,arcrun 是底層 |
|
|
||||||
| u6u branding | 整體退場改 arcrun(repo / worker / tool 命名一次 rename) | u6u 不存在了 |
|
|
||||||
| Deprecation 期 | 不留(一次切換 + 公告) | 用戶量極少,留 alias 反而讓新 AI 學到舊名 |
|
|
||||||
| 擴張 vs 建新 | 擴張既有 u6u-mcp(rename 成 arcrun-mcp) | 不 fork,零移轉痛 |
|
|
||||||
| AGENTS.md 位置 | repo `arcrun/AGENTS.md` + 自動同步 KBDB block | 兩面都拿到(git-access 也 OK,純 MCP 也 OK) |
|
|
||||||
| feedback 寫入 auth | 要(驗 ak_ 存在即可,不查餘額) | 防 spam |
|
|
||||||
|
|
||||||
### 8.2 仍可商議(小議題)
|
|
||||||
|
|
||||||
| 議題 | 建議 |
|
|
||||||
|---|---|
|
|
||||||
| LI 是否包含 KBDB MCP | 部分(abstracted),KBDB MCP 另立 SDD `kbdb-llm-interface` |
|
|
||||||
| Telemetry 保留多久 | 90 天 hot + 1 年 cold archive |
|
|
||||||
| AGENTS.md 第一版用中文還是英文 | 中文(leo 自家 + mira 一致),英文版 v2 開源時補 |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 9. 跨 SDD 連動
|
|
||||||
|
|
||||||
| 其他 SDD | 連動點 |
|
|
||||||
|---|---|
|
|
||||||
| `credential-primitives-wasm` | LI 不重做 auth,只用 `auth_static_key` 等既有零件 |
|
|
||||||
| `recipe-system` | LI 暴露 recipe CRUD tool 對應 |
|
|
||||||
| `component-registry-canon` | LI `list_components` / `get_contract` 走 registry |
|
|
||||||
| `resumable-workflow` | LI `list_paused_executions` / `resume` 用 |
|
|
||||||
| `arcrun-platform-evolution` | LI 是 evolution 之一,未來分 user-tier 時要考慮 |
|
|
||||||
| **mira-app** (polaris/mira) | LI 是 mira dogfood 痛點轉化的產物,roadmap 含對 mira 的回饋 |
|
|
||||||
@@ -1,246 +0,0 @@
|
|||||||
# Requirements: LI (LLM Interface) for arcrun
|
|
||||||
|
|
||||||
> 把 arcrun 平台對「AI 操盤手」的完整使用面,當成 first-class product 設計。
|
|
||||||
> 對比:以前做網站要 user-friendly(UI),現在 AI 是主要用戶(LI)。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 背景
|
|
||||||
|
|
||||||
arcrun 是 n8n-like workflow 平台,平台本身的 end-user 有兩類:
|
|
||||||
- **人類**:透過 arcrun-gui canvas / arcrun.dev landing 操作
|
|
||||||
- **AI agent**:透過 MCP / API 操作(Claude Code、Cursor、Codex、自製 agent 等)
|
|
||||||
|
|
||||||
過去設計集中在「人」(UI / docs)。AI 對 arcrun 的「可用性」沒被當第一公民。
|
|
||||||
|
|
||||||
### 證據:3 天 mira dogfood 累積的 14 個痛點(baseline)
|
|
||||||
|
|
||||||
| 編號 | 痛點 | 性質 |
|
|
||||||
|---|---|---|
|
|
||||||
| 1 | 新零件 deploy 要手動 dashboard 點 workers.dev | platform DX |
|
|
||||||
| 2 | CF 同 zone self-fetch 死鎖 | platform infra |
|
|
||||||
| 3 | `http_request` 4xx 回 success=true cascade | error semantics |
|
|
||||||
| 4 | interpolateString array stringified | dev experience |
|
|
||||||
| 5 | `kbdb_upsert_block` 沒在 `WASM_HTTP_RUNNER_IDS` 白名單 | discovery |
|
|
||||||
| 6 | `resumeFromPaused` paused_node_id 沒 namespace | implicit behavior |
|
|
||||||
| 7 | `acr validate` 不認「對每個 X」 | validator stale |
|
|
||||||
| 8 | cron 從零寫起 | feature gap |
|
|
||||||
| 9 | CF Pages 沒接 auto-deploy | infra setup |
|
|
||||||
| 10 | watcher self-fetch 死鎖(剛解) | architecture |
|
|
||||||
| 11 | skill 改要寫 python script | tooling gap |
|
|
||||||
| 12 | 不知道有什麼零件可用 | **LI gap** |
|
|
||||||
| 13 | 不知道現有 workflow 怎麼長 | **LI gap** |
|
|
||||||
| 14 | trace / log 看不到結構化的 | **LI gap** |
|
|
||||||
|
|
||||||
**14 個裡有 7 個(編號 3-7, 12-14)純粹是 LI 缺失**。如果 LI 完整,等於把 50% 的踩坑時間還給開發者。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 目標用戶(personas)
|
|
||||||
|
|
||||||
### P1:Claude Code / Cursor 等 IDE-embedded AI(主力)
|
|
||||||
|
|
||||||
特徵:
|
|
||||||
- 跟 leo 一起 pair programming
|
|
||||||
- 有 file access、shell 能力
|
|
||||||
- 透過 MCP / curl / SDK 跟 arcrun 對話
|
|
||||||
|
|
||||||
需求:
|
|
||||||
- 知道 arcrun 全貌(不靠人類解釋)
|
|
||||||
- 知道目前用戶有什麼資產(workflow / component / template / credential)
|
|
||||||
- 寫 YAML 不靠猜
|
|
||||||
- 部署前能 dry-run
|
|
||||||
- 部署後能看執行結果
|
|
||||||
- 卡住能 introspect + ask
|
|
||||||
|
|
||||||
### P2:用戶私人 agent(次要,未來)
|
|
||||||
|
|
||||||
特徵:
|
|
||||||
- 在 mira / 用戶 app 內跑
|
|
||||||
- 透過 MCP 對 arcrun
|
|
||||||
- 沒有檔案系統,只有 HTTP
|
|
||||||
|
|
||||||
需求:基本同 P1,但**完全靠 MCP**(沒 grep / file read fallback)
|
|
||||||
|
|
||||||
### P3:自製腳本 / SDK 使用者
|
|
||||||
|
|
||||||
不在本 SDD 範圍。SDK 既有規格走 `.agents/specs/arcrun/sdk-and-website/`。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 範圍(系統涵蓋)
|
|
||||||
|
|
||||||
LI 不是「新建一個 service」,是**跨 5 個既有系統的橫向 layer**:
|
|
||||||
|
|
||||||
| 系統 | 既有狀態 | LI 涵蓋的部分 |
|
|
||||||
|---|---|---|
|
|
||||||
| **arcrun cypher-executor** | 31 HTTP 路由 | 哪些對外、哪些 AI 該看得到 |
|
|
||||||
| **arcrun registry** | component 管理 Worker | discovery layer |
|
|
||||||
| **arcrun-mcp**(目前 `matrix/u6u-mcp/`,本 SDD M5 改名) | 15 MCP tools(HTTP→MCP 薄包裝) | 主擴張面(gap-fill) |
|
|
||||||
| **arcrun-gui**(目前 `matrix/u6u-gui/`,本 SDD M5 改名) | 人類 canvas IDE | 取其 endpoint 觀念,不取其 UI 元素 |
|
|
||||||
| **kbdb** | 50 個 HTTP 路由 | KBDB 該不該直接給 AI、用什麼姿勢 |
|
|
||||||
| **arcrun CLI (acr)** | Node CLI | LI 不依賴 CLI(CLI 是人的工具) |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 非範圍(這個 SDD 不處理)
|
|
||||||
|
|
||||||
- 不處理 GUI 設計 / 視覺
|
|
||||||
- 不處理 SDK(Python/JS)API 設計
|
|
||||||
- 不處理 user OAuth flow
|
|
||||||
- 不重新設計 KBDB schema(只決定 LI 該包什麼 API)
|
|
||||||
- 不重新設計 cypher binding 語法
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 功能需求(FR)
|
|
||||||
|
|
||||||
### FR-1:AI 一條指令就能上手 arcrun
|
|
||||||
|
|
||||||
- 提供 `AGENTS.md`(onboarding doc),AI 載入就能用
|
|
||||||
- MCP server URL 寫進 doc,「直接 connect」一步搞定
|
|
||||||
- 不需讀 SDD / 不需 grep codebase / 不需問人
|
|
||||||
|
|
||||||
### FR-2:完整 CRUD + Discovery 對等
|
|
||||||
|
|
||||||
人類在 arcrun-gui 能做的 8 個動作(list_workflows / get / update / execute / search_components / get_component / list_templates / list_credentials),**MCP 至少同等覆蓋**,不能有「人類能做但 AI 不能做」的 gap。
|
|
||||||
|
|
||||||
### FR-3:Dry-run 是預設行為
|
|
||||||
|
|
||||||
- `validate_workflow(yaml)` MCP tool(取既有 `/validate` 路由)
|
|
||||||
- `preview_workflow(yaml, input)` 不寫入 KV,模擬執行
|
|
||||||
- AI 養成習慣「先 dry-run 再 push」
|
|
||||||
|
|
||||||
### FR-4:可診斷的 trace
|
|
||||||
|
|
||||||
- `get_execution_trace(execution_id)` 回結構化 JSON:每個 node 的 status / input / output / error / duration
|
|
||||||
- `list_paused_executions()` 列卡住的執行(callback 沒回時 debug 用)
|
|
||||||
- 不靠 `wrangler tail` 純文字
|
|
||||||
|
|
||||||
### FR-5:錯誤訊息是「給 AI 看的下一步」
|
|
||||||
|
|
||||||
所有 MCP tool error response 必須含:
|
|
||||||
- `error_code`(穩定字串 enum,可程式化 catch)
|
|
||||||
- `human_message`(描述)
|
|
||||||
- `next_actions`(陣列,可選:「call X」/「執行 Y」/「修改 Z」)
|
|
||||||
|
|
||||||
範例:
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"error_code": "component_not_in_whitelist",
|
|
||||||
"human_message": "零件 'filter' 不在用戶可用清單",
|
|
||||||
"next_actions": [
|
|
||||||
"call list_components() 看完整可用清單",
|
|
||||||
"若該零件需平台啟用,告訴用戶執行 `acr enable filter`",
|
|
||||||
"若是自製零件,先 push 零件再 push workflow"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### FR-6:AI 可以回報問題
|
|
||||||
|
|
||||||
- `report_feedback(workflow_name, issue_type, description, ...)` MCP tool
|
|
||||||
- 結構化 enum issue_type(防自由文字難聚合)
|
|
||||||
- 寫進 KBDB 成 `agent-feedback` block,可被定期 review
|
|
||||||
- AI 規範「順利 / 卡住 / 不確定 都該 call」(透過 AGENTS.md 強制)
|
|
||||||
|
|
||||||
### FR-7:Implicit telemetry 不依賴 AI 自覺
|
|
||||||
|
|
||||||
- cypher-executor 每次 deploy / execute / fail 自動寫 `agent-telemetry` block
|
|
||||||
- 含 client_user_agent(哪個 AI 用的)、error_message、duration
|
|
||||||
- 不依賴 AI 主動 call,平台自己收
|
|
||||||
|
|
||||||
### FR-8:Skill blocks 可重用
|
|
||||||
|
|
||||||
- 「pattern / playbook」存 KBDB type=`agent-skill`
|
|
||||||
- MCP `list_skills(tag)` / `get_skill(id)` 給 AI 查
|
|
||||||
- 範例:`build_watcher_workflow` / `debug_paused_workflow` / `migrate_http_to_trigger_workflow`
|
|
||||||
|
|
||||||
### FR-9:範例庫可搜尋
|
|
||||||
|
|
||||||
- `search_examples(use_case)` 給 use case → 回相似 workflow YAML
|
|
||||||
- 範例存 `registry/examples/` git + 一份 index 在 KV
|
|
||||||
- 進階:semantic search(既有 KBDB `/search/embed`)
|
|
||||||
|
|
||||||
### FR-10:定期 review 機制(自動化)
|
|
||||||
|
|
||||||
- `agent_feedback_weekly_review` workflow(**arcrun 自己跑**)
|
|
||||||
- 每週一聚合 feedback + telemetry,產出 Top N 痛點 + 建議
|
|
||||||
- 寫進 `arcrun-roadmap` KBDB block
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 非功能需求(NFR)
|
|
||||||
|
|
||||||
### NFR-1:向下相容
|
|
||||||
|
|
||||||
LI 在「擴張」階段(M1-M4)不破壞既有:
|
|
||||||
- M1-M4 階段:arcrun-mcp 既有 15 tool 保留,新增 13 個 tool(pre-rename)
|
|
||||||
- arcrun-gui 不動
|
|
||||||
- cypher-executor 既有路由不改 contract(只加新 endpoint)
|
|
||||||
|
|
||||||
M5(rename)階段是 breaking change(leo 拍板),需事前公告。
|
|
||||||
|
|
||||||
### NFR-2:transport 不鎖死
|
|
||||||
|
|
||||||
MCP 主,但 HTTP 同等可用(讓沒 MCP 客戶端的 agent 也可用)。每個 MCP tool 都對應一個 HTTP endpoint。
|
|
||||||
|
|
||||||
### NFR-3:error contract 穩定
|
|
||||||
|
|
||||||
`error_code` enum 是 public API,加新值是 minor,移除值是 major。版本化。
|
|
||||||
|
|
||||||
### NFR-4:feedback 數據可外部 export
|
|
||||||
|
|
||||||
`agent-feedback` / `agent-telemetry` 走 KBDB `/blocks` 標準 API,任何 AI / 人都能拉。不鎖死在 dashboard。
|
|
||||||
|
|
||||||
### NFR-5:覆蓋率可量化
|
|
||||||
|
|
||||||
- 「人類在 arcrun-gui 能做但 MCP 不能做」清單必須能列出
|
|
||||||
- 每次 LI 改動後,這份清單往 0 收斂
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 成功指標
|
|
||||||
|
|
||||||
| 指標 | 量測 | 目標 |
|
|
||||||
|---|---|---|
|
|
||||||
| 新 AI agent 上手時間(從 zero 到第一個 workflow 部署) | 手動實驗 | < 10 min |
|
|
||||||
| 一次 workflow 部署需要的 MCP call 次數 | telemetry | < 5 calls |
|
|
||||||
| AI 回報「文件不清楚」週次數 | feedback aggregation | 持續下降 |
|
|
||||||
| 「MCP 缺工具」issue 類型佔比 | feedback type ratio | < 10% |
|
|
||||||
| Implicit telemetry 與 explicit feedback 比例 | 計數 | 9:1 不算問題(implicit 量大正常) |
|
|
||||||
| Coverage gap(人類能做 vs AI 能做) | 手動審查 | 0 |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 風險與假設
|
|
||||||
|
|
||||||
### 假設
|
|
||||||
- MCP 是主要 AI 接觸點(不是 SDK / CLI)
|
|
||||||
- AI 願意按 AGENTS.md 規範 call feedback tool
|
|
||||||
- KBDB 能承載 telemetry 量級(短期:< 1000 events / day)
|
|
||||||
|
|
||||||
### 風險
|
|
||||||
|
|
||||||
| 風險 | 應對 |
|
|
||||||
|---|---|
|
|
||||||
| AI 不照 AGENTS.md 規範用 | implicit telemetry 不依賴自覺,仍能收 data |
|
|
||||||
| MCP tool 設計錯誤越改越亂 | 每個 tool 有 contract test,error_code enum 版本化 |
|
|
||||||
| feedback 雜訊太多無法 review | review workflow 用 LLM 聚合,不靠人讀原始 |
|
|
||||||
| arcrun-mcp 重構搞壞既有用戶 | 用戶量還很少(dogfood 階段),一次切換可接受;切換前公告 |
|
|
||||||
| KBDB telemetry 量爆掉 | sample rate / 老資料自動 archive |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 決策(2026-05-16 leo 拍板)
|
|
||||||
|
|
||||||
1. **deployment 名稱**:✅ `mcp.arcrun.dev` 單一 URL
|
|
||||||
- `mcp.finally.click` 直接退場,不並存
|
|
||||||
- 理由(leo):「finally.click 是還沒申請新網址的暫用,那是一個服務,arcrun 是底層」
|
|
||||||
2. **u6u branding 整體退場**:✅ u6u 命名全部改 arcrun
|
|
||||||
- `u6u-mcp` repo → `arcrun-mcp`
|
|
||||||
- `u6u-gui` repo → `arcrun-gui`
|
|
||||||
- 工具命名 `u6u_*` → `arcrun_*`(**單一 rename,不留 alias 也不留 deprecation 期**)
|
|
||||||
- 理由:「u6u 不存在,改成 Arcrun」
|
|
||||||
3. **AGENTS.md 位置**:✅ repo `arcrun/AGENTS.md` + 自動同步 KBDB block(兩面都拿到)
|
|
||||||
4. **feedback 寫入需 auth**:✅ 驗 ak_ 存在即可(防 spam,tier 低不查餘額)
|
|
||||||
@@ -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 已 rename,Worker name 待後段 DNS 遷移
|
|
||||||
|
|
||||||
阻擋項:GH Actions 用戶層被 disable(leo 申訴中)→ 改用本機 wrangler deploy + scripts/local-deploy.sh fallback。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Milestone 1:可量測(先收 data)✅
|
|
||||||
|
|
||||||
### M1.1 AGENTS.md v1
|
|
||||||
- [x] 寫 `arcrun/AGENTS.md`(5697355 + 3892dc3,263 行)
|
|
||||||
- [ ] CI hook:repo `AGENTS.md` 變動 → 自動同步 KBDB block
|
|
||||||
- [ ] `arcrun_get_onboarding` MCP tool(讀 KBDB block)
|
|
||||||
|
|
||||||
### M1.2 Implicit telemetry 收集 ✅
|
|
||||||
- [x] 建 KBDB block type=`agent-telemetry`(slots 直接 metadata_json 不走 template)
|
|
||||||
- [x] `webhook-handlers.executeWebhookGraph` 末尾加 telemetry(成功 / 失敗 / paused 都記)
|
|
||||||
- [x] `routes/webhooks-named.ts` push deploy 事件(deploy_success)
|
|
||||||
- [x] `routes/validate.ts` validation 失敗事件(schema_failed / edge_node_missing)
|
|
||||||
- [x] `hashApiKey` SHA-256 截 16 字元 helper
|
|
||||||
- [x] 隱私:只記 workflow name 不記 content
|
|
||||||
- [x] 實測:KBDB block `68635dcb-62e5-49ca-9c67-33f4ca82b7a0` event=run_success, paused_awaiting_resume
|
|
||||||
|
|
||||||
### M1.3 Explicit feedback tool ✅
|
|
||||||
- [x] KBDB block type=`agent-feedback`
|
|
||||||
- [x] arcrun-mcp tool `arcrun_report_feedback` (commit e637c3e)
|
|
||||||
- [x] Zod enum 鎖死 issue_type
|
|
||||||
- [x] user_id 從 partnerAuth 取
|
|
||||||
- [x] tags_json auto: ['agent-feedback', 'issue:{type}', 'blocked'?, 'wf:{name}'?]
|
|
||||||
- [x] schema 實測:KBDB block `80f1d2d1-c95a-4dfe-a889-d23b2e9b247d`
|
|
||||||
|
|
||||||
### M1.4 驗收 ✅
|
|
||||||
- [x] 觸發 mira watcher → KBDB agent-telemetry 即時出現
|
|
||||||
- [x] curl + python verify 8 個 telemetry blocks(event=run_success, workflow_name 對, duration_ms 對)
|
|
||||||
- [x] feedback block 寫入測 schema 通
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Milestone 2:gap-fill(補 MCP 工具)
|
|
||||||
|
|
||||||
目標:人類 GUI 能做的,AI 透過 MCP 都能做。
|
|
||||||
|
|
||||||
### M2.1 新增 cypher-executor 路由 ✅
|
|
||||||
|
|
||||||
- [x] `GET /executions/:task_id` — 回結構化 paused state (989fbeb)
|
|
||||||
- [x] `GET /workflows/:name/executions?limit=10` — 走 ANALYTICS_KV stats:* prefix (989fbeb)
|
|
||||||
- [x] `GET /executions/paused` — 改 per-user index 強 consistent (4e7880c)
|
|
||||||
- [ ] `POST /preview` — dry-run,不寫 KV(暫緩)
|
|
||||||
- [ ] `POST /webhooks/named/:name/diff` — 新舊 YAML diff(暫緩)
|
|
||||||
- [ ] `GET /my-telemetry?limit=N` — 用戶自己看 telemetry(暫緩)
|
|
||||||
|
|
||||||
### M2.2 MCP tools ✅ (9/13)
|
|
||||||
|
|
||||||
完成(commit faf75cd + f91b1fd):
|
|
||||||
- [x] `arcrun_validate_yaml` — wrap /validate
|
|
||||||
- [x] `arcrun_get_execution_trace`
|
|
||||||
- [x] `arcrun_list_recent_executions`
|
|
||||||
- [x] `arcrun_list_paused_executions`
|
|
||||||
- [x] `arcrun_push_workflow` — wrap /webhooks/named POST(取代壞掉的 u6u_deploy_workflow)
|
|
||||||
- [x] `arcrun_list_workflows`
|
|
||||||
- [x] `arcrun_get_workflow`
|
|
||||||
- [x] `arcrun_delete_workflow` (require confirm:true literal)
|
|
||||||
- [x] `arcrun_run_workflow` (paused 視為 success)
|
|
||||||
|
|
||||||
暫緩(等 endpoint 完成):
|
|
||||||
- [ ] `arcrun_resume_execution` — 包既有 /workflows/resume
|
|
||||||
- [ ] `arcrun_preview_workflow` — 待 M2.1 /preview
|
|
||||||
- [ ] `arcrun_diff_workflow` — 待 M2.1 diff
|
|
||||||
- [ ] `arcrun_list_recipes` / `create_recipe`
|
|
||||||
- [ ] `arcrun_list_auth_recipes` / `create_auth_recipe`
|
|
||||||
- [ ] `arcrun_my_telemetry`
|
|
||||||
|
|
||||||
### M2.3 Error contract 統一 ✅
|
|
||||||
|
|
||||||
- [x] `error_code` enum v1 定義在 design.md §1.4 + cypher-executor /executions/* 路由都用
|
|
||||||
- [x] arcrun-mcp `lib/cypher-client.ts` errorResponse() / successResponse() 統一 helper
|
|
||||||
- [x] 所有新 MCP tool(10 個)都用統一 contract(ok, data?, error_code?, human_message?, next_actions?, hints?)
|
|
||||||
- [ ] cypher-executor 既有 route(非 /executions/*)改成統一格式(暫緩)
|
|
||||||
- [ ] 每個 error_code 對應 unit test(暫緩)
|
|
||||||
|
|
||||||
### M2.4 驗收(部分)
|
|
||||||
|
|
||||||
- [ ] 模擬 zero-knowledge AI 跑 hello workflow(待 leo 提供 pk_live)
|
|
||||||
- [ ] 量測:from list_components 到 run_workflow 成功 MCP call < 5
|
|
||||||
- [ ] 比較人類 GUI 路徑,clickwise 對等
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Milestone 3:skill blocks + examples ✅
|
|
||||||
|
|
||||||
目標:AI 寫第一個 workflow 不靠猜,有範本和 playbook。
|
|
||||||
|
|
||||||
### M3.1 種子 skill blocks ✅ (commit 388c193)
|
|
||||||
|
|
||||||
- [x] `skill-build_watcher_workflow` — cron + 過濾 + trigger 模式
|
|
||||||
- [x] `skill-debug_paused_workflow` — claude_api callback 流程 + 怎麼追
|
|
||||||
- [x] `skill-migrate_http_to_trigger_workflow` — 從 self-fetch 換 trigger_workflow
|
|
||||||
- [x] `skill-rag_with_arcrun` — KBDB search + claude_api 組裝
|
|
||||||
- [x] `skill-add_new_wasm_component` — TinyGo 寫 + push + 註冊白名單
|
|
||||||
|
|
||||||
### M3.2 MCP tools(暫緩,待 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 (idempotent,page_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 4:closed loop ✅
|
|
||||||
|
|
||||||
目標:data 收得到 → 平台自己消化產出 roadmap。
|
|
||||||
|
|
||||||
### M4.1 Weekly review workflow ✅ (mira commit de11625)
|
|
||||||
|
|
||||||
- [x] 寫 `polaris/mira/arcrun/agent_feedback_weekly_review.yaml`
|
|
||||||
- [x] cron `0 9 * * 1` (台灣 17:00 週一)
|
|
||||||
- [x] `acr push` 部署
|
|
||||||
- [x] 手動觸發測試一次(5/6 nodes success,唯一 fail 是 notify_leo 缺 credential)
|
|
||||||
|
|
||||||
### M4.2 LLM 聚合 prompt ✅
|
|
||||||
|
|
||||||
- [x] prompt 結構化:數字 / Top 5 痛點(含證據 / 嚴重度)/ 成功 pattern / 下週優先 3 件
|
|
||||||
- [x] 一律繁體中文 + 引用 block_id 為證據
|
|
||||||
- [x] 存 KBDB type=`arcrun-roadmap`, page_name=roadmap-latest(每週覆蓋)
|
|
||||||
- [x] 實測產出真有用:抓到「paused_awaiting_resume 語意不清」「data 量太少」「自動建議包 skill」三個真實 LI 改進建議
|
|
||||||
|
|
||||||
### M4.3 通知
|
|
||||||
|
|
||||||
- [x] notify_leo 節點:telegram chat_id from secret
|
|
||||||
- [ ] leo 補 telegram_bot_token credential 後生效
|
|
||||||
- [ ] 同時寫進 mira 河道(讓 leo 在熟悉介面看)— 暫緩
|
|
||||||
|
|
||||||
### M4.4 驗收
|
|
||||||
|
|
||||||
- [x] 第一次手動觸發 → 收到第一份 roadmap (KBDB block id e924c231-cf5e-4541-89d8-da550ecae2f3)
|
|
||||||
- [ ] cron 自動跑首次(下週一驗證)
|
|
||||||
- [ ] leo review 後挑 1-2 個 issue 修補
|
|
||||||
- [ ] 跑第二週 → 確認該 issue 從 top list 消失
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Milestone 5:rename + cleanup(u6u → arcrun 一次切換)
|
|
||||||
|
|
||||||
目標:完成 LI 品牌化,u6u branding 整體退場。**leo 2026-05-16 拍板:單一 rename,不留 alias 也不留 deprecation 期**。
|
|
||||||
|
|
||||||
### M5.1 切換前準備
|
|
||||||
|
|
||||||
- [ ] 全 monorepo `grep -rn "u6u_\|mcp\.finally\.click"` 列出所有受影響檔案 + 用戶配置
|
|
||||||
- [ ] mira 河道 + telegram 公告:「ak_xxx 用戶請更新 MCP 配置 URL → mcp.arcrun.dev」(**至少切換前 24h**)
|
|
||||||
- [ ] 列 leo 自己的 IDE(Claude Code / Cursor)配置位置,準備同步更新
|
|
||||||
|
|
||||||
### M5.2 Repo / Worker rename
|
|
||||||
|
|
||||||
- [ ] `matrix/u6u-mcp/` → `matrix/arcrun-mcp/` (git mv)
|
|
||||||
- [ ] `matrix/u6u-gui/` → `matrix/arcrun-gui/` (git mv)
|
|
||||||
- [ ] `arcrun-mcp/wrangler.toml`: name = `arcrun-mcp`
|
|
||||||
- [ ] `arcrun-gui/wrangler.toml`: name = `arcrun-gui`
|
|
||||||
- [ ] DNS:`mcp.arcrun.dev` route 接到 arcrun-mcp worker
|
|
||||||
- [ ] CI(deploy.yml)若有寫死 path 同步改
|
|
||||||
|
|
||||||
### M5.3 Tool rename(一次切換)
|
|
||||||
|
|
||||||
- [ ] 所有 `u6u_*` MCP tool 改 `arcrun_*`(不留 alias)
|
|
||||||
- [ ] AGENTS.md 全用新名
|
|
||||||
- [ ] `arcrun-mcp/README.md` / `GUIDE.md` 全部用新名
|
|
||||||
|
|
||||||
### M5.4 舊 URL 退場
|
|
||||||
|
|
||||||
- [ ] `mcp.finally.click` 接 410 Gone + 訊息「請改用 mcp.arcrun.dev」
|
|
||||||
- 或 301 redirect 到 arcrun.dev landing 一個說明頁
|
|
||||||
- [ ] DNS 紀錄保留 30 天(防意外 client 還沒切)後刪除
|
|
||||||
|
|
||||||
### M5.5 文件最終化
|
|
||||||
|
|
||||||
- [ ] `arcrun/AGENTS.md` 最終版發布
|
|
||||||
- [ ] `matrix/arcrun/.agents/specs/llm-interface/design.md` 加「實際部署狀態」附錄
|
|
||||||
- [ ] 寫一篇 retrospective:LI 做完前後 AI 使用 arcrun 的 time-to-first-workflow 對比
|
|
||||||
|
|
||||||
### M5.6 連動(不在本 SDD 範圍但要追蹤)
|
|
||||||
|
|
||||||
- [ ] `matrix/arcrun/.agents/specs/arcrun-core-mvp/` SDD 改名(另立 task,跨 SDD rename)
|
|
||||||
- [ ] `matrix/arcrun/.agents/specs/arcrun-platform-evolution/` 同上
|
|
||||||
- [ ] 兩個 SDD 重命名屬「u6u 品牌退場」系列,需要單獨追蹤 task
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Backlog(暫不排)
|
|
||||||
|
|
||||||
### B.1 KBDB MCP 獨立 SDD
|
|
||||||
- LI 範圍只包 KBDB 的 `agent-*` template
|
|
||||||
- 完整 KBDB AI 介面(type=note/page/triplet/template/record 等)另立 SDD `kbdb-llm-interface`
|
|
||||||
- 跟 mira KM 系統互動最密
|
|
||||||
|
|
||||||
### B.2 多 agent 隔離
|
|
||||||
- 多 AI 共用同 ak_xxx 時,telemetry 區隔 agent_user_agent
|
|
||||||
- 進階:每個 AI 子 namespace(mira / cursor / 自製 agent)
|
|
||||||
|
|
||||||
### B.3 AGENTS.md i18n
|
|
||||||
- v1 純中文(leo + 自家用)
|
|
||||||
- v2 英文版(給開源用戶)
|
|
||||||
|
|
||||||
### B.4 自動 skill 萃取
|
|
||||||
- weekly_review 產出的 pattern 自動包成 skill draft
|
|
||||||
- leo review approve → publish
|
|
||||||
|
|
||||||
### B.5 SDK 對等(python-sdk / js-sdk)
|
|
||||||
- SDK 提供和 MCP 同樣的 25 個 method
|
|
||||||
- 給「不想用 MCP 的人」也能 AI-friendly
|
|
||||||
- 走 sdk-and-website SDD 範圍
|
|
||||||
|
|
||||||
### B.6 GUI side 補 LI 看板
|
|
||||||
- arcrun-gui 加 `/li-dashboard` 顯示用戶自己的 telemetry / feedback
|
|
||||||
- 不阻擋 LI 推出(leo 先看 KBDB 原始 block 即可)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 依賴關係
|
|
||||||
|
|
||||||
```
|
|
||||||
M1 (data 收集)
|
|
||||||
↓
|
|
||||||
M2 (MCP gap-fill)
|
|
||||||
↓
|
|
||||||
M3 (skill + examples) ← 可與 M2 並行後段
|
|
||||||
↓
|
|
||||||
M4 (closed loop) ←─── 需 M1 data 累積 1-2 週
|
|
||||||
↓
|
|
||||||
M5 (rename)
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 工估算
|
|
||||||
|
|
||||||
| Milestone | 工 | 阻擋項 |
|
|
||||||
|---|---|---|
|
|
||||||
| M1 | 5 個工作日 | 無 |
|
|
||||||
| M2 | 5 個工作日 | M1 完(telemetry 先就位才好驗證 M2 改動) |
|
|
||||||
| M3 | 5 個工作日 | M2 完(tool 介面定型才寫 skill) |
|
|
||||||
| M4 | 3 個工作日 | M1 data 累積 1 週 |
|
|
||||||
| M5 | 5 個工作日 | M2-M4 完 |
|
|
||||||
| **總** | **23 個工作日 (~5 週)** | |
|
|
||||||
|
|
||||||
實際視 leo 排程,可邊用邊改、不必一氣呵成。**M1 是硬前置**——資料不收,改了也不知道改對沒。
|
|
||||||
@@ -1,240 +0,0 @@
|
|||||||
# SDD: arcrun Recipe System(容器 + Recipe 模式)
|
|
||||||
|
|
||||||
> 2026-05-07 建立。吃狗糧寫 wiki 合成 workflow 時撞牆發現的平台缺口。
|
|
||||||
> 核心原則:**一個 WASM 零件 = 容器,內容(recipe)存資料庫**。
|
|
||||||
> n8n 為每種 API 寫獨立 node,arcrun 走「容器 + recipe」減少零件數量。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 1. 問題
|
|
||||||
|
|
||||||
### 1.1 撞牆現場
|
|
||||||
|
|
||||||
寫 mira wiki 合成 workflow(7-B)時:
|
|
||||||
- 流程:`kbdb_get(stale)` → foreach → `kbdb_get(drafts)` → `claude_api(合成 prompt)` → `kbdb_ingest`
|
|
||||||
- 第三步要組 prompt:`schema 內容 + skill 模板 + drafts array + existing_entities`
|
|
||||||
- cypher binding 內建 `{{var}}` 模板太弱(只支援 top-level,不支援嵌套 / array → string)
|
|
||||||
- 沒有 `string_template` 零件、沒有 `array_to_markdown` 零件
|
|
||||||
- 寫專用 `wiki_prompt_builder` 零件 = 走 n8n 老路,每個 AI workflow 都要寫一個
|
|
||||||
|
|
||||||
### 1.2 根因
|
|
||||||
|
|
||||||
**arcrun recipe 系統只覆蓋 HTTP / auth 兩層**:
|
|
||||||
|
|
||||||
| Recipe 種類 | 存哪 | 容器 | 狀態 |
|
|
||||||
|---|---|---|---|
|
|
||||||
| auth_recipe | RECIPES KV (`auth_recipe:{service}`) | auth_static_key / auth_oauth2 / ... | ✅ 已有 |
|
|
||||||
| api_recipe | RECIPES KV (`rec_{hash}`) | http_request | ✅ 已有(hard-code 在 cypher-executor 待清,Phase 1-3 處理)|
|
|
||||||
| **prompt_recipe** | ❌ 不存在 | claude_api(容器) | **缺** |
|
|
||||||
|
|
||||||
`claude_api` 零件目前吃 `prompt: string`(已組好的字串),沒有「recipe 模式」可以讓 AI 用「組合配方」的方式呼叫。
|
|
||||||
|
|
||||||
### 1.3 影響
|
|
||||||
|
|
||||||
- **致命**:寫不出第一個 wiki 合成 workflow(7-B 卡關)
|
|
||||||
- **推廣破功**:arcrun 對外 prop 是「容器 + recipe,AI 不用寫 code」,但 prompt 這層做不到
|
|
||||||
- **未來所有 AI workflow 都會撞同樣問題**:rss-tech-news 評語、河道 AI 副駕、ai-comment、文章摘要⋯ 全部需要組 prompt
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 2. 設計
|
|
||||||
|
|
||||||
### 2.1 核心:prompt_recipe 平行於 auth_recipe / api_recipe
|
|
||||||
|
|
||||||
**儲存**:`RECIPES` KV,key 格式 `prompt_recipe:{name}`
|
|
||||||
|
|
||||||
**結構**:
|
|
||||||
```yaml
|
|
||||||
id: prompt_recipe:wiki_synthesis
|
|
||||||
version: v1
|
|
||||||
description: "Mira wiki 合成(抽 triplet + 寫 wiki paragraph)"
|
|
||||||
model: sonnet # haiku / sonnet / opus(claude_api 沿用既有 routing)
|
|
||||||
|
|
||||||
# 從 KBDB / 其他來源取的 fragment(在 prompt 組合時抓並插入)
|
|
||||||
fragments:
|
|
||||||
- var: schema
|
|
||||||
source: kbdb_block
|
|
||||||
block_id: "7a4e456e-1b0f-406a-8842-5e01d1cf1eef" # mira-wiki-schema
|
|
||||||
field: content
|
|
||||||
- var: skill_template
|
|
||||||
source: kbdb_block
|
|
||||||
block_id: "85e3b81e-dca8-4131-bcdc-990bd0d3a16f" # source-skill-wiki-synthesis
|
|
||||||
field: content
|
|
||||||
|
|
||||||
# 從 workflow context 取(input/前置節點輸出)
|
|
||||||
inputs:
|
|
||||||
- var: drafts # 草稿 array
|
|
||||||
from: "ctx.read_drafts.blocks"
|
|
||||||
transform: "json_array" # 轉成 JSON array string
|
|
||||||
- var: existing_entities
|
|
||||||
from: "ctx.read_entities.blocks"
|
|
||||||
transform: "extract_field:page_name" # 抽 array 的 page_name 欄位 join 成 list
|
|
||||||
- var: entity_name
|
|
||||||
from: "ctx.loop.item" # foreach 迴圈當前元素
|
|
||||||
|
|
||||||
# 最終 prompt 由 fragments + inputs 套進 skill_template 組成
|
|
||||||
prompt_assembly:
|
|
||||||
system: "{{schema}}" # 直接用 schema 當 system prompt
|
|
||||||
user: "{{skill_template}}" # skill template 內含 {{drafts}} {{existing_entities}} {{entity_name}} 變數
|
|
||||||
|
|
||||||
# 期待輸出
|
|
||||||
output:
|
|
||||||
format: json # claude_api 自動 parse 為 object
|
|
||||||
schema: # zod-style,parse 失敗回 success:false
|
|
||||||
type: object
|
|
||||||
required: [triplets, entities, paragraphs, source_summary]
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2.2 Recipe 解析在 cypher-executor(架構選擇 B)
|
|
||||||
|
|
||||||
**設計決策**(2026-05-07):recipe 解析跟 prompt 組裝**在 cypher-executor TS**,不改既有 claude_api WASM。
|
|
||||||
|
|
||||||
理由:
|
|
||||||
1. recipe 解析是 cypher-executor 既有 `api_recipe / auth_recipe` 同性質工作
|
|
||||||
2. 既有 claude_api 已部署 + 已測試,不動影響面最小
|
|
||||||
3. transform 邏輯(json_array / extract_field 等)TS 寫起來比 TinyGo 簡單 10 倍
|
|
||||||
4. 不違反 §1.6 — skill 還是 KBDB block,cypher-executor 只是組合者,不寫死 prompt
|
|
||||||
|
|
||||||
**流程:**
|
|
||||||
|
|
||||||
```
|
|
||||||
workflow YAML 節點 config 出現 `recipe: prompt_recipe:xxx`
|
|
||||||
│
|
|
||||||
▼
|
|
||||||
cypher-executor graph-executor.ts
|
|
||||||
在執行該節點前 → 偵測 recipe 欄位 → 走 recipe expander
|
|
||||||
│
|
|
||||||
▼
|
|
||||||
recipe expander(新 module)
|
|
||||||
1. 從 RECIPES KV 抓 `prompt_recipe:xxx` 定義
|
|
||||||
2. 按 fragments 規則 → 用既有 KBDB client 抓 block content
|
|
||||||
3. 按 inputs 規則 → 從 context 取值 + 跑 transform
|
|
||||||
4. 組 system prompt + user prompt
|
|
||||||
5. 把 {prompt, model, mira_token, ...} 當作節點實際 input
|
|
||||||
│
|
|
||||||
▼
|
|
||||||
loader 呼叫 claude_api 容器(不知道 recipe 存在,仍吃舊介面)
|
|
||||||
│
|
|
||||||
▼
|
|
||||||
claude_api 容器 → Mira daemon → 回 LLM 結果
|
|
||||||
│
|
|
||||||
▼
|
|
||||||
graph-executor 取結果 → 按 recipe.output 規則 parse JSON / 驗 schema
|
|
||||||
```
|
|
||||||
|
|
||||||
**對 claude_api 容器的影響**:完全沒有。它仍吃 `{mira_token, prompt, model}`。
|
|
||||||
|
|
||||||
**對 workflow 作者的體驗**:
|
|
||||||
```yaml
|
|
||||||
config:
|
|
||||||
synthesize:
|
|
||||||
component: claude_api
|
|
||||||
recipe: "prompt_recipe:wiki_synthesis" # ← cypher-executor 偵測到這欄位,自動解析
|
|
||||||
mira_token: "{{secret.mira_token}}"
|
|
||||||
```
|
|
||||||
|
|
||||||
不寫 recipe 走舊路:
|
|
||||||
```yaml
|
|
||||||
config:
|
|
||||||
reply:
|
|
||||||
component: claude_api
|
|
||||||
prompt: "{{ctx.user_message}}" # ← 沒 recipe,cypher-executor 直接透傳
|
|
||||||
mira_token: "{{secret.mira_token}}"
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2.3 Workflow YAML 體驗
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
name: wiki_synthesis
|
|
||||||
flow:
|
|
||||||
- "input >> 完成後 >> read_stale"
|
|
||||||
- "read_stale >> 對每個 >> read_drafts"
|
|
||||||
- "read_drafts >> 完成後 >> synthesize"
|
|
||||||
- "synthesize >> 完成後 >> write_wiki"
|
|
||||||
config:
|
|
||||||
read_stale:
|
|
||||||
component: kbdb_get
|
|
||||||
page_name: "mira-wiki-index-stale"
|
|
||||||
read_drafts:
|
|
||||||
component: kbdb_get
|
|
||||||
page_name: "{{loop.item}}" # entity name
|
|
||||||
synthesize:
|
|
||||||
component: claude_api
|
|
||||||
recipe: "prompt_recipe:wiki_synthesis" # ← 重點:指 recipe,不寫 prompt
|
|
||||||
mira_token: "{{secret.mira_token}}"
|
|
||||||
write_wiki:
|
|
||||||
component: kbdb_ingest
|
|
||||||
text: "{{prev.paragraphs}}"
|
|
||||||
```
|
|
||||||
|
|
||||||
**AI 寫這 workflow 只需要:**
|
|
||||||
1. 知道有 `kbdb_get / claude_api / kbdb_ingest` 三個容器(MCP search 找得到)
|
|
||||||
2. 知道有 `prompt_recipe:wiki_synthesis` 這個配方(MCP search 找得到)
|
|
||||||
3. 不需要懂 prompt 怎麼組、不需要看 wiki schema 文字
|
|
||||||
|
|
||||||
### 2.4 Recipe 是 KBDB block 還是 KV?
|
|
||||||
|
|
||||||
**選 KV**(`RECIPES` namespace),跟既有 auth_recipe / api_recipe 一致:
|
|
||||||
- key: `prompt_recipe:{name}`
|
|
||||||
- value: YAML/JSON
|
|
||||||
- CLI 跟 MCP 用既有 `recipe push` / `recipe list` 工具管理(不需新工具)
|
|
||||||
|
|
||||||
**不選 KBDB block**:
|
|
||||||
- 雖然 polaris/mira/CLAUDE.md §1.6 說「source-skill 存 KBDB block」
|
|
||||||
- 但 §1.6 講的是 mira 業務的 skill template(schema / skill 模板)
|
|
||||||
- recipe 是「組合配方」(指向哪些 block + 怎麼組),是 platform 層
|
|
||||||
- recipe **裡面** 引用 KBDB block id(fragments.source: kbdb_block)— 兩層關係清楚
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 3. 範圍邊界
|
|
||||||
|
|
||||||
**在本 SDD 範圍內:**
|
|
||||||
- ✅ Phase 1: prompt_recipe schema + RECIPES KV 規範
|
|
||||||
- ✅ Phase 2: claude_api 改吃 recipe(向後相容舊 prompt 參數)
|
|
||||||
- ✅ Phase 3: 寫第一個 recipe `prompt_recipe:wiki_synthesis`
|
|
||||||
- ✅ Phase 4: 用此 recipe 完成 mira 7-B workflow
|
|
||||||
- ✅ Phase 5: MCP 加 recipe 管理 tool(list / get / push / delete prompt_recipe)
|
|
||||||
|
|
||||||
**不在範圍內:**
|
|
||||||
- HTTP api_recipe / auth_recipe 改造(已有,不動)
|
|
||||||
- 多模態 prompt(image input)— 等 P2
|
|
||||||
- recipe 沙盒驗收(recipe 是資料不是 code,不需要)
|
|
||||||
|
|
||||||
**前置依賴(已完成):**
|
|
||||||
- ✅ kbdb_get 零件(5.3)
|
|
||||||
- ✅ component-registry MCP backfill(component-registry-canon Phase 1)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 4. 為什麼這個設計重要
|
|
||||||
|
|
||||||
| n8n | arcrun |
|
|
||||||
|---|---|
|
|
||||||
| Gmail node、Slack node、OpenAI node、Anthropic node、各 LLM node ⋯(每種 API 一個 node)| `http_request` 容器 + 各 service 的 api_recipe |
|
|
||||||
| 每個 LLM 用法新 node(chat / completion / embedding)| `claude_api` 容器 + 各用途的 prompt_recipe |
|
|
||||||
| AI 要學「Gmail node 怎麼用」「Slack node 怎麼用」⋯ | AI 要學「容器 + 配方」一次學會 |
|
|
||||||
| 零件數爆炸(500+) | 容器固定(< 30),配方無限擴充 |
|
|
||||||
| 配方藏在程式碼 | 配方在 KV,AI 直接 CRUD |
|
|
||||||
|
|
||||||
**對 AI 推廣**:第三方 AI 看到「30 個容器 + 100 個配方」遠比「500 個 node」好理解,且配方是文字資料不是 code,AI 寫配方比寫 node 簡單。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 5. 風險與緩解
|
|
||||||
|
|
||||||
| 風險 | 緩解 |
|
|
||||||
|---|---|
|
|
||||||
| recipe 結構過度複雜,AI 寫不出來 | Phase 3 寫第一個 recipe(wiki_synthesis)作為範本,未來 AI 抄 |
|
|
||||||
| 向後相容讓 claude_api 變兩條路 | 內部統一用 recipe path,舊 prompt 參數 → 自動轉成 inline recipe |
|
|
||||||
| recipe 引用 KBDB block id 寫死,block 改 id 就壞 | KBDB block 用 `page_name` 識別比 id 穩定,recipe 支援 `block_page_name` 欄位 |
|
|
||||||
| KV 寫入頻繁的 transform 邏輯(json_array, extract_field:x)→ 變 mini DSL | 限制 transform 種類(10 個內),列白名單,超過就請寫零件 |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 6. 變更紀錄
|
|
||||||
|
|
||||||
| 版本 | 日期 | 內容 |
|
|
||||||
|---|---|---|
|
|
||||||
| v1.0 | 2026-05-07 | 初版。吃狗糧寫 wiki 合成 workflow 撞到「prompt 組裝缺口」,補 prompt_recipe 層平行於既有 auth_recipe / api_recipe。 |
|
|
||||||
| v1.1 | 2026-05-07 | 架構選擇 B:recipe 解析在 cypher-executor TS(不改 claude_api WASM)。減少改動面、可單元測試、跟既有 api_recipe 同層次。 |
|
|
||||||
@@ -1,110 +0,0 @@
|
|||||||
# Tasks — Recipe System (容器 + Recipe 模式)
|
|
||||||
|
|
||||||
> 對應 SDD:[design.md](design.md)
|
|
||||||
> 上次更新:2026-05-07
|
|
||||||
|
|
||||||
**狀態 legend**:`[ ]` 待辦 / `[🔄]` 進行中 / `[x]` 完成
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Phase 1:prompt_recipe Schema + KV 規範
|
|
||||||
|
|
||||||
- [x] 1.1 寫 `cypher-executor/src/lib/prompt-recipe-schema.ts`(85 行 Zod schema:fragments / inputs / prompt_assembly / output + transform 白名單 7 個)
|
|
||||||
- [x] 1.2 確認 cypher-executor wrangler.toml 已有 RECIPES KV binding
|
|
||||||
- [x] 1.3 寫 recipe loader (`recipe-loader.ts` 50 行) + transforms (`recipe-transforms.ts` 58 行) + expander (`recipe-expander.ts` 127 行)
|
|
||||||
- transform 7 個:json_array / to_string / join / markdown_list / extract_field / first / pluck_content
|
|
||||||
- expander:fragments(KBDB) + inputs(context+transform) → 套 {{var}} 模板 → {prompt, model, output_*}
|
|
||||||
- type-check 全通過
|
|
||||||
|
|
||||||
## Phase 2:cypher-executor recipe expander(架構選擇 B,不改 claude_api)
|
|
||||||
|
|
||||||
- [x] 2.1 寫 `recipe-expander.ts`(127 行:load → fragments → inputs+transform → 套模板 → 回傳 prompt+model+output_*)
|
|
||||||
- [x] 2.2 寫 `recipe-transforms.ts`(58 行:7 個 transform)
|
|
||||||
- [x] 2.3 改 `graph-executor.ts` Component case:偵測 `node.data.recipe` → 呼叫 expandPromptRecipe → merge 進 mergedContext
|
|
||||||
- [x] 2.4 output parser hook:執行完若 `_recipe_output_format === 'json'` 自動 parse + required_fields 驗證
|
|
||||||
- [x] 2.5 部署 cypher-executor v426b099e
|
|
||||||
- [x] 2.6 端對端驗證:用 curl 打 `/cypher/execute` 帶 recipe,trace 顯示 recipe 展開正確 + claude_api 拿到組好 prompt(Mira daemon 端 522 timeout 是 daemon 問題,不是 recipe 系統)
|
|
||||||
- [x] 2.7 [紅利修復] cypher-executor `WASM_HTTP_RUNNER_IDS` 加 5 個 mira 零件(claude_api / kbdb_*)— 短期解,根本修法見 KI-13
|
|
||||||
|
|
||||||
## Phase 3:第一個 recipe — wiki_synthesis
|
|
||||||
|
|
||||||
- [x] 3.1 寫 `polaris/mira/recipes/wiki_synthesis.json`(4 fragments + 4 inputs + system/user template + json output)
|
|
||||||
- [x] 3.2 用 `wrangler kv key put --remote` 推進 RECIPES KV (key: `prompt_recipe:wiki_synthesis`)
|
|
||||||
- [x] 3.3 確認 KV 寫入成功(wrangler kv get 驗證)
|
|
||||||
- [ ] 3.4 不適用(架構選擇 B 不改 claude_api,recipe 在 cypher-executor 解析)
|
|
||||||
- [x] 3.5 端對端測試:用 MCP `u6u_execute_workflow` 跑 wiki_synthesis 成功
|
|
||||||
- input:1 句草稿(黃仁勳 GTC 2026 物理 AI)
|
|
||||||
- output:3 triplets + 3 entities + 1 wiki paragraph + source_summary
|
|
||||||
- 過程修了 KI-14 (service binding 指錯)、KI-15 (token 沒轉發)、KI-16 (Claude markdown fence 沒剝)
|
|
||||||
|
|
||||||
## Phase 4:mira 7-B 用 recipe 完成 wiki workflow
|
|
||||||
|
|
||||||
- [🔄] 4.1 寫 `polaris/mira/workflows/wiki_synthesis.yaml`(cypher binding YAML)
|
|
||||||
- 用 `recipe: prompt_recipe:wiki_synthesis` 指 recipe
|
|
||||||
- 4-5 個節點:read_stale → foreach → read_drafts → synthesize → write_wiki + log
|
|
||||||
- [ ] 4.2 用 MCP `u6u_execute_workflow` sandbox 跑(試一個 entity 不真寫 KBDB)
|
|
||||||
- [ ] 4.3 用 MCP `u6u_deploy_workflow` 部署到 cypher-executor
|
|
||||||
- [ ] 4.4 手動觸發 cron,驗 wiki page 真的出現
|
|
||||||
- [ ] 4.5 在 mira/wiki/ 前端看到第一張 AI 合成 wiki page
|
|
||||||
|
|
||||||
## Phase 5:MCP recipe 管理 tools
|
|
||||||
|
|
||||||
- [ ] 5.1 加 MCP tool `arcrun_list_recipes(prefix?)`:列所有 prompt_recipe
|
|
||||||
- [ ] 5.2 加 MCP tool `arcrun_get_recipe(name)`:取單一 recipe 內容
|
|
||||||
- [ ] 5.3 加 MCP tool `arcrun_push_recipe(name, yaml_content)`:upsert recipe
|
|
||||||
- [ ] 5.4 加 MCP tool `arcrun_delete_recipe(name)`
|
|
||||||
- [ ] 5.5 既有 auth_recipe / api_recipe 也通用同套 tool(不只 prompt_recipe)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 風險追蹤
|
|
||||||
|
|
||||||
- 風險 1:claude_api 改造跟 mira-app 同時動,可能影響河道 AI 副駕
|
|
||||||
- 緩解:向後相容,舊 input 仍可用,mira 河道先不切 recipe
|
|
||||||
- 風險 2:recipe transform 白名單漏了某種需求
|
|
||||||
- 緩解:發現缺什麼再加,第一版優先支援 wiki 用到的(json_array, extract_field, join)
|
|
||||||
- 風險 3:KV 跟 KBDB 都存配置,AI 困惑「該存哪邊」
|
|
||||||
- 緩解:清楚分層 — recipe(容器組合方式) KV,data(schema 文字、skill 模板) KBDB
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Known Issues(吃狗糧發現,記錄)
|
|
||||||
|
|
||||||
### KI-11:MCP `u6u_execute_workflow` 不暴露 config 欄位 ✅ 修復(2026-05-07)
|
|
||||||
- 已修:tool schema 加 optional `config: Record<string, Record<string, any>>`
|
|
||||||
- 部署:u6u-mcp v11d7e366
|
|
||||||
- 用戶要重啟 client session 才能看到新 schema
|
|
||||||
|
|
||||||
### KI-12:MCP execute 路由打 `/execute` 而非 `/cypher/execute` ✅ 修復(2026-05-07)
|
|
||||||
- 已修:service binding fetch URL 改成 `http://cypher-executor/cypher/execute`
|
|
||||||
- 部署:u6u-mcp v11d7e366
|
|
||||||
|
|
||||||
### KI-14:u6u-mcp service binding 指向已廢棄的 inkstone-cypher-executor ✅ 修復
|
|
||||||
- 現象:MCP 路徑跑 workflow trace 顯示 synth 變 Output、config 被忽略
|
|
||||||
- 根因:`u6u-mcp/wrangler.toml` services binding 是舊 worker `inkstone-cypher-executor`,不是現役 `arcrun-cypher-executor`
|
|
||||||
- 解法:改 service name + redeploy
|
|
||||||
|
|
||||||
### KI-15:u6u-mcp 沒把 partner token 轉發給 cypher-executor ✅ 修復
|
|
||||||
- 現象:recipe expander 抓 KBDB block 401(沒 auth)
|
|
||||||
- 根因:partnerAuthMiddleware 驗完 token 但只 set org_namespace,沒留 token;execute_workflow tool fetch 沒帶 X-Arcrun-API-Key
|
|
||||||
- 解法:middleware 也 set partner_token、handleMcpRequest + registerAllTools + execute_workflow 多一個 partnerToken 參數、fetch header 加 X-Arcrun-API-Key
|
|
||||||
|
|
||||||
### KI-16:Recipe JSON output 被 Claude 包在 ```json``` markdown fence ✅ 修復
|
|
||||||
- 現象:JSON.parse 失敗 "Unexpected token \`"
|
|
||||||
- 根因:Claude 預設輸出 ```json\n{...}\n``` 包裝
|
|
||||||
- 解法:cypher-executor 解析前 regex 剝 fence
|
|
||||||
|
|
||||||
### KI-13:cypher-executor `WASM_HTTP_RUNNER_IDS` 寫死白名單
|
|
||||||
- 現象:每加新零件要回 cypher-executor 改白名單 + 重部署
|
|
||||||
- 影響:違反 arcrun「容器+ recipe,新零件無需改 platform」承諾
|
|
||||||
- 短期解:手動加進白名單(claude_api / kbdb_* 已加)
|
|
||||||
- 根本解:改成從 component-registry KV 動態查 canonical_id
|
|
||||||
- 優先級:P1(推廣破口),需新 SDD `cypher-executor-dynamic-component-discovery`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 對外推廣(Phase 6+,本 SDD 不執行,記錄)
|
|
||||||
|
|
||||||
- README 示範「容器 + recipe = 一個 service」(Gmail / Slack / Claude)
|
|
||||||
- onboarding kit GitHub template 內含 5 個經典 recipe 當範例
|
|
||||||
- 「recipe market」想法:用戶分享 recipe 幫他人少寫 prompt
|
|
||||||
@@ -1,285 +0,0 @@
|
|||||||
# SDD: Resumable Workflow(webhook callback 喚醒)
|
|
||||||
|
|
||||||
> 2026-05-07 建立。狗糧寫 wiki 合成 workflow 時,Mira daemon 對長草稿(>2KB)切非同步模式回 `{pending, task_id, poll_url}`,cypher-executor 沒處理就直接傳下游。
|
|
||||||
> 本 SDD 解這層:**workflow 跑到一半遇到 pending 任務 → 暫停 + 持久化狀態 → 外部 callback 進來時喚醒繼續**。
|
|
||||||
> 範圍:兩家自家服務之間(Mira daemon ↔ cypher-executor)走 webhook 推。對外服務無 webhook 的場景留 wishlist 用 poll 解。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 1. 問題
|
|
||||||
|
|
||||||
### 1.1 撞牆現場
|
|
||||||
|
|
||||||
wiki 合成 workflow 第一節點 `claude_api(recipe:wiki_synthesis)`:
|
|
||||||
- 短草稿(< 2KB)→ daemon 同步回 `{success, data: {text}}`,recipe output parser 解 JSON 成功
|
|
||||||
- 長草稿(> 2KB)→ daemon 估 75s,切非同步模式回:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"success": true,
|
|
||||||
"pending": true,
|
|
||||||
"task_id": "task_14_1778133152480",
|
|
||||||
"poll_url": "https://mira.uncle6.me/mira/execute/task_14_1778133152480",
|
|
||||||
"estimated_seconds": 75
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
cypher-executor 拿到這個物件就當 result,但裡面沒 `data.text`,下游 recipe output parser 找不到要 parse 的東西,整個 workflow 算「success」但實際上 wiki 還沒生出來。
|
|
||||||
|
|
||||||
### 1.2 現有 toolkit 不夠
|
|
||||||
|
|
||||||
- `wait` 零件:固定 sleep N ms,沒 retry / 條件判斷
|
|
||||||
- `http_request` 零件:通用 HTTP,不認 daemon 的 polling 協議
|
|
||||||
- cypher-executor `visited` Set:擋住節點重訪,沒辦法做迴圈式 poll
|
|
||||||
- Worker CPU 30s 限制:同步 poll 75s 任務不可能
|
|
||||||
|
|
||||||
### 1.3 Push vs Pull 抉擇(2026-05-07 拍板)
|
|
||||||
|
|
||||||
| | Webhook 推 | Poll 拉 |
|
|
||||||
|---|---|---|
|
|
||||||
| 適用 | 雙方都自家 | 對方無 callback 能力 |
|
|
||||||
| Worker 時間消耗 | 趨近 0 | 全程占用 |
|
|
||||||
| 時長限制 | 無 | Worker CPU 30s |
|
|
||||||
| 工程位置 | runtime 能力(cypher-executor)| 零件(poll_task) |
|
|
||||||
|
|
||||||
**走 Webhook 推**(自家服務優先,poll_task 進 wishlist)。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 2. 設計
|
|
||||||
|
|
||||||
### 2.1 三層改動
|
|
||||||
|
|
||||||
**A. Mira daemon 端(infra/cloud-cto)**
|
|
||||||
- `/mira/execute` 接受新欄位 `callback_url: string`(optional)
|
|
||||||
- task 完成時 POST 到 `callback_url`,body:
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"task_id": "task_14_xxx",
|
|
||||||
"success": true,
|
|
||||||
"data": { "text": "..." }
|
|
||||||
}
|
|
||||||
```
|
|
||||||
- 失敗也要 callback,body 含 `error` 欄位
|
|
||||||
- 重試策略:3 次 backoff(1s / 5s / 30s),最後失敗就放棄(task 狀態存進 daemon 自己 KV)
|
|
||||||
|
|
||||||
**B. cypher-executor 端(resumable runtime)**
|
|
||||||
|
|
||||||
新概念:**workflow run 可以暫停**。
|
|
||||||
|
|
||||||
設計:
|
|
||||||
1. 新 KV namespace(或用既有 `EXEC_CONTEXT`)存暫停的 run state:
|
|
||||||
- key: `paused_run:{task_id}` 或 `paused_run:{run_id}`
|
|
||||||
- value: `{ run_id, graph, paused_node_id, paused_node_pending_result, context, trace_so_far, kv_store_ref, expires_at }`
|
|
||||||
2. graph-executor 偵測節點 result 含 `pending: true` + `task_id` → 暫停 + 寫 KV + 回 `{paused: true, task_id, run_id}`
|
|
||||||
3. 新 endpoint `POST /workflows/resume`:
|
|
||||||
- body: `{ task_id, result }`(result 是 daemon callback 給的完整資料)
|
|
||||||
- 從 KV 拿 paused state → merge result 進 paused_node 的 output → 從下個節點繼續執行
|
|
||||||
4. claude_api 容器呼叫 daemon 時自動帶 `callback_url`:
|
|
||||||
- `https://cypher.arcrun.dev/workflows/resume?task_id={預先派發的 task_id}`
|
|
||||||
- 但 task_id 是 daemon 自己派的,cypher-executor 不知道。需先 daemon 派完 task_id 才能組 URL
|
|
||||||
- 解:daemon 改成「先回 task_id,再啟動實際工作 + 完成時 callback」— 兩階段 hand-shake
|
|
||||||
|
|
||||||
實際流程(兩階段):
|
|
||||||
|
|
||||||
```
|
|
||||||
cypher-executor Mira daemon
|
|
||||||
│ │
|
|
||||||
│ POST /mira/execute │
|
|
||||||
│ { prompt, │
|
|
||||||
│ callback_url: "?run_id=R1" }
|
|
||||||
├─────────────────────────────>│
|
|
||||||
│ │ 立即回 task_id(決定走非同步)
|
|
||||||
│<─────────────────────────────┤ { pending, task_id: T9 }
|
|
||||||
│ │
|
|
||||||
├─ 看到 pending → 寫 KV │ 啟動實際 LLM 任務
|
|
||||||
│ paused_run:T9 = {run R1, │
|
|
||||||
│ paused_node, ctx, ...} │
|
|
||||||
│ │
|
|
||||||
│ 立即回 client (MCP): │
|
|
||||||
│ { paused, task_id: T9 } │
|
|
||||||
│ │
|
|
||||||
⋯⋯⋯⋯⋯ 75s 後 ⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯
|
|
||||||
│ │ task done
|
|
||||||
│ POST /workflows/resume │
|
|
||||||
│ { task_id: T9, result: {...} }
|
|
||||||
│<─────────────────────────────┤
|
|
||||||
│ │
|
|
||||||
│ 從 KV 取 paused_run:T9 │
|
|
||||||
│ → merge result 進 paused 節點 │
|
|
||||||
│ → 從下個節點繼續 │
|
|
||||||
│ │
|
|
||||||
│ run 跑完 → 寫 trace │
|
|
||||||
│ → 通知 client (?) │
|
|
||||||
│ │
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2.2 範圍邊界
|
|
||||||
|
|
||||||
**第一版(v1)做:**
|
|
||||||
- ✅ 單節點 pending → resume(最常見:claude_api 拿到 daemon pending)
|
|
||||||
- ✅ daemon 加 callback_url 支援
|
|
||||||
- ✅ cypher-executor `/workflows/resume` endpoint
|
|
||||||
- ✅ run state 寫 EXEC_CONTEXT KV,含 24h TTL(避免 KV 累積)
|
|
||||||
- ✅ 整合測:用 wiki 合成跑長草稿,驗 callback 進來能繼續
|
|
||||||
|
|
||||||
**第一版不做:**
|
|
||||||
- ❌ 多節點都 pending 的 nested 場景(例如 claude_api → 又一個 claude_api)— v2
|
|
||||||
- ❌ foreach 內 pending(item-level resume)— v2
|
|
||||||
- ❌ pending 期間用戶看到「進度」的前端 UI — 走 trace 有 paused 標記,前端 polling 自己做即可
|
|
||||||
- ❌ pending callback 失敗時的 retry / DLQ — v2,先記 log
|
|
||||||
|
|
||||||
**前置依賴:**
|
|
||||||
- ✅ recipe-system 已部署(cypher-executor 已會解 recipe)
|
|
||||||
- ✅ Mira daemon 在 Hetzner,可改 code
|
|
||||||
|
|
||||||
### 2.3 為什麼不用 Cloudflare Queues / Durable Objects
|
|
||||||
|
|
||||||
- **CF Queues**:適合大量 fan-out,這裡是點對點 callback,KV 已夠
|
|
||||||
- **Durable Objects**:long-lived state 比 KV 強,但成本高 + 複雜
|
|
||||||
- **EXEC_CONTEXT KV**:既有 binding,工程量最小
|
|
||||||
|
|
||||||
未來真撞到 KV 限制(每 partner 寫入頻率上限)再升級。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 3. 詳細設計
|
|
||||||
|
|
||||||
### 3.1 daemon 端 callback 機制
|
|
||||||
|
|
||||||
`infra/cloud-cto/index.js`(Mira daemon):
|
|
||||||
|
|
||||||
```js
|
|
||||||
// /mira/execute handler
|
|
||||||
{
|
|
||||||
// 既有 input + 新加:
|
|
||||||
callback_url: string // optional
|
|
||||||
}
|
|
||||||
|
|
||||||
// 處理邏輯:
|
|
||||||
// 1. 啟動 task(既有邏輯)
|
|
||||||
// 2. 預估時間 > 30s → 切非同步:
|
|
||||||
// - 立即回 { success: true, pending: true, task_id, poll_url, estimated_seconds }
|
|
||||||
// - 背景 task 完成時:
|
|
||||||
// if (callback_url) POST callback_url with { task_id, success, data, error? }
|
|
||||||
// (不論用戶有沒有 poll,callback 一定會送)
|
|
||||||
```
|
|
||||||
|
|
||||||
callback 失敗策略:
|
|
||||||
- 3 次重試(1s / 5s / 30s)
|
|
||||||
- 全失敗:task 狀態維持完成,等 client 主動 poll(poll_url 仍有效)
|
|
||||||
- 超過 24h 沒被消化的 task:daemon GC
|
|
||||||
|
|
||||||
### 3.2 cypher-executor 端 resumable runtime
|
|
||||||
|
|
||||||
#### 3.2.1 偵測 pending(graph-executor)
|
|
||||||
|
|
||||||
在 Component case,runner 回傳後:
|
|
||||||
|
|
||||||
```ts
|
|
||||||
result = await runner(mergedContext);
|
|
||||||
|
|
||||||
// 偵測 pending pattern(daemon 約定的回應結構)
|
|
||||||
if (isResumablePending(result)) {
|
|
||||||
await persistPausedRun(this.env.EXEC_CONTEXT, taskIdFromResult(result), {
|
|
||||||
run_id, graph, paused_node_id: node.id, paused_context: context,
|
|
||||||
paused_result: result, trace_so_far: trace, expires_at: Date.now() + 24*60*60*1000
|
|
||||||
});
|
|
||||||
// 提早結束此 run,回 paused 狀態
|
|
||||||
return { paused: true, task_id, run_id };
|
|
||||||
}
|
|
||||||
|
|
||||||
// ... 既有的 recipe output parsing / kvSetNodeOutput / 等
|
|
||||||
```
|
|
||||||
|
|
||||||
`isResumablePending(result)` = `result?.pending === true && typeof result?.task_id === 'string'`
|
|
||||||
|
|
||||||
#### 3.2.2 callback URL 注入(claude_api 之前的 layer)
|
|
||||||
|
|
||||||
問題:claude_api 容器發 daemon 請求時,要帶 `callback_url`。但 task_id 是 daemon 派的,URL 裡只能放 run_id,daemon 收到 callback 時填 task_id:
|
|
||||||
|
|
||||||
`callback_url = https://cypher.arcrun.dev/workflows/resume?run_id={current_run_id}`
|
|
||||||
|
|
||||||
但 cypher-executor 端用 task_id 找 paused state(一個 run 可能多個 pending),所以 callback URL 應該是:
|
|
||||||
|
|
||||||
`callback_url = https://cypher.arcrun.dev/workflows/resume`(不帶 query,task_id 在 body)
|
|
||||||
|
|
||||||
**實作位置**:在 graph-executor 呼叫 claude_api 前,自動注入 `callback_url` 到 mergedContext:
|
|
||||||
|
|
||||||
```ts
|
|
||||||
if (node.componentId === 'claude_api' && this.env?.PUBLIC_BASE_URL) {
|
|
||||||
mergedContext.callback_url = `${this.env.PUBLIC_BASE_URL}/workflows/resume`;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
> 暫先用「componentId 寫死匹配」是 hacky,未來 component contract 加 `supports_async_callback: true` 標記就 generic 了。
|
|
||||||
|
|
||||||
#### 3.2.3 resume endpoint
|
|
||||||
|
|
||||||
`POST /workflows/resume`:
|
|
||||||
|
|
||||||
```ts
|
|
||||||
{
|
|
||||||
task_id: string, // daemon 給的
|
|
||||||
success: boolean,
|
|
||||||
data?: { text: string }, // 跟同步呼叫一樣的結構
|
|
||||||
error?: string
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
處理:
|
|
||||||
1. 從 EXEC_CONTEXT KV `paused_run:{task_id}` 拿 state
|
|
||||||
2. 沒拿到(過期 / 重複 callback)→ 回 200 + log
|
|
||||||
3. 把 callback 給的 result 當作 paused_node 的 output
|
|
||||||
4. 重建 GraphExecutor,從下個節點繼續執行
|
|
||||||
5. 跑完寫完整 trace
|
|
||||||
|
|
||||||
**問題:resume 後沒辦法再回給原 client。** 用戶最初打 `/cypher/execute`(同步),拿到 `{paused, task_id}` 之後就斷了;resume 跑完 result 沒地方送。
|
|
||||||
|
|
||||||
**v1 解法**:resume 完寫進 `analytics_kv` 或 D1,**用戶要主動 query**。簡單但 UX 差。
|
|
||||||
**v2 想法**:resume 完發另一個 webhook 給原 client(client 在 trigger 時帶 final_callback_url)。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 4. 範圍
|
|
||||||
|
|
||||||
**在本 SDD 範圍內:**
|
|
||||||
- 4.1 daemon `/mira/execute` 加 callback_url 支援
|
|
||||||
- 4.2 cypher-executor 偵測 pending + 持久化 paused state
|
|
||||||
- 4.3 cypher-executor `/workflows/resume` endpoint
|
|
||||||
- 4.4 callback_url 自動注入(claude_api 場景)
|
|
||||||
- 4.5 wiki 合成 workflow 用長草稿端對端測試
|
|
||||||
|
|
||||||
**不在本 SDD 範圍:**
|
|
||||||
- nested pending(v2)
|
|
||||||
- foreach 內 pending(v2)
|
|
||||||
- final_callback 給原 client(v2)
|
|
||||||
- poll_task 零件(wishlist)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 5. 驗收標準
|
|
||||||
|
|
||||||
1. wiki 合成 workflow 餵 5KB+ 草稿,跑完後 wiki page 有寫進 KBDB(不再 trace `pending` 假成功)
|
|
||||||
2. trace 有 `paused` 紀錄,能看到 task_id
|
|
||||||
3. 從 daemon 觸發 callback 後 < 5s 內 cypher-executor 把 paused state 撿起來繼續
|
|
||||||
4. 24h 沒 callback 的 paused state KV 自動 expire(看 KV TTL 列表)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 6. 風險
|
|
||||||
|
|
||||||
| 風險 | 緩解 |
|
|
||||||
|---|---|
|
|
||||||
| daemon callback 進來時 cypher-executor 重啟 → state 還在 KV,OK | KV 持久化 |
|
|
||||||
| 同 task_id 重複 callback(網路重試)→ 重複執行下游 | resume endpoint idempotent:拿到 state 後立刻刪 KV,重複 callback 找不到 state |
|
|
||||||
| daemon callback 失敗(網路)| daemon 端 3 retry + 24h GC,超過就需手動干預(v1 接受) |
|
|
||||||
| paused state 含敏感資料(partner key)| KV 有 24h TTL;不寫 plaintext secrets(既有 credential injection 在執行前才解,paused state 存的是執行前的 context,secret 還沒解)|
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 7. 變更紀錄
|
|
||||||
|
|
||||||
| 版本 | 日期 | 內容 |
|
|
||||||
|---|---|---|
|
|
||||||
| v1.0 | 2026-05-07 | 初版。狗糧 wiki 合成撞 daemon 非同步 → 補 resumable workflow runtime。第一版只做單節點 pending + claude_api callback 注入。|
|
|
||||||
@@ -1,61 +0,0 @@
|
|||||||
# Tasks — Resumable Workflow
|
|
||||||
|
|
||||||
> 對應 SDD:[design.md](design.md)
|
|
||||||
> 上次更新:2026-05-07
|
|
||||||
|
|
||||||
**狀態 legend**:`[ ]` 待辦 / `[🔄]` 進行中 / `[x]` 完成
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Phase 1:Mira daemon 端 callback 支援
|
|
||||||
|
|
||||||
- [x] 1.1 改 `/opt/mira/mira-daemon.js`(Hetzner mira container)`/execute` 接受 `params.callback_url`
|
|
||||||
- [x] 1.2 fireCallback function:task done/failed 時 POST callback_url,body = `{task_id, success, data?, error?}`
|
|
||||||
- [x] 1.3 callback retry:4 次(立即 + 1s/5s/30s backoff),全失敗 log
|
|
||||||
- [x] 1.4 patch script 寫好 `/tmp/patch-mira-daemon.py`,docker cp 進 container(注意:rebuild image 會丟失,需重 patch 或正式 commit 進 Dockerfile/git repo)
|
|
||||||
- [x] 1.5 真實端對端驗證:daemon log 顯示 `[Mira callback] task=task_2_... POST https://cypher.arcrun.dev/workflows/resume OK 200`(2026-05-07 07:24:04 + task_3 短測試)
|
|
||||||
|
|
||||||
## Phase 2:cypher-executor resumable runtime
|
|
||||||
|
|
||||||
- [x] 2.1 寫 `paused-runs.ts`(81 行):persistPausedRun / loadPausedRun / consumePausedRun + isResumablePending 偵測器,24h TTL
|
|
||||||
- [x] 2.2 改 `graph-executor.ts` Component case:偵測 pending → 寫 KV + throw WorkflowPaused
|
|
||||||
- [x] 2.3 改 `cypher-handlers.ts`:catch WorkflowPaused → 回 `{success:true, paused:true, task_id, run_id, paused_node_id, trace, graph}`
|
|
||||||
- [x] 2.4 callback_url 自動注入:componentId==='claude_api' 時 mergedContext.callback_url = PUBLIC_BASE_URL 或預設 cypher.arcrun.dev/workflows/resume
|
|
||||||
|
|
||||||
## Phase 3:resume endpoint
|
|
||||||
|
|
||||||
- [x] 3.1 寫 `routes/resume.ts`:POST /workflows/resume,consumePausedRun → resumeFromPaused
|
|
||||||
- [x] 3.2 graph-executor 加 `resumeFromPaused()` 方法:把 callback_result 當 paused_node 輸出 + spread 進 ctx + 從下游節點繼續
|
|
||||||
- [x] 3.3 idempotent 驗證:第二次 callback 回 `{noop:true, reason:"state 不存在或過期"}`
|
|
||||||
- [x] 3.4 cypher-executor 部署 v0580980b
|
|
||||||
- [x] 3.5 mount /workflows/resume 進 index.ts
|
|
||||||
|
|
||||||
## Phase 4:claude_api 容器透傳 callback_url
|
|
||||||
|
|
||||||
- [x] 4.1 改 `claude_api/main.go`:Input 加 CallbackURL;timeout 預設改 120s
|
|
||||||
- [x] 4.2 重 build wasm + redeploy claude-api.arcrun.dev (v f926e3dd)
|
|
||||||
- [x] 4.3 真實端對端驗證:daemon 收到 callback_url → task done 後 POST cypher-executor/workflows/resume → 200 OK
|
|
||||||
|
|
||||||
## Phase 5:端對端整合測試
|
|
||||||
|
|
||||||
- [ ] 5.1 用 MCP `u6u_execute_workflow` 跑 wiki 合成 + 5KB+ 草稿
|
|
||||||
- [ ] 5.2 第一次回應應為 `{paused, task_id, run_id}`
|
|
||||||
- [ ] 5.3 等 daemon callback 進來(log 看到 /workflows/resume 命中)
|
|
||||||
- [ ] 5.4 觀察 wiki page 真的寫進 KBDB(即使原 MCP call 已斷線)
|
|
||||||
- [ ] 5.5 trace 含完整節點紀錄(paused → resumed)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 風險追蹤
|
|
||||||
|
|
||||||
- 風險 1:daemon callback 進來時,cypher.arcrun.dev 還沒醒(CF Worker cold start)→ 第一次 retry 接住(daemon retry policy 涵蓋)
|
|
||||||
- 風險 2:v1 沒 final_callback 給原 client → 用戶要主動查狀態
|
|
||||||
- 接受:mira 河道 UI 可定期 refetch wiki page,或用既有 KBDB 觸發機制
|
|
||||||
- v2 加 final_callback 統一處理
|
|
||||||
|
|
||||||
## v2 已記錄
|
|
||||||
|
|
||||||
- nested pending(一個 run 多個 paused 節點)
|
|
||||||
- foreach 內 pending(item-level resume)
|
|
||||||
- final_callback 給原 client(trigger 時帶 final_callback_url)
|
|
||||||
- poll_task 零件(外部 API 沒 webhook 時用)
|
|
||||||
@@ -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`
|
|
||||||
@@ -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
|
|
||||||
@@ -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.1:Service Binding 新增警示
|
|
||||||
# ─────────────────────────────────────────────────────────────────────────────
|
|
||||||
# 偵測在 wrangler.toml 新增 [[services]] 的 echo/cat/sed 操作(非 100% 準確,但夠用)
|
|
||||||
if echo "$CMD" | grep -qE "echo.*\[\[services\]\].*>>"; then
|
|
||||||
block "3.1" \
|
|
||||||
"偵測到要在 wrangler.toml 新增 [[services]] binding" \
|
|
||||||
"零件串接一律走 HTTP URL(cypher 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
|
|
||||||
@@ -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: content;Edit: new_string;MultiEdit: 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.1:registry/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.2:cypher-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.go(TinyGo)"
|
|
||||||
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
|
|
||||||
@@ -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 URL(cypher 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 的 SDD(design.md + tasks.md)
|
|
||||||
3. 在回覆開頭宣告(格式見 .claude/rules/00-sdd-protocol.md):
|
|
||||||
📋 已讀 SDD:<檔案清單>
|
|
||||||
🎯 本次對應 task:<task 編號>
|
|
||||||
📐 本次 task 的 SDD 規範摘要:<重點>
|
|
||||||
🚧 執行範圍:修改/建立/刪除 <檔案>
|
|
||||||
4. 每完成一個 task,立刻更新 tasks.md 的 [x],不批次
|
|
||||||
|
|
||||||
🔥 當前進行中 Phase:Credential Primitives TS → WASM
|
|
||||||
|
|
||||||
SDD:.agents/specs/arcrun/credential-primitives-wasm/
|
|
||||||
已完成:Phase 0.1-0.5(核心合併 + u6u-core 刪除)
|
|
||||||
未完成硬前置:
|
|
||||||
- Phase 0.6 wasi-shim 加 host functions(kv_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
|
|
||||||
@@ -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
|
|
||||||
@@ -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 functions(kv_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 / SDK(Python/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**。
|
|
||||||
|
|
||||||
### 步驟 4:check 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 協議強制把「先讀 → 定位 → 宣告 → 執行 → 更新」做成一條死規矩,沒有繞過去的路徑。
|
|
||||||
@@ -1,76 +0,0 @@
|
|||||||
# 技術棧硬限制
|
|
||||||
|
|
||||||
## 三層語言對應(絕對不可混用)
|
|
||||||
|
|
||||||
| 層級 | 語言 | 位置 | 職責 |
|
|
||||||
|-----|------|------|------|
|
|
||||||
| 零件(Component) | **TinyGo 或 AssemblyScript → WASM** | `registry/components/{name}/` | 所有業務邏輯 |
|
|
||||||
| 零件 Worker 包裝 | TypeScript(固定模板,不寫業務邏輯) | `.component-builds/{name}/` | WASI shim,stdin/stdout JSON |
|
|
||||||
| Orchestration Worker | TypeScript + Hono | `cypher-executor/` | HTTP routing、workflow 執行排程、host functions |
|
|
||||||
| CLI | TypeScript + Node.js | `cli/` | `acr` 指令 |
|
|
||||||
| Python SDK | Python | `python-sdk/` | HTTP thin wrapper + client 端加密 |
|
|
||||||
| JS SDK | TypeScript + Web Crypto | `js-sdk/` | HTTP thin wrapper + client 端加密 |
|
|
||||||
| Frontend | React 19 + Vite + Tailwind v4 | `landing/` | Cloudflare Pages |
|
|
||||||
|
|
||||||
## 零件實作規範
|
|
||||||
|
|
||||||
### 只有兩種合法語言
|
|
||||||
- **TinyGo**:`tinygo build -target=wasi -o {name}.wasm main.go`
|
|
||||||
- **AssemblyScript**:`asc main.ts --target release -o {name}.wasm`
|
|
||||||
|
|
||||||
### I/O 模型
|
|
||||||
- **stdin**:JSON input
|
|
||||||
- **stdout**:JSON output
|
|
||||||
- 不用 HTTP server,不監聽 socket(WASI preview1 沒 socket)
|
|
||||||
|
|
||||||
### Host Functions(零件呼叫外部能力的唯一管道)
|
|
||||||
在 `u6u` namespace 下:
|
|
||||||
|
|
||||||
| Host Function | 用途 |
|
|
||||||
|---|---|
|
|
||||||
| `u6u.http_request` | 發 HTTP 請求 |
|
|
||||||
| `u6u.kv_get` | 讀 Cloudflare KV(Worker 側依 key 前綴路由到正確 KV) |
|
|
||||||
| `u6u.crypto_decrypt` | AES-GCM 解密(encryption key 永不暴露給 WASM) |
|
|
||||||
| `u6u.crypto_sign_rs256` | RSA-SHA256 簽章(PKCS8 bytes 傳入) |
|
|
||||||
|
|
||||||
**所有 host function 在 `cypher-executor/src/lib/wasi-shim.ts` 實作**。零件透過 WASI import 使用。
|
|
||||||
|
|
||||||
## 資料儲存
|
|
||||||
|
|
||||||
| 儲存 | 用途 | Key 格式 |
|
|
||||||
|-----|------|---------|
|
|
||||||
| Cloudflare KV `WEBHOOKS` | workflow 定義(cypher binding YAML) | `webhook:{api_key}:{name}` |
|
|
||||||
| Cloudflare KV `CREDENTIALS_KV` | 加密 credential | `{api_key}:cred:{name}` |
|
|
||||||
| Cloudflare KV `RECIPES` | auth recipe / 動態 API recipe | `auth_recipe:{service}`, `rec_{hash}` |
|
|
||||||
| Cloudflare KV `USERS_KV` | 用戶資料 | `user:{api_key}` |
|
|
||||||
| Cloudflare KV `SESSIONS_KV` | session | `session:{token}` |
|
|
||||||
| Cloudflare KV `ANALYTICS_KV` | 執行分析 | `execution:{timestamp}:{id}` |
|
|
||||||
| Cloudflare KV `EXEC_CONTEXT` | workflow 執行中的 context | `ctx:{execution_id}:{node_id}` |
|
|
||||||
| Cloudflare R2 `WASM_BUCKET` | **只用於用戶自製零件**(Phase 5 以後啟用) | `{api_key}:cmp:{hash}` |
|
|
||||||
|
|
||||||
**警告:R2 不存平台內建零件的 WASM**。平台零件已 bundle 進各自的 Worker binary(`[[wasm_modules]]` 或 `import ... assert { type: 'webassembly' }`)。
|
|
||||||
|
|
||||||
## 加解密規範
|
|
||||||
|
|
||||||
- **演算法**:AES-GCM 256-bit
|
|
||||||
- **加密位置**:Client 端(CLI / Python SDK / JS SDK)
|
|
||||||
- Python:`cryptography` 套件
|
|
||||||
- JS:Web Crypto API(`crypto.subtle`)
|
|
||||||
- **解密位置**:Server 端 **WASM primitive**(透過 host function `crypto_decrypt`)
|
|
||||||
- cypher-executor TS **不解密**,只提供 host function
|
|
||||||
- `ENCRYPTION_KEY` 只在 Worker host function 內部讀取,**永不經 stdin / 回傳值傳給 WASM**
|
|
||||||
- **傳輸格式**:`{ name, encrypted, iv }`(iv base64、encrypted base64)
|
|
||||||
|
|
||||||
## 網路部署
|
|
||||||
|
|
||||||
- **平台 API(對外)**:`cypher.arcrun.dev`(cypher-executor)
|
|
||||||
- **Landing**:`arcrun.dev`
|
|
||||||
- **每個零件 Worker**:
|
|
||||||
- **對內(cypher-executor 用來 fetch component,避開同 zone 死鎖)**:`arcrun-{kebab}.{WORKER_SUBDOMAIN}.workers.dev`
|
|
||||||
- 例:`arcrun-kbdb-get.uncle6-me.workers.dev`
|
|
||||||
- cypher-executor 從 `wrangler.toml [vars] WORKER_SUBDOMAIN` 組出此 URL
|
|
||||||
- **對外(可選,零件對全網開放被 curl 用)**:`{kebab}.arcrun.dev`
|
|
||||||
- 例:`gmail.arcrun.dev`、`kbdb-get.arcrun.dev`
|
|
||||||
- 仍允許保留,但**禁止 cypher-executor 透過此 URL fetch**(會撞同 zone 自循環,見 [docs/incidents/2026-05-13-cypher-outbound-522.md](../../docs/incidents/2026-05-13-cypher-outbound-522.md))
|
|
||||||
- **新增 component worker 部署清單**:`name = "arcrun-{kebab}"` + `[[routes]]` 對外(可選)+ dashboard 啟用 workers.dev(必須)
|
|
||||||
- **部署工具**:Wrangler
|
|
||||||
@@ -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 signing,block
|
|
||||||
|
|
||||||
### 2.2 禁止在 cypher-executor 任何 TS 裡實作以下邏輯
|
|
||||||
這些邏輯全部屬於 WASM 零件職責:
|
|
||||||
|
|
||||||
- AES-GCM 解密(`crypto.subtle.decrypt`)— 只准出現在 `wasi-shim.ts` 的 `crypto_decrypt` host function
|
|
||||||
- RSA-SHA256 簽章(`crypto.subtle.sign` with RSASSA-PKCS1-v1_5)— 只准出現在 `wasi-shim.ts` 的 `crypto_sign_rs256` host function
|
|
||||||
- Template 展開(`{{secret.X}}` / `{{runtime.X}}` 替換)— 只能在 WASM 零件內
|
|
||||||
- PEM → PKCS8 解析
|
|
||||||
- JWT header/payload/signature 組裝
|
|
||||||
- Token exchange(拿 service account JWT 換 access_token)
|
|
||||||
- 具體 API call 實作(例如 gmail send / telegram sendMessage / google sheets append)
|
|
||||||
|
|
||||||
**Hook 會擋**:
|
|
||||||
- Write/Edit 到 `cypher-executor/src/` 下的 `.ts` 時,內容含:
|
|
||||||
- `crypto\.subtle\.decrypt` 且檔名不是 `wasi-shim.ts`
|
|
||||||
- `crypto\.subtle\.sign.*RSASSA` 且檔名不是 `wasi-shim.ts`
|
|
||||||
- `interpolateTemplate`、`\{\{secret\.` 的模板邏輯
|
|
||||||
- `BUILTIN_API_RECIPES`、`BUILTIN_CREDENTIALS_MAP`(新增用)
|
|
||||||
- `gmail.googleapis.com/gmail/v1/users/me/messages/send` 類 hard-code API URL
|
|
||||||
- `api.telegram.org/bot.*sendMessage`
|
|
||||||
- `sheets.googleapis.com/v4/spreadsheets`
|
|
||||||
- `notify-api.line.me/api/notify`
|
|
||||||
|
|
||||||
### 2.3 cypher-executor TS 的合法職責(允許)
|
|
||||||
- HTTP routing(Hono routes)
|
|
||||||
- workflow 執行排程(`graph-executor.ts`)
|
|
||||||
- 呼叫 WASM 零件(透過 HTTP fetch 到對應 Worker URL,或 Service Binding fallback)
|
|
||||||
- 提供 host function(`wasi-shim.ts` 的 `kv_get` / `crypto_decrypt` / `crypto_sign_rs256`)
|
|
||||||
- KV/R2/Service Binding 存取封裝
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 第三類:架構層級的禁令
|
|
||||||
|
|
||||||
### 3.1 禁止新增 Service Binding
|
|
||||||
**Cypher binding 不是 Cloudflare service binding**。它是 YAML/KV 裡的 URL 清單。
|
|
||||||
|
|
||||||
零件串接(workflow 層)一律走 HTTP URL,不走 `[[services]]`。
|
|
||||||
|
|
||||||
15 個現有的 `SVC_*` 綁定(`cypher-executor/wrangler.toml`)是歷史遺產(邏輯零件效能優化),**保留但不新增**。
|
|
||||||
|
|
||||||
**Hook 會擋**:bash 指令含 `wrangler tail` 以外、涉及 `[[services]]` 新增的 pattern;Edit wrangler.toml 新增 `[[services]]` 區塊時警告確認。
|
|
||||||
|
|
||||||
### 3.2 禁止以「從 R2 取 WASM」為設計
|
|
||||||
平台內建零件已 bundle 進各自 Worker,不從 R2 取。
|
|
||||||
R2 只在 Phase 5(用戶自製零件)啟用。
|
|
||||||
|
|
||||||
**Hook 會警告**:TS 中出現 `env.WASM_BUCKET.get(` 的新增 code(除非在明確標註的 Phase 5 user-submit 路徑中)。
|
|
||||||
|
|
||||||
### 3.3 禁止複製貼上 Worker 程式碼到新目錄
|
|
||||||
要改 `gmail` 零件 → 改 `registry/components/gmail/main.go`,重新編譯、部署。
|
|
||||||
**不准**新建 `gmail-v2/`、`new-gmail/`、`gmail-worker/` 等目錄。
|
|
||||||
|
|
||||||
**Hook 會擋**:`mkdir` 或 `Write` 到 `{component-name}-v2/`、`new-{component-name}/`、`{component-name}-worker/` 類路徑。
|
|
||||||
|
|
||||||
### 3.4 禁止在 SDK 內做 server 職責
|
|
||||||
- **禁止**:SDK 裡做 server 端解密、credential-injector 重實作、workflow executor、auth recipe 解析
|
|
||||||
- **允許**:SDK 做 HTTP thin wrapper + client 端加密(AES-GCM)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 第四類:流程層級的禁令
|
|
||||||
|
|
||||||
### 4.1 禁止沒讀 SDD 就動 code
|
|
||||||
見 `00-sdd-protocol.md`。
|
|
||||||
|
|
||||||
### 4.2 禁止批次更新 tasks.md
|
|
||||||
每完成一個 task 就立刻 mark `- [x]`。不准「先全部做完再一次更新」。
|
|
||||||
|
|
||||||
### 4.3 禁止新建 SDD 而不事先與 richblack 確認
|
|
||||||
SDD 屬於架構決策,必須人確認。CC 不可以自行在 `.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 拿到錯誤訊息後有機會自行導正,不是被擋死就愣住。
|
|
||||||
@@ -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 shim(POST / → stdin → WASM → stdout → JSON)
|
|
||||||
```
|
|
||||||
|
|
||||||
**src/index.ts 是通用模板**,所有零件都用同一份。這個 TS 只做 WASI runtime,不是業務邏輯。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## R2(WASM_BUCKET)的真正用途
|
|
||||||
|
|
||||||
R2 存 WASM 只是**用戶自製零件上傳**用的。
|
|
||||||
|
|
||||||
**平台內建零件不從 R2 讀取**——它們在部署時就已 bundle 進 Worker 的 binary(透過 `[[wasm_modules]]` 或 `import` with `assert { type: 'webassembly' }`)。
|
|
||||||
|
|
||||||
Phase 5(封測後)才會啟用「用戶 push 自製零件 → 存 R2 → 動態執行」這條路徑。
|
|
||||||
|
|
||||||
**結論:當 CC 問「怎麼從 R2 取出 WASM」時,幾乎都是走錯路徑**。平台零件是獨立 Worker,走 HTTP 呼叫,不是 R2 動態載入。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Cypher binding 的正確定義
|
|
||||||
|
|
||||||
**Cypher binding 不是 Cloudflare 的任何 binding 機制。**
|
|
||||||
|
|
||||||
Cypher binding 是一張 YAML 清單,內容是「一個 workflow 要呼叫哪些零件 URL」。存放在:
|
|
||||||
- 本地:`workflow.yaml`(用戶寫的 workflow)
|
|
||||||
- KV:`WEBHOOKS` KV(用戶 `acr push` 後存入)
|
|
||||||
|
|
||||||
Cypher executor 執行 workflow 時:
|
|
||||||
1. 從 KV 讀出 workflow YAML
|
|
||||||
2. 按 graph 順序解析每個節點的 `component`
|
|
||||||
3. 用 HTTP fetch 打對應的零件 URL
|
|
||||||
4. 把 output 當作下個節點的 input
|
|
||||||
|
|
||||||
**這就是 Cypher binding——用 HTTP URL 把零件串起來,存在 YAML/KV 裡**。
|
|
||||||
|
|
||||||
### 為什麼不能用 Service Binding?
|
|
||||||
|
|
||||||
Service binding 需要 `wrangler.toml` 裡寫死 `[[services]]`,且要 redeploy 才生效。arcrun 是類 n8n 服務,用戶建立新 workflow 時**絕對不可能**要他 redeploy。所以 workflow 層一定要 HTTP。
|
|
||||||
|
|
||||||
### Service Binding 的僅存合法用途
|
|
||||||
|
|
||||||
只在 `cypher-executor` 和**平台內建邏輯零件之間**保留(效能優化,避免公網往返)。看 `cypher-executor/wrangler.toml` 裡的 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-executor(Worker)讀 workflow YAML
|
|
||||||
│
|
|
||||||
├─ 節點 1: component = gmail
|
|
||||||
│ a. 查 auth_recipe:gmail → primitive = static_key
|
|
||||||
│ b. HTTP POST https://auth-static-key.arcrun.dev
|
|
||||||
│ { action: "authenticate", api_key, service: "gmail" }
|
|
||||||
│ → 回傳 { auth_headers: { Authorization: "Bearer ..." } }
|
|
||||||
│ c. HTTP POST https://gmail.arcrun.dev
|
|
||||||
│ { to, subject, body, _auth_headers }
|
|
||||||
│ → gmail 零件 Worker 執行 WASM → 回傳 { success, data }
|
|
||||||
│
|
|
||||||
└─ 節點 2: component = google_sheets
|
|
||||||
... 相同模式
|
|
||||||
```
|
|
||||||
|
|
||||||
**cypher-executor 本身不做 credential 解密、不做 JWT signing、不做 auth header 組裝**。這些全在 auth primitive WASM 零件內,cypher-executor 只負責 HTTP routing 和工作流排程。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 實際禁令(CC 看這裡)
|
|
||||||
|
|
||||||
### 禁止在 `registry/components/` 下建立 TypeScript 檔案
|
|
||||||
零件邏輯一律 TinyGo 或 AssemblyScript,編譯成 `.wasm`。
|
|
||||||
|
|
||||||
### 禁止把 auth 邏輯寫在 `cypher-executor/src/` 裡
|
|
||||||
credential 解密、JWT signing、template 展開(`{{secret.X}}`)全部屬於 auth primitive WASM 零件的職責。cypher-executor 只呼叫它們。
|
|
||||||
|
|
||||||
### 禁止問「怎麼從 R2 取 WASM」
|
|
||||||
平台內建零件**不從 R2 取**。每個零件已部署成獨立 Worker,走 HTTP URL。用戶自製零件才用 R2(Phase 5,未啟用)。
|
|
||||||
|
|
||||||
### 禁止新增 Service Binding
|
|
||||||
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 #9(2026-05-13)。cypher-executor 走對內 URL 避開同 zone 自循環死鎖;若 workers.dev 未啟用,cypher-executor fetch 該 component 會 404。
|
|
||||||
@@ -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 要清除的程式碼),在清除前不封測。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 目前 Phase:Credential Primitives TS → WASM
|
|
||||||
|
|
||||||
**SDD 位置**:`.agents/specs/arcrun/credential-primitives-wasm/design.md` + `tasks.md`
|
|
||||||
|
|
||||||
### 已完成
|
|
||||||
|
|
||||||
- **Phase 0.1–0.5**:核心合併(u6u-core 併入 arcrun、21 個零件 contract 完整、刪除重複 `credentials/` 目錄、CREDENTIALS_KV binding 確認、刪除 `matrix/u6u-core/`)
|
|
||||||
- `registry/components/` 下 21 個零件(邏輯 + API)都有 `main.go` + `.wasm`
|
|
||||||
|
|
||||||
### 進行中 / 未完成
|
|
||||||
|
|
||||||
| Task | 狀態 | 阻擋關係 |
|
|
||||||
|-----|------|---------|
|
|
||||||
| 0.6 wasi-shim 新增 `kv_get` / `crypto_decrypt` / `crypto_sign_rs256` host functions | ⬜ 未開始 | **Phase 1-3 的硬前置** |
|
|
||||||
| 0.7 component-loader 新增 WASM runner 路徑 | ⬜ 未開始 | **Phase 1-3 的硬前置** |
|
|
||||||
| 1.1-1.8 `auth_static_key` WASM 零件(TinyGo) | ⬜ 未開始 | 涵蓋 80% 服務 |
|
|
||||||
| 2.1-2.6 `auth_service_account` WASM 零件(JWT signing) | ⬜ 未開始 | Google Service Account 等 |
|
|
||||||
| 3.1-3.5 清除 `component-loader.ts` 的 `BUILTIN_API_RECIPES` | ⬜ 未開始 | 要先有 Phase 1-2 的 WASM 零件 |
|
|
||||||
| 4.1-4.4 `auth_oauth2` + `auth_mtls`(封測後) | ⬜ 未開始 | 非阻擋項 |
|
|
||||||
| 5.1-5.7 核心穩定驗證(全域搜尋確認無殘餘 TS) | ⬜ 未開始 | 封測啟動門檻 |
|
|
||||||
|
|
||||||
### Phase 1-3 要**徹底刪除**的 TS 檔案(不是搬、不是改,是刪)
|
|
||||||
|
|
||||||
| 檔案 | 違反什麼 |
|
|
||||||
|-----|---------|
|
|
||||||
| `cypher-executor/src/actions/credential-injector.ts` | AES 解密、template 展開、JWT 邏輯 —— 應在 WASM |
|
|
||||||
| `cypher-executor/src/lib/jwt-signer.ts` | RS256 JWT 簽章邏輯 —— 應在 `auth_service_account.wasm` |
|
|
||||||
| `cypher-executor/src/lib/component-loader.ts` 的 `BUILTIN_API_RECIPES`(~100 行) | gmail/telegram/line/gsheets/http_request/cron 的 TS 實作 —— 應全部走對應 WASM 零件 |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 下一個 session 第一件要做的事
|
|
||||||
|
|
||||||
**讀 `.agents/specs/arcrun/credential-primitives-wasm/tasks.md`**,然後決定從 Phase 0.6 還是 0.7 開始。
|
|
||||||
|
|
||||||
0.6(host functions)和 0.7(WASM runner)是並列的前置工作,哪個先都可以,但都要在 Phase 1 開始之前完成。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## SDD 索引
|
|
||||||
|
|
||||||
| 子系統 | SDD |
|
|
||||||
|--------|-----|
|
|
||||||
| **主要(正在動)** Credential Primitives WASM 改寫 | `.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`。
|
|
||||||
@@ -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-02,self-hosted 開源策略)**:
|
|
||||||
> 原慣例「`.component-builds/{name}/component.wasm` 不 commit 進 repo」**已推翻**。
|
|
||||||
> 現在 **commit `.component-builds/*/component.wasm` 進 repo**,因為 self-host 用戶 / `acr init --self-hosted`
|
|
||||||
> 從 GitHub(codeload tarball)直接拿這份 wasm 部署到自己的 CF——repo 必須自帶可部署的 wasm。
|
|
||||||
> 決策依據:`.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`**。
|
|
||||||
- CI(deploy.yml):仍在 deploy 前自動 rebuild + copy(部署 prod 用最新 source;與 repo 內 commit 的 wasm 不衝突——前者給 CI deploy prod,後者給 self-host 用戶當部署來源)。
|
|
||||||
|
|
||||||
### 誠實 trade-off(mindset §7)
|
|
||||||
|
|
||||||
commit wasm 進 repo → 每次 rebuild 在 git 歷史累積二進位,**repo 長期會膨脹**。
|
|
||||||
可接受(self-host 體驗優先),未來若膨脹過劇再考慮 git-lfs / 按需安裝(self-hosted-init.md §6.6)。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 並行度
|
|
||||||
|
|
||||||
`max-parallel: 5` — 避免觸發 Cloudflare Workers API rate limit。
|
|
||||||
|
|
||||||
Worker 數量 > 5 時,deploy 會分批跑。25 個 Worker 大約 5 輪 × ~30 秒 = 2-3 分鐘可完成全部。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 禁止事項
|
|
||||||
|
|
||||||
1. **禁止**為新 Worker 手動加 deploy job 到 `deploy.yml`。通用掃描會自動處理,手加就是重複工作。
|
|
||||||
2. **禁止**把 runtime secret(API key / encryption key / credential)放進 GH Actions secrets 或 `wrangler.toml` `[vars]`,只能用 `wrangler secret put`。
|
|
||||||
3. **禁止**在 CI 裡跑不必要的測試阻擋 deploy。測試在 PR / 本地跑,`main` 推上去就 deploy(trunk-based)。若要測試關,開新 workflow 檔,不要污染 deploy workflow。
|
|
||||||
4. **禁止**跳過 TinyGo rebuild 直接 deploy 舊 `.wasm`。CI 的 rebuild 步驟是確保部署的是最新 source。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 驗證指令
|
|
||||||
|
|
||||||
本地模擬 CI 的掃描結果:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
find . -name 'wrangler.toml' -not -path '*/node_modules/*' -not -name 'wrangler.test.toml' \
|
|
||||||
| xargs -n1 dirname | sort -u
|
|
||||||
```
|
|
||||||
|
|
||||||
應列出 ~25 個目錄。任何「我新增了 Worker 但沒被 deploy」的問題,先跑這條確認目錄被掃到。
|
|
||||||
@@ -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 跑 wasm,CF Worker 不能)。
|
|
||||||
§8「不依賴 CI」指執行鏈路(高頻);零件投稿稀有,走 PR/CI 是例外、不違反。
|
|
||||||
|
|
||||||
## 5. 發佈安全的底氣是純 WASI 沙箱,不是 Gherkin
|
|
||||||
|
|
||||||
Gherkin 全綠 ≠ 零件安全(投稿者可寫避重就輕的 Gherkin)。真正框死破壞力的是**純 WASI 沙箱**
|
|
||||||
(零件只能 stdin→stdout、無網路 syscall、無檔案系統)。Gherkin 驗契約 + 沙箱框死 + 市場補長尾 = 風險可控,非零風險。
|
|
||||||
|
|
||||||
## 6. 暴露 / 送資料的動作 → 人類明示同意(資料外流警示)
|
|
||||||
|
|
||||||
把資料 / workflow 變成「可被外部呼叫」(部署 webhook、recipe push)= 暴露面 → 需人類明示同意,不分公私庫。
|
|
||||||
**不禁止**用戶公開(他的自由),但要**確定他自己明示同意**(不是 AI 替他決定)。
|
|
||||||
警示同時是「保護措施入口」(提示可加 API Key / 權限 / 限流)。
|
|
||||||
|
|
||||||
## 7. 誠實限制(最重要的 mindset:不假裝、不假綠)
|
|
||||||
|
|
||||||
- **AI 技術上能偽造人類確認**(confirmed_by_human、exposure_consent、gherkin_evidence 都能塞)。
|
|
||||||
這些機制的價值是**法律歸責 + 軌跡可審**,不是技術防偽。**絕不在文件 / 程式裡聲稱「不可能繞過」。**
|
|
||||||
- **絕不代替人類做有風險的確認**(建零件、暴露資料)。非 TTY(你直跑)就拒絕,不要自己塞 flag 假裝人類同意了 —— 那是明確越界。
|
|
||||||
- **禁假綠**(DECISIONS §3c/§7):stub / 未實作就回 success:false 或明確標 unimplemented,不要回傳假資料假裝成功。
|
|
||||||
缺 credential 打不到 2xx 就誠實標「未驗收:缺 X」,不 mock 充綠燈。
|
|
||||||
- **完成 = 客觀證據**(編譯 exit code / HTTP status + trace),不是口頭宣布「我做好了」。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
詳細判準與來龍去脈見 `DECISIONS.md`。每條都有對應的慘痛教訓,不是憑空規定。
|
|
||||||
@@ -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
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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 自動 cancel(landing 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 }}"
|
|
||||||
@@ -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 min,doc-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
@@ -21,6 +21,22 @@ docs/test_credentials/
|
|||||||
*.sa.json
|
*.sa.json
|
||||||
*-service-account*.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
|
# macOS
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
|
||||||
|
|||||||
@@ -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.1,2026-05-16)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 1. Arcrun 是什麼(30 秒)
|
|
||||||
|
|
||||||
Arcrun = 用 YAML 把 WASM 零件串成可重複執行 workflow 的平台。**用戶寫 YAML,平台跑**。
|
|
||||||
|
|
||||||
- 每個零件是 TinyGo / AssemblyScript 編譯的 `.wasm`,stdin/stdout JSON I/O
|
|
||||||
- 每個 workflow 是一份 cypher binding YAML,描述「節點 + 邊」的圖
|
|
||||||
- 觸發機制:HTTP webhook、cron、callback resume
|
|
||||||
- 部署完成回 `webhook_url`,用戶或下游服務 POST 即可執行
|
|
||||||
|
|
||||||
n8n 從手寫程式碼開始,arcrun 從 AI 描述開始:你跟用戶聊出他想要什麼自動化,**你(AI)寫 YAML 部署,之後不需要 AI 也能跑**。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 2. 連線(一步搞定)
|
|
||||||
|
|
||||||
加到你的 MCP config(Claude Desktop / Cursor / 任何支援 MCP 的 client):
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"mcpServers": {
|
|
||||||
"arcrun": {
|
|
||||||
"type": "http",
|
|
||||||
"url": "https://mcp.arcrun.dev/mcp",
|
|
||||||
"headers": {
|
|
||||||
"Authorization": "Bearer ak_YOUR_API_KEY"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
> **暫時**:M5 完成前 URL 仍是 `https://mcp.finally.click/mcp`,預計 2026-06 切。
|
|
||||||
> Tool 命名暫時仍是 `u6u_*` prefix,M5 一次改 `arcrun_*`。
|
|
||||||
> 本 doc 描述目標狀態,實際用 `list_*` tool 取得當前可用名單。
|
|
||||||
|
|
||||||
取得 ak_ 金鑰:到 https://arcrun.dev/me(OAuth Google / GitHub 登入),右下角複製。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 3. 五個核心概念
|
|
||||||
|
|
||||||
| 概念 | 一句話 |
|
|
||||||
|---|---|
|
|
||||||
| **Component(零件)** | WASM Worker,獨立部署成 `arcrun-{kebab}.{user}.workers.dev`。用 `list_components` 看可用清單 |
|
|
||||||
| **Cypher binding** | YAML 三元組 `A >> 關係 >> B`,定義 workflow 圖。常用關係:`ON_SUCCESS` / `對每個 X` / `IF` |
|
|
||||||
| **FOREACH** | `>> 對每個 item >> next_node` 迭代陣列。`item` 變數自動可用 |
|
|
||||||
| **Paused-resume** | claude_api 等需等外部 callback 的零件會「paused」,cypher-executor 透過 `/workflows/resume` 接續 |
|
|
||||||
| **api_key (ak_xxx)** | 所有 call 必帶(MCP header 或 workflow `{{api_key}}`),同時當 partition key |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 4. 你的第一個 workflow(5 分鐘 e2e)
|
|
||||||
|
|
||||||
### Step 1:看有什麼零件可用
|
|
||||||
```
|
|
||||||
arcrun_list_components() # 全部零件名單
|
|
||||||
# 或
|
|
||||||
arcrun_search_examples('rag') # 範例庫搜尋(從 use case 找範本)
|
|
||||||
arcrun_list_skills() # 看 playbook 清單
|
|
||||||
arcrun_get_skill('build_watcher_workflow') # 拿特定 playbook 細節
|
|
||||||
```
|
|
||||||
常用零件:`http_request`、`claude_api`、`kbdb_get`、`kbdb_create_block`、`telegram`、`gmail`、`cron`、`filter`、`trigger_workflow`。
|
|
||||||
|
|
||||||
### Step 2:寫 minimal YAML(從範例改 > 從零寫)
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
name: hello_world
|
|
||||||
description: 接 webhook,回個 hi
|
|
||||||
|
|
||||||
flow:
|
|
||||||
- "input >> ON_SUCCESS >> say_hi"
|
|
||||||
|
|
||||||
config:
|
|
||||||
say_hi:
|
|
||||||
component: http_request
|
|
||||||
url: "https://httpbin.org/post"
|
|
||||||
method: POST
|
|
||||||
body_json:
|
|
||||||
hello: "{{input.name}}"
|
|
||||||
```
|
|
||||||
|
|
||||||
### Step 3:dry-run 校驗
|
|
||||||
```
|
|
||||||
arcrun_validate_yaml(api_key, graph)
|
|
||||||
```
|
|
||||||
|
|
||||||
### Step 4:部署
|
|
||||||
```
|
|
||||||
arcrun_push_workflow(api_key, yaml_content)
|
|
||||||
```
|
|
||||||
回 `{name, webhook_url: 'https://cypher.arcrun.dev/webhooks/named/hello_world/trigger'}`。
|
|
||||||
|
|
||||||
### Step 5:觸發測試
|
|
||||||
```
|
|
||||||
arcrun_run_workflow(api_key, name='hello_world', input={name: 'leo'})
|
|
||||||
```
|
|
||||||
|
|
||||||
### Step 6:看結果 / debug
|
|
||||||
```
|
|
||||||
arcrun_list_recent_executions(api_key, workflow_name='hello_world')
|
|
||||||
# 若 paused:
|
|
||||||
arcrun_list_paused_executions(api_key)
|
|
||||||
arcrun_get_execution_trace(api_key, task_id='task_XXX')
|
|
||||||
```
|
|
||||||
|
|
||||||
### Step 7:回報(**必做**,見 §8)
|
|
||||||
```
|
|
||||||
arcrun_report_feedback(api_key, issue_type='success_story', description='...')
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 5. URL 慣例(很重要,搞錯會撞 522)
|
|
||||||
|
|
||||||
| URL pattern | 用途 |
|
|
||||||
|---|---|
|
|
||||||
| `cypher.arcrun.dev` | Orchestration API(你的 workflow CRUD + trigger 走這) |
|
|
||||||
| `arcrun-{kebab}.{user}.workers.dev` | 零件 worker(cypher-executor 走 workers.dev 對內 URL,避 CF 同 zone 自循環死鎖) |
|
|
||||||
| `{kebab}.arcrun.dev` | 零件 worker 對外公開 URL(用戶 / 直接 curl 用,cypher-executor 不要走這) |
|
|
||||||
| `kbdb-*.arcrun.dev` | KBDB 操作(資料層) |
|
|
||||||
| `mcp.arcrun.dev` | MCP server(**你**用這個) |
|
|
||||||
| `mcp.finally.click` | MCP server 舊網址(過渡) |
|
|
||||||
|
|
||||||
**踩坑警示**:cypher-executor 自打 `cypher.arcrun.dev` 或自打 `arcrun-cypher-executor.*.workers.dev` 都會撞 CF self-fetch 防護回 1042/522。要 in-process 觸發另一個 workflow,**用內建零件 `trigger_workflow`**(不是 `http_request` 自打)。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 6. 常見錯誤 + 怎麼讀
|
|
||||||
|
|
||||||
| error_code | 含義 | 你該做什麼 |
|
|
||||||
|---|---|---|
|
|
||||||
| `auth_missing` / `auth_invalid` | ak_ 沒帶 / 錯了 | 去 https://arcrun.dev/me 重拿,更新 MCP config |
|
|
||||||
| `component_not_found` | 零件名打錯 | call `list_components()` 看正確名 |
|
|
||||||
| `component_not_in_whitelist` | 零件存在但 cypher-executor 不認 | 告訴用戶聯絡平台維護者(這是平台 bug) |
|
|
||||||
| `validation_failed` | YAML / schema 不過 | 看 response 的 `next_actions` 陣列,照著修 |
|
|
||||||
| `running_async` (status field) | workflow 已接受,在背景跑等 callback(claude_api 等) | **正常**,wait 或 call `get_execution_trace` 看狀態。舊名 `paused_awaiting_resume` 2026-05-16 改 |
|
|
||||||
| `dependency_unavailable` | 下游 API(Claude / Gmail / KBDB)掛 | retry,仍掛 → 公告型 issue |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 7. 不確定的時候,按這個順序
|
|
||||||
|
|
||||||
1. **不知道有什麼零件可用** → `arcrun_list_components()` / `arcrun_get_component_contract(id)`
|
|
||||||
2. **不知道用戶有什麼 workflow** → `arcrun_list_workflows(api_key)`
|
|
||||||
3. **想看零件 input/output schema** → `arcrun_get_component_contract('xxx')`
|
|
||||||
4. **要不要 dry-run** → 預設 yes,永遠先 `arcrun_validate_yaml` 再 `arcrun_push_workflow`
|
|
||||||
5. **觸發後不知道結果** → `arcrun_list_recent_executions(api_key, name)` / `arcrun_get_execution_trace(api_key, task_id)`,paused 不算錯
|
|
||||||
6. **有沒有現成範例可參考** → `arcrun_search_examples('rag llm chain')`(範例庫 10+ 個 workflow)
|
|
||||||
7. **有沒有 playbook** → `arcrun_list_skills() / arcrun_get_skill(slug)`(5 個 playbook:watcher / paused-debug / migrate-trigger / rag / new-component)
|
|
||||||
8. **不知道下一步** → 看任何 error response 的 `next_actions` 陣列
|
|
||||||
|
|
||||||
### 過渡期 tool 命名注意
|
|
||||||
|
|
||||||
LI 開發中(2026-05-16~)。目前 arcrun-mcp 同時提供:
|
|
||||||
- **`arcrun_*`**(新規範,**主用這套**):報 feedback / 工作流 CRUD / 執行 trace
|
|
||||||
- **`u6u_*`**(舊規範,等 M5 一次切):component 操作、tag 操作、GUI context
|
|
||||||
|
|
||||||
舊 `u6u_*` 仍可用。其中 `u6u_deploy_workflow` 是壞的(呼叫不存在的 endpoint),用 `arcrun_push_workflow` 取代。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 8. 回報機制(**必做**)
|
|
||||||
|
|
||||||
平台用回饋資料 self-improve。**每次部署 workflow / 卡住 / 解掉問題後**,call:
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
report_feedback({
|
|
||||||
workflow_name: "hello_world", // 你剛做的 workflow
|
|
||||||
issue_type:
|
|
||||||
| "success_story" // 順利完成,值得記錄這個 pattern
|
|
||||||
| "doc_unclear" // AGENTS.md / skill / contract 講不清楚
|
|
||||||
| "tool_missing" // 該有的 MCP tool 沒有
|
|
||||||
| "error_unhelpful" // 錯誤訊息看不懂下一步
|
|
||||||
| "unexpected_behavior" // 跟我預期的不一樣
|
|
||||||
| "feature_request", // 我想要 X 功能
|
|
||||||
description: "...",
|
|
||||||
retry_count: 2, // 你試了幾次才搞定
|
|
||||||
blocked: false, // 完全擋住嗎
|
|
||||||
suggested_fix: "..." // optional,你建議的修補
|
|
||||||
})
|
|
||||||
```
|
|
||||||
|
|
||||||
「success_story」也要報,**那是告訴平台「這個 pattern 已經 work,可以推廣」**。
|
|
||||||
|
|
||||||
不需要怕回報太多 — 你不報,平台拿 implicit telemetry(每個 deploy / run 平台自己 log)也會看到問題,但 explicit feedback 質感高很多。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 9. KBDB(資料儲存)速覽
|
|
||||||
|
|
||||||
arcrun 的「資料庫」是 KBDB(Cloudflare D1)。萬物皆 block:note / wiki-page / chat / triplet / template / skill / feedback / 等,靠 `type` 區分。
|
|
||||||
|
|
||||||
工具:
|
|
||||||
- `kbdb_get(type, block_id?, page_name?, ...)` — 讀
|
|
||||||
- `kbdb_create_block(type, content, ...)` — 建
|
|
||||||
- `kbdb_patch_block(block_id, content?, tags?, ...)` — 改
|
|
||||||
- `kbdb_upsert_block(page_name, content, ...)` — page_name 當 idempotency key
|
|
||||||
|
|
||||||
寫 workflow 要 RAG / KM / 用戶資料持久化時,直接用這幾個 component(在 YAML `component: kbdb_get` 等)。
|
|
||||||
|
|
||||||
完整 KBDB API 將有獨立 SDD(`kbdb-llm-interface`),目前看 `https://kbdb.finally.click/ui`(Swagger)。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 10. 範例:寫一個 cron watcher(最常見 pattern)
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
name: my_watcher
|
|
||||||
description: 每 5 分鐘掃未處理資料 → 觸發 wiki_synthesis
|
|
||||||
|
|
||||||
flow:
|
|
||||||
- "watch_cron >> ON_SUCCESS >> list_unprocessed"
|
|
||||||
- "list_unprocessed >> ON_SUCCESS >> filter_new"
|
|
||||||
- "filter_new >> 對每個 item >> trigger_synthesis"
|
|
||||||
|
|
||||||
config:
|
|
||||||
watch_cron:
|
|
||||||
component: cron
|
|
||||||
cron_expr: "*/5 * * * *"
|
|
||||||
|
|
||||||
list_unprocessed:
|
|
||||||
component: kbdb_get
|
|
||||||
api_key: "{{api_key}}"
|
|
||||||
type: "note"
|
|
||||||
source: "user-input"
|
|
||||||
limit: 20
|
|
||||||
|
|
||||||
filter_new:
|
|
||||||
component: filter
|
|
||||||
items: "{{list_unprocessed.blocks}}"
|
|
||||||
condition:
|
|
||||||
key: "tags_json"
|
|
||||||
op: "eq"
|
|
||||||
value: "[]"
|
|
||||||
|
|
||||||
trigger_synthesis:
|
|
||||||
component: trigger_workflow # 不要用 http_request 自打 — 會撞 CF self-fetch
|
|
||||||
workflow_name: "wiki_synthesis"
|
|
||||||
api_key: "{{api_key}}"
|
|
||||||
input:
|
|
||||||
api_key: "{{api_key}}"
|
|
||||||
raw_block_id: "{{item.id}}"
|
|
||||||
```
|
|
||||||
|
|
||||||
部署完每 5 分鐘自動跑。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 10.5 內建 magic vars(`_` prefix reserved)
|
|
||||||
|
|
||||||
YAML 內可直接用以下變數,cypher-executor 自動展開為當下時間(UTC):
|
|
||||||
|
|
||||||
| 變數 | 範例 | 用途 |
|
|
||||||
|---|---|---|
|
|
||||||
| `{{_today}}` | `2026-05-16` | 日 log / page_name |
|
|
||||||
| `{{_yesterday}}` | `2026-05-15` | digest 取昨日 |
|
|
||||||
| `{{_now}}` | `2026-05-16T09:30:00.123Z` | ISO 8601 |
|
|
||||||
| `{{_now_unix}}` | `1778937000123` | unix ms |
|
|
||||||
| `{{_now_unix_s}}` | `1778937000` | unix sec |
|
|
||||||
| `{{_iso_week}}` | `2026-W20` | weekly archive (本 doc 推薦) |
|
|
||||||
| `{{_iso_week_num}}` / `{{_iso_year}}` | `20` / `2026` | 拆開用 |
|
|
||||||
| `{{_yyyymm}}` / `{{_yyyymmdd}}` | `202605` / `20260516` | 緊湊路徑 |
|
|
||||||
| `{{_year}}` / `{{_month}}` / `{{_day}}` / `{{_hour}}` / `{{_minute}}` | 各別 zero-padded | 自己拼路徑 |
|
|
||||||
| `{{_weekday}}` | `0`-`6`(0=日)| if-control |
|
|
||||||
| `{{_iso_weekday}}` | `1`-`7`(1=一)| ISO 風格 |
|
|
||||||
|
|
||||||
**rule**:`_` prefix reserved for system,**用戶自己 ctx 變數不要用 `_` 開頭**。
|
|
||||||
|
|
||||||
**範例**:weekly archive
|
|
||||||
```yaml
|
|
||||||
publish_roadmap_archive:
|
|
||||||
component: kbdb_upsert_block
|
|
||||||
page_name: "roadmap-{{_iso_week}}" # roadmap-2026-W20
|
|
||||||
tags_json: '["weekly", "week:{{_iso_week}}"]'
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 11. 給寫 LI 的 AI 自己的 meta-規範
|
|
||||||
|
|
||||||
你(AI)在寫 arcrun workflow 時,**遵守以下習慣**會少踩坑:
|
|
||||||
|
|
||||||
1. **永遠先 list → validate → push → run → trace**:5 步流程,缺一個都會多繞路
|
|
||||||
2. **error 一定讀 `next_actions`**:不是讀 `human_message` 然後猜
|
|
||||||
3. **paused 不是錯**:claude_api、外部 OAuth flow 都會 paused,正常
|
|
||||||
4. **`{{api_key}}` 是 trigger context 帶進來的**:手動觸發要在 body 帶;cron 觸發 cypher-executor 自動塞
|
|
||||||
5. **新增零件不在 list_components 出來的清單裡** → 平台沒部署該零件,告訴用戶「我們需要先做 component」,不是你寫 workflow 的鍋
|
|
||||||
6. **完成後 call `report_feedback`**:哪怕 success_story,也回報。AI 用得順不順不能靠人類事後回顧
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 12. 進階參考
|
|
||||||
|
|
||||||
- 完整 SDD:`matrix/arcrun/.agents/specs/llm-interface/`
|
|
||||||
- 平台架構(rules):`matrix/arcrun/.claude/rules/`
|
|
||||||
- 零件開發指南:call `get_component_guide()` MCP tool
|
|
||||||
- KBDB Swagger:https://kbdb.finally.click/ui
|
|
||||||
- 範例庫(M3 完成後):`registry/examples/`
|
|
||||||
- 平台週報(M4 完成後):KBDB block `type=arcrun-roadmap`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
> 本 doc 是 source of truth。每次更新後 GH Actions 自動同步 KBDB block (`type=agent-onboarding`),AI 可透過 `get_onboarding` MCP tool 拿最新版(M1 完成)。
|
|
||||||
-175
@@ -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 recipe(static_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 — 降級假零件成 recipe(2026-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` 降級:建 recipe(gmail_send / telegram_send / line_notify_send / google_sheets_read|append)+ 補 auth_recipe(line_notify / telegram),刪 4 零件目錄
|
|
||||||
- telegram_send:✅ auth 注入驗收(inject.path,400 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_block(deferred)
|
|
||||||
|
|
||||||
### 步驟 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 到公司群)。
|
|
||||||
|
|
||||||
- [ ] 新建 SDD(requirements/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-executor(matrix 頂層 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 開發時讀)。
|
|
||||||
- [ ] 會回嘴的 CLI(acr 偵測走歪 → exit 2 + 指回正路,見 DECISIONS §7 層二)
|
|
||||||
→ 部分已做:pre-bash hook 擋假零件/暴露動作 + 指回正路;CLI 本身的 exit-2 回嘴待補
|
|
||||||
- [ ] README 重寫成單一路徑(砍掉「玩法一/二/三」三選一)
|
|
||||||
- [ ] `acr init --self-hosted`:貼 CF token → 自動建 KV、部署 Worker、自動 workers.dev
|
|
||||||
subdomain、寫回 config(用戶自己建 token,CLI 不代管)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 第一期之後 / 待決策(不要現在做)
|
|
||||||
|
|
||||||
- [ ] **用戶自己的 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
@@ -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 URL:https://cypher.arcrun.dev/webhooks/named/my-workflow/trigger
|
|
||||||
需帶 Header:X-Arcrun-API-Key: ak_...
|
|
||||||
|
|
||||||
curl 觸發範例:
|
|
||||||
curl -X POST https://cypher.arcrun.dev/webhooks/named/my-workflow/trigger \
|
|
||||||
-H 'X-Arcrun-API-Key: ak_your-key' \
|
|
||||||
-H 'Content-Type: application/json' \
|
|
||||||
-d '{"message": "hello"}'
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## API Recipe(整合外部服務)
|
|
||||||
|
|
||||||
不需要 deploy Worker,只要上傳 recipe YAML:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
acr recipe push my-recipe.yaml
|
|
||||||
acr recipe list
|
|
||||||
acr recipe delete rec_xxxxxxxx
|
|
||||||
```
|
|
||||||
|
|
||||||
Recipe 上傳後會得到 `rec_xxxxxxxx` hash,可直接在 workflow config 的 `component` 欄位使用。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Credential 管理(Standard 模式)
|
|
||||||
|
|
||||||
需要帶 token 的零件(gmail、telegram、notion 等)可以提前上傳 credential,執行 workflow 時自動注入。
|
|
||||||
|
|
||||||
**加密金鑰在 `acr init` 時已自動取得並存入 `~/.arcrun/config.yaml`,不需要手動設定。**
|
|
||||||
|
|
||||||
**步驟一:查看某服務需要哪些 credential**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
acr auth-recipe scaffold notion # 輸出 credentials.yaml 範本 + workflow 使用範例
|
|
||||||
acr auth-recipe list # 列出所有支援的服務(20 個)
|
|
||||||
```
|
|
||||||
|
|
||||||
**步驟二:建立 credentials.yaml**(參考 scaffold 的輸出):
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
# 範例:Notion
|
|
||||||
notion_token: "secret_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
|
|
||||||
|
|
||||||
# 範例:Telegram Bot
|
|
||||||
telegram_bot_token: "123456789:your-bot-token"
|
|
||||||
```
|
|
||||||
|
|
||||||
**步驟三:上傳**:
|
|
||||||
```bash
|
|
||||||
acr creds push credentials.yaml
|
|
||||||
```
|
|
||||||
|
|
||||||
上傳後執行 workflow 時,tokens 自動注入,不需要在 `--input` 手動帶。
|
|
||||||
|
|
||||||
### 支援的第三方服務(20 個)
|
|
||||||
|
|
||||||
```bash
|
|
||||||
acr auth-recipe list
|
|
||||||
```
|
|
||||||
|
|
||||||
輸出:Notion、Slack、GitHub、OpenAI、Anthropic、Airtable、Discord、Stripe、Twilio、SendGrid、HubSpot、Linear、Shopify、Resend、Supabase、Typeform、Jira、Google Sheets(Service Account)、Gmail(Service Account)、Google Drive(Service Account)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 回饋格式
|
|
||||||
|
|
||||||
請把你的觀察記錄在 `FEEDBACK.md`,格式不限,但希望包含:
|
|
||||||
|
|
||||||
1. **成功的地方** — 哪些功能符合預期?
|
|
||||||
2. **失敗的地方** — 錯誤訊息是什麼?步驟是?
|
|
||||||
3. **困惑的地方** — 不知道怎麼用、文件不清楚的地方
|
|
||||||
4. **想要的功能** — 你覺得少了什麼
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 已知限制
|
|
||||||
|
|
||||||
- `number_ops` 的數字參數(`a`、`b`)若從 `--input` 帶入為字串,需要零件自行做型別轉換(目前已支援)
|
|
||||||
- `ON_FAIL` 觸發時,fallback 節點收到的 context 包含上游的錯誤物件(`{success: false, ...}`)
|
|
||||||
- 多節點串連時,context 為 flat merge,上游的 `data.result` 會直接合併到頂層
|
|
||||||
- `if_control` 條件為 false 時,不執行任何下游節點(沒有明確的 else 分支)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 有問題?
|
|
||||||
|
|
||||||
遇到任何問題直接問。你的 API Key 是確定性的,只要用同一個 email 呼叫 `/register` 就能拿回來:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
curl -X POST https://cypher.arcrun.dev/register \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
-d '{"email":"your@email.com"}'
|
|
||||||
```
|
|
||||||
@@ -1,70 +0,0 @@
|
|||||||
# CLAUDE.md — arcrun
|
|
||||||
|
|
||||||
> 本檔是**索引 + 最高原則**,詳細規範拆到 `.claude/rules/`。
|
|
||||||
> Hook 強制機制在 `.claude/hooks/`,違反會直接 block(exit 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
@@ -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** | 小(20–150KB) | 良好 | TypeScript 語法,前端開發者最快上手 |
|
|
||||||
| **Rust** | 小–中(30–300KB) | 良好 | 效能最強;適合複雜演算法,工具鏈稍複雜 |
|
|
||||||
|
|
||||||
**AI 開發建議:**
|
|
||||||
- 選 **TinyGo**:Go 語法與 TypeScript 差異夠大,AI 不易把 TS 邏輯直接搬過來造成錯誤,是最穩的選擇。
|
|
||||||
- 選 **AssemblyScript**:適合已熟悉 TypeScript 的開發者,但要注意 AS 不是 TS — 提示 AI 時明確說「AssemblyScript,不是 TypeScript」。
|
|
||||||
- 選 **Rust**:效能要求高時使用;需要更詳細的提示和更仔細的審查。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 零件規格:共通規則
|
|
||||||
|
|
||||||
無論使用哪個語言,零件必須遵守:
|
|
||||||
|
|
||||||
- **I/O 模型**:從 `stdin` 讀取 JSON,往 `stdout` 輸出 JSON,不使用 return value
|
|
||||||
- **回傳格式**:成功 `{"success":true,"result":...}`,失敗 `{"success":false,"error":"..."}`
|
|
||||||
- **不 panic**:任何錯誤都應輸出 `success:false` JSON,不讓執行器收到空輸出
|
|
||||||
- **不使用網路 / 檔案系統**(功能類零件):`no_network_syscall: true`
|
|
||||||
- **允許網路**(整合類零件):`no_network_syscall: false`,必須宣告 `credentials_required`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 目錄結構
|
|
||||||
|
|
||||||
```
|
|
||||||
registry/components/my_component/
|
|
||||||
├── component.contract.yaml # 零件規格宣告(必填)
|
|
||||||
├── main.go # TinyGo 源碼(TinyGo 零件)
|
|
||||||
├── assembly/index.ts # AssemblyScript 源碼(AS 零件)
|
|
||||||
├── src/lib.rs # Rust 源碼(Rust 零件)
|
|
||||||
└── my_component.wasm # 編譯產出(不提交至 git,CI 自動產生)
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## component.contract.yaml
|
|
||||||
|
|
||||||
所有語言共用相同的合約格式:
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
# component_hash_id 由 Registry 在提交時自動派發,格式為 cmp_{8碼hex}
|
|
||||||
# 提交者不需要填這個欄位,Registry 會根據 canonical_id 確定性生成
|
|
||||||
# Workflow 引用零件時,用 component_hash_id 才能保證永久不壞:
|
|
||||||
# component://cmp_a3f9b2c1 ← 推薦,即使 canonical_id 改名也不受影響
|
|
||||||
# component://string_reverse ← 方便,AI 寫 workflow 時用這個,Registry 自動解析
|
|
||||||
|
|
||||||
canonical_id: "string_reverse" # 見下方命名規範
|
|
||||||
display_name: "字串反轉" # 人類可讀,可中文,供 UI 顯示用
|
|
||||||
description: > # 語意搜尋用,見下方說明
|
|
||||||
將字串內容倒序排列,適合測試、資料清洗、回文判斷等場景。
|
|
||||||
不依賴外部服務,純本地運算。
|
|
||||||
category: "data" # api / logic / data / ai / style / anim / ui
|
|
||||||
version: "v1"
|
|
||||||
author: "@your-github-username"
|
|
||||||
wasi_target: "preview1"
|
|
||||||
stability: "floating" # floating / stable / pinned
|
|
||||||
runtime_compat:
|
|
||||||
- "cf-workers"
|
|
||||||
- "workerd"
|
|
||||||
- "wazero"
|
|
||||||
constraints:
|
|
||||||
max_size_kb: 2048
|
|
||||||
max_cold_start_ms: 50
|
|
||||||
no_network_syscall: true # 功能類 true,整合類 false
|
|
||||||
io_model: "stdin_stdout_json"
|
|
||||||
input_schema:
|
|
||||||
type: object
|
|
||||||
required: [text]
|
|
||||||
properties:
|
|
||||||
text:
|
|
||||||
type: string
|
|
||||||
description: 輸入文字
|
|
||||||
output_schema:
|
|
||||||
type: object
|
|
||||||
properties:
|
|
||||||
result:
|
|
||||||
type: string
|
|
||||||
gherkin_tests:
|
|
||||||
- scenario: "基本轉換"
|
|
||||||
given: '{"text":"hello"}'
|
|
||||||
then_contains: '"result"'
|
|
||||||
- scenario: "缺少必填欄位"
|
|
||||||
given: '{}'
|
|
||||||
then_contains: '"success":false'
|
|
||||||
config_example: |
|
|
||||||
transform:
|
|
||||||
text: "{{input.text}}"
|
|
||||||
description: "我的零件功能說明。"
|
|
||||||
```
|
|
||||||
|
|
||||||
整合類零件額外加入:
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
credentials_required:
|
|
||||||
- key: my_api_token
|
|
||||||
type: api_key
|
|
||||||
description: "My Service API token"
|
|
||||||
inject_as: api_token
|
|
||||||
```
|
|
||||||
|
|
||||||
### canonical_id 命名規範
|
|
||||||
|
|
||||||
`canonical_id` 是零件的永久識別符,一旦上架不能更改(改名 = 新零件)。命名不統一會導致功能重複,請遵守以下規則:
|
|
||||||
|
|
||||||
| category | 格式 | 範例 |
|
|
||||||
|----------|------|------|
|
|
||||||
| `api`(整合類) | `{服務名}` 或 `{服務名}_{動作}` | `gmail`、`gmail_send`、`google_sheets`、`google_sheets_append`、`telegram` |
|
|
||||||
| `data`(資料處理) | `{資料型別}_ops` 或 `{動詞}_{名詞}` | `string_ops`、`array_ops`、`json_transform`、`csv_parse` |
|
|
||||||
| `logic`(控制流) | `{結構名}_control` 或單詞動詞 | `if_control`、`foreach_control`、`try_catch`、`switch`、`wait` |
|
|
||||||
| `ai`(AI 類) | `ai_{動作}` | `ai_transform_compile`、`ai_summarize`、`ai_classify` |
|
|
||||||
|
|
||||||
**規則:**
|
|
||||||
- 全部小寫、底線分隔、最多 4 個單詞
|
|
||||||
- 禁止:中文、空格、大寫、連字號(`-`)、版本號混入(用 `version` 欄位表達)
|
|
||||||
- `display_name` 才是人類可讀名稱,可以是「宇宙無敵 gsheets 新增一列」,`canonical_id` 不行
|
|
||||||
|
|
||||||
**提交前自問:** 如果有人想用 AI 搜尋「幫我找一個可以新增 Google Sheets 列的零件」,他搜到的名字應該是什麼?答案就是你的 `canonical_id`。
|
|
||||||
|
|
||||||
### description 寫法(語意搜尋)
|
|
||||||
|
|
||||||
`description` 是語意搜尋的索引來源,用自然語言描述「能做什麼、適合什麼情境」,而不是重複零件名稱。
|
|
||||||
|
|
||||||
**好的 description:**
|
|
||||||
```yaml
|
|
||||||
description: >
|
|
||||||
傳送 Gmail 電子郵件,適合 Workflow 完成後通知使用者、發送訂閱確認信、
|
|
||||||
錯誤警報通知等場景。支援自訂主旨、內文與收件人。需要 Gmail OAuth token。
|
|
||||||
```
|
|
||||||
|
|
||||||
**不好的 description(等於沒有):**
|
|
||||||
```yaml
|
|
||||||
description: "Gmail 發信零件" # 只是名稱的同義詞,搜不到任何額外資訊
|
|
||||||
```
|
|
||||||
|
|
||||||
原則:把這個 description 給一個不知道這個零件存在的人看,他能判斷「這就是我要的東西」嗎?
|
|
||||||
|
|
||||||
### aliases(搜尋同義詞)
|
|
||||||
|
|
||||||
arcrun 在 `registry/aliases.yaml` 維護一份 scope 級別的同義詞表。當你的零件 `canonical_id` 以已知 scope 為前綴,Registry 建立搜尋索引時會**自動**把對應的同義詞合併進去,不需要在 contract 裡手動填。
|
|
||||||
|
|
||||||
例如 `canonical_id: google_sheets_append`,Registry 會自動從 aliases.yaml 取得 `google_sheets` scope 的同義詞(`gsheets`、`試算表`、`spreadsheet`...),搜這些詞都能找到你的零件。
|
|
||||||
|
|
||||||
**如果你的零件有額外的情境同義詞**(超出 scope 範圍),可以在 contract 內手動補充:
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
canonical_id: "google_sheets_append"
|
|
||||||
aliases:
|
|
||||||
- "新增資料列" # 情境同義詞,超出 scope 範圍
|
|
||||||
- "insert row"
|
|
||||||
# google_sheets scope 的同義詞(gsheets / 試算表 / spreadsheet...)
|
|
||||||
# 由 registry/aliases.yaml 自動合併,不需要重複填寫
|
|
||||||
```
|
|
||||||
|
|
||||||
**想新增新 scope 的同義詞**(例如你要加一個 `notion` 零件):在 `registry/aliases.yaml` 的對應 category 下加一個新 key,開 PR,merge 後所有以 `notion_` 開頭的零件都自動繼承。
|
|
||||||
|
|
||||||
> 這個機制目前是手工維護。未來接入 KBDB 後,`canonical_id` 將獲得系統派發的唯一 hash id,同義詞表將成為 KBDB synonym graph 的初始資料。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## TinyGo 零件開發
|
|
||||||
|
|
||||||
### 環境安裝
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# TinyGo
|
|
||||||
brew install tinygo # macOS
|
|
||||||
# 其他平台:https://tinygo.org/getting-started/
|
|
||||||
|
|
||||||
# 本機測試執行器
|
|
||||||
brew install wasmtime # macOS
|
|
||||||
```
|
|
||||||
|
|
||||||
### 給 AI 的提示範本
|
|
||||||
|
|
||||||
```
|
|
||||||
請幫我用 TinyGo 寫一個 arcrun WASM 零件。
|
|
||||||
|
|
||||||
需求:[你的需求]
|
|
||||||
|
|
||||||
規則:
|
|
||||||
- 從 stdin 讀取 JSON,往 stdout 輸出 JSON
|
|
||||||
- 成功回傳 {"success":true,"result":...}
|
|
||||||
- 失敗回傳 {"success":false,"error":"..."},不 panic
|
|
||||||
- 不使用網路、不使用檔案系統(純功能類零件)
|
|
||||||
- import 只用標準庫(encoding/json, os, fmt, strings 等)
|
|
||||||
|
|
||||||
請生成 main.go 和 component.contract.yaml。
|
|
||||||
```
|
|
||||||
|
|
||||||
### main.go 範本
|
|
||||||
|
|
||||||
```go
|
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Input struct {
|
|
||||||
Text string `json:"text"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type Output struct {
|
|
||||||
Success bool `json:"success"`
|
|
||||||
Result string `json:"result,omitempty"`
|
|
||||||
Error string `json:"error,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
var input Input
|
|
||||||
if err := json.NewDecoder(os.Stdin).Decode(&input); err != nil {
|
|
||||||
writeError("invalid input: " + err.Error())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if input.Text == "" {
|
|
||||||
writeError("text is required")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 你的邏輯
|
|
||||||
result := "[transformed] " + input.Text
|
|
||||||
|
|
||||||
out, _ := json.Marshal(Output{Success: true, Result: result})
|
|
||||||
fmt.Println(string(out))
|
|
||||||
}
|
|
||||||
|
|
||||||
func writeError(msg string) {
|
|
||||||
out, _ := json.Marshal(Output{Success: false, Error: msg})
|
|
||||||
fmt.Println(string(out))
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 編譯
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd registry/components/my_component
|
|
||||||
tinygo build -o my_component.wasm -target wasi .
|
|
||||||
```
|
|
||||||
|
|
||||||
### 本機測試
|
|
||||||
|
|
||||||
```bash
|
|
||||||
echo '{"text":"hello world"}' | wasmtime run my_component.wasm
|
|
||||||
# 預期:{"success":true,"result":"[transformed] hello world"}
|
|
||||||
|
|
||||||
echo '{}' | wasmtime run my_component.wasm
|
|
||||||
# 預期:{"success":false,"error":"text is required"}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## AssemblyScript 零件開發
|
|
||||||
|
|
||||||
### 環境安裝
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Node.js >= 18
|
|
||||||
node --version
|
|
||||||
|
|
||||||
# 初始化 AS 專案
|
|
||||||
npm init -y
|
|
||||||
npm install --save-dev assemblyscript
|
|
||||||
npx asinit .
|
|
||||||
|
|
||||||
# 本機測試執行器
|
|
||||||
brew install wasmtime # macOS
|
|
||||||
```
|
|
||||||
|
|
||||||
### 給 AI 的提示範本
|
|
||||||
|
|
||||||
```
|
|
||||||
請幫我用 AssemblyScript(不是 TypeScript)寫一個 arcrun WASM 零件。
|
|
||||||
|
|
||||||
需求:[你的需求]
|
|
||||||
|
|
||||||
規則:
|
|
||||||
- AssemblyScript 是 TypeScript 的子集,編譯為 WASM,不能使用 DOM / Node.js API
|
|
||||||
- 從 stdin 讀取 JSON(使用 WASI fd_read),往 stdout 輸出 JSON(使用 Console.log)
|
|
||||||
- 成功回傳 {"success":true,"result":...}
|
|
||||||
- 失敗回傳 {"success":false,"error":"..."}
|
|
||||||
- 不使用網路、不使用檔案系統
|
|
||||||
|
|
||||||
請生成 assembly/index.ts 和 component.contract.yaml。
|
|
||||||
注意:AssemblyScript 沒有 JSON.parse,需要手動解析或使用 as-json 套件。
|
|
||||||
```
|
|
||||||
|
|
||||||
### assembly/index.ts 範本
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// AssemblyScript — 注意:這不是 Node.js / TypeScript!
|
|
||||||
// 沒有 DOM、沒有 fetch、沒有 require
|
|
||||||
|
|
||||||
import { Console } from "as-wasi/assembly";
|
|
||||||
import { JSON } from "assemblyscript-json/assembly";
|
|
||||||
|
|
||||||
export function _start(): void {
|
|
||||||
// 從 stdin 讀取輸入
|
|
||||||
const input = Console.readAll();
|
|
||||||
|
|
||||||
// 解析 JSON
|
|
||||||
const parsed = JSON.parse(input);
|
|
||||||
if (!parsed.isObj) {
|
|
||||||
Console.log('{"success":false,"error":"invalid input"}');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const obj = parsed as JSON.Obj;
|
|
||||||
const textVal = obj.getString("text");
|
|
||||||
if (textVal == null) {
|
|
||||||
Console.log('{"success":false,"error":"text is required"}');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const text = textVal.valueOf();
|
|
||||||
|
|
||||||
// 你的邏輯
|
|
||||||
const result = "[transformed] " + text;
|
|
||||||
|
|
||||||
Console.log('{"success":true,"result":"' + result + '"}');
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**專案依賴(package.json):**
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"dependencies": {
|
|
||||||
"as-wasi": "^0.4.7",
|
|
||||||
"assemblyscript-json": "^1.1.0"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 編譯
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd registry/components/my_component
|
|
||||||
npm install
|
|
||||||
npx asc assembly/index.ts \
|
|
||||||
--target release \
|
|
||||||
--outFile my_component.wasm \
|
|
||||||
--exportRuntime \
|
|
||||||
--use abort=~lib/wasi_abort
|
|
||||||
```
|
|
||||||
|
|
||||||
### 本機測試
|
|
||||||
|
|
||||||
```bash
|
|
||||||
echo '{"text":"hello world"}' | wasmtime run my_component.wasm
|
|
||||||
# 預期:{"success":true,"result":"[transformed] hello world"}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Rust 零件開發
|
|
||||||
|
|
||||||
Rust 零件支援已就緒,但文件尚在完善中。如果你熟悉 Rust + WASM,歡迎參考 [wasm-wasi 官方文件](https://doc.rust-lang.org/stable/reference/linkage.html),核心要求與其他語言相同:WASI preview1,stdin/stdout JSON I/O。
|
|
||||||
|
|
||||||
基本設定:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
rustup target add wasm32-wasip1
|
|
||||||
cargo build --target wasm32-wasip1 --release
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 提交至公眾 Registry
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# 確保 .wasm 已編譯
|
|
||||||
ls my_component.wasm
|
|
||||||
|
|
||||||
# 提交(需要 arcrun.dev API Key)
|
|
||||||
acr parts publish ./registry/components/my_component/
|
|
||||||
```
|
|
||||||
|
|
||||||
提交後流程:
|
|
||||||
|
|
||||||
| 狀態 | 說明 |
|
|
||||||
|------|------|
|
|
||||||
| `sandbox_pending` | 沙盒驗收執行中 |
|
|
||||||
| `author_only` | 驗收通過,你自己可用 |
|
|
||||||
| `public` | 人工審核通過,所有人可用,開始累積統計 |
|
|
||||||
|
|
||||||
查詢審核進度:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
acr parts publish --status <submission_id>
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 常見問題
|
|
||||||
|
|
||||||
### `no_network_syscall` 設定錯誤
|
|
||||||
|
|
||||||
- **功能類**(category: logic / data / ai):`no_network_syscall: true`。這類零件應完全沙盒化。
|
|
||||||
- **整合類**(category: api):`no_network_syscall: false`,因為要呼叫外部 API。
|
|
||||||
|
|
||||||
兩者都需要宣告在 `constraints` 下,設錯會在 syscall 掃描步驟被沙盒拒絕。
|
|
||||||
|
|
||||||
### `gherkin_tests` 必須包含 happy path 和 error path
|
|
||||||
|
|
||||||
至少兩個測試場景:一個輸入正確的 happy path、一個缺少必填欄位或輸入非法的 error path。
|
|
||||||
|
|
||||||
### 體積超過上限
|
|
||||||
|
|
||||||
- TinyGo:確認使用 `-target wasi`(而非 `-target wasm`),前者體積更小
|
|
||||||
- AssemblyScript:加上 `--optimize` 或 `--target release`
|
|
||||||
- Rust:使用 `--release` 並加入 `opt-level = "z"` 到 `Cargo.toml`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 問題回報
|
|
||||||
|
|
||||||
開 Issue:[github.com/richblack/arcrun/issues](https://github.com/richblack/arcrun/issues)
|
|
||||||
-340
@@ -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 WASM:auth_static_key / auth_service_account / auth_oauth2 / auth_mtls)
|
|
||||||
|
|
||||||
**其他一切都是 recipe。** recipe = http_request + 一組固定設定(YAML 文字)。
|
|
||||||
|
|
||||||
| | primitive | recipe |
|
|
||||||
|---|---|---|
|
|
||||||
| 是什麼 | WASM Worker | YAML 文字 |
|
|
||||||
| 要不要 deploy | 要 | 不用 |
|
|
||||||
| 多久變一次 | 幾乎不變 | 一直長新的 |
|
|
||||||
|
|
||||||
**判準(真零件 vs 假零件):** 一個零件若滿足任一條,它是假零件,該降級成 recipe:
|
|
||||||
- contract 或原始碼出現具體外部服務的 URL / domain
|
|
||||||
- 它宣告的能力是 http_request 的子集(打某固定 endpoint)
|
|
||||||
|
|
||||||
→ D1 / KV / Vectorize / Supabase / KBDB 的存取,**一律是 http_request + recipe**,
|
|
||||||
絕不做 `d1_crud`、`kbdb_get` 這種「假零件」。
|
|
||||||
|
|
||||||
### 「recipe」有三種,不要混用這個詞
|
|
||||||
|
|
||||||
Arcrun 裡有三個都叫 "recipe" 的東西,職責不同:
|
|
||||||
|
|
||||||
| 名稱 | 是什麼 | KV key |
|
|
||||||
|---|---|---|
|
|
||||||
| **API recipe** | http_request + endpoint/method/headers/body 模板(`RecipeDefinition`) | `recipe:{canonical_id}` |
|
|
||||||
| **auth recipe** | 認證設定:primitive + base_url + 注入規則(`AuthRecipeDefinition`) | `auth_recipe:{service}` |
|
|
||||||
| **prompt recipe** | LLM prompt 的封裝,與 KBDB block 有關 | `prompt_recipe:{name}` |
|
|
||||||
|
|
||||||
一般講「recipe」= API recipe。假零件降級的終點是 **API recipe**(`recipe:{id}`)。
|
|
||||||
降級作業不碰 auth recipe、不碰 prompt recipe(prompt recipe 犍涉 KBDB block 展開,
|
|
||||||
是 BACKLOG 待決策項,不在此範圍)。
|
|
||||||
|
|
||||||
### recipe 與 primitive 的驗收標準不同(早期已定,2026-05 重新確認)
|
|
||||||
|
|
||||||
- **primitive:Gherkin 通過 = 驗收通過。** primitive 是封閉的邏輯,正確性不依賴外部世界,
|
|
||||||
可以用「given / when / then」確定地驗證。
|
|
||||||
- **recipe:打得通(2xx)= 驗收通過。** recipe 是「指向外部 API 的指針」,正確性一半在定義
|
|
||||||
(打不通就代表定義錯,「打通」已驗)、一半在外部服務當下的行為。
|
|
||||||
|
|
||||||
**關鍵認識:recipe 不用 Gherkin,不是偷懶,是 Gherkin 對 recipe 沒用。**
|
|
||||||
「2xx 但外部服務沒真的做事」「外部服務改了 API」——這些是 recipe 唯一的真實風險,
|
|
||||||
而 Gherkin 一樣擋不住(Gherkin 測的當下沒改就過,之後改了它早跑完了)。
|
|
||||||
能補這個風險的只有「執行 → 回報 → 修正」的市場機制,不是任何靜態驗收。
|
|
||||||
給 recipe 加 Gherkin = 花成本做一件不會多驗到任何東西的事。
|
|
||||||
|
|
||||||
recipe 的「語義正確性」(真的刪了那列嗎)交給市場:A 的 recipe 在 B 那裡失效,
|
|
||||||
B 的 AI 會因為「目的沒達成」去查、去修、提修改版。Arcrun 不監控全世界的 API 變動。
|
|
||||||
|
|
||||||
**例外——可以是 primitive 的「引擎內建能力」:** 若某能力不依賴任何外部 endpoint、
|
|
||||||
是 Arcrun 執行環境本身自帶的(如「workflow 中途暫存」),它可以是 primitive。
|
|
||||||
判準:「這段邏輯依不依賴外部服務的 endpoint?」依賴→recipe;引擎自帶→可 primitive。
|
|
||||||
|
|
||||||
### 工作流是 default,建零件要過人類閘門(2026-05 補,CC 把自用服務錯做成零件後定)
|
|
||||||
|
|
||||||
AI 開發時的預設順序:
|
|
||||||
|
|
||||||
1. **預設寫工作流**(串服務 / 自用 / 給少數人,都先工作流)。default、阻力最小。
|
|
||||||
2. **零件的正當時機**:服務不提供串接但有 API,且**有必要讓全 Arcrun 生態重用** → 才建零件
|
|
||||||
(零件 = API 薄殼,只打一個 endpoint)。
|
|
||||||
3. **建零件 = 過人類閘門**。看到「有 API 可包成零件」≠「該包」,先問「你有必要嗎?」。
|
|
||||||
AI 不可自行建零件,必須 (a) 經人類互動確認;(b) 明示舉證「為何工作流做不到」
|
|
||||||
(舉證責任在 AI,預設假設工作流能做)。把關點在「建立零件的 API」本身——
|
|
||||||
CLI / MCP / Python lib / JS lib 四路全收斂到這關。
|
|
||||||
|
|
||||||
**為什麼要人類閘門**:零件進公共庫 = 全生態都能打它。自用服務(通訊錄 / 帳本)沒設驗證就變零件
|
|
||||||
= 公開後門。安全 / 意圖機器判不了,必須人看。規範會忘、hook 不會(§7:判準寫成機械紅燈)。
|
|
||||||
|
|
||||||
**不限制自由**:別人要建零件是他的自由(開無驗證服務給人串也是),唯一硬約束「零件 = endpoint 薄殼」。
|
|
||||||
閘門不是禁止,是「要建得先說服人 + 舉證」的摩擦。
|
|
||||||
|
|
||||||
**ABC 三管齊下讓 AI 不選難路**:A 審核當場擋(§7 層二)+ B 工作流範本好寫(§7 層一)
|
|
||||||
+ C mindset 明示預設(§7 層三)。人類閘門是第四道,專擋意圖 + 安全。
|
|
||||||
原理:難路走的當下要痛、易路選的當下要爽、事先有聲音說易路是 default。
|
|
||||||
CC 把自用服務錯做成零件,正因這三者當時全缺。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 2. TS 邊界規則(哪些程式碼能用 TS,哪些不能)
|
|
||||||
|
|
||||||
**判準:這段程式碼是「執行引擎 / 工具本身」,還是「一個零件該做的事」?**
|
|
||||||
- 引擎 / 工具(cypher-executor 解析 graph、CLI 讀 YAML、registry 驗收)→ **TS,第一期合法**
|
|
||||||
- 零件該做的事(打 API、處理資料、做認證)→ **必須 WASM(TinyGo),用 TS = 犯規**
|
|
||||||
|
|
||||||
**靠位置判斷,不靠肉眼判斷內容:**
|
|
||||||
- `registry/components/` 底下出現 `.ts` = 犯規(該目錄只准 .wasm + contract.yaml)
|
|
||||||
- `cypher-executor/src/` 底下的 `.ts` = 第一期合法(引擎程式碼,Tier 1/2 部署於 Cloudflare,
|
|
||||||
本來就用 TS)。注意:這是「引擎邏輯可用 TS」,**不是**「引擎永遠是 Cloudflare 專屬」。長期見 §4。
|
|
||||||
|
|
||||||
**為什麼零件必須 TinyGo 不能 TS:** TS/JS 編不出獨立輕量的 WASI .wasm——
|
|
||||||
JS 需要一個 JS 引擎來跑,塞進 wasm 體積爆炸。TinyGo/AS/Rust 直接編譯成
|
|
||||||
自包含、只依賴 WASI 標準介面的 wasm(10–80KB)。這是三層 runtime 的物理前提(見 §4)。
|
|
||||||
TinyGo 為官方首選(語法與 TS 差異夠大,AI 不易把純 TS 邏輯誤搬)。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 3. 開源 / 商業邊界
|
|
||||||
|
|
||||||
| 開源核心(MIT) | 服務側(付費 / 需 API Key) |
|
|
||||||
|---|---|
|
|
||||||
| cypher-executor、四種 primitive WASM、CLI(acr)、registry | KBDB 語義搜尋、KBDB graph 查詢、Persona / Mira 等 |
|
|
||||||
|
|
||||||
**KBDB 採 Supabase 模式:** KBDB 的 recipe 顯示在公共零件庫(能力可見=引子),
|
|
||||||
要用就申請 API Key(註冊=轉化),用爽了付費。arcrun.dev 已有「取得 API Key」入口。
|
|
||||||
|
|
||||||
**「執行」與「發現」解耦:**
|
|
||||||
- 執行:永遠本地、免費、可離線。不依賴公共庫。
|
|
||||||
- 發現(語義搜尋):連線加值。self-hosted 用 local 關鍵字搜本地下載過的;
|
|
||||||
連公共庫才能用 KBDB 語義搜尋(在大集合裡查意圖)。
|
|
||||||
|
|
||||||
**cypher-executor 裡的 KBDB = 污染(清);公共零件庫的 KBDB = 服務(留,但不在第一期)。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 3b. credential 是引擎能力(不是用戶零件)
|
|
||||||
|
|
||||||
(基於 2026-05 查核 graph-executor / credential-injector / auth-dispatcher 原始碼)
|
|
||||||
|
|
||||||
credential 的注入不是一個 workflow 節點做的事。它是**執行引擎在「呼叫零件之前」
|
|
||||||
自動做的一個步驟**:graph-executor.executeNode 的流程是:
|
|
||||||
組 ctx → 注入 credential → 才 runner(ctx)。零件拿到 ctx 時 credential 已被放進去,
|
|
||||||
零件自己不知道 credential 怎麼來的。
|
|
||||||
|
|
||||||
**credential 的本質 = KV 裡的加密值 + 一段「在零件執行前注入 ctx」的引擎逓輯。**
|
|
||||||
那四個 `auth_*` WASM(static_key / service_account / oauth2 / mtls)是「注入步驟的後端」,
|
|
||||||
不是用戶會在 workflow 擺的零件。用戶永遠不會直接呼叫 `auth_static_key`——
|
|
||||||
是 auth-dispatcher 在背後呼叫它。
|
|
||||||
→ 「零件白名單」「假零件判準」不適用於 `auth_*`,它們不是用戶零件。
|
|
||||||
|
|
||||||
**credential 系統現状是「新舊兩路並存」的半成品:**
|
|
||||||
- 新路(對的方向):`auth-dispatcher` → HTTP 打 `auth_*` WASM,解密/JWT 全在 WASM 內。
|
|
||||||
已支援 static_key / service_account / oauth2;mtls 尚未(Phase 4)。
|
|
||||||
- 舊路(要砸):`injectCredentials` TS 裡解密,含 `BUILTIN_CREDENTIALS_MAP`。
|
|
||||||
註解自認 Phase 1.9 將刪除。砦舊路是獨立清理,**不擋降級**(見 BACKLOG)。
|
|
||||||
|
|
||||||
**注入靠 `auth_recipe:{componentId}` 觸發。** 一個服務要能被注入 credential,
|
|
||||||
必須有對應的 auth recipe。KBDB 用 static_key,而 static_key 新路已支援
|
|
||||||
→ 降級 KBDB 的 credential 前置是「小」的(只需建一個 `auth_recipe:kbdb`)。
|
|
||||||
|
|
||||||
**修正一個舊裁決:** API recipe 的 `credentials_required` 欄位 **要留**。
|
|
||||||
`makeRecipeRunner`(零件執行)不讀它,但 `injectCredentials`(零件執行前的注入步驟)
|
|
||||||
會讀它。credential 不是在 runner 裡處理,是在 runner 之前那一步處理。**
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 3c. execute vs test:意圖決定路徑(服務側,不在第一期)
|
|
||||||
|
|
||||||
一個 AI 開發 recipe,有兩種意圖,對應兩個指令:
|
|
||||||
|
|
||||||
- **只打算自己用** → 用 `execute`。直連目標 API,**不經過 arcrun**,self-hosted 純粋。
|
|
||||||
- **打算公佈到公共零件庫換 credits** → 從開發的第一次打就用 `test`,`test` 明示走 arcrun relay。
|
|
||||||
|
|
||||||
**關鍵:用 `test` 這個動作本身,就是「我打算公佈」的意思表示。**
|
|
||||||
AI 用 test 開發時,每一次打都經過 relay,arcrun 當下就看到真實打通記錄——
|
|
||||||
不需 AI 事後交一份自己寫的 log(執行者不能驗證自己,見 §7),也不需 arcrun 事後重打
|
|
||||||
(重打 delete 要自備測試環境,跟開發者工作重複、荒謬)。
|
|
||||||
|
|
||||||
### test relay 經手 credential — 誠實處理
|
|
||||||
|
|
||||||
`test` 走 relay,請求裡帶著 credential。**relay 為了轉發給目標 API,必須在內部
|
|
||||||
持有明文 credential 一瞬間——這是 proxy 的物理本質,加密絕對絍不過。**
|
|
||||||
(加密只能保護「客戶→relay」傳輸途中防監聽;relay 內部必然看得到明文。)
|
|
||||||
|
|
||||||
唯一誠實的處理方式(四道合起來才站得住):
|
|
||||||
1. **明示告知**:`acr` 第一次用 test 就告訴用戶「test 會讓請求(含 credential)經過 arcrun relay,
|
|
||||||
arcrun 只記錄 HTTP 回應、不記錄 credential。要完全不經過請用 execute」。
|
|
||||||
2. **「不記錄」從「承諸」升級成「可驗證」**:relay 是開源的(arcrun 核心 MIT),
|
|
||||||
用戶不需「相信」,可以讀 relay 原碼確認它沒記錄 credential。
|
|
||||||
relay 程式上:credential 欄位全程不寫日誌、不寫存儲,只在轉發那一瞬間的記憶體。
|
|
||||||
3. **傳輸層 TLS**:客戶 → relay → 目標全程加密,防線路監聽。
|
|
||||||
4. **縮小經手 credential 的價值**:用 test 開發時建議用測試帳號的 credential,不是生產環境的。
|
|
||||||
|
|
||||||
**絕不做「假加密」**——不要讓用戶以為連 relay 都看不到 credential。
|
|
||||||
兩段分開誠實講:「傳輸途中加密(防監聽)+ relay 內部短暫持有明文(開源可驗證、不記錄)」。
|
|
||||||
|
|
||||||
**範圍:** `test` / relay / credits 這整套是服務側工程,依賴公共零件庫與 credits 系統存在。
|
|
||||||
**不在第一期**(第一期是 self-hosted 能跡、`execute` 能用)。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 4. cypher-binding、三層 runtime、執行核心(長期,現在不做)
|
|
||||||
|
|
||||||
> 註:credential 是引擎能力、不是第四種 primitive。詳見下方 §3b(基於 2026-05 原始碼查核)。
|
|
||||||
|
|
||||||
### cypher-binding 是什麼
|
|
||||||
workflow 不是「部署出來的東西」,是「一張可隨時改的紙」——紙上寫一排零件 + 順序 + 條件。
|
|
||||||
執行核心讀紙:跑第一個,跑完回來問紙,紙說下一個是誰,就跑下一個。
|
|
||||||
對比 service binding(CF 機制,a/b 都要 deploy):cypher-binding **不 deploy**,
|
|
||||||
改 workflow 零部署成本。可能慢一點,但「寫好、按下去就跑」(像 n8n)。
|
|
||||||
|
|
||||||
### 三層 runtime 目標
|
|
||||||
零件與執行核心目標是能跑在三種環境:
|
|
||||||
- Tier 1:Cloudflare Workers(現況)
|
|
||||||
- Tier 2:企業自架 workerd(不信任 CF 雲)
|
|
||||||
- Tier 3:極輕量 WASI runtime(wazero,無人機 / 邊緣設備)
|
|
||||||
|
|
||||||
**現在不寫任何 wazero / workerd 程式碼。** 第一期沒有 Tier 2/3 用戶。
|
|
||||||
避債的方式不是「現在做三層」,是「現在不要做任何擋死三層的決定」。
|
|
||||||
|
|
||||||
### cypher-executor 概念上分兩層
|
|
||||||
(現在糊在一起。這**不是會累積的債**——它是一支程式、就一支,未來一次性重構即可;
|
|
||||||
不像零件會複利累積。它是「待設計的未來解法」,不需要為它焦慮。)
|
|
||||||
|
|
||||||
- **(1) 執行核心** — 讀紙、依序/依條件呼叫零件。應能編成純 WASI,跑三層任何地方。
|
|
||||||
- **(2) Cloudflare 整合層** — webhook / KV / cron / Service Binding / HTTP 路由,只服務 Tier 1/2。
|
|
||||||
|
|
||||||
Tier 3(無人機)只需要 (1)。現在不拆,但**從現在起新程式碼要有意識地把
|
|
||||||
「讀紙、呼叫零件的核心邏輯」和「Cloudflare 特有存取」分開寫,不要糊得更死**。
|
|
||||||
|
|
||||||
### KV 依賴的根源,與「紙要自包含」
|
|
||||||
現在 cypher-executor 依賴 KV,根源**不是**「紙存在 KV」,是「紙上寫的是 hash
|
|
||||||
(cmp_xxx / rec_xxx),要查 KV 才能翻譯成真正的零件」。
|
|
||||||
|
|
||||||
**hash 查表不是會累積的債**——它是 component-loader 裡固定的一段邏輯,零件再多它也不變
|
|
||||||
(同一段邏輯處理更多資料 ≠ 需要更多段邏輯)。但它讓「紙」無法自包含。
|
|
||||||
|
|
||||||
Tier 3 的正解**不是**「帶一個 SQLite 上無人機來翻譯 hash」——那只是把依賴從 KV 換成
|
|
||||||
SQLite,沒有消除依賴。正解是**紙本身自包含**:紙上直接寫 URL / 內嵌 recipe YAML,
|
|
||||||
不寫 hash。無人機上只有「自包含的紙 + WASM 零件」,不需要任何查表設施。
|
|
||||||
|
|
||||||
- hash = Tier 1 的**儲存格式**(KV 去重 / 版本管理,Tier 1 保留無妨)
|
|
||||||
- 自包含 = **執行格式**
|
|
||||||
- 中間隔一個「展開」步驟:打包給無人機時做一次,不是執行時做(類似 `acr push` 的轉換)
|
|
||||||
|
|
||||||
**附帶好處:** 自包含的紙人類 / AI 可直讀——這正是 Arcrun 核心賣點(紙人人可讀)。
|
|
||||||
hash 其實偷偷腐蝕了這個賣點。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 5. 第一期範圍鎖定
|
|
||||||
|
|
||||||
**第一期用戶 = 會開 CF 帳號的開發者,在 VSCode 用 Claude Code(CC),self-hosted。**
|
|
||||||
鎖定 Claude,因為所有「防做歪」機制都是針對 Claude 行為校準的;
|
|
||||||
Gemini / Codex 服從度不同,第一期不支援。
|
|
||||||
|
|
||||||
**第一期做:** 清污染 → 降級假零件成 recipe → 補零件庫真把關 →
|
|
||||||
白名單 hook → (搬家)→ mindset Skill → README 重寫成單一路徑 → acr init --self-hosted。
|
|
||||||
|
|
||||||
**第一期明確不做:** SaaS、API Key 多租戶、小白 onboarding、視覺化的圖、
|
|
||||||
arcrun-gui 拖拉畫布、arcrun-mcp 命名大改、新 primitive、Gemini/Codex 支援、
|
|
||||||
三層 runtime、KBDB 訂閱層。
|
|
||||||
|
|
||||||
**SaaS 解凍條件(不靠心情):** (a) self-hosted 有 ≥3 個你以外的人部署成功並貢獻
|
|
||||||
≥1 個 recipe,且 (b) 視覺化 Skill 已驗證「AI 畫的圖能讓非技術者看懂」。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 6. CF 帳號與專案模型
|
|
||||||
|
|
||||||
- **CF 帳號:一個人一個,永遠就一個。** 不隨專案增加。「實驗環境」是一個 prefix,不是一個帳號。
|
|
||||||
- **Arcrun 部署:一套就夠**(像 n8n 就一套)。不是每個專案各一套。
|
|
||||||
- **「專案」是 Arcrun 的一等公民**:專案 = 一組「引用 workflow」的三元組關係(存在 KBDB / 三元組儲存,
|
|
||||||
不需要新資料表)。同一個 workflow 可被多個專案引用,改一次全部生效。
|
|
||||||
- 預設共用 workflow(引用);只有結構性差異才 fork,且 fork 要有摩擦感。
|
|
||||||
- credential 綁專案:KV key 帶專案前綴(`mira:notion_token`),邏輯隔離,非物理隔離。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 7. 「讓 AI 不做歪」的三層機制 + 閉環
|
|
||||||
|
|
||||||
**閉環原則:下指令的、執行的、驗證的,必須是三個不同的角色。**
|
|
||||||
執行者驗證自己 = 沒有驗證。驗證的標準必須來自執行者碰不到、改不了的地方
|
|
||||||
(一份判準 / 一個程式 / 一個拿著判準的獨立角色)。
|
|
||||||
|
|
||||||
閉環:指令(說要什麼,不說怎麼做)→ 查閱判準 → 執行 → 獨立驗證 → 不合格退回並告知正路。
|
|
||||||
|
|
||||||
**事前防禦(擋已知的錯):**
|
|
||||||
- 層一:範本——AI 不從白紙生成,永遠在改一個正確的範本(acr new / scaffold)
|
|
||||||
- 層二:會回嘴的 CLI——走歪當場 exit 2 + 指回正路(這是真護城河)
|
|
||||||
- 層三:mindset Skill——給 AI「Arcrun 很簡單、一切皆 recipe」的世界觀
|
|
||||||
|
|
||||||
**事後機制(抓漏網的錯):** 事前防禦永遠堵不滿,剩下交給事後:
|
|
||||||
- 第一層:可審計軌跡——每個動作留不可竄改記錄,事後能追
|
|
||||||
- 第二層:不變式測試——核心原則寫成自動測試,每次 commit 跑(最重要)
|
|
||||||
- 第三層:定期獨立審查——拿判準重看,審查者手上必須有判準
|
|
||||||
|
|
||||||
**關鍵心態:不要訓練自己的「辨識能力」**——不可靠、會累、無法轉移給 low-code 用戶。
|
|
||||||
要把判準寫成機械測試,讓「紅燈」代替「辨識」。同一個錯誤,以「問句」形式出現你抓不到,
|
|
||||||
以「紅燈」形式出現你一定抓到。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 8. 執行鏈路不依賴 GitHub Actions(零件投稿例外,2026-05-30 釐清)
|
|
||||||
|
|
||||||
Arcrun 第一期的**執行鏈路**(init / push / run / recipe)全在「用戶機器 + Cloudflare」之間,
|
|
||||||
不經過 GitHub Actions。這是常態高頻動作,用戶不該被 CI 卡住。理由見 §0 第一條。
|
|
||||||
|
|
||||||
**零件投稿是例外,走 GitHub PR + CI(2026-05-30):**
|
|
||||||
- 零件投稿是**稀有低頻**事件(primitive 極少、未來絕大部分是 recipe;建零件要過人類閘門)。
|
|
||||||
- 稀有事件用 PR 治理最自然:**PR 必須有人 merge = 人類閘門**(AI 偽造不了 GitHub approve);
|
|
||||||
把關(假零件偵測 / 純WASI / Gherkin / 覆蓋檢查)由 **CI(PR check)跑**。
|
|
||||||
- 為何非 CI 不可:**CF Workers 禁止 request-time 編譯 WASM** → registry Worker 跑不了 Gherkin / 向量;
|
|
||||||
CI 有 tinygo + 能 runtime 跑 wasm,是唯一既能跑 wasm 又「執行者碰不到」的 venue。
|
|
||||||
- **這不違反 §8 精神**:§8 防的是「高頻執行鏈路被 CI 卡住」;零件投稿稀有、且該由 PR 把關,
|
|
||||||
用 PR/CI 反而更對。兩者是不同性質的事。
|
|
||||||
- 詳見 `.agents/specs/component-gatekeeping/`。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 附:如何判斷「某個東西會不會累積成債」
|
|
||||||
|
|
||||||
通用判準——當「東西」變多時:
|
|
||||||
- **同一段邏輯處理更多資料** → 不累積。(例:hash 查表,零件再多也是同一段查表邏輯)
|
|
||||||
- **需要更多段邏輯、更多特例** → 會累積。(例:零件,每個是獨立程式、各自可能出錯)
|
|
||||||
|
|
||||||
不累積的東西不需要焦慮,它頂多是「未來一次性處理的設計點」。
|
|
||||||
會累積的東西必須在「進入點」就把關(例:零件在投稿時就驗純淨)。
|
|
||||||
@@ -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.
|
||||||
@@ -6,6 +6,20 @@ arcrun 是一套**給 AI(Claude 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 協作做自動化還是很有摩擦:
|
AI 愈來愈強,但跟 AI 協作做自動化還是很有摩擦:
|
||||||
|
|||||||
@@ -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 -->
|
||||||
@@ -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. 需要 credential(API 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 後面的文字會接在這裡)
|
||||||
@@ -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 包成 recipe,workflow 裡用 component 引用它。見 arcrun-mindset Skill。"
|
||||||
|
fi
|
||||||
|
|
||||||
|
exit 0
|
||||||
@@ -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
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "arcrun",
|
"name": "arcrun",
|
||||||
"version": "1.1.0",
|
"version": "1.2.0",
|
||||||
"description": "AI Workflow CLI for arcrun — deploy and run WASM-based AI workflows on Cloudflare",
|
"description": "AI Workflow CLI for arcrun — self-host WASM-based AI workflows on your own Cloudflare",
|
||||||
"bin": {
|
"bin": {
|
||||||
"acr": "dist/index.js"
|
"acr": "dist/index.js"
|
||||||
},
|
},
|
||||||
@@ -27,7 +27,8 @@
|
|||||||
"node": ">=18.0.0"
|
"node": ">=18.0.0"
|
||||||
},
|
},
|
||||||
"files": [
|
"files": [
|
||||||
"dist/"
|
"dist/",
|
||||||
|
"harness/"
|
||||||
],
|
],
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"cloudflare",
|
"cloudflare",
|
||||||
@@ -40,6 +41,6 @@
|
|||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "git+https://github.com/richblack/arcrun.git"
|
"url": "git+https://github.com/uncle6me-web/Arcrun.git"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import {
|
|||||||
type DeployContext,
|
type DeployContext,
|
||||||
} from '../lib/deploy.js';
|
} from '../lib/deploy.js';
|
||||||
import { API_RECIPE_SEEDS } from '../lib/api-recipe-seeds.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';
|
const ARCRUN_REGISTER_URL = 'https://cypher.arcrun.dev/register';
|
||||||
|
|
||||||
@@ -42,6 +43,14 @@ export async function cmdInit(options: { local?: boolean; selfHosted?: boolean }
|
|||||||
} finally {
|
} finally {
|
||||||
rl.close();
|
rl.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// init 末尾順便裝 CC harness 進當前專案(SDD user-cc-harness §2:init 裝 + 可單獨裝)。
|
||||||
|
// 失敗不影響 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> {
|
async function initLocal(): Promise<void> {
|
||||||
|
|||||||
@@ -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.md:append/取代 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 MCP(MCP 對齊中,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, '\\$&');
|
||||||
|
}
|
||||||
@@ -17,6 +17,7 @@ import { cmdRecipePush, cmdRecipeList, cmdRecipeDelete } from './commands/recipe
|
|||||||
import { cmdList } from './commands/list.js';
|
import { cmdList } from './commands/list.js';
|
||||||
import { cmdLogs } from './commands/logs.js';
|
import { cmdLogs } from './commands/logs.js';
|
||||||
import { cmdUpdate } from './commands/update.js';
|
import { cmdUpdate } from './commands/update.js';
|
||||||
|
import { cmdInstallHarness } from './commands/install-harness.js';
|
||||||
import { cmdAuthRecipeList, cmdAuthRecipeInfo, cmdAuthRecipeScaffold } from './commands/auth-recipe.js';
|
import { cmdAuthRecipeList, cmdAuthRecipeInfo, cmdAuthRecipeScaffold } from './commands/auth-recipe.js';
|
||||||
|
|
||||||
const program = new Command();
|
const program = new Command();
|
||||||
@@ -127,4 +128,10 @@ program
|
|||||||
.description('self-hosted:拉新 release 並重新部署到你的 Cloudflare')
|
.description('self-hosted:拉新 release 並重新部署到你的 Cloudflare')
|
||||||
.action(() => cmdUpdate());
|
.action(() => cmdUpdate());
|
||||||
|
|
||||||
|
// acr install-harness(把 arcrun 的 CC harness 裝進當前專案)
|
||||||
|
program
|
||||||
|
.command('install-harness')
|
||||||
|
.description('把 arcrun 的 CC harness(mindset/提醒/防做歪 hook/指令)裝進當前專案')
|
||||||
|
.action(() => cmdInstallHarness());
|
||||||
|
|
||||||
program.parse(process.argv);
|
program.parse(process.argv);
|
||||||
|
|||||||
@@ -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. 外部 URL(http(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 不 commit,rule 05)→
|
|
||||||
> `downloadAndDeploy()` 目前**誠實回 implemented:false 不假裝部署**(mindset §7)。建 KV/R2/seed/config
|
|
||||||
> 已可跑;release 產製管道補上後部署即自動化。定稿設計見 SDD `self-hosted-init.md`(含 §6 前置依賴)。
|
|
||||||
> **以下原始子步驟保留供對照**;KBDB recipe 採 Supabase 模式進 seed(richblack 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 #9:cypher-executor 走 `arcrun-{name}.{subdomain}.workers.dev` 對內 URL)。
|
|
||||||
- **seed 降級 recipe + auth recipe 進 RECIPES KV**:新帳號 KV 是空的。把 §2.1 那些 recipe(kbdb_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.ts,typecheck 過):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.ts(LEGAL_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 3:README 重寫成單一路徑 — harness「事前提醒」
|
|
||||||
|
|
||||||
**目標**:self-hosted 開源後,README 是外界 CC 唯一入口。砍掉「玩法一/二/三」三選一,講清楚單一正確路徑。
|
|
||||||
|
|
||||||
**子步驟**(改根 `README.md`):
|
|
||||||
1. 砍三選一玩法,留**一條路**:`acr init --self-hosted` → 寫 workflow(primitive 串 + 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/` 下禁止 TS;cypher-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 步驟 2(credential 注入鏈路)阻擋;Phase 3 降級假零件成 recipe 的前置。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 症狀
|
|
||||||
|
|
||||||
`acr recipe test kbdb` 端到端打不到 2xx。直接 probe `auth_static_key`:
|
|
||||||
|
|
||||||
```
|
|
||||||
POST https://auth-static-key.arcrun.dev/ {action:"authenticate", api_key:"ak_…", service:"kbdb"}
|
|
||||||
→ {"success":false, "error":"credential kbdb_api_key 解密失敗", ...}
|
|
||||||
```
|
|
||||||
|
|
||||||
前置都綠(排除誤判方向):
|
|
||||||
- `auth_recipe:kbdb` 存在、`primitive=static_key`(kv_get 命中 410 bytes)
|
|
||||||
- `kbdb_api_key` credential 存在 KV(kv_get 命中 108 bytes 的 `{encrypted, iv}`)
|
|
||||||
- 失敗精準落在「解密」這一步
|
|
||||||
|
|
||||||
## 定位(key-fingerprint 診斷,只印 SHA-256 前綴,不印 key/明文)
|
|
||||||
|
|
||||||
在 `aesGcmDecrypt`(`wasi-shim.ts`)暫加:
|
|
||||||
|
|
||||||
```
|
|
||||||
console.error(`[decrypt] ENCRYPTION_KEY sha256_prefix=${fpHex} keyLen=${len}`)
|
|
||||||
```
|
|
||||||
|
|
||||||
deploy auth-static-key + `wrangler tail` 抓到:
|
|
||||||
|
|
||||||
| 來源 | keyLen | sha256 前綴 | 格式 |
|
|
||||||
|---|---|---|---|
|
|
||||||
| 加密端(CLI `~/.arcrun/config.yaml` 的 `encryption_key`) | 64 | `fa84f2ce9027` | hex(→32 bytes)✓ |
|
|
||||||
| 解密端(`arcrun-auth-static-key` 的 `ENCRYPTION_KEY` secret) | **44** | **`ff219b123c89`** | base64 ✗ |
|
|
||||||
|
|
||||||
**兩個 mismatch 同時存在**:值不同 + 格式不同。`hexToUint8Array` 套在 44-char base64 上會解成垃圾 bytes,AES-GCM 必失敗。
|
|
||||||
|
|
||||||
漂移源頭:`arcrun/.env` 裡的 `ENCRYPTION_KEY` 就是那把錯的 base64(`ff219b123c89`),有人拿它去 `wrangler secret put` 設進 auth-static-key。
|
|
||||||
|
|
||||||
## 為什麼正本是 64-hex
|
|
||||||
|
|
||||||
`/register`(register.ts:42)把 `encryption_key: c.env.ENCRYPTION_KEY` 原樣回給用戶 —— 即 **cypher-executor 的** `ENCRYPTION_KEY`。用戶 config 是 64-hex(`fa84f2ce9027`),所以正本 = cypher-executor 那把 64-hex。CLI 加密 credential 也用這把。auth-static-key 必須跟它一致才能解開。
|
|
||||||
|
|
||||||
診斷用完即移除(`wasi-shim.ts` 還原,git diff 為空)。
|
|
||||||
|
|
||||||
## 附帶 bug:Body has already been used
|
|
||||||
|
|
||||||
修對 key 後,`/execute` 端到端從 500 變成「Node n1 failed: Body has already been used」。
|
|
||||||
|
|
||||||
`component-loader.ts` 的 `makeRecipeRunner` / `makeAuthRecipeRunner`:
|
|
||||||
|
|
||||||
```ts
|
|
||||||
const data = await res.json().catch(() => res.text()); // ✗ res.json() 失敗時 body 已消費
|
|
||||||
```
|
|
||||||
|
|
||||||
KBDB `/health` 回非 JSON(純文字)→ `res.json()` throw → `.catch(() => res.text())` 第二次讀 body → throw。
|
|
||||||
|
|
||||||
修法 — 讀一次:
|
|
||||||
|
|
||||||
```ts
|
|
||||||
async function readBodyOnce(res: Response): Promise<unknown> {
|
|
||||||
const text = await res.text();
|
|
||||||
try { return JSON.parse(text); } catch { return text; }
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## 修法步驟
|
|
||||||
|
|
||||||
1. `cd .component-builds/auth_static_key && wrangler secret put ENCRYPTION_KEY`,貼正本 64-hex(= `~/.arcrun/config.yaml` 的 `encryption_key`)。**richblack 手動**(rule 05:runtime secret 不進 CI、CC 不碰)。
|
|
||||||
2. `component-loader.ts` 加 `readBodyOnce()`,兩處 `res.json().catch(...)` 換掉。`tsc --noEmit` 綠,deploy cypher-executor。
|
|
||||||
3. 修正源頭文件 `arcrun/.env` 的 `ENCRYPTION_KEY` 改成 64-hex(避免下次再設錯)。
|
|
||||||
|
|
||||||
## 驗證證據
|
|
||||||
|
|
||||||
- 直接 probe auth-static-key:**HTTP 200**, `success:true`, 產出 `Authorization: Bearer …`
|
|
||||||
- 端到端 `/execute`:**HTTP 200**, trace 乾淨
|
|
||||||
- auth 確證:直接 curl KBDB `/blocks` 不帶 token → `401 {"error":"Missing token"}`;經 cypher-executor(注入 token)→ 過 auth,進 KBDB handler 回 ZodError(缺 `content`)。**無 401 = token 被接受**。
|
|
||||||
|
|
||||||
## 教訓
|
|
||||||
|
|
||||||
- **同一把 key 出現在 ≥2 個 Worker 的 secret = 漂移風險**。auth-static-key / auth_service_account / cypher-executor 都讀 `ENCRYPTION_KEY`,靠人各設一次必漂。長期應有單一發放來源或部署時自動同步。
|
|
||||||
- **debug 加密問題,先比 key 指紋(SHA-256 前綴),不要碰 key 明文**。一個 fingerprint log 就分辨出「值錯」vs「格式錯」vs「資料壞」。
|
|
||||||
- **`res.json().catch(() => res.text())` 是反模式** —— body 只能讀一次。永遠先 `res.text()` 再 `JSON.parse`。
|
|
||||||
@@ -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 param,Cloudflare 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)
|
|
||||||
├── 只取 token(escape 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 D1(API 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 Worker:API call 時寫入 D1
|
|
||||||
- [ ] 每小時聚合 Worker:D1 → 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-py:Python 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
|
|
||||||
選項 A:google-api-python-client(官方,但使用者要先去 GCP 設 OAuth)
|
|
||||||
選項 B:arcrun-auth(pip 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 C:arcrun 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 到 localhost,lib 接到 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,開源 SDK,hosted service 付費。SDK 下載數 = 付費轉換引流。
|
|
||||||
- **PostHog**:product analytics,同樣策略。
|
|
||||||
- **Supabase**:auth + db,開源 self-hosted,hosted 服務付費。
|
|
||||||
|
|
||||||
共同特徵:
|
|
||||||
1. SDK 免費下載、單機/本地能完整 work。
|
|
||||||
2. 從本地升級到 hosted 是「多人/多機/審計」的自然需求。
|
|
||||||
3. SDK 本身就是最強的 marketing(開發者口耳相傳 + 訓練資料擴散)。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 6. 為什麼 AI 會主動選 arcrun-auth(機率分析)
|
|
||||||
|
|
||||||
讓我給你一個分層的答案。
|
|
||||||
|
|
||||||
### 6.1 會主動選的場景(高機率)
|
|
||||||
|
|
||||||
| 場景 | 原因 |
|
|
||||||
|---|---|
|
|
||||||
| 使用者在 prompt 說「用最簡單的方式認證」 | AI 權衡 UX,arcrun-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:核心 lib(2-3 週)
|
|
||||||
|
|
||||||
- [ ] `arcrun-auth` PyPI 骨架(pyproject.toml + src layout)
|
|
||||||
- [ ] Recipe loader(從 GitHub 或平台 API 拉 YAML)
|
|
||||||
- [ ] `auth.bind(service_id, instance?)` → httpx Client
|
|
||||||
- [ ] Static key primitive(Notion / OpenAI / Stripe 當試金石)
|
|
||||||
- [ ] Keyring 整合 + 本地加密檔 fallback
|
|
||||||
- [ ] CLI:`arcrun setup <service>`, `arcrun list`, `arcrun test`
|
|
||||||
|
|
||||||
### Phase 2:OAuth2(2 週)
|
|
||||||
|
|
||||||
- [ ] OAuth2 primitive(authorization_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 自動化
|
|
||||||
- [ ] Recipe:Google Drive / Gmail / GitHub / Slack
|
|
||||||
|
|
||||||
### Phase 3:Service Account(1-2 週)
|
|
||||||
|
|
||||||
- [ ] Google Service Account(JWT signing)
|
|
||||||
- [ ] AWS SigV4
|
|
||||||
- [ ] Recipe 繼承(`extends: _google_base`)
|
|
||||||
|
|
||||||
### Phase 4:雲端同步(2 週)
|
|
||||||
|
|
||||||
- [ ] `arcrun login` → 綁定雲端帳號
|
|
||||||
- [ ] Secret sync 協議(本地加密後上傳,平台只存密文)
|
|
||||||
- [ ] 多機同步
|
|
||||||
- [ ] Audit log
|
|
||||||
|
|
||||||
### Phase 5:AI 生態整合(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 卡關,有 fallback:lib 自動引導使用者建自己的 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 Secret,lib 支援直接讀這些來源。
|
|
||||||
|
|
||||||
### 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,低階,還是要自己組。
|
|
||||||
- **各家 SDK(google-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-2(static 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
@@ -1,761 +0,0 @@
|
|||||||
# arcrun Credential System 設計規格
|
|
||||||
20260418
|
|
||||||
|
|
||||||
> **讀者**:Claude Code(CC),負責實作
|
|
||||||
> **作者**: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 body(JSON 欄位)
|
|
||||||
<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 grant(2026 已被多數 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 Role(assume 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 SigV4(kind: 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_case,e.g. "notion", "google_calendar"
|
|
||||||
version: integer # recipe schema version,breaking 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
|
|
||||||
|
|
||||||
// 檢查是否需要 refresh(oauth2 / 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 Store,tenant 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 state(primitive 自動管理)
|
|
||||||
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 **不**進 KBDB(KBDB 不該存敏感資料),只有 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 2:Static Key(1 週)
|
|
||||||
|
|
||||||
- [ ] **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 3:OAuth2(1-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 flow:startAuth → 跳轉 → callback → 寫 secret
|
|
||||||
|
|
||||||
### Phase 4:Service Account(1 週)
|
|
||||||
|
|
||||||
- [ ] **T4.1** `service_account.wasm` 實作(google_jwt)
|
|
||||||
- [ ] **T4.2** Google JWT signing(ES256 / 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 5:mTLS + 收尾(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 記錄到 KBDB(metadata,不含 secret)
|
|
||||||
- [ ] **T5.5** Docs:recipe 撰寫指南(讓社群能貢獻)
|
|
||||||
|
|
||||||
### Phase 6:Recipe 生成器(選配,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,存在 KV,callback 時比對 |
|
|
||||||
| **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` 欄位 semver,tenant secret 記錄 `recipe_version`,primitive 內處理遷移 |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 12. 對比 n8n(給內部 review / 行銷用)
|
|
||||||
|
|
||||||
| 維度 | n8n | arcrun |
|
|
||||||
|---|---|---|
|
|
||||||
| Credential types 數量 | 400+(一個服務一個) | 4(primitive) + 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 App(GitHub、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. 最小可行 recipe(OpenAI)
|
|
||||||
|
|
||||||
```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 recipe(Slack)
|
|
||||||
|
|
||||||
```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 recipe(Google 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 AI(auto-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 token(gmail.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 → 查 contract:credentials_required.inject_as = access_token
|
|
||||||
↓
|
|
||||||
去 CREDENTIALS_KV 讀 gmail_token → 解密
|
|
||||||
↓
|
|
||||||
注入到 WASM input:{ to, subject, body, access_token: "ya29..." }
|
|
||||||
↓
|
|
||||||
WASM 執行,用戶的 config 裡完全不出現 token
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 任務四:CLI 開發
|
|
||||||
|
|
||||||
### 技術選型
|
|
||||||
|
|
||||||
- **語言**:Node.js(TypeScript)
|
|
||||||
- **安裝**:`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_token(google_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. 建立新的獨立 repo:u6u-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_required(gmail, google_sheets, telegram, line_notify)
|
|
||||||
8. 補充 config_example(全部 21 個)
|
|
||||||
9. 驗證 main.go required 欄位與 contract 一致
|
|
||||||
```
|
|
||||||
|
|
||||||
### Phase 3:CLI(完成度補充後)
|
|
||||||
|
|
||||||
```
|
|
||||||
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 publish(u6u 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 + Vectorize(KBDB)
|
|
||||||
|
|
||||||
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
|
|
||||||
❌ 禁止打包 runtime(QuickJS、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 搜尋時讀到坑,強制繞道
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 十、現況與未來路徑
|
|
||||||
|
|
||||||
### 現在已有
|
|
||||||
|
|
||||||
- KBDB(blocks/templates/slots + Vectorize)
|
|
||||||
- IS-Squad MCP(execute_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 Model(preview2)**:目前用 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
Reference in New Issue
Block a user