# 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 : query: # URL query string : body: # request body(JSON 欄位) : basic_auth: # HTTP Basic Auth(會自動 base64 編碼) username: password: ``` **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: oauth: authorize_url: token_url: scopes: - - 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: 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: 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 // 首次授權(僅 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 // 管理 listRecipes(): Promise getRecipe(serviceId: string): Promise } interface AuthenticatedClient { fetch(path: string, init?: RequestInit): Promise } ``` **使用範例(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: 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": | { "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 夠用再開始批量生成**。