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:
@@ -0,0 +1,761 @@
|
||||
# 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 夠用再開始批量生成**。
|
||||
Reference in New Issue
Block a user