docs: add SDD specs + user requirements + tests
- .agents/specs/: spec-driven-dev docs for arcrun MVP, auth-recipe, credential-primitives-wasm (active refactor), landing-page, sdk-and-website, u6u-core-mvp, u6u-platform-evolution. - .agents/steerings/tech.md: detailed tech stack rationale. - docs/user_requirements/: long-form requirements incl. credential primitives, pages spec, py strategy analysis. - tests/: end-to-end harness scaffolding. These are the durable context backing CLAUDE.md's SDD protocol. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,118 @@
|
||||
# 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 |
|
||||
|
||||
**目前狀況**:P0 全部完成。Google Sheets 實際寫入可由封測者用真實 token 驗證,不阻塞啟動。
|
||||
|
||||
---
|
||||
|
||||
## 四、封測前 P3(啟動當天)
|
||||
|
||||
- [ ] 用封測者 email 呼叫 `/register`,取得 api_key
|
||||
- [ ] 將 ARCRUN_ENCRYPTION_KEY 以安全方式提供給封測者
|
||||
- [ ] 確認聯絡管道
|
||||
|
||||
---
|
||||
|
||||
## 五、已知限制(封測期間不修)
|
||||
|
||||
1. `if_control` false branch 不路由(條件 false 時後續節點不執行)
|
||||
2. 多節點 context 不自動解包(上游輸出 flat merge,下游需從 `data.result` 取值)
|
||||
3. 用戶自製邏輯零件(Phase 5)封測後才實作
|
||||
|
||||
---
|
||||
|
||||
## 六、實作進度
|
||||
|
||||
| Phase | 內容 | 狀態 |
|
||||
|-------|------|------|
|
||||
| 0 | Workers 部署、CI/CD、DNS | ✅ |
|
||||
| 1 | CLI 基礎(init / validate / run / parts) | ✅ |
|
||||
| 2 | /register、/cypher/execute、21 個零件 | ✅ |
|
||||
| 3 | Service Binding 架構、{{variable}} 插值、ON_FAIL 修正 | ✅ |
|
||||
| 4 | 動態 Recipe KV(CRUD)、acr recipe 指令 | ✅ |
|
||||
| 5 | 用戶自製邏輯零件(WASM push) | ⏸ 封測後 |
|
||||
| 6 | Credential 多租戶({api_key}:cred:{name})、acr creds push | ✅ |
|
||||
| 7 | acr parts 內建清單、acr parts scaffold | ✅ |
|
||||
| 8 | /webhooks/named、acr push 改版、config 套入 graph | ✅ |
|
||||
|
||||
### CLI 版本
|
||||
|
||||
| 版本 | 變更 |
|
||||
|------|------|
|
||||
| 1.1.0 | auth recipe 系統:20 個服務預建(Notion/Slack/GitHub/OpenAI/Google SA 等);acr auth-recipe 指令 |
|
||||
| 1.0.9 | /register 回傳 encryption_key;acr init 自動儲存;creds push 不需手動設環境變數 |
|
||||
| 1.0.8 | acr push → webhooks/named;config 套入 graph;acr parts 內建清單 |
|
||||
| 1.0.7 | acr creds push → POST /credentials |
|
||||
| 1.0.6 | acr recipe push / list / delete |
|
||||
| 1.0.5 | hello.yaml 改 string_ops,--version 修正 |
|
||||
| 1.0.4 | config/context 分離 |
|
||||
| 1.0.3 | 初始發布 |
|
||||
@@ -0,0 +1,254 @@
|
||||
# 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>;
|
||||
};
|
||||
|
||||
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 |
|
||||
|
||||
`makeAuthRecipeRunner` 在發出 fetch 前讀取這三個欄位,之後從 body 中剔除(不迴傳給下游)。
|
||||
|
||||
---
|
||||
|
||||
## 七、向後相容
|
||||
|
||||
- 現有 `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 邏輯
|
||||
@@ -0,0 +1,178 @@
|
||||
# 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 內完成
|
||||
@@ -0,0 +1,165 @@
|
||||
# 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 各自)
|
||||
- [ ] 6.6 驗證:`workflow_dispatch` + `force_all=true` 手動跑一次,24 個 Worker(tier1 21 個 + tier2 3 個)全綠(待 richblack 手動觸發)
|
||||
- [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
|
||||
@@ -0,0 +1,325 @@
|
||||
# arcrun.dev Landing Page — SDD
|
||||
|
||||
> **目標**:給工程師一個門面,可以取得 API Key、管理 Key、探索 API(Swagger),同時藉此獲得會員 Email。
|
||||
> **原則**:先快速可用,不追求功能完整。榮譽牆、Python Lib 是後期。
|
||||
|
||||
---
|
||||
|
||||
## 0. 範圍(這份 SDD 涵蓋)
|
||||
|
||||
| 功能 | 說明 |
|
||||
|---|---|
|
||||
| 首頁 Hero | 說明 arcrun 是什麼,CTA 取得 API Key |
|
||||
| OAuth 登入 | Google / GitHub(用自己的 auth recipe — dogfooding) |
|
||||
| API Key 管理 | 查看、Rotate、Revoke |
|
||||
| Swagger UI | 嵌入 `/api`,讓工程師直接試打 |
|
||||
| 榮譽牆 `/integrations` | 靜態骨架,先列 20 個 recipe,無動態數字 |
|
||||
| 中英切換 | `?lang=zh` |
|
||||
|
||||
**不在本次範圍**:Python lib、Donate 整合、Social Proof 即時數字、貢獻者排行。
|
||||
|
||||
---
|
||||
|
||||
## 1. 技術選型
|
||||
|
||||
### 1.1 框架:Next.js(App Router)
|
||||
|
||||
**選 Next.js 而非 Astro 的原因**:
|
||||
- `finally-click` 已有完整 Next.js + OAuth 回調實作,可直接複用模式
|
||||
- API Key 管理頁有登入態保護需求,Next.js 的 middleware 最直接
|
||||
- Cloudflare Pages 支援 Next.js(`@cloudflare/next-on-pages`)
|
||||
- Astro 在動態路由保護上摩擦較多
|
||||
|
||||
### 1.2 部署:Cloudflare Pages
|
||||
|
||||
```
|
||||
arcrun.dev → Cloudflare Pages(Next.js)
|
||||
API calls → cypher.arcrun.dev(現有 Worker)
|
||||
```
|
||||
|
||||
### 1.3 儲存:現有 cypher-executor CREDENTIALS_KV + 新增 USERS_KV
|
||||
|
||||
現有 cypher-executor Worker 已有:
|
||||
- `CREDENTIALS_KV`:`{api_key}:cred:{name}` 存 tenant credentials
|
||||
- `RECIPES`:auth recipes
|
||||
|
||||
新增需求:
|
||||
- **USERS_KV**:存 user 帳號,key = `user:{provider}:{provider_user_id}`
|
||||
- value: `{ email, display_name, api_key, created_at, provider }`
|
||||
- **SESSIONS_KV**:存 login session,key = `sess:{session_id}`
|
||||
- value: `{ api_key, email, expires_at }`
|
||||
- TTL = 7 天
|
||||
|
||||
兩個 KV 都加到 cypher-executor `wrangler.toml`。
|
||||
|
||||
### 1.4 OAuth — Dogfooding 自己的 Auth Recipe
|
||||
|
||||
登入用 arcrun 自己的 auth recipe:
|
||||
- 不是用 arcrun auth recipe 的 `http_request` runner 去打第三方
|
||||
- 而是 **直接複用 recipe YAML 裡定義的 OAuth App 設定(client_id/secret)**
|
||||
- Worker 端實作 standard OAuth2 authorization_code flow
|
||||
|
||||
**支援提供商(MVP)**:
|
||||
- Google(google_drive recipe 的 OAuth App,或另建 arcrun-login Google App)
|
||||
- GitHub(github recipe 的 OAuth App)
|
||||
|
||||
登入 OAuth App 與 auth recipe 的 OAuth App **可以是同一個**(只要 scopes 包含 `openid profile email`),但更乾淨的做法是登入用獨立的 Google/GitHub App(只要 email scope),auth recipe 用的是資源存取 App。
|
||||
|
||||
**決策:登入用獨立 OAuth App**
|
||||
- `GOOGLE_CLIENT_ID`、`GOOGLE_CLIENT_SECRET` — 只申請 `openid profile email`
|
||||
- `GITHUB_CLIENT_ID`、`GITHUB_CLIENT_SECRET` — 只申請 `read:user` + `user:email`
|
||||
- 以 Worker Secret 方式存入 cypher-executor
|
||||
|
||||
---
|
||||
|
||||
## 2. 頁面結構
|
||||
|
||||
```
|
||||
arcrun.dev/
|
||||
├── / 首頁(Hero + Code snippet + CTA)
|
||||
├── /login 登入頁(Google / GitHub 按鈕)
|
||||
├── /auth/callback OAuth callback(Pages Function)
|
||||
├── /dashboard API Key 管理(需登入)
|
||||
├── /api Swagger UI(嵌入 swagger.json)
|
||||
└── /integrations 服務目錄(靜態,20 個 recipe)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. 登入 / OAuth 流程
|
||||
|
||||
### 3.1 流程圖
|
||||
|
||||
```
|
||||
用戶點「Google 登入」
|
||||
→ GET /auth/google/start(Worker 端)
|
||||
→ redirect 到 Google OAuth(state = random, 存 SESSIONS_KV sess:state:{state} = {provider, redirect_back})
|
||||
→ 用戶同意
|
||||
→ GET /auth/callback?code=...&state=...(Worker 端)
|
||||
→ 驗 state
|
||||
→ 用 code 換 access_token(POST google token endpoint)
|
||||
→ 用 token 取 userinfo(GET google userinfo)
|
||||
→ upsert USERS_KV user:{provider}:{provider_id} = {email, display_name, api_key, ...}
|
||||
→ 若新用戶:呼叫現有 /register?email=... 取得 arcrun API Key
|
||||
→ 建立 session:SESSIONS_KV sess:{session_id} = {api_key, email, ...},TTL=7d
|
||||
→ Set-Cookie: arcrun_session={session_id}; HttpOnly; Secure; SameSite=Lax
|
||||
→ redirect 到 /dashboard
|
||||
```
|
||||
|
||||
### 3.2 「若新用戶取得 API Key」的邏輯
|
||||
|
||||
現有 `/register` endpoint 接受 `email` 回傳 `api_key`(HMAC 確定性)。
|
||||
但 landing page 需要的是**真正綁定到用戶帳號的 key**,且用戶可以 rotate/revoke。
|
||||
|
||||
**方案:延伸現有 register endpoint**
|
||||
|
||||
`/register` 目前:`HMAC(secret, email)` → 確定性 api_key,存 CREDENTIALS_KV
|
||||
|
||||
新增邏輯:
|
||||
1. 若 USERS_KV 已有此 user → 直接用記錄裡的 api_key
|
||||
2. 若新 user → 呼叫現有 `/register`(保持 HMAC 確定性邏輯)→ 拿到 api_key → 存入 USERS_KV
|
||||
|
||||
**好處**:不破壞現有 register 邏輯;登入後的 dashboard 顯示的 key = 現有 key = 封測用的 key。
|
||||
|
||||
### 3.3 Rotate / Revoke
|
||||
|
||||
- **Rotate**:產生新 UUID v4 key → 更新 USERS_KV 記錄 → 舊 key 失效(透過把新 key 加到 CREDENTIALS_KV,舊 key 的資料都跟著 API Key 命名空間走,所以 credentials 會留在舊 namespace)
|
||||
- 簡化版:Rotate 後顯示提示「您的 workflow credentials 已和舊 Key 分離,請重新設定」
|
||||
- **Revoke**:USERS_KV 記錄 `revoked: true` → Worker middleware 拒絕此 key
|
||||
|
||||
---
|
||||
|
||||
## 4. API 端點(新增到 cypher-executor)
|
||||
|
||||
```
|
||||
GET /auth/google/start → redirect 到 Google OAuth
|
||||
GET /auth/github/start → redirect 到 GitHub OAuth
|
||||
GET /auth/callback?code=&state= → 換 token、建立 session
|
||||
POST /auth/logout → 清 session cookie
|
||||
GET /me → 回傳當前登入用戶資訊(需 session cookie 或 API Key)
|
||||
PUT /me/api-key/rotate → 產生新 key
|
||||
DELETE /me/api-key → Revoke(標記撤銷)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. 前端頁面設計
|
||||
|
||||
### 5.1 首頁(/)
|
||||
|
||||
```
|
||||
Hero:
|
||||
Stop fighting OAuth.
|
||||
One API key. Every service. Works anywhere.
|
||||
|
||||
[Get API Key — Free] [View on GitHub]
|
||||
|
||||
Before/After:
|
||||
40 行 OAuth 程式碼 → auth.bind("google_drive")
|
||||
|
||||
Code Demo(三個 tab):
|
||||
Python / JavaScript / HTTP(n8n 用戶)
|
||||
|
||||
[Get Free API Key] 按鈕
|
||||
```
|
||||
|
||||
### 5.2 登入頁(/login)
|
||||
|
||||
```
|
||||
arcrun
|
||||
|
||||
登入或建立帳號
|
||||
|
||||
[Continue with Google]
|
||||
[Continue with GitHub]
|
||||
|
||||
不需要信用卡。API Key 立即可用。
|
||||
```
|
||||
|
||||
### 5.3 Dashboard(/dashboard)
|
||||
|
||||
```
|
||||
歡迎,{display_name}
|
||||
|
||||
您的 API Key
|
||||
┌────────────────────────────────┐
|
||||
│ ak_xxxxxxxxxxxxxxxxxxxx [複製] │
|
||||
└────────────────────────────────┘
|
||||
|
||||
[Rotate Key] [Revoke Key]
|
||||
|
||||
使用說明:
|
||||
Authorization: Bearer {key}
|
||||
或 X-Arcrun-API-Key: {key}
|
||||
|
||||
[登出]
|
||||
```
|
||||
|
||||
### 5.4 Swagger UI(/api)
|
||||
|
||||
- 嵌入 `<SwaggerUIBundle>` JS(CDN)
|
||||
- `url: 'https://cypher.arcrun.dev/swagger.json'`(現有 Worker 已有 `/docs` openapi endpoint)
|
||||
- 頂部說明:「這是 arcrun 的原始 API。Python / JS lib 是它的包裝,任何能發 HTTP request 的工具都能直接用。」
|
||||
|
||||
### 5.5 服務目錄(/integrations)
|
||||
|
||||
- 靜態列出 20 個 auth recipe(從 seed data 產生)
|
||||
- 每個 recipe:名稱、認證方式(static_key / service_account)、所需 credentials
|
||||
- 「找不到你要的服務?開 PR 貢獻 Recipe」CTA
|
||||
|
||||
---
|
||||
|
||||
## 6. 檔案結構
|
||||
|
||||
```
|
||||
arcrun/landing/ ← 新 Next.js 專案
|
||||
├── app/
|
||||
│ ├── layout.tsx
|
||||
│ ├── page.tsx 首頁
|
||||
│ ├── login/
|
||||
│ │ └── page.tsx
|
||||
│ ├── dashboard/
|
||||
│ │ ├── page.tsx
|
||||
│ │ └── middleware.ts (或 root middleware)
|
||||
│ ├── api-docs/
|
||||
│ │ └── page.tsx Swagger UI
|
||||
│ └── integrations/
|
||||
│ └── page.tsx
|
||||
├── middleware.ts 保護 /dashboard(讀 cookie)
|
||||
├── lib/
|
||||
│ └── auth.ts session helpers
|
||||
├── public/
|
||||
├── next.config.ts
|
||||
├── package.json
|
||||
└── wrangler.toml CF Pages 設定
|
||||
```
|
||||
|
||||
cypher-executor 新增:
|
||||
```
|
||||
arcrun/cypher-executor/src/routes/
|
||||
├── auth.ts ← 新增(OAuth start/callback/logout/me)
|
||||
```
|
||||
|
||||
cypher-executor wrangler.toml 新增:
|
||||
```toml
|
||||
[[kv_namespaces]]
|
||||
binding = "USERS_KV"
|
||||
id = "<to be created>"
|
||||
|
||||
[[kv_namespaces]]
|
||||
binding = "SESSIONS_KV"
|
||||
id = "455d0505c7534883a4d4985ab8295857"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. 環境變數 / Secrets
|
||||
|
||||
### cypher-executor(Worker Secrets)
|
||||
|
||||
```
|
||||
GOOGLE_CLIENT_ID Google OAuth App client_id(僅 openid profile email scope)
|
||||
GOOGLE_CLIENT_SECRET Google OAuth App client_secret
|
||||
GITHUB_CLIENT_ID GitHub OAuth App client_id(read:user + user:email scope)
|
||||
GITHUB_CLIENT_SECRET GitHub OAuth App client_secret
|
||||
SESSION_SECRET 隨機 32 bytes,用於 HMAC session ID(或直接用 UUID)
|
||||
```
|
||||
|
||||
### landing(Pages Environment Variables)
|
||||
|
||||
```
|
||||
NEXT_PUBLIC_API_BASE https://cypher.arcrun.dev
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 8. 實作步驟(Checklist)
|
||||
|
||||
### Phase 1:cypher-executor 後端擴充
|
||||
|
||||
- [x] `wrangler kv:namespace create USERS_KV` → 填入 wrangler.toml (id: 25bef01d079148919578894434d58c4d)
|
||||
- [x] `wrangler kv:namespace create SESSIONS_KV` → 填入 wrangler.toml (id: 455d0505c7534883a4d4985ab8295857)
|
||||
- [x] 建立 `arcrun/cypher-executor/src/routes/auth.ts`
|
||||
- [x] GET `/auth/google/start`
|
||||
- [x] GET `/auth/github/start`
|
||||
- [x] GET `/auth/callback`(換 token → userinfo → upsert USERS_KV → 建 session → Set-Cookie → redirect)
|
||||
- [x] POST `/auth/logout`
|
||||
- [x] GET `/me`(讀 session cookie 或 API Key header)
|
||||
- [x] PUT `/me/api-key/rotate`
|
||||
- [x] DELETE `/me/api-key`(revoke)
|
||||
- [x] 在 `src/index.ts` 掛載 `authRouter`
|
||||
- [ ] `wrangler secret put GOOGLE_CLIENT_ID` 等 4 個 secrets ← **用戶需自建 Google/GitHub OAuth App**
|
||||
- [x] `wrangler deploy` ← 已部署(Worker version 7877857b)
|
||||
|
||||
### Phase 2:Next.js Landing 專案
|
||||
|
||||
- [x] `npx create-next-app@latest arcrun/landing --typescript --tailwind --app`
|
||||
- [x] 設定 `@cloudflare/next-on-pages`(Next.js 15 + .npmrc legacy-peer-deps)
|
||||
- [ ] 建立 `middleware.ts`(保護 /dashboard,讀 `arcrun_session` cookie)← 待做
|
||||
- [x] 首頁(`app/page.tsx`):Hero + Code Demo tab + CTA
|
||||
- [x] 登入頁(`app/login/page.tsx`):Google / GitHub 按鈕(href 到 cypher.arcrun.dev/auth/google/start)
|
||||
- [x] Dashboard(`app/dashboard/page.tsx`):顯示 API Key,Rotate / Revoke 按鈕
|
||||
- [x] Swagger UI(`app/api-docs/page.tsx`):client component,動態 import swagger-ui CDN
|
||||
- [x] 服務目錄(`app/integrations/page.tsx`):靜態,列 20 個 recipe
|
||||
- [ ] 中英切換 ← 低優先,可延後
|
||||
- [x] `wrangler pages deploy` → https://42a8d302.arcrun-landing.pages.dev
|
||||
- [ ] Cloudflare dashboard 設定 arcrun.dev custom domain → arcrun-landing Pages project
|
||||
|
||||
### Phase 3:驗收(待 OAuth Secrets 填入後)
|
||||
|
||||
- [ ] Google / GitHub OAuth 完整流程(登入 → dashboard → 看到 key)
|
||||
- [ ] Rotate:新 key 出現
|
||||
- [ ] Revoke:舊 key 的 API 呼叫回傳 401
|
||||
- [ ] Swagger UI 正常載入,可試打 `/health`
|
||||
- [x] `/integrations` 正確列出 20 個服務
|
||||
|
||||
---
|
||||
|
||||
## 9. 待決事項(開始實作前確認)
|
||||
|
||||
| 問題 | 預設決策 |
|
||||
|---|---|
|
||||
| Google OAuth App 是否要另建(只有 email scope)? | 是,另建;auth recipe 的 App 不動 |
|
||||
| Rotate 後舊 credentials 是否遷移? | 不遷移,顯示提示 |
|
||||
| Domain arcrun.dev 是否已購入且在 Cloudflare? | 假設是(wrangler.toml 有設 zone_name) |
|
||||
| 登入後 redirect 預設到 /dashboard | 是,可從 `?redirect=` 覆寫 |
|
||||
@@ -0,0 +1,281 @@
|
||||
# Design Document: arcrun SDK Libraries + Website
|
||||
|
||||
## Overview
|
||||
|
||||
本設計涵蓋 arcrun 的三個新增交付物:
|
||||
1. Python SDK lib(`pip install arcrun`)
|
||||
2. JS/TS SDK lib(`npm install arcrun` 或 `@arcrun/sdk`)
|
||||
3. arcrun.dev 網站完善(零件列表、recipe 列表、登入管理)
|
||||
|
||||
**設計原則:修改不重建。** SDK 是 `cypher.arcrun.dev` HTTP API 的 thin wrapper。不在 client 端重新實作任何 server 端已有的邏輯(workflow 執行、credential 注入、auth recipe 解析)。唯一在 client 做的是 AES-GCM 加密(因為 server 的 POST /credentials 期望收到加密後的 payload)。
|
||||
|
||||
---
|
||||
|
||||
## Architecture
|
||||
|
||||
### 系統關係圖
|
||||
|
||||
```
|
||||
使用者程式碼
|
||||
├── CLI(acr) → cypher.arcrun.dev(HTTP API)
|
||||
├── Python SDK(arcrun) → cypher.arcrun.dev(HTTP API)
|
||||
└── JS SDK(arcrun / @arcrun/sdk) → cypher.arcrun.dev(HTTP API)
|
||||
|
||||
arcrun.dev 網站(Next.js / Cloudflare Pages)
|
||||
├── /login → /auth/google/start, /auth/github/start(cypher.arcrun.dev)
|
||||
├── /dashboard → /me, /me/api-key/rotate(cypher.arcrun.dev)
|
||||
├── /integrations → /auth-recipes(cypher.arcrun.dev)
|
||||
└── /components → /recipes + 靜態零件清單(embedded)
|
||||
|
||||
cypher.arcrun.dev(Cloudflare Worker — cypher-executor,不改)
|
||||
├── POST /credentials ← 接收 { name, encrypted, iv }
|
||||
├── GET /credentials ← 列出 credential 名稱
|
||||
├── DELETE /credentials/:name ← 刪除 credential
|
||||
├── GET /auth-recipes ← 列出 20 個 auth recipe
|
||||
├── GET /auth-recipes/:service ← 單一 recipe 詳情
|
||||
├── POST /webhooks/named ← 部署 workflow
|
||||
├── POST /webhooks/named/:name/trigger ← 觸發 workflow
|
||||
├── GET /webhooks/named ← 列出 workflow
|
||||
├── POST /register ← 註冊取得 API Key
|
||||
├── GET /me ← 當前用戶資訊
|
||||
└── /auth/* ← OAuth 流程
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Python SDK(`arcrun/python-sdk/`)
|
||||
|
||||
### 目錄結構
|
||||
|
||||
```
|
||||
arcrun/python-sdk/
|
||||
├── pyproject.toml ← hatchling build, name="arcrun", deps=[httpx>=0.27, cryptography>=42]
|
||||
├── README.md
|
||||
└── arcrun/
|
||||
├── __init__.py ← from .client import Arcrun
|
||||
├── client.py ← Arcrun class(主入口)
|
||||
├── crypto.py ← AES-GCM 加密(client 端,用 cryptography 套件)
|
||||
├── creds.py ← CredentialsClient(push/list/delete)
|
||||
├── auth.py ← AuthClient(setup/bind/get_token/list_services)
|
||||
└── workflows.py ← WorkflowClient(run/push/list/delete)
|
||||
```
|
||||
|
||||
### API 設計
|
||||
|
||||
```python
|
||||
from arcrun import Arcrun
|
||||
|
||||
# 建構 — api_key 從參數 > 環境變數 > ~/.arcrun/config.yaml 自動取得
|
||||
client = Arcrun()
|
||||
# 或明確指定
|
||||
client = Arcrun(api_key="ak_xxx", encryption_key="hexstring")
|
||||
|
||||
# Auth:設定並綁定服務
|
||||
client.auth.setup("openai", api_key="sk-xxx") # 加密 + 上傳
|
||||
openai_client = client.auth.bind("openai") # 取回 pre-auth client
|
||||
response = openai_client.get("/models") # httpx.Client
|
||||
token = client.auth.get_token("openai") # raw token string
|
||||
services = client.auth.list_services() # [{ service, display_name, ... }]
|
||||
|
||||
# Credentials:低階操作
|
||||
client.creds.push("my_token", "value123")
|
||||
names = client.creds.list()
|
||||
client.creds.delete("my_token")
|
||||
|
||||
# Workflows
|
||||
result = client.workflows.run("my-flow", {"email": "user@example.com"})
|
||||
url = client.workflows.push("my-flow", graph_dict)
|
||||
workflows = client.workflows.list()
|
||||
```
|
||||
|
||||
### Credential 加密流程
|
||||
|
||||
```
|
||||
setup("openai", api_key="sk-xxx")
|
||||
1. GET /auth-recipes/openai → recipe(含 required_secrets, inject)
|
||||
2. 對應 required_secrets[0].key = "openai_api_key"
|
||||
3. crypto.py 用 encryption_key AES-GCM 加密 "sk-xxx"
|
||||
4. POST /credentials → { name: "openai_api_key", encrypted, iv }
|
||||
5. 本地 _cred_cache["openai_api_key"] = "sk-xxx"(供 bind() 用)
|
||||
|
||||
bind("openai")
|
||||
1. GET /auth-recipes/openai → recipe.inject.header = { Authorization: "Bearer {{secret.openai_api_key}}" }
|
||||
2. 用 _cred_cache["openai_api_key"] 替換 template → "Bearer sk-xxx"
|
||||
3. 回傳 AuthenticatedClient(base_url="https://api.openai.com/v1", headers={"Authorization": "Bearer sk-xxx"})
|
||||
```
|
||||
|
||||
**注意**:`bind()` 依賴 `setup()` 在同一 session 建立的 `_cred_cache`。跨 session 使用時(credential 已上傳但 cache 不存在),`bind()` 無法解析 template — 此時 `get_token()` 也無法返回值。**這是已知限制,封測期間先接受。** 長期解法是 server 提供 `/credentials/:name/secret` 解密端點(u6u-core/credentials 已有)。
|
||||
|
||||
### 關鍵差異:crypto.py 的定位
|
||||
|
||||
`crypto.py` 只做 **加密**(encrypt),不做解密。
|
||||
功能等同 `u6u-core/credentials/src/actions/crypto.ts` 的 `encrypt()` 函數。
|
||||
解密只在 server 端發生(cypher-executor 的 `credential-injector.ts` 或 `u6u-core/credentials/getCredentialSecret.ts`)。
|
||||
|
||||
---
|
||||
|
||||
## JS/TS SDK(`arcrun/js-sdk/`)
|
||||
|
||||
### 目錄結構
|
||||
|
||||
```
|
||||
arcrun/js-sdk/
|
||||
├── package.json ← name TBD(arcrun vs @arcrun/sdk),tsup build
|
||||
├── tsconfig.json ← ES2020, NodeNext
|
||||
└── src/
|
||||
├── index.ts ← export class Arcrun
|
||||
├── crypto.ts ← Web Crypto API AES-GCM encrypt(client 端)
|
||||
├── creds.ts ← CredentialsClient(push/list/delete)
|
||||
├── auth.ts ← AuthClient(setup/bind/getToken/listServices)
|
||||
└── workflows.ts ← WorkflowClient(run/push/list/delete)
|
||||
```
|
||||
|
||||
### API 與 Python SDK 對等
|
||||
|
||||
```typescript
|
||||
import { Arcrun } from 'arcrun' // or '@arcrun/sdk'
|
||||
|
||||
const client = new Arcrun() // reads ARCRUN_API_KEY from env
|
||||
|
||||
await client.auth.setup('openai', { api_key: 'sk-xxx' })
|
||||
const oai = await client.auth.bind('openai')
|
||||
const models = await (await oai.get('/models')).json()
|
||||
|
||||
const token = await client.auth.getToken('openai')
|
||||
const services = await client.auth.listServices()
|
||||
|
||||
await client.creds.push('my_token', 'value')
|
||||
const names = await client.creds.list()
|
||||
|
||||
const result = await client.workflows.run('my-flow', { email: 'user@example.com' })
|
||||
```
|
||||
|
||||
### Build 產物
|
||||
|
||||
```
|
||||
dist/
|
||||
├── index.js ← ESM
|
||||
├── index.cjs ← CJS
|
||||
├── index.d.ts ← TypeScript 型別
|
||||
└── index.d.cts
|
||||
```
|
||||
|
||||
### Crypto 實作
|
||||
|
||||
使用 Web Crypto API(`crypto.subtle`),相容 Node 18+ / browsers / CF Workers / Deno:
|
||||
|
||||
```typescript
|
||||
async function encrypt(plaintext: string, hexKey: string): Promise<{ encrypted: string; iv: string }> {
|
||||
const key = await crypto.subtle.importKey('raw', hexToBytes(hexKey), { name: 'AES-GCM' }, false, ['encrypt']);
|
||||
const iv = crypto.getRandomValues(new Uint8Array(12));
|
||||
const ciphertext = await crypto.subtle.encrypt({ name: 'AES-GCM', iv }, key, new TextEncoder().encode(plaintext));
|
||||
return { encrypted: toBase64(ciphertext), iv: toBase64(iv.buffer) };
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## arcrun.dev 網站
|
||||
|
||||
### 現有狀態(`arcrun/landing/`)
|
||||
|
||||
已完成:
|
||||
- [x] `/` — Hero + Code Demo(Python/JS/HTTP tabs)
|
||||
- [x] `/login` — Google + GitHub OAuth 按鈕(前端 OK,需設 OAuth secrets)
|
||||
- [x] `/dashboard` — API Key 查看/Copy/Rotate/Revoke(依賴 `/me` API)
|
||||
- [x] `/integrations` — 20 個 recipe 靜態卡片
|
||||
- [x] `/api-docs` — Swagger UI CDN 嵌入
|
||||
- [x] `middleware.ts` — 保護 `/dashboard`(未登入 → `/login`)
|
||||
- [x] Cloudflare Pages 部署
|
||||
|
||||
待完成:
|
||||
- [ ] OAuth secrets 設定(`GOOGLE_CLIENT_ID` / `GOOGLE_CLIENT_SECRET` / `GITHUB_CLIENT_ID` / `GITHUB_CLIENT_SECRET`)
|
||||
- [ ] `/components` 頁面(零件列表 — 21 個 WASM 零件的 input/output/config_example)
|
||||
- [ ] 首頁 code demo 更新為三種使用方式(CLI / Python / JS)
|
||||
- [ ] 登入流程真實驗證
|
||||
|
||||
### 新增頁面:`/components`
|
||||
|
||||
```
|
||||
/components
|
||||
├── 零件卡片(21 個)
|
||||
│ ├── canonical_id
|
||||
│ ├── display_name
|
||||
│ ├── description
|
||||
│ ├── input_schema(required / optional 欄位)
|
||||
│ ├── output_schema
|
||||
│ ├── credentials_required(if any)
|
||||
│ └── config_example(YAML code block)
|
||||
└── 分類篩選(邏輯 / API / 控制流)
|
||||
```
|
||||
|
||||
資料來源:靜態嵌入(從 `registry/components/*/component.contract.yaml` 在 build 時讀取),不依賴 runtime API。
|
||||
|
||||
### OAuth 設定(待 richblack 操作)
|
||||
|
||||
需要在 Cloudflare Worker 設定以下 secrets:
|
||||
|
||||
```bash
|
||||
wrangler secret put GOOGLE_CLIENT_ID --name arcrun-cypher-executor
|
||||
wrangler secret put GOOGLE_CLIENT_SECRET --name arcrun-cypher-executor
|
||||
wrangler secret put GITHUB_CLIENT_ID --name arcrun-cypher-executor
|
||||
wrangler secret put GITHUB_CLIENT_SECRET --name arcrun-cypher-executor
|
||||
wrangler secret put SESSION_SIGNING_SECRET --name arcrun-cypher-executor
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## server 端需要的修改
|
||||
|
||||
### cypher-executor 修改(最小化)
|
||||
|
||||
目前 `POST /credentials` 端點(`routes/credentials.ts`)接收 `{ name, encrypted, iv }` 後直接存 KV。
|
||||
|
||||
SDK 需要的改動:
|
||||
|
||||
1. **`GET /auth-recipes` 回應格式**:目前 list 端點回 `{ recipes: [...] }` 但 recipe 的 `service` 欄位是 key — SDK 已在 list_services() 正確處理 ✅
|
||||
|
||||
2. **`GET /auth-recipes/:service` 回應格式**:目前回 `{ success: true, recipe: {...} }` — SDK 需讀 `body.recipe` 而非 body 本身 ✅
|
||||
|
||||
3. **`POST /credentials` 不需改動** — SDK 自己做 AES-GCM 加密後送 `{ name, encrypted, iv }` ✅
|
||||
|
||||
4. **未來**:新增 `GET /credentials/:name/secret` 端點(解密返回 plaintext),讓跨 session 的 `bind()` 能工作。但此端點在 `u6u-core/credentials/src/actions/getCredentialSecret.ts` 已有實作 — 需要在 cypher-executor 整合或 Service Binding 到 u6u-credentials Worker。**封測後再做。**
|
||||
|
||||
---
|
||||
|
||||
## 不做的事(明確排除)
|
||||
|
||||
- ❌ 不在 SDK 裡做 workflow 解析或 YAML 處理 — 那是 CLI 的職責
|
||||
- ❌ 不在 SDK 裡做 server-side 解密 — 解密只在 server 端
|
||||
- ❌ 不建新的 credentials Worker — 用現有的
|
||||
- ❌ 不建新的 KV namespace — 用現有的 CREDENTIALS_KV
|
||||
- ❌ 不改 cypher-executor 的 credential-injector.ts — 那已經完成且測試通過
|
||||
|
||||
---
|
||||
|
||||
## 實作順序
|
||||
|
||||
```
|
||||
Phase 1:Python SDK 重建 + 測試
|
||||
1.1 重建 arcrun/python-sdk/(按本 SDD 的結構)
|
||||
1.2 修正上次的 bug:recipe 回應 wrapper、inject key "header" vs "headers"、secret key mapping
|
||||
1.3 對 cypher.arcrun.dev live 測試全部 API
|
||||
1.4 本地安裝測試(pip install -e .)
|
||||
|
||||
Phase 2:JS SDK 重建 + 測試
|
||||
2.1 重建 arcrun/js-sdk/(按本 SDD 的結構)
|
||||
2.2 同步修正 Python SDK 發現的所有 recipe 格式問題
|
||||
2.3 build(tsup)+ 本地測試
|
||||
|
||||
Phase 3:arcrun.dev 網站補完
|
||||
3.1 新增 /components 頁面
|
||||
3.2 更新首頁 code demo(三種使用方式)
|
||||
3.3 OAuth secrets 設定(需 richblack 操作 GCP / GitHub)
|
||||
3.4 登入流程驗證
|
||||
|
||||
Phase 4:GitHub README + 發布
|
||||
4.1 更新 arcrun/README.md — 三種 Quick Start
|
||||
4.2 pip publish(arcrun)
|
||||
4.3 npm publish(TBD 套件名)
|
||||
4.4 最終驗證:從零開始 pip install / npm install / 打 API
|
||||
```
|
||||
@@ -0,0 +1,131 @@
|
||||
# Requirements: arcrun SDK Libraries + Website
|
||||
|
||||
## Introduction
|
||||
|
||||
arcrun 目前有三個使用介面:
|
||||
1. **CLI**(`acr` 指令)— 已完成,用 YAML 定義 workflow 並推送執行
|
||||
2. **Python / JS SDK lib**(本次新增)— `pip install arcrun` / `npm install arcrun`,讓開發者在寫程式時直接用 arcrun 功能
|
||||
3. **arcrun.dev 網站**(本次完成)— 登入取得 API Key、管理 Key、瀏覽零件 / recipe 列表
|
||||
|
||||
**核心原則**:SDK lib 是 `cypher.arcrun.dev` HTTP API 的 thin wrapper。所有業務邏輯(加解密、credential 注入、workflow 執行)都在 server 端完成。Client 端不重做 server 已有的邏輯。
|
||||
|
||||
**現有基礎設施**(不重建,直接使用):
|
||||
- `cypher.arcrun.dev`:cypher-executor Worker(workflow 執行、credential 管理、auth recipe、webhook)
|
||||
- `u6u-core/credentials`:credential Worker(AES-GCM 加解密)— arcrun/credentials 是其 cherry-pick
|
||||
- `arcrun/cli`:CLI 工具(已發布 npm `arcrun@1.1.0`)
|
||||
- `arcrun/landing`:Next.js 前端(已部署 Cloudflare Pages,有 hero/login/dashboard/integrations 骨架)
|
||||
|
||||
---
|
||||
|
||||
## Glossary
|
||||
|
||||
- **SDK lib**:Python / JS 套件,wrapping `cypher.arcrun.dev` HTTP API,安裝後可在程式碼中直接使用
|
||||
- **auth.setup()**:上傳一個服務的 credential(如 Notion token、OpenAI API Key)到 arcrun
|
||||
- **auth.bind()**:取回已設定服務的 pre-authenticated HTTP client
|
||||
- **auth.get_token()**:取回某服務的 raw token(escape hatch,給官方 SDK 用)
|
||||
- **workflows.run()**:觸發已部署的 workflow
|
||||
- **workflows.push()**:上傳 workflow 定義
|
||||
- **Recipe**:描述「如何對某服務認證」的 YAML 設定,存在 RECIPES KV
|
||||
|
||||
---
|
||||
|
||||
## Requirements
|
||||
|
||||
### Requirement 1:Python SDK(`pip install arcrun`)
|
||||
|
||||
**User Story:** As a Python 開發者, I want `pip install arcrun` 後在程式碼中使用 arcrun, so that 不用離開寫程式環境就能串接 20+ 服務。
|
||||
|
||||
#### Acceptance Criteria
|
||||
|
||||
1. THE Python SDK SHALL 以 `arcrun` 套件名發布到 PyPI,支援 Python 3.10+。
|
||||
2. THE SDK SHALL 提供以下 API:
|
||||
- `Arcrun(api_key=, base_url=)` — 建構 client,api_key 支援從環境變數 `ARCRUN_API_KEY` 或 `~/.arcrun/config.yaml` 自動讀取
|
||||
- `client.health()` — 健康檢查
|
||||
- `client.auth.list_services()` — 列出可用 auth recipe 服務
|
||||
- `client.auth.setup(service, **kwargs)` — 上傳 credential
|
||||
- `client.auth.bind(service)` — 取得 pre-authenticated HTTP client
|
||||
- `client.auth.get_token(service)` — 取得 raw token
|
||||
- `client.creds.push(name, value)` — 上傳加密 credential
|
||||
- `client.creds.list()` — 列出 credential 名稱
|
||||
- `client.creds.delete(name)` — 刪除 credential
|
||||
- `client.workflows.run(name, input)` — 觸發 workflow
|
||||
- `client.workflows.push(name, graph)` — 上傳 workflow
|
||||
- `client.workflows.list()` — 列出已部署 workflow
|
||||
3. THE SDK 的 credential 加密 SHALL 在 client 端完成(使用 `cryptography` 套件 AES-GCM),然後以 `POST /credentials` 上傳加密後的 `{ name, encrypted, iv }` 到 server。
|
||||
4. THE `auth.bind()` SHALL 從 server 取得 auth recipe 的 inject template,在 client 端用 cache 的 plaintext 值填入,回傳 pre-configured `httpx.Client`。
|
||||
5. THE SDK SHALL 使用 `httpx` 做 HTTP client(async 版使用 `httpx.AsyncClient`)。
|
||||
6. THE SDK 位置 SHALL 為 `arcrun/python-sdk/`,build 系統用 `hatchling`(`pyproject.toml`)。
|
||||
|
||||
---
|
||||
|
||||
### Requirement 2:JavaScript/TypeScript SDK(`npm install arcrun`)
|
||||
|
||||
**User Story:** As a JS/TS 開發者, I want `npm install arcrun` 後在程式碼中使用 arcrun, so that 可以嵌入現有 Node.js / Deno / Cloudflare Workers 專案。
|
||||
|
||||
#### Acceptance Criteria
|
||||
|
||||
1. THE JS SDK SHALL 以 `arcrun` 套件名發布到 npm,提供 ESM + CJS 雙格式 + TypeScript 型別定義。
|
||||
2. THE SDK SHALL 提供與 Python SDK 對等的 API(camelCase 版):
|
||||
- `new Arcrun({ apiKey?, baseUrl? })` — 讀 `process.env.ARCRUN_API_KEY`
|
||||
- `client.health()` — 回傳 `Promise<unknown>`
|
||||
- `client.auth.listServices()` / `setup()` / `bind()` / `getToken()`
|
||||
- `client.creds.push()` / `list()` / `delete()`
|
||||
- `client.workflows.run()` / `push()` / `list()` / `delete()`
|
||||
3. THE SDK 的 credential 加密 SHALL 使用 Web Crypto API(`crypto.subtle` AES-GCM),相容 Node 18+、browsers、Cloudflare Workers、Deno。
|
||||
4. THE `auth.bind()` SHALL 回傳一個有 `get/post/put/delete/patch` 方法的 `AuthenticatedClient`,base URL + auth headers 已配置。
|
||||
5. THE SDK SHALL 使用原生 `fetch()` API,不依賴外部 HTTP client 套件。
|
||||
6. THE SDK 位置 SHALL 為 `arcrun/js-sdk/`,build 用 `tsup`(ESM + CJS + DTS),`tsconfig.json` target ES2020 + NodeNext module。
|
||||
7. THE JS SDK 套件名與 CLI 套件名衝突(都叫 `arcrun`),SHALL 使用 `@arcrun/sdk` 或由 richblack 決定套件名。
|
||||
|
||||
---
|
||||
|
||||
### Requirement 3:arcrun.dev 網站完成
|
||||
|
||||
**User Story:** As a 潛在用戶, I want 在 arcrun.dev 上登入取得 API Key、瀏覽零件和 recipe 列表, so that 我可以評估 arcrun 是否符合需求並立即開始使用。
|
||||
|
||||
#### Acceptance Criteria
|
||||
|
||||
1. THE 網站 SHALL 在 `arcrun.dev` 提供以下頁面:
|
||||
- `/` — 首頁 Hero + 三種使用方式(CLI / Python / JS)
|
||||
- `/login` — Google + GitHub OAuth 登入
|
||||
- `/dashboard` — 登入後顯示 API Key(查看/Copy/Rotate/Revoke)
|
||||
- `/integrations` — 列出 20 個 auth recipe 服務,可按分類篩選
|
||||
- `/components` — 列出所有零件(21 個 WASM 零件),顯示 input/output schema、config_example
|
||||
- `/api-docs` — Swagger UI,可直接試打 API
|
||||
2. THE 登入 SHALL 使用 Google + GitHub OAuth,流程走 `cypher.arcrun.dev` 的 `/auth/*` 端點。
|
||||
3. THE 登入後 SHALL 自動對該 email 呼叫 `/register` 取得 API Key(若已有則取回現有 key)。
|
||||
4. THE `/dashboard` SHALL 允許 Rotate(生成新 key)、Revoke(標記失效)、Copy to clipboard。
|
||||
5. THE 網站 SHALL 部署在 Cloudflare Pages(現有 `arcrun/landing`),使用 Next.js App Router。
|
||||
6. THE 首頁 code demo 區 SHALL 包含三個 tab:Python、JavaScript、HTTP/curl,展示三種使用方式。
|
||||
|
||||
---
|
||||
|
||||
### Requirement 4:GitHub README 更新
|
||||
|
||||
**User Story:** As a GitHub 訪客, I want README 清楚說明三種使用方式, so that 我能選擇最適合的方式開始用 arcrun。
|
||||
|
||||
#### Acceptance Criteria
|
||||
|
||||
1. THE `arcrun/README.md` SHALL 包含三種 Quick Start:
|
||||
- **CLI**:`npm i -g arcrun && acr init && acr push workflow.yaml && acr run`
|
||||
- **Python**:`pip install arcrun && from arcrun import Arcrun && ...`
|
||||
- **JavaScript**:`npm install arcrun && import { Arcrun } from 'arcrun' && ...`
|
||||
2. THE README SHALL 包含完整零件列表(21 個)和 auth recipe 列表(20 個服務)。
|
||||
3. THE README SHALL 連結到 `arcrun.dev`(取得 API Key)和 Swagger UI(API 文件)。
|
||||
|
||||
---
|
||||
|
||||
### Requirement 5:SDK 發布
|
||||
|
||||
**User Story:** As a SDK 使用者, I want 公開安裝並直接使用, so that 不需要從原始碼 build。
|
||||
|
||||
#### Acceptance Criteria
|
||||
|
||||
1. THE Python SDK SHALL 發布到 PyPI,`pip install arcrun` 可安裝。
|
||||
2. THE JS SDK SHALL 發布到 npm,`npm install arcrun`(或 `@arcrun/sdk`)可安裝。
|
||||
3. THE 發布前 SHALL 完成以下測試(對 `cypher.arcrun.dev` live API):
|
||||
- `health()` ✅
|
||||
- `auth.list_services()` ✅
|
||||
- `auth.setup()` + `auth.bind()` ✅(至少一個 static_key 服務如 openai)
|
||||
- `creds.push()` + `creds.list()` ✅
|
||||
- `workflows.list()` ✅
|
||||
@@ -0,0 +1,120 @@
|
||||
# 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
|
||||
|
||||
---
|
||||
|
||||
## Notes
|
||||
|
||||
- JS SDK 套件名需 richblack 決定(`arcrun` 已被 CLI 佔用 → 可能用 `@arcrun/sdk`)
|
||||
- OAuth secrets 設定需 richblack 手動操作(GCP Console + GitHub Settings)
|
||||
- `bind()` 跨 session 限制是已知的,封測期間先接受
|
||||
- credential 加密用的 `encryption_key` 目前由 `/register` 回傳,`acr init` 自動存入 config
|
||||
@@ -0,0 +1,611 @@
|
||||
# Design Document: arcrun MVP
|
||||
|
||||
## Overview
|
||||
|
||||
arcrun MVP 的核心設計原則是**最小異動、最快可用**。所有目標都能透過以下三個操作達成:
|
||||
1. **Cherry-pick**:從 `matrix` 搬移指定目錄,不重寫
|
||||
2. **Carve-out**:移除 cypher-executor 中與 InkStone 耦合的程式碼路徑
|
||||
3. **Supplement**:補充 contract.yaml 缺少的欄位、新增 CLI
|
||||
|
||||
不建立新的抽象層,不改變現有零件邏輯,只做讓開源可用所需的最小改動。
|
||||
|
||||
---
|
||||
|
||||
## Architecture
|
||||
|
||||
### 目標 Repo 結構
|
||||
|
||||
```
|
||||
arcrun/(新獨立開源 repo)
|
||||
├── README.md
|
||||
├── CONTRIBUTING.md
|
||||
├── cypher-executor/
|
||||
│ ├── src/
|
||||
│ │ ├── index.ts
|
||||
│ │ ├── types.ts
|
||||
│ │ ├── graph-executor.ts
|
||||
│ │ ├── lib/
|
||||
│ │ │ ├── component-loader.ts ← 改:只從 WASM_BUCKET 讀,移除 KBDB/REGISTRY 邏輯
|
||||
│ │ │ ├── component-dispatcher.ts
|
||||
│ │ │ ├── wasm-executor.ts
|
||||
│ │ │ ├── wasi-shim.ts
|
||||
│ │ │ └── constants.ts ← 改:移除 MINI_ME / KBDB 特殊零件 hardcode
|
||||
│ │ └── actions/
|
||||
│ │ ├── triplet-parser.ts
|
||||
│ │ ├── graph-builder.ts
|
||||
│ │ ├── execution-evaluator.ts
|
||||
│ │ ├── execution-logger.ts
|
||||
│ │ ├── webhook-handlers.ts
|
||||
│ │ ├── webhook-graph-resolver.ts ← 改:加入 credential 注入邏輯
|
||||
│ │ └── (移除 autoPublishMissing.ts)
|
||||
│ └── wrangler.toml ← 改:移除 9 個 InkStone bindings,新增 CREDENTIALS_KV
|
||||
├── credentials/ ← 直接搬移,無需修改
|
||||
│ └── src/...
|
||||
├── builtins/ ← 直接搬移,無需修改
|
||||
│ └── src/...
|
||||
└── registry/
|
||||
└── components/ ← 搬移後補充 contract.yaml
|
||||
├── gmail/
|
||||
├── google_sheets/
|
||||
├── telegram/
|
||||
├── line_notify/
|
||||
├── ... (其餘 17 個零件)
|
||||
└── cli/ ← 新增:arcrun CLI
|
||||
├── package.json
|
||||
├── tsconfig.json
|
||||
└── src/
|
||||
├── index.ts
|
||||
├── commands/
|
||||
│ ├── init.ts
|
||||
│ ├── creds.ts
|
||||
│ ├── push.ts
|
||||
│ ├── run.ts
|
||||
│ ├── validate.ts
|
||||
│ ├── parts.ts
|
||||
│ ├── list.ts
|
||||
│ └── logs.ts
|
||||
└── lib/
|
||||
├── config.ts # 讀寫 ~/.arcrun/config.yaml
|
||||
├── cf-api.ts # Cloudflare KV / R2 HTTP API wrapper
|
||||
└── yaml-parser.ts # workflow.yaml 解析與三元組轉換
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Component Loader 改造(關鍵變更)
|
||||
|
||||
### 現況(matrix 版)
|
||||
|
||||
```typescript
|
||||
// component-loader.ts 現有四層優先序:
|
||||
// 1. 特殊零件 hardcode → MINI_ME / KBDB Service Binding
|
||||
// 2. 內建零件 Map → 本地純轉換
|
||||
// 3. 新版:查詢 KBDB record 含 component_type → WASM 或 Service Binding
|
||||
// 4. 舊版 fallback:查詢無 component_type 的 KBDB record
|
||||
```
|
||||
|
||||
### 開源版(arcrun)
|
||||
|
||||
```typescript
|
||||
// component-loader.ts 簡化為三層:
|
||||
// 1. 內建零件 Map → 本地純轉換(passthrough / counter 等,保留)
|
||||
// 2. WASM_BUCKET R2 直讀 → component-name.wasm
|
||||
// 3. 找不到 → 回傳結構化錯誤
|
||||
|
||||
async function loadComponent(componentId: string, env: Env) {
|
||||
// 層 1:內建零件(無需 R2)
|
||||
if (BUILTIN_COMPONENTS.has(componentId)) {
|
||||
return BUILTIN_COMPONENTS.get(componentId)
|
||||
}
|
||||
|
||||
// 層 2:從 WASM_BUCKET R2 讀取
|
||||
const wasmKey = `${componentId}/${componentId}.wasm`
|
||||
const wasmObj = await env.WASM_BUCKET.get(wasmKey)
|
||||
if (wasmObj) {
|
||||
return { type: 'wasm', buffer: await wasmObj.arrayBuffer() }
|
||||
}
|
||||
|
||||
// 層 3:找不到
|
||||
throw new Error(`Component not found: ${componentId}. 請確認 ${wasmKey} 存在於 WASM_BUCKET。`)
|
||||
}
|
||||
```
|
||||
|
||||
移除:`MINI_ME`、`KBDB` 特殊零件的 hardcode 路徑(`comp_claude_chat`、`comp_kbdb_search`、`comp_kbdb_history`)。
|
||||
|
||||
---
|
||||
|
||||
## Credential 注入流程設計
|
||||
|
||||
### 執行時序
|
||||
|
||||
```
|
||||
acr run newsletter_subscribe
|
||||
↓
|
||||
cypher-executor POST /webhook/:id
|
||||
↓
|
||||
webhook-graph-resolver 讀 WEBHOOKS KV → workflow 定義
|
||||
↓
|
||||
graph-executor 執行節點 send_thanks
|
||||
↓
|
||||
執行前:credential-injector(新增)
|
||||
查 send_thanks 對應零件 canonical_id = "gmail"
|
||||
讀 registry/components/gmail/component.contract.yaml
|
||||
發現 credentials_required: [{key: "gmail_token", inject_as: "access_token"}]
|
||||
GET CREDENTIALS_KV["gmail_token"] → AES-GCM 解密
|
||||
input.access_token = decryptedToken
|
||||
↓
|
||||
wasm-executor 執行 gmail.wasm(stdin = 含 access_token 的完整 input)
|
||||
↓
|
||||
回傳結果
|
||||
```
|
||||
|
||||
### credential-injector 實作位置
|
||||
|
||||
放在 `cypher-executor/src/actions/credential-injector.ts`(新增),由 `graph-executor.ts` 在每個節點執行前呼叫。
|
||||
|
||||
```typescript
|
||||
async function injectCredentials(
|
||||
componentId: string,
|
||||
input: Record<string, unknown>,
|
||||
env: Env
|
||||
): Promise<Record<string, unknown>> {
|
||||
const contract = await loadContract(componentId) // 從 WASM_BUCKET 或本地讀取
|
||||
if (!contract.credentials_required) return input
|
||||
|
||||
const enriched = { ...input }
|
||||
for (const cred of contract.credentials_required) {
|
||||
const record = await env.CREDENTIALS_KV.get(cred.key)
|
||||
if (!record) {
|
||||
throw new Error(
|
||||
`缺少 credential: ${cred.key}\n修復:在 credentials.yaml 加入 ${cred.key} 後執行 acr creds push`
|
||||
)
|
||||
}
|
||||
const { encrypted, iv } = JSON.parse(record)
|
||||
enriched[cred.inject_as] = await decrypt(encrypted, iv, env.ENCRYPTION_KEY)
|
||||
}
|
||||
return enriched
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## workflow.yaml 解析設計
|
||||
|
||||
### CLI push 流程
|
||||
|
||||
```
|
||||
acr push newsletter_subscribe.yaml
|
||||
↓
|
||||
yaml-parser.ts 讀取 workflow.yaml
|
||||
↓
|
||||
解析 flow[] 三元組 → triplets: [{subject, relation, object}]
|
||||
驗證關係詞(拒絕 PIPE)
|
||||
↓
|
||||
POST cypher-executor /cypher/search → ExecutionGraph(節點 + 邊)
|
||||
↓
|
||||
合併 config: + ExecutionGraph → WorkflowDefinition
|
||||
↓
|
||||
PUT WEBHOOKS KV[workflow_name] = JSON.stringify(WorkflowDefinition)
|
||||
↓
|
||||
輸出 webhook URL
|
||||
```
|
||||
|
||||
### workflow.yaml 格式(確認版)
|
||||
|
||||
```yaml
|
||||
name: newsletter_subscribe
|
||||
description: 訂閱電子報,發感謝信並記錄到 GSheets
|
||||
|
||||
flow:
|
||||
- "input >> 完成後 >> send_thanks"
|
||||
- "input >> 完成後 >> save_to_sheet"
|
||||
- "send_thanks >> 完成後 >> output"
|
||||
- "send_thanks >> 失敗時 >> notify_error"
|
||||
- "save_to_sheet >> 完成後 >> output"
|
||||
|
||||
config:
|
||||
send_thanks:
|
||||
to: "{{input.email}}"
|
||||
subject: "感謝訂閱!"
|
||||
body: "歡迎加入!"
|
||||
# access_token 由 credentials.yaml 的 gmail_token 自動注入
|
||||
|
||||
save_to_sheet:
|
||||
action: write
|
||||
spreadsheet_id: "{{creds.sheet_id}}"
|
||||
range: "訂閱者!A:B"
|
||||
values: [["{{input.email}}", "{{input.timestamp}}"]]
|
||||
|
||||
notify_error:
|
||||
chat_id: "{{creds.telegram_chat_id}}"
|
||||
text: "發信失敗:{{input.email}}"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## contract.yaml 補充欄位格式
|
||||
|
||||
### credentials_required(gmail 範例)
|
||||
|
||||
```yaml
|
||||
credentials_required:
|
||||
- key: gmail_token
|
||||
type: google_oauth
|
||||
description: "Google OAuth access token(gmail.send scope)"
|
||||
inject_as: access_token
|
||||
```
|
||||
|
||||
### config_example(gmail 範例)
|
||||
|
||||
```yaml
|
||||
config_example: |
|
||||
send_email: # 節點名稱(可自訂)
|
||||
to: "" # 收件人 Email(必填)
|
||||
subject: "" # 主旨(必填)
|
||||
body: "" # 內文(必填)
|
||||
# access_token 由 credentials.yaml 的 gmail_token 自動注入
|
||||
```
|
||||
|
||||
### 各零件 credentials_required 對照表
|
||||
|
||||
| 零件 | key | type | inject_as |
|
||||
|------|-----|------|-----------|
|
||||
| gmail | gmail_token | google_oauth | access_token |
|
||||
| google_sheets | google_oauth | google_oauth | access_token |
|
||||
| telegram | telegram_bot_token | telegram_bot_token | bot_token |
|
||||
| line_notify | line_token | line_token | token |
|
||||
|
||||
---
|
||||
|
||||
## CLI 技術設計
|
||||
|
||||
### 依賴
|
||||
|
||||
```json
|
||||
{
|
||||
"dependencies": {
|
||||
"commander": "^12.0.0",
|
||||
"js-yaml": "^4.1.0",
|
||||
"chalk": "^5.3.0",
|
||||
"ora": "^8.0.1"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### config.yaml 格式(~/.arcrun/config.yaml)
|
||||
|
||||
```yaml
|
||||
cloudflare_account_id: abc123
|
||||
webhooks_kv_id: xyz789
|
||||
credentials_kv_id: abc456
|
||||
wasm_bucket: arcrun-wasm
|
||||
cypher_executor_url: https://cypher-executor.xxx.workers.dev
|
||||
credentials_worker_url: https://arcrun-credentials.xxx.workers.dev
|
||||
api_token: ***(加密存本機)
|
||||
```
|
||||
|
||||
### Cloudflare API 操作
|
||||
|
||||
CLI 使用 Cloudflare REST API(不依賴 Wrangler CLI):
|
||||
- KV 寫入:`PUT /client/v4/accounts/{id}/storage/kv/namespaces/{ns_id}/values/{key}`
|
||||
- KV 讀取:`GET /client/v4/accounts/{id}/storage/kv/namespaces/{ns_id}/values/{key}`
|
||||
- KV 列出:`GET /client/v4/accounts/{id}/storage/kv/namespaces/{ns_id}/keys`
|
||||
|
||||
---
|
||||
|
||||
## wrangler.toml 變更對照
|
||||
|
||||
### 移除(InkStone 專屬)
|
||||
|
||||
```toml
|
||||
# 全部移除:
|
||||
[[services]]
|
||||
binding = "KBDB"
|
||||
service = "inkstone-kbdb-api"
|
||||
|
||||
[[services]]
|
||||
binding = "REGISTRY"
|
||||
service = "inkstone-component-registry"
|
||||
|
||||
[[services]]
|
||||
binding = "CLINIC_GDRIVE"
|
||||
service = "clinic-gdrive"
|
||||
|
||||
# ... CLINIC_EXCEL, CLINIC_ANALYSIS, CLINIC_RENDER, CLINIC_GSHEETS
|
||||
|
||||
[[services]]
|
||||
binding = "AICEO"
|
||||
service = "inkstone-aiceo-bot"
|
||||
|
||||
[[services]]
|
||||
binding = "MINI_ME"
|
||||
service = "inkstone-mini-me"
|
||||
```
|
||||
|
||||
### 保留
|
||||
|
||||
```toml
|
||||
[[kv_namespaces]]
|
||||
binding = "EXEC_CONTEXT"
|
||||
|
||||
[[kv_namespaces]]
|
||||
binding = "WEBHOOKS"
|
||||
|
||||
[[r2_buckets]]
|
||||
binding = "WASM_BUCKET"
|
||||
|
||||
[ai]
|
||||
binding = "AI"
|
||||
```
|
||||
|
||||
### 新增
|
||||
|
||||
```toml
|
||||
[[kv_namespaces]]
|
||||
binding = "CREDENTIALS_KV"
|
||||
id = "" # 用戶自行填入
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Standard 模式架構(用戶自己的 KV,arcrun.dev 的引擎)
|
||||
|
||||
### 儲存責任分界
|
||||
|
||||
```
|
||||
arcrun.dev 負責:
|
||||
WASM_BUCKET 公眾零件庫(.wasm 二進位)
|
||||
ANALYTICS_KV 零件執行統計
|
||||
ACCOUNTS_KV API Key → tenant_id + CF API Token 對應
|
||||
SUBMISSIONS_KV 零件提交審核狀態
|
||||
|
||||
用戶自己負責(一個 CF KV,arcrun.dev 不存取明文):
|
||||
USER_KV
|
||||
workflow:{name} → workflow 執行圖(JSON)
|
||||
cred:{key} → AES-GCM 加密 credential
|
||||
```
|
||||
|
||||
### 完整系統圖
|
||||
|
||||
```
|
||||
┌──────────────────────────────────────────────────────────────┐
|
||||
│ arcrun.dev(你的 Cloudflare 帳號) │
|
||||
│ │
|
||||
│ auth-worker(api.arcrun.dev) │
|
||||
│ POST /register → { api_key, tenant_id } │
|
||||
│ ACCOUNTS_KV: { tenant_id, cf_api_token, api_key_hash } │
|
||||
│ ※ 不儲存用戶 credential 或 workflow 內容 │
|
||||
│ │
|
||||
│ cypher-executor(cypher.arcrun.dev,共享) │
|
||||
│ X-Arcrun-API-Key → tenant_id → cf_api_token │
|
||||
│ 用 cf_api_token 呼叫 CF KV API → 讀用戶自己的 USER_KV │
|
||||
│ WASM_BUCKET: gmail.wasm / telegram.wasm / ...(共享) │
|
||||
│ │
|
||||
│ public registry(registry.arcrun.dev) │
|
||||
│ GET /components → 零件清單 + 統計 + author + visibility │
|
||||
│ POST /submit → 接收零件,沙盒驗收後設 author_only │
|
||||
│ POST /analytics/record → 執行統計(非同步) │
|
||||
└──────────────────────────────────────────────────────────────┘
|
||||
|
||||
↕ CF KV API(用戶的 cf_api_token,KV Edit 權限)
|
||||
|
||||
┌──────────────────────────────────────────────────────────────┐
|
||||
│ 用戶自己的 CF 帳號 │
|
||||
│ USER_KV │
|
||||
│ workflow:newsletter → { triplets, config } │
|
||||
│ cred:gmail_token → { encrypted, iv } │
|
||||
│ cred:telegram_bot → { encrypted, iv } │
|
||||
└──────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### acr init 互動流程
|
||||
|
||||
```
|
||||
$ acr init
|
||||
|
||||
? 你的 Cloudflare Account ID: abc123
|
||||
? USER_KV Namespace ID(在 CF Dashboard 建立一個 KV 後貼上): kv_xyz
|
||||
? CF API Token(只需 KV Edit 權限,arcrun 用此存取你的 KV): ***
|
||||
? Email(取得 arcrun.dev API Key): you@example.com
|
||||
|
||||
→ 呼叫 POST https://api.arcrun.dev/register { email, cf_api_token_hash }
|
||||
→ 取得 api_key: ak_xxxxxxxx
|
||||
|
||||
✓ 設定完成 → ~/.arcrun/config.yaml
|
||||
✓ 建立 credentials.yaml(已加入 .gitignore)
|
||||
|
||||
你的 credential 與 workflow 存在你自己的 CF KV,arcrun 不會儲存它們。
|
||||
```
|
||||
|
||||
### API Key 驗證與 KV 存取 Middleware
|
||||
|
||||
```typescript
|
||||
// cypher-executor/src/lib/tenant.ts(新增)
|
||||
export async function resolveUserKv(request: Request, env: Env) {
|
||||
if (env.MULTI_TENANT === 'false') {
|
||||
// Self-hosted:直接用本地 KV binding
|
||||
return { kv: env.LOCAL_KV, prefix: '' }
|
||||
}
|
||||
|
||||
const apiKey = request.headers.get('X-Arcrun-API-Key')
|
||||
if (!apiKey) throw new Response('Missing API Key', { status: 401 })
|
||||
|
||||
const hash = await sha256(apiKey)
|
||||
const account = await env.ACCOUNTS_KV.get(`hash:${hash}`)
|
||||
if (!account) throw new Response('Invalid API Key', { status: 401 })
|
||||
|
||||
const { cf_api_token, account_id, kv_namespace_id } = JSON.parse(account)
|
||||
|
||||
// 回傳 CF KV API wrapper,用用戶自己的 token 存取
|
||||
return {
|
||||
kv: new CfKvClient({ cf_api_token, account_id, kv_namespace_id }),
|
||||
prefix: ''
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### USER_KV Key Schema
|
||||
|
||||
```
|
||||
Standard 模式(用戶自己的 KV):
|
||||
workflow:{name} → WorkflowDefinition JSON
|
||||
cred:{key} → { encrypted (base64), iv (base64) }
|
||||
|
||||
Self-hosted(本地 KV binding):
|
||||
維持現有 key 格式,無 prefix
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 執行統計設計
|
||||
|
||||
### Analytics Record(非同步,不阻擋執行)
|
||||
|
||||
```typescript
|
||||
// cypher-executor/src/actions/analytics.ts(新增)
|
||||
export function recordExecution(
|
||||
componentId: string,
|
||||
version: string,
|
||||
success: boolean,
|
||||
durationMs: number
|
||||
): void {
|
||||
// fire-and-forget,不 await
|
||||
fetch('https://registry.arcrun.dev/analytics/record', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ canonical_id: componentId, version, success, duration_ms: durationMs })
|
||||
}).catch(() => {}) // 統計失敗不影響執行
|
||||
}
|
||||
```
|
||||
|
||||
### 統計聚合(registry Worker)
|
||||
|
||||
```
|
||||
ANALYTICS_KV 結構:
|
||||
"stats:gmail:v1" → { total_runs: 140382, success_runs: 139444, total_ms: 16845840 }
|
||||
|
||||
每次 POST /analytics/record:
|
||||
原子更新(KV 樂觀鎖)→ total_runs++, success_runs += success, total_ms += duration_ms
|
||||
|
||||
GET /components 回傳:
|
||||
success_rate = success_runs / total_runs * 100
|
||||
avg_duration_ms = total_ms / total_runs
|
||||
排序:total_runs × success_rate(DESC)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 零件貢獻流程設計
|
||||
|
||||
### `acr parts publish` 流程
|
||||
|
||||
```
|
||||
$ acr parts publish gmail-v2
|
||||
|
||||
1. CLI 讀取 registry/components/gmail-v2/ 目錄
|
||||
- component.contract.yaml(必須有 author 欄位)
|
||||
- main.go
|
||||
- gmail-v2.wasm
|
||||
|
||||
2. POST https://registry.arcrun.dev/submit
|
||||
multipart form:
|
||||
contract: <yaml content>
|
||||
source: <main.go content>
|
||||
wasm: <binary>
|
||||
Header: X-Arcrun-API-Key: ak_xxxxx
|
||||
|
||||
3. registry 回應:
|
||||
{ submission_id: "sub_abc123", status: "pending_review" }
|
||||
|
||||
4. CLI 輸出:
|
||||
✓ 提交成功(submission_id: sub_abc123)
|
||||
查詢進度:acr parts publish --status sub_abc123
|
||||
```
|
||||
|
||||
### Registry 沙盒驗收與 visibility 狀態機
|
||||
|
||||
```
|
||||
POST /submit 觸發(非同步執行):
|
||||
|
||||
[整合類零件:gmail、telegram、google_sheets、line_notify、http_request]
|
||||
Step 1: 體積檢查(< 2048KB)
|
||||
Step 2: syscall 掃描(無 filesystem syscall;網路 syscall 允許,因需呼叫外部 API)
|
||||
通過 → visibility: author_only(作者立即可用,等人工審核)
|
||||
|
||||
[功能類零件:所有其他零件]
|
||||
Step 1: 體積檢查(< 2048KB)
|
||||
Step 2: 冷啟動時間(< 50ms)
|
||||
Step 3: syscall 掃描(無網路 / 無 filesystem)
|
||||
Step 4: Gherkin 測試(contract 中所有 scenario 100% 通過)
|
||||
通過 → visibility: author_only(作者立即可用,等人工審核)
|
||||
|
||||
任一步驟失敗 → status: rejected(回傳 failed_step + reason)
|
||||
|
||||
人工審核通過 → visibility: public
|
||||
- 零件出現在所有人的 GET /components
|
||||
- 開始累積公開執行統計
|
||||
|
||||
人工審核拒絕 → visibility 維持 author_only
|
||||
- 作者仍可使用,但收到拒絕原因
|
||||
- 作者修改後可重新提交
|
||||
|
||||
acr parts 顯示規則:
|
||||
visibility: author_only → [待審核] 只有你可用(不顯示統計)
|
||||
visibility: public → ★ 成功率 | N 次執行 | by @author
|
||||
|
||||
任一失敗 → status: rejected
|
||||
- 回傳 { failed_step, reason },格式與 Requirement 2 相同
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 開發順序(Phase 對齊 requirements)
|
||||
|
||||
```
|
||||
Phase 1:搬移與清理(Requirement 1)
|
||||
1.1 建立 arcrun repo,搬移四個目錄
|
||||
1.2 清理 cypher-executor/wrangler.toml
|
||||
1.3 改寫 component-loader(移除 KBDB/REGISTRY/MINI_ME 路徑)
|
||||
1.4 移除 autoPublishMissing.ts(依賴 REGISTRY binding)
|
||||
1.5 本機 wrangler dev 測試 /health
|
||||
|
||||
Phase 2:零件完整度(Requirement 2)
|
||||
2.1 審查 21 個零件 contract.yaml(表格回報)
|
||||
2.2 補充 credentials_required(4 個零件)
|
||||
2.3 補充 config_example(全部 21 個)
|
||||
2.4 驗證 main.go required 與 contract 一致
|
||||
|
||||
Phase 3:credential 注入(Requirement 3)
|
||||
3.1 新增 credential-injector.ts
|
||||
3.2 整合進 graph-executor 節點執行前
|
||||
3.3 測試 gmail 零件端對端(credentials.yaml → push → run)
|
||||
|
||||
Phase 4:CLI(Requirement 4)
|
||||
4.1 acr init(--hosted / --self-hosted 分支)
|
||||
4.2 acr creds push(Hosted 走 API,Self-hosted 走 KV)
|
||||
4.3 acr push
|
||||
4.4 arcrun run
|
||||
4.5 acr validate
|
||||
4.6 acr parts / acr parts scaffold / acr parts publish
|
||||
4.7 acr list / acr logs
|
||||
|
||||
Phase 5:開源發布(Requirement 5)
|
||||
5.1 撰寫 README.md(含 --hosted 快速開始)
|
||||
5.2 撰寫 CONTRIBUTING.md
|
||||
5.3 確認無 InkStone 內部資訊殘留
|
||||
5.4 GitHub 發布 + npm publish
|
||||
|
||||
Phase 6:Hosted SaaS(Requirement 6)
|
||||
6.1 建立 auth-worker(api.arcrun.dev)
|
||||
6.2 cypher-executor 加入 tenant middleware
|
||||
6.3 CREDENTIALS_KV key schema 加 tenant prefix
|
||||
6.4 部署至 arcrun.dev
|
||||
|
||||
Phase 7:統計與貢獻(Requirement 7 + 8)
|
||||
7.1 analytics.ts(執行後 fire-and-forget)
|
||||
7.2 registry /analytics/record 端點
|
||||
7.3 ANALYTICS_KV 聚合邏輯
|
||||
7.4 GET /components 加入統計排序
|
||||
7.5 POST /submit 沙盒驗收 + author 寫入
|
||||
7.6 acr parts publish 指令
|
||||
```
|
||||
@@ -0,0 +1,180 @@
|
||||
# Requirements Document
|
||||
|
||||
## Introduction
|
||||
|
||||
arcrun MVP 是從 `matrix` monorepo 中 cherry-pick 出最小可獨立運作的 AI 工作流執行引擎,目標是作為**獨立開源 repo**(`arcrun`)發布。
|
||||
|
||||
**背景**:`matrix` 因為同時承載 InkStone 內部服務(KBDB、CLINIC_*、AICEO、MINI_ME 等)與核心執行引擎,複雜度過高,難以讓外部開發者使用或貢獻。MVP 的任務是將執行引擎從內部服務中解耦,讓任何人都能自行在 Cloudflare 上部署一套完整的 AI 工作流系統。
|
||||
|
||||
**護城河邏輯**:
|
||||
- 開源:cypher-executor(執行引擎)、WASM 零件庫(21 個)、credentials Worker、CLI
|
||||
- Hosted SaaS:一行指令註冊取得 API Key,直接使用公眾零件庫,無需部署任何 Worker
|
||||
- 閉源(InkStone 付費):KBDB 向量搜尋、graph 查詢、Persona SDK、MatchGPT
|
||||
|
||||
**不在此次範圍**:KBDB 整合、前端管理介面、向量搜尋、新增 WASM 零件。
|
||||
|
||||
---
|
||||
|
||||
## Glossary
|
||||
|
||||
- **cypher-executor**:原 `matrix/cypher-executor`,執行 workflow 的 Cloudflare Worker。開源版移除所有 InkStone 內部 Service Binding,只保留 KV / R2 / Workers AI。
|
||||
- **component(零件)**:以 TinyGo 編譯的 `.wasm` 檔案,以 WASI preview1 / stdin-stdout JSON 為 I/O 模型。
|
||||
- **component.contract.yaml**:每個零件的規格宣告,含 `canonical_id`、`input_schema`、`output_schema`、`gherkin_tests`,開源版補充 `credentials_required` 與 `config_example`。
|
||||
- **credentials Worker**:`arcrun/credentials`,以 AES-GCM 加密存取 API token,部署在用戶自己的 CF 帳號。
|
||||
- **WASM_BUCKET**:arcrun.dev 的 R2 bucket,儲存所有公眾 `.wasm` 零件二進位,由 Arcrun 負責。
|
||||
- **USER_KV**:用戶自己 CF 帳號下的 KV Namespace,同時存放 workflow YAML 與加密 credential,由用戶負責,Arcrun 不經手。
|
||||
- **workflow.yaml**:用戶撰寫的工作流定義,`flow:` 用 `>>` 三元組描述,`config:` 對應各節點參數,存在用戶自己的 USER_KV。
|
||||
- **CLI(套件名 arcrun,指令 acr)**:Node.js/TypeScript CLI 工具,管理 credentials、workflow 的上傳與執行。安裝:`npm i -g arcrun`,使用:`acr <指令>`。
|
||||
- **credentials_required**:contract.yaml 新欄位,宣告零件需要哪個 credential 以及注入到哪個 input 欄位。
|
||||
- **config_example**:contract.yaml 新欄位,提供 `acr parts scaffold` 指令使用的 config 範本。
|
||||
- **Standard 模式(預設)**:用戶只需在自己 CF 帳號開一個 KV(存 credential + workflow),使用 arcrun.dev 的執行引擎與公眾零件庫,無需部署任何 Worker。
|
||||
- **Self-hosted 模式**:用戶自行部署全套 Worker 至自己的 Cloudflare 帳號,有完全控制權,可貢獻零件至公眾庫。
|
||||
- **auth-worker**:arcrun.dev 上的帳號服務 Worker,處理 `POST /register` 自動發放 API Key,不儲存用戶 credential。
|
||||
- **tenant_id**:每個 API Key 對應的租戶識別碼,用於讓 cypher-executor 知道要用哪個 Cloudflare API Token 去存取用戶的 USER_KV。
|
||||
- **public registry**:arcrun.dev 上的公眾零件庫,所有人共用,有執行統計與 author 資訊。
|
||||
- **`acr parts publish`**:CLI 指令,自架用戶將自製零件提交至公眾 registry 審核。
|
||||
- **execution analytics**:每次零件執行後非同步記錄的統計資料(使用次數、成功率),公開顯示於 `acr parts`。
|
||||
- **visibility**:contract.yaml 欄位,值為 `author_only`(沙盒通過後作者立即可用)或 `public`(人工審核後所有人可用)。
|
||||
|
||||
---
|
||||
|
||||
## Requirements
|
||||
|
||||
### Requirement 1:搬移 cypher-executor 至獨立 repo 並移除 InkStone bindings
|
||||
|
||||
**User Story:** As a 開源用戶, I want 自行部署 cypher-executor 至我的 Cloudflare 帳號, so that 我不需要依賴 InkStone 的任何服務就能執行 AI 工作流。
|
||||
|
||||
#### Acceptance Criteria
|
||||
|
||||
1. THE cypher-executor `wrangler.toml` SHALL 移除以下 Service Bindings:`KBDB`、`REGISTRY`、`CLINIC_GDRIVE`、`CLINIC_EXCEL`、`CLINIC_ANALYSIS`、`CLINIC_RENDER`、`CLINIC_GSHEETS`、`AICEO`、`MINI_ME`。
|
||||
2. THE cypher-executor `wrangler.toml` SHALL 保留以下 bindings:`EXEC_CONTEXT`(KV)、`WEBHOOKS`(KV)、`WASM_BUCKET`(R2)、`AI`(Workers AI)。
|
||||
3. THE cypher-executor `wrangler.toml` SHALL 新增 `CREDENTIALS_KV`(KV Namespace binding),用於 credential 解密注入。
|
||||
4. THE component-loader SHALL 從 `WASM_BUCKET` R2 直接讀取 `.wasm` 檔案,不透過任何 Service Binding 或外部 HTTP 查詢。
|
||||
5. WHEN cypher-executor 收到執行請求,THE cypher-executor SHALL 不依賴 `KBDB`、`REGISTRY` 或任何 InkStone 內部 Service Binding,只使用 KV / R2 / Workers AI 完成執行。
|
||||
6. THE arcrun repo SHALL 包含以下目錄:`cypher-executor/`、`credentials/`、`builtins/`、`registry/components/`(21 個零件)。
|
||||
|
||||
---
|
||||
|
||||
### Requirement 2:component.contract.yaml 完整度補充
|
||||
|
||||
**User Story:** As a 零件使用者, I want 每個零件的 contract.yaml 都有 `credentials_required` 與 `config_example`, so that CLI 能自動注入 credential,用戶也能快速知道如何設定節點。
|
||||
|
||||
#### Acceptance Criteria
|
||||
|
||||
1. THE `credentials_required` 欄位 SHALL 出現在以下 4 個零件的 contract.yaml 中:`gmail`、`google_sheets`、`telegram`、`line_notify`。
|
||||
2. WHEN `credentials_required` 存在,THE 欄位 SHALL 包含以下子欄位:`key`(對應 credentials.yaml 的 key 名稱)、`type`(token 類型,如 `google_oauth`、`telegram_bot_token`)、`description`(說明)、`inject_as`(執行時注入到 input 的哪個欄位名稱)。
|
||||
3. THE `config_example` 欄位 SHALL 出現在所有 21 個零件的 contract.yaml 中。
|
||||
4. WHEN `config_example` 存在,THE 欄位 SHALL 為 YAML 字串,內容為可直接貼入 workflow.yaml `config:` 區塊的範本,需有人類可讀的說明註解。
|
||||
5. FOR 需要 credential 的零件,THE `config_example` SHALL 包含一行註解,說明哪個 credential key 會被自動注入到哪個欄位(如 `# access_token 由 credentials.yaml 的 gmail_token 自動注入`)。
|
||||
6. THE main.go 的 `required` 欄位 SHALL 與 contract 的 `input_schema.required[]` 保持一致,不得有欄位名稱不符。
|
||||
|
||||
---
|
||||
|
||||
### Requirement 3:workflow YAML 格式與執行時 credential 注入
|
||||
|
||||
**User Story:** As a 工作流設計者, I want 用有語意的關係詞撰寫 workflow.yaml,且 credential 自動注入, so that workflow 定義中完全不出現明文 token。
|
||||
|
||||
#### Acceptance Criteria
|
||||
|
||||
1. THE workflow.yaml `flow:` 欄位 SHALL 以 `"A >> 關係詞 >> B"` 三元組陣列描述資料流。
|
||||
2. THE cypher-executor SHALL 支援以下關係詞:`完成後`、`失敗時`、`對每個`、`條件滿足時`、`ON_SUCCESS`、`ON_FAIL`、`FOREACH`、`IF`、`ON_CLICK`、`CALLS_SUBFLOW`。
|
||||
3. THE cypher-executor SHALL 拒絕使用 `PIPE` 關係詞,並回傳明確錯誤訊息。
|
||||
4. WHEN cypher-executor 執行一個節點,THE cypher-executor SHALL 查詢該節點對應零件的 `credentials_required`,若存在則從 `CREDENTIALS_KV` 解密對應 credential,並注入到 input 的 `inject_as` 欄位。
|
||||
5. THE credential 注入 SHALL 發生在 WASM 執行前,用戶的 workflow `config:` 中不需也不應包含 token 值。
|
||||
6. IF `credentials_required` 宣告的 credential key 在 `CREDENTIALS_KV` 中不存在,THE cypher-executor SHALL 回傳結構化錯誤,包含缺少的 key 名稱與修復步驟說明。
|
||||
|
||||
---
|
||||
|
||||
### Requirement 4:CLI(arcrun,指令 acr)核心指令
|
||||
|
||||
**User Story:** As a 開發者, I want 透過 `acr` CLI 管理 workflow 與 credentials, so that 不需要直接操作 Cloudflare KV / R2 API 就能完成部署與執行。
|
||||
|
||||
#### Acceptance Criteria
|
||||
|
||||
1. THE CLI SHALL 以 Node.js/TypeScript 實作,套件名 `arcrun`,bin 名 `acr`,可透過 `npm i -g arcrun` 安裝,依賴只使用 `commander`、`js-yaml`、`chalk`、`ora`。
|
||||
2. THE `acr init` 指令 SHALL 以互動式問答產生 `~/.arcrun/config.yaml`,問答內容為:CF Account ID、USER_KV namespace ID、CF API Token(用於 cypher-executor 代存取用戶 KV)、email(取得 arcrun.dev API Key);並建立空白本機 `credentials.yaml`。
|
||||
3. THE `acr creds push [credentials.yaml]` 指令 SHALL 讀取 credentials.yaml,逐一加密上傳至用戶自己的 USER_KV,並顯示每個 key 的上傳結果。
|
||||
4. THE `acr push <workflow.yaml>` 指令 SHALL 解析 `flow:` 三元組,轉換成執行圖,連同 `config:` 存入 `WEBHOOKS KV`,並輸出 webhook URL。
|
||||
5. THE `acr run <workflow_name> [--input key=value...]` 指令 SHALL 觸發 cypher-executor 執行指定 workflow,顯示各節點執行結果;失敗時顯示具體節點、原因與修復步驟。
|
||||
6. THE `acr validate <workflow.yaml>` 指令 SHALL 在執行前驗證:YAML 格式、關係詞合法性(無 PIPE)、所有節點在 config 中有對應、所有零件存在於 WASM_BUCKET、所有 credentials 已上傳至 CREDENTIALS_KV。
|
||||
7. THE `acr parts` 指令 SHALL 列出所有可用零件(按類型分組),顯示每個零件的必填欄位與所需 credential。
|
||||
8. THE `acr parts scaffold <component>` 指令 SHALL 從 contract 的 `config_example` 輸出可直接貼入 workflow.yaml 的 config 範本,以及對應的 credentials.yaml 欄位範本。
|
||||
9. THE `acr list` 指令 SHALL 列出 WEBHOOKS KV 中所有已上傳的 workflow,顯示名稱與更新時間。
|
||||
10. THE `acr logs <workflow_name>` 指令 SHALL 顯示最近執行記錄,包含時間、成功/失敗狀態、執行時間,失敗時顯示失敗節點與原因。
|
||||
|
||||
---
|
||||
|
||||
### Requirement 5:README 與開源發布準備
|
||||
|
||||
**User Story:** As a 外部開發者, I want 看到清楚的 README,5 分鐘內能完成部署, so that 降低試用門檻,吸引社群貢獻。
|
||||
|
||||
#### Acceptance Criteria
|
||||
|
||||
1. THE README.md SHALL 包含以下章節:專案定位(開源核心 vs 閉源付費服務說明)、快速開始(`acr init` → `acr creds push` → `acr push` → `acr run` 四步驟)、零件列表(21 個零件分類說明)、workflow YAML 語法說明(三元組 + 關係詞表格)、自行部署說明(Cloudflare Workers 部署步驟)。
|
||||
2. THE README.md 快速開始 SHALL 以 `newsletter_subscribe` 為範例 workflow,展示 gmail + google_sheets + telegram 的完整串接。
|
||||
3. THE repo SHALL 包含 `CONTRIBUTING.md`,說明如何新增零件(TinyGo 開發環境、contract.yaml 格式、本機測試指令)。
|
||||
4. THE repo SHALL 確保所有 InkStone 內部資訊(Worker URL、KV namespace ID、帳號資訊)不出現在任何已提交的檔案中。
|
||||
5. WHEN cypher-executor 部署後第一次被呼叫,THE cypher-executor SHALL 能正常回應 health check(`GET /health` 回傳 `{ ok: true }`),不需要任何 InkStone 服務可用。
|
||||
|
||||
---
|
||||
|
||||
### Requirement 6:Standard 模式 — API Key 註冊與用戶 KV 存取
|
||||
|
||||
**User Story:** As a 新用戶, I want 只需開一個 CF KV 就能開始使用 Arcrun,不需要部署任何 Worker, so that 最低門檻試用整個平台,且我的 credential 永遠在我自己的環境。
|
||||
|
||||
#### Acceptance Criteria
|
||||
|
||||
1. THE auth-worker SHALL 提供 `POST /register` 端點,接受 `{ email }` 後自動生成 API Key(格式:`ak_` 前綴 + 32 字元隨機字串),無需人工審核,立即回傳 `{ api_key, tenant_id }`。
|
||||
2. THE auth-worker SHALL 將 `{ tenant_id, email, created_at, api_key_hash }` 存入 `ACCOUNTS_KV`,只存 hash 不存明文 API Key。arcrun.dev 不儲存任何用戶 credential 或 workflow 內容。
|
||||
3. WHEN `acr init` 執行,THE CLI SHALL 互動式詢問以下資料並寫入 `~/.arcrun/config.yaml`:
|
||||
- CF Account ID(用戶自己的)
|
||||
- USER_KV namespace ID(用戶自己開的,存 credential + workflow)
|
||||
- CF API Token(供 cypher-executor 用 CF API 存取用戶 KV,只需 KV Edit 權限)
|
||||
- email(呼叫 `POST https://api.arcrun.dev/register` 取得 API Key)
|
||||
4. THE cypher-executor SHALL 在每個 request 的 header 讀取 `X-Arcrun-API-Key`,驗證後取得該 tenant 的 CF API Token,用 Cloudflare API 存取用戶自己的 USER_KV;缺少或無效的 API Key 回傳 `401 Unauthorized`。
|
||||
5. THE `acr creds push` 指令 SHALL 使用用戶的 CF API Token,直接呼叫 Cloudflare KV API 將加密 credential 寫入用戶自己的 USER_KV,不經過 arcrun.dev。
|
||||
6. THE `acr push <workflow.yaml>` 指令 SHALL 同樣直接寫入用戶自己的 USER_KV,不經過 arcrun.dev。
|
||||
7. WHEN Self-hosted 模式,THE cypher-executor SHALL 可透過環境變數 `MULTI_TENANT=false` 停用 API Key 驗證,直接使用本地 KV binding,與現有行為相容。
|
||||
|
||||
---
|
||||
|
||||
### Requirement 7:公眾零件庫執行統計與貢獻榮譽
|
||||
|
||||
**User Story:** As a 零件使用者, I want 在 `acr parts` 看到每個零件的真實執行統計與作者資訊, so that 我能選擇最可靠的零件;As a 零件貢獻者, I want 我的名字和統計數字公開顯示, so that 我有動機將好零件推入公眾庫而非留在私庫。
|
||||
|
||||
#### Acceptance Criteria
|
||||
|
||||
1. THE contract.yaml SHALL 新增可選欄位 `author`(GitHub username,如 `@alice`),在 `acr parts` 顯示時一起展示。
|
||||
2. WHEN cypher-executor 執行完一個零件節點,THE cypher-executor SHALL 非同步 POST 以下資料至 `https://registry.arcrun.dev/analytics/record`,不阻擋主流程:
|
||||
```json
|
||||
{ "canonical_id": "gmail", "version": "v1", "success": true, "duration_ms": 120 }
|
||||
```
|
||||
不含任何用戶資料或 tenant_id。
|
||||
3. THE public registry SHALL 聚合每個零件的執行統計:`total_runs`(總執行次數)、`success_rate`(成功率,百分比)、`avg_duration_ms`(平均執行時間)。
|
||||
4. THE `acr parts` 指令 SHALL 顯示每個零件的統計資料,格式為:
|
||||
```
|
||||
• gmail Gmail 發信 by @alice
|
||||
★ 99.2% 成功 | 140,382 次執行 | 平均 120ms
|
||||
```
|
||||
5. IF 零件存在於用戶自架的私有 WASM_BUCKET 而非公眾庫,THE `acr parts` SHALL 顯示該零件但標註 `[私有]`,不顯示統計數字與 author。
|
||||
6. THE public registry SHALL 在 `GET /components` 回傳的零件清單中,依 `total_runs × success_rate` 排序,讓高品質高使用量的零件排在前面。
|
||||
|
||||
---
|
||||
|
||||
### Requirement 8:零件貢獻流程與 visibility 狀態
|
||||
|
||||
**User Story:** As a 零件開發者, I want 提交零件後立即能自己使用,等審核通過後公開給所有人, so that 不用等待審核就能驗證自己的零件是否有用。
|
||||
|
||||
#### Acceptance Criteria
|
||||
|
||||
1. THE contract.yaml SHALL 包含 `visibility` 欄位,值為 `author_only`(沙盒通過後作者立即可用)或 `public`(人工審核通過後所有人可用)。
|
||||
2. THE `acr parts publish <component>` 指令 SHALL 打包指定零件的原始碼、`component.contract.yaml`、`.wasm`,POST 至 `https://registry.arcrun.dev/submit`(帶 `X-Arcrun-API-Key` header)。原始碼語言不限,但編譯產出必須為 WASM + WASI preview1。
|
||||
3. WHEN 零件提交後,THE registry SHALL 依零件類型執行不同層級的沙盒驗收:
|
||||
- **整合類**(需呼叫外部 API,如 gmail、telegram):體積 / syscall 掃描通過 → `author_only`
|
||||
- **功能類**(純邏輯,如 string_ops、if_control):體積 / syscall 掃描 / Gherkin 測試全通過 → `author_only`
|
||||
- 任一必要步驟失敗 → `rejected`(回傳具體失敗步驟與原因)
|
||||
4. WHEN 零件 visibility 為 `author_only`,THE registry SHALL 讓該零件只對提交者的 API Key 可見,`acr parts` 顯示時標註 `[待審核]`,其他用戶看不到。
|
||||
5. WHEN 人工審核通過,THE registry SHALL 將 visibility 改為 `public`,零件立即出現在所有人的 `acr parts` 清單,並開始累積公開執行統計。
|
||||
6. WHEN 審核拒絕,THE registry SHALL 回傳具體失敗原因,零件保留 `author_only` 狀態讓作者繼續修改後重新提交。
|
||||
7. THE `acr parts publish` 指令 SHALL 在提交後顯示 `submission_id`、目前 visibility 狀態,以及查詢審核進度的指令提示。
|
||||
8. THE `acr parts` 指令 SHALL 對 `author_only` 零件顯示「[待審核] 只有你可用」,對 `public` 零件顯示執行統計與 author,讓貢獻者清楚知道零件的可用範圍。
|
||||
@@ -0,0 +1,206 @@
|
||||
# Implementation Plan: arcrun MVP
|
||||
|
||||
## Overview
|
||||
|
||||
依照 Design 的七個 Phase 實作。原則:最小異動,不重寫現有邏輯,只 cherry-pick + carve-out + supplement。
|
||||
所有 Phase 1–3 工作在 `matrix` repo 對應目錄驗證後再搬到新 repo。
|
||||
|
||||
**PR #2(claude/review-mvp-specs-8Bvdu)狀態:** 初始實作已提交,已修復以下問題後準備 merge:
|
||||
- CF API Token 傳至 arcrun.dev 安全問題(已修復)
|
||||
- 加密 fallback 格式不相容(已修復)
|
||||
- submitComponent KBDB 依賴(已修復,改用 SUBMISSIONS_KV)
|
||||
- Webhook 路由缺 analytics(已修復)
|
||||
- `require()` 在 ES module 中(已修復)
|
||||
- api 類零件 `no_network_syscall: true` 錯誤(已修復)
|
||||
|
||||
---
|
||||
|
||||
## Phase 1:搬移與清理
|
||||
|
||||
- [x] 1. 建立 `arcrun` 獨立 repo 並初始化
|
||||
- [x] 1.1 在 GitHub 建立新的 public repo(使用 matrix monorepo 的 `arcrun/` 子目錄代替,PR #2)
|
||||
- [x] 1.2 設定 `.gitignore`(排除 `node_modules/`、`.wrangler/`、`credentials.yaml`、`~/.arcrun/`)
|
||||
- [x] 1.3 從 `matrix` cherry-pick 四個目錄:
|
||||
- `matrix/cypher-executor/` → `arcrun/cypher-executor/`
|
||||
- `matrix/u6u-core/credentials/` → `arcrun/credentials/`
|
||||
- `matrix/u6u-core/registry/components/` → `arcrun/registry/components/`
|
||||
- _Requirements: 1.6_
|
||||
|
||||
- [x] 2. 清理 `cypher-executor/wrangler.toml`
|
||||
- [x] 2.1 移除 9 個 InkStone Service Bindings(KBDB、REGISTRY、CLINIC_*、AICEO、MINI_ME)
|
||||
- [x] 2.2 確認保留:`EXEC_CONTEXT`、`WEBHOOKS`、`WASM_BUCKET`、`AI`
|
||||
- [x] 2.3 新增 `CREDENTIALS_KV` 與 `ANALYTICS_KV` KV namespace binding
|
||||
- [x] 2.4 更新 `name` 為 `arcrun-cypher-executor`
|
||||
- _Requirements: 1.1, 1.2, 1.3_
|
||||
|
||||
- [x] 3. 改寫 `cypher-executor/src/lib/component-loader.ts`
|
||||
- [x] 3.1 移除對 MINI_ME、KBDB、InkStone bindings 的 hardcode
|
||||
- [x] 3.2 實作三層邏輯:builtin Map → WASM_BUCKET R2 直讀 → 結構化錯誤
|
||||
- _Requirements: 1.4, 1.5_
|
||||
|
||||
- [x] 4. 移除對 InkStone bindings 的依賴程式碼
|
||||
- [x] 4.1 刪除 `autoPublishMissing.ts`(依賴 REGISTRY binding)
|
||||
- [x] 4.2 移除所有 `env.KBDB`、`env.REGISTRY`、`env.MINI_ME`、`env.AICEO`、`env.CLINIC_*` 引用
|
||||
- _Requirements: 1.1, 1.5_
|
||||
|
||||
- [ ] 5. 本機驗證
|
||||
- [ ] 5.1 `cd arcrun/cypher-executor && wrangler dev` 能啟動(無 binding 錯誤)
|
||||
- [ ] 5.2 `GET /health` 回傳 `{ ok: true }`
|
||||
- [ ] 5.3 上傳 `validate_json.wasm` 到 WASM_BUCKET,執行 `POST /execute` 能正常回傳結果
|
||||
- _Requirements: 1.5, 5.5_
|
||||
|
||||
---
|
||||
|
||||
## Phase 2:零件完整度補充
|
||||
|
||||
- [x] 6. api 類零件 `no_network_syscall` 修正
|
||||
- [x] 6.1 gmail、telegram、google_sheets、line_notify、http_request 改為 `no_network_syscall: false`
|
||||
- _Requirements: 2.1_
|
||||
|
||||
- [ ] 7. 審查 21 個零件 contract.yaml 並補充 `credentials_required`
|
||||
- [ ] 7.1 確認 gmail、google_sheets、telegram、line_notify 有 `credentials_required`(PR #2 已加入,需驗證格式正確)
|
||||
- [ ] 7.2 確認所有 21 個零件有 `config_example` 欄位
|
||||
- [ ] 7.3 驗證 `main.go` required 欄位與 `contract.yaml` input_schema.required[] 一致
|
||||
- _Requirements: 2.1, 2.2, 2.3_
|
||||
|
||||
---
|
||||
|
||||
## Phase 3:Credential 注入整合
|
||||
|
||||
- [x] 10. `credential-injector.ts` 已實作(`arcrun/cypher-executor/src/actions/credential-injector.ts`)
|
||||
- [x] 10.1 讀取 contract.yaml from R2,解析 `credentials_required`
|
||||
- [x] 10.2 從 `CREDENTIALS_KV` 讀取 AES-GCM 加密 token,注入到 input 對應欄位(inject_as)
|
||||
- [x] 10.3 credential 不存在時拋出結構化錯誤(含 key 名稱與修復步驟)
|
||||
- _Requirements: 3.4, 3.5, 3.6_
|
||||
|
||||
- [ ] 11. 驗證 credential 注入整合進 graph-executor
|
||||
- [ ] 11.1 確認 `graph-executor.ts` 在節點執行前正確呼叫 `injectCredentials`
|
||||
- [ ] 11.2 確認注入只影響 WASM input,不修改 WEBHOOKS KV 中儲存的 workflow 定義
|
||||
- _Requirements: 3.4, 3.5_
|
||||
|
||||
- [ ] 12. 端對端測試(手動)
|
||||
- [ ] 12.1 建立 `credentials.yaml`,加入測試 token
|
||||
- [ ] 12.2 執行 `acr creds push`,確認寫入 CREDENTIALS_KV 格式為 `{ encrypted, iv }`(無 `mode: 'base64'`)
|
||||
- [ ] 12.3 執行含 credential 的 workflow,確認 inject_as 欄位正確注入
|
||||
- _Requirements: 3.4, 3.5_
|
||||
|
||||
---
|
||||
|
||||
## Phase 4:CLI 開發
|
||||
|
||||
- [x] 13. CLI 專案骨架已建立(`arcrun/cli/`)
|
||||
- [x] 13.1 `package.json`(name: `arcrun`,bin: `acr`)
|
||||
- [x] 13.2 `tsconfig.json`(module: NodeNext)
|
||||
- [x] 13.3 所有 10 個指令已實作骨架
|
||||
- _Requirements: 4.1_
|
||||
|
||||
- [x] 14. `acr init` 已實作,修正項:
|
||||
- [x] 14.1 Standard 模式不再傳送 `cf_api_token` 至 arcrun.dev(只傳 `email`)
|
||||
- [x] 14.2 `require()` 改用 `await import()` 修正 ES module 相容
|
||||
- [ ] 14.3 **待補**:`acr init` 需詢問 `ARCRUN_ENCRYPTION_KEY` 並寫入 config(目前加密 key 需手動設定)
|
||||
- _Requirements: 4.2, 6.3_
|
||||
|
||||
- [x] 15. `acr creds push` 已實作
|
||||
- [x] 15.1 讀取 `credentials.yaml`,AES-GCM 加密後寫入用戶 CF KV(`cred:{name}`)
|
||||
- [x] 15.2 加密 fallback(base64)已移除,key 不足時直接拋錯提示生成指令
|
||||
- _Requirements: 4.3, 6.5_
|
||||
|
||||
- [x] 16. `acr push` 已實作
|
||||
- _Requirements: 4.4_
|
||||
|
||||
- [x] 17. `acr run` 已實作
|
||||
- _Requirements: 4.5_
|
||||
|
||||
- [ ] 18. `acr validate` credential 檢測邏輯有誤,需修復
|
||||
- [ ] 18.1 `extractCredentialRefs()` 目前掃描 `{{creds.xxx}}` 語法,但 injection 使用 `inject_as` key
|
||||
- [ ] 18.2 改為讀取 contract.yaml 的 `credentials_required[].key`,與 `cred:{key}` KV 存在性比對
|
||||
- _Requirements: 4.6_
|
||||
|
||||
- [x] 19. `acr parts`、`acr parts scaffold`、`acr parts publish` 已實作
|
||||
- [ ] 19.1 `acr parts` 中 YAML 解析改用 `js-yaml`(目前用 regex,可能解析失敗)
|
||||
- _Requirements: 4.7, 4.8_
|
||||
|
||||
- [x] 20. `acr list` 與 `acr logs` 已實作
|
||||
- _Requirements: 4.9, 4.10_
|
||||
|
||||
---
|
||||
|
||||
## Phase 5:開源發布準備
|
||||
|
||||
- [x] 21. README.md 已撰寫(`arcrun/README.md`)
|
||||
- [x] 22. CONTRIBUTING.md 已撰寫(`arcrun/CONTRIBUTING.md`)
|
||||
- [ ] 23. 安全審查(PR merge 前執行)
|
||||
- [ ] 23.1 搜尋 `.workers.dev` InkStone 網域
|
||||
- [ ] 23.2 確認 wrangler.toml 所有 KV id 欄位留空
|
||||
- [ ] 23.3 確認 `credentials.yaml` 在 `.gitignore` 中
|
||||
- _Requirements: 5.4_
|
||||
|
||||
- [ ] 24. 發布(安全審查後)
|
||||
- [ ] 24.1 `npm publish`(CLI package `arcrun`)
|
||||
- _Requirements: 5.1_
|
||||
|
||||
---
|
||||
|
||||
## Phase 6:Standard 模式 — auth-worker 與用戶 KV 代存取
|
||||
|
||||
- [ ] 25. 建立 `auth-worker`(新 Worker,部署至 `api.arcrun.dev`)
|
||||
- [ ] 25.1 建立 `auth-worker/` 目錄,初始化 Hono + wrangler.toml
|
||||
- [ ] 25.2 實作 `POST /register`:接收 `{ email, account_id, kv_namespace_id }` + CF API Token 透過 header 傳入
|
||||
- **不在 request body 中接收 CF API Token**(Token 透過 header `CF-Api-Token` 傳入,減少 TLS 以外的洩漏面)
|
||||
- 生成 `tenant_id` 與 `api_key`,存入 `ACCOUNTS_KV`
|
||||
- [ ] 25.3 Bindings:`ACCOUNTS_KV`
|
||||
- _Requirements: 6.1, 6.2_
|
||||
|
||||
- [ ] 26. 改造 `cypher-executor` 支援 multi-tenant 用戶 KV 代存取
|
||||
- [ ] 26.1 讀取 `MULTI_TENANT` env var(目前已宣告但未讀取),實作 tenant middleware
|
||||
- [ ] 26.2 `X-Arcrun-API-Key` → 查 `ACCOUNTS_KV` → 取得用戶 cf_api_token + kv_namespace_id → 建立 `CfKvClient`
|
||||
- [ ] 26.3 `CfKvClient` 已實作(`arcrun/cli/src/lib/cf-api.ts`),需移植到 `cypher-executor/src/lib/`
|
||||
- [ ] 26.4 `credential-injector.ts` 改用 userKv 取得加密 credential
|
||||
- [ ] 26.5 webhook 路由注入 userKv
|
||||
- _Requirements: 6.4, 6.5, 6.6_
|
||||
|
||||
- [ ] 27. 端對端測試(用戶 KV 隔離)
|
||||
- _Requirements: 6.4, 6.5_
|
||||
|
||||
---
|
||||
|
||||
## Phase 7:公眾零件統計與貢獻審核
|
||||
|
||||
- [x] 28. Analytics 基礎設施已建立
|
||||
- [x] 28.1 `execution-logger.ts` 建立,`writeExecutionVerdict` 寫入 `ANALYTICS_KV`(fire-and-forget)
|
||||
- [x] 28.2 `/execute` 路由已整合 `waitUntil(writeExecutionVerdict(...))`
|
||||
- [x] 28.3 `/webhooks/:token/trigger` 路由已補上 `waitUntil(writeExecutionVerdict(...))`
|
||||
- _Requirements: 7.2_
|
||||
|
||||
- [ ] 29. registry Worker analytics 端點
|
||||
- [ ] 29.1 新增 `POST /analytics/record` 路由,原子更新 `ANALYTICS_KV`
|
||||
- [ ] 29.2 `GET /components` 回傳加入 `total_runs`、`success_rate`、`avg_duration_ms`
|
||||
- _Requirements: 7.3, 7.6_
|
||||
|
||||
- [x] 30. `author` 欄位已加入 contract.yaml 規格
|
||||
- _Requirements: 7.1_
|
||||
|
||||
- [x] 31. 零件提交審核流程已實作(`arcrun/registry/src/actions/submitComponent.ts`)
|
||||
- [x] 31.1 沙盒驗收流程(sandboxAcceptance.ts):size_check + syscall_scan 已實作;cold_start + gherkin_tests 為 Phase 0 mock
|
||||
- [x] 31.2 `SUBMISSIONS_KV` 儲存元數據,預設 `visibility: author_only`
|
||||
- [ ] 31.3 `PATCH /submit/:id/approve` → 將 visibility 改為 `public`(待實作)
|
||||
- [ ] 31.4 Gherkin 測試執行(取代 mock)
|
||||
- _Requirements: 8.2, 8.3, 8.4, 8.5_
|
||||
|
||||
---
|
||||
|
||||
## 待辦(無相依順序,可平行處理)
|
||||
|
||||
- [ ] A. `builtins/` 清理:`initComponents.ts` 仍用舊的 HTTP endpoint 模式上架零件(`buildComponentDefs` 含 URL),應改為呼叫 `POST /submit` 送 WASM binary + contract,或直接移除 builtins(功能已整合到 registry)
|
||||
- [ ] B. `validate` 指令 credential 檢測邏輯修復(見 Phase 4 Task 18)
|
||||
- [ ] C. `acr init` 加入 `ARCRUN_ENCRYPTION_KEY` 設定步驟
|
||||
- [ ] D. `acr parts` YAML 解析改用 `js-yaml`
|
||||
|
||||
---
|
||||
|
||||
## Notes
|
||||
|
||||
- 標記 `*` 的子任務為選填,可跳過以加速 MVP 交付
|
||||
- Gherkin 測試執行(sandbox 步驟 d)為 Phase 0 mock,Phase 7 補充
|
||||
- cold-start 測量(sandbox 步驟 b)為 Phase 0 mock,Phase 2 補充
|
||||
- CF API Token 永遠不離開用戶本機,arcrun.dev 只收 email + account_id + kv_namespace_id
|
||||
@@ -0,0 +1,822 @@
|
||||
# Design Document: u6u Platform Evolution
|
||||
|
||||
## Overview
|
||||
|
||||
u6u 平台演進的核心目標是將現有的「HTTP endpoint 零件 + 單一 Cloudflare 部署」架構,演進為「WASM 零件模型 + 三層物理部署 + 雙面翻轉畫布」的完整平台。
|
||||
|
||||
設計的最高原則是 **Dogfooding**:每一層都是下一層的第一個用戶。底層先建立最小可運行的能力,再用自己的方式往上蓋。這確保每個設計決策都被真實使用場景驗證,而非紙上談兵。
|
||||
|
||||
### Bootstrap 順序(不可跳過)
|
||||
|
||||
```
|
||||
Phase 0:最小 WASM 執行核心
|
||||
→ Component Dispatcher 能在 CF Workers 執行一個 .wasm(stdin/stdout JSON)
|
||||
→ validate_json.wasm 作為第一個真實零件(TinyGo,< 50KB,驗證整個 pipeline)
|
||||
→ Component Registry API(/guide、/validate-contract、/components)
|
||||
|
||||
Phase 1:遷移現有零件
|
||||
→ 將 u6u-builtins 的 20 個 HTTP endpoint 逐一遷移為 .wasm
|
||||
→ 遷移期間 Component Dispatcher 雙模式並存(HTTP fallback)
|
||||
→ 每個零件附帶 component.contract.yaml
|
||||
|
||||
Phase 2:Cypher 語意擴展 + Multi-Tier Dispatcher
|
||||
→ 支援 IS_A、ON_SUCCESS、ON_FAIL、CALLS_SUBFLOW、ON_CLICK
|
||||
→ Component Dispatcher 路由層(Tier 1 CF / Tier 2 workerd / Tier 3 Wazero)
|
||||
|
||||
Phase 3:前端畫布(用自己的 Web Components 開發)
|
||||
→ 先建立 Web Components 零件庫(u6u-btn、u6u-card 等)
|
||||
→ 畫布本身用這些 Web Components 組裝
|
||||
→ 雙面翻轉介面
|
||||
```
|
||||
|
||||
### 關鍵設計約束
|
||||
|
||||
- **KBDB 不變量**:永遠只有三張表(blocks / templates / slots),不新增表
|
||||
- **API-First 鐵律**:所有跨服務通訊只透過 HTTP API,禁止相對路徑引用
|
||||
- **零件 I/O 不變量**:唯一合法的 I/O 模型是 `stdin_stdout_json`
|
||||
- **Tier 3 約束**:無 V8、無 Node.js、無網路,所有零件必須在 Wazero 上跑
|
||||
|
||||
---
|
||||
|
||||
## Architecture
|
||||
|
||||
### 系統全景圖
|
||||
|
||||
```mermaid
|
||||
graph TB
|
||||
subgraph "Tier 1 — Cloudflare Workers(雲端)"
|
||||
CE[Cypher Executor<br/>GraphExecutor]
|
||||
CD[Component Dispatcher<br/>路由層]
|
||||
CR[Component Registry<br/>KBDB HTTP API]
|
||||
KBDB[(KBDB<br/>blocks/templates/slots<br/>+ Vectorize)]
|
||||
R2[(R2<br/>.wasm 二進位)]
|
||||
CE --> CD
|
||||
CD --> CR
|
||||
CR --> KBDB
|
||||
CR --> R2
|
||||
end
|
||||
|
||||
subgraph "Tier 2 — workerd self-hosted(企業地端)"
|
||||
T2D[Tier 2 Dispatcher<br/>同 wasi-shim,不同部署]
|
||||
T2R[本地 Registry 快取]
|
||||
T2D --> T2R
|
||||
end
|
||||
|
||||
subgraph "Tier 3 — 邊緣載具"
|
||||
T3E[Go 排程引擎]
|
||||
Wazero[Wazero Runtime]
|
||||
SQLite[(SQLite<br/>本地 KBDB)]
|
||||
DTN[DTN 佇列]
|
||||
T3E --> Wazero
|
||||
T3E --> SQLite
|
||||
T3E --> DTN
|
||||
end
|
||||
|
||||
subgraph "前端"
|
||||
Canvas[雙面翻轉畫布<br/>React 19 + Web Components]
|
||||
WC[Web Components 零件庫<br/>u6u-btn / u6u-card / ...]
|
||||
Canvas --> WC
|
||||
end
|
||||
|
||||
CD -->|WASM 執行| Tier1WASM[.wasm 執行]
|
||||
CD -->|Cypher binding| ExtSvc[外部服務<br/>MCP / n8n / 任意 URL]
|
||||
CD -->|HTTP| T2D
|
||||
T2D -->|Wazero IPC| Wazero
|
||||
DTN -->|Burst 傳輸| T2D
|
||||
Canvas -->|u6u:trigger event| CE
|
||||
```
|
||||
|
||||
### Cypher Binding 的正確定義
|
||||
|
||||
**Cypher binding** 是 u6u 的核心執行機制,指「用 Cypher 三元組語法把零件串接成工作流,串接關係儲存在 KBDB,不寫死在程式碼裡」。
|
||||
|
||||
這個概念相對於 Cloudflare Workers 原生的 **Service Binding**(需要 deploy、串接關係寫死在 wrangler.toml)。
|
||||
|
||||
`cypher-executor` 就是執行 Cypher binding 的引擎。
|
||||
|
||||
**零件本身只有兩種 component_type:**
|
||||
|
||||
| component_type | 說明 | 需要 deploy? |
|
||||
|---|---|---|
|
||||
| `wasm` | 所有後端零件(內建或用戶自建),本地 WASM 執行 | 否 |
|
||||
| `service_binding` | 多個零件預組合成單一高頻零件的效能最佳化(如 OAuth + GSheets 常用組合) | 是 |
|
||||
|
||||
> **重要:`cypher_binding` 不是 component_type。** 它是整個執行引擎的名字,描述「零件如何被串接」,而不是「零件如何被執行」。所有零件(不管是內建還是用戶自建、不管是打外部 API 還是純邏輯)都是 `.wasm`,透過 Cypher 三元組串接。
|
||||
|
||||
> **所有後端零件都是 `.wasm`。** 需要呼叫外部 HTTP API 的零件(如 google-sheets、http-request),透過 WASI shim 注入的 **host function** 發出網路請求,不在 .wasm 內部直接呼叫網路 syscall。
|
||||
|
||||
### Component Dispatcher 路由決策樹
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
A[Cypher Executor 呼叫零件 id] --> B{查 Component Registry<br/>取得合約}
|
||||
B --> C{component_type?}
|
||||
C -->|wasm| E{當前 Tier?}
|
||||
C -->|service_binding| SB{有 CF Service Binding?}
|
||||
SB -->|是| SBExec[CF Service Binding 執行<br/>需 deploy,效能最佳]
|
||||
SB -->|否| SBErr[回傳錯誤:binding 未宣告]
|
||||
E -->|Tier 1 / Tier 2| I[workerd WASM<br/>WebAssembly.instantiate<br/>+ WASI shim(兩者相同)]
|
||||
E -->|Tier 3| L[Wazero IPC<br/>stdin/stdout,完全離線]
|
||||
I --> RC{runtime_compat<br/>包含 cf-workers?}
|
||||
RC -->|否| J[回傳 RUNTIME_INCOMPATIBLE 錯誤]
|
||||
RC -->|是| Exec[執行 .wasm]
|
||||
```
|
||||
|
||||
### KBDB 資料模型(tpl-component)
|
||||
|
||||
```mermaid
|
||||
erDiagram
|
||||
TEMPLATES {
|
||||
string template_id "tpl-component"
|
||||
string name
|
||||
string description
|
||||
}
|
||||
BLOCKS {
|
||||
string block_id "comp-{id}-{version}"
|
||||
string template_id
|
||||
string user_id
|
||||
string page_name
|
||||
}
|
||||
SLOTS {
|
||||
string slot_id
|
||||
string block_id
|
||||
string key
|
||||
string value
|
||||
}
|
||||
TEMPLATES ||--o{ BLOCKS : "defines"
|
||||
BLOCKS ||--o{ SLOTS : "has"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Components and Interfaces
|
||||
|
||||
### 1. Component Registry(`u6u-core/registry/`)
|
||||
|
||||
Component Registry 是 KBDB 的薄包裝層,透過 HTTP API 管理零件合約。
|
||||
|
||||
#### 零件命名機制
|
||||
|
||||
零件有兩個名稱,職責完全不同:
|
||||
|
||||
| 欄位 | 由誰決定 | 用途 | 範例 |
|
||||
|---|---|---|---|
|
||||
| `display_name` | 建立者自由取 | 顯示用,不影響任何邏輯 | `宇宙無敵 GSheets 超級寫入器` |
|
||||
| `canonical_id` | Registry AI 正規化後確認 | 搜尋、版本控制、Cypher 引用的唯一鍵 | `gsheets_create_table` |
|
||||
|
||||
**canonical_id 正規化流程:**
|
||||
|
||||
```
|
||||
提交者輸入 display_name
|
||||
↓
|
||||
Registry 用 Workers AI 建議 canonical_id
|
||||
(格式:{service}_{verb}_{object},全小寫底線)
|
||||
↓
|
||||
同時搜尋 Vectorize,若相似度 > 0.9 的 canonical_id 已存在
|
||||
→ 提示「可能與 gsheets_create_table 重複,是否作為新版本提交?」
|
||||
↓
|
||||
提交者確認或修改 canonical_id
|
||||
↓
|
||||
上架,canonical_id 永久不變
|
||||
```
|
||||
|
||||
#### 零件分類機制
|
||||
|
||||
採用「強制 category + 自由 tags」雙層分類:
|
||||
|
||||
- **`category`**:強制填,有限集合,定義前後端邊界
|
||||
- `logic`:後端邏輯零件(.wasm,純計算/轉換)
|
||||
- `api`:後端 API 零件(.wasm + cypher_binding,呼叫外部服務)
|
||||
- `ui`:前端 UI 元件(Web Component,瀏覽器執行)
|
||||
- `style`:前端樣式零件(CSS tokens)
|
||||
- `anim`:前端動畫零件
|
||||
|
||||
- **`tags`**:自由增加,跨零件共享語意
|
||||
- 例:`gsheets_create_table` 有 `["google", "sheets", "spreadsheet", "storage", "write"]`
|
||||
- 例:`excel_write_row` 有 `["microsoft", "excel", "spreadsheet", "storage", "write"]`
|
||||
- 搜尋「外部存儲」時,兩個都能透過 Vectorize 語意搜尋找到
|
||||
|
||||
**HTTP 端點:**
|
||||
|
||||
```
|
||||
GET /components/guide → 機器可讀開發指引(Markdown)
|
||||
POST /components/validate-contract → 驗證 component.contract.yaml 格式
|
||||
POST /components → 提交零件(.wasm + contract)觸發沙盒驗收
|
||||
GET /components/:id → 取得零件合約(最優版本)
|
||||
GET /components/:id/versions → 取得所有版本清單(含評分)
|
||||
GET /components/search?q=... → 語意搜尋零件
|
||||
```
|
||||
|
||||
**KBDB 整合:**
|
||||
- 每個零件版本 = 一個 Block,`block_id = comp-{id}-{version}`
|
||||
- Template = `tpl-component`(預先建立,不新增表)
|
||||
- `.wasm` 二進位存 R2,KBDB slot 只存 `wasm_r2_key`
|
||||
- `description` + `tags` 欄位寫入 Vectorize 索引,支援語意搜尋
|
||||
|
||||
**Slot 欄位對應(tpl-component):**
|
||||
|
||||
| Slot key | 說明 | 範例值 |
|
||||
|---|---|---|
|
||||
| `canonical_id` | 正規化功能名稱(永久不變,搜尋/版本控制用) | `gsheets_create_table` |
|
||||
| `display_name` | 建立者自取的顯示名稱 | `宇宙無敵 GSheets 超級寫入器` |
|
||||
| `category` | 零件分類(有限集合) | `logic` / `api` / `ui` / `style` / `anim` |
|
||||
| `version` | 實作版本 | `v1` |
|
||||
| `wasi_target` | WASM 目標 | `preview1` |
|
||||
| `stability` | 穩定性標籤 | `floating` |
|
||||
| `runtime_compat` | 相容 runtime(JSON 陣列) | `["cf-workers","wazero"]` |
|
||||
| `component_type` | 零件類型 | `wasm` / `service_binding` |
|
||||
| `max_size_kb` | 體積上限 | `2048` |
|
||||
| `max_cold_start_ms` | 冷啟動上限 | `50` |
|
||||
| `no_network_syscall` | 禁止網路 syscall | `true` |
|
||||
| `input_schema` | JSON Schema(JSON 字串) | `{"type":"object",...}` |
|
||||
| `output_schema` | JSON Schema(JSON 字串) | `{"type":"object",...}` |
|
||||
| `gherkin_tests` | 測試案例(JSON 字串) | `[{"scenario":"..."}]` |
|
||||
| `wasm_r2_key` | R2 物件鍵(wasm 模式) | `components/validate_json/v1.wasm` |
|
||||
| `service_binding_key` | CF binding key(service_binding 模式) | `CLINIC_GSHEETS` |
|
||||
| `description` | 自然語言描述(寫入 Vectorize) | `在 Google Sheets 建立新工作表` |
|
||||
| `tags` | 自由標籤(JSON 陣列,跨零件共享語意) | `["google","sheets","storage","write"]` |
|
||||
| `success_rate` | 成功率(0-1) | `0.98` |
|
||||
| `avg_duration_ms` | 平均執行時間 | `12` |
|
||||
| `call_count` | 被調用次數 | `1024` |
|
||||
| `status` | 狀態 | `active` / `deprecated` / `tombstone` |
|
||||
| `deprecated_at` | 棄用時間戳記 | `1700000000000` |
|
||||
|
||||
### 2. Component Dispatcher(`cypher-executor/src/lib/component-loader.ts` 擴展)
|
||||
|
||||
Component Dispatcher 是 `createComponentLoader` 的升級版,新增 WASM 執行路徑。
|
||||
|
||||
**介面定義:**
|
||||
|
||||
```typescript
|
||||
// 零件類型(只有兩種)
|
||||
type ComponentType =
|
||||
| 'wasm' // 所有後端零件,透過 Cypher binding 串接,本地 WASM 執行
|
||||
| 'service_binding'; // 效能最佳化:CF Service Binding,需 deploy,用於高頻預組合零件
|
||||
|
||||
// 新版 ComponentDescriptor
|
||||
type ComponentDescriptor = {
|
||||
component_type: ComponentType;
|
||||
// WASM 模式
|
||||
wasm_r2_key?: string;
|
||||
runtime_compat?: string[];
|
||||
max_cold_start_ms?: number;
|
||||
// Service Binding 模式(CF Worker 間高效呼叫,需 deploy)
|
||||
binding?: string; // wrangler.toml 中宣告的 binding key
|
||||
path?: string;
|
||||
};
|
||||
```
|
||||
|
||||
**Tier 1 WASM 執行(CF Workers 原生):**
|
||||
|
||||
Cloudflare Workers 原生支援 `WebAssembly.instantiate`,但 WASI preview1 需要手動實作 WASI imports。設計採用輕量 WASI shim 方案:
|
||||
|
||||
```typescript
|
||||
// WASI preview1 shim(只實作 stdin/stdout/stderr,其餘 syscall 回傳 ENOSYS)
|
||||
function createWasiImports(stdin: Uint8Array): {
|
||||
imports: WebAssembly.Imports;
|
||||
getStdout: () => Uint8Array;
|
||||
} {
|
||||
const stdoutChunks: Uint8Array[] = [];
|
||||
let stdinOffset = 0;
|
||||
|
||||
return {
|
||||
imports: {
|
||||
wasi_snapshot_preview1: {
|
||||
fd_write: (fd: number, iovs: number, iovs_len: number, nwritten: number) => { /* ... */ },
|
||||
fd_read: (fd: number, iovs: number, iovs_len: number, nread: number) => { /* ... */ },
|
||||
proc_exit: (code: number) => { throw new Error(`wasm exit: ${code}`); },
|
||||
// 其餘 syscall 回傳 ENOSYS(76)
|
||||
fd_seek: () => 76,
|
||||
fd_close: () => 0,
|
||||
environ_get: () => 0,
|
||||
environ_sizes_get: () => 0,
|
||||
args_get: () => 0,
|
||||
args_sizes_get: () => 0,
|
||||
clock_time_get: () => 0,
|
||||
random_get: (buf: number, buf_len: number) => { /* crypto.getRandomValues */ return 0; },
|
||||
},
|
||||
},
|
||||
getStdout: () => { /* 合併 stdoutChunks */ },
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
> **設計決策**:不使用 `@cloudflare/workers-wasi`(已停止維護)。改用自製輕量 WASI shim,只實作 `fd_read`/`fd_write`/`proc_exit`/`random_get`,其餘 syscall 回傳 `ENOSYS`。這足以支援 TinyGo/Rust/AssemblyScript 的 stdin/stdout 零件,且不引入外部依賴。
|
||||
|
||||
**執行流程:**
|
||||
|
||||
```
|
||||
1. 從 R2 取得 .wasm 二進位(ArrayBuffer)
|
||||
2. WebAssembly.compile(buffer) → WebAssembly.Module
|
||||
3. 建立 WASI imports shim(注入 stdin = JSON.stringify(input))
|
||||
4. WebAssembly.instantiate(module, imports)
|
||||
5. 呼叫 _start() 或 main()
|
||||
6. 從 stdout buffer 讀取輸出
|
||||
7. JSON.parse(stdout) → output
|
||||
```
|
||||
|
||||
**R2 快取策略:**
|
||||
- 第一次呼叫:從 R2 fetch `.wasm`,`WebAssembly.compile` 後快取 `WebAssembly.Module`(Worker 記憶體,跨請求共享)
|
||||
- 後續呼叫:直接用快取的 Module,只重新 instantiate(避免重複編譯)
|
||||
|
||||
### 3. Cypher Triplet Parser 擴展(`cypher-executor/src/actions/triplet-parser.ts`)
|
||||
|
||||
現有 parser 只支援 `PIPE / IF / FOREACH / CONTINUE`。需擴展支援新語意關係。
|
||||
|
||||
**新增 EdgeType:**
|
||||
|
||||
```typescript
|
||||
export type EdgeType =
|
||||
| 'PIPE' | 'IF' | 'FOREACH' | 'CONTINUE' // 現有
|
||||
| 'IS_A' | 'ON_SUCCESS' | 'ON_FAIL' // 新增:執行語意
|
||||
| 'ON_CLICK' | 'CALLS_SUBFLOW' // 新增:觸發語意
|
||||
| 'CONTAINS' | 'HAS_STYLE' | 'HAS_BEHAVIOR'; // 新增:結構語意
|
||||
```
|
||||
|
||||
**URI 協議解析:**
|
||||
|
||||
```typescript
|
||||
// 節點 componentId 解析
|
||||
function resolveComponentId(uri: string): {
|
||||
type: 'component' | 'workflow' | 'ui' | 'style';
|
||||
canonicalId: string;
|
||||
stability: 'floating' | 'stable' | 'pinned';
|
||||
pinnedVersion?: string;
|
||||
} {
|
||||
// component://validate_json@stable → { type: 'component', canonicalId: 'validate_json', stability: 'stable' }
|
||||
// component://validate_json@pinned:v1 → { type: 'component', canonicalId: 'validate_json', stability: 'pinned', pinnedVersion: 'v1' }
|
||||
// workflow://wf_save_to_db → { type: 'workflow', canonicalId: 'wf_save_to_db', stability: 'floating' }
|
||||
// ui://u6u-btn → { type: 'ui', canonicalId: 'u6u-btn', stability: 'floating' }
|
||||
}
|
||||
```
|
||||
|
||||
**ON_SUCCESS / ON_FAIL 執行語意:**
|
||||
|
||||
GraphExecutor 需要區分「節點執行成功」vs「節點執行失敗」,而非依賴 context 欄位:
|
||||
|
||||
```typescript
|
||||
// 在 executeNode 中,捕捉 try/catch 後分別走 ON_SUCCESS / ON_FAIL 邊
|
||||
case 'ON_SUCCESS':
|
||||
// 只在上游節點成功時執行
|
||||
if (!nodeError) {
|
||||
result = await this.executeNode(nextNode, ...);
|
||||
}
|
||||
break;
|
||||
case 'ON_FAIL':
|
||||
// 只在上游節點失敗時執行(接收 error context)
|
||||
if (nodeError) {
|
||||
result = await this.executeNode(nextNode, graph, { ...context, error: nodeError }, ...);
|
||||
}
|
||||
break;
|
||||
```
|
||||
|
||||
**CALLS_SUBFLOW 執行語意:**
|
||||
|
||||
```typescript
|
||||
case 'CALLS_SUBFLOW': {
|
||||
// 從 KBDB 載入子 Workflow 定義
|
||||
const subWorkflowId = nextNode.componentId!.replace('workflow://', '');
|
||||
const subGraph = await loadWorkflowFromKBDB(subWorkflowId, env);
|
||||
const subExecutor = new GraphExecutor(loader);
|
||||
const subResult = await subExecutor.execute(subGraph, result as Record<string, unknown>, kvNamespace);
|
||||
result = { ...(result as Record<string, unknown>), ...subResult.data as Record<string, unknown> };
|
||||
break;
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Web Components 零件庫(`u6u-core/web-components/`)
|
||||
|
||||
Web Components 以原生 Custom Elements API 實作,不依賴任何框架。
|
||||
|
||||
**`<u6u-btn>` 介面:**
|
||||
|
||||
```typescript
|
||||
// HTML attributes
|
||||
interface U6uBtnAttributes {
|
||||
label: string; // 顯示文字
|
||||
color?: string; // 主題色(CSS custom property)
|
||||
tooltip?: string; // 滑鼠懸停提示(純靜態)
|
||||
workflow?: string; // workflow://id
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
// 發出的自訂事件
|
||||
interface U6uTriggerEvent extends CustomEvent {
|
||||
detail: {
|
||||
workflowId: string;
|
||||
payload: Record<string, unknown>;
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
**`<u6u-card>` Smart Container 邏輯:**
|
||||
|
||||
```typescript
|
||||
// u6u-card 攔截子元件的 u6u:trigger 事件
|
||||
// 收集同容器內所有 u6u-text-input / u6u-text-field 的值
|
||||
// 合併至 payload 後再向上冒泡
|
||||
connectedCallback() {
|
||||
this.addEventListener('u6u:trigger', (e: Event) => {
|
||||
const trigger = e as CustomEvent;
|
||||
e.stopPropagation();
|
||||
|
||||
const inputs = this.querySelectorAll('u6u-text-input, u6u-text-field');
|
||||
const collected: Record<string, unknown> = {};
|
||||
inputs.forEach(input => {
|
||||
const name = input.getAttribute('name');
|
||||
const value = (input as any).value;
|
||||
if (name) collected[name] = value;
|
||||
});
|
||||
|
||||
this.dispatchEvent(new CustomEvent('u6u:trigger', {
|
||||
bubbles: true,
|
||||
composed: true,
|
||||
detail: {
|
||||
...trigger.detail,
|
||||
payload: { ...trigger.detail.payload, ...collected },
|
||||
},
|
||||
}));
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
### 5. 雙面翻轉畫布(`inkstone-admin/frontend/web/`)
|
||||
|
||||
畫布本身用 React 19 + Web Components 組裝,體現 dogfooding 原則。
|
||||
|
||||
**翻轉狀態機:**
|
||||
|
||||
```mermaid
|
||||
stateDiagram-v2
|
||||
[*] --> UIView: 初始狀態
|
||||
UIView --> LogicView: 點擊翻面按鈕
|
||||
LogicView --> UIView: 點擊翻面按鈕
|
||||
LogicView --> Editing: 修改三元組
|
||||
Editing --> Saving: 確認儲存
|
||||
Saving --> LogicView: KBDB 寫入成功
|
||||
Saving --> LogicView: KBDB 寫入失敗(顯示錯誤)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Data Models
|
||||
|
||||
### Component Contract YAML(完整規格)
|
||||
|
||||
```yaml
|
||||
canonical_id: "validate_json" # 正規化功能名稱(永久不變,Registry AI 正規化後確認)
|
||||
display_name: "JSON 格式驗證器" # 建立者自取,顯示用
|
||||
category: "logic" # logic | api | ui | style | anim
|
||||
version: "v1" # 實作版本
|
||||
wasi_target: "preview1" # WASM 目標格式
|
||||
stability: "floating" # floating | stable | pinned
|
||||
|
||||
runtime_compat:
|
||||
- "cf-workers"
|
||||
- "workerd"
|
||||
- "wazero"
|
||||
|
||||
constraints:
|
||||
max_size_kb: 2048
|
||||
max_cold_start_ms: 50
|
||||
no_network_syscall: true
|
||||
no_filesystem_syscall: true
|
||||
io_model: "stdin_stdout_json" # 唯一合法值
|
||||
|
||||
input_schema:
|
||||
type: object
|
||||
required: ["json_string"]
|
||||
properties:
|
||||
json_string:
|
||||
type: string
|
||||
description: "待驗證的 JSON 字串"
|
||||
|
||||
output_schema:
|
||||
type: object
|
||||
properties:
|
||||
valid:
|
||||
type: boolean
|
||||
error:
|
||||
type: string
|
||||
description: "驗證失敗時的錯誤訊息"
|
||||
|
||||
gherkin_tests:
|
||||
- scenario: "合法 JSON 通過驗證"
|
||||
given: '{"json_string":"{\"key\":\"value\"}"}'
|
||||
then_contains: '{"valid":true}'
|
||||
- scenario: "非法 JSON 回傳錯誤"
|
||||
given: '{"json_string":"not-json"}'
|
||||
then_contains: '{"valid":false,"error":'
|
||||
|
||||
tags: ["validation", "json", "utility"]
|
||||
description: "驗證輸入字串是否為合法 JSON 格式"
|
||||
```
|
||||
|
||||
### 零件開發語言決策
|
||||
|
||||
**內建零件使用 TinyGo**(純邏輯零件)和 TinyGo + `json.RawMessage`(需要任意 HTTP body 的零件)。不引入 Rust 作為內建零件語言。
|
||||
|
||||
**用戶自建零件支援三種語言,按難度分層:**
|
||||
|
||||
| 語言 | 目標用戶 | JSON 能力 | 備註 |
|
||||
|---|---|---|---|
|
||||
| **AssemblyScript** | 一般用戶(TS 背景) | 社群套件 `assemblyscript-json`,支援動態 JSON | 語法最接近 TS,門檻最低;靜默錯誤風險,沙盒驗收必須通過 |
|
||||
| **TinyGo** | 技術較強用戶(Go 背景) | 靜態 struct 完整支援;`json.RawMessage` 處理任意 body | 編譯期報錯,AI 生成安全性較高 |
|
||||
| **Rust** | 進階用戶 | `serde_json::Value` 完整動態 JSON | 生態最成熟,體積最小;學習曲線陡 |
|
||||
|
||||
**`/components/guide` 端點提供三份語言範例**,用戶根據自身背景選擇。
|
||||
|
||||
**內建零件 JSON 策略(TinyGo):**
|
||||
|
||||
```go
|
||||
// 固定 schema 零件(google-sheets、gmail 等)→ 靜態 struct
|
||||
type Input struct {
|
||||
SpreadsheetId string `json:"spreadsheet_id"`
|
||||
Range string `json:"range"`
|
||||
AccessToken string `json:"access_token"`
|
||||
}
|
||||
|
||||
// 任意 body 零件(http-request)→ json.RawMessage 傳遞 raw bytes,不解析
|
||||
type Input struct {
|
||||
URL string `json:"url"`
|
||||
Method string `json:"method"`
|
||||
Body json.RawMessage `json:"body"` // 任意 JSON,不解析
|
||||
}
|
||||
```
|
||||
|
||||
### Workflow Cypher 三元組(完整語法)
|
||||
|
||||
```yaml
|
||||
kind: Workflow
|
||||
id: wf_submit_form
|
||||
|
||||
triplets:
|
||||
# 節點類型宣告
|
||||
- "btn_submit >> IS_A >> ui://u6u-btn"
|
||||
- "step_validate >> IS_A >> component://validate_json"
|
||||
- "step_save >> IS_A >> component://kbdb_write"
|
||||
|
||||
# 前端觸發後端
|
||||
- "btn_submit >> ON_CLICK >> step_validate"
|
||||
|
||||
# 成功/失敗分支
|
||||
- "step_validate >> ON_SUCCESS >> step_save"
|
||||
- "step_validate >> ON_FAIL >> step_notify_error"
|
||||
|
||||
# 子流程呼叫
|
||||
- "step_save >> ON_SUCCESS >> CALLS_SUBFLOW >> workflow://wf_notify_user"
|
||||
|
||||
# 容器結構
|
||||
- "card_main >> CONTAINS >> btn_submit"
|
||||
- "card_main >> CONTAINS >> input_name"
|
||||
```
|
||||
|
||||
### Evaluation Block(KBDB tpl-evaluation)
|
||||
|
||||
每次 Workflow 執行後,Evaluator Agent 寫入一個 Evaluation Block:
|
||||
|
||||
| Slot key | 說明 |
|
||||
|---|---|
|
||||
| `run_id` | 執行唯一 ID |
|
||||
| `workflow_id` | Workflow ID |
|
||||
| `component_id` | 被評價的零件 ID |
|
||||
| `verdict` | `success` / `failed` / `timeout` |
|
||||
| `duration_ms` | 執行時間 |
|
||||
| `error_message` | 失敗訊息(可選) |
|
||||
| `evaluated_at` | 評價時間戳記 |
|
||||
|
||||
### Pitfall Block(KBDB tpl-pitfall)
|
||||
|
||||
| Slot key | 說明 |
|
||||
|---|---|
|
||||
| `component_id` | 問題零件 ID |
|
||||
| `failure_pattern` | 失敗模式描述 |
|
||||
| `first_seen_at` | 首次發現時間戳記 |
|
||||
| `occurrence_count` | 發生次數 |
|
||||
|
||||
---
|
||||
|
||||
## Correctness Properties
|
||||
|
||||
*A property is a characteristic or behavior that should hold true across all valid executions of a system — essentially, a formal statement about what the system should do. Properties serve as the bridge between human-readable specifications and machine-verifiable correctness guarantees.*
|
||||
|
||||
|
||||
### Property Reflection(去重分析)
|
||||
|
||||
在寫出最終屬性前,先做冗餘分析:
|
||||
|
||||
- **1.1 + 1.2**:都是合約欄位完整性驗證,合併為 Property 1「合約格式完整性」
|
||||
- **2.1 + 2.2**:驗收流程執行 + 失敗回報,合併為 Property 2「沙盒驗收流程正確性」
|
||||
- **2.3 + 2.4**:提交後可讀取 + 冪等提交,合併為 Property 3「零件提交冪等性與持久性」
|
||||
- **3.1 + 3.5**:WASM 執行 + 雙模式路由,合併為 Property 4「Component Dispatcher 路由正確性」
|
||||
- **3.4 + 6.4**:不相容 Tier 回傳錯誤 + 結構化錯誤,合併為 Property 5「Dispatcher 錯誤結構完整性」
|
||||
- **4.1**:URI 解析 round-trip,獨立為 Property 6
|
||||
- **4.2 + 4.4**:版本選擇算法(floating 最高分 + pinned 固定版本),合併為 Property 7「版本選擇策略正確性」
|
||||
- **4.5**:版本保留不變量,獨立為 Property 8
|
||||
- **5.7**:Confluence 屬性,獨立為 Property 9
|
||||
- **8.3 + 8.4 + 8.5**:Web Components 事件與渲染,合併為 Property 10「Web Components 事件與渲染冪等性」
|
||||
- **10.6 + 12.5**:評價冪等性 + 查詢冪等性,合併為 Property 11「系統操作冪等性」
|
||||
|
||||
最終保留 11 個屬性,每個提供獨立驗證價值。
|
||||
|
||||
---
|
||||
|
||||
### Property 1: 合約格式完整性
|
||||
|
||||
*For any* component contract object,若缺少任何必填欄位(`id`、`version`、`wasi_target`、`stability`、`runtime_compat`、`constraints.max_size_kb`、`constraints.max_cold_start_ms`、`constraints.no_network_syscall`、`constraints.io_model`、`input_schema`、`output_schema`、`gherkin_tests`),合約驗證器 SHALL 拒絕該合約並回傳包含缺失欄位名稱的錯誤;反之,包含所有必填欄位的合約 SHALL 通過格式驗證。
|
||||
|
||||
**Validates: Requirements 1.1, 1.2, 1.4**
|
||||
|
||||
---
|
||||
|
||||
### Property 2: 沙盒驗收流程正確性
|
||||
|
||||
*For any* 提交的零件(.wasm + contract),若零件在驗收步驟 N 失敗,Component_Registry 的回應 SHALL 包含步驟 N 的名稱與具體失敗原因,且不執行步驟 N+1 之後的步驟。
|
||||
|
||||
**Validates: Requirements 2.1, 2.2**
|
||||
|
||||
---
|
||||
|
||||
### Property 3: 零件提交冪等性與持久性
|
||||
|
||||
*For any* 通過驗收的零件(id, version),提交後從 Component_Registry 讀取該零件的合約,所有欄位值 SHALL 與提交時的合約完全一致(序列化 round-trip);對相同 (id, version) 重複提交 N 次,KBDB 中 SHALL 只存在一個對應的 Block。
|
||||
|
||||
**Validates: Requirements 2.3, 2.4**
|
||||
|
||||
---
|
||||
|
||||
### Property 4: Component Dispatcher 路由正確性
|
||||
|
||||
*For any* 零件合約,若 `io_model = "stdin_stdout_json"`,Component_Dispatcher SHALL 使用 WASM 執行路徑,將 input JSON 寫入 stdin,從 stdout 讀取 output JSON;若 `io_model = "http_endpoint"`,SHALL 使用 HTTP 路徑。對任意合法 JSON input,WASM 執行路徑的輸出 SHALL 與 HTTP 執行路徑的輸出語意等效。
|
||||
|
||||
**Validates: Requirements 3.1, 3.5, 3.6**
|
||||
|
||||
---
|
||||
|
||||
### Property 5: Dispatcher 錯誤結構完整性
|
||||
|
||||
*For any* (component_id, tier) 組合,若該零件的 `runtime_compat` 不包含當前 tier,Component_Dispatcher 的錯誤回應 SHALL 同時包含:零件 id、當前 tier 名稱、已嘗試的呼叫路徑清單,三個欄位缺一不可。
|
||||
|
||||
**Validates: Requirements 3.4, 6.4**
|
||||
|
||||
---
|
||||
|
||||
### Property 6: 零件 URI 解析 Round-Trip
|
||||
|
||||
*For any* 合法的零件 URI 字串(格式為 `component://id`、`component://id@stable`、`component://id@pinned:vN`),解析後再重新序列化 SHALL 產生與原始 URI 語意等效的字串;解析出的 `id`、`stability`、`pinnedVersion` 欄位 SHALL 與原始 URI 中的對應部分完全一致。
|
||||
|
||||
**Validates: Requirements 4.1**
|
||||
|
||||
---
|
||||
|
||||
### Property 7: 版本選擇策略正確性
|
||||
|
||||
*For any* 零件 id 下的版本集合(每個版本有 success_rate、avg_duration_ms、call_count 評分),當 stability = `floating` 時,Component_Dispatcher SHALL 選取「成功率 × 速度評分 × 被調用次數」最高的版本;當 stability = `pinned:vN` 時,無論版本集合中其他版本的評分如何,SHALL 永遠選取版本 vN。
|
||||
|
||||
**Validates: Requirements 4.2, 4.4**
|
||||
|
||||
---
|
||||
|
||||
### Property 8: 歷史版本永久保留不變量
|
||||
|
||||
*For any* 已上架的零件版本(id, version),無論該版本後來被標記為 `deprecated` 或 `tombstone`,其 `.wasm` 二進位 SHALL 永遠可從 R2 讀取,且 `pinned` 引用 SHALL 永遠能透過 Component_Dispatcher 執行該版本。
|
||||
|
||||
**Validates: Requirements 4.5, 10.5**
|
||||
|
||||
---
|
||||
|
||||
### Property 9: Cypher 三元組解析 Confluence(順序無關性)
|
||||
|
||||
*For any* 合法的 Cypher 三元組集合,無論三元組在輸入陣列中的排列順序如何,`parseTriplets` 產生的執行圖(節點集合、邊集合、拓撲結構)SHALL 語意等效——即相同的節點 id 集合、相同的 (from, to, type) 邊集合。
|
||||
|
||||
**Validates: Requirements 5.7**
|
||||
|
||||
---
|
||||
|
||||
### Property 10: Web Components 事件與渲染冪等性
|
||||
|
||||
*For any* workflow URI 設定於 `<u6u-btn>` 的 `workflow` attribute,使用者點擊後發出的 `u6u:trigger` 事件 detail 中的 `workflowId` SHALL 與 URI 中的 id 完全一致;*For any* 一組具名 `<u6u-text-input>` 元件置於 `<u6u-card>` 內,觸發事件後收集到的 payload SHALL 包含所有輸入元件的 name-value 對;*For any* attribute 值,對同一 Web Component 設定相同 attribute 值 N 次,渲染結果 SHALL 與設定一次相同(冪等渲染)。
|
||||
|
||||
**Validates: Requirements 8.3, 8.4, 8.5**
|
||||
|
||||
---
|
||||
|
||||
### Property 11: 系統操作冪等性
|
||||
|
||||
*For any* Workflow 執行日誌(run_id),Evaluator_Agent 對相同 run_id 處理 N 次,KBDB 中 SHALL 只存在一個對應的 Evaluation Block,不產生重複記錄;*For any* Component_Registry 讀取操作的查詢參數,在 KBDB 資料不變的前提下,對相同參數呼叫 N 次 SHALL 回傳完全相同的結果。
|
||||
|
||||
**Validates: Requirements 10.6, 12.5**
|
||||
|
||||
---
|
||||
|
||||
## Error Handling
|
||||
|
||||
### Component Dispatcher 錯誤分類
|
||||
|
||||
| 錯誤類型 | 觸發條件 | 回應格式 |
|
||||
|---|---|---|
|
||||
| `COMPONENT_NOT_FOUND` | KBDB 中找不到零件 | `{ error: "COMPONENT_NOT_FOUND", component_id, tier }` |
|
||||
| `RUNTIME_INCOMPATIBLE` | runtime_compat 不含當前 Tier | `{ error: "RUNTIME_INCOMPATIBLE", component_id, tier, attempted_paths: [] }` |
|
||||
| `WASM_EXECUTION_TIMEOUT` | 超過 max_cold_start_ms | `{ error: "WASM_EXECUTION_TIMEOUT", component_id, timeout_ms }` |
|
||||
| `WASM_INVALID_OUTPUT` | stdout 不是合法 JSON | `{ error: "WASM_INVALID_OUTPUT", component_id, raw_output }` |
|
||||
| `WASM_SYSCALL_VIOLATION` | .wasm 嘗試網路/檔案 syscall | `{ error: "WASM_SYSCALL_VIOLATION", component_id, syscall_name }` |
|
||||
| `CONTRACT_VALIDATION_FAILED` | 合約格式不合規 | `{ error: "CONTRACT_VALIDATION_FAILED", missing_fields: [] }` |
|
||||
|
||||
### 沙盒驗收失敗回應格式
|
||||
|
||||
```json
|
||||
{
|
||||
"success": false,
|
||||
"failed_step": "syscall_scan",
|
||||
"reason": "發現禁止的 syscall:sock_connect",
|
||||
"guide_anchor": "#syscall-constraints",
|
||||
"component_id": "my_component",
|
||||
"version": "v1"
|
||||
}
|
||||
```
|
||||
|
||||
### Tier 3 離線錯誤處理
|
||||
|
||||
Tier 3 在離線環境中,所有無法執行的操作都寫入 DTN 佇列,不拋出錯誤:
|
||||
|
||||
```go
|
||||
// Go 排程引擎的錯誤處理策略
|
||||
type DTNEntry struct {
|
||||
Type string // "missing_component" | "sync_log" | "request_wasm"
|
||||
Payload json.RawMessage
|
||||
CreatedAt time.Time
|
||||
RetryCount int
|
||||
}
|
||||
```
|
||||
|
||||
### Web Components 錯誤邊界
|
||||
|
||||
`<u6u-btn>` 在 `workflow` attribute 未設定時,點擊不發出事件,僅在 console 輸出警告:
|
||||
|
||||
```
|
||||
[u6u-btn] workflow attribute is not set, click event ignored
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Testing Strategy
|
||||
|
||||
### 雙軌測試策略
|
||||
|
||||
本功能採用「單元測試 + 屬性測試」雙軌策略:
|
||||
|
||||
- **單元測試(Vitest)**:驗證具體範例、邊界條件、錯誤情境
|
||||
- **屬性測試(fast-check)**:驗證上述 11 個 Correctness Properties,每個屬性最少執行 100 次迭代
|
||||
|
||||
### 屬性測試配置
|
||||
|
||||
使用 `fast-check`(已在 tech stack 中),每個屬性測試標記格式:
|
||||
|
||||
```typescript
|
||||
// Feature: u6u-platform-evolution, Property N: {property_text}
|
||||
it.prop([fc.record({ id: fc.string(), version: fc.string(), ... })])(
|
||||
'Property 1: 合約格式完整性',
|
||||
(contract) => {
|
||||
// ...
|
||||
},
|
||||
{ numRuns: 100 }
|
||||
);
|
||||
```
|
||||
|
||||
### 各 Phase 測試重點
|
||||
|
||||
**Phase 0(WASM 執行核心):**
|
||||
- Property 4:WASM 執行路徑 round-trip(`validate_json.wasm` 作為 ground truth)
|
||||
- Property 1:合約格式驗證
|
||||
- 單元測試:WASI shim 的 `fd_read`/`fd_write` 正確性
|
||||
|
||||
**Phase 1(零件遷移):**
|
||||
- Property 3:提交冪等性(20 個零件各提交兩次,驗證無重複)
|
||||
- Property 2:沙盒驗收流程(各步驟失敗案例)
|
||||
- Property 8:歷史版本保留(deprecate 後仍可讀取)
|
||||
|
||||
**Phase 2(Cypher 擴展):**
|
||||
- Property 9:Confluence(三元組順序無關性,fast-check shuffle)
|
||||
- Property 6:URI 解析 round-trip
|
||||
- Property 7:版本選擇策略(floating 最高分、pinned 固定版本)
|
||||
- Property 5:錯誤結構完整性
|
||||
|
||||
**Phase 3(前端畫布):**
|
||||
- Property 10:Web Components 事件與渲染冪等性(`@testing-library/react` + fast-check)
|
||||
- Property 11:評價冪等性(Evaluator Agent 重複處理)
|
||||
|
||||
### 整合測試
|
||||
|
||||
以下場景使用整合測試(1-3 個具體範例,不用 PBT):
|
||||
|
||||
- Tier 1 CF Workers 環境中實際執行 `validate_json.wasm`(驗證 WASM 在 Workers 環境可運行)
|
||||
- KBDB tpl-component Template 建立與 Slot 讀寫(驗證 KBDB 整合)
|
||||
- R2 `.wasm` 上傳與讀取(驗證 R2 整合)
|
||||
- Vectorize 語意搜尋(驗證「查詢 Google Sheets 資料」能找到 `gsheets_get_entries`)
|
||||
|
||||
### 單元測試重點(非 PBT)
|
||||
|
||||
- WASI shim:`fd_read` 正確讀取 stdin、`fd_write` 正確寫入 stdout
|
||||
- `evaluateCondition`:現有條件評估函數的邊界案例
|
||||
- `resolveComponentId`:URI 解析的邊界案例(空字串、特殊字元)
|
||||
- `<u6u-card>` Smart Container:巢狀容器的事件冒泡行為
|
||||
@@ -0,0 +1,219 @@
|
||||
# Requirements Document
|
||||
|
||||
## Introduction
|
||||
|
||||
u6u 平台演進規格描述從現況(HTTP endpoint 零件、單一 Cloudflare 部署、無前端畫布)
|
||||
到目標架構(WASM 零件模型、三層物理部署、雙面翻轉畫布)的完整演進路徑。
|
||||
|
||||
本規格涵蓋三個核心演進軸:
|
||||
|
||||
1. **零件模型遷移**:將 20 個內建零件從 Cloudflare Worker HTTP endpoint 遷移至 WASI preview1 `.wasm` 格式,附帶 `component.contract.yaml`,以 stdin/stdout JSON 作為唯一 I/O 模型。
|
||||
2. **多 Tier 執行抽象**:讓 Cypher Executor 透過統一的 Component Dispatcher 介面,跨 Tier 1(Cloudflare Workers)、Tier 2(workerd 地端叢集)、Tier 3(Go + Wazero 邊緣載具)呼叫零件。
|
||||
3. **前端雙面畫布**:建立以 Web Components 為基礎的視覺化畫布,正面為 UI 視圖,反面為 Cypher 邏輯視圖,智慧容器自動打包表單值。
|
||||
|
||||
---
|
||||
|
||||
## Glossary
|
||||
|
||||
- **Component(零件)**:系統最小執行單元,一個零件只做一件事,以 `.wasm`(WASI preview1)或 Web Component 形式存在。
|
||||
- **Component_Contract**:每個零件附帶的 `component.contract.yaml`,定義 id、version、wasi_target、stability、runtime_compat、constraints、input_schema、output_schema、gherkin_tests。
|
||||
- **Component_Registry**:KBDB 中儲存所有零件合約與 `.wasm` 位置的索引,以 `tpl-component` Template Block 實作。
|
||||
- **Component_Dispatcher**:Cypher Executor 內部的路由層,根據零件的 `runtime_compat` 與目標 Tier 決定呼叫路徑(Service Binding / workerd HTTP / Wazero IPC)。
|
||||
- **Cypher_Executor**:Workflow 執行引擎,解析三元組語法,透過 GraphExecutor 執行節點,現部署於 Cloudflare Workers。
|
||||
- **Cypher_Triplet**:`"A >> 關係 >> B"` 格式的三元組,描述節點間的語意關係。
|
||||
- **KBDB**:三位一體記憶庫(blocks / templates / slots),搭配 Cloudflare Vectorize,是平台唯一的持久化狀態來源。
|
||||
- **Tier_1**:雲端層,Cloudflare Workers + D1 + Vectorize + R2,全球無伺服器部署。
|
||||
- **Tier_2**:企業地端層,workerd 叢集 + Kùzu 或 PostgreSQL + AGE,高機密內網環境。
|
||||
- **Tier_3**:邊緣載具層,Go 排程引擎 + 內嵌 Wazero + SQLite,無 V8、無網路的極限環境(無人機、AGV)。
|
||||
- **WASI_Preview1**:WebAssembly System Interface preview1 規格,零件唯一合法的 WASM 目標格式。
|
||||
- **Canvas(畫布)**:前端雙面翻轉介面,正面為 UI 視圖,反面為 Cypher 邏輯視圖。
|
||||
- **Smart_Container**:畫布上的排版容器(如 `<u6u-card>`),自動打包同容器內所有輸入元件的值並附加至觸發事件的 payload。
|
||||
- **Forge_AI**:工匠 AI,負責在 Tier 2 地端接收零件規格、生成 TinyGo 程式碼、編譯並測試 `.wasm`。
|
||||
- **Evaluator_Agent**:強制評價代理,每次 Workflow 執行後自動評估成功率、效能、警告訊息。
|
||||
- **Pitfall_Block**:KBDB 中記錄已知問題的 Block,AI 搜尋時強制讀取以繞道。
|
||||
- **Stability_Tag**:零件版本穩定性標籤,值為 `floating`(AI 自動選最優)、`stable`(人工確認才換)、`pinned`(版本凍結)。
|
||||
- **DTN**:Delay-Tolerant Networking,Tier 3 邊緣載具在間歇性網路下的短點射傳輸協議。
|
||||
|
||||
---
|
||||
|
||||
## Requirements
|
||||
|
||||
### Requirement 1:零件合約規格標準化
|
||||
|
||||
**User Story:** As a 平台架構師, I want 每個零件都有標準化的 `component.contract.yaml` 合約, so that AI 能透過統一介面讀取零件能力,並在任何 Tier 上驗證相容性。
|
||||
|
||||
#### Acceptance Criteria
|
||||
|
||||
1. THE Component_Contract SHALL 包含以下必填欄位:`canonical_id`(功能合約名稱,永久不變)、`display_name`(人類可讀名稱,可自由撰寫)、`description`(語意搜尋用途,需精確描述零件能做什麼、適用情境,至少 20 字)、`version`(實作版本)、`wasi_target`(值為 `"preview1"`)、`stability`(值為 `floating`、`stable` 或 `pinned` 之一)、`runtime_compat`(陣列,值為 `cf-workers`、`workerd`、`wazero` 的子集)、`constraints`、`input_schema`、`output_schema`、`gherkin_tests`。
|
||||
1a. THE `canonical_id` SHALL 遵循以下命名規範,以確保全庫一致性:
|
||||
- 格式:`{scope}_{action}` 或 `{scope}_{object}` 或 `{scope}`(單詞),全部小寫底線,不超過 4 個單詞
|
||||
- **整合類**(category: api):以服務名稱為 scope,可加動作;範例:`gmail`、`gmail_send`、`google_sheets`、`google_sheets_append`、`telegram`、`telegram_send`
|
||||
- **資料處理類**(category: data):以資料型別或操作為 scope;範例:`string_ops`、`array_ops`、`date_ops`、`json_transform`
|
||||
- **控制流類**(category: logic):以控制結構名稱命名;範例:`if_control`、`foreach_control`、`try_catch`、`switch`、`wait`
|
||||
- **AI 類**(category: ai):以 `ai_` 為前綴;範例:`ai_transform_compile`、`ai_summarize`、`ai_classify`
|
||||
- 禁止:中文、空格、大寫、連字號(`-`)、版本號混入 id(`gmail_v2` 用 `version: v2` 表達)
|
||||
1b. THE `display_name` SHALL 為人類可讀的自由格式名稱(可中文、可含空格);此欄位供 UI 顯示用,不作為系統識別符。範例:`canonical_id: google_sheets_append` 配 `display_name: "Google Sheets — 新增一列"`。
|
||||
1c. THE `description` SHALL 用於 Vectorize 語意搜尋索引;撰寫時應以「能做什麼、適合什麼情境」為核心,避免只寫零件名稱的同義詞。範例:`"傳送 Gmail 電子郵件,適合 Workflow 完成時通知使用者、訂閱確認信、錯誤警報等場景。需要 Gmail OAuth token。"` 而非 `"Gmail 發信零件"`。
|
||||
2. THE Component_Contract SHALL 在 `constraints` 中包含以下欄位:`max_size_kb`(上限 2048)、`max_cold_start_ms`(上限 50)、`no_network_syscall`(布林值)、`io_model`(值為 `"stdin_stdout_json"`)。
|
||||
3. WHEN 一個零件的 `id` 已存在於 Component_Registry,THE Component_Registry SHALL 允許以新 `version` 值新增該零件的新實作,而不覆蓋舊版本。
|
||||
4. THE Component_Contract SHALL 在 `gherkin_tests` 中至少包含一個正常情境(happy path)與一個錯誤情境(error path)的測試案例。
|
||||
5. IF 一個零件的 `input_schema` 或 `output_schema` 涉及序列化或反序列化操作,THEN THE Component_Contract SHALL 包含一個 round-trip 測試案例,驗證 `parse(format(x)) == x`。
|
||||
|
||||
---
|
||||
|
||||
### Requirement 2:零件沙盒驗收流程
|
||||
|
||||
**User Story:** As a 零件提交者(AI 或開發者), I want 提交的零件自動通過沙盒驗收, so that 只有符合品質標準的零件才能進入零件宇宙。
|
||||
|
||||
#### Acceptance Criteria
|
||||
|
||||
1. WHEN 一個零件被提交至 Component_Registry,THE Component_Registry SHALL 依序執行以下驗收步驟:(a)體積檢查(`.wasm` 小於 `max_size_kb`)、(b)冷啟動時間測量(小於 `max_cold_start_ms`)、(c)syscall 掃描(不含網路或檔案系統 syscall)、(d)Gherkin 測試執行(所有 scenario 100% 通過)、(e)多 runtime 相容測試(`runtime_compat` 列出的所有 runtime)。
|
||||
2. IF 任一驗收步驟失敗,THEN THE Component_Registry SHALL 拒絕該零件上架,並回傳包含失敗步驟名稱與具體原因的錯誤訊息。
|
||||
3. WHEN 所有驗收步驟通過,THE Component_Registry SHALL 將零件合約存入 KBDB 的 `tpl-component` Template Block,並記錄上架時間戳記。
|
||||
4. THE Component_Registry SHALL 以冪等方式執行驗收流程,對相同 `id` 與 `version` 的重複提交回傳相同結果而不重複執行測試。
|
||||
|
||||
---
|
||||
|
||||
### Requirement 3:現有 HTTP 零件遷移至 WASM
|
||||
|
||||
**User Story:** As a 平台開發者, I want 將現有 20 個 HTTP endpoint 零件遷移為 WASI preview1 `.wasm` 格式, so that 零件能在 Tier 3 邊緣載具(無 V8、無網路)上執行,消除技術債。
|
||||
|
||||
#### Acceptance Criteria
|
||||
|
||||
1. THE Component_Dispatcher SHALL 支援以 WASM 模式呼叫零件:讀取 `.wasm` 二進位、透過 WASI preview1 runtime 執行、將 input JSON 寫入 stdin、從 stdout 讀取 output JSON。
|
||||
2. WHEN 一個 WASM 零件需要呼叫外部 HTTP API(如 Google Sheets),THE Component_Dispatcher SHALL 透過 host function 注入方式提供網路能力,而非允許 `.wasm` 內部直接發出網路 syscall。
|
||||
3. THE Component_Dispatcher SHALL 在 Tier 1(Cloudflare Workers)環境中,以 `workerd` 內建的 WASM 執行能力執行 WASI preview1 零件。
|
||||
4. WHEN 一個零件的 `runtime_compat` 不包含當前執行環境的 Tier,THE Component_Dispatcher SHALL 回傳錯誤,說明該零件不相容於當前 Tier,而非嘗試執行。
|
||||
5. THE Component_Dispatcher SHALL 在遷移期間同時支援舊有 HTTP endpoint 模式(Service Binding 或外部 URL)與新 WASM 模式,以 Component_Contract 的 `io_model` 欄位區分呼叫路徑。
|
||||
6. FOR ALL 現有 20 個內建零件,遷移後的 WASM 版本 SHALL 通過與原 HTTP 版本相同的 Gherkin 測試案例(round-trip 等效性)。
|
||||
|
||||
---
|
||||
|
||||
### Requirement 4:零件版本控制與穩定性標籤
|
||||
|
||||
**User Story:** As a Workflow 設計者, I want 在 Cypher 三元組中指定零件的穩定性需求, so that 關鍵業務流程不會因 AI 自動升級零件而中斷。
|
||||
|
||||
#### Acceptance Criteria
|
||||
|
||||
1. THE Cypher_Executor SHALL 支援以下三種零件引用語法:`component://id`(預設 floating)、`component://id@stable`、`component://id@pinned:vN`。
|
||||
2. WHEN 一個 Workflow 引用 `component://id`(floating),THE Component_Dispatcher SHALL 從 Component_Registry 選取該 `id` 下「成功率 × 速度 × 被調用次數」評分最高的版本執行。
|
||||
3. WHEN 一個 Workflow 引用 `component://id@stable`,THE Component_Dispatcher SHALL 使用當前標記為 stable 的版本,並在有更優版本時記錄提示至 KBDB,但不自動切換。
|
||||
4. WHEN 一個 Workflow 引用 `component://id@pinned:vN`,THE Component_Dispatcher SHALL 永遠使用版本 `vN`,即使該版本已被標記為 Deprecated。
|
||||
5. THE Component_Registry SHALL 保留所有歷史版本的 `.wasm` 二進位,不因版本淘汰而刪除檔案。
|
||||
|
||||
---
|
||||
|
||||
### Requirement 5:Cypher 語意關係擴展
|
||||
|
||||
**User Story:** As a Workflow 設計者, I want Cypher 三元組支援完整的語意關係集合, so that 能描述條件分支、子流程呼叫、前端觸發等複雜業務邏輯。
|
||||
|
||||
#### Acceptance Criteria
|
||||
|
||||
1. THE Cypher_Executor SHALL 解析並執行以下語意關係:`IS_A`(節點類型宣告)、`ON_SUCCESS`(成功後繼)、`ON_FAIL`(失敗後繼)、`ON_CLICK`(前端點擊觸發)、`CALLS_SUBFLOW`(呼叫子 Workflow)、`CONTAINS`(容器包含關係)、`HAS_STYLE`(樣式關聯)、`HAS_BEHAVIOR`(行為關聯)。
|
||||
2. WHEN 解析 `IS_A` 關係,THE Cypher_Executor SHALL 從 Component_Registry 載入對應的零件合約,並以合約的 `input_schema` 驗證節點的輸入 context。
|
||||
3. WHEN 解析 `ON_SUCCESS` 或 `ON_FAIL` 關係,THE Cypher_Executor SHALL 根據上游節點的執行結果(成功或拋出錯誤)決定走向,而非依賴 context 中的特定欄位。
|
||||
4. WHEN 解析 `CALLS_SUBFLOW` 關係,THE Cypher_Executor SHALL 以當前 context 作為子 Workflow 的 initialContext 執行,並將子 Workflow 的輸出合併回主流程 context。
|
||||
5. WHEN 解析 `ON_CLICK` 關係,THE Cypher_Executor SHALL 接受來自前端 Smart_Container 打包的 payload,並以該 payload 作為 Workflow 的 initialContext。
|
||||
6. THE Cypher_Executor SHALL 支援 URI 協議前綴:`component://`(零件引用)、`workflow://`(Workflow 引用)、`ui://`(前端零件引用)、`style://`(樣式零件引用)。
|
||||
7. FOR ALL 合法的 Cypher 三元組序列,THE Cypher_Executor SHALL 保證解析結果的冪等性:對相同輸入三元組集合,無論排列順序,產生語意等效的執行圖(Confluence 屬性)。
|
||||
|
||||
---
|
||||
|
||||
### Requirement 6:Component Dispatcher 多 Tier 路由
|
||||
|
||||
**User Story:** As a 平台架構師, I want Cypher Executor 透過統一的 Component Dispatcher 介面呼叫跨 Tier 零件, so that Workflow 設計者不需要知道零件部署在哪個 Tier。
|
||||
|
||||
#### Acceptance Criteria
|
||||
|
||||
1. THE Component_Dispatcher SHALL 根據以下優先序決定呼叫路徑:(1)Tier 1:Cloudflare Service Binding(若零件部署為 Worker)或 WASM 直接執行;(2)Tier 2:workerd 叢集 HTTP endpoint;(3)Tier 3:Wazero IPC(stdin/stdout)。
|
||||
2. WHEN Component_Dispatcher 在 Tier 1 環境中呼叫一個 `runtime_compat` 包含 `cf-workers` 的零件,THE Component_Dispatcher SHALL 優先使用 Cloudflare Service Binding,若 binding 不存在則退回 WASM 執行模式。
|
||||
3. WHEN Component_Dispatcher 在 Tier 3 環境中呼叫零件,THE Component_Dispatcher SHALL 只使用 Wazero 執行本地 `.wasm` 檔案,不發出任何網路請求。
|
||||
4. IF Component_Dispatcher 無法在當前 Tier 找到可用的呼叫路徑,THEN THE Component_Dispatcher SHALL 回傳結構化錯誤,包含:零件 id、當前 Tier、嘗試的呼叫路徑清單。
|
||||
5. THE Component_Dispatcher SHALL 對每次零件呼叫記錄執行時間(ms)、成功或失敗狀態,並非同步寫入 KBDB 的 Evaluation Block,不阻擋主流程。
|
||||
6. WHILE Component_Dispatcher 執行零件呼叫,THE Component_Dispatcher SHALL 強制套用 Component_Contract 中的 `max_cold_start_ms` 作為逾時上限,超時後回傳逾時錯誤。
|
||||
|
||||
---
|
||||
|
||||
### Requirement 7:Tier 3 邊緣離線生存能力
|
||||
|
||||
**User Story:** As a 邊緣載具操作者(無人機、AGV), I want 載具在完全離線環境中仍能執行預載的 Workflow, so that 業務不因網路中斷而停擺。
|
||||
|
||||
#### Acceptance Criteria
|
||||
|
||||
1. THE Tier_3 執行引擎 SHALL 在無網路連線的環境中,使用本地 SQLite 作為 KBDB 替代儲存,執行預先下載的 Cypher Workflow 與 `.wasm` 零件。
|
||||
2. WHEN Tier_3 執行引擎在執行中發現缺少所需零件,THE Tier_3 執行引擎 SHALL 記錄缺失零件的 `id` 與 `input_schema` 至本地 DTN 佇列,待下次連網時以 Burst 傳輸方式送至 Tier_2 請求代工。
|
||||
3. WHEN Tier_3 執行引擎收到來自 Tier_2 的新 `.wasm` 零件,THE Tier_3 執行引擎 SHALL 在執行前對該零件進行 syscall 掃描,確認不含網路或檔案系統 syscall,通過後才載入執行。
|
||||
4. THE Tier_3 執行引擎 SHALL 在 Cypher 圖譜執行中途動態替換失敗零件(如感測器零件因環境變化失效),以 Component_Registry 中相同 `input_schema` 的備用零件繼續執行,不中斷整體 Workflow。
|
||||
5. WHEN Tier_3 執行引擎重新連線至 Tier_2,THE Tier_3 執行引擎 SHALL 將本地執行日誌(包含 trace、評價結果、Pitfall 記錄)同步至 Tier_2 的 KBDB,確保全局狀態一致。
|
||||
|
||||
---
|
||||
|
||||
### Requirement 8:前端 Web Components 零件庫
|
||||
|
||||
**User Story:** As a 前端開發者, I want 一套以 Web Components 標準實作的 u6u UI 零件庫, so that 畫布上的 UI 元件能在任何現代瀏覽器中獨立運作,不依賴特定前端框架。
|
||||
|
||||
#### Acceptance Criteria
|
||||
|
||||
1. THE Canvas SHALL 提供以下核心 Web Components:`<u6u-btn>`(按鈕)、`<u6u-text-input>`(文字輸入)、`<u6u-text-field>`(多行文字)、`<u6u-chart>`(圖表)、`<u6u-card>`(智慧容器)。
|
||||
2. THE `<u6u-btn>` SHALL 支援以下 HTML attributes:`label`(顯示文字)、`color`(主題色)、`tooltip`(滑鼠懸停提示,純靜態,不觸發 Webhook)、`workflow`(綁定的 Workflow URI,格式為 `workflow://id`)。
|
||||
3. WHEN `<u6u-btn>` 的 `workflow` attribute 被設定且使用者點擊按鈕,THE `<u6u-btn>` SHALL 發出 `u6u:trigger` 自訂事件,事件 detail 包含 `{ workflowId, payload }`。
|
||||
4. THE `<u6u-card>` SHALL 在接收到子元件的 `u6u:trigger` 事件時,自動收集同容器內所有 `<u6u-text-input>` 與 `<u6u-text-field>` 的當前值,合併至事件的 `payload` 後再向上冒泡。
|
||||
5. FOR ALL Web Components,THE Canvas SHALL 保證元件的 HTML attribute 變更能即時反映至視覺渲染,且渲染結果與 attribute 值之間的對應關係具有冪等性(相同 attribute 值永遠產生相同渲染結果)。
|
||||
|
||||
---
|
||||
|
||||
### Requirement 9:雙面翻轉畫布介面
|
||||
|
||||
**User Story:** As a 業務使用者(非工程師), I want 畫布上每個 UI 元件都能翻面查看並編輯其 Cypher 邏輯連線, so that 不需要寫程式就能理解並修改業務邏輯。
|
||||
|
||||
#### Acceptance Criteria
|
||||
|
||||
1. THE Canvas SHALL 為每個 UI 零件提供「翻面」操作,切換至邏輯視圖後,顯示該零件關聯的 Cypher 三元組(以視覺化節點連線方式呈現)。
|
||||
2. WHEN 使用者在邏輯視圖中修改 Cypher 連線(新增、刪除或修改三元組),THE Canvas SHALL 即時更新對應 Workflow 的 KBDB Block,並在正面 UI 視圖中反映連線狀態變更(如按鈕顏色或 badge 提示)。
|
||||
3. THE Canvas SHALL 在邏輯視圖中提供 Workflow URI 選擇器,列出 KBDB 中所有可用的 Workflow,讓使用者透過下拉選單完成 `ON_CLICK >> workflow://id` 的綁定,不需手動輸入 URI。
|
||||
4. WHEN 使用者在畫布上將兩個 UI 零件拖入同一個 `<u6u-card>` 容器,THE Canvas SHALL 自動在邏輯視圖中顯示 Smart_Container 的自動打包關係,說明哪些輸入值會被自動收集。
|
||||
5. THE Canvas SHALL 在使用者嘗試替換一個已綁定 Workflow 的 UI 零件時,只顯示 Component_Registry 中具備相同觸發能力(即 `u6u:trigger` 事件)的候選零件,過濾掉不相容的零件。
|
||||
|
||||
---
|
||||
|
||||
### Requirement 10:自動演化評價迴圈
|
||||
|
||||
**User Story:** As a 平台維運者, I want 每次 Workflow 執行後自動觸發 AI 評價, so that 系統能持續識別問題零件並累積避坑知識。
|
||||
|
||||
#### Acceptance Criteria
|
||||
|
||||
1. WHEN 一個 Workflow 執行完畢(無論成功或失敗),THE Evaluator_Agent SHALL 在執行結束後非同步評估以下維度:執行狀態(成功 / 失敗 / 逾時)、各節點執行時間、零件錯誤率趨勢。
|
||||
2. WHEN Evaluator_Agent 發現某零件的錯誤率在連續 5 次執行中超過 50%,THE Evaluator_Agent SHALL 在 KBDB 中為該零件建立 Pitfall_Block,記錄:零件 id、失敗模式描述、首次發現時間戳記。
|
||||
3. WHEN Component_Dispatcher 在 Component_Registry 搜尋零件時,THE Component_Dispatcher SHALL 讀取目標零件的所有關聯 Pitfall_Block,並在選擇版本時降低有 Pitfall 記錄的版本的評分權重。
|
||||
4. WHEN 一個零件連續 30 天無任何 Workflow 引用,THE Component_Registry SHALL 將該零件標記為 `Deprecated`,並從預設搜尋結果中移除,但保留 `.wasm` 二進位與合約。
|
||||
5. WHEN 一個 `Deprecated` 零件再經過 90 天仍無引用,THE Component_Registry SHALL 將該零件移入墓地(tombstone 狀態),從所有搜尋結果中移除,但 `pinned` 版本的 `.wasm` 永遠保留且可被 Component_Dispatcher 存取。
|
||||
6. THE Evaluator_Agent SHALL 以冪等方式處理重複的執行日誌,對相同 `run_id` 的重複評價請求回傳相同結果而不重複建立 Pitfall_Block。
|
||||
|
||||
---
|
||||
|
||||
### Requirement 11:零件開發指引(Component Authoring Guide)
|
||||
|
||||
**User Story:** As a 零件開發者(使用自己的 AI 工具,如 Claude、GPT、本地模型), I want 平台提供完整的零件開發指引, so that 我的 AI 能根據指引生成符合合約規格的 `.wasm` 零件,並一次通過沙盒驗收。
|
||||
|
||||
#### Acceptance Criteria
|
||||
|
||||
1. THE Component_Registry SHALL 在 `GET /components/guide` 端點提供機器可讀的開發指引文件(Markdown 格式),內容包含:零件合約 YAML 完整範例、I/O 模型說明(stdin/stdout JSON)、各語言(TinyGo、Rust、AssemblyScript)的最小可運行範例程式碼、本地測試指令(`wasmtime` 執行方式)、常見錯誤與解法。
|
||||
2. THE Component_Registry SHALL 在開發指引中明確列出所有禁止行為:網路 syscall、檔案系統 syscall、打包 runtime(QuickJS、Node.js 等)、超過 2MB、混合前後端邏輯於同一零件。
|
||||
3. THE Component_Registry SHALL 在開發指引中提供 `component.contract.yaml` 的 JSON Schema 定義,讓開發者的 AI 能在提交前自行驗證合約格式正確性。
|
||||
4. WHEN 一個零件提交驗收失敗,THE Component_Registry SHALL 在錯誤回應中附上指向開發指引對應章節的錨點連結(如 `#syscall-constraints`),讓開發者的 AI 能直接定位修復方向。
|
||||
5. THE Component_Registry SHALL 提供 `POST /components/validate-contract` 端點,接受 `component.contract.yaml` 內容,回傳格式驗證結果(欄位完整性、schema 合法性、gherkin_tests 最低數量),讓開發者在提交 `.wasm` 前先驗證合約。
|
||||
6. FOR ALL 開發指引中的程式碼範例,THE Component_Registry SHALL 保證範例能通過 Requirement 2 定義的沙盒驗收流程(指引本身是可執行的 ground truth)。
|
||||
|
||||
---
|
||||
|
||||
### Requirement 12:KBDB Component Registry 整合
|
||||
|
||||
**User Story:** As a 系統開發者, I want Component Registry 完全以 KBDB 的 Template/Block/Slot 機制實作, so that 零件狀態與平台其他知識共享同一個持久化層,不引入新的資料庫。
|
||||
|
||||
#### Acceptance Criteria
|
||||
|
||||
1. THE Component_Registry SHALL 以 KBDB 的 `tpl-component` Template 儲存零件合約,每個零件版本對應一個 Block,Block 的 slots 對應合約的各欄位(id、version、wasi_target、stability、runtime_compat、constraints 等)。
|
||||
2. THE Component_Registry SHALL 以 KBDB 的 Vectorize 索引零件的 `description` 與 `tags` 欄位,支援語意搜尋(如「查詢 Google Sheets 資料」能找到 `gsheets_get_entries`)。
|
||||
3. WHEN Component_Dispatcher 搜尋零件時,THE Component_Registry SHALL 回傳按「成功率 × 速度評分 × 被調用次數」排序的版本清單,最多回傳 10 個候選版本。
|
||||
4. THE Component_Registry SHALL 透過 KBDB 的 HTTP API 存取所有資料,不直接操作 D1 SQL,符合平台的 API-First 通訊鐵律。
|
||||
5. FOR ALL Component_Registry 的讀取操作,THE Component_Registry SHALL 保證在 KBDB 資料不變的情況下,對相同查詢參數回傳相同結果(查詢冪等性)。
|
||||
@@ -0,0 +1,411 @@
|
||||
# Implementation Plan: u6u Platform Evolution
|
||||
|
||||
## Overview
|
||||
|
||||
依照 Bootstrap 順序分四個 Phase 實作,每個 Phase 都是下一個 Phase 的基礎。
|
||||
技術棧:TypeScript、Hono、Zod、Vitest、fast-check,部署於 Cloudflare Workers。
|
||||
|
||||
---
|
||||
|
||||
## Phase 0:最小 WASM 執行核心
|
||||
|
||||
- [x] 1. 建立 Component Registry 基礎架構(`u6u-core/registry/`)
|
||||
- [x] 1.1 建立 `tpl-component` Template Block(透過 KBDB HTTP API)
|
||||
- 呼叫 KBDB `/templates` 建立 `tpl-component` template(若不存在)
|
||||
- 定義所有 slot keys(canonical_id、display_name、category、version、wasi_target、stability、runtime_compat、constraints、input_schema、output_schema、gherkin_tests、wasm_r2_key、cypher_binding_url、service_binding_key、description、tags、success_rate、avg_duration_ms、call_count、status、deprecated_at)
|
||||
- _Requirements: 12.1_
|
||||
|
||||
- [x] 1.2 實作 `POST /components/validate-contract` 端點
|
||||
- 以 Zod schema 驗證 component.contract.yaml 所有必填欄位
|
||||
- 回傳缺失欄位清單(`missing_fields: string[]`)
|
||||
- _Requirements: 1.1, 1.2, 11.5_
|
||||
|
||||
- [ ]* 1.3 寫 property test for 合約格式完整性
|
||||
- **Property 1: 合約格式完整性**
|
||||
- **Validates: Requirements 1.1, 1.2, 1.4**
|
||||
- 用 fast-check 生成隨機缺少任意必填欄位的合約物件,驗證 validator 必定拒絕並回傳該欄位名稱
|
||||
- 用 fast-check 生成包含所有必填欄位的合約物件,驗證 validator 必定通過
|
||||
|
||||
- [x] 1.4 實作 `GET /components/guide` 端點
|
||||
- 回傳 Markdown 格式開發指引(TinyGo 白名單、禁止行為、contract YAML 範例、wasmtime 測試指令)
|
||||
- _Requirements: 11.1, 11.2, 11.3_
|
||||
|
||||
- [x] 1.5 實作 `POST /components` 零件提交端點(沙盒驗收流程)
|
||||
- 依序執行五個驗收步驟:(a) 體積檢查、(b) 冷啟動時間測量、(c) syscall 掃描、(d) Gherkin 測試執行、(e) runtime 相容測試
|
||||
- 任一步驟失敗立即停止,回傳 `{ success: false, failed_step, reason, guide_anchor, component_id, version }`
|
||||
- 通過後以 KBDB HTTP API 寫入 Block(`block_id = comp-{id}-{version}`)
|
||||
- 同時上傳 `.wasm` 至 R2,slot `wasm_r2_key` 記錄 R2 key
|
||||
- _Requirements: 2.1, 2.2, 2.3_
|
||||
|
||||
- [ ]* 1.6 寫 property test for 沙盒驗收流程正確性
|
||||
- **Property 2: 沙盒驗收流程正確性**
|
||||
- **Validates: Requirements 2.1, 2.2**
|
||||
- 用 fast-check 生成在步驟 N 失敗的零件,驗證回應包含步驟 N 名稱與原因,且不執行步驟 N+1
|
||||
|
||||
- [ ]* 1.7 寫 property test for 零件提交冪等性與持久性
|
||||
- **Property 3: 零件提交冪等性與持久性**
|
||||
- **Validates: Requirements 2.3, 2.4**
|
||||
- 用 fast-check 生成通過驗收的零件,提交後讀取合約驗證所有欄位 round-trip 一致
|
||||
- 對相同 (id, version) 重複提交 N 次,驗證 KBDB 只存在一個 Block
|
||||
|
||||
- [x] 2. 實作 WASI preview1 shim 與 WASM 執行核心(`cypher-executor/src/lib/`)
|
||||
- [x] 2.1 實作輕量 WASI preview1 shim(`wasi-shim.ts`)
|
||||
- 實作 `fd_read`(從 stdin buffer 讀取)、`fd_write`(寫入 stdout/stderr buffer)、`proc_exit`(拋出 Error)、`random_get`(`crypto.getRandomValues`)
|
||||
- 其餘 syscall 一律回傳 ENOSYS(76)
|
||||
- 不引入任何外部依賴(不使用 `@cloudflare/workers-wasi`)
|
||||
- _Requirements: 3.1, 3.3_
|
||||
|
||||
- [x]* 2.2 寫單元測試 for WASI shim
|
||||
- 測試 `fd_read` 正確讀取 stdin buffer(含多次讀取、邊界條件)
|
||||
- 測試 `fd_write` 正確寫入 stdout buffer(fd=1)與 stderr buffer(fd=2)
|
||||
- 測試 `proc_exit` 拋出 Error
|
||||
|
||||
- [x] 2.3 實作 Tier 1 WASM 執行器(`wasm-executor.ts`)
|
||||
- 從 R2 fetch `.wasm` ArrayBuffer
|
||||
- `WebAssembly.compile` 後快取 `WebAssembly.Module`(Worker 記憶體,跨請求共享)
|
||||
- 建立 WASI shim,注入 stdin = `JSON.stringify(input)`
|
||||
- `WebAssembly.instantiate(module, imports)` → 呼叫 `_start()` 或 `main()`
|
||||
- 從 stdout buffer 讀取輸出,`JSON.parse` 後回傳
|
||||
- 套用 `max_cold_start_ms` 逾時(`Promise.race`)
|
||||
- _Requirements: 3.1, 3.3, 6.6_
|
||||
|
||||
- [ ]* 2.4 寫 property test for Component Dispatcher 路由正確性
|
||||
- **Property 4: Component Dispatcher 路由正確性**
|
||||
- **Validates: Requirements 3.1, 3.5, 3.6**
|
||||
- 用 fast-check 生成合法 JSON input,驗證 WASM 執行路徑輸出與預期語意等效
|
||||
|
||||
- [x] 3. 建立 `validate_json.wasm` 第一個真實零件(TinyGo)
|
||||
- [x] 3.1 撰寫 `validate_json` TinyGo 原始碼(`u6u-core/registry/components/validate_json/main.go`)
|
||||
- 只使用白名單 import:`os`、`io`、`encoding/json`
|
||||
- 讀取 stdin JSON,解析 `json_string` 欄位,嘗試 `json.Unmarshal`
|
||||
- 成功輸出 `{"valid":true}`,失敗輸出 `{"valid":false,"error":"..."}`
|
||||
- _Requirements: 3.1, 11.6_
|
||||
|
||||
- [x] 3.2 撰寫 `validate_json` component.contract.yaml
|
||||
- 包含所有必填欄位、gherkin_tests(happy path + error path)
|
||||
- `runtime_compat: ["cf-workers","workerd","wazero"]`
|
||||
- _Requirements: 1.1, 1.2, 1.4_
|
||||
|
||||
- [ ]* 3.3 寫單元測試 for validate_json(Gherkin 場景驗證)
|
||||
- 測試合法 JSON 輸入回傳 `{"valid":true}`
|
||||
- 測試非法 JSON 輸入回傳 `{"valid":false,"error":...}`
|
||||
|
||||
- [x] 4. Checkpoint — Phase 0 驗收
|
||||
- 確認 `validate_json.wasm` 能在 CF Workers 環境中透過 WASM 執行器執行
|
||||
- 確認 Component Registry `/guide`、`/validate-contract`、`/components` 端點可用
|
||||
- 確認所有 Phase 0 測試通過,向使用者確認是否繼續 Phase 1
|
||||
|
||||
---
|
||||
|
||||
## Phase 1:遷移現有零件(20 個 HTTP → WASM)
|
||||
|
||||
- [x] 5. 升級 Component Dispatcher 支援雙模式(`cypher-executor/src/lib/component-loader.ts`)
|
||||
- [x] 5.1 重構 `ComponentDescriptor` 型別(移除舊 `http_endpoint`,新增 `component_type`)
|
||||
- 定義 `ComponentType = 'wasm' | 'cypher_binding' | 'service_binding'`
|
||||
- 新版 `ComponentDescriptor` 欄位:`component_type`、`wasm_r2_key`、`runtime_compat`、`max_cold_start_ms`、`url`(cypher_binding)、`method`、`binding`(service_binding)、`path`
|
||||
- _Requirements: 3.5_
|
||||
|
||||
- [x] 5.2 實作路由決策邏輯(`component-dispatcher.ts`)
|
||||
- 查 Component Registry 取得合約
|
||||
- 依 `component_type` 分流:`wasm` → WASM 執行器;`cypher_binding` → HTTP POST 到外部 URL;`service_binding` → CF Service Binding
|
||||
- 檢查 `runtime_compat` 是否包含當前 Tier,不包含則回傳 `RUNTIME_INCOMPATIBLE` 錯誤
|
||||
- _Requirements: 3.4, 6.1, 6.2_
|
||||
|
||||
- [ ]* 5.3 寫 property test for Dispatcher 錯誤結構完整性
|
||||
- **Property 5: Dispatcher 錯誤結構完整性**
|
||||
- **Validates: Requirements 3.4, 6.4**
|
||||
- 用 fast-check 生成 (component_id, tier) 組合,當 runtime_compat 不含當前 tier,驗證錯誤回應同時包含 component_id、tier、attempted_paths 三個欄位
|
||||
|
||||
- [x] 6. 遷移 20 個內建零件(`u6u-core/builtins/` → `u6u-core/registry/components/`)
|
||||
- [x] 6.1 為每個零件撰寫 TinyGo 原始碼與 component.contract.yaml(批次作業)
|
||||
- 每個零件:只用白名單 import、stdin/stdout JSON I/O、附帶 gherkin_tests
|
||||
- 需要外部 API 的零件(如 gsheets):改用 `cypher_binding` 模式,contract 中記錄 `cypher_binding_url`
|
||||
- _Requirements: 3.6, 11.6_
|
||||
|
||||
- [x] 6.2 透過 `POST /components` 批次提交 20 個零件至 Component Registry
|
||||
- 每個零件通過沙盒驗收後自動寫入 KBDB
|
||||
- 驗證 20 個零件的 Gherkin 測試全部通過
|
||||
- _Requirements: 2.1, 3.6_
|
||||
|
||||
- [ ]* 6.3 寫 property test for 歷史版本永久保留不變量
|
||||
- **Property 8: 歷史版本永久保留不變量**
|
||||
- **Validates: Requirements 4.5, 10.5**
|
||||
- 用 fast-check 生成已上架零件,標記為 deprecated 後,驗證 `.wasm` 仍可從 R2 讀取,pinned 引用仍可執行
|
||||
|
||||
- [x] 7. 實作 Component Registry 查詢端點
|
||||
- [x] 7.1 實作 `GET /components/:id` 與 `GET /components/:id/versions`
|
||||
- 透過 KBDB HTTP API 查詢 `tpl-component` blocks
|
||||
- `/versions` 回傳按「成功率 × 速度評分 × 被調用次數」排序的版本清單(最多 10 個)
|
||||
- _Requirements: 12.3_
|
||||
|
||||
- [x] 7.2 實作 `GET /components/search?q=...` 語意搜尋
|
||||
- 呼叫 KBDB Vectorize API,以 `description` + `tags` 欄位做語意搜尋
|
||||
- _Requirements: 12.2_
|
||||
|
||||
- [ ]* 7.3 寫單元測試 for 查詢冪等性
|
||||
- 驗證相同查詢參數在 KBDB 資料不變時回傳相同結果
|
||||
|
||||
- [x] 8. Checkpoint — Phase 1 驗收
|
||||
- 確認 20 個零件全部通過沙盒驗收並存入 KBDB
|
||||
- 確認 Component Dispatcher 雙模式路由正確(WASM + cypher_binding)
|
||||
- 確認所有 Phase 1 測試通過,向使用者確認是否繼續 Phase 2
|
||||
|
||||
---
|
||||
|
||||
## Phase 2:Cypher 語意擴展 + Multi-Tier Dispatcher
|
||||
|
||||
- [x] 9. 擴展 Cypher Triplet Parser(`cypher-executor/src/actions/triplet-parser.ts`)
|
||||
- [x] 9.1 新增 EdgeType 定義
|
||||
- 在現有 `PIPE | IF | FOREACH | CONTINUE` 基礎上新增:`IS_A | ON_SUCCESS | ON_FAIL | ON_CLICK | CALLS_SUBFLOW | CONTAINS | HAS_STYLE | HAS_BEHAVIOR`
|
||||
- _Requirements: 5.1_
|
||||
|
||||
- [x] 9.2 實作 URI 協議解析函數(`resolveComponentId`)
|
||||
- 解析 `component://id`、`component://id@stable`、`component://id@pinned:vN`、`workflow://id`、`ui://id`、`style://id`
|
||||
- 回傳 `{ type, canonicalId, stability, pinnedVersion? }`
|
||||
- _Requirements: 4.1, 5.6_
|
||||
|
||||
- [ ]* 9.3 寫 property test for 零件 URI 解析 Round-Trip
|
||||
- **Property 6: 零件 URI 解析 Round-Trip**
|
||||
- **Validates: Requirements 4.1**
|
||||
- 用 fast-check 生成合法 URI 字串,解析後再序列化,驗證語意等效;驗證解析出的 id、stability、pinnedVersion 與原始 URI 完全一致
|
||||
|
||||
- [ ]* 9.4 寫 property test for Cypher 三元組解析 Confluence
|
||||
- **Property 9: Cypher 三元組解析 Confluence(順序無關性)**
|
||||
- **Validates: Requirements 5.7**
|
||||
- 用 fast-check 生成合法三元組集合,用 `fc.shuffledSubarray` 打亂順序,驗證 `parseTriplets` 產生相同節點集合與邊集合
|
||||
|
||||
- [x] 10. 擴展 GraphExecutor 執行語意(`cypher-executor/src/graph-executor.ts`)
|
||||
- [x] 10.1 實作 `IS_A` 關係處理
|
||||
- 從 Component Registry 載入零件合約,以 `input_schema` 驗證節點輸入 context
|
||||
- _Requirements: 5.2_
|
||||
|
||||
- [x] 10.2 實作 `ON_SUCCESS` / `ON_FAIL` 分支執行
|
||||
- 在 `executeNode` 的 try/catch 中,成功走 `ON_SUCCESS` 邊,失敗走 `ON_FAIL` 邊(傳遞 error context)
|
||||
- _Requirements: 5.3_
|
||||
|
||||
- [x] 10.3 實作 `CALLS_SUBFLOW` 子流程呼叫
|
||||
- 從 KBDB 載入子 Workflow 定義,建立子 GraphExecutor 執行,將輸出合併回主流程 context
|
||||
- _Requirements: 5.4_
|
||||
|
||||
- [x] 10.4 實作 `ON_CLICK` 前端觸發處理
|
||||
- 接受來自前端 Smart Container 打包的 payload,作為 Workflow initialContext
|
||||
- _Requirements: 5.5_
|
||||
|
||||
- [x] 10.5 實作 `CONTAINS` / `HAS_STYLE` / `HAS_BEHAVIOR` 結構語意解析(不執行,僅記錄圖結構)
|
||||
- _Requirements: 5.1_
|
||||
|
||||
- [x] 11. 實作版本選擇策略(Component Dispatcher 升級)
|
||||
- [x] 11.1 實作 floating 版本選擇算法
|
||||
- 從 KBDB 查詢該 id 下所有版本,計算「成功率 × 速度評分 × 被調用次數」,選取最高分版本
|
||||
- _Requirements: 4.2_
|
||||
|
||||
- [x] 11.2 實作 stable / pinned 版本選擇
|
||||
- `stable`:使用當前標記為 stable 的版本,有更優版本時記錄提示至 KBDB 但不切換
|
||||
- `pinned:vN`:永遠使用版本 vN,即使已 deprecated
|
||||
- _Requirements: 4.3, 4.4_
|
||||
|
||||
- [ ]* 11.3 寫 property test for 版本選擇策略正確性
|
||||
- **Property 7: 版本選擇策略正確性**
|
||||
- **Validates: Requirements 4.2, 4.4**
|
||||
- 用 fast-check 生成版本集合(各有不同 success_rate、avg_duration_ms、call_count),驗證 floating 選最高分;驗證 pinned:vN 無論其他版本評分如何永遠選 vN
|
||||
|
||||
- [x] 12. 實作 Evaluator Agent 與評價迴圈(`cypher-executor/src/actions/`)
|
||||
- [x] 12.1 實作 `execution-evaluator.ts`(擴展現有 `execution-logger.ts`)
|
||||
- Workflow 執行完畢後非同步寫入 KBDB Evaluation Block(`tpl-evaluation`)
|
||||
- 記錄:run_id、workflow_id、component_id、verdict、duration_ms、error_message、evaluated_at
|
||||
- 冪等處理:相同 run_id 不重複建立 Block
|
||||
- _Requirements: 10.1, 10.6_
|
||||
|
||||
- [x] 12.2 實作 Pitfall Block 建立邏輯
|
||||
- 偵測某零件連續 5 次執行錯誤率 > 50%,建立 `tpl-pitfall` Block
|
||||
- 版本選擇時降低有 Pitfall 記錄的版本評分權重
|
||||
- _Requirements: 10.2, 10.3_
|
||||
|
||||
- [x] 12.3 實作零件自動 Deprecated / Tombstone 狀態轉換
|
||||
- 連續 30 天無引用 → 標記 `deprecated`,從預設搜尋移除
|
||||
- 再 90 天無引用 → 標記 `tombstone`,從所有搜尋移除(pinned `.wasm` 永久保留)
|
||||
- _Requirements: 10.4, 10.5_
|
||||
|
||||
- [ ]* 12.4 寫 property test for 系統操作冪等性
|
||||
- **Property 11: 系統操作冪等性**
|
||||
- **Validates: Requirements 10.6, 12.5**
|
||||
- 用 fast-check 生成 run_id,對相同 run_id 呼叫 Evaluator N 次,驗證 KBDB 只存在一個 Evaluation Block
|
||||
- 驗證相同查詢參數在資料不變時回傳相同結果
|
||||
|
||||
- [x] 13. Checkpoint — Phase 2 驗收
|
||||
- 確認新 EdgeType 全部可解析執行(IS_A、ON_SUCCESS、ON_FAIL、CALLS_SUBFLOW、ON_CLICK)
|
||||
- 確認版本選擇策略(floating / stable / pinned)行為正確
|
||||
- 確認 Evaluator Agent 冪等寫入 KBDB
|
||||
- 確認所有 Phase 2 測試通過,向使用者確認是否繼續 Phase 3
|
||||
|
||||
---
|
||||
|
||||
## Phase 3:前端畫布(Web Components + 雙面翻轉)
|
||||
|
||||
- [x] 14. 建立 Web Components 零件庫(`u6u-core/web-components/`)
|
||||
- [x] 14.1 實作 `<u6u-btn>` Custom Element
|
||||
- 支援 attributes:`label`、`color`、`tooltip`、`workflow`、`disabled`
|
||||
- `workflow` 設定且點擊時發出 `u6u:trigger` CustomEvent(`{ workflowId, payload }`)
|
||||
- `workflow` 未設定時點擊不發出事件,console 輸出警告
|
||||
- _Requirements: 8.2, 8.3_
|
||||
|
||||
- [x] 14.2 實作 `<u6u-text-input>` 與 `<u6u-text-field>` Custom Elements
|
||||
- 支援 `name`、`placeholder`、`value` attributes
|
||||
- `value` property 可被 `<u6u-card>` 讀取
|
||||
- _Requirements: 8.1_
|
||||
|
||||
- [x] 14.3 實作 `<u6u-card>` Smart Container
|
||||
- 攔截子元件的 `u6u:trigger` 事件(`stopPropagation`)
|
||||
- 收集同容器內所有 `<u6u-text-input>` / `<u6u-text-field>` 的 name-value 對
|
||||
- 合併至 payload 後重新發出 `u6u:trigger`(`bubbles: true, composed: true`)
|
||||
- _Requirements: 8.4_
|
||||
|
||||
- [x] 14.4 實作 `<u6u-chart>` Custom Element(基礎版)
|
||||
- 支援 `data` attribute(JSON 字串)、基本折線圖渲染
|
||||
- _Requirements: 8.1_
|
||||
|
||||
- [ ]* 14.5 寫 property test for Web Components 事件與渲染冪等性
|
||||
- **Property 10: Web Components 事件與渲染冪等性**
|
||||
- **Validates: Requirements 8.3, 8.4, 8.5**
|
||||
- 用 fast-check 生成 workflow URI,驗證點擊後 `u6u:trigger` detail.workflowId 與 URI id 完全一致
|
||||
- 用 fast-check 生成具名 input 集合置於 u6u-card,驗證收集到的 payload 包含所有 name-value 對
|
||||
- 用 fast-check 生成 attribute 值,對同一元件設定相同值 N 次,驗證渲染結果冪等
|
||||
|
||||
- [x] 15. 建立雙面翻轉畫布(`inkstone-admin/frontend/web/`)
|
||||
- [x] 15.1 實作翻轉狀態機(`Canvas.tsx`)
|
||||
- 狀態:`UIView` ↔ `LogicView`(點擊翻面按鈕切換)
|
||||
- `LogicView` → `Editing`(修改三元組)→ `Saving`(確認儲存)→ `LogicView`
|
||||
- _Requirements: 9.1_
|
||||
|
||||
- [x] 15.2 實作邏輯視圖(Cypher 三元組視覺化)
|
||||
- 顯示零件關聯的 Cypher 三元組(節點連線方式)
|
||||
- 修改三元組後即時更新 KBDB Workflow Block(透過 KBDB HTTP API)
|
||||
- _Requirements: 9.2_
|
||||
|
||||
- [x] 15.3 實作 Workflow URI 選擇器
|
||||
- 列出 KBDB 中所有可用 Workflow,下拉選單完成 `ON_CLICK >> workflow://id` 綁定
|
||||
- _Requirements: 9.3_
|
||||
|
||||
- [x] 15.4 實作 Smart Container 拖放與自動打包關係顯示
|
||||
- 拖入同一 `<u6u-card>` 時,邏輯視圖自動顯示 CONTAINS 關係與自動打包說明
|
||||
- _Requirements: 9.4_
|
||||
|
||||
- [x] 15.5 實作零件替換過濾器
|
||||
- 替換已綁定 Workflow 的 UI 零件時,只顯示具備 `u6u:trigger` 能力的候選零件
|
||||
- _Requirements: 9.5_
|
||||
|
||||
- [x] 15.6 在畫布中整合 u6u Web Components(Dogfooding)
|
||||
- 畫布 UI 本身使用 `<u6u-btn>`、`<u6u-card>`、`<u6u-text-input>` 組裝
|
||||
- 驗證 Web Components 在 React 19 環境中正確運作
|
||||
- _Requirements: 8.1, 9.1_
|
||||
|
||||
- [ ]* 15.7 寫整合測試 for 畫布翻轉流程
|
||||
- 測試 UIView → LogicView → Editing → Saving → LogicView 完整狀態轉換
|
||||
- 測試 KBDB 寫入成功與失敗兩種情境
|
||||
|
||||
- [x] 16. Final Checkpoint — 全平台驗收
|
||||
- 確認四個 Phase 的所有測試通過(`pnpm test` in each service)
|
||||
- 確認 Dogfooding:畫布本身用 u6u Web Components 組裝,每一層都是下一層的第一個用戶
|
||||
- 確認 KBDB 不變量:仍只有三張表(blocks / templates / slots)
|
||||
- 確認 API-First 鐵律:所有跨服務通訊只透過 HTTP API
|
||||
- 向使用者確認所有任務完成
|
||||
|
||||
---
|
||||
|
||||
## Phase 4:邊緣基礎設施(Tier 3 支援)
|
||||
|
||||
> 這個 Phase 不在零件遷移範圍內,是獨立的基礎設施工作。
|
||||
|
||||
- [ ] 17. Credentials 邊緣支援評估與改寫(`u6u-core/credentials/`)
|
||||
- [ ] 17.1 評估 Tier 3 是否需要 Credentials
|
||||
- 場景:無人機在有網路時呼叫外部 API(如取得感測器資料),需要 access_token
|
||||
- 結論:Tier 3 需要在連網時從 Tier 2 取得 Credential,離線時使用本地快取的加密 token
|
||||
- _Requirements: 7.1, 7.2_
|
||||
|
||||
- [ ] 17.2 實作 Credential 本地快取機制(Tier 3 用)
|
||||
- Tier 3 Go 排程引擎在連網時從 Tier 2 Credentials Worker 取得加密 token
|
||||
- 存入本地 SQLite(AES-GCM 加密,key 存於設備安全儲存)
|
||||
- 離線時從本地快取讀取,過期時加入 DTN 佇列等待更新
|
||||
- _Requirements: 7.1, 7.3_
|
||||
|
||||
- [ ] 18. Go Cypher Executor(Tier 3 邊緣執行引擎)(`u6u-core/executor/`)
|
||||
- [ ] 18.1 用 Go 實作 Cypher 三元組解析器
|
||||
- 解析 `"A >> 關係 >> B"` 格式,建立執行圖(nodes + edges)
|
||||
- 支援 IS_A、ON_SUCCESS、ON_FAIL、CALLS_SUBFLOW、ON_CLICK 語意關係
|
||||
- 對應 `cypher-executor/src/actions/triplet-parser.ts` 的 Go 版本
|
||||
- _Requirements: 5.1, 5.7_
|
||||
|
||||
- [ ] 18.2 用 Go + Wazero 實作 WASM 零件執行器
|
||||
- 載入本地 `.wasm` 檔案,透過 Wazero 原生 WASI preview1 執行
|
||||
- 注入 `u6u` host module(`http_request` host function,透過 DTN 或直接 HTTP)
|
||||
- stdin/stdout JSON I/O,與 Tier 1/2 的 `wasm-executor.ts` 語意等效
|
||||
- _Requirements: 7.1, 7.4_
|
||||
|
||||
- [ ] 18.3 實作 DTN 佇列(離線請求緩衝)
|
||||
- 零件需要網路但當前離線時,寫入本地 SQLite DTN 佇列
|
||||
- 連網時 Burst 傳輸:批次送出佇列中的請求,接收回應後繼續執行
|
||||
- _Requirements: 7.2, 7.5_
|
||||
|
||||
- [ ]* 18.4 整合測試:validate_json.wasm 在 Wazero 執行
|
||||
- 確認同一個 `.wasm` 在 Tier 1(wasi-shim.ts)和 Tier 3(Wazero)執行結果一致
|
||||
|
||||
---
|
||||
|
||||
## Notes
|
||||
|
||||
- 標記 `*` 的子任務為選填,可跳過以加速 MVP 交付
|
||||
- 每個任務都引用具體的 Requirements 條款以確保可追溯性
|
||||
- Checkpoint 任務確保每個 Phase 完成後有明確的驗收點
|
||||
- Property tests 使用 fast-check,每個屬性最少執行 100 次迭代
|
||||
- 所有跨服務呼叫只透過 KBDB HTTP API,不直接操作 D1 SQL
|
||||
- TinyGo 零件只使用白名單 import(`os`、`io`、`encoding/json`)
|
||||
|
||||
## Phase 5:u6u-mcp 對齊新 Registry + u6u-gui 前端
|
||||
|
||||
> 壓測前必須完成,讓 AI(u6u-mcp)和人類(u6u-gui)都能操作新的 WASM 零件架構。
|
||||
|
||||
- [x] 19. 更新 u6u-mcp 對齊新 Component Registry(`u6u-mcp/src/tools/`)
|
||||
- [x] 19.1 更新 `u6u_publish_component`
|
||||
- 舊:呼叫 `/components/publish`,payload 為 `{ component_id, gherkin, api_config }`
|
||||
- 新:呼叫 `POST /components`,payload 為 `{ contract: ComponentContract, wasm_base64: string }`
|
||||
- 新增 `contract` 和 `wasm_base64` 參數,更新工具描述說明 TinyGo 零件提交流程
|
||||
|
||||
- [x] 19.2 更新 `u6u_search_components`
|
||||
- 舊:呼叫 `/components/match`(不存在的端點)
|
||||
- 新:呼叫 `GET /components/search?q={query}`(新 Registry 語意搜尋端點)
|
||||
- 更新工具描述:AI 可用自然語言搜尋零件(如「查詢 Google Sheets 資料」)
|
||||
|
||||
- [x] 19.3 更新 `u6u_get_component`
|
||||
- 舊:讀舊格式 slots(`component_id`、`name`、`published_at`)
|
||||
- 新:對齊 `tpl-component` slot 欄位(`canonical_id`、`display_name`、`category`、`version`、`stability`、`wasm_r2_key` 等)
|
||||
- 呼叫新 Registry `GET /components/:id` 端點
|
||||
|
||||
- [x] 19.4 新增 `u6u_get_component_guide` 工具
|
||||
- 呼叫 `GET /components/guide`,回傳開發指引給 AI
|
||||
- AI 在開發新零件前可先讀取指引,確保生成符合規範的 TinyGo 程式碼
|
||||
|
||||
- [x] 20. 建立 u6u-gui 前端(`u6u-gui/`)
|
||||
- [x] 20.1 建立 Cloudflare Pages 專案結構
|
||||
- React 19 + Vite + Tailwind CSS v4
|
||||
- 整合 `@u6u/web-components`(alias 指向 `u6u-core/web-components/src`)
|
||||
- wrangler.toml 設定 Pages 部署
|
||||
|
||||
- [x] 20.2 建立主畫布頁面(`/canvas`)
|
||||
- 整合 `Canvas.tsx`(從 inkstone-admin 移植)
|
||||
- 連接 Cypher Executor API(`POST /cypher/execute`)
|
||||
- 連接 Component Registry API(搜尋、查詢零件)
|
||||
- AI 操作後(透過 u6u-mcp 修改 KBDB)畫布即時反映變更
|
||||
|
||||
- [x] 20.3 建立零件庫頁面(`/components`)
|
||||
- 列出所有已上架零件(呼叫 `GET /components/search`)
|
||||
- 顯示零件合約、評分、版本歷史
|
||||
- 提供「提交新零件」入口(連結到開發指引)
|
||||
|
||||
- [x] 20.4 建立 Workflow 管理頁面(`/workflows`)
|
||||
- 列出所有 Workflow(從 KBDB 查詢 `tpl-workflow`)
|
||||
- 點擊進入畫布編輯
|
||||
- 顯示執行歷史(Evaluation Block)
|
||||
|
||||
- [x] 20.5 部署至 Cloudflare Pages
|
||||
- `pnpm build && npx wrangler pages deploy dist`
|
||||
- 設定環境變數(KBDB_URL、CYPHER_URL、REGISTRY_URL)
|
||||
@@ -0,0 +1,62 @@
|
||||
# Tech Stack — arcrun
|
||||
|
||||
## Runtime & Deployment
|
||||
- **Cloudflare Workers** — all backend services deploy as Workers via Wrangler
|
||||
- **Cloudflare KV** — workflow definitions, credentials (encrypted), recipes, sessions
|
||||
- **Cloudflare R2** — WASM binary storage (`WASM_BUCKET`)
|
||||
- **Cloudflare Pages** — frontend deployment (arcrun.dev landing page)
|
||||
|
||||
## Languages & Frameworks
|
||||
- **TypeScript** — Workers (HTTP routing/orchestration only), CLI, SDKs
|
||||
- **TinyGo / AssemblyScript** — all component logic, compiled to WASM (WASI preview1)
|
||||
- **Hono** — HTTP framework for all Workers (routing, middleware, OpenAPI)
|
||||
- **Zod** — schema validation and OpenAPI spec generation (`@hono/zod-openapi`)
|
||||
- **React 19** + **Vite** — frontend (landing page)
|
||||
- **Tailwind CSS v4** — styling
|
||||
|
||||
## Testing
|
||||
- **Vitest** — test runner
|
||||
- **@cloudflare/vitest-pool-workers** — Workers-specific test pool for cypher-executor
|
||||
|
||||
## Package Management
|
||||
- **pnpm** — used in most packages (some use npm)
|
||||
|
||||
---
|
||||
|
||||
## Common Commands
|
||||
|
||||
### Per-service (run from the service directory)
|
||||
|
||||
```bash
|
||||
# Start local dev server
|
||||
pnpm dev # or: npm run dev
|
||||
|
||||
# Deploy to Cloudflare
|
||||
pnpm deploy
|
||||
|
||||
# Run tests (single pass)
|
||||
pnpm test # runs: vitest run
|
||||
```
|
||||
|
||||
### WASM Components (TinyGo)
|
||||
|
||||
```bash
|
||||
# Build a component
|
||||
cd registry/components/{name}/
|
||||
tinygo build -target=wasi -o main.wasm main.go
|
||||
```
|
||||
|
||||
### CLI
|
||||
|
||||
```bash
|
||||
cd cli/
|
||||
npm run build
|
||||
acr --help
|
||||
```
|
||||
|
||||
## API Documentation
|
||||
Each Worker exposes OpenAPI docs at runtime:
|
||||
- `/doc` — OpenAPI JSON spec
|
||||
- `/ui` — Swagger UI
|
||||
|
||||
Dev base URLs: Cypher Executor → `http://localhost:8788`
|
||||
Reference in New Issue
Block a user