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,274 @@
|
||||
# Auth Recipe System — SDD
|
||||
|
||||
> 文件類型:SDD(Software Design Document)
|
||||
> 建立:2026-04-19
|
||||
> 狀態:實作中
|
||||
|
||||
---
|
||||
|
||||
## 一、目標
|
||||
|
||||
封測前完成,讓封測者碰到「我要連 X 服務」都有辦法,而不是「還沒做」。
|
||||
|
||||
**精神**:`http_request` 是容器零件,auth recipe 是「如何對這個服務認證」的設定層,兩者分離。新增一個服務 = 寫一份 YAML,不需要改程式碼、不需要重新部署 Worker。
|
||||
|
||||
---
|
||||
|
||||
## 二、三層模型
|
||||
|
||||
```
|
||||
Layer 3: Auth Recipe (YAML/JSON in RECIPES KV)
|
||||
公共,描述「如何對某服務認證」
|
||||
key: auth_recipe:{service}
|
||||
例: auth_recipe:notion, auth_recipe:slack
|
||||
↓ 引用
|
||||
Layer 2: Auth Primitive (TypeScript in Worker)
|
||||
四個通用認證邏輯:static_key | oauth2 | service_account | mtls
|
||||
封測只做 static_key 和 service_account (Google JWT)
|
||||
↑ 使用
|
||||
Layer 1: Tenant Secret (CREDENTIALS_KV)
|
||||
每個 tenant 自己的加密 credential
|
||||
key: {api_key}:cred:{name}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 三、Auth Recipe Schema
|
||||
|
||||
```typescript
|
||||
interface AuthRecipeDefinition {
|
||||
kind: 'auth_recipe'; // 區別 RecipeDefinition 用
|
||||
service: string; // canonical_id, e.g. "notion"
|
||||
version: number;
|
||||
primitive: 'static_key' | 'oauth2' | 'service_account' | 'mtls';
|
||||
base_url: string;
|
||||
display_name?: string;
|
||||
description?: string;
|
||||
|
||||
// service_account 用
|
||||
service_account_kind?: 'google_jwt';
|
||||
token_exchange?: {
|
||||
endpoint: string; // e.g. https://oauth2.googleapis.com/token
|
||||
scopes: string[];
|
||||
};
|
||||
|
||||
required_secrets: Array<{
|
||||
key: string; // CREDENTIALS_KV 的名稱
|
||||
label: string; // UI/CLI 顯示
|
||||
type?: 'string' | 'json_blob'; // default: string
|
||||
help?: string;
|
||||
help_url?: string;
|
||||
}>;
|
||||
|
||||
inject: {
|
||||
header?: Record<string, string>; // "Authorization": "Bearer {{secret.token}}"
|
||||
query?: Record<string, string>;
|
||||
body?: Record<string, string>;
|
||||
// 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<string, string>;
|
||||
};
|
||||
|
||||
created_at: number;
|
||||
updated_at: number;
|
||||
}
|
||||
```
|
||||
|
||||
**Template 語法**:
|
||||
- `{{secret.KEY}}` → 從 tenant 的 CREDENTIALS_KV 解密取值
|
||||
- `{{runtime.access_token}}` → service_account JWT exchange 後取得的短期 token
|
||||
|
||||
---
|
||||
|
||||
## 四、KV 儲存
|
||||
|
||||
沿用現有 `RECIPES` KV namespace,不新增 binding。
|
||||
|
||||
```
|
||||
auth_recipe:{service} → AuthRecipeDefinition JSON
|
||||
```
|
||||
|
||||
與現有 `recipe:{id}` / `idx:{hash}` 的 key 不衝突。
|
||||
|
||||
---
|
||||
|
||||
## 五、執行流程
|
||||
|
||||
### 5.1 static_key(涵蓋 ~80% 服務)
|
||||
|
||||
```
|
||||
trigger → graph-executor
|
||||
→ injectCredentials(componentId, input, env, apiKey)
|
||||
→ resolveAuthRecipe("notion", RECIPES KV)
|
||||
→ 取得 required_secrets: [{key: "notion_token", ...}]
|
||||
→ 從 CREDENTIALS_KV 讀 "{api_key}:cred:notion_token"
|
||||
→ AES-GCM 解密
|
||||
→ 展開 inject.header templates ({{secret.notion_token}} → 實際值)
|
||||
→ 注入 _auth_headers, _auth_query, _auth_body 到 input
|
||||
→ makeAuthRecipeRunner(recipe)
|
||||
→ 合併 _auth_headers 到 fetch headers
|
||||
→ 呼叫 recipe.base_url + input._path
|
||||
→ 回傳結果
|
||||
```
|
||||
|
||||
### 5.2 service_account(Google 家族)
|
||||
|
||||
```
|
||||
injectCredentials
|
||||
→ resolveAuthRecipe("google_sheets_sa", RECIPES KV)
|
||||
→ 解密 service_account_json (JSON blob)
|
||||
→ signGoogleJwt(serviceAccountJson, scopes) via crypto.subtle (RSASSA-PKCS1-v1_5 + SHA-256)
|
||||
→ POST token_exchange.endpoint → 取得 access_token
|
||||
→ 展開 inject.header: { Authorization: "Bearer {{runtime.access_token}}" }
|
||||
→ 注入 _auth_headers
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 六、Context key 慣例
|
||||
|
||||
注入後的認證資訊以 `_auth_` 前綴攜帶,不污染業務欄位:
|
||||
|
||||
| Key | 說明 |
|
||||
|---|---|
|
||||
| `_auth_headers` | `Record<string, string>` — 要合併進 fetch headers |
|
||||
| `_auth_query` | `Record<string, string>` — 要附加到 URL query string |
|
||||
| `_auth_body` | `Record<string, string>` — 要合併進 request body |
|
||||
| `_auth_path` | `Record<string, string>` — 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 邏輯
|
||||
Reference in New Issue
Block a user