# arcrun Credential System 設計規格
20260418

> **讀者**：Claude Code（CC），負責實作
> **作者**：richblack（架構決策）
> **版本**：v1.0
> **狀態**：Draft — 等 CC 確認技術可行性後開工

---

## 0. TL;DR（給 CC 的三句話版）

1. **不要**為每個服務寫一個 credential 零件，n8n 是錯的。
2. 做**四個 TinyGo/WASM 零件**（primitives），每個服務只需要一份 **YAML recipe** + 用戶自己的 **secret**。
3. Recipe 存 arcrun 平台 KV（公共），secret 存 tenant KV（私有），兩者在 runtime 由 `AuthBroker` 組裝成可用的 HTTP client。

---

## 1. 設計目標與反目標

### 目標
- **新增一個服務的成本 = 寫一份 YAML**，不需要 rebuild、不需要改 code。
- **AI agent 理解成本 ≈ 0**：recipe 就是呼叫該服務的完整說明書。
- **人類設定成本 < 10 分鐘**：即使是對 OAuth 不熟的使用者，UI 只問「你的 API Key 是什麼」這類 secret 層級問題。
- **Secret 隔離**：每個 tenant 的 secret 絕對不互相可見，arcrun 平台本身也無法明文讀取（用 Cloudflare Secrets Store 或加密儲存）。

### 反目標（明確不做的事）
- ❌ 不做 n8n 那種「每個服務一個 credential type」的視覺化面板。
- ❌ 不支援 OAuth1（2026 年還在用的服務極少，真遇到再加）。
- ❌ 不做 credential sharing 的複雜 ACL（全 tenant scope 即可，未來再擴充）。
- ❌ 不在 arcrun 內部明文持久化任何長期 secret（只有加密過的密文或 Secrets Store reference）。

---

## 2. 核心架構：三層模型

```
┌─────────────────────────────────────────────────────────┐
│  Layer 3: Service Recipe (YAML)                         │
│  arcrun 平台共享，describe "如何呼叫這個服務"            │
│  存在 Workers KV: arcrun-recipes                        │
│  例：recipe/notion.yaml, recipe/google_calendar.yaml    │
└─────────────────────────────────────────────────────────┘
                         ↓ 引用
┌─────────────────────────────────────────────────────────┐
│  Layer 2: Auth Primitive (TinyGo → WASM)                │
│  四個通用認證零件，實作注入邏輯與 token 交換              │
│  1. static_key    2. oauth2                             │
│  3. service_account 4. mtls                            │
└─────────────────────────────────────────────────────────┘
                         ↑ 使用
┌─────────────────────────────────────────────────────────┐
│  Layer 1: Tenant Secret (KV + Secrets Store)            │
│  每個 tenant 自己的 KV namespace                        │
│  存 encrypted secret 或 Secrets Store reference         │
│  例：secret/{tenant_id}/notion-prod                     │
└─────────────────────────────────────────────────────────┘
```

### 為什麼這樣切？

| 切分維度 | Recipe | Primitive | Secret |
|---|---|---|---|
| **誰擁有** | arcrun 平台 | arcrun 平台 | tenant 自己 |
| **變化頻率** | 中（新服務時） | 低（認證機制穩定） | 高（rotate、revoke） |
| **敏感度** | 公開 | 公開 | 最高機密 |
| **儲存位置** | 平台 KV（`arcrun-recipes`） | WASM binary | tenant KV + Secrets Store |
| **可否社群貢獻** | ✅ PR | ⚠️ 核心團隊 | ❌ 永遠不 |

---

## 3. 四個 Primitive 詳細規格

### 3.1 `static_key`

**適用**：API Key、Bearer Token、Basic Auth、任何「一組 secret 不會自動過期」的認證。

**涵蓋 n8n 的**：API Key、Basic Auth、Header Auth、Query Auth、Custom Auth、Digest Auth（~80% 服務）。

**Recipe 欄位**：
```yaml
primitive: static_key
inject:
  # 四個注入位置，可以同時用多個
  header:     # HTTP headers
    <key>: <value template>
  query:      # URL query string
    <key>: <value template>
  body:       # request body（JSON 欄位）
    <key>: <value template>
  basic_auth: # HTTP Basic Auth（會自動 base64 編碼）
    username: <value template>
    password: <value template>
```

**Value template 語法**：`{{secret.xxx}}` 取 secret 欄位，`{{const.yyy}}` 取 recipe 內定義的常數。

**Secret schema**：tenant 存 JSON，欄位由 recipe 的 `required_secrets` 宣告。

**範例（Notion）**：
```yaml
# arcrun-recipes KV: recipe/notion
service: notion
version: 1
primitive: static_key
base_url: https://api.notion.com/v1
required_secrets:
  - key: token
    label: "Internal Integration Token"
    help_url: https://www.notion.so/my-integrations
inject:
  header:
    Authorization: "Bearer {{secret.token}}"
    Notion-Version: "2022-06-28"
test:
  method: GET
  path: /users/me
  expect_status: 200
```

**Secret 範例**：
```json
// tenant KV: secret/tenant_123/notion-prod
{
  "token": "secret_abc123..."
}
```

---

### 3.2 `oauth2`

**適用**：需要人類首次授權、之後用 refresh token 續命的場景。

**Grant types 支援**：
- `authorization_code`（最常見：GitHub、Slack、Google 用戶授權）
- `client_credentials`（機器對機器）
- `pkce`（SPA、行動應用）
- ❌ 不支援：password grant（2026 已被多數 OAuth 提供者棄用）、implicit（已棄用）

**Recipe 欄位**：
```yaml
primitive: oauth2
grant: authorization_code  # or client_credentials, pkce
base_url: <service API base>
oauth:
  authorize_url: <IdP authorize endpoint>
  token_url: <IdP token endpoint>
  scopes:
    - <default scope 1>
    - <default scope 2>
  client_auth: header  # or body
  # 是否使用 refresh token
  refresh: true
  # PKCE 時額外參數
  pkce_method: S256  # only for grant: pkce
required_secrets:
  - key: client_id
    label: "Client ID"
  - key: client_secret
    label: "Client Secret"
    secret: true
inject:
  header:
    Authorization: "Bearer {{runtime.access_token}}"
```

**Runtime 欄位**（primitive 自動維護，存在 tenant KV 的 `oauth_state/{secret_id}` key）：
- `access_token`
- `refresh_token`
- `expires_at`

**首次授權流程**（人類要做的部分）：
1. arcrun UI 呼叫 `AuthBroker.startAuth(recipe_id, tenant_id)` 回傳 authorize URL。
2. 使用者瀏覽器跳轉到 IdP，同意授權。
3. IdP redirect 回 arcrun callback endpoint（固定一個 URL，無論哪個服務）。
4. `AuthBroker` 用 authorization code 換 token，寫入 tenant KV。

**之後 agent 呼叫時完全自動**：primitive 檢查 `expires_at`，過期自動用 refresh token 續，失敗再觸發重新授權通知。

---

### 3.3 `service_account`

**適用**：Google Service Account、AWS IAM Role（assume role）、任何需要「私鑰簽 JWT 換短期 token」的機器身份。

**這個就是讓你 debug 兩天那個爆炸點。** 我們用 primitive 把地雷全部包起來。

**Recipe 欄位**：
```yaml
primitive: service_account
kind: google_jwt  # or aws_sigv4, generic_jwt
base_url: <service API base>
token_exchange:
  # Google 的 JWT → OAuth access token 流程
  endpoint: https://oauth2.googleapis.com/token
  audience: https://oauth2.googleapis.com/token
  scopes:
    - https://www.googleapis.com/auth/calendar
  # JWT claims
  issuer_from_secret: client_email
  subject_from_secret: client_email  # optional, for domain-wide delegation 改成其他 user
  ttl_seconds: 3600
required_secrets:
  - key: service_account_json
    label: "Service Account JSON"
    type: json_blob  # 特別型別，UI 可以接受貼整個 JSON
    help: "到 GCP Console → IAM → Service Accounts → Keys → Add Key (JSON) 下載整份 JSON 貼上"
inject:
  header:
    Authorization: "Bearer {{runtime.access_token}}"
```

**為什麼不是每個服務一個 recipe？**
- Google Calendar、Gmail、Drive、Sheets 全部可以共用同一個 `service_account` primitive。
- 差別只在 `scopes` 和 `base_url`。
- Recipe 本身可以 import 共通片段（見 §5 recipe 繼承）。

**AWS SigV4（kind: aws_sigv4）**：這是特例，不是 JWT-based，但概念一樣——用 access_key_id + secret_access_key 在每次 request 上簽章。Primitive 內建處理，recipe 只要宣告 region 和 service name。

---

### 3.4 `mtls`

**適用**：mTLS / client certificate。銀行 API、企業內部服務、醫療系統。

**Recipe 欄位**：
```yaml
primitive: mtls
base_url: <service API base>
required_secrets:
  - key: client_cert
    label: "Client Certificate (PEM)"
    type: pem_cert
  - key: client_key
    label: "Client Private Key (PEM)"
    type: pem_key
    secret: true
  - key: ca_cert
    label: "CA Certificate (PEM) — optional"
    type: pem_cert
    optional: true
# mtls 通常不需要額外 inject，憑證在 TLS 層
inject: {}
```

**實作注意**：Cloudflare Workers 有原生 mTLS 支援（`mTLSCertificate` binding），primitive 只需要把 secret 轉成 Cloudflare mTLS binding 即可。

---

## 4. Recipe YAML Schema（完整版）

```yaml
# 必填
service: string              # 唯一識別，snake_case，e.g. "notion", "google_calendar"
version: integer             # recipe schema version，breaking change 要升版
primitive: enum              # static_key | oauth2 | service_account | mtls
base_url: string             # service API base URL

# primitive 相關（依 primitive 不同）
inject: object               # 如何把 secret 注入 HTTP request
oauth: object                # 僅 oauth2 primitive
token_exchange: object       # 僅 service_account primitive

# Secret 宣告（讓 UI 知道要問什麼）
required_secrets:
  - key: string              # secret 欄位名
    label: string            # UI 顯示
    secret: boolean          # 是否遮蔽顯示（default: true）
    type: enum               # text | json_blob | pem_cert | pem_key | url
    optional: boolean        # default: false
    help: string             # 給使用者的提示
    help_url: string         # 導向服務文件

# 測試（驗證 credential 是否有效）
test:
  method: GET | POST
  path: string               # 相對 base_url
  expect_status: integer
  expect_json: object        # 選填，JSON path assertion

# Metadata
display_name: string         # UI 顯示名
description: string
icon_url: string
docs_url: string
tags:
  - communication
  - crm
  - ai
maintainers:
  - github: username

# 可選：共通片段繼承
extends: string              # recipe name，繼承其 schema 後覆寫
```

---

## 5. Recipe 繼承（reduce 重複）

Google 家族的 API 長得很像，重複寫 15 次太蠢。支援 `extends`：

```yaml
# recipe/_google_base.yaml（底線開頭 = 抽象 recipe，不能直接用）
service: _google_base
version: 1
primitive: service_account
token_exchange:
  endpoint: https://oauth2.googleapis.com/token
  audience: https://oauth2.googleapis.com/token
  ttl_seconds: 3600
required_secrets:
  - key: service_account_json
    type: json_blob
inject:
  header:
    Authorization: "Bearer {{runtime.access_token}}"
```

```yaml
# recipe/google_calendar.yaml
extends: _google_base
service: google_calendar
version: 1
base_url: https://www.googleapis.com/calendar/v3
token_exchange:
  scopes:
    - https://www.googleapis.com/auth/calendar
test:
  method: GET
  path: /users/me/calendarList
  expect_status: 200
```

繼承規則：scalar 覆寫，object 深度合併，array 預設覆寫（可用 `!append` 標記 append）。

---

## 6. TinyGo WASM Primitive 實作介面

### 6.1 統一介面（四個 primitive 都實作這個）

```go
// primitive/interface.go
package primitive

type AuthRequest struct {
    Method  string
    URL     string
    Headers map[string]string
    Body    []byte
}

type AuthContext struct {
    Recipe   Recipe            // parsed YAML
    Secret   map[string]any    // decrypted secret
    Runtime  RuntimeState      // oauth token cache 等
    Now      int64             // for testing
}

type Primitive interface {
    // 在 HTTP request 上注入認證資訊
    Authenticate(req *AuthRequest, ctx *AuthContext) error

    // 檢查是否需要 refresh（oauth2 / service_account 用）
    NeedsRefresh(ctx *AuthContext) bool

    // 執行 refresh / token exchange，回傳新的 RuntimeState
    Refresh(ctx *AuthContext) (RuntimeState, error)

    // 驗證 credential 是否有效（執行 recipe.test）
    Test(ctx *AuthContext) error
}
```

### 6.2 編譯與部署

```bash
# 四個 primitive 各自編譯成獨立 WASM
tinygo build -o dist/static_key.wasm -target=wasi ./primitive/static_key
tinygo build -o dist/oauth2.wasm -target=wasi ./primitive/oauth2
tinygo build -o dist/service_account.wasm -target=wasi ./primitive/service_account
tinygo build -o dist/mtls.wasm -target=wasi ./primitive/mtls

# 部署時放到 Cloudflare Workers 的 Assets 或直接內嵌
```

### 6.3 Runtime 載入（Worker 端）

```typescript
// worker/src/auth-broker.ts
import staticKeyWasm from "../dist/static_key.wasm"
import oauth2Wasm from "../dist/oauth2.wasm"
// ...

const primitives = {
  static_key:      await instantiate(staticKeyWasm),
  oauth2:          await instantiate(oauth2Wasm),
  service_account: await instantiate(serviceAccountWasm),
  mtls:            await instantiate(mtlsWasm),
}
```

> **為什麼 WASM 而不是直接 TS？**
> 1. 跨 runtime 可攜性（未來若 arcrun 要跑在 Fly.io、local、或客戶自建環境，同一個 primitive 能用）。
> 2. 配合 u6u/arcrun 既定的 WASM 架構方向，不破壞統一性。
> 3. 沙箱化：primitive 只能透過明確的 host function 存取外部世界（網路、KV），降低惡意 recipe 攻擊面。

---

## 7. AuthBroker API（給 arcrun 其他部分調用）

```typescript
interface AuthBroker {
  // Agent 執行時用的主要 API
  bind(serviceId: string, secretRef: string, tenantId: string): Promise<AuthenticatedClient>

  // 首次授權（僅 oauth2 用）
  startAuth(serviceId: string, tenantId: string): Promise<{ authorizeUrl: string, state: string }>
  completeAuth(state: string, code: string): Promise<{ secretRef: string }>

  // 測試 credential
  test(serviceId: string, secretRef: string, tenantId: string): Promise<TestResult>

  // 管理
  listRecipes(): Promise<Recipe[]>
  getRecipe(serviceId: string): Promise<Recipe>
}

interface AuthenticatedClient {
  fetch(path: string, init?: RequestInit): Promise<Response>
}
```

**使用範例（agent 端）**：
```typescript
const notion = await authBroker.bind("notion", "notion-prod", ctx.tenantId)
const res = await notion.fetch("/databases/abc/query", {
  method: "POST",
  body: JSON.stringify({ filter: {...} })
})
```

**Agent 完全不需要知道是 API Key 還是 OAuth**——`authBroker.bind()` 回傳的 client 已經注入好認證，fetch 路徑用相對 base_url 的路徑即可。

---

## 8. Storage Layout

### 8.1 Recipe 儲存（arcrun 平台共享）

**Cloudflare KV namespace**：`arcrun-recipes`

```
key: recipe/{service_id}
value: <recipe YAML 的 JSON 化版本>

key: recipe-list
value: [{ service_id, display_name, icon_url, tags }, ...]  # 加速 UI 列表
```

**更新流程**：
1. Recipe YAML 存在 arcrun 主 repo 的 `recipes/` 目錄下（version control + PR review）。
2. CI 跑 schema validator，通過後上傳到 KV。
3. UI 的 recipe 列表 5 分鐘 cache。

### 8.2 Secret 儲存（tenant 私有）

**雙層策略**：
- **短期、低敏感** → tenant KV，用 AES-256-GCM 加密，key 從 Cloudflare Secrets Store 拿。
- **高敏感（如 service account JSON、private key）** → 直接存 Cloudflare Secrets Store，tenant KV 只存 reference。

```
# tenant KV namespace: arcrun-tenant-{tenant_id}
key: secret/{service_id}/{instance_name}
value: {
  "recipe_version": 1,
  "storage_mode": "kv_encrypted" | "secrets_store_ref",
  "data": <encrypted blob> | { "ref": "secrets-store-id" },
  "created_at": "...",
  "last_verified_at": "..."
}

# oauth2 runtime state（primitive 自動管理）
key: oauth_state/{service_id}/{instance_name}
value: {
  "access_token": "...",   # encrypted
  "refresh_token": "...",  # encrypted
  "expires_at": 1234567890
}
```

**secretRef 格式**：`{service_id}/{instance_name}`，例如 `notion/prod`、`google_calendar/workspace-a`。
一個 tenant 可以同一個服務存多個 instance（多帳號場景）。

### 8.3 KBDB 整合（可選，但建議）

**按照 KBDB 架構，recipe metadata 可以用 Block + Template 表達**（不是 credential 本體，只是 metadata）：

建立一個 `service_recipe` Template：
```json
{
  "name": "service_recipe",
  "display_name": "服務 Recipe Metadata",
  "schema": {
    "fields": [
      {"key": "service_id", "type": "text", "required": true, "description": "服務識別"},
      {"key": "primitive", "type": "text", "required": true, "description": "使用的 primitive"},
      {"key": "version", "type": "number", "required": true, "description": "Recipe 版本"},
      {"key": "display_name", "type": "text", "required": false, "description": "顯示名稱"},
      {"key": "docs_url", "type": "text", "required": false, "description": "文件 URL"},
      {"key": "kv_key", "type": "text", "required": true, "description": "KV 實際存取 key"}
    ]
  }
}
```

Secret **不**進 KBDB（KBDB 不該存敏感資料），只有 metadata 在 KBDB 裡方便搜尋和關聯。

---

## 9. 首次授權 UI Flow（給人類看的部分）

這是「學員不知道該選哪個 credential 的痛點」的終結方案。

### 9.1 Static Key 的 UI

```
┌──────────────────────────────────────────┐
│ 連接 Notion                              │
├──────────────────────────────────────────┤
│                                          │
│ Internal Integration Token                │
│ ┌────────────────────────────────────┐  │
│ │ secret_•••••••••••••               │  │
│ └────────────────────────────────────┘  │
│ ↳ 如何取得？→ 開啟 Notion 整合設定頁  │
│                                          │
│ [ 測試連線 ]  [ 儲存 ]                   │
└──────────────────────────────────────────┘
```

**零選項。** UI 從 recipe 的 `required_secrets` 動態生成。使用者不用選「這是 Header Auth 還是 Query Auth 還是 Custom Auth」——那是 recipe 的事，不是使用者的事。

### 9.2 OAuth2 的 UI

```
┌──────────────────────────────────────────┐
│ 連接 GitHub                              │
├──────────────────────────────────────────┤
│                                          │
│  [ 🔗 使用 GitHub 帳號登入 ]             │
│                                          │
│  將跳轉到 GitHub，授權後自動返回          │
│                                          │
└──────────────────────────────────────────┘
```

**一個按鈕。** Client ID / Secret 由 arcrun 平台統一管理（OAuth App 註冊在 arcrun 這邊），使用者看不到也不用知道。

### 9.3 Service Account 的 UI

```
┌──────────────────────────────────────────┐
│ 連接 Google Calendar                     │
├──────────────────────────────────────────┤
│                                          │
│ Service Account JSON                     │
│ ┌────────────────────────────────────┐  │
│ │ 將整份 JSON 貼到這裡               │  │
│ │                                    │  │
│ │ {                                  │  │
│ │   "type": "service_account",       │  │
│ │   "project_id": "...",             │  │
│ │   ...                              │  │
│ │ }                                  │  │
│ └────────────────────────────────────┘  │
│                                          │
│ 如何取得？→ 展開步驟說明 ▼               │
│ 1. 打開 GCP Console                     │
│ 2. IAM → Service Accounts                │
│ 3. 建立 Service Account                  │
│ 4. Keys → Add Key → JSON                │
│ 5. 下載後整份貼到上方                    │
│                                          │
│ [ 測試連線 ]  [ 儲存 ]                   │
└──────────────────────────────────────────┘
```

**貼 JSON + 按鈕**。不用寫任何程式碼，不用 debug 兩天。

---

## 10. 實作任務分解（CC 的 TODO list）

### Phase 1：核心骨架（1-2 週）

- [ ] **T1.1** Recipe YAML schema 定義 + JSON Schema validator（放 `arcrun/schemas/recipe.schema.json`）
- [ ] **T1.2** Recipe loader：從 `recipes/` 目錄讀 YAML → validate → 轉 JSON 存入 KV namespace `arcrun-recipes`
- [ ] **T1.3** TinyGo WASM 專案骨架（`arcrun/primitives/`），四個子目錄，統一 interface
- [ ] **T1.4** Worker runtime 的 WASM loader + host function（網路、KV 讀寫）
- [ ] **T1.5** `AuthBroker` TypeScript 類別骨架 + unit test

### Phase 2：Static Key（1 週）

- [ ] **T2.1** `static_key.wasm` 實作（header/query/body/basic_auth 四種注入）
- [ ] **T2.2** 寫三個 recipe：`notion.yaml`, `openai.yaml`, `stripe.yaml`
- [ ] **T2.3** Tenant KV secret 加密寫入 + `AuthBroker.bind()` 整合
- [ ] **T2.4** `recipe.test` 執行器（驗證 credential 有效性）
- [ ] **T2.5** E2E test：存 secret → bind → fetch Notion API → assert

### Phase 3：OAuth2（1-2 週）

- [ ] **T3.1** `oauth2.wasm` 實作（authorization_code + client_credentials + pkce）
- [ ] **T3.2** OAuth callback endpoint（統一 URL，用 state 路由到正確 tenant/recipe）
- [ ] **T3.3** Refresh token 自動續命邏輯（rate-limit 保護：同一 token 不能 1 秒內 refresh 多次）
- [ ] **T3.4** 寫三個 recipe：`github.yaml`, `slack.yaml`, `google_oauth_user.yaml`
- [ ] **T3.5** UI flow：startAuth → 跳轉 → callback → 寫 secret

### Phase 4：Service Account（1 週）

- [ ] **T4.1** `service_account.wasm` 實作（google_jwt）
- [ ] **T4.2** Google JWT signing（ES256 / RS256）— **這個 TinyGo 需要注意 crypto 支援**
- [ ] **T4.3** AWS SigV4 簽章實作（kind: aws_sigv4）
- [ ] **T4.4** Recipe 繼承機制（`extends` 支援）
- [ ] **T4.5** 寫 recipes：`_google_base`, `google_calendar`, `google_drive`, `gmail`, `aws_s3`

### Phase 5：mTLS + 收尾（1 週）

- [ ] **T5.1** `mtls.wasm` 實作（對接 Cloudflare `mTLSCertificate` binding）
- [ ] **T5.2** Cloudflare Secrets Store 整合（高敏感 secret 用）
- [ ] **T5.3** Recipe marketplace UI（列出可用 recipe，搜尋，一鍵設定）
- [ ] **T5.4** Observability：每次 bind / refresh / test 記錄到 KBDB（metadata，不含 secret）
- [ ] **T5.5** Docs：recipe 撰寫指南（讓社群能貢獻）

### Phase 6：Recipe 生成器（選配，1 週）

- [ ] **T6.1** 給 Claude 一份 API doc，自動產 recipe YAML 草稿 + 人類 review 介面
- [ ] **T6.2** 從 OpenAPI spec 自動推論 recipe
- [ ] **T6.3** 從 n8n credential file 反向轉譯（擷取 400+ 現成整合）

---

## 11. 關鍵技術風險與對策

| 風險 | 對策 |
|---|---|
| **TinyGo 的 crypto 支援不完整**（ES256 / RS256 JWT 簽章） | 先用 `crypto/rsa` + `crypto/ecdsa` 確認 TinyGo 版本支援；若不行，fallback 用 Worker runtime 的 `crypto.subtle` 實作這部分，WASM 透過 host function 呼叫 |
| **Recipe 被惡意提交**（如 inject 內含 `https://evil.com` 當 token_url） | Recipe 走 PR review + CI 自動檢查 URL 白名單；社群貢獻的 recipe 預設隔離在 `community/` 目錄，使用者明確選擇才啟用 |
| **OAuth state CSRF** | state 用 `crypto.randomUUID()` + 5 分鐘 TTL，存在 KV，callback 時比對 |
| **Secret 在 Worker log 外洩** | `AuthContext.Secret` 禁止 `toString` / `JSON.stringify`，用 Proxy 攔截；log 層強制 redact |
| **Token refresh 風暴**（100 個並發 request 同時發現過期） | 用 Durable Object 單執行緒化每個 secret 的 refresh，其他 request 等結果 |
| **TinyGo WASM bundle size** | 四個 primitive 分開編譯，最大 500KB/個；lazy load |
| **Recipe 版本升級破壞相容** | `version` 欄位 semver，tenant secret 記錄 `recipe_version`，primitive 內處理遷移 |

---

## 12. 對比 n8n（給內部 review / 行銷用）

| 維度 | n8n | arcrun |
|---|---|---|
| Credential types 數量 | 400+（一個服務一個） | 4（primitive） + N recipe |
| 新增一個服務 | 寫 TypeScript class + rebuild + npm publish | 寫一份 YAML + PR merge |
| AI agent 使用 | 需要讀 node 文件 + 猜參數 | 讀 recipe YAML 即可 |
| 使用者首次設定 | 從 400+ 選項選一個（常選錯） | 搜尋服務名，只問必要 secret |
| OAuth App 管理 | 使用者自己註冊 OAuth app | arcrun 平台統一管理（使用者只需點「授權」） |
| 社群貢獻成本 | 高（TS + 編譯 + 測試） | 低（YAML + 測試） |

---

## 13. 接下來的決策點（需要 richblack 確認）

- [ ] **Recipe 版本管理策略**：採用 semver？每個 recipe 獨立版本？還是整個 recipe set 一個版本？
- [ ] **OAuth App 註冊**：arcrun 平台要統一註冊幾個主流服務的 OAuth App（GitHub、Google、Slack、Microsoft）？還是讓 tenant 自己帶 client_id/secret？
  - 建議：**雙模式**——平台模式（方便）+ BYO 模式（企業客戶用自己的 OAuth app 有稽核好處）
- [ ] **Recipe registry 的審核流程**：完全開放 PR 還是僅核心團隊維護？
  - 建議：`recipes/official/`（核心維護）+ `recipes/community/`（PR 審核後 merge，使用者需明確啟用）
- [ ] **Secret rotation 政策**：要不要內建提醒 / 自動 rotate？（Phase 7+）

---

## 14. 附錄：完整範例

### A. 最小可行 recipe（OpenAI）

```yaml
service: openai
version: 1
primitive: static_key
display_name: OpenAI
base_url: https://api.openai.com/v1
required_secrets:
  - key: api_key
    label: API Key
    help_url: https://platform.openai.com/api-keys
inject:
  header:
    Authorization: "Bearer {{secret.api_key}}"
test:
  method: GET
  path: /models
  expect_status: 200
tags: [ai]
```

### B. OAuth2 recipe（Slack）

```yaml
service: slack
version: 1
primitive: oauth2
display_name: Slack
base_url: https://slack.com/api
grant: authorization_code
oauth:
  authorize_url: https://slack.com/oauth/v2/authorize
  token_url: https://slack.com/api/oauth.v2.access
  scopes:
    - chat:write
    - channels:read
  refresh: true
  client_auth: header
inject:
  header:
    Authorization: "Bearer {{runtime.access_token}}"
test:
  method: POST
  path: /auth.test
  expect_json:
    ok: true
tags: [communication]
```

### C. Service Account recipe（Google Calendar）

```yaml
extends: _google_base
service: google_calendar
version: 1
display_name: Google Calendar
base_url: https://www.googleapis.com/calendar/v3
token_exchange:
  scopes:
    - https://www.googleapis.com/auth/calendar
test:
  method: GET
  path: /users/me/calendarList
  expect_status: 200
tags: [calendar, google]
```

---

## 15. 給 CC 的行動指引

1. **先不要動既有 arcrun 的 credential 相關 code**，保持現狀到 Phase 2 完成再切換。
2. **Phase 1 + 2 是 MVP**，做完可以接 80% 服務（API key 類）。
3. **遇到 TinyGo 的技術阻礙（特別是 crypto），立刻回報**，不要自己 workaround 兩天。
4. 每個 Phase 完成後寫一份 brief report（能跑什麼、不能跑什麼、下一步）。
5. Recipe 撰寫先做 3-5 個手工範例，**確認 schema 夠用再開始批量生成**。