diff --git a/.agents/specs/arcrun-core-mvp/design.md b/.agents/specs/arcrun-core-mvp/design.md deleted file mode 100644 index ede7b37..0000000 --- a/.agents/specs/arcrun-core-mvp/design.md +++ /dev/null @@ -1,611 +0,0 @@ -# Design Document: arcrun MVP - -## Overview - -arcrun MVP 的核心設計原則是**最小異動、最快可用**。所有目標都能透過以下三個操作達成: -1. **Cherry-pick**:從 `matrix` 搬移指定目錄,不重寫 -2. **Carve-out**:移除 cypher-executor 中與 InkStone 耦合的程式碼路徑 -3. **Supplement**:補充 contract.yaml 缺少的欄位、新增 CLI - -不建立新的抽象層,不改變現有零件邏輯,只做讓開源可用所需的最小改動。 - ---- - -## Architecture - -### 目標 Repo 結構 - -``` -arcrun/(新獨立開源 repo) -├── README.md -├── CONTRIBUTING.md -├── cypher-executor/ -│ ├── src/ -│ │ ├── index.ts -│ │ ├── types.ts -│ │ ├── graph-executor.ts -│ │ ├── lib/ -│ │ │ ├── component-loader.ts ← 改:只從 WASM_BUCKET 讀,移除 KBDB/REGISTRY 邏輯 -│ │ │ ├── component-dispatcher.ts -│ │ │ ├── wasm-executor.ts -│ │ │ ├── wasi-shim.ts -│ │ │ └── constants.ts ← 改:移除 MINI_ME / KBDB 特殊零件 hardcode -│ │ └── actions/ -│ │ ├── triplet-parser.ts -│ │ ├── graph-builder.ts -│ │ ├── execution-evaluator.ts -│ │ ├── execution-logger.ts -│ │ ├── webhook-handlers.ts -│ │ ├── webhook-graph-resolver.ts ← 改:加入 credential 注入邏輯 -│ │ └── (移除 autoPublishMissing.ts) -│ └── wrangler.toml ← 改:移除 9 個 InkStone bindings,新增 CREDENTIALS_KV -├── credentials/ ← 直接搬移,無需修改 -│ └── src/... -├── builtins/ ← 直接搬移,無需修改 -│ └── src/... -└── registry/ - └── components/ ← 搬移後補充 contract.yaml - ├── gmail/ - ├── google_sheets/ - ├── telegram/ - ├── line_notify/ - ├── ... (其餘 17 個零件) - └── cli/ ← 新增:arcrun CLI - ├── package.json - ├── tsconfig.json - └── src/ - ├── index.ts - ├── commands/ - │ ├── init.ts - │ ├── creds.ts - │ ├── push.ts - │ ├── run.ts - │ ├── validate.ts - │ ├── parts.ts - │ ├── list.ts - │ └── logs.ts - └── lib/ - ├── config.ts # 讀寫 ~/.arcrun/config.yaml - ├── cf-api.ts # Cloudflare KV / R2 HTTP API wrapper - └── yaml-parser.ts # workflow.yaml 解析與三元組轉換 -``` - ---- - -## Component Loader 改造(關鍵變更) - -### 現況(matrix 版) - -```typescript -// component-loader.ts 現有四層優先序: -// 1. 特殊零件 hardcode → MINI_ME / KBDB Service Binding -// 2. 內建零件 Map → 本地純轉換 -// 3. 新版:查詢 KBDB record 含 component_type → WASM 或 Service Binding -// 4. 舊版 fallback:查詢無 component_type 的 KBDB record -``` - -### 開源版(arcrun) - -```typescript -// component-loader.ts 簡化為三層: -// 1. 內建零件 Map → 本地純轉換(passthrough / counter 等,保留) -// 2. WASM_BUCKET R2 直讀 → component-name.wasm -// 3. 找不到 → 回傳結構化錯誤 - -async function loadComponent(componentId: string, env: Env) { - // 層 1:內建零件(無需 R2) - if (BUILTIN_COMPONENTS.has(componentId)) { - return BUILTIN_COMPONENTS.get(componentId) - } - - // 層 2:從 WASM_BUCKET R2 讀取 - const wasmKey = `${componentId}/${componentId}.wasm` - const wasmObj = await env.WASM_BUCKET.get(wasmKey) - if (wasmObj) { - return { type: 'wasm', buffer: await wasmObj.arrayBuffer() } - } - - // 層 3:找不到 - throw new Error(`Component not found: ${componentId}. 請確認 ${wasmKey} 存在於 WASM_BUCKET。`) -} -``` - -移除:`MINI_ME`、`KBDB` 特殊零件的 hardcode 路徑(`comp_claude_chat`、`comp_kbdb_search`、`comp_kbdb_history`)。 - ---- - -## Credential 注入流程設計 - -### 執行時序 - -``` -acr run newsletter_subscribe - ↓ -cypher-executor POST /webhook/:id - ↓ -webhook-graph-resolver 讀 WEBHOOKS KV → workflow 定義 - ↓ -graph-executor 執行節點 send_thanks - ↓ -執行前:credential-injector(新增) - 查 send_thanks 對應零件 canonical_id = "gmail" - 讀 registry/components/gmail/component.contract.yaml - 發現 credentials_required: [{key: "gmail_token", inject_as: "access_token"}] - GET CREDENTIALS_KV["gmail_token"] → AES-GCM 解密 - input.access_token = decryptedToken - ↓ -wasm-executor 執行 gmail.wasm(stdin = 含 access_token 的完整 input) - ↓ -回傳結果 -``` - -### credential-injector 實作位置 - -放在 `cypher-executor/src/actions/credential-injector.ts`(新增),由 `graph-executor.ts` 在每個節點執行前呼叫。 - -```typescript -async function injectCredentials( - componentId: string, - input: Record, - env: Env -): Promise> { - 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: - source: - wasm: - 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 指令 -``` diff --git a/.agents/specs/arcrun-core-mvp/requirements.md b/.agents/specs/arcrun-core-mvp/requirements.md deleted file mode 100644 index 4753155..0000000 --- a/.agents/specs/arcrun-core-mvp/requirements.md +++ /dev/null @@ -1,180 +0,0 @@ -# Requirements Document - -## Introduction - -arcrun MVP 是從 `matrix` monorepo 中 cherry-pick 出最小可獨立運作的 AI 工作流執行引擎,目標是作為**獨立開源 repo**(`arcrun`)發布。 - -**背景**:`matrix` 因為同時承載 InkStone 內部服務(KBDB、CLINIC_*、AICEO、MINI_ME 等)與核心執行引擎,複雜度過高,難以讓外部開發者使用或貢獻。MVP 的任務是將執行引擎從內部服務中解耦,讓任何人都能自行在 Cloudflare 上部署一套完整的 AI 工作流系統。 - -**護城河邏輯**: -- 開源:cypher-executor(執行引擎)、WASM 零件庫(21 個)、credentials Worker、CLI -- Hosted SaaS:一行指令註冊取得 API Key,直接使用公眾零件庫,無需部署任何 Worker -- 閉源(InkStone 付費):KBDB 向量搜尋、graph 查詢、Persona SDK、MatchGPT - -**不在此次範圍**:KBDB 整合、前端管理介面、向量搜尋、新增 WASM 零件。 - ---- - -## Glossary - -- **cypher-executor**:原 `matrix/cypher-executor`,執行 workflow 的 Cloudflare Worker。開源版移除所有 InkStone 內部 Service Binding,只保留 KV / R2 / Workers AI。 -- **component(零件)**:以 TinyGo 編譯的 `.wasm` 檔案,以 WASI preview1 / stdin-stdout JSON 為 I/O 模型。 -- **component.contract.yaml**:每個零件的規格宣告,含 `canonical_id`、`input_schema`、`output_schema`、`gherkin_tests`,開源版補充 `credentials_required` 與 `config_example`。 -- **credentials Worker**:`arcrun/credentials`,以 AES-GCM 加密存取 API token,部署在用戶自己的 CF 帳號。 -- **WASM_BUCKET**:arcrun.dev 的 R2 bucket,儲存所有公眾 `.wasm` 零件二進位,由 Arcrun 負責。 -- **USER_KV**:用戶自己 CF 帳號下的 KV Namespace,同時存放 workflow YAML 與加密 credential,由用戶負責,Arcrun 不經手。 -- **workflow.yaml**:用戶撰寫的工作流定義,`flow:` 用 `>>` 三元組描述,`config:` 對應各節點參數,存在用戶自己的 USER_KV。 -- **CLI(套件名 arcrun,指令 acr)**:Node.js/TypeScript CLI 工具,管理 credentials、workflow 的上傳與執行。安裝:`npm i -g arcrun`,使用:`acr <指令>`。 -- **credentials_required**:contract.yaml 新欄位,宣告零件需要哪個 credential 以及注入到哪個 input 欄位。 -- **config_example**:contract.yaml 新欄位,提供 `acr parts scaffold` 指令使用的 config 範本。 -- **Standard 模式(預設)**:用戶只需在自己 CF 帳號開一個 KV(存 credential + workflow),使用 arcrun.dev 的執行引擎與公眾零件庫,無需部署任何 Worker。 -- **Self-hosted 模式**:用戶自行部署全套 Worker 至自己的 Cloudflare 帳號,有完全控制權,可貢獻零件至公眾庫。 -- **auth-worker**:arcrun.dev 上的帳號服務 Worker,處理 `POST /register` 自動發放 API Key,不儲存用戶 credential。 -- **tenant_id**:每個 API Key 對應的租戶識別碼,用於讓 cypher-executor 知道要用哪個 Cloudflare API Token 去存取用戶的 USER_KV。 -- **public registry**:arcrun.dev 上的公眾零件庫,所有人共用,有執行統計與 author 資訊。 -- **`acr parts publish`**:CLI 指令,自架用戶將自製零件提交至公眾 registry 審核。 -- **execution analytics**:每次零件執行後非同步記錄的統計資料(使用次數、成功率),公開顯示於 `acr parts`。 -- **visibility**:contract.yaml 欄位,值為 `author_only`(沙盒通過後作者立即可用)或 `public`(人工審核後所有人可用)。 - ---- - -## Requirements - -### Requirement 1:搬移 cypher-executor 至獨立 repo 並移除 InkStone bindings - -**User Story:** As a 開源用戶, I want 自行部署 cypher-executor 至我的 Cloudflare 帳號, so that 我不需要依賴 InkStone 的任何服務就能執行 AI 工作流。 - -#### Acceptance Criteria - -1. THE cypher-executor `wrangler.toml` SHALL 移除以下 Service Bindings:`KBDB`、`REGISTRY`、`CLINIC_GDRIVE`、`CLINIC_EXCEL`、`CLINIC_ANALYSIS`、`CLINIC_RENDER`、`CLINIC_GSHEETS`、`AICEO`、`MINI_ME`。 -2. THE cypher-executor `wrangler.toml` SHALL 保留以下 bindings:`EXEC_CONTEXT`(KV)、`WEBHOOKS`(KV)、`WASM_BUCKET`(R2)、`AI`(Workers AI)。 -3. THE cypher-executor `wrangler.toml` SHALL 新增 `CREDENTIALS_KV`(KV Namespace binding),用於 credential 解密注入。 -4. THE component-loader SHALL 從 `WASM_BUCKET` R2 直接讀取 `.wasm` 檔案,不透過任何 Service Binding 或外部 HTTP 查詢。 -5. WHEN cypher-executor 收到執行請求,THE cypher-executor SHALL 不依賴 `KBDB`、`REGISTRY` 或任何 InkStone 內部 Service Binding,只使用 KV / R2 / Workers AI 完成執行。 -6. THE arcrun repo SHALL 包含以下目錄:`cypher-executor/`、`credentials/`、`builtins/`、`registry/components/`(21 個零件)。 - ---- - -### Requirement 2:component.contract.yaml 完整度補充 - -**User Story:** As a 零件使用者, I want 每個零件的 contract.yaml 都有 `credentials_required` 與 `config_example`, so that CLI 能自動注入 credential,用戶也能快速知道如何設定節點。 - -#### Acceptance Criteria - -1. THE `credentials_required` 欄位 SHALL 出現在以下 4 個零件的 contract.yaml 中:`gmail`、`google_sheets`、`telegram`、`line_notify`。 -2. WHEN `credentials_required` 存在,THE 欄位 SHALL 包含以下子欄位:`key`(對應 credentials.yaml 的 key 名稱)、`type`(token 類型,如 `google_oauth`、`telegram_bot_token`)、`description`(說明)、`inject_as`(執行時注入到 input 的哪個欄位名稱)。 -3. THE `config_example` 欄位 SHALL 出現在所有 21 個零件的 contract.yaml 中。 -4. WHEN `config_example` 存在,THE 欄位 SHALL 為 YAML 字串,內容為可直接貼入 workflow.yaml `config:` 區塊的範本,需有人類可讀的說明註解。 -5. FOR 需要 credential 的零件,THE `config_example` SHALL 包含一行註解,說明哪個 credential key 會被自動注入到哪個欄位(如 `# access_token 由 credentials.yaml 的 gmail_token 自動注入`)。 -6. THE main.go 的 `required` 欄位 SHALL 與 contract 的 `input_schema.required[]` 保持一致,不得有欄位名稱不符。 - ---- - -### Requirement 3:workflow YAML 格式與執行時 credential 注入 - -**User Story:** As a 工作流設計者, I want 用有語意的關係詞撰寫 workflow.yaml,且 credential 自動注入, so that workflow 定義中完全不出現明文 token。 - -#### Acceptance Criteria - -1. THE workflow.yaml `flow:` 欄位 SHALL 以 `"A >> 關係詞 >> B"` 三元組陣列描述資料流。 -2. THE cypher-executor SHALL 支援以下關係詞:`完成後`、`失敗時`、`對每個`、`條件滿足時`、`ON_SUCCESS`、`ON_FAIL`、`FOREACH`、`IF`、`ON_CLICK`、`CALLS_SUBFLOW`。 -3. THE cypher-executor SHALL 拒絕使用 `PIPE` 關係詞,並回傳明確錯誤訊息。 -4. WHEN cypher-executor 執行一個節點,THE cypher-executor SHALL 查詢該節點對應零件的 `credentials_required`,若存在則從 `CREDENTIALS_KV` 解密對應 credential,並注入到 input 的 `inject_as` 欄位。 -5. THE credential 注入 SHALL 發生在 WASM 執行前,用戶的 workflow `config:` 中不需也不應包含 token 值。 -6. IF `credentials_required` 宣告的 credential key 在 `CREDENTIALS_KV` 中不存在,THE cypher-executor SHALL 回傳結構化錯誤,包含缺少的 key 名稱與修復步驟說明。 - ---- - -### Requirement 4:CLI(arcrun,指令 acr)核心指令 - -**User Story:** As a 開發者, I want 透過 `acr` CLI 管理 workflow 與 credentials, so that 不需要直接操作 Cloudflare KV / R2 API 就能完成部署與執行。 - -#### Acceptance Criteria - -1. THE CLI SHALL 以 Node.js/TypeScript 實作,套件名 `arcrun`,bin 名 `acr`,可透過 `npm i -g arcrun` 安裝,依賴只使用 `commander`、`js-yaml`、`chalk`、`ora`。 -2. THE `acr init` 指令 SHALL 以互動式問答產生 `~/.arcrun/config.yaml`,問答內容為:CF Account ID、USER_KV namespace ID、CF API Token(用於 cypher-executor 代存取用戶 KV)、email(取得 arcrun.dev API Key);並建立空白本機 `credentials.yaml`。 -3. THE `acr creds push [credentials.yaml]` 指令 SHALL 讀取 credentials.yaml,逐一加密上傳至用戶自己的 USER_KV,並顯示每個 key 的上傳結果。 -4. THE `acr push ` 指令 SHALL 解析 `flow:` 三元組,轉換成執行圖,連同 `config:` 存入 `WEBHOOKS KV`,並輸出 webhook URL。 -5. THE `acr run [--input key=value...]` 指令 SHALL 觸發 cypher-executor 執行指定 workflow,顯示各節點執行結果;失敗時顯示具體節點、原因與修復步驟。 -6. THE `acr validate ` 指令 SHALL 在執行前驗證:YAML 格式、關係詞合法性(無 PIPE)、所有節點在 config 中有對應、所有零件存在於 WASM_BUCKET、所有 credentials 已上傳至 CREDENTIALS_KV。 -7. THE `acr parts` 指令 SHALL 列出所有可用零件(按類型分組),顯示每個零件的必填欄位與所需 credential。 -8. THE `acr parts scaffold ` 指令 SHALL 從 contract 的 `config_example` 輸出可直接貼入 workflow.yaml 的 config 範本,以及對應的 credentials.yaml 欄位範本。 -9. THE `acr list` 指令 SHALL 列出 WEBHOOKS KV 中所有已上傳的 workflow,顯示名稱與更新時間。 -10. THE `acr logs ` 指令 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 ` 指令 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 ` 指令 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,讓貢獻者清楚知道零件的可用範圍。 diff --git a/.agents/specs/arcrun-core-mvp/tasks.md b/.agents/specs/arcrun-core-mvp/tasks.md deleted file mode 100644 index 477a015..0000000 --- a/.agents/specs/arcrun-core-mvp/tasks.md +++ /dev/null @@ -1,206 +0,0 @@ -# Implementation Plan: arcrun MVP - -## Overview - -依照 Design 的七個 Phase 實作。原則:最小異動,不重寫現有邏輯,只 cherry-pick + carve-out + supplement。 -所有 Phase 1–3 工作在 `matrix` repo 對應目錄驗證後再搬到新 repo。 - -**PR #2(claude/review-mvp-specs-8Bvdu)狀態:** 初始實作已提交,已修復以下問題後準備 merge: -- CF API Token 傳至 arcrun.dev 安全問題(已修復) -- 加密 fallback 格式不相容(已修復) -- submitComponent KBDB 依賴(已修復,改用 SUBMISSIONS_KV) -- Webhook 路由缺 analytics(已修復) -- `require()` 在 ES module 中(已修復) -- api 類零件 `no_network_syscall: true` 錯誤(已修復) - ---- - -## Phase 1:搬移與清理 - -- [x] 1. 建立 `arcrun` 獨立 repo 並初始化 - - [x] 1.1 在 GitHub 建立新的 public repo(使用 matrix monorepo 的 `arcrun/` 子目錄代替,PR #2) - - [x] 1.2 設定 `.gitignore`(排除 `node_modules/`、`.wrangler/`、`credentials.yaml`、`~/.arcrun/`) - - [x] 1.3 從 `matrix` cherry-pick 四個目錄: - - `matrix/cypher-executor/` → `arcrun/cypher-executor/` - - `matrix/u6u-core/credentials/` → `arcrun/credentials/` - - `matrix/u6u-core/registry/components/` → `arcrun/registry/components/` - - _Requirements: 1.6_ - -- [x] 2. 清理 `cypher-executor/wrangler.toml` - - [x] 2.1 移除 9 個 InkStone Service Bindings(KBDB、REGISTRY、CLINIC_*、AICEO、MINI_ME) - - [x] 2.2 確認保留:`EXEC_CONTEXT`、`WEBHOOKS`、`WASM_BUCKET`、`AI` - - [x] 2.3 新增 `CREDENTIALS_KV` 與 `ANALYTICS_KV` KV namespace binding - - [x] 2.4 更新 `name` 為 `arcrun-cypher-executor` - - _Requirements: 1.1, 1.2, 1.3_ - -- [x] 3. 改寫 `cypher-executor/src/lib/component-loader.ts` - - [x] 3.1 移除對 MINI_ME、KBDB、InkStone bindings 的 hardcode - - [x] 3.2 實作三層邏輯:builtin Map → WASM_BUCKET R2 直讀 → 結構化錯誤 - - _Requirements: 1.4, 1.5_ - -- [x] 4. 移除對 InkStone bindings 的依賴程式碼 - - [x] 4.1 刪除 `autoPublishMissing.ts`(依賴 REGISTRY binding) - - [x] 4.2 移除所有 `env.KBDB`、`env.REGISTRY`、`env.MINI_ME`、`env.AICEO`、`env.CLINIC_*` 引用 - - _Requirements: 1.1, 1.5_ - -- [ ] 5. 本機驗證 - - [ ] 5.1 `cd arcrun/cypher-executor && wrangler dev` 能啟動(無 binding 錯誤) - - [ ] 5.2 `GET /health` 回傳 `{ ok: true }` - - [ ] 5.3 上傳 `validate_json.wasm` 到 WASM_BUCKET,執行 `POST /execute` 能正常回傳結果 - - _Requirements: 1.5, 5.5_ - ---- - -## Phase 2:零件完整度補充 - -- [x] 6. api 類零件 `no_network_syscall` 修正 - - [x] 6.1 gmail、telegram、google_sheets、line_notify、http_request 改為 `no_network_syscall: false` - - _Requirements: 2.1_ - -- [ ] 7. 審查 21 個零件 contract.yaml 並補充 `credentials_required` - - [ ] 7.1 確認 gmail、google_sheets、telegram、line_notify 有 `credentials_required`(PR #2 已加入,需驗證格式正確) - - [ ] 7.2 確認所有 21 個零件有 `config_example` 欄位 - - [ ] 7.3 驗證 `main.go` required 欄位與 `contract.yaml` input_schema.required[] 一致 - - _Requirements: 2.1, 2.2, 2.3_ - ---- - -## Phase 3:Credential 注入整合 - -- [x] 10. `credential-injector.ts` 已實作(`arcrun/cypher-executor/src/actions/credential-injector.ts`) - - [x] 10.1 讀取 contract.yaml from R2,解析 `credentials_required` - - [x] 10.2 從 `CREDENTIALS_KV` 讀取 AES-GCM 加密 token,注入到 input 對應欄位(inject_as) - - [x] 10.3 credential 不存在時拋出結構化錯誤(含 key 名稱與修復步驟) - - _Requirements: 3.4, 3.5, 3.6_ - -- [ ] 11. 驗證 credential 注入整合進 graph-executor - - [ ] 11.1 確認 `graph-executor.ts` 在節點執行前正確呼叫 `injectCredentials` - - [ ] 11.2 確認注入只影響 WASM input,不修改 WEBHOOKS KV 中儲存的 workflow 定義 - - _Requirements: 3.4, 3.5_ - -- [ ] 12. 端對端測試(手動) - - [ ] 12.1 建立 `credentials.yaml`,加入測試 token - - [ ] 12.2 執行 `acr creds push`,確認寫入 CREDENTIALS_KV 格式為 `{ encrypted, iv }`(無 `mode: 'base64'`) - - [ ] 12.3 執行含 credential 的 workflow,確認 inject_as 欄位正確注入 - - _Requirements: 3.4, 3.5_ - ---- - -## Phase 4:CLI 開發 - -- [x] 13. CLI 專案骨架已建立(`arcrun/cli/`) - - [x] 13.1 `package.json`(name: `arcrun`,bin: `acr`) - - [x] 13.2 `tsconfig.json`(module: NodeNext) - - [x] 13.3 所有 10 個指令已實作骨架 - - _Requirements: 4.1_ - -- [x] 14. `acr init` 已實作,修正項: - - [x] 14.1 Standard 模式不再傳送 `cf_api_token` 至 arcrun.dev(只傳 `email`) - - [x] 14.2 `require()` 改用 `await import()` 修正 ES module 相容 - - [ ] 14.3 **待補**:`acr init` 需詢問 `ARCRUN_ENCRYPTION_KEY` 並寫入 config(目前加密 key 需手動設定) - - _Requirements: 4.2, 6.3_ - -- [x] 15. `acr creds push` 已實作 - - [x] 15.1 讀取 `credentials.yaml`,AES-GCM 加密後寫入用戶 CF KV(`cred:{name}`) - - [x] 15.2 加密 fallback(base64)已移除,key 不足時直接拋錯提示生成指令 - - _Requirements: 4.3, 6.5_ - -- [x] 16. `acr push` 已實作 - - _Requirements: 4.4_ - -- [x] 17. `acr run` 已實作 - - _Requirements: 4.5_ - -- [ ] 18. `acr validate` credential 檢測邏輯有誤,需修復 - - [ ] 18.1 `extractCredentialRefs()` 目前掃描 `{{creds.xxx}}` 語法,但 injection 使用 `inject_as` key - - [ ] 18.2 改為讀取 contract.yaml 的 `credentials_required[].key`,與 `cred:{key}` KV 存在性比對 - - _Requirements: 4.6_ - -- [x] 19. `acr parts`、`acr parts scaffold`、`acr parts publish` 已實作 - - [ ] 19.1 `acr parts` 中 YAML 解析改用 `js-yaml`(目前用 regex,可能解析失敗) - - _Requirements: 4.7, 4.8_ - -- [x] 20. `acr list` 與 `acr logs` 已實作 - - _Requirements: 4.9, 4.10_ - ---- - -## Phase 5:開源發布準備 - -- [x] 21. README.md 已撰寫(`arcrun/README.md`) -- [x] 22. CONTRIBUTING.md 已撰寫(`arcrun/CONTRIBUTING.md`) -- [ ] 23. 安全審查(PR merge 前執行) - - [ ] 23.1 搜尋 `.workers.dev` InkStone 網域 - - [ ] 23.2 確認 wrangler.toml 所有 KV id 欄位留空 - - [ ] 23.3 確認 `credentials.yaml` 在 `.gitignore` 中 - - _Requirements: 5.4_ - -- [ ] 24. 發布(安全審查後) - - [ ] 24.1 `npm publish`(CLI package `arcrun`) - - _Requirements: 5.1_ - ---- - -## Phase 6:Standard 模式 — auth-worker 與用戶 KV 代存取 - -- [ ] 25. 建立 `auth-worker`(新 Worker,部署至 `api.arcrun.dev`) - - [ ] 25.1 建立 `auth-worker/` 目錄,初始化 Hono + wrangler.toml - - [ ] 25.2 實作 `POST /register`:接收 `{ email, account_id, kv_namespace_id }` + CF API Token 透過 header 傳入 - - **不在 request body 中接收 CF API Token**(Token 透過 header `CF-Api-Token` 傳入,減少 TLS 以外的洩漏面) - - 生成 `tenant_id` 與 `api_key`,存入 `ACCOUNTS_KV` - - [ ] 25.3 Bindings:`ACCOUNTS_KV` - - _Requirements: 6.1, 6.2_ - -- [ ] 26. 改造 `cypher-executor` 支援 multi-tenant 用戶 KV 代存取 - - [ ] 26.1 讀取 `MULTI_TENANT` env var(目前已宣告但未讀取),實作 tenant middleware - - [ ] 26.2 `X-Arcrun-API-Key` → 查 `ACCOUNTS_KV` → 取得用戶 cf_api_token + kv_namespace_id → 建立 `CfKvClient` - - [ ] 26.3 `CfKvClient` 已實作(`arcrun/cli/src/lib/cf-api.ts`),需移植到 `cypher-executor/src/lib/` - - [ ] 26.4 `credential-injector.ts` 改用 userKv 取得加密 credential - - [ ] 26.5 webhook 路由注入 userKv - - _Requirements: 6.4, 6.5, 6.6_ - -- [ ] 27. 端對端測試(用戶 KV 隔離) - - _Requirements: 6.4, 6.5_ - ---- - -## Phase 7:公眾零件統計與貢獻審核 - -- [x] 28. Analytics 基礎設施已建立 - - [x] 28.1 `execution-logger.ts` 建立,`writeExecutionVerdict` 寫入 `ANALYTICS_KV`(fire-and-forget) - - [x] 28.2 `/execute` 路由已整合 `waitUntil(writeExecutionVerdict(...))` - - [x] 28.3 `/webhooks/:token/trigger` 路由已補上 `waitUntil(writeExecutionVerdict(...))` - - _Requirements: 7.2_ - -- [ ] 29. registry Worker analytics 端點 - - [ ] 29.1 新增 `POST /analytics/record` 路由,原子更新 `ANALYTICS_KV` - - [ ] 29.2 `GET /components` 回傳加入 `total_runs`、`success_rate`、`avg_duration_ms` - - _Requirements: 7.3, 7.6_ - -- [x] 30. `author` 欄位已加入 contract.yaml 規格 - - _Requirements: 7.1_ - -- [x] 31. 零件提交審核流程已實作(`arcrun/registry/src/actions/submitComponent.ts`) - - [x] 31.1 沙盒驗收流程(sandboxAcceptance.ts):size_check + syscall_scan 已實作;cold_start + gherkin_tests 為 Phase 0 mock - - [x] 31.2 `SUBMISSIONS_KV` 儲存元數據,預設 `visibility: author_only` - - [ ] 31.3 `PATCH /submit/:id/approve` → 將 visibility 改為 `public`(待實作) - - [ ] 31.4 Gherkin 測試執行(取代 mock) - - _Requirements: 8.2, 8.3, 8.4, 8.5_ - ---- - -## 待辦(無相依順序,可平行處理) - -- [ ] A. `builtins/` 清理:`initComponents.ts` 仍用舊的 HTTP endpoint 模式上架零件(`buildComponentDefs` 含 URL),應改為呼叫 `POST /submit` 送 WASM binary + contract,或直接移除 builtins(功能已整合到 registry) -- [ ] B. `validate` 指令 credential 檢測邏輯修復(見 Phase 4 Task 18) -- [ ] C. `acr init` 加入 `ARCRUN_ENCRYPTION_KEY` 設定步驟 -- [ ] D. `acr parts` YAML 解析改用 `js-yaml` - ---- - -## Notes - -- 標記 `*` 的子任務為選填,可跳過以加速 MVP 交付 -- Gherkin 測試執行(sandbox 步驟 d)為 Phase 0 mock,Phase 7 補充 -- cold-start 測量(sandbox 步驟 b)為 Phase 0 mock,Phase 2 補充 -- CF API Token 永遠不離開用戶本機,arcrun.dev 只收 email + account_id + kv_namespace_id diff --git a/.agents/specs/arcrun-platform-evolution/design.md b/.agents/specs/arcrun-platform-evolution/design.md deleted file mode 100644 index bd59887..0000000 --- a/.agents/specs/arcrun-platform-evolution/design.md +++ /dev/null @@ -1,822 +0,0 @@ -# Design Document: u6u Platform Evolution - -## Overview - -u6u 平台演進的核心目標是將現有的「HTTP endpoint 零件 + 單一 Cloudflare 部署」架構,演進為「WASM 零件模型 + 三層物理部署 + 雙面翻轉畫布」的完整平台。 - -設計的最高原則是 **Dogfooding**:每一層都是下一層的第一個用戶。底層先建立最小可運行的能力,再用自己的方式往上蓋。這確保每個設計決策都被真實使用場景驗證,而非紙上談兵。 - -### Bootstrap 順序(不可跳過) - -``` -Phase 0:最小 WASM 執行核心 - → Component Dispatcher 能在 CF Workers 執行一個 .wasm(stdin/stdout JSON) - → validate_json.wasm 作為第一個真實零件(TinyGo,< 50KB,驗證整個 pipeline) - → Component Registry API(/guide、/validate-contract、/components) - -Phase 1:遷移現有零件 - → 將 u6u-builtins 的 20 個 HTTP endpoint 逐一遷移為 .wasm - → 遷移期間 Component Dispatcher 雙模式並存(HTTP fallback) - → 每個零件附帶 component.contract.yaml - -Phase 2:Cypher 語意擴展 + Multi-Tier Dispatcher - → 支援 IS_A、ON_SUCCESS、ON_FAIL、CALLS_SUBFLOW、ON_CLICK - → Component Dispatcher 路由層(Tier 1 CF / Tier 2 workerd / Tier 3 Wazero) - -Phase 3:前端畫布(用自己的 Web Components 開發) - → 先建立 Web Components 零件庫(u6u-btn、u6u-card 等) - → 畫布本身用這些 Web Components 組裝 - → 雙面翻轉介面 -``` - -### 關鍵設計約束 - -- **KBDB 不變量**:永遠只有三張表(blocks / templates / slots),不新增表 -- **API-First 鐵律**:所有跨服務通訊只透過 HTTP API,禁止相對路徑引用 -- **零件 I/O 不變量**:唯一合法的 I/O 模型是 `stdin_stdout_json` -- **Tier 3 約束**:無 V8、無 Node.js、無網路,所有零件必須在 Wazero 上跑 - ---- - -## Architecture - -### 系統全景圖 - -```mermaid -graph TB - subgraph "Tier 1 — Cloudflare Workers(雲端)" - CE[Cypher Executor
GraphExecutor] - CD[Component Dispatcher
路由層] - CR[Component Registry
KBDB HTTP API] - KBDB[(KBDB
blocks/templates/slots
+ Vectorize)] - R2[(R2
.wasm 二進位)] - CE --> CD - CD --> CR - CR --> KBDB - CR --> R2 - end - - subgraph "Tier 2 — workerd self-hosted(企業地端)" - T2D[Tier 2 Dispatcher
同 wasi-shim,不同部署] - T2R[本地 Registry 快取] - T2D --> T2R - end - - subgraph "Tier 3 — 邊緣載具" - T3E[Go 排程引擎] - Wazero[Wazero Runtime] - SQLite[(SQLite
本地 KBDB)] - DTN[DTN 佇列] - T3E --> Wazero - T3E --> SQLite - T3E --> DTN - end - - subgraph "前端" - Canvas[雙面翻轉畫布
React 19 + Web Components] - WC[Web Components 零件庫
u6u-btn / u6u-card / ...] - Canvas --> WC - end - - CD -->|WASM 執行| Tier1WASM[.wasm 執行] - CD -->|Cypher binding| ExtSvc[外部服務
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
取得合約} - B --> C{component_type?} - C -->|wasm| E{當前 Tier?} - C -->|service_binding| SB{有 CF Service Binding?} - SB -->|是| SBExec[CF Service Binding 執行
需 deploy,效能最佳] - SB -->|否| SBErr[回傳錯誤:binding 未宣告] - E -->|Tier 1 / Tier 2| I[workerd WASM
WebAssembly.instantiate
+ WASI shim(兩者相同)] - E -->|Tier 3| L[Wazero IPC
stdin/stdout,完全離線] - I --> RC{runtime_compat
包含 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, kvNamespace); - result = { ...(result as Record), ...subResult.data as Record }; - break; -} -``` - -### 4. Web Components 零件庫(`u6u-core/web-components/`) - -Web Components 以原生 Custom Elements API 實作,不依賴任何框架。 - -**`` 介面:** - -```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; - }; -} -``` - -**`` 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 = {}; - 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 設定於 `` 的 `workflow` attribute,使用者點擊後發出的 `u6u:trigger` 事件 detail 中的 `workflowId` SHALL 與 URI 中的 id 完全一致;*For any* 一組具名 `` 元件置於 `` 內,觸發事件後收集到的 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 錯誤邊界 - -`` 在 `workflow` attribute 未設定時,點擊不發出事件,僅在 console 輸出警告: - -``` -[u6u-btn] workflow attribute is not set, click event ignored -``` - ---- - -## Testing Strategy - -### 雙軌測試策略 - -本功能採用「單元測試 + 屬性測試」雙軌策略: - -- **單元測試(Vitest)**:驗證具體範例、邊界條件、錯誤情境 -- **屬性測試(fast-check)**:驗證上述 11 個 Correctness Properties,每個屬性最少執行 100 次迭代 - -### 屬性測試配置 - -使用 `fast-check`(已在 tech stack 中),每個屬性測試標記格式: - -```typescript -// Feature: arcrun-platform-evolution, Property N: {property_text} -it.prop([fc.record({ id: fc.string(), version: fc.string(), ... })])( - 'Property 1: 合約格式完整性', - (contract) => { - // ... - }, - { numRuns: 100 } -); -``` - -### 各 Phase 測試重點 - -**Phase 0(WASM 執行核心):** -- Property 4:WASM 執行路徑 round-trip(`validate_json.wasm` 作為 ground truth) -- Property 1:合約格式驗證 -- 單元測試:WASI shim 的 `fd_read`/`fd_write` 正確性 - -**Phase 1(零件遷移):** -- Property 3:提交冪等性(20 個零件各提交兩次,驗證無重複) -- Property 2:沙盒驗收流程(各步驟失敗案例) -- Property 8:歷史版本保留(deprecate 後仍可讀取) - -**Phase 2(Cypher 擴展):** -- Property 9:Confluence(三元組順序無關性,fast-check shuffle) -- Property 6:URI 解析 round-trip -- Property 7:版本選擇策略(floating 最高分、pinned 固定版本) -- Property 5:錯誤結構完整性 - -**Phase 3(前端畫布):** -- Property 10:Web Components 事件與渲染冪等性(`@testing-library/react` + fast-check) -- Property 11:評價冪等性(Evaluator Agent 重複處理) - -### 整合測試 - -以下場景使用整合測試(1-3 個具體範例,不用 PBT): - -- Tier 1 CF Workers 環境中實際執行 `validate_json.wasm`(驗證 WASM 在 Workers 環境可運行) -- KBDB tpl-component Template 建立與 Slot 讀寫(驗證 KBDB 整合) -- R2 `.wasm` 上傳與讀取(驗證 R2 整合) -- Vectorize 語意搜尋(驗證「查詢 Google Sheets 資料」能找到 `gsheets_get_entries`) - -### 單元測試重點(非 PBT) - -- WASI shim:`fd_read` 正確讀取 stdin、`fd_write` 正確寫入 stdout -- `evaluateCondition`:現有條件評估函數的邊界案例 -- `resolveComponentId`:URI 解析的邊界案例(空字串、特殊字元) -- `` Smart Container:巢狀容器的事件冒泡行為 diff --git a/.agents/specs/arcrun-platform-evolution/requirements.md b/.agents/specs/arcrun-platform-evolution/requirements.md deleted file mode 100644 index d9fd31e..0000000 --- a/.agents/specs/arcrun-platform-evolution/requirements.md +++ /dev/null @@ -1,219 +0,0 @@ -# Requirements Document - -## Introduction - -u6u 平台演進規格描述從現況(HTTP endpoint 零件、單一 Cloudflare 部署、無前端畫布) -到目標架構(WASM 零件模型、三層物理部署、雙面翻轉畫布)的完整演進路徑。 - -本規格涵蓋三個核心演進軸: - -1. **零件模型遷移**:將 20 個內建零件從 Cloudflare Worker HTTP endpoint 遷移至 WASI preview1 `.wasm` 格式,附帶 `component.contract.yaml`,以 stdin/stdout JSON 作為唯一 I/O 模型。 -2. **多 Tier 執行抽象**:讓 Cypher Executor 透過統一的 Component Dispatcher 介面,跨 Tier 1(Cloudflare Workers)、Tier 2(workerd 地端叢集)、Tier 3(Go + Wazero 邊緣載具)呼叫零件。 -3. **前端雙面畫布**:建立以 Web Components 為基礎的視覺化畫布,正面為 UI 視圖,反面為 Cypher 邏輯視圖,智慧容器自動打包表單值。 - ---- - -## Glossary - -- **Component(零件)**:系統最小執行單元,一個零件只做一件事,以 `.wasm`(WASI preview1)或 Web Component 形式存在。 -- **Component_Contract**:每個零件附帶的 `component.contract.yaml`,定義 id、version、wasi_target、stability、runtime_compat、constraints、input_schema、output_schema、gherkin_tests。 -- **Component_Registry**:KBDB 中儲存所有零件合約與 `.wasm` 位置的索引,以 `tpl-component` Template Block 實作。 -- **Component_Dispatcher**:Cypher Executor 內部的路由層,根據零件的 `runtime_compat` 與目標 Tier 決定呼叫路徑(Service Binding / workerd HTTP / Wazero IPC)。 -- **Cypher_Executor**:Workflow 執行引擎,解析三元組語法,透過 GraphExecutor 執行節點,現部署於 Cloudflare Workers。 -- **Cypher_Triplet**:`"A >> 關係 >> B"` 格式的三元組,描述節點間的語意關係。 -- **KBDB**:三位一體記憶庫(blocks / templates / slots),搭配 Cloudflare Vectorize,是平台唯一的持久化狀態來源。 -- **Tier_1**:雲端層,Cloudflare Workers + D1 + Vectorize + R2,全球無伺服器部署。 -- **Tier_2**:企業地端層,workerd 叢集 + Kùzu 或 PostgreSQL + AGE,高機密內網環境。 -- **Tier_3**:邊緣載具層,Go 排程引擎 + 內嵌 Wazero + SQLite,無 V8、無網路的極限環境(無人機、AGV)。 -- **WASI_Preview1**:WebAssembly System Interface preview1 規格,零件唯一合法的 WASM 目標格式。 -- **Canvas(畫布)**:前端雙面翻轉介面,正面為 UI 視圖,反面為 Cypher 邏輯視圖。 -- **Smart_Container**:畫布上的排版容器(如 ``),自動打包同容器內所有輸入元件的值並附加至觸發事件的 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:``(按鈕)、``(文字輸入)、``(多行文字)、``(圖表)、``(智慧容器)。 -2. THE `` SHALL 支援以下 HTML attributes:`label`(顯示文字)、`color`(主題色)、`tooltip`(滑鼠懸停提示,純靜態,不觸發 Webhook)、`workflow`(綁定的 Workflow URI,格式為 `workflow://id`)。 -3. WHEN `` 的 `workflow` attribute 被設定且使用者點擊按鈕,THE `` SHALL 發出 `u6u:trigger` 自訂事件,事件 detail 包含 `{ workflowId, payload }`。 -4. THE `` SHALL 在接收到子元件的 `u6u:trigger` 事件時,自動收集同容器內所有 `` 與 `` 的當前值,合併至事件的 `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 零件拖入同一個 `` 容器,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 資料不變的情況下,對相同查詢參數回傳相同結果(查詢冪等性)。 diff --git a/.agents/specs/arcrun-platform-evolution/tasks.md b/.agents/specs/arcrun-platform-evolution/tasks.md deleted file mode 100644 index f568ec1..0000000 --- a/.agents/specs/arcrun-platform-evolution/tasks.md +++ /dev/null @@ -1,411 +0,0 @@ -# Implementation Plan: u6u Platform Evolution - -## Overview - -依照 Bootstrap 順序分四個 Phase 實作,每個 Phase 都是下一個 Phase 的基礎。 -技術棧:TypeScript、Hono、Zod、Vitest、fast-check,部署於 Cloudflare Workers。 - ---- - -## Phase 0:最小 WASM 執行核心 - -- [x] 1. 建立 Component Registry 基礎架構(`u6u-core/registry/`) - - [x] 1.1 建立 `tpl-component` Template Block(透過 KBDB HTTP API) - - 呼叫 KBDB `/templates` 建立 `tpl-component` template(若不存在) - - 定義所有 slot keys(canonical_id、display_name、category、version、wasi_target、stability、runtime_compat、constraints、input_schema、output_schema、gherkin_tests、wasm_r2_key、cypher_binding_url、service_binding_key、description、tags、success_rate、avg_duration_ms、call_count、status、deprecated_at) - - _Requirements: 12.1_ - - - [x] 1.2 實作 `POST /components/validate-contract` 端點 - - 以 Zod schema 驗證 component.contract.yaml 所有必填欄位 - - 回傳缺失欄位清單(`missing_fields: string[]`) - - _Requirements: 1.1, 1.2, 11.5_ - - - [ ]* 1.3 寫 property test for 合約格式完整性 - - **Property 1: 合約格式完整性** - - **Validates: Requirements 1.1, 1.2, 1.4** - - 用 fast-check 生成隨機缺少任意必填欄位的合約物件,驗證 validator 必定拒絕並回傳該欄位名稱 - - 用 fast-check 生成包含所有必填欄位的合約物件,驗證 validator 必定通過 - - - [x] 1.4 實作 `GET /components/guide` 端點 - - 回傳 Markdown 格式開發指引(TinyGo 白名單、禁止行為、contract YAML 範例、wasmtime 測試指令) - - _Requirements: 11.1, 11.2, 11.3_ - - - [x] 1.5 實作 `POST /components` 零件提交端點(沙盒驗收流程) - - 依序執行五個驗收步驟:(a) 體積檢查、(b) 冷啟動時間測量、(c) syscall 掃描、(d) Gherkin 測試執行、(e) runtime 相容測試 - - 任一步驟失敗立即停止,回傳 `{ success: false, failed_step, reason, guide_anchor, component_id, version }` - - 通過後以 KBDB HTTP API 寫入 Block(`block_id = comp-{id}-{version}`) - - 同時上傳 `.wasm` 至 R2,slot `wasm_r2_key` 記錄 R2 key - - _Requirements: 2.1, 2.2, 2.3_ - - - [ ]* 1.6 寫 property test for 沙盒驗收流程正確性 - - **Property 2: 沙盒驗收流程正確性** - - **Validates: Requirements 2.1, 2.2** - - 用 fast-check 生成在步驟 N 失敗的零件,驗證回應包含步驟 N 名稱與原因,且不執行步驟 N+1 - - - [ ]* 1.7 寫 property test for 零件提交冪等性與持久性 - - **Property 3: 零件提交冪等性與持久性** - - **Validates: Requirements 2.3, 2.4** - - 用 fast-check 生成通過驗收的零件,提交後讀取合約驗證所有欄位 round-trip 一致 - - 對相同 (id, version) 重複提交 N 次,驗證 KBDB 只存在一個 Block - -- [x] 2. 實作 WASI preview1 shim 與 WASM 執行核心(`cypher-executor/src/lib/`) - - [x] 2.1 實作輕量 WASI preview1 shim(`wasi-shim.ts`) - - 實作 `fd_read`(從 stdin buffer 讀取)、`fd_write`(寫入 stdout/stderr buffer)、`proc_exit`(拋出 Error)、`random_get`(`crypto.getRandomValues`) - - 其餘 syscall 一律回傳 ENOSYS(76) - - 不引入任何外部依賴(不使用 `@cloudflare/workers-wasi`) - - _Requirements: 3.1, 3.3_ - - - [x]* 2.2 寫單元測試 for WASI shim - - 測試 `fd_read` 正確讀取 stdin buffer(含多次讀取、邊界條件) - - 測試 `fd_write` 正確寫入 stdout buffer(fd=1)與 stderr buffer(fd=2) - - 測試 `proc_exit` 拋出 Error - - - [x] 2.3 實作 Tier 1 WASM 執行器(`wasm-executor.ts`) - - 從 R2 fetch `.wasm` ArrayBuffer - - `WebAssembly.compile` 後快取 `WebAssembly.Module`(Worker 記憶體,跨請求共享) - - 建立 WASI shim,注入 stdin = `JSON.stringify(input)` - - `WebAssembly.instantiate(module, imports)` → 呼叫 `_start()` 或 `main()` - - 從 stdout buffer 讀取輸出,`JSON.parse` 後回傳 - - 套用 `max_cold_start_ms` 逾時(`Promise.race`) - - _Requirements: 3.1, 3.3, 6.6_ - - - [ ]* 2.4 寫 property test for Component Dispatcher 路由正確性 - - **Property 4: Component Dispatcher 路由正確性** - - **Validates: Requirements 3.1, 3.5, 3.6** - - 用 fast-check 生成合法 JSON input,驗證 WASM 執行路徑輸出與預期語意等效 - -- [x] 3. 建立 `validate_json.wasm` 第一個真實零件(TinyGo) - - [x] 3.1 撰寫 `validate_json` TinyGo 原始碼(`u6u-core/registry/components/validate_json/main.go`) - - 只使用白名單 import:`os`、`io`、`encoding/json` - - 讀取 stdin JSON,解析 `json_string` 欄位,嘗試 `json.Unmarshal` - - 成功輸出 `{"valid":true}`,失敗輸出 `{"valid":false,"error":"..."}` - - _Requirements: 3.1, 11.6_ - - - [x] 3.2 撰寫 `validate_json` component.contract.yaml - - 包含所有必填欄位、gherkin_tests(happy path + error path) - - `runtime_compat: ["cf-workers","workerd","wazero"]` - - _Requirements: 1.1, 1.2, 1.4_ - - - [ ]* 3.3 寫單元測試 for validate_json(Gherkin 場景驗證) - - 測試合法 JSON 輸入回傳 `{"valid":true}` - - 測試非法 JSON 輸入回傳 `{"valid":false,"error":...}` - -- [x] 4. Checkpoint — Phase 0 驗收 - - 確認 `validate_json.wasm` 能在 CF Workers 環境中透過 WASM 執行器執行 - - 確認 Component Registry `/guide`、`/validate-contract`、`/components` 端點可用 - - 確認所有 Phase 0 測試通過,向使用者確認是否繼續 Phase 1 - ---- - -## Phase 1:遷移現有零件(20 個 HTTP → WASM) - -- [x] 5. 升級 Component Dispatcher 支援雙模式(`cypher-executor/src/lib/component-loader.ts`) - - [x] 5.1 重構 `ComponentDescriptor` 型別(移除舊 `http_endpoint`,新增 `component_type`) - - 定義 `ComponentType = 'wasm' | 'cypher_binding' | 'service_binding'` - - 新版 `ComponentDescriptor` 欄位:`component_type`、`wasm_r2_key`、`runtime_compat`、`max_cold_start_ms`、`url`(cypher_binding)、`method`、`binding`(service_binding)、`path` - - _Requirements: 3.5_ - - - [x] 5.2 實作路由決策邏輯(`component-dispatcher.ts`) - - 查 Component Registry 取得合約 - - 依 `component_type` 分流:`wasm` → WASM 執行器;`cypher_binding` → HTTP POST 到外部 URL;`service_binding` → CF Service Binding - - 檢查 `runtime_compat` 是否包含當前 Tier,不包含則回傳 `RUNTIME_INCOMPATIBLE` 錯誤 - - _Requirements: 3.4, 6.1, 6.2_ - - - [ ]* 5.3 寫 property test for Dispatcher 錯誤結構完整性 - - **Property 5: Dispatcher 錯誤結構完整性** - - **Validates: Requirements 3.4, 6.4** - - 用 fast-check 生成 (component_id, tier) 組合,當 runtime_compat 不含當前 tier,驗證錯誤回應同時包含 component_id、tier、attempted_paths 三個欄位 - -- [x] 6. 遷移 20 個內建零件(`u6u-core/builtins/` → `u6u-core/registry/components/`) - - [x] 6.1 為每個零件撰寫 TinyGo 原始碼與 component.contract.yaml(批次作業) - - 每個零件:只用白名單 import、stdin/stdout JSON I/O、附帶 gherkin_tests - - 需要外部 API 的零件(如 gsheets):改用 `cypher_binding` 模式,contract 中記錄 `cypher_binding_url` - - _Requirements: 3.6, 11.6_ - - - [x] 6.2 透過 `POST /components` 批次提交 20 個零件至 Component Registry - - 每個零件通過沙盒驗收後自動寫入 KBDB - - 驗證 20 個零件的 Gherkin 測試全部通過 - - _Requirements: 2.1, 3.6_ - - - [ ]* 6.3 寫 property test for 歷史版本永久保留不變量 - - **Property 8: 歷史版本永久保留不變量** - - **Validates: Requirements 4.5, 10.5** - - 用 fast-check 生成已上架零件,標記為 deprecated 後,驗證 `.wasm` 仍可從 R2 讀取,pinned 引用仍可執行 - -- [x] 7. 實作 Component Registry 查詢端點 - - [x] 7.1 實作 `GET /components/:id` 與 `GET /components/:id/versions` - - 透過 KBDB HTTP API 查詢 `tpl-component` blocks - - `/versions` 回傳按「成功率 × 速度評分 × 被調用次數」排序的版本清單(最多 10 個) - - _Requirements: 12.3_ - - - [x] 7.2 實作 `GET /components/search?q=...` 語意搜尋 - - 呼叫 KBDB Vectorize API,以 `description` + `tags` 欄位做語意搜尋 - - _Requirements: 12.2_ - - - [ ]* 7.3 寫單元測試 for 查詢冪等性 - - 驗證相同查詢參數在 KBDB 資料不變時回傳相同結果 - -- [x] 8. Checkpoint — Phase 1 驗收 - - 確認 20 個零件全部通過沙盒驗收並存入 KBDB - - 確認 Component Dispatcher 雙模式路由正確(WASM + cypher_binding) - - 確認所有 Phase 1 測試通過,向使用者確認是否繼續 Phase 2 - ---- - -## Phase 2:Cypher 語意擴展 + Multi-Tier Dispatcher - -- [x] 9. 擴展 Cypher Triplet Parser(`cypher-executor/src/actions/triplet-parser.ts`) - - [x] 9.1 新增 EdgeType 定義 - - 在現有 `PIPE | IF | FOREACH | CONTINUE` 基礎上新增:`IS_A | ON_SUCCESS | ON_FAIL | ON_CLICK | CALLS_SUBFLOW | CONTAINS | HAS_STYLE | HAS_BEHAVIOR` - - _Requirements: 5.1_ - - - [x] 9.2 實作 URI 協議解析函數(`resolveComponentId`) - - 解析 `component://id`、`component://id@stable`、`component://id@pinned:vN`、`workflow://id`、`ui://id`、`style://id` - - 回傳 `{ type, canonicalId, stability, pinnedVersion? }` - - _Requirements: 4.1, 5.6_ - - - [ ]* 9.3 寫 property test for 零件 URI 解析 Round-Trip - - **Property 6: 零件 URI 解析 Round-Trip** - - **Validates: Requirements 4.1** - - 用 fast-check 生成合法 URI 字串,解析後再序列化,驗證語意等效;驗證解析出的 id、stability、pinnedVersion 與原始 URI 完全一致 - - - [ ]* 9.4 寫 property test for Cypher 三元組解析 Confluence - - **Property 9: Cypher 三元組解析 Confluence(順序無關性)** - - **Validates: Requirements 5.7** - - 用 fast-check 生成合法三元組集合,用 `fc.shuffledSubarray` 打亂順序,驗證 `parseTriplets` 產生相同節點集合與邊集合 - -- [x] 10. 擴展 GraphExecutor 執行語意(`cypher-executor/src/graph-executor.ts`) - - [x] 10.1 實作 `IS_A` 關係處理 - - 從 Component Registry 載入零件合約,以 `input_schema` 驗證節點輸入 context - - _Requirements: 5.2_ - - - [x] 10.2 實作 `ON_SUCCESS` / `ON_FAIL` 分支執行 - - 在 `executeNode` 的 try/catch 中,成功走 `ON_SUCCESS` 邊,失敗走 `ON_FAIL` 邊(傳遞 error context) - - _Requirements: 5.3_ - - - [x] 10.3 實作 `CALLS_SUBFLOW` 子流程呼叫 - - 從 KBDB 載入子 Workflow 定義,建立子 GraphExecutor 執行,將輸出合併回主流程 context - - _Requirements: 5.4_ - - - [x] 10.4 實作 `ON_CLICK` 前端觸發處理 - - 接受來自前端 Smart Container 打包的 payload,作為 Workflow initialContext - - _Requirements: 5.5_ - - - [x] 10.5 實作 `CONTAINS` / `HAS_STYLE` / `HAS_BEHAVIOR` 結構語意解析(不執行,僅記錄圖結構) - - _Requirements: 5.1_ - -- [x] 11. 實作版本選擇策略(Component Dispatcher 升級) - - [x] 11.1 實作 floating 版本選擇算法 - - 從 KBDB 查詢該 id 下所有版本,計算「成功率 × 速度評分 × 被調用次數」,選取最高分版本 - - _Requirements: 4.2_ - - - [x] 11.2 實作 stable / pinned 版本選擇 - - `stable`:使用當前標記為 stable 的版本,有更優版本時記錄提示至 KBDB 但不切換 - - `pinned:vN`:永遠使用版本 vN,即使已 deprecated - - _Requirements: 4.3, 4.4_ - - - [ ]* 11.3 寫 property test for 版本選擇策略正確性 - - **Property 7: 版本選擇策略正確性** - - **Validates: Requirements 4.2, 4.4** - - 用 fast-check 生成版本集合(各有不同 success_rate、avg_duration_ms、call_count),驗證 floating 選最高分;驗證 pinned:vN 無論其他版本評分如何永遠選 vN - -- [x] 12. 實作 Evaluator Agent 與評價迴圈(`cypher-executor/src/actions/`) - - [x] 12.1 實作 `execution-evaluator.ts`(擴展現有 `execution-logger.ts`) - - Workflow 執行完畢後非同步寫入 KBDB Evaluation Block(`tpl-evaluation`) - - 記錄:run_id、workflow_id、component_id、verdict、duration_ms、error_message、evaluated_at - - 冪等處理:相同 run_id 不重複建立 Block - - _Requirements: 10.1, 10.6_ - - - [x] 12.2 實作 Pitfall Block 建立邏輯 - - 偵測某零件連續 5 次執行錯誤率 > 50%,建立 `tpl-pitfall` Block - - 版本選擇時降低有 Pitfall 記錄的版本評分權重 - - _Requirements: 10.2, 10.3_ - - - [x] 12.3 實作零件自動 Deprecated / Tombstone 狀態轉換 - - 連續 30 天無引用 → 標記 `deprecated`,從預設搜尋移除 - - 再 90 天無引用 → 標記 `tombstone`,從所有搜尋移除(pinned `.wasm` 永久保留) - - _Requirements: 10.4, 10.5_ - - - [ ]* 12.4 寫 property test for 系統操作冪等性 - - **Property 11: 系統操作冪等性** - - **Validates: Requirements 10.6, 12.5** - - 用 fast-check 生成 run_id,對相同 run_id 呼叫 Evaluator N 次,驗證 KBDB 只存在一個 Evaluation Block - - 驗證相同查詢參數在資料不變時回傳相同結果 - -- [x] 13. Checkpoint — Phase 2 驗收 - - 確認新 EdgeType 全部可解析執行(IS_A、ON_SUCCESS、ON_FAIL、CALLS_SUBFLOW、ON_CLICK) - - 確認版本選擇策略(floating / stable / pinned)行為正確 - - 確認 Evaluator Agent 冪等寫入 KBDB - - 確認所有 Phase 2 測試通過,向使用者確認是否繼續 Phase 3 - ---- - -## Phase 3:前端畫布(Web Components + 雙面翻轉) - -- [x] 14. 建立 Web Components 零件庫(`u6u-core/web-components/`) - - [x] 14.1 實作 `` Custom Element - - 支援 attributes:`label`、`color`、`tooltip`、`workflow`、`disabled` - - `workflow` 設定且點擊時發出 `u6u:trigger` CustomEvent(`{ workflowId, payload }`) - - `workflow` 未設定時點擊不發出事件,console 輸出警告 - - _Requirements: 8.2, 8.3_ - - - [x] 14.2 實作 `` 與 `` Custom Elements - - 支援 `name`、`placeholder`、`value` attributes - - `value` property 可被 `` 讀取 - - _Requirements: 8.1_ - - - [x] 14.3 實作 `` Smart Container - - 攔截子元件的 `u6u:trigger` 事件(`stopPropagation`) - - 收集同容器內所有 `` / `` 的 name-value 對 - - 合併至 payload 後重新發出 `u6u:trigger`(`bubbles: true, composed: true`) - - _Requirements: 8.4_ - - - [x] 14.4 實作 `` 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 拖放與自動打包關係顯示 - - 拖入同一 `` 時,邏輯視圖自動顯示 CONTAINS 關係與自動打包說明 - - _Requirements: 9.4_ - - - [x] 15.5 實作零件替換過濾器 - - 替換已綁定 Workflow 的 UI 零件時,只顯示具備 `u6u:trigger` 能力的候選零件 - - _Requirements: 9.5_ - - - [x] 15.6 在畫布中整合 u6u Web Components(Dogfooding) - - 畫布 UI 本身使用 ``、``、`` 組裝 - - 驗證 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) diff --git a/.agents/specs/arcrun/arcrun.md b/.agents/specs/arcrun/arcrun.md deleted file mode 100644 index 56a91a2..0000000 --- a/.agents/specs/arcrun/arcrun.md +++ /dev/null @@ -1,381 +0,0 @@ -# arcrun — 進度與待辦 - -> 設計細節見 `arcrun/README.md`(產品說明)和 `arcrun/BETA_TEST.md`(封測指南)。 -> 這份文件只記錄:目前狀態、還差什麼、封測能不能啟動。 - ---- - -## 一、封測目標場景 - -封測者是工程師朋友,有自己的網頁,需要後端自動化。目標是他能在 AI 協助下,一次或很少次完成以下完整流程: - -1. `acr init` 取得 api_key -2. `acr parts scaffold` 查零件格式,AI 幫寫 workflow YAML -3. 若內建零件不足,`acr recipe push` 增加打外部 API 的 recipe -4. `acr creds push` 上傳 OAuth token(gmail / google_sheets 等) -5. `acr push` 部署 workflow,取得 Webhook URL -6. 網頁 POST /webhooks/named/{name}/trigger,結果存 Google Sheets - ---- - -## 二、場景各步驟驗證狀態 - -### Step 1:acr init → api_key -- [x] `acr init` Standard 模式完成,api_key 存入 `~/.arcrun/config.yaml` -- [x] 已驗證:`mode: standard, api_key: ak_...` 正確 - -### Step 2:acr parts scaffold → AI 看到零件格式 -- [x] `acr parts` 列出 21 個零件,完全內建,不依賴 registry.arcrun.dev -- [x] `acr parts scaffold google_sheets` 輸出 spreadsheet_id / range / operation / values 格式與 credentials.yaml 範本 -- [x] 已驗證:輸出可直接貼入 YAML - -### Step 3:acr recipe push → 打外部 API -- [x] `acr recipe push` 上傳成功,回傳 rec_hash -- [x] workflow 使用 `component: rec_xxxxxxxx`,acr push 後 trigger 能正確呼叫外部 API -- [x] 已驗證(2026-04-18):httpbin_post recipe → trigger → httpbin.org/post 回傳正確 ✅ - -### Step 4:acr creds push → 自動注入 token -- [x] `POST /credentials` API 完成,以 `{api_key}:cred:{name}` 存入 KV -- [x] Webhook trigger 時 injectCredentials 從 KV 取得 token 自動注入 -- [x] `/register` 現在回傳 `encryption_key`,`acr init` 自動存入 config -- [x] `acr creds push` 從 config 讀 encryption_key,不再需要手動設定環境變數 -- [x] 已驗證(2026-04-18):beta@arcrun.dev 帳號完整流程:init → creds push → trigger → credential 注入成功 ✅ - -### Step 5:acr push → Webhook URL -- [x] `acr push workflow.yaml` 部署成功,顯示 Webhook URL 和完整 curl 範例 -- [x] config 中的 `component` / 參數在 push 時套入 graph 節點 -- [x] 已驗證(2026-04-18):sheet-test workflow push 成功 ✅ - -### Step 6:網頁 POST → 執行 → 結果到 Google Sheets -- [x] `POST /webhooks/named/{name}/trigger -H 'X-Arcrun-API-Key: ...'` 觸發執行正常 -- [x] google_sheets 零件有實作(append row 到 Sheets API) -- [x] 已驗證(2026-04-18):trigger sheet-test → 報「缺少 credential」(符合預期,credential 未上傳)✅ -- [ ] **未驗證**:真實 google_oauth token + acr creds push → trigger → Google Sheets 實際寫入 - - 需要真實 OAuth token 才能完整驗證 - ---- - -## 三、封測啟動阻擋項 - -P0 全部清除才啟動封測。 - -| # | 項目 | 狀態 | 說明 | -|---|------|------|------| -| 1 | acr parts scaffold 正確輸出 | ✅ 完成 | 21 個零件內建清單 | -| 2 | acr recipe push 端對端 | ✅ 完成 | httpbin_post 驗證通過 | -| 3 | acr creds push 代碼 | ✅ 完成 | 需 ARCRUN_ENCRYPTION_KEY | -| 4 | credential 注入端對端 | ✅ 完成 | 無 token 時錯誤訊息正確 | -| 5 | acr push + webhook trigger | ✅ 完成 | 端對端驗證通過 | -| 6 | acr creds push 實測 | ✅ 完成 | /register 回傳 encryption_key,acr init 自動存入 config(CLI 1.0.9)| -| 7 | Google Sheets 真實寫入 | ⚠️ 部分驗證 | credential 注入已驗證;實際 Sheets 寫入需真實 OAuth token | -| 8 | 第三方服務認證 recipe | ✅ 完成 | 20 個服務(Notion/Slack/GitHub/OpenAI 等),CLI 1.1.0 | -| **9** | **cypher-executor outbound HTTP fetch 全失效** | ✅ **已解決 2026-05-13**(CF 同 zone 自循環死鎖,改走 workers.dev)| 詳見下方專段 | -| **10** | **multi-node chain context propagation 漏失** | ✅ **已解決 2026-05-13**(ON_SUCCESS/ON_FAIL/IF/ON_CLICK 沒 spread baseCtx)| 詳見下方專段 | - -**目前狀況**:P0 全部解決。 -- #9 修復方式:component worker URL 從 `*.arcrun.dev`(同 cypher zone)改走 `arcrun-{name}.{WORKER_SUBDOMAIN}.workers.dev`(避開同 zone 自循環) -- #10 修復方式:4 個 edge type 補 `{...baseCtx, ...result}`,跟 PIPE/FOREACH 一致 - -兩個 P0 解完 mira 7 節點 workflow 端對端通(含真 Claude 16 秒呼叫)。 - ---- - -### ✅ P0 #9(2026-05-13 已解決):cypher-executor outbound fetch 全失效 - -**完整事件報告(含誤判路徑)**:[docs/incidents/2026-05-13-cypher-outbound-522.md](../../../docs/incidents/2026-05-13-cypher-outbound-522.md) - -**修復方式**:cypher-executor fetch component worker 從 `*.arcrun.dev`(同 zone)改走 `arcrun-{name}.{WORKER_SUBDOMAIN}.workers.dev`。對外 `cypher.arcrun.dev` 不變,用戶 0 感知。 - -**改動檔案**(2026-05-13): -- `cypher-executor/src/lib/component-loader.ts`:`wasmWorkerUrl(canonicalId, subdomain)` 簽名加 subdomain 參數 + URL pattern 改 workers.dev -- `cypher-executor/src/actions/auth-dispatcher.ts`:同步新簽名 -- `cypher-executor/src/types.ts`:`Bindings` 加 `WORKER_SUBDOMAIN: string` -- `cypher-executor/wrangler.toml`:`[vars]` 加 `WORKER_SUBDOMAIN = "uncle6-me"` -- 5 個 component worker 在 dashboard 啟用 workers.dev URL(kbdb-get / kbdb-ingest / kbdb-create-block / kbdb-patch-block / claude-api,**未來新 component 也都要開**) - -**驗證**:cypher-executor → kbdb-get / claude-api 從 522 → 200。mira `acr run wiki_synthesis` 5 節點 workflow 跑通前 3 節點(kbdb_get chain)。 - -**Self-hosted fork 注意**:必須改 `wrangler.toml [vars] WORKER_SUBDOMAIN` 為自己的 CF 帳號 subdomain,並把所有 component worker 在 dashboard 啟用 workers.dev URL。 - ---- - -### ✅ P0 #10(2026-05-13 已解決):multi-node chain context propagation 漏失 - -**現象**:cypher binding workflow 從第 2 個節點開始,原始 input context(top-level `api_key` / `mira_token` 等)丟失,下游節點 `{{api_key}}` 模板原文未替換傳給零件 → 401 Unauthorized 或類似錯。 - -**測試重現**: - -```yaml -flow: - - "input >> ON_SUCCESS >> n1" - - "n1 >> ON_SUCCESS >> n2" -config: - n1: { component: kbdb_get, api_key: "{{api_key}}", block_id: "{{b1}}" } - n2: { component: kbdb_get, api_key: "{{api_key}}", block_id: "{{b2}}" } -context: { api_key: "ak_xxx", b1: "...", b2: "..." } -``` - -n1 收到 ctx 含 `api_key / b1 / b2` ✓ → 跑通。 -n2 收到的 ctx 只有 `n1.output spread`(blocks/count/success/block_id),**`api_key / b1 / b2` 不見**,`{{api_key}}` 原文傳到零件回 401。 - -**根因**:`graph-executor.ts` 在 PIPE / FOREACH 邊類型已修「baseCtx ∪ result」,但 **ON_SUCCESS / ON_FAIL / IF / ON_CLICK 四個 edge type 沒套同模式**,直接把 `result` 當下游 ctx 傳,丟掉原始 context。 - -**修法**(`cypher-executor/src/graph-executor.ts` line 407 / 415 / 423 / 472): - -```typescript -// 改前 -result = await this.executeNode(nextNode, graph, result, ...); - -// 改後(同 PIPE/FOREACH 模式) -const baseCtx = (typeof context === 'object' && context !== null) ? context as Record : {}; -const baseResult = (typeof result === 'object' && result !== null) ? result as Record : {}; -const mergedCtx = { ...baseCtx, ...baseResult }; -result = await this.executeNode(nextNode, graph, mergedCtx, ...); -``` - -**驗證**:mira `acr run wiki_synthesis` 7 節點 workflow 端對端跑通(16 秒,含真 Claude 呼叫)。每個節點都拿到正確 `api_key` 不再 401。 - -**歷史脈絡**:類似問題 2026-05-07 commit e8fca33 在 FOREACH edge 已修一次("FOREACH preserves outer context"),但當時沒同步處理另外 4 個 edge type。本次補完。 - ---- - -### ✅ P0 #10 補完三個衍生問題(2026-05-13 晚 ~ 2026-05-14) - -P0 #10 修完後 mira 嘗試做 wiki 多段結構,又踩出三個 cypher binding 設計缺陷。**都是同一天解掉**。 - -#### A. interpolateData() 不遞迴 nested object - -**現象**:`set` / `kbdb_create_block` 的 `values: { text: "{{classify.data.text}}" }`、`tags_json: ["facet:{{paragraph.facet}}"]` 等 nested config 內的 `{{x}}` 不被替換,原文傳給零件。 - -**根因**:`interpolateData()` 只 iterate top-level,對非 string 值(object / array)直接 pass-through 不下沉。 - -**修法**:拆 `interpolateString` + `interpolateValue`(遞迴 object / array),`interpolateData` 改 call `interpolateValue`。 - -**測試**:`set values: { text: "hello {{name}}", arr: ["item {{name}}"] }` 帶 `name=world` → 全展開。 - -#### B. ctx 沒存上游 output 的 node id namespace - -**現象**:`{{classify.data.text}}` 找不到上游 classify 的 output;只能用 `{{data.text}}`(直接 spread 取),但會被下個節點覆蓋,多節點 chain 用不了。 - -**根因**:`propagateCtx` 只把上游 result spread 進 ctx,沒額外存 `[node.id]: result`。 - -**修法**:`propagateCtx` 改回傳 `{ ...baseCtx, ...baseResult, [upstreamNodeId]: upstreamResult }`。讓下游能用 `{{node_id.data.text}}` 從 namespace 取,永不被覆蓋。 - -**測試**:5 節點 chain 用 `{{load_schema.blocks.0.content}}` / `{{classify.data.text}}` 全展開。 - -#### C. FOREACH 找 iterable 只看 result,不看 ctx + 不看 nested - -**現象**:mira wiki_synthesis 雙重 FOREACH(外層 `對每個 paragraph`、內層 `對每個 triplet`),外層 OK,內層跑 0 次。 - -**根因 (C1)**:`getIterableFromContext(result, key)` 只看當前節點 output。`result` 是 `create_paragraph` output(`{data, success}`),不含 paragraphs。但 `paragraphs` 早就在 ctx 從 classify spread 來。 - -**根因 (C2)**:當外層 FOREACH 把 `paragraph` item 注入 ctx,內層 FOREACH 要找 `paragraph.triplets`。`getIterableFromContext` 只看 top-level,看不到 `paragraph` 物件裡的 `triplets`。 - -**修法**: -- (C1) FOREACH `result` 找不到 iterable → fallback 找 `context` -- (C2) `getIterableFromContext` 加一輪「掃 ctx 內每個 object 找 nested key」 - -**測試**:mira wiki_synthesis 3 層樹(wiki-page → paragraphs → triplets)端對端跑通,KBDB 內驗證 `物理 AI` wiki 有 2 段 paragraph + 4 個 triplet,parent_id 正確接到對應 paragraph。 - -#### Edge type 一致化 - -抽 `propagateCtx(context, result, upstreamNodeId)` helper,5 個 edge type(PIPE / ON_SUCCESS / ON_FAIL / IF / ON_CLICK / FOREACH)全部用同一 function 組下游 ctx。**未來新 edge type 必須用這 helper**,避免再漏。 - -#### CLI validator 同步 - -`cli/src/lib/yaml-parser.ts` validateRelations 加 regex 支援 `對每個 X` / `FOREACH X` 迭代器命名(之前 validator 字串完全比對擋住,但 graph-builder 執行端早已支援)。 - ---- - -### 三-A、P1 待改進(不擋封測,但 mira 已踩到) - -#### ✅ P1 #3:cypher-executor `scheduled()` handler(2026-05-14 完成) - -**原痛點**:cron 零件只做 expression validation;cypher-executor 沒 `scheduled()` handler。寫了 cron 首節點的 workflow 不會真的跑。 - -**之前的 workaround**(已撤):mira 寫了個 `/mira/wiki-from-raw` route 從前端 fire-and-forget 觸發 wiki_synthesis。但這違反「一律 arcrun-native」原則,也讓 arcrun 永遠補不齊缺失。**已刪 route,回 arcrun-native 路線**。 - -**落地**: -1. `wrangler.toml`:`[triggers] crons = ["* * * * *"]`(每分鐘 tick) -2. `src/lib/cron-match.ts`:5 欄位 cron expression matcher(支援 `*` / `N` / `*/N` / `1-5` / `5,10` 組合) -3. `src/scheduled.ts`:scheduled handler 掃 KV `cron-idx:` prefix,比對 controller.scheduledTime,匹配就 `executeWebhookGraph` 背景跑 -4. `routes/webhooks-named.ts`:acr push 偵測首節點是 cron 零件 → 抽 `cron_expr` 存進 record + 額外寫 `cron-idx:{api_key}:{name}` 輕量 index entry。DELETE 一併清理 -5. `src/index.ts`:export default 改 `{ fetch, scheduled }` -6. cypher-executor 自己加 `workers_dev = true` 給未來 self-trigger 用(fork 用 path-based 子 trigger 也走 workers.dev 避同 zone) - -**workflow YAML 慣例**: -```yaml -flow: - - "my_cron >> ON_SUCCESS >> downstream_node" -config: - my_cron: - component: cron - cron_expr: "*/5 * * * *" # 每 5 分鐘 -``` -acr push 就會自動建立 cron-idx 並開始定時觸發。 - -**測試**:`tests/arcrun-test/cron_heartbeat.yaml` — 每分鐘 fire 一次 + set 節點 log。 -`wrangler tail arcrun-cypher-executor` 應看 `[scheduled] trigger cron_heartbeat ...`。 - -**對應 use case**:mira `mira_feed_watcher`(7B.3h,下一輪做)/ RSS 每日抓 / voice-stt 每小時掃 / 等所有 cron-driven source。 - ---- - -#### P1 #1:workflow 缺 IF/branch 能力(2026-05-14 mira 7B.3f 提出) - -**現象**:mira 想做「找有則 PATCH 沒則 CREATE」(index-entry upsert),arcrun 目前只有 `ON_SUCCESS` + `對每個 X`(FOREACH)+ 已存在但壞掉的 `if_control`(見已知限制 #1),沒有 `>> ON_TRUE >>` / `>> ON_FALSE >>` 條件路由。 - -**短期 workaround**(已採用,2026-05-14):建 `kbdb_upsert_block` 零件,把分支邏輯封進零件內部(GET by page_name → 找到 PATCH 沒找到 POST)。caller 看到的是單純的 upsert 介面。 - -**長期解**:升 `if_control` false branch 路由 / 加 `>> ON_TRUE >>` edge type,讓 workflow 層可表達分支。對未來所有「找則改否則建」/「條件分流」場景都會撞到,不只 mira。 - -**位置**:cypher-executor/src/graph-executor.ts edge type 處理(5 個 edge type 抽出 `propagateCtx` 後新增 IF 應該不難)+ cli/src/lib/yaml-parser.ts validator。 - ---- - -#### P0 #11(2026-05-14 已解決):interpolateString stringify array 撐爆下游 - -**現象**:mira_feed_watcher 用 `items: "{{list_raws.blocks}}"` 把 kbdb_get 拿到的 blocks 陣列傳給 filter 零件。watcher 跑 264ms 完成、0 raw 處理。 - -**根因**:`interpolateString` 看到模板就用 `String.replace`,非 string 值(陣列)一律 `JSON.stringify`。filter 零件收到字串 `"[{...},{...}]"` 不是 array,items 被忽略 → 0 matches → FOREACH 跑 0 次。 - -**修法**:`interpolateString` 加 single-ref pass-through 規則:若整個值是純單一 `{{x}}` 引用,回 raw value(保留 array / object 型別)。多 ref / 混合文字仍 stringify 拼接字串。 - -**測試**:mira_feed_watcher 推到 prod 後下一個 cron tick 觀察 wiki-processed tag 是否在 raws 上出現。 - ---- - -### 三-B、新零件加入紀錄 - -| 日期 | 零件 | 動機 | 對應 SDD | -|---|---|---|---| -| 2026-05-14 | `kbdb_upsert_block` | mira 7B.3f index-entry per-entity upsert,繞過 workflow 缺 IF/branch 能力(P1 #1)。內部 GET by page_name → 找到 PATCH 沒找到 POST。page_name 當 idempotency key。 | polaris/mira/.agents/specs/mira-app/design.md §3.5.12.4.1 | - -**新零件 checklist(避免 P1 #1 重蹈 kbdb_upsert_block 漏白名單覆轍)**: - -每加一個 API/data 零件(不是 logic primitive),都要: -- [ ] `registry/components/{name}/main.go` + `component.contract.yaml` + `go.mod` -- [ ] `tinygo build -target=wasi` 通 -- [ ] `.component-builds/{name}/` 完整 4 檔(`wrangler.toml` 含 `workers_dev = true` + `pnpm-lock.yaml` + `tsconfig.json` + `src/index.ts`) -- [ ] **`cypher-executor/src/lib/component-loader.ts` 的 `WASM_HTTP_RUNNER_IDS` 加 canonical_id**(漏這條 cypher-executor 永遠拋「找不到零件」,端對端會死靜悄悄) -- [ ] `acr validate workflow.yaml` 通 -- [ ] 直接 curl `https://{kebab}.arcrun.dev` + `https://arcrun-{kebab}.{WORKER_SUBDOMAIN}.workers.dev` 都 200 - ---- - -### 三-C、P1 #2:workers_dev = true 全 component 自動化(2026-05-14 已收) - -**原痛點**:每新部署一個 component worker,要去 CF Dashboard 手動 Enable workers.dev URL,否則 cypher-executor fetch 該 worker 會 404。 - -**解**:32 個 `.component-builds/*/wrangler.toml` 全部加 `workers_dev = true`。CI 每次 deploy 自動啟用對應 workers.dev URL,零手動。 - -**未來新 component**:模板 (`component-worker-template/`) 應該預設帶 `workers_dev = true`,新人 fork 不會踩。已列入「新零件 checklist」第 3 條。 - -**為何不走 `*.acr-comp.uncle6.me` 自訂 zone**:CF Universal SSL 只發一層子域,sub-sub `*.acr-comp.uncle6.me` 不蓋;要 ACM ($10/月) 才能簽。違反 arcrun「fork 後 self-host 用 free tier 跑得起來」核心目標。workers_dev=true 走 CF 默認的 workers.dev cert,free tier OK,更乾淨。 - ---- - -### 原 P0 #9 調查紀錄(保留作歷史參考) - -**現象**:cypher-executor 的 `makeHttpRunner` (`cypher-executor/src/lib/component-loader.ts:142`) 對任何 outbound URL fetch 都回 CF **522 (origin timeout, ~1000ms)**。 - -**測試矩陣**(mira repo `polaris/mira/arcrun/wiki_synthesis.yaml` 端對端壓測時發現): - -| 路徑 | 結果 | 證明 | -|---|---|---| -| 本機 curl → kbdb-get.arcrun.dev | 200 (22ms) | KBDB worker 本身健康 | -| cypher-executor → kbdb-get.arcrun.dev (HTTP) | **522** (1002ms) | outbound HTTP fetch 壞 | -| cypher-executor → claude-api.arcrun.dev (HTTP) | **522** | 同 zone 也壞 | -| cypher-executor → httpbin.org (外部) | **522** | 不只是 same-account loop | -| cypher-executor → string_ops (Service Binding) | 200 ✅ | SVC_* 路徑正常 | -| acr run hello (built-in via SB) | ✅ | hello.yaml 仍跑得通 | - -**衍生小 bug**:`set` 零件 input schema 變了(要 `assignments` 陣列或 `values` 物件,不是 `value`)。tests/arcrun-test/hello.yaml 跑 string_ops 沒踩到。 - -**影響範圍(封測啟動阻擋)**: - -- 任何用戶 workflow 含 outbound HTTP 都壞: - - 用戶 `acr recipe push` 後 trigger → 打外部 API 全 522(推翻 P0 #2 4/18 紀錄) - - 用戶要存資料進 arcrun 內建 KBDB → 522(mira 7B.3c 卡這裡) - - 任何 auth primitive 走獨立 Worker URL 路徑 → 522 - - Google Sheets 寫入 → 522(推翻 P0 #7「待真實 OAuth 驗證」評估,根本還沒到 OAuth 步驟就壞) -- 只有「全內建邏輯零件 + 純 service binding」workflow 還能跑 - -**根因(2026-05-13 確認):5/8-5/9 9 次 manual `wrangler deploy` 把含 WIP bug 的 cypher-executor 推上 prod** - -調查路徑(按時序): - -1. 一開始懷疑 free tier CPU cap(10ms / invocation)→ 對照測試「5 節點 SB chain」跑了 2.2 秒卻通過,**推翻** -2. 換懷疑 CF zone 規則 / Bot Fight Mode → dashboard bindings 乾淨無攔截,**推翻** -3. 用戶補繳費恢復 Workers Paid → 重測仍全 522,**徹底排除付費假設** -4. 看 dashboard Version History:5/8 4 次 + 5/9 5 次 = **9 次 manual `wrangler deploy by uncle6.me`** -5. 對照 GitHub Actions:4/24 後完全沒 deploy(最後是 commit e222116 `fix(wasi-shim)`) -6. 對照 git:本機 main **領先 origin/main 3 commits 未 push**,含: - - `497f92a feat(arcrun): recipe system + resumable workflow + component registry canon` - - `e8fca33 feat(cypher): 3-node wiki workflow end-to-end (FOREACH + nested interp + unified parsing)` - - `519423c feat(arcrun): mira wiki page with tag filter + accumulated WIP`(自描含 `cypher-executor: auth-dispatcher / wasi-shim adjustments (WIP)`) - -**結論**:那 9 次 manual deploy 把上面 3 個 unpushed commit 的 cypher-executor 改動推上 prod,其中至少一個改動破壞了 outbound fetch(最可能是 519423c WIP 內的 wasi-shim / auth-dispatcher 改動)。GitHub Actions 因為沒 push 沒跑,CI 沒 catch,4/18-4/24 那段 SDD「驗證通過」的紀錄是 truth,現在 prod 是壞的版本。 - -**為何 SB 路徑沒事 / HTTP 路徑全死**:SB 走 cypher-executor 內部 service binding API(`env.SVC_X.fetch()`),不經過 outbound HTTP code path。HTTP 路徑走 `makeHttpRunner` (component-loader.ts:142) 的 `fetch(url, ...)`,這條路被 WIP code 弄壞。具體壞在哪要 diff 那 3 個 commit 的 cypher-executor 改動才知道。 - -**驗證 wrangler tail 證據**:trigger 任何 outbound HTTP 的 graph,cypher-executor 自己 `wallTime: 497ms, cpuTime: 2ms, outcome: ok`、無 logs、無 exceptions。代表 cypher-executor 把「fetch 失敗的 522 response」當作 component 正常輸出包回 client,自己沒撞任何錯。 - -**解法(三選一)**: - -- **A. Rollback prod 到 4/24 的 e222116** — CF dashboard → arcrun-cypher-executor → Deployments → 找 4/24 那筆 → Rollback。5 分鐘恢復 outbound fetch,丟失 wiki workflow / recipe / resumable 等 cypher 端 WIP 改動(但前端、registry components、KBDB blocks 都不丟,因為它們是別的 worker / 別的儲存)。**richblack 操作。** -- **B. Diff 3 個 unpushed commit 找出壞掉的改動修掉** — 不丟功能,但要動 src code 走 SDD 協議,30min - 數小時。 -- **C. 架構切換**(mira 老闆 2026-05-13 提的):sub-workflow 自殺交棒模式,cypher-executor 不再做集中 graph executor。從根本繞開「cypher-executor 一個 invocation 跑長 graph」這條脆弱路徑。一勞永逸但是大改。 - -**衍生小 bug 仍要修**(跟付費無關):`set` 零件 input schema 變了(要 `assignments` 陣列或 `values` 物件,不是 `value`)。要嘛 update set 零件 contract 容錯,要嘛文件化新 schema。 - -**為什麼這直接擋封測**: - -封測場景 Step 6「網頁 POST → 結果存 Google Sheets」走 google_sheets 零件 (HTTP outbound to googleapis.com)。如果 cypher-executor outbound 全壞,**封測者跑任何含外部 API 的 workflow 都會 522**,不是「Google Sheets 實際寫入未驗證」級別的小事。 - -**也直接擋 mira**:[polaris/mira/.agents/specs/mira-app/tasks.md] 7B.3c-f(wiki 合成 workflow)卡這裡。 - ---- - -## 四、封測前 P3(啟動當天) - -- [ ] 用封測者 email 呼叫 `/register`,取得 api_key -- [ ] 將 ARCRUN_ENCRYPTION_KEY 以安全方式提供給封測者 -- [ ] 確認聯絡管道 - ---- - -## 五、已知限制(封測期間不修) - -1. `if_control` false branch 不路由(條件 false 時後續節點不執行)→ 升級計畫見 P1 #1,2026-05-14 mira 用 `kbdb_upsert_block` workaround -2. 多節點 context 不自動解包(上游輸出 flat merge,下游需從 `data.result` 取值) -3. 用戶自製邏輯零件(Phase 5)封測後才實作 - ---- - -## 六、實作進度 - -| Phase | 內容 | 狀態 | -|-------|------|------| -| 0 | Workers 部署、CI/CD、DNS | ✅ | -| 1 | CLI 基礎(init / validate / run / parts) | ✅ | -| 2 | /register、/cypher/execute、21 個零件 | ✅ | -| 3 | Service Binding 架構、{{variable}} 插值、ON_FAIL 修正 | ✅ | -| 4 | 動態 Recipe KV(CRUD)、acr recipe 指令 | ✅ | -| 5 | 用戶自製邏輯零件(WASM push) | ⏸ 封測後 | -| 6 | Credential 多租戶({api_key}:cred:{name})、acr creds push | ✅ | -| 7 | acr parts 內建清單、acr parts scaffold | ✅ | -| 8 | /webhooks/named、acr push 改版、config 套入 graph | ✅ | - -### CLI 版本 - -| 版本 | 變更 | -|------|------| -| 1.1.0 | auth recipe 系統:20 個服務預建(Notion/Slack/GitHub/OpenAI/Google SA 等);acr auth-recipe 指令 | -| 1.0.9 | /register 回傳 encryption_key;acr init 自動儲存;creds push 不需手動設環境變數 | -| 1.0.8 | acr push → webhooks/named;config 套入 graph;acr parts 內建清單 | -| 1.0.7 | acr creds push → POST /credentials | -| 1.0.6 | acr recipe push / list / delete | -| 1.0.5 | hello.yaml 改 string_ops,--version 修正 | -| 1.0.4 | config/context 分離 | -| 1.0.3 | 初始發布 | diff --git a/.agents/specs/arcrun/auth-recipe.md b/.agents/specs/arcrun/auth-recipe.md deleted file mode 100644 index 2f3bcaf..0000000 --- a/.agents/specs/arcrun/auth-recipe.md +++ /dev/null @@ -1,274 +0,0 @@ -# Auth Recipe System — SDD - -> 文件類型:SDD(Software Design Document) -> 建立:2026-04-19 -> 狀態:實作中 - ---- - -## 一、目標 - -封測前完成,讓封測者碰到「我要連 X 服務」都有辦法,而不是「還沒做」。 - -**精神**:`http_request` 是容器零件,auth recipe 是「如何對這個服務認證」的設定層,兩者分離。新增一個服務 = 寫一份 YAML,不需要改程式碼、不需要重新部署 Worker。 - ---- - -## 二、三層模型 - -``` -Layer 3: Auth Recipe (YAML/JSON in RECIPES KV) - 公共,描述「如何對某服務認證」 - key: auth_recipe:{service} - 例: auth_recipe:notion, auth_recipe:slack - ↓ 引用 -Layer 2: Auth Primitive (TypeScript in Worker) - 四個通用認證邏輯:static_key | oauth2 | service_account | mtls - 封測只做 static_key 和 service_account (Google JWT) - ↑ 使用 -Layer 1: Tenant Secret (CREDENTIALS_KV) - 每個 tenant 自己的加密 credential - key: {api_key}:cred:{name} -``` - ---- - -## 三、Auth Recipe Schema - -```typescript -interface AuthRecipeDefinition { - kind: 'auth_recipe'; // 區別 RecipeDefinition 用 - service: string; // canonical_id, e.g. "notion" - version: number; - primitive: 'static_key' | 'oauth2' | 'service_account' | 'mtls'; - base_url: string; - display_name?: string; - description?: string; - - // service_account 用 - service_account_kind?: 'google_jwt'; - token_exchange?: { - endpoint: string; // e.g. https://oauth2.googleapis.com/token - scopes: string[]; - }; - - required_secrets: Array<{ - key: string; // CREDENTIALS_KV 的名稱 - label: string; // UI/CLI 顯示 - type?: 'string' | 'json_blob'; // default: string - help?: string; - help_url?: string; - }>; - - inject: { - header?: Record; // "Authorization": "Bearer {{secret.token}}" - query?: Record; - body?: Record; - // path:注入 endpoint URL path 的 secret(2026-05-29 加)。 - // 解 telegram 類「token 在 URL path」(/bot{token}/)—— header/query/body 都不適用。 - // key = 模板變數名,API recipe 的 endpoint 用 {{auth.K}} 引用。 - // 例:auth_recipe:telegram inject.path = { bot_token: "{{secret.telegram_bot_token}}" } - // recipe:telegram_send endpoint = "https://api.telegram.org/bot{{auth.bot_token}}/sendMessage" - path?: Record; - }; - - 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` — 要合併進 fetch headers | -| `_auth_query` | `Record` — 要附加到 URL query string | -| `_auth_body` | `Record` — 要合併進 request body | -| `_auth_path` | `Record` — endpoint URL path 用(2026-05-29 加)。`makeRecipeRunner` 的 endpoint interpolate 用 `{{auth.K}}` 從這裡取值 | - -`makeAuthRecipeRunner` / `makeRecipeRunner` 在發出 fetch 前讀取這些 `_auth_*` 欄位, -之後從 auto-body 中剔除所有 `_` 前綴欄位(不洩漏給下游)。 - -## 七、API recipe 的 auth_service(多 recipe 共用一把 auth,2026-05-29 加) - -`RecipeDefinition` 加 `auth_service?: string` 欄位:API recipe **自報它屬於哪個服務**, -auth-dispatcher 用它查 `auth_recipe:{auth_service}`,而非假設 componentId == service name。 - -- 讓多個 recipe 共用同一把 auth:`recipe:kbdb_get` / `kbdb_create_block` 都設 `auth_service: "kbdb"` - → 共用唯一的 `auth_recipe:kbdb`,加新 action 不必複製 auth recipe。 -- auth-dispatcher 解析順序:先查 `recipe:{componentId}` 拿 `auth_service`,有就用它; - 沒有則 fallback 把 componentId 當 service name(向後相容舊行為)。 -- 這是「服務身分標籤」非「許可清單」:auth_recipe 只定義「怎麼認證」,不含「誰准用」。 - 授權由發 API key 的服務裁決,arcrun 不做內部授權判斷(見 DECISIONS.md「arcrun 不做授權判斷」)。 - ---- - -## 七、向後相容 - -- 現有 `BUILTIN_API_RECIPES`(gmail, google_sheets, telegram, line_notify)**不動** -- 現有 `BUILTIN_CREDENTIALS_MAP` **不動** -- auth recipe 解析在 component-loader step 5.5(新增),在 step 6 KV recipe 和 step 7 builtin 之前 -- 若 `auth_recipe:{service}` 不存在 → 繼續往下走,行為與現在完全相同 - ---- - -## 八、新增/修改的檔案 - -| 檔案 | 類型 | 說明 | -|---|---|---| -| `cypher-executor/src/routes/recipes.ts` | 修改 | 加 `AuthRecipeDefinition` 型別、`resolveAuthRecipe`、`/auth-recipes` CRUD routes | -| `cypher-executor/src/actions/credential-injector.ts` | 修改 | 加 auth recipe 分支:static_key + service_account | -| `cypher-executor/src/lib/jwt-signer.ts` | 新增 | Google JWT signing via crypto.subtle | -| `cypher-executor/src/lib/component-loader.ts` | 修改 | step 5.5 auth recipe lookup + `makeAuthRecipeRunner` | -| `cypher-executor/src/lib/auth-recipe-seeds.ts` | 新增 | 20 個常用服務的 auth recipe 定義 | -| `cli/src/commands/auth-recipe.ts` | 新增 | `acr auth-recipe list/info/scaffold` | -| `cli/src/commands/parts.ts` | 修改 | `cmdPartsScaffold` fallback 到 auth recipe | -| `cli/src/index.ts` | 修改 | 註冊 auth-recipe 指令 | - ---- - -## 九、封測前預計的 Auth Recipe 清單(20 個) - -### static_key 類(~80% 服務) - -| service | 認證方式 | credential key | -|---|---|---| -| `notion` | Bearer token (header) | `notion_token` | -| `slack` | Bot Token (Bearer) | `slack_bot_token` | -| `github` | PAT (Bearer) | `github_token` | -| `openai` | API key (Bearer) | `openai_api_key` | -| `anthropic` | API key (x-api-key) | `anthropic_api_key` | -| `airtable` | PAT (Bearer) | `airtable_token` | -| `discord` | Bot token ("Bot TOKEN") | `discord_bot_token` | -| `stripe` | Secret key (Bearer) | `stripe_secret_key` | -| `twilio` | AccountSid + AuthToken (Basic Auth) | `twilio_account_sid`, `twilio_auth_token` | -| `sendgrid` | API key (Bearer) | `sendgrid_api_key` | -| `hubspot` | Private App token (Bearer) | `hubspot_token` | -| `linear` | API key (Bearer) | `linear_api_key` | -| `shopify` | Admin API token (X-Shopify-Access-Token) | `shopify_access_token` | -| `resend` | API key (Bearer) | `resend_api_key` | -| `supabase` | Service role key (Bearer + apikey) | `supabase_service_key` | -| `typeform` | PAT (Bearer) | `typeform_token` | -| `jira` | API token + email (Basic Auth) | `jira_api_token`, `jira_email` | - -### service_account 類(Google 家族,JWT signing) - -| service | scopes | credential key | -|---|---|---| -| `google_sheets_sa` | spreadsheets | `google_service_account` | -| `google_gmail_sa` | gmail.send | `google_service_account` | -| `google_drive_sa` | drive | `google_service_account` | - -> 注意:三個 Google 服務可共用同一個 `google_service_account` credential,只是 scope 不同。 - ---- - -## 十、實作進度 - -### Server (cypher-executor) - -- [x] `AuthRecipeDefinition` 型別 + `resolveAuthRecipe` -- [x] `/auth-recipes` CRUD routes -- [x] `injectFromAuthRecipe` — static_key primitive -- [x] `lib/jwt-signer.ts` — Google JWT via crypto.subtle -- [x] `injectFromAuthRecipe` — service_account primitive -- [x] `makeAuthRecipeRunner` in component-loader -- [x] step 5.5 in createComponentLoader -- [x] auth-recipe-seeds.ts (20 services) -- [x] seed script / deploy seeds to KV(2026-04-19 全部 ✅) - -### CLI (arcrun) - -- [x] `commands/auth-recipe.ts` — list / info / scaffold -- [x] 更新 `commands/parts.ts` — scaffold fallback -- [x] 更新 `index.ts` — 註冊指令 -- [x] 版本升 1.1.0 -- [x] npm publish(arcrun@1.1.0) - -### 驗證 - -- [ ] notion (static_key) 端對端 -- [ ] google_sheets_sa (service_account) 端對端 -- [ ] 舊有 google_sheets builtin 向後相容確認 - ---- - -## 十一、長期演進:TinyGo WASM Primitive(封測後) - -> 參考:`docs/user_requirements/arcrun/credential_parts.md` - -**目前封測版**:Layer 2 primitive 邏輯在 `cypher-executor` TypeScript 中實作(`credential-injector.ts`)。 - -**長期目標**:四個 primitive 各自編譯為獨立 TinyGo WASM,取代現有 TS 實作: - -``` -arcrun/registry/components/auth_static_key/ ← TinyGo WASM -arcrun/registry/components/auth_oauth2/ ← TinyGo WASM -arcrun/registry/components/auth_service_account/ ← TinyGo WASM -arcrun/registry/components/auth_mtls/ ← TinyGo WASM -``` - -每個 primitive 實作統一 interface(`Authenticate` / `NeedsRefresh` / `Refresh` / `Test`)。 -切換時 `cypher-executor` 的 `injectFromAuthRecipe` 改為呼叫對應 WASM,邏輯不變。 - -**何時做**:封測驗證完成、TinyGo crypto 支援確認後(特別是 RS256/ES256 JWT signing)。 -在此之前,**不建立任何 TypeScript SDK 或 Python SDK 來包裝 credential 邏輯**。 - -### 禁止的做法 - -- ❌ 建立 `js-sdk/`、`python-sdk/` 包裝 credential 加解密 -- ❌ 在 client 端重實作 AES-GCM encrypt/decrypt -- ❌ 用 TypeScript 重寫已計劃用 TinyGo 實作的 primitive 邏輯 diff --git a/.agents/specs/arcrun/credential-primitives-wasm/design.md b/.agents/specs/arcrun/credential-primitives-wasm/design.md deleted file mode 100644 index 1e45f85..0000000 --- a/.agents/specs/arcrun/credential-primitives-wasm/design.md +++ /dev/null @@ -1,178 +0,0 @@ -# Design Document: Credential Primitives TS → WASM 改寫 - -## Overview - -將 `cypher-executor` 中以 TypeScript 實作的 credential 注入邏輯,改寫為 4 個獨立的 WASM 零件。這是 `credential_parts.md` 長期規格的實現,不再是「未來 Phase」。 - -**動機**:TS 實作無法在地端(workerd)和邊緣端(Wazero)執行。WASM 零件跨 runtime 可攜,符合 u6u 三層部署架構。 - -**嚴格規範(richblack 2026-04-19 確認)**:cypher-executor TS **完全不實作**任何 credential / auth / template / JWT / 解密邏輯。所有業務邏輯必須在 TinyGo WASM 零件內。TS 僅負責 HTTP routing + 呼叫 WASM + host function 提供 runtime primitive(crypto.subtle / KV / fetch)。 - ---- - -## 現有 TS 實作(要刪除的) - -| 檔案 | 功能 | 對應 WASM Primitive | -|------|------|---------------------| -| `credential-injector.ts` — `injectFromAuthRecipe()` | static_key template 展開 | `auth_static_key` | -| `credential-injector.ts` — service_account 分支 | JWT signing + token exchange | `auth_service_account` | -| `credential-injector.ts` — `decryptCredential()` | AES-GCM 解密 | host function(所有 primitive 共用) | -| `credential-injector.ts` — `interpolateTemplate()` | `{{secret.KEY}}` 替換 | 內建在各 primitive | -| `jwt-signer.ts` — `exchangeGoogleJwt()` | PEM→PKCS8→RS256→token | `auth_service_account` | -| `component-loader.ts` — BUILTIN_API_RECIPES | gmail/telegram/line/gsheets 寫死邏輯 | 刪除,改用 auth recipe + `http_request` 零件 | -| `credential-injector.ts` — BUILTIN_CREDENTIALS_MAP | 舊路徑 flat injection | 刪除,統一走 auth recipe | -| `arcrun/credentials/` | 重複的 credentials Worker | 刪除,路由已在 cypher-executor | - ---- - -## 4 個 WASM Primitive 設計 - -### 統一 I/O 介面(stdin/stdout JSON) - -``` -stdin(Worker → WASM): -{ - "action": "authenticate" | "needs_refresh" | "refresh" | "test", - "api_key": "ak_xxx", // 租戶識別,用來組 KV key - "service": "openai", // 對應 auth_recipe:{service} - "request": { "method": "GET", "url": "/path", "headers": {}, "body": null } -} - -WASM 內部流程: - 1. recipeJSON = kv_get("auth_recipe:" + service) - 2. 依 recipe.required_secrets 逐一 kv_get("{api_key}:cred:{name}") → {encrypted, iv} - 3. secrets[name] = crypto_decrypt(encrypted, iv) - 4. (service_account)crypto_sign_rs256(jwt, pkcs8) + http_request 換 token - 5. 展開 recipe.inject 的 {{secret.X}} / {{runtime.X}} 模板 - -stdout(WASM → Worker): -{ - "success": true, - "auth_headers": { "Authorization": "Bearer xxx" }, - "auth_query": {}, - "auth_body": {}, - "runtime": { ... updated runtime state,供下次 refresh 用 } -} -``` - -### auth_static_key - -**位置**:`arcrun/registry/components/auth_static_key/` -**語言**:TinyGo 或 AssemblyScript - -功能: -1. 讀取 `recipe.inject.header/query/body` 模板 -2. 用 `secrets` 展開 `{{secret.KEY}}` 模板 -3. 回傳 `auth_headers` / `auth_query` / `auth_body` - -涵蓋:~80% 服務(Bearer token, API Key, Basic Auth, custom header) - -### auth_service_account - -**位置**:`arcrun/registry/components/auth_service_account/` -**語言**:TinyGo 或 AssemblyScript - -功能: -1. 從 `secrets.service_account_json` 解析 private key -2. JWT signing(RS256:PEM→PKCS8→sign) -3. POST token exchange endpoint → 取得 access_token -4. 展開 `{{runtime.access_token}}` 模板 - -**crypto 考量**: -- TinyGo 的 `crypto/rsa` + `crypto/x509` 支援有限 -- 若 TinyGo 不支援 RS256:使用 host function 讓 Worker 的 `crypto.subtle` 代簽 -- 或改用 AssemblyScript(有 as-crypto 套件) - -### auth_oauth2(新建) - -**位置**:`arcrun/registry/components/auth_oauth2/` - -功能: -1. `needs_refresh`:檢查 `runtime.expires_at` 是否過期 -2. `refresh`:用 `runtime.refresh_token` + `secrets.client_secret` 換新 token -3. `authenticate`:展開 `{{runtime.access_token}}` 到 headers - -### auth_mtls(新建) - -**位置**:`arcrun/registry/components/auth_mtls/` - -功能: -1. 從 `secrets` 讀取 client cert + key -2. 回傳 TLS 設定(由 Worker runtime 執行實際 mTLS handshake) - ---- - -## cypher-executor 改動 - -### 保留(TS routing 層) - -- `routes/credentials.ts` — HTTP CRUD for credentials(接收加密的 payload) -- `routes/recipes.ts` — HTTP CRUD for auth recipes -- `routes/auth.ts` — OAuth flow routing -- `graph-executor.ts` — workflow 執行排程 -- `lib/wasi-shim.ts` — WASM runtime + host functions(加解密 / KV / 簽章 / HTTP 實際由 `crypto.subtle` / env binding / fetch 執行,但**呼叫時機由 WASM 決定**) - -### 修改 - -- `actions/credential-injector.ts` — **整檔刪除**,改為新檔 `actions/auth-dispatcher.ts`(約 30 行): - 1. 查 `resolveAuthRecipe(componentId)` 取得 `primitive` 名稱(static_key / service_account / oauth2 / mtls) - 2. 載入對應的 `auth_{primitive}.wasm` - 3. 送 stdin:`{ action, api_key, service, request }`(**不送 secrets、不送 recipe plaintext**) - 4. WASM 透過 host function 自行 `kv_get` 讀 recipe + 加密 secret,`crypto_decrypt` 解密 - 5. 讀 stdout → 合併 `_auth_headers` / `_auth_query` / `_auth_body` 進 ctx - -- `lib/component-loader.ts` — **刪除 `BUILTIN_API_RECIPES`**(含 http_request / gmail / telegram / line_notify / google_sheets 的 TS 實作),全部改走 WASM runner。每個 `.wasm` 零件都已編譯並以獨立 Worker 部署(`{canonical-id-kebab}.arcrun.dev`)。loader 新增的「WASM runner」路徑就是「canonical_id → HTTP URL 查表後 fetch」,**不做** WASM instantiate。 - - **R2 動態注入 WASM 路徑作廢**(richblack 2026-04-19 確認:CF workerd 無法以 R2 物件臨時 instantiate WASM)。用戶自製零件(Phase 5)同樣走「產生獨立 Worker」流程,不從 R2 讀。 - -### 刪除 - -- `lib/jwt-signer.ts` — 整檔刪除,RS256 簽章移入 `auth_service_account` WASM(透過 host function `crypto_sign_rs256`) -- `credential-injector.ts` 整檔刪除(見上) -- `component-loader.ts` 的 `BUILTIN_API_RECIPES` 整段刪除 -- `BUILTIN_CREDENTIALS_MAP` 已在 `credential-injector.ts` 內,隨檔一併刪 - ---- - -## Host Functions(WASM ↔ Worker 的橋接) - -auth primitive WASM 需要呼叫外部能力時,透過 host function。全部放 `u6u` namespace。**錯誤回傳非零 uint32;成功 = 0 且把結果寫入 `outPtr` 指向的 buffer**。 - -| Host Function | TinyGo 簽章 | 用途 | -|---|---|---| -| `http_request` | `(urlPtr/Len, methodPtr/Len, headersPtr/Len, bodyPtr/Len, outPtr, outLenPtr) uint32` | HTTP 請求(已實作) | -| `kv_get` | `(keyPtr, keyLen, outPtr, outLenPtr) uint32` | 讀 KV。Worker 依 key 前綴路由到 `CREDENTIALS_KV` / `RECIPES` | -| `crypto_decrypt` | `(encPtr, encLen, ivPtr, ivLen, outPtr, outLenPtr) uint32` | AES-GCM 解密。encryption key 由 Worker 從 `env.ENCRYPTION_KEY` 內部讀取,**永遠不暴露給 WASM** | -| `crypto_sign_rs256` | `(dataPtr, dataLen, pkcs8Ptr, pkcs8Len, outPtr, outLenPtr) uint32` | Worker 用 `crypto.subtle.sign('RSASSA-PKCS1-v1_5' + SHA-256)`;private key 以 PKCS8 bytes 傳入 | - -這些 host function 在 `lib/wasi-shim.ts` 中以 WASI import 提供。 - -### 安全邊界 - -- `ENCRYPTION_KEY` 只在 `crypto_decrypt` host function 內部使用,**絕不**經 stdin / 回傳值 / 任何路徑傳給 WASM -- `api_key` 經 stdin 傳入 WASM(讓 WASM 自己組 `{api_key}:cred:{name}` KV key) -- `kv_get` 在 Worker 側檢查 key 前綴: - - `auth_recipe:*` → 讀 `RECIPES` - - `{api_key}:cred:*` → 讀 `CREDENTIALS_KV`,且 `{api_key}` 必須等於 stdin 傳入的 api_key(防越權) - - 其他前綴 → 回傳錯誤 - ---- - -## 關於解密位置 - -採用**方案 B(唯一方案)**:WASM 透過 host function `crypto_decrypt()` 自行解密。 - -- cypher-executor TS 完全不解密、不知道 plaintext -- `ENCRYPTION_KEY` 永遠留在 Worker host function 內 -- WASM 知道要解哪份 ciphertext(經 `kv_get` 讀到的 `{encrypted, iv}`),但拿不到 encryption key -- 這樣 TS 層完全沒有零件業務邏輯,符合 CLAUDE.md §禁止行為 1/6 - -(歷史註記:曾規劃方案 A「TS 先解密再送 stdin」,已廢棄 — 違反「TS 不得實作零件邏輯」。) - ---- - -## 不做的事 - -- ❌ 不改 recipe YAML schema — 沿用現有格式 -- ❌ 不改 KV 儲存結構 — `auth_recipe:{service}` / `{api_key}:cred:{name}` 不變 -- ❌ 不改 SDK API — SDK 仍是 HTTP thin wrapper -- ❌ 不建新的 Worker — 在 cypher-executor 內完成 diff --git a/.agents/specs/arcrun/credential-primitives-wasm/tasks.md b/.agents/specs/arcrun/credential-primitives-wasm/tasks.md deleted file mode 100644 index 0e15aae..0000000 --- a/.agents/specs/arcrun/credential-primitives-wasm/tasks.md +++ /dev/null @@ -1,168 +0,0 @@ -# Implementation Tasks: Credential Primitives TS → WASM - -**嚴格規範(richblack 2026-04-19)**:cypher-executor TS 不得實作任何 credential / auth / template / JWT / 解密邏輯。全部走 TinyGo WASM + host functions(方案 B)。 - -**封測狀態**:推遲(richblack 2026-04-19 決定)。先完成 Phase 1-3 清除違規 TS,再啟動封測。 - ---- - -## Phase 0:核心合併(u6u-core → arcrun) - -- [x] 0.1 把 `u6u-core/builtins/` 搬到 `arcrun/builtins/` -- [x] 0.2 確認 `arcrun/registry/components/` 21 個零件的 contract.yaml 完整(21/21) -- [x] 0.3 刪除 `arcrun/credentials/` 整個目錄(重複,credential route 已在 cypher-executor) -- [x] 0.4 更新 `arcrun/cypher-executor/wrangler.toml`:確認 CREDENTIALS_KV binding 存在 -- [x] 0.5 刪除 `matrix/u6u-core/` 整個目錄(2026-04-19 完成,只剩 credentials/ 已被 cypher-executor 取代) -- [x] 0.6 在 `cypher-executor/src/lib/wasi-shim.ts` 新增 host functions: - - `u6u.kv_get(keyPtr, keyLen, outPtr, outLenPtr) uint32` — 依 key 前綴路由到 `CREDENTIALS_KV` / `RECIPES`,越權檢查 api_key - - `u6u.crypto_decrypt(encPtr, encLen, ivPtr, ivLen, outPtr, outLenPtr) uint32` — 用 `env.ENCRYPTION_KEY` + `crypto.subtle` AES-GCM 解密;key 不暴露給 WASM - - `u6u.crypto_sign_rs256(dataPtr, dataLen, pkcs8Ptr, pkcs8Len, outPtr, outLenPtr) uint32` — `crypto.subtle.sign('RSASSA-PKCS1-v1_5' + SHA-256)` - - 2026-04-19 完成:wasi-shim.ts 新增 `createArcrunHostFunctions(env, apiKey)` factory,集中 AES-GCM 解密 + RSA sign + KV 前綴路由越權檢查。WASI imports 的 u6u namespace wiring 本來就已接好(只是當時沒有實作 factory)。typecheck 通過。 -- [x] 0.7 在 `cypher-executor/src/lib/component-loader.ts` 新增 WASM runner 路徑: - - 所有 WASM 零件(含 auth primitive、API 零件、未來用戶自製)一律走 HTTP URL(`{canonical-id-kebab}.arcrun.dev`)到獨立 Worker - - **R2 動態注入路徑作廢**(richblack 2026-04-19 確認:CF workerd 不支援以 R2 物件臨時 instantiate WASM;用戶自製零件同樣走「產生獨立 Worker」流程,不走 R2) - - cypher-executor 本身**不做** WASM instantiate,也不直接呼叫 `createArcrunHostFunctions`;那個 factory 是**零件 Worker 側**(`.component-builds/{name}/src/index.ts`)用的,在 Phase 1 建立 auth_static_key Worker 時接上 - - 2026-04-19 完成:`component-loader.ts` 新增 `WASM_HTTP_RUNNER_IDS`(10 個 canonical_id,6 個 API 零件 + 4 個 auth primitive)+ `wasmWorkerUrl()` URL 慣例輔助函數;解析鏈新增為第 8 層(放在 `BUILTIN_API_RECIPES` fallback 之後,避免 Phase 3 尚未完成時 API 零件 Worker 未部署造成 404;Phase 3 刪除 `BUILTIN_API_RECIPES` 後,API 零件會自然落到此層)。auth primitive 從此層進入。`tsc --noEmit` 通過。 - ---- - -## Phase 1:auth_static_key WASM(優先,涵蓋 80% 服務) - -方案 B:WASM 自行讀 KV + 解密,TS 不碰 plaintext。 - -- [x] 1.1 建立 `arcrun/registry/components/auth_static_key/` 目錄 -- [x] 1.2 寫 `component.contract.yaml`(input: `{action, api_key, service, request}` → output: `{success, auth_headers, auth_query, auth_body, runtime}`) -- [x] 1.3 實作 `main.go`(TinyGo): - - 宣告 host imports:`kv_get` / `crypto_decrypt`(static_key 不需要 http_request) - - 從 stdin 讀 `{action, api_key, service}` - - `kv_get("auth_recipe:" + service)` → recipe JSON → 驗證 `primitive == "static_key"` - - 對每個 non-optional `recipe.required_secrets`:`kv_get("{api_key}:cred:{name}")` → `{encrypted, iv}` → `crypto_decrypt` → plaintext - - 展開 `{{secret.X}}` / `{{runtime.X}}` 模板於 `inject.header/query/body`;未知 key 展空字串(與 TS parity);其他 namespace 的 `{{...}}` 原樣保留 - - 輸出 stdout JSON `{success, auth_headers, auth_query, auth_body, runtime}` -- [x] 1.4 `tinygo build -o auth_static_key.wasm -target=wasi main.go` — 2026-04-19 編譯通過(1.1MB,在 contract 限制 2MB 內) -- [🔄] 1.5 建立 `.component-builds/auth_static_key/`(用 `component-worker-template`)並部署到 `auth-static-key.arcrun.dev` - - 2026-04-20 完成**建置**部分:`.component-builds/auth_static_key/{wrangler.toml, package.json, tsconfig.json, src/index.ts, component.wasm}` 全數到位 - - 方案 A:`src/index.ts` 直接 import `../../../cypher-executor/src/lib/wasi-shim` 的 `createWasiShim` + `createArcrunHostFunctions`(以 `ArcrunHostEnv` 結構型別相容);AES 解密邏輯仍只存在於 wasi-shim.ts 一處(rule 02 §2.2) - - 綁同組 KV:CREDENTIALS_KV (e7f4320f88d343f187e35e3543dd74c9) / RECIPES (9cf9db905c6241f78503199e58b2ffe0);ENCRYPTION_KEY 走 `wrangler secret put` - - `wrangler deploy --dry-run` 通過(1192 KiB, 419 KiB gzip);實際 `wrangler deploy` + `secret put ENCRYPTION_KEY` 留給 richblack 執行 -- [x] 1.6 建立 `auth-dispatcher.ts`(取代 `credential-injector.ts`):查 auth recipe → HTTP POST 到對應 auth primitive URL → 合併 `_auth_headers` 進 ctx - - 2026-04-20 完成:`cypher-executor/src/actions/auth-dispatcher.ts` 新建,export `tryAuthDispatch(componentId, input, env, apiKey)` - - 流程:查 `resolveAuthRecipe` → primitive 在 `SUPPORTED_PRIMITIVES`(目前只有 `static_key`)→ fetch `wasmWorkerUrl('auth_static_key')` → 合併 `_auth_headers/_auth_query/_auth_body` - - 自引用防護:`AUTH_PRIMITIVE_IDS` set 排除 4 個 `auth_*` componentId - - `wasmWorkerUrl` 從 `component-loader.ts` export 出來共用 - - `graph-executor.ts` 改為:先試 `tryAuthDispatch`(新路徑),沒命中 fallback 到舊 `injectCredentials`(Phase 1.9 刪) - - 檢查過 auth-dispatcher.ts 無 `crypto.subtle` / `interpolate` / `{{secret.` / hard-code API URL,符合 rule 02 §2.2 - - `tsc --noEmit` 通過 -- [ ] 1.7 端對端測試:openai recipe → 成功注入 `Authorization: Bearer ` -- [ ] 1.8 端對端測試:twilio recipe(Basic Auth)→ 成功注入 -- [ ] 1.9 **刪除 `credential-injector.ts` 整檔**(`decryptCredential` / `decryptSecrets` / `interpolateTemplate` / `BUILTIN_CREDENTIALS_MAP` 全刪) - ---- - -## Phase 2:auth_service_account WASM - -- [🔄] 2.1 建立 `arcrun/registry/components/auth_service_account/` 目錄 -- [🔄] 2.2 寫 `component.contract.yaml` -- [🔄] 2.3 實作 `main.go`: - - 從 stdin 讀 `{api_key, service}` + `kv_get` 拿 recipe + 解密 SA JSON - - 解析 SA JSON 取 `client_email` / `private_key`(PEM) - - PEM → PKCS8 bytes(純 Go,base64 decode + 去 header/footer) - - 組 JWT header + payload(base64url),呼叫 `crypto_sign_rs256(signingInput, pkcs8)` 拿 signature - - 組完整 JWT → `http_request` POST `token_uri` → 拿 `access_token` - - 展開 `{{runtime.access_token}}` 模板 -- [x] 2.4 `tinygo build -o auth_service_account.wasm -target=wasi main.go` — 2026-04-20 編譯通過(1.1MB,在 contract 限制 2MB 內) -- [x] 2.5 建立 `.component-builds/auth_service_account/` 並部署到 `auth-service-account.arcrun.dev` - - 2026-04-20 完成**建置**部分:`.component-builds/auth_service_account/{wrangler.toml, package.json, tsconfig.json, src/index.ts, component.wasm}` 全數到位 - - 方案 A:`src/index.ts` 重用 `createArcrunHostFunctions` 提供 kv_get/crypto_decrypt/crypto_sign_rs256,**額外加 `http_request` host function**(token exchange 用,非 crypto 不受 §2.2 約束)。http_request 直接回 response body 原文(WASM 端 json.Unmarshal 找 access_token) - - 綁同組 KV:CREDENTIALS_KV / RECIPES;ENCRYPTION_KEY 走 `wrangler secret put` - - `wrangler deploy --dry-run` 通過(1248 KiB, 440 KiB gzip);實際 `wrangler deploy` + `secret put ENCRYPTION_KEY` 留給 richblack 執行 - - `auth-dispatcher.ts` 的 `SUPPORTED_PRIMITIVES` 加入 `'service_account'`,workflow 用 google SA recipe 會自動走新 WASM 路徑 -- [ ] 2.6 端對端測試:google_sheets_sa recipe → 成功取得 access_token → 注入 header -- [x] 2.7 **刪除 `lib/jwt-signer.ts` 整檔** — 2026-04-20 完成 - - `cypher-executor/src/lib/jwt-signer.ts` 已刪除(RS256 JWT 邏輯移入 `auth_service_account.wasm`) - - `credential-injector.ts` 原 line 23 `import { exchangeGoogleJwt }` 移除 - - `credential-injector.ts` 原 line 140-150 service_account 分支改為 throw(任何 service_account recipe 已被 auth-dispatcher 攔截;這條 TS fallback 若被觸發即表架構錯亂,直接爆錯比沈默解密更安全) - - `cypher-executor` tsc --noEmit 通過 - ---- - -## Phase 3:清理 component-loader 的 TS 實作(全刪) - -目標:`BUILTIN_API_RECIPES` 整段刪除,所有服務走 WASM runner(HTTP URL 路徑)。 - -- [x] 3.1 確認 `http_request.wasm` / `gmail.wasm` / `telegram.wasm` / `line_notify.wasm` / `google_sheets.wasm` 都在 `registry/components/` 且可執行 — 2026-04-20 驗證 6 個(含 cron)全數存在,main.go + .wasm 齊備 -- [x] 3.2 確認上述零件 Worker 都已部署(`{name}.arcrun.dev` 可用) — 2026-04-20 完成**建置**部分 - - 6 個 Worker 建置到位:`.component-builds/{http_request, gmail, telegram, line_notify, google_sheets, cron}/{wrangler.toml, package.json, tsconfig.json, src/index.ts, component.wasm}` - - 方案 A:5 個需 http_request 的零件(http_request/gmail/telegram/line_notify/google_sheets)`src/index.ts` 共用模板;cron 是純計算不註冊 host function - - 全部透過 `createWasiShim` 複用 cypher-executor/src/lib/wasi-shim.ts(rule 02 §2.2 邊界) - - 6 個 `wrangler deploy --dry-run` 全通過(~1.17 MB / ~413 KB gzip 每個);實際 `wrangler deploy` 留給 richblack 執行 -- [x] 3.3 `component-loader.ts` 的內建路徑改為查對應 Worker URL → HTTP POST — 2026-04-20 完成 - - 原本第 7 層是 `BUILTIN_API_RECIPES` fallback、第 8 層是 `WASM_HTTP_RUNNER_IDS` (HTTP URL);兩層合併為第 7 層 `WASM_HTTP_RUNNER_IDS` 直接走 `makeHttpRunner(wasmWorkerUrl(id))` - - 解析鏈新編號 1-8,順序不變(外部 URL → recipe hash → component hash → R2 → Service Binding → auth recipe runner → WASM HTTP runner → 找不到) -- [x] 3.4 **刪除 `BUILTIN_API_RECIPES` 整個 Record**(`http_request` / `gmail` / `telegram` / `line_notify` / `google_sheets` / `cron` 的 TS 實作全刪) — 2026-04-20 完成 - - `cypher-executor/src/lib/component-loader.ts` 原 line 253-326 `BUILTIN_API_RECIPES` 常數 + fallback lookup 全刪(約 80 行) - - 全域搜尋確認:`gmail.googleapis.com/...messages/send` / `api.telegram.org/bot.*sendMessage` / `sheets.googleapis.com/v4/spreadsheets` / `notify-api.line.me/api/notify` 在 cypher-executor TS 中已不存在(auth-recipe-seeds.ts 的 `base_url` 是 recipe 資料欄位,不是 hard-coded API call) - - `cypher-executor` tsc --noEmit 通過 -- [ ] 3.5 端對端測試:workflow 用 gmail auth recipe + gmail.wasm Worker → 成功發信 -- [ ] 3.6 端對端測試:workflow 用 http_request.wasm Worker + auth_static_key 注入 → 成功呼叫任意 API - ---- - -## Phase 4:auth_oauth2 + auth_mtls WASM(封測後) - -- [ ] 4.1 建立 `arcrun/registry/components/auth_oauth2/` -- [ ] 4.2 實作:`needs_refresh` / `refresh` / `authenticate` 三個 action -- [ ] 4.3 建立 `arcrun/registry/components/auth_mtls/` -- [ ] 4.4 實作:輸出 TLS cert/key(實際 mTLS handshake 由 Worker runtime 執行,WASM 無法做 socket) - ---- - -## Phase 5:封測啟動門檻 — 核心穩定驗證 - -**全部通過才能啟動封測**。 - -- [ ] 5.1 所有 20 個 auth recipe seed 可正常運作(static_key 17 個 + service_account 3 個) -- [ ] 5.2 `cypher-executor/src/actions/credential-injector.ts` **不存在** -- [ ] 5.3 `cypher-executor/src/lib/jwt-signer.ts` **不存在** -- [ ] 5.4 `cypher-executor/src/lib/component-loader.ts` 無 `BUILTIN_API_RECIPES` / `BUILTIN_CREDENTIALS_MAP` -- [ ] 5.5 `cypher-executor/src/` 全域搜尋 `crypto.subtle.decrypt` 只出現在 `wasi-shim.ts` 的 `crypto_decrypt` host function -- [ ] 5.6 `cypher-executor/src/` 全域搜尋 `crypto.subtle.sign` 只出現在 `wasi-shim.ts` 的 `crypto_sign_rs256` host function -- [ ] 5.7 `cypher-executor/src/` 全域搜尋 `interpolate` 回傳 0 筆(template 展開全在 WASM) -- [ ] 5.8 全域搜尋 `{{secret\.` / `{{runtime\.` 在 TS 檔案中回傳 0 筆 - ---- - -## Phase 6:通用 CI/CD deploy workflow - -**背景**(2026-04-20 richblack 決定):現 `.github/workflows/deploy.yml` 只部署 cypher-executor + registry + 已刪除的 credentials,漏掉 Phase 1-3 產出的 8 個 Worker,且硬編碼每個 job 導致未來新增 Worker 都要改 CI。改為**通用掃描式 workflow**:任何含 `wrangler.toml` 的目錄 = 部署單位,改到該目錄下任何檔案 = 觸發重新 deploy。 - -**關鍵決策**: -- 零件 `.wasm` 由 CI build(不 commit):`registry/components/{name}/main.go` 改動時才重 build,用 timestamp / content hash 判斷 -- `.component-builds/{name}/component.wasm` 由 CI 從 `registry/components/{name}/{name}.wasm` 複製產生(deploy 前一步) -- 統一用 pnpm(`.component-builds/*` 本來就是;順勢把 cypher-executor 的 `package-lock.json` 砍了) -- runtime secret(`ENCRYPTION_KEY`)不進 CI,由 richblack 一次性 `wrangler secret put` -- registry Worker 的 `wrangler.toml` 現階段不改(職責是合約管理,與封測無關;`sandboxAcceptance.ts` 的 rule 02 §2.2 審查留到 Phase 5 用戶自製零件啟動時) - -### Tasks - -- [x] 6.1 改寫 `.github/workflows/deploy.yml`:動態掃描所有含 `wrangler.toml` 的目錄(排除 `node_modules/` + Pages 專案),用 matrix job fanout 部署;分兩層(tier1=`.component-builds/*`,tier2=其他),tier1 全綠後才 tier2(避免 service binding target 未存在) -- [x] 6.2 加上 TinyGo build 步驟:tier1 matrix 一律 setup-tinygo + 從 `registry/components/{name}/main.go` rebuild `.wasm` → copy 到 `.component-builds/{name}/component.wasm` -- [x] 6.3 diff-aware:push 到 main 時比對 `github.event.before..github.sha`,只 deploy 有 diff 的 Worker(含 `registry/components/{name}/` 連動 `.component-builds/{name}/`);`workflow_dispatch` 提供 `force_all` + `only` 選項 -- [x] 6.4 統一 pnpm:刪除 `cypher-executor/package-lock.json` + `registry/package-lock.json`;workflow 優先 `pnpm install --frozen-lockfile`,若該目錄無 `pnpm-lock.yaml` 則 fallback 到 `--no-frozen-lockfile`(混合期容錯) -- [x] 6.5 加 `max-parallel: 5` 控制 Workers API rate limit(tier1 和 tier2 各自) -- [x] 6.6 驗證:`workflow_dispatch` + `force_all=true` 手動跑一次,24 個 Worker 全綠 — 2026-04-20 完成 - - 最終綠色 run 24668903627(28/28 jobs,含 discover + summary):tier1 24 個零件 Worker + tier2 2 個 orchestration Worker(cypher-executor / registry)全 success - - 過程中修兩輪:先修 `setup-node` 的 `cache: 'pnpm'` 對 legacy `package-lock.json` 目錄失效(改為不用 cache);再修 tier2 三個 package.json(cypher-executor/registry/builtins)遺漏 `wrangler` devDependency + regen pnpm-lock.yaml - - ENCRYPTION_KEY secret 已由 richblack 授權、CC 從 .env pipe 到三個 Worker:`arcrun-auth-static-key`、`arcrun-auth-service-account`、`arcrun-cypher-executor`(不顯示內容) -- [x] 6.7 文件:在 `.claude/rules/` 加一份 `05-deploy-convention.md`(「新增 Worker = 新目錄 + wrangler.toml,不用改 CI」) - ---- - -## Notes - -- 方案 B 是唯一方案(方案 A 已廢棄,違反 CLAUDE.md §禁止行為) -- Phase 0.6(host functions)+ 0.7(WASM runner)是 Phase 1-3 的硬前置,必須先做 -- 若 TinyGo `encoding/base64` 可用就直接用;若不可用則自行實作(見 gmail/main.go 的 `base64URLEncode`) -- `auth_mtls` 的 TLS handshake 無法在 WASM 內做(WASI preview1 沒 socket),只能輸出 cert/key 讓 Worker 在 fetch 時用 -- **每個 auth primitive WASM 都是獨立部署的 Worker**(透過 `component-worker-template/`),不是從 R2 動態載入 -- Cypher binding = workflow YAML 裡的 URL 清單,不是 Cloudflare service binding diff --git a/.agents/specs/arcrun/frontend-redesign/design-source/SOURCE_README.md b/.agents/specs/arcrun/frontend-redesign/design-source/SOURCE_README.md deleted file mode 100644 index 9e1ca3d..0000000 --- a/.agents/specs/arcrun/frontend-redesign/design-source/SOURCE_README.md +++ /dev/null @@ -1,25 +0,0 @@ -# CODING AGENTS: READ THIS FIRST - -This is a **handoff bundle** from Claude Design (claude.ai/design). - -A user mocked up designs in HTML/CSS/JS using an AI design tool, then exported this bundle so a coding agent can implement the designs for real. - -## What you should do — IMPORTANT - -**Read the chat transcripts first.** There are 1 chat transcript(s) in `arcrun/chats/`. The transcripts show the full back-and-forth between the user and the design assistant — they tell you **what the user actually wants** and **where they landed** after iterating. Don't skip them. The final HTML files are the output, but the chat is where the intent lives. - -**Find the primary design file under `arcrun/project/` and read it top to bottom.** The chat transcripts will tell you which file the user was last iterating on. Then **follow its imports**: open every file it pulls in (shared components, CSS, scripts) so you understand how the pieces fit together before you start implementing. - -**If anything is ambiguous, ask the user to confirm before you start implementing.** It's much cheaper to clarify scope up front than to build the wrong thing. - -## About the design files - -The design medium is **HTML/CSS/JS** — these are prototypes, not production code. Your job is to **recreate them pixel-perfectly** in whatever technology makes sense for the target codebase (React, Vue, native, whatever fits). Match the visual output; don't copy the prototype's internal structure unless it happens to fit. - -**Don't render these files in a browser or take screenshots unless the user asks you to.** Everything you need — dimensions, colors, layout rules — is spelled out in the source. Read the HTML and CSS directly; a screenshot won't tell you anything they don't. - -## Bundle contents - -- `arcrun/README.md` — this file -- `arcrun/chats/` — conversation transcripts (read these!) -- `arcrun/project/` — the `arcrun` project files (HTML prototypes, assets, components) diff --git a/.agents/specs/arcrun/frontend-redesign/design-source/app.jsx b/.agents/specs/arcrun/frontend-redesign/design-source/app.jsx deleted file mode 100644 index 23a9f42..0000000 --- a/.agents/specs/arcrun/frontend-redesign/design-source/app.jsx +++ /dev/null @@ -1,56 +0,0 @@ -// App root — screen switcher with persistent route -const { useState, useEffect } = React; - -const SCREENS = [ - { id: 'landing', label: 'Landing' }, - { id: 'auth', label: 'Auth' }, - { id: 'dashboard', label: 'Dashboard' }, - { id: 'keys', label: 'API Keys' }, - { id: 'workflow', label: 'Workflow' }, -]; - -// Synonyms from sidebar ids -const aliases = { apps: 'dashboard', workflows: 'dashboard', docs: 'landing', settings: 'keys' }; - -function App() { - const [screen, setScreen] = useState(() => { - const saved = localStorage.getItem('arcrun:screen'); - return saved && SCREENS.some(s => s.id === saved) ? saved : 'landing'; - }); - - useEffect(() => { - localStorage.setItem('arcrun:screen', screen); - window.scrollTo(0, 0); - }, [screen]); - - const nav = (id) => { - const resolved = aliases[id] || id; - if (SCREENS.some(s => s.id === resolved)) setScreen(resolved); - }; - - const Current = { - landing: Landing, - auth: Auth, - dashboard: Dashboard, - keys: ApiKeys, - workflow: WorkflowViewer, - }[screen]; - - return ( -
- - -
- {SCREENS.map(s => ( - - ))} -
-
- ); -} - -ReactDOM.createRoot(document.getElementById('root')).render(); diff --git a/.agents/specs/arcrun/frontend-redesign/design-source/components/chrome.jsx b/.agents/specs/arcrun/frontend-redesign/design-source/components/chrome.jsx deleted file mode 100644 index b14ed26..0000000 --- a/.agents/specs/arcrun/frontend-redesign/design-source/components/chrome.jsx +++ /dev/null @@ -1,92 +0,0 @@ -// Top nav and sidebar - -const TopNav = ({ onNav, current }) => { - const [scrolled, setScrolled] = React.useState(false); - React.useEffect(() => { - const onScroll = () => setScrolled(window.scrollY > 8); - window.addEventListener('scroll', onScroll); - return () => window.removeEventListener('scroll', onScroll); - }, []); - return ( - - ); -}; - -const Footer = ({ onNav }) => ( - -); - -// App shell with sidebar for logged-in screens -const Sidebar = ({ current, onNav }) => { - const items = [ - { id: 'dashboard', label: 'Dashboard', icon: 'home' }, - { id: 'apps', label: 'Apps', icon: 'grid', count: 6 }, - { id: 'workflows', label: 'Workflows', icon: 'workflow', count: 12 }, - { id: 'keys', label: 'API Keys', icon: 'key' }, - { id: 'docs', label: 'Docs', icon: 'book' }, - ]; - const bottom = [ - { id: 'settings', label: 'Settings', icon: 'settings' }, - ]; - return ( - - ); -}; - -Object.assign(window, { TopNav, Footer, Sidebar }); diff --git a/.agents/specs/arcrun/frontend-redesign/design-source/components/primitives.jsx b/.agents/specs/arcrun/frontend-redesign/design-source/components/primitives.jsx deleted file mode 100644 index 1aa5fd5..0000000 --- a/.agents/specs/arcrun/frontend-redesign/design-source/components/primitives.jsx +++ /dev/null @@ -1,86 +0,0 @@ -// Shared primitives: icons, logo, etc. - -const Icon = ({ name, size = 16, stroke = 1.7 }) => { - const paths = { - arrow_right: , - arrow_left: , - plus: , - copy: <>, - check: , - close: , - eye: <>, - search: <>, - warn: <>, - home: <>, - grid: <>, - workflow: <>, - key: <>, - book: <>, - settings: <>, - chevron_right: , - chevron_down: , - external: <>, - trash: <>, - spark: , - bolt: , - github: , - google: <>, - share: <>, - download: <>, - zoom_in: <>, - zoom_out: <>, - maximize: <>, - slack: <>, - database: <>, - mail: <>, - filter: , - star: , - linear: <>, - clock: <>, - send: <>, - terminal: <>, - logout: <>, - }; - - return ( - - {paths[name]} - - ); -}; - -// Arcrun wordmark — custom "arc" glyph made of an arc stroke + ascending dot/node -const Logo = ({ size = 'md', onClick }) => { - const dims = size === 'sm' ? { w: 18, h: 18, f: 10 } : size === 'lg' ? { w: 28, h: 28, f: 14 } : { w: 22, h: 22, f: 12 }; - return ( -
- - - - - - - Arcrun -
- ); -}; - -// App icon with gradient background -const AppIcon = ({ tone = 'indigo', children, size = 38 }) => { - const tones = { - indigo: 'linear-gradient(135deg, #6366F1, #8B5CF6)', - orange: 'linear-gradient(135deg, #F59E0B, #EF4444)', - green: 'linear-gradient(135deg, #10B981, #22C55E)', - pink: 'linear-gradient(135deg, #EC4899, #8B5CF6)', - blue: 'linear-gradient(135deg, #3B82F6, #06B6D4)', - slate: 'linear-gradient(135deg, #475569, #334155)', - amber: 'linear-gradient(135deg, #F59E0B, #D97706)', - }; - return ( -
- {children} -
- ); -}; - -Object.assign(window, { Icon, Logo, AppIcon }); diff --git a/.agents/specs/arcrun/frontend-redesign/design-source/design-chat.md b/.agents/specs/arcrun/frontend-redesign/design-source/design-chat.md deleted file mode 100644 index 7088767..0000000 --- a/.agents/specs/arcrun/frontend-redesign/design-source/design-chat.md +++ /dev/null @@ -1,95 +0,0 @@ -# Chat - -_Started 2026-04-23 04:37 UTC_ - ---- - -## User - - - -## Assistant - -I'll design Arcrun as a cohesive 5-screen system. Let me plan and build this as an interactive prototype with all screens navigable. - -_[tool: update_todos]_ - -Let me plan the architecture. I'll build a single HTML file with React, using multiple JSX component files, with screen navigation via state. - -Design system note: I'll use Inter, electric indigo #6366F1 primary, #0F0F0F bg, #1A1A1A cards, with subtle indigo→violet gradients on CTAs. Dark, minimal, developer-focused. "Arcrun" wordmark will be original — a stylized "↯" or arc mark paired with the name. - -Let me start building. - -_[tool: write_file]_ - -_[tool: write_file]_ - -Now the primitives and chrome components: - -_[tool: write_file]_ - -_[tool: write_file]_ - -_[tool: snip]_ - -Now the 5 screens. Let me write them concisely. - -_[tool: write_file]_ - -_[tool: write_file]_ - -_[tool: snip]_ - -_[tool: write_file]_ - -_[tool: write_file]_ - -_[tool: snip]_ - -_[tool: write_file]_ - -_[tool: write_file]_ - -_[tool: snip]_ - -_[tool: update_todos]_ - -_[tool: register_assets]_ - -_[tool: done]_ - -_[tool: snip]_ - -_[tool: list_files]_ - -The file exists. The error may have been transient. Let me verify by checking the page loads. - -_[tool: show_html]_ - -_[tool: snip]_ - -_[tool: done]_ - -The CSS exists but isn't loading through the preview path. Let me inline it to eliminate the dependency. - -_[tool: read_file]_ - -_[tool: run_script]_ - -_[tool: snip]_ - -_[tool: done]_ - -_[tool: fork_verifier_agent]_ - -Built **Arcrun** as a cohesive 5-screen dark-mode prototype — Landing, Auth, API Keys, Dashboard, and Workflow Viewer — all navigable via the floating screen switcher at the bottom. - -**Highlights** -- **Landing** — hero with grid/glow bg, two-path split (developer terminal w/ npm·pip·bun tabs + Claude-style chat preview showing an Arcrun tool call) -- **Auth** — centered card, Sign in / Sign up tabs, GitHub + Google OAuth, email + password -- **API Keys** — first-time key callout with copy + hide-again warning, full keys table with live status toggles -- **Dashboard** — sidebar shell, 3-col apps grid (+ empty-state card), workflows table below -- **Workflow Viewer** — full-bleed dark canvas, 8 connected nodes with left-to-right bezier arrows, selectable with detail panel (I/O schema, editable triplet config for the AI node), minimap + zoom controls, prominent "Edit in Claude" CTA - -Screen state persists in localStorage so reloads land you where you were. - diff --git a/.agents/specs/arcrun/frontend-redesign/design-source/index.html b/.agents/specs/arcrun/frontend-redesign/design-source/index.html deleted file mode 100644 index 8a41c33..0000000 --- a/.agents/specs/arcrun/frontend-redesign/design-source/index.html +++ /dev/null @@ -1,1211 +0,0 @@ - - - - - -Arcrun — AI Workflow Automation - - - - - - - - - - -
- - - - - - - - - - - diff --git a/.agents/specs/arcrun/frontend-redesign/design-source/screens/ApiKeys.jsx b/.agents/specs/arcrun/frontend-redesign/design-source/screens/ApiKeys.jsx deleted file mode 100644 index e39af48..0000000 --- a/.agents/specs/arcrun/frontend-redesign/design-source/screens/ApiKeys.jsx +++ /dev/null @@ -1,128 +0,0 @@ -const ApiKeys = ({ onNav }) => { - const [newKeyCopied, setNewKeyCopied] = React.useState(false); - const [keys, setKeys] = React.useState([ - { id: 'k_dev', name: 'Local Development', prefix: 'ar_dev_', created: 'Mar 12, 2026', lastUsed: '2 min ago', active: true }, - { id: 'k_prod', name: 'Production — Northwind API', prefix: 'ar_live_', created: 'Feb 3, 2026', lastUsed: '12 sec ago', active: true }, - { id: 'k_staging', name: 'Staging — Vercel', prefix: 'ar_test_', created: 'Jan 28, 2026', lastUsed: '4 hours ago', active: true }, - { id: 'k_ci', name: 'CI/CD (GitHub Actions)', prefix: 'ar_live_', created: 'Jan 10, 2026', lastUsed: 'Yesterday', active: false }, - { id: 'k_old', name: 'Legacy — Zapier import', prefix: 'ar_live_', created: 'Nov 4, 2025', lastUsed: '3 weeks ago', active: false, revoked: true }, - ]); - - const newKey = 'ar_live_sk_7x9Qf2vLm8nR4TpW6ZjKc3bEhN1aSyU5oP0dI'; - - const copyKey = () => { - setNewKeyCopied(true); - setTimeout(() => setNewKeyCopied(false), 1800); - }; - - const toggleKey = (id) => { - setKeys(keys.map(k => k.id === id ? { ...k, active: !k.active } : k)); - }; - - return ( -
- -
-
-
-
- Workspace - - Settings -
-

API Keys

-
Scoped credentials for calling the Arcrun API from your code and CI.
-
-
- - -
-
- -
-
-
- - Save this key now. For security, we won't show it again — if you lose it, you'll need to create a new one. -
-

Your new API key

-

Key named "Production — Northwind API" · created just now · all scopes

-
- {newKey} - -
-
- Full workspace access - Never expires - Add expiry or restrict scopes → -
-
- -
-
-

All keys

-
{keys.filter(k => !k.revoked).length} active · {keys.filter(k => k.revoked).length} revoked
-
-
- -
-
- -
- - - - - - - - - - - - - {keys.map(k => ( - - - - - - - - - ))} - -
NameKeyCreatedLast usedStatus
-
{k.name}
-
{k.prefix}••••{k.id.slice(-4)}{k.created}{k.lastUsed} - {k.revoked ? ( - Revoked - ) : ( -
- toggleKey(k.id)} /> - - {k.active ? 'Active' : 'Paused'} - -
- )} -
- {!k.revoked && ( - - )} -
-
- -
- - Revoking a key stops all in-flight requests within 60 seconds. This cannot be undone. -
-
-
-
- ); -}; - -window.ApiKeys = ApiKeys; diff --git a/.agents/specs/arcrun/frontend-redesign/design-source/screens/Auth.jsx b/.agents/specs/arcrun/frontend-redesign/design-source/screens/Auth.jsx deleted file mode 100644 index cd4ace7..0000000 --- a/.agents/specs/arcrun/frontend-redesign/design-source/screens/Auth.jsx +++ /dev/null @@ -1,90 +0,0 @@ -const Auth = ({ onNav }) => { - const [mode, setMode] = React.useState('signin'); - const [email, setEmail] = React.useState(''); - const [pw, setPw] = React.useState(''); - const [remember, setRemember] = React.useState(true); - - const submit = (e) => { e.preventDefault(); onNav('dashboard'); }; - - return ( -
-
-
- -
- onNav('landing')} /> -
- -
-

{mode === 'signin' ? 'Welcome back' : 'Create your account'}

-

{mode === 'signin' ? 'Sign in to your Arcrun workspace.' : 'Start building AI workflows in minutes.'}

- -
- - -
- -
- - -
- -
or continue with email
- -
- {mode === 'signup' && ( -
- - -
- )} -
- - setEmail(e.target.value)} /> -
-
-
- - {mode === 'signin' && Forgot password?} -
- setPw(e.target.value)} /> -
- - {mode === 'signin' && ( -
-
setRemember(!remember)} - style={{width: 15, height: 15, borderRadius: 4, border: '1px solid var(--line-2)', - background: remember ? 'var(--primary)' : 'transparent', - display: 'flex', alignItems: 'center', justifyContent: 'center', cursor: 'pointer'}}> - {remember && } -
- setRemember(!remember)} style={{cursor: 'pointer'}}>Keep me signed in for 30 days -
- )} - - -
- - {mode === 'signup' && ( -

- By signing up, you agree to our Terms and Privacy Policy. -

- )} - -
- {mode === 'signin' - ? <>New to Arcrun? setMode('signup')}>Create an account - : <>Already have an account? setMode('signin')}>Sign in} -
-
-
- ); -}; - -window.Auth = Auth; diff --git a/.agents/specs/arcrun/frontend-redesign/design-source/screens/Dashboard.jsx b/.agents/specs/arcrun/frontend-redesign/design-source/screens/Dashboard.jsx deleted file mode 100644 index b9033aa..0000000 --- a/.agents/specs/arcrun/frontend-redesign/design-source/screens/Dashboard.jsx +++ /dev/null @@ -1,126 +0,0 @@ -const Dashboard = ({ onNav }) => { - const apps = [ - { id: 'digest', name: 'Weekly Digest', desc: 'Summarize customer activity into a Monday email for the revenue team.', icon: 'mail', tone: 'indigo' }, - { id: 'triage', name: 'Support Triage', desc: 'Classify inbound tickets, attach context from the CRM, and route.', icon: 'filter', tone: 'orange' }, - { id: 'seo', name: 'SEO Brief Generator', desc: 'Turn a keyword into a draft brief with outline, FAQs, and SERP notes.', icon: 'search', tone: 'green' }, - { id: 'slack', name: 'Standup Bot', desc: 'Collect Linear updates and post a tidy engineering standup to Slack.', icon: 'slack', tone: 'pink' }, - { id: 'doc', name: 'Docs Sync', desc: 'Keep Notion runbooks in sync with the production API surface.', icon: 'book', tone: 'blue' }, - ]; - - const workflows = [ - { id: 'digest_weekly', name: 'digest/weekly', nodes: 9, modified: '2 hours ago', runs: '147 runs', status: 'healthy' }, - { id: 'triage_inbound', name: 'triage/inbound-email', nodes: 14, modified: 'Yesterday', runs: '2,318 runs', status: 'healthy' }, - { id: 'seo_brief', name: 'seo/brief-from-keyword', nodes: 7, modified: '3 days ago', runs: '42 runs', status: 'healthy' }, - { id: 'standup', name: 'slack/standup-collector', nodes: 6, modified: '1 week ago', runs: '24 runs', status: 'idle' }, - { id: 'docs_sync', name: 'docs/sync-notion', nodes: 11, modified: '2 weeks ago', runs: '8 runs', status: 'failed' }, - ]; - - return ( -
- -
-
-
-
- Northwind - - Dashboard -
-

Welcome back, Maya

-
5 apps running · 12 workflows · 2,538 runs this week
-
-
- - -
-
- -
- {/* Apps grid */} -
-
-

My Apps

-
Packaged workflows your team can run from chat or code
-
- {apps.length} apps -
- -
- {apps.map(a => ( -
- -

{a.name}

-

{a.desc}

-
- onNav('workflow')}>Open app - -
-
- ))} -
-
-
Create new app
-
Start from scratch or template
-
-
- - {/* Workflows */} -
-
-
-

My Workflows

-
The graphs that power your apps
-
-
- - -
-
- -
- - - - - - - - - - - - - {workflows.map(w => ( - - - - - - - - - ))} - -
WorkflowNodesLast modifiedActivityStatus
-
- - {w.name} -
-
{w.nodes}{w.modified}{w.runs} - - {w.status} - - - -
-
-
-
-
-
- ); -}; - -window.Dashboard = Dashboard; diff --git a/.agents/specs/arcrun/frontend-redesign/design-source/screens/Landing.jsx b/.agents/specs/arcrun/frontend-redesign/design-source/screens/Landing.jsx deleted file mode 100644 index 460d35c..0000000 --- a/.agents/specs/arcrun/frontend-redesign/design-source/screens/Landing.jsx +++ /dev/null @@ -1,168 +0,0 @@ -const Landing = ({ onNav }) => { - const [installer, setInstaller] = React.useState('npm'); - const installCmds = { - npm: '$ npm install arcrun', - pip: '$ pip install arcrun', - bun: '$ bun add arcrun', - }; - - return ( -
- - -
-
-
-
-
- - Now in public beta — MCP-native -
-

Build AI workflows
without the glue code.

-

Connect your tools, automate your work. Orchestrate workflows from Claude.ai, your IDE, or a few lines of code — Arcrun handles auth, retries, and state.

-
- - -
-
- -
- {/* Developer path */} -
-
- For Developers -
-

Three lines, any runtime.

-

Install once, call Arcrun from Node, Python, or your edge runtime. OAuth, rate limits, and retries are handled.

- -
- {Object.keys(installCmds).map(k => ( - - ))} -
- -
-
-
-
terminal
-
-
-
{installCmds[installer]}
-
-
- -
-
-
-
{installer === 'pip' ? 'app.py' : 'app.ts'}
-
-
- {installer === 'pip' ? ( - <> -
from arcrun import Arcrun
-
-
client = Arcrun(token=os.getenv("ARCRUN_KEY"))
-
run = client.run("digest/weekly", inputs={'{'}"user": "u_219"{'}'})
- - ) : ( - <> -
import {'{'} Arcrun {'}'} from "arcrun";
-
-
const client = new Arcrun({'{'} token: process.env.ARCRUN_KEY {'}'});
-
const run = await client.run("digest/weekly", {'{'} user: "u_219" {'}'});
- - )} -
-
- -
-
- Typed SDKs - Idempotent runs - Self-host ready -
-
- - {/* Everyone path */} -
-
- For Everyone -
-

Talk to your workflows.

-

Install Arcrun inside your AI assistant and run your apps by asking. Trigger workflows, fetch data, or draft messages — in plain English.

- -
-
- AI - Your assistant — Arcrun connected - 2 apps -
-
-
-
M
-
Send this week's customer digest to the revenue team.
-
-
-
A
-
- Running digest/weekly for 147 accounts, then posting to #revenue. -
-
AR
-
-
arcrun · digest/weekly
-
4 of 5 steps complete · 00:12 elapsed
-
- running -
-
-
-
-
- Reply to your assistant… - -
-
- -
-
- One-click connect - Works in your IDE - Audit trail -
-
-
- -
-
-
-

Run anywhere

-

Node, Python, Deno, Bun, Cloudflare Workers. One API, same semantics.

-
-
-
-

Composable steps

-

Model calls, HTTP, database, branching — wire them visually or in code.

-
-
-
-

Scoped keys

-

Per-workflow API keys with fine-grained scopes and live revocation.

-
-
-
-

Observable

-

Every run is replayable. Inspect inputs, outputs, and token usage.

-
-
-
- -
-
- ); -}; - -window.Landing = Landing; diff --git a/.agents/specs/arcrun/frontend-redesign/design-source/screens/WorkflowViewer.jsx b/.agents/specs/arcrun/frontend-redesign/design-source/screens/WorkflowViewer.jsx deleted file mode 100644 index cb943ed..0000000 --- a/.agents/specs/arcrun/frontend-redesign/design-source/screens/WorkflowViewer.jsx +++ /dev/null @@ -1,255 +0,0 @@ -const WorkflowViewer = ({ onNav }) => { - const nodes = [ - { id: 'trigger', x: 60, y: 260, title: 'Weekly Schedule', type: 'trigger', badge: 'CRON', icon: 'clock', tone: '#22C55E', - inputs: [], outputs: [{k: 'timestamp', t: 'ISO8601'}, {k: 'runId', t: 'string'}] }, - { id: 'fetch', x: 320, y: 140, title: 'Fetch Accounts', type: 'database.query', badge: 'DB', icon: 'database', tone: '#3B82F6', - inputs: [{k: 'segment', t: 'string'}], outputs: [{k: 'accounts', t: 'Account[]'}, {k: 'count', t: 'number'}] }, - { id: 'events', x: 320, y: 380, title: 'Pull Events', type: 'segment.events', badge: 'API', icon: 'bolt', tone: '#F59E0B', - inputs: [{k: 'since', t: 'ISO8601'}], outputs: [{k: 'events', t: 'Event[]'}] }, - { id: 'summarize', x: 600, y: 260, title: 'Summarize with Claude', type: 'ai.completion', badge: 'AI', icon: 'spark', tone: '#8B5CF6', - inputs: [{k: 'accounts', t: 'Account[]'}, {k: 'events', t: 'Event[]'}, {k: 'prompt', t: 'string'}], - outputs: [{k: 'digest', t: 'Digest'}, {k: 'tokens', t: 'number'}] }, - { id: 'filter', x: 880, y: 160, title: 'Filter — priority ≥ 2', type: 'logic.filter', badge: 'IF', icon: 'filter', tone: '#64748B', - inputs: [{k: 'digest', t: 'Digest'}], outputs: [{k: 'items', t: 'Item[]'}] }, - { id: 'slack', x: 1140, y: 100, title: 'Post to #revenue', type: 'slack.message', badge: 'OUT', icon: 'slack', tone: '#EC4899', - inputs: [{k: 'channel', t: 'string'}, {k: 'blocks', t: 'Block[]'}], outputs: [{k: 'ts', t: 'string'}] }, - { id: 'mail', x: 1140, y: 260, title: 'Email Digest', type: 'mail.send', badge: 'OUT', icon: 'mail', tone: '#6366F1', - inputs: [{k: 'to', t: 'string[]'}, {k: 'subject', t: 'string'}, {k: 'html', t: 'string'}], outputs: [{k: 'messageId', t: 'string'}] }, - { id: 'log', x: 880, y: 400, title: 'Log run metadata', type: 'arcrun.log', badge: 'LOG', icon: 'terminal', tone: '#475569', - inputs: [{k: 'runId', t: 'string'}, {k: 'stats', t: 'Stats'}], outputs: [] }, - ]; - - const edges = [ - ['trigger', 'fetch'], - ['trigger', 'events'], - ['fetch', 'summarize'], - ['events', 'summarize'], - ['summarize', 'filter'], - ['summarize', 'log'], - ['filter', 'slack'], - ['filter', 'mail'], - ]; - - const [selectedId, setSelectedId] = React.useState('summarize'); - const [title, setTitle] = React.useState('digest/weekly'); - const [zoom, setZoom] = React.useState(100); - - const selected = nodes.find(n => n.id === selectedId); - - // Edit triplet inline (for the summarize node's prompt config) - const [triplet, setTriplet] = React.useState({ - model: 'claude-haiku-4-5', - temperature: '0.3', - prompt: 'Summarize this week\'s account activity for the revenue team.', - }); - - // Measure node widths for edge endpoint accuracy - const nodeRefs = React.useRef({}); - const [sizes, setSizes] = React.useState({}); - React.useEffect(() => { - const ns = {}; - for (const n of nodes) { - const el = nodeRefs.current[n.id]; - if (el) ns[n.id] = { w: el.offsetWidth, h: el.offsetHeight }; - } - setSizes(ns); - }, []); - - const getPort = (id, side) => { - const n = nodes.find(x => x.id === id); - const sz = sizes[id] || { w: 200, h: 60 }; - return { - x: side === 'out' ? n.x + sz.w : n.x, - y: n.y + sz.h / 2, - }; - }; - - return ( -
-
-
onNav('dashboard')} title="Back to dashboard"> - -
- onNav('landing')} /> -
-
- onNav('dashboard')}>Workflows - - setTitle(e.target.value)} - /> -
- - - Saved · 2m ago - -
- - - -
- -
- - - - - - - - - - {edges.map(([a, b], i) => { - const p1 = getPort(a, 'out'); - const p2 = getPort(b, 'in'); - const dx = Math.max(40, (p2.x - p1.x) * 0.5); - const d = `M ${p1.x} ${p1.y} C ${p1.x + dx} ${p1.y}, ${p2.x - dx} ${p2.y}, ${p2.x - 2} ${p2.y}`; - const highlight = a === selectedId || b === selectedId; - return ( - - ); - })} - - -
- {nodes.map(n => ( -
(nodeRefs.current[n.id] = el)} - className={`wf-node ${selectedId === n.id ? 'selected' : ''}`} - style={{left: n.x, top: n.y}} - onClick={() => setSelectedId(n.id)}> - {n.inputs.length > 0 && } - {n.outputs.length > 0 && } -
- - - - {n.title} - {n.badge} -
-
{n.type}
-
- ))} -
- - {/* Detail panel */} - {selected && ( -
-
- - - -
-

{selected.title}

-
{selected.type}
-
- -
-
-
-

Input schema

- {selected.inputs.length === 0 ? ( -
No inputs — this is a trigger.
- ) : selected.inputs.map(f => ( -
- {f.k} - {f.t} -
- ))} -
- -
-

Output schema

- {selected.outputs.length === 0 ? ( -
No outputs — terminal node.
- ) : selected.outputs.map(f => ( -
- {f.k} - {f.t} -
- ))} -
- - {selected.id === 'summarize' && ( -
-

Configuration

-
-
-
model
- setTriplet({...triplet, model: e.target.value})} /> -
-
-
temp
- setTriplet({...triplet, temperature: e.target.value})} /> -
-
-
prompt
- setTriplet({...triplet, prompt: e.target.value})} /> -
-
-
- )} - -
-

