arcrun — AI workflow execution engine (clean history)

Self-hosted 開源:WASM 零件 + recipe + cypher-executor,跑在你自己的 Cloudflare。

此為重建的乾淨歷史起點(移除曾誤 commit 的 GCP SA 金鑰,舊歷史保留在
richblack/arcrun 與本地 backup 分支)。含:
- acr init --self-hosted installer(建 KV/R2 + codeload 拉預編譯 wasm + wrangler deploy + seed recipe)
- recipe push 把關(資料外流提醒 + 打通檢查)
- 19 個正當零件預編譯 wasm(claude_api/km_writer/kbdb_upsert_block 排除:違反 DECISIONS §1)
- CLI / cypher-executor / registry / 完整 SDD

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
uncle6me-web
2026-06-03 15:52:38 +08:00
commit 922a57fe34
485 changed files with 89356 additions and 0 deletions
+761
View File
@@ -0,0 +1,761 @@
# arcrun Credential System 設計規格
20260418
> **讀者**Claude CodeCC),負責實作
> **作者**richblack(架構決策)
> **版本**v1.0
> **狀態**:Draft — 等 CC 確認技術可行性後開工
---
## 0. TL;DR(給 CC 的三句話版)
1. **不要**為每個服務寫一個 credential 零件,n8n 是錯的。
2. 做**四個 TinyGo/WASM 零件**primitives),每個服務只需要一份 **YAML recipe** + 用戶自己的 **secret**
3. Recipe 存 arcrun 平台 KV(公共),secret 存 tenant KV(私有),兩者在 runtime 由 `AuthBroker` 組裝成可用的 HTTP client。
---
## 1. 設計目標與反目標
### 目標
- **新增一個服務的成本 = 寫一份 YAML**,不需要 rebuild、不需要改 code。
- **AI agent 理解成本 ≈ 0**:recipe 就是呼叫該服務的完整說明書。
- **人類設定成本 < 10 分鐘**:即使是對 OAuth 不熟的使用者,UI 只問「你的 API Key 是什麼」這類 secret 層級問題。
- **Secret 隔離**:每個 tenant 的 secret 絕對不互相可見,arcrun 平台本身也無法明文讀取(用 Cloudflare Secrets Store 或加密儲存)。
### 反目標(明確不做的事)
- ❌ 不做 n8n 那種「每個服務一個 credential type」的視覺化面板。
- ❌ 不支援 OAuth1(2026 年還在用的服務極少,真遇到再加)。
- ❌ 不做 credential sharing 的複雜 ACL(全 tenant scope 即可,未來再擴充)。
- ❌ 不在 arcrun 內部明文持久化任何長期 secret(只有加密過的密文或 Secrets Store reference)。
---
## 2. 核心架構:三層模型
```
┌─────────────────────────────────────────────────────────┐
│ Layer 3: Service Recipe (YAML) │
│ arcrun 平台共享,describe "如何呼叫這個服務" │
│ 存在 Workers KV: arcrun-recipes │
│ 例:recipe/notion.yaml, recipe/google_calendar.yaml │
└─────────────────────────────────────────────────────────┘
↓ 引用
┌─────────────────────────────────────────────────────────┐
│ Layer 2: Auth Primitive (TinyGo → WASM) │
│ 四個通用認證零件,實作注入邏輯與 token 交換 │
│ 1. static_key 2. oauth2 │
│ 3. service_account 4. mtls │
└─────────────────────────────────────────────────────────┘
↑ 使用
┌─────────────────────────────────────────────────────────┐
│ Layer 1: Tenant Secret (KV + Secrets Store) │
│ 每個 tenant 自己的 KV namespace │
│ 存 encrypted secret 或 Secrets Store reference │
│ 例:secret/{tenant_id}/notion-prod │
└─────────────────────────────────────────────────────────┘
```
### 為什麼這樣切?
| 切分維度 | Recipe | Primitive | Secret |
|---|---|---|---|
| **誰擁有** | arcrun 平台 | arcrun 平台 | tenant 自己 |
| **變化頻率** | 中(新服務時) | 低(認證機制穩定) | 高(rotate、revoke |
| **敏感度** | 公開 | 公開 | 最高機密 |
| **儲存位置** | 平台 KV`arcrun-recipes` | WASM binary | tenant KV + Secrets Store |
| **可否社群貢獻** | ✅ PR | ⚠️ 核心團隊 | ❌ 永遠不 |
---
## 3. 四個 Primitive 詳細規格
### 3.1 `static_key`
**適用**API Key、Bearer Token、Basic Auth、任何「一組 secret 不會自動過期」的認證。
**涵蓋 n8n 的**API Key、Basic Auth、Header Auth、Query Auth、Custom Auth、Digest Auth~80% 服務)。
**Recipe 欄位**
```yaml
primitive: static_key
inject:
# 四個注入位置,可以同時用多個
header: # HTTP headers
<key>: <value template>
query: # URL query string
<key>: <value template>
body: # request bodyJSON 欄位)
<key>: <value template>
basic_auth: # HTTP Basic Auth(會自動 base64 編碼)
username: <value template>
password: <value template>
```
**Value template 語法**`{{secret.xxx}}` 取 secret 欄位,`{{const.yyy}}` 取 recipe 內定義的常數。
**Secret schema**tenant 存 JSON,欄位由 recipe 的 `required_secrets` 宣告。
**範例(Notion**
```yaml
# arcrun-recipes KV: recipe/notion
service: notion
version: 1
primitive: static_key
base_url: https://api.notion.com/v1
required_secrets:
- key: token
label: "Internal Integration Token"
help_url: https://www.notion.so/my-integrations
inject:
header:
Authorization: "Bearer {{secret.token}}"
Notion-Version: "2022-06-28"
test:
method: GET
path: /users/me
expect_status: 200
```
**Secret 範例**
```json
// tenant KV: secret/tenant_123/notion-prod
{
"token": "secret_abc123..."
}
```
---
### 3.2 `oauth2`
**適用**:需要人類首次授權、之後用 refresh token 續命的場景。
**Grant types 支援**
- `authorization_code`(最常見:GitHub、Slack、Google 用戶授權)
- `client_credentials`(機器對機器)
- `pkce`SPA、行動應用)
- ❌ 不支援:password grant2026 已被多數 OAuth 提供者棄用)、implicit(已棄用)
**Recipe 欄位**
```yaml
primitive: oauth2
grant: authorization_code # or client_credentials, pkce
base_url: <service API base>
oauth:
authorize_url: <IdP authorize endpoint>
token_url: <IdP token endpoint>
scopes:
- <default scope 1>
- <default scope 2>
client_auth: header # or body
# 是否使用 refresh token
refresh: true
# PKCE 時額外參數
pkce_method: S256 # only for grant: pkce
required_secrets:
- key: client_id
label: "Client ID"
- key: client_secret
label: "Client Secret"
secret: true
inject:
header:
Authorization: "Bearer {{runtime.access_token}}"
```
**Runtime 欄位**primitive 自動維護,存在 tenant KV 的 `oauth_state/{secret_id}` key):
- `access_token`
- `refresh_token`
- `expires_at`
**首次授權流程**(人類要做的部分):
1. arcrun UI 呼叫 `AuthBroker.startAuth(recipe_id, tenant_id)` 回傳 authorize URL。
2. 使用者瀏覽器跳轉到 IdP,同意授權。
3. IdP redirect 回 arcrun callback endpoint(固定一個 URL,無論哪個服務)。
4. `AuthBroker` 用 authorization code 換 token,寫入 tenant KV。
**之後 agent 呼叫時完全自動**primitive 檢查 `expires_at`,過期自動用 refresh token 續,失敗再觸發重新授權通知。
---
### 3.3 `service_account`
**適用**Google Service Account、AWS IAM Roleassume role)、任何需要「私鑰簽 JWT 換短期 token」的機器身份。
**這個就是讓你 debug 兩天那個爆炸點。** 我們用 primitive 把地雷全部包起來。
**Recipe 欄位**
```yaml
primitive: service_account
kind: google_jwt # or aws_sigv4, generic_jwt
base_url: <service API base>
token_exchange:
# Google 的 JWT → OAuth access token 流程
endpoint: https://oauth2.googleapis.com/token
audience: https://oauth2.googleapis.com/token
scopes:
- https://www.googleapis.com/auth/calendar
# JWT claims
issuer_from_secret: client_email
subject_from_secret: client_email # optional, for domain-wide delegation 改成其他 user
ttl_seconds: 3600
required_secrets:
- key: service_account_json
label: "Service Account JSON"
type: json_blob # 特別型別,UI 可以接受貼整個 JSON
help: "到 GCP Console → IAM → Service Accounts → Keys → Add Key (JSON) 下載整份 JSON 貼上"
inject:
header:
Authorization: "Bearer {{runtime.access_token}}"
```
**為什麼不是每個服務一個 recipe?**
- Google Calendar、Gmail、Drive、Sheets 全部可以共用同一個 `service_account` primitive。
- 差別只在 `scopes``base_url`
- Recipe 本身可以 import 共通片段(見 §5 recipe 繼承)。
**AWS SigV4kind: aws_sigv4**:這是特例,不是 JWT-based,但概念一樣——用 access_key_id + secret_access_key 在每次 request 上簽章。Primitive 內建處理,recipe 只要宣告 region 和 service name。
---
### 3.4 `mtls`
**適用**mTLS / client certificate。銀行 API、企業內部服務、醫療系統。
**Recipe 欄位**
```yaml
primitive: mtls
base_url: <service API base>
required_secrets:
- key: client_cert
label: "Client Certificate (PEM)"
type: pem_cert
- key: client_key
label: "Client Private Key (PEM)"
type: pem_key
secret: true
- key: ca_cert
label: "CA Certificate (PEM) — optional"
type: pem_cert
optional: true
# mtls 通常不需要額外 inject,憑證在 TLS 層
inject: {}
```
**實作注意**Cloudflare Workers 有原生 mTLS 支援(`mTLSCertificate` binding),primitive 只需要把 secret 轉成 Cloudflare mTLS binding 即可。
---
## 4. Recipe YAML Schema(完整版)
```yaml
# 必填
service: string # 唯一識別,snake_casee.g. "notion", "google_calendar"
version: integer # recipe schema versionbreaking change 要升版
primitive: enum # static_key | oauth2 | service_account | mtls
base_url: string # service API base URL
# primitive 相關(依 primitive 不同)
inject: object # 如何把 secret 注入 HTTP request
oauth: object # 僅 oauth2 primitive
token_exchange: object # 僅 service_account primitive
# Secret 宣告(讓 UI 知道要問什麼)
required_secrets:
- key: string # secret 欄位名
label: string # UI 顯示
secret: boolean # 是否遮蔽顯示(default: true
type: enum # text | json_blob | pem_cert | pem_key | url
optional: boolean # default: false
help: string # 給使用者的提示
help_url: string # 導向服務文件
# 測試(驗證 credential 是否有效)
test:
method: GET | POST
path: string # 相對 base_url
expect_status: integer
expect_json: object # 選填,JSON path assertion
# Metadata
display_name: string # UI 顯示名
description: string
icon_url: string
docs_url: string
tags:
- communication
- crm
- ai
maintainers:
- github: username
# 可選:共通片段繼承
extends: string # recipe name,繼承其 schema 後覆寫
```
---
## 5. Recipe 繼承(reduce 重複)
Google 家族的 API 長得很像,重複寫 15 次太蠢。支援 `extends`
```yaml
# recipe/_google_base.yaml(底線開頭 = 抽象 recipe,不能直接用)
service: _google_base
version: 1
primitive: service_account
token_exchange:
endpoint: https://oauth2.googleapis.com/token
audience: https://oauth2.googleapis.com/token
ttl_seconds: 3600
required_secrets:
- key: service_account_json
type: json_blob
inject:
header:
Authorization: "Bearer {{runtime.access_token}}"
```
```yaml
# recipe/google_calendar.yaml
extends: _google_base
service: google_calendar
version: 1
base_url: https://www.googleapis.com/calendar/v3
token_exchange:
scopes:
- https://www.googleapis.com/auth/calendar
test:
method: GET
path: /users/me/calendarList
expect_status: 200
```
繼承規則:scalar 覆寫,object 深度合併,array 預設覆寫(可用 `!append` 標記 append)。
---
## 6. TinyGo WASM Primitive 實作介面
### 6.1 統一介面(四個 primitive 都實作這個)
```go
// primitive/interface.go
package primitive
type AuthRequest struct {
Method string
URL string
Headers map[string]string
Body []byte
}
type AuthContext struct {
Recipe Recipe // parsed YAML
Secret map[string]any // decrypted secret
Runtime RuntimeState // oauth token cache 等
Now int64 // for testing
}
type Primitive interface {
// 在 HTTP request 上注入認證資訊
Authenticate(req *AuthRequest, ctx *AuthContext) error
// 檢查是否需要 refreshoauth2 / service_account 用)
NeedsRefresh(ctx *AuthContext) bool
// 執行 refresh / token exchange,回傳新的 RuntimeState
Refresh(ctx *AuthContext) (RuntimeState, error)
// 驗證 credential 是否有效(執行 recipe.test
Test(ctx *AuthContext) error
}
```
### 6.2 編譯與部署
```bash
# 四個 primitive 各自編譯成獨立 WASM
tinygo build -o dist/static_key.wasm -target=wasi ./primitive/static_key
tinygo build -o dist/oauth2.wasm -target=wasi ./primitive/oauth2
tinygo build -o dist/service_account.wasm -target=wasi ./primitive/service_account
tinygo build -o dist/mtls.wasm -target=wasi ./primitive/mtls
# 部署時放到 Cloudflare Workers 的 Assets 或直接內嵌
```
### 6.3 Runtime 載入(Worker 端)
```typescript
// worker/src/auth-broker.ts
import staticKeyWasm from "../dist/static_key.wasm"
import oauth2Wasm from "../dist/oauth2.wasm"
// ...
const primitives = {
static_key: await instantiate(staticKeyWasm),
oauth2: await instantiate(oauth2Wasm),
service_account: await instantiate(serviceAccountWasm),
mtls: await instantiate(mtlsWasm),
}
```
> **為什麼 WASM 而不是直接 TS**
> 1. 跨 runtime 可攜性(未來若 arcrun 要跑在 Fly.io、local、或客戶自建環境,同一個 primitive 能用)。
> 2. 配合 u6u/arcrun 既定的 WASM 架構方向,不破壞統一性。
> 3. 沙箱化:primitive 只能透過明確的 host function 存取外部世界(網路、KV),降低惡意 recipe 攻擊面。
---
## 7. AuthBroker API(給 arcrun 其他部分調用)
```typescript
interface AuthBroker {
// Agent 執行時用的主要 API
bind(serviceId: string, secretRef: string, tenantId: string): Promise<AuthenticatedClient>
// 首次授權(僅 oauth2 用)
startAuth(serviceId: string, tenantId: string): Promise<{ authorizeUrl: string, state: string }>
completeAuth(state: string, code: string): Promise<{ secretRef: string }>
// 測試 credential
test(serviceId: string, secretRef: string, tenantId: string): Promise<TestResult>
// 管理
listRecipes(): Promise<Recipe[]>
getRecipe(serviceId: string): Promise<Recipe>
}
interface AuthenticatedClient {
fetch(path: string, init?: RequestInit): Promise<Response>
}
```
**使用範例(agent 端)**
```typescript
const notion = await authBroker.bind("notion", "notion-prod", ctx.tenantId)
const res = await notion.fetch("/databases/abc/query", {
method: "POST",
body: JSON.stringify({ filter: {...} })
})
```
**Agent 完全不需要知道是 API Key 還是 OAuth**——`authBroker.bind()` 回傳的 client 已經注入好認證,fetch 路徑用相對 base_url 的路徑即可。
---
## 8. Storage Layout
### 8.1 Recipe 儲存(arcrun 平台共享)
**Cloudflare KV namespace**`arcrun-recipes`
```
key: recipe/{service_id}
value: <recipe YAML 的 JSON 化版本>
key: recipe-list
value: [{ service_id, display_name, icon_url, tags }, ...] # 加速 UI 列表
```
**更新流程**
1. Recipe YAML 存在 arcrun 主 repo 的 `recipes/` 目錄下(version control + PR review)。
2. CI 跑 schema validator,通過後上傳到 KV。
3. UI 的 recipe 列表 5 分鐘 cache。
### 8.2 Secret 儲存(tenant 私有)
**雙層策略**
- **短期、低敏感** → tenant KV,用 AES-256-GCM 加密,key 從 Cloudflare Secrets Store 拿。
- **高敏感(如 service account JSON、private key** → 直接存 Cloudflare Secrets Storetenant KV 只存 reference。
```
# tenant KV namespace: arcrun-tenant-{tenant_id}
key: secret/{service_id}/{instance_name}
value: {
"recipe_version": 1,
"storage_mode": "kv_encrypted" | "secrets_store_ref",
"data": <encrypted blob> | { "ref": "secrets-store-id" },
"created_at": "...",
"last_verified_at": "..."
}
# oauth2 runtime stateprimitive 自動管理)
key: oauth_state/{service_id}/{instance_name}
value: {
"access_token": "...", # encrypted
"refresh_token": "...", # encrypted
"expires_at": 1234567890
}
```
**secretRef 格式**`{service_id}/{instance_name}`,例如 `notion/prod``google_calendar/workspace-a`
一個 tenant 可以同一個服務存多個 instance(多帳號場景)。
### 8.3 KBDB 整合(可選,但建議)
**按照 KBDB 架構,recipe metadata 可以用 Block + Template 表達**(不是 credential 本體,只是 metadata):
建立一個 `service_recipe` Template
```json
{
"name": "service_recipe",
"display_name": "服務 Recipe Metadata",
"schema": {
"fields": [
{"key": "service_id", "type": "text", "required": true, "description": "服務識別"},
{"key": "primitive", "type": "text", "required": true, "description": "使用的 primitive"},
{"key": "version", "type": "number", "required": true, "description": "Recipe 版本"},
{"key": "display_name", "type": "text", "required": false, "description": "顯示名稱"},
{"key": "docs_url", "type": "text", "required": false, "description": "文件 URL"},
{"key": "kv_key", "type": "text", "required": true, "description": "KV 實際存取 key"}
]
}
}
```
Secret **不**進 KBDBKBDB 不該存敏感資料),只有 metadata 在 KBDB 裡方便搜尋和關聯。
---
## 9. 首次授權 UI Flow(給人類看的部分)
這是「學員不知道該選哪個 credential 的痛點」的終結方案。
### 9.1 Static Key 的 UI
```
┌──────────────────────────────────────────┐
│ 連接 Notion │
├──────────────────────────────────────────┤
│ │
│ Internal Integration Token │
│ ┌────────────────────────────────────┐ │
│ │ secret_••••••••••••• │ │
│ └────────────────────────────────────┘ │
│ ↳ 如何取得?→ 開啟 Notion 整合設定頁 │
│ │
│ [ 測試連線 ] [ 儲存 ] │
└──────────────────────────────────────────┘
```
**零選項。** UI 從 recipe 的 `required_secrets` 動態生成。使用者不用選「這是 Header Auth 還是 Query Auth 還是 Custom Auth」——那是 recipe 的事,不是使用者的事。
### 9.2 OAuth2 的 UI
```
┌──────────────────────────────────────────┐
│ 連接 GitHub │
├──────────────────────────────────────────┤
│ │
│ [ 🔗 使用 GitHub 帳號登入 ] │
│ │
│ 將跳轉到 GitHub,授權後自動返回 │
│ │
└──────────────────────────────────────────┘
```
**一個按鈕。** Client ID / Secret 由 arcrun 平台統一管理(OAuth App 註冊在 arcrun 這邊),使用者看不到也不用知道。
### 9.3 Service Account 的 UI
```
┌──────────────────────────────────────────┐
│ 連接 Google Calendar │
├──────────────────────────────────────────┤
│ │
│ Service Account JSON │
│ ┌────────────────────────────────────┐ │
│ │ 將整份 JSON 貼到這裡 │ │
│ │ │ │
│ │ { │ │
│ │ "type": "service_account", │ │
│ │ "project_id": "...", │ │
│ │ ... │ │
│ │ } │ │
│ └────────────────────────────────────┘ │
│ │
│ 如何取得?→ 展開步驟說明 ▼ │
│ 1. 打開 GCP Console │
│ 2. IAM → Service Accounts │
│ 3. 建立 Service Account │
│ 4. Keys → Add Key → JSON │
│ 5. 下載後整份貼到上方 │
│ │
│ [ 測試連線 ] [ 儲存 ] │
└──────────────────────────────────────────┘
```
**貼 JSON + 按鈕**。不用寫任何程式碼,不用 debug 兩天。
---
## 10. 實作任務分解(CC 的 TODO list
### Phase 1:核心骨架(1-2 週)
- [ ] **T1.1** Recipe YAML schema 定義 + JSON Schema validator(放 `arcrun/schemas/recipe.schema.json`
- [ ] **T1.2** Recipe loader:從 `recipes/` 目錄讀 YAML → validate → 轉 JSON 存入 KV namespace `arcrun-recipes`
- [ ] **T1.3** TinyGo WASM 專案骨架(`arcrun/primitives/`),四個子目錄,統一 interface
- [ ] **T1.4** Worker runtime 的 WASM loader + host function(網路、KV 讀寫)
- [ ] **T1.5** `AuthBroker` TypeScript 類別骨架 + unit test
### Phase 2Static Key1 週)
- [ ] **T2.1** `static_key.wasm` 實作(header/query/body/basic_auth 四種注入)
- [ ] **T2.2** 寫三個 recipe`notion.yaml`, `openai.yaml`, `stripe.yaml`
- [ ] **T2.3** Tenant KV secret 加密寫入 + `AuthBroker.bind()` 整合
- [ ] **T2.4** `recipe.test` 執行器(驗證 credential 有效性)
- [ ] **T2.5** E2E test:存 secret → bind → fetch Notion API → assert
### Phase 3OAuth21-2 週)
- [ ] **T3.1** `oauth2.wasm` 實作(authorization_code + client_credentials + pkce
- [ ] **T3.2** OAuth callback endpoint(統一 URL,用 state 路由到正確 tenant/recipe
- [ ] **T3.3** Refresh token 自動續命邏輯(rate-limit 保護:同一 token 不能 1 秒內 refresh 多次)
- [ ] **T3.4** 寫三個 recipe`github.yaml`, `slack.yaml`, `google_oauth_user.yaml`
- [ ] **T3.5** UI flowstartAuth → 跳轉 → callback → 寫 secret
### Phase 4Service Account1 週)
- [ ] **T4.1** `service_account.wasm` 實作(google_jwt
- [ ] **T4.2** Google JWT signingES256 / RS256)— **這個 TinyGo 需要注意 crypto 支援**
- [ ] **T4.3** AWS SigV4 簽章實作(kind: aws_sigv4
- [ ] **T4.4** Recipe 繼承機制(`extends` 支援)
- [ ] **T4.5** 寫 recipes`_google_base`, `google_calendar`, `google_drive`, `gmail`, `aws_s3`
### Phase 5mTLS + 收尾(1 週)
- [ ] **T5.1** `mtls.wasm` 實作(對接 Cloudflare `mTLSCertificate` binding
- [ ] **T5.2** Cloudflare Secrets Store 整合(高敏感 secret 用)
- [ ] **T5.3** Recipe marketplace UI(列出可用 recipe,搜尋,一鍵設定)
- [ ] **T5.4** Observability:每次 bind / refresh / test 記錄到 KBDBmetadata,不含 secret
- [ ] **T5.5** Docs:recipe 撰寫指南(讓社群能貢獻)
### Phase 6Recipe 生成器(選配,1 週)
- [ ] **T6.1** 給 Claude 一份 API doc,自動產 recipe YAML 草稿 + 人類 review 介面
- [ ] **T6.2** 從 OpenAPI spec 自動推論 recipe
- [ ] **T6.3** 從 n8n credential file 反向轉譯(擷取 400+ 現成整合)
---
## 11. 關鍵技術風險與對策
| 風險 | 對策 |
|---|---|
| **TinyGo 的 crypto 支援不完整**ES256 / RS256 JWT 簽章) | 先用 `crypto/rsa` + `crypto/ecdsa` 確認 TinyGo 版本支援;若不行,fallback 用 Worker runtime 的 `crypto.subtle` 實作這部分,WASM 透過 host function 呼叫 |
| **Recipe 被惡意提交**(如 inject 內含 `https://evil.com` 當 token_url | Recipe 走 PR review + CI 自動檢查 URL 白名單;社群貢獻的 recipe 預設隔離在 `community/` 目錄,使用者明確選擇才啟用 |
| **OAuth state CSRF** | state 用 `crypto.randomUUID()` + 5 分鐘 TTL,存在 KVcallback 時比對 |
| **Secret 在 Worker log 外洩** | `AuthContext.Secret` 禁止 `toString` / `JSON.stringify`,用 Proxy 攔截;log 層強制 redact |
| **Token refresh 風暴**100 個並發 request 同時發現過期) | 用 Durable Object 單執行緒化每個 secret 的 refresh,其他 request 等結果 |
| **TinyGo WASM bundle size** | 四個 primitive 分開編譯,最大 500KB/個;lazy load |
| **Recipe 版本升級破壞相容** | `version` 欄位 semvertenant secret 記錄 `recipe_version`primitive 內處理遷移 |
---
## 12. 對比 n8n(給內部 review / 行銷用)
| 維度 | n8n | arcrun |
|---|---|---|
| Credential types 數量 | 400+(一個服務一個) | 4primitive + N recipe |
| 新增一個服務 | 寫 TypeScript class + rebuild + npm publish | 寫一份 YAML + PR merge |
| AI agent 使用 | 需要讀 node 文件 + 猜參數 | 讀 recipe YAML 即可 |
| 使用者首次設定 | 從 400+ 選項選一個(常選錯) | 搜尋服務名,只問必要 secret |
| OAuth App 管理 | 使用者自己註冊 OAuth app | arcrun 平台統一管理(使用者只需點「授權」) |
| 社群貢獻成本 | 高(TS + 編譯 + 測試) | 低(YAML + 測試) |
---
## 13. 接下來的決策點(需要 richblack 確認)
- [ ] **Recipe 版本管理策略**:採用 semver?每個 recipe 獨立版本?還是整個 recipe set 一個版本?
- [ ] **OAuth App 註冊**:arcrun 平台要統一註冊幾個主流服務的 OAuth AppGitHub、Google、Slack、Microsoft)?還是讓 tenant 自己帶 client_id/secret
- 建議:**雙模式**——平台模式(方便)+ BYO 模式(企業客戶用自己的 OAuth app 有稽核好處)
- [ ] **Recipe registry 的審核流程**:完全開放 PR 還是僅核心團隊維護?
- 建議:`recipes/official/`(核心維護)+ `recipes/community/`(PR 審核後 merge,使用者需明確啟用)
- [ ] **Secret rotation 政策**:要不要內建提醒 / 自動 rotate?(Phase 7+
---
## 14. 附錄:完整範例
### A. 最小可行 recipeOpenAI
```yaml
service: openai
version: 1
primitive: static_key
display_name: OpenAI
base_url: https://api.openai.com/v1
required_secrets:
- key: api_key
label: API Key
help_url: https://platform.openai.com/api-keys
inject:
header:
Authorization: "Bearer {{secret.api_key}}"
test:
method: GET
path: /models
expect_status: 200
tags: [ai]
```
### B. OAuth2 recipeSlack
```yaml
service: slack
version: 1
primitive: oauth2
display_name: Slack
base_url: https://slack.com/api
grant: authorization_code
oauth:
authorize_url: https://slack.com/oauth/v2/authorize
token_url: https://slack.com/api/oauth.v2.access
scopes:
- chat:write
- channels:read
refresh: true
client_auth: header
inject:
header:
Authorization: "Bearer {{runtime.access_token}}"
test:
method: POST
path: /auth.test
expect_json:
ok: true
tags: [communication]
```
### C. Service Account recipeGoogle Calendar
```yaml
extends: _google_base
service: google_calendar
version: 1
display_name: Google Calendar
base_url: https://www.googleapis.com/calendar/v3
token_exchange:
scopes:
- https://www.googleapis.com/auth/calendar
test:
method: GET
path: /users/me/calendarList
expect_status: 200
tags: [calendar, google]
```
---
## 15. 給 CC 的行動指引
1. **先不要動既有 arcrun 的 credential 相關 code**,保持現狀到 Phase 2 完成再切換。
2. **Phase 1 + 2 是 MVP**,做完可以接 80% 服務(API key 類)。
3. **遇到 TinyGo 的技術阻礙(特別是 crypto),立刻回報**,不要自己 workaround 兩天。
4. 每個 Phase 完成後寫一份 brief report(能跑什麼、不能跑什麼、下一步)。
5. Recipe 撰寫先做 3-5 個手工範例,**確認 schema 夠用再開始批量生成**。