Last run

-
-
-
Duration
-
2.4s
-
-
-
Status
-
success
-
-
-
- - -
-
- )} - - {/* Minimap */} -
-
Overview
- {nodes.map(n => { - const sz = sizes[n.id] || {w: 180, h: 60}; - return ( -
- ); - })} -
- - {/* Zoom controls */} -
- -
{zoom}%
- - -
-
-
- ); -}; - -window.WorkflowViewer = WorkflowViewer; diff --git a/.agents/specs/arcrun/frontend-redesign/design.md b/.agents/specs/arcrun/frontend-redesign/design.md deleted file mode 100644 index 9ea56f8..0000000 --- a/.agents/specs/arcrun/frontend-redesign/design.md +++ /dev/null @@ -1,300 +0,0 @@ -# Frontend Redesign — Design - -> 讀此檔前請先讀 `requirements.md` 和 `design-source/index.html`。 -> 視覺 spec 的 single source of truth 是 `design-source/`(Claude Design 匯出的 HTML/JSX prototype)。 - ---- - -## 1. 架構總覽 - -``` -landing/ (Next.js 15 App Router) -├── app/ -│ ├── layout.tsx ← 全站 layout:next/font + design tokens + 全域 CSS 匯入 -│ ├── globals.css ← 匯入 design-tokens.css;Tailwind @import -│ ├── design-tokens.css ← 新增:從 design-source 抽出的 CSS variables(:root {...}) -│ ├── page.tsx ← Landing(RSC) -│ ├── auth/ -│ │ └── page.tsx ← Auth("use client") -│ ├── dashboard/ -│ │ └── page.tsx ← Dashboard("use client",仍靠 middleware 保護) -│ ├── keys/ -│ │ └── page.tsx ← API Keys("use client") -│ ├── workflows/ -│ │ ├── page.tsx ← Workflows 清單(redirect 到 dashboard 的 table,本身極簡) -│ │ └── [name]/page.tsx ← Workflow Viewer("use client") -│ ├── integrations/page.tsx ← 保留現有 -│ ├── api-docs/page.tsx ← 保留現有 -│ └── login/page.tsx ← 保留現有(redirect /auth 同義;見 §9 遷移策略) -├── components/ -│ ├── shell/ -│ │ ├── Logo.tsx -│ │ ├── Icon.tsx -│ │ ├── TopNav.tsx -│ │ ├── Footer.tsx -│ │ └── Sidebar.tsx -│ ├── primitives/ -│ │ ├── Button.tsx ← btn / btn-primary / btn-secondary / btn-ghost 對應 class -│ │ ├── Pill.tsx -│ │ ├── Toggle.tsx -│ │ ├── Terminal.tsx ← landing hero 右卡用 -│ │ └── ChatPreview.tsx ← landing hero 右卡用 -│ └── workflow/ -│ ├── Canvas.tsx ← wf-viewer 本體(節點 + SVG edges) -│ ├── NodeCard.tsx -│ ├── DetailPanel.tsx -│ ├── Minimap.tsx -│ └── ZoomControls.tsx -├── lib/ -│ ├── api.ts ← typed fetch wrapper(fetch ${API_BASE}${path}, credentials: 'include') -│ ├── workflows.ts ← listWorkflows / getWorkflow / getWorkflowYaml -│ ├── apiKeys.ts ← listKeys / createKey / patchKey / deleteKey -│ └── me.ts ← 已存在邏輯,集中到此 -├── middleware.ts ← 擴展 matcher(加 /keys, /workflows/*) -└── ...(既有 package.json / wrangler.toml 不變) -``` - -**路由對照設計稿的 5 screen**: - -| Screen | Route | -|---|---| -| Landing | `/` | -| Auth | `/auth`(新增;`/login` 保留並內部 `redirect('/auth')`) | -| Dashboard | `/dashboard` | -| API Keys | `/keys` | -| Workflow Viewer | `/workflows/[name]` | - ---- - -## 2. Design tokens 對應 - -設計稿所有 CSS 變數抄進 `app/design-tokens.css`,**不解析、不改名**: - -```css -:root { - --bg: #0F0F0F; - --bg-1: #141414; - --card: #1A1A1A; - --card-2: #222222; - --line: #262626; - --line-2: #303030; - --text: #EDEDED; - --text-dim: #A0A0A0; - --text-mute: #6B6B6B; - --primary: #6366F1; - --primary-2: #8B5CF6; - --primary-soft: rgba(99, 102, 241, 0.12); - --primary-ring: rgba(99, 102, 241, 0.32); - --success: #22C55E; - --warn: #F59E0B; - --danger: #EF4444; - --gradient: linear-gradient(135deg, #6366F1 0%, #8B5CF6 100%); - --gradient-soft: linear-gradient(135deg, rgba(99,102,241,0.16) 0%, rgba(139,92,246,0.16) 100%); -} -``` - -並在 Tailwind v4 的 `@theme inline` block 內對應出: - -```css -@theme inline { - --color-bg: var(--bg); - --color-card: var(--card); - --color-card-2: var(--card-2); - --color-line: var(--line); - --color-line-2: var(--line-2); - --color-text: var(--text); - --color-text-dim: var(--text-dim); - --color-text-mute: var(--text-mute); - --color-primary: var(--primary); - --color-primary-2: var(--primary-2); -} -``` - -這樣 JSX 裡可用 `bg-bg / text-text-dim / border-line`,又保留 CSS 變數語義。 - -**現有的 `--background: #0a0a0a` 要換成 `#0F0F0F`**(視覺 breaking change;受影響:所有沿用 `bg-[#0a0a0a]` 的 inline 值)。 - ---- - -## 3. 字型 - -```tsx -// app/layout.tsx -import { Inter, JetBrains_Mono } from 'next/font/google'; - -const inter = Inter({ - subsets: ['latin'], - variable: '--font-inter', - weight: ['300', '400', '500', '600', '700', '800'], -}); -const mono = JetBrains_Mono({ - subsets: ['latin'], - variable: '--font-mono', - weight: ['400', '500', '600'], -}); - -// body class = `${inter.variable} ${mono.variable}` -``` - -`globals.css` 中的 `body { font-family: var(--font-inter), -apple-system, sans-serif; }`,`.mono` class 用 `font-family: var(--font-mono)`。 - -**移除**: -- `design-source/index.html` 第 7-9 行的 ` / `(不寫入 production)。 -- React / Babel standalone script 標籤(prototype 專用,不進 production)。 - ---- - -## 4. 元件 porting 規則 - -Claude Design 用了 `window.Icon / window.Logo / window.AppIcon / window.TopNav ...` 的 globals 風格 — 那是 prototype 專用。Port 到 Next.js 時: - -1. 每個元件拆單檔、具名 export。 -2. 用 Tailwind + `className` 模板字串;共用 variant(如 btn)用 `cva`-style helper 即可(自己寫 5 行的 `clsx`-alike 函式),**不引入 class-variance-authority / clsx 套件**(避免新依賴)。 -3. Icon 的 `paths` 直接搬,但每個 icon 拆成自己的 functional component 或集中在一個 ``(沿用 design source 的 pattern)。 -4. SVG arc wordmark 的 logo 直接 port。 - ---- - -## 5. 各 screen 實作細節 - -### 5.1 Landing — `app/page.tsx` - -- 結構:`` + `` + `` + `` + `