Files
Arcrun/.agents/specs/arcrun/auth-recipe.md
T
Leo 17a076d35c feat(arcrun): Phase 2 降級假零件成 recipe + credential 鏈路修復
Phase 1(credential 注入鏈路):
- 修 auth_static_key ENCRYPTION_KEY 漂移根因(見 docs/incidents)
- component-loader: readBodyOnce() 修 "Body has already been used"

Phase 2(降級假零件成 recipe,registry/components 33→22):
- 引擎: RecipeDefinition 加 auth_service(多 recipe 共用一把 auth)
  auth-dispatcher 先查 recipe.auth_service 再 fallback componentId
- 引擎: auth_static_key inject.path + makeRecipeRunner {{auth.K}}
  (endpoint 可插 secret,解 telegram 類 URL-path token)
- 引擎: makeRecipeRunner auto-body 剔除 _ 前綴內部欄位
- 降級並刪除: kbdb_{get,create_block,patch_block,delete,ingest}
  gmail/telegram/line_notify/google_sheets(改建為 recipe)
- 刪除: ai_transform_{compile,run}(Arcrun 是 AI 呼叫的工具,
  工作流不該內嵌 AI 節點回頭呼叫 AI)
- deferred(源碼暫留): claude_api/km_writer(交 Mira 收成工作流)、
  kbdb_upsert_block(交 KBDB 出 upsert endpoint)

文件: DECISIONS.md(工作流是 default/建零件人類閘門/AI→工具)、
BACKLOG.md、auth-recipe.md §七、docs/incidents 加密 key 漂移

驗收: KBDB get/create/ingest/delete 2xx;telegram auth 注入綠;
gmail/sheets/line recipe 正確但缺 credential 未驗收;
kbdb patch 403 為 KBDB 端 bug(已交 kbdb/docs)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-29 16:18:18 +08:00

10 KiB
Raw Blame History

Auth Recipe System — SDD

文件類型:SDDSoftware 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

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 的 secret2026-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_accountGoogle 家族)

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 共用一把 auth2026-05-29 加)

RecipeDefinitionauth_service?: string 欄位:API recipe 自報它屬於哪個服務 auth-dispatcher 用它查 auth_recipe:{auth_service},而非假設 componentId == service name。

  • 讓多個 recipe 共用同一把 authrecipe: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_RECIPESgmail, 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)

  • AuthRecipeDefinition 型別 + resolveAuthRecipe
  • /auth-recipes CRUD routes
  • injectFromAuthRecipe — static_key primitive
  • lib/jwt-signer.ts — Google JWT via crypto.subtle
  • injectFromAuthRecipe — service_account primitive
  • makeAuthRecipeRunner in component-loader
  • step 5.5 in createComponentLoader
  • auth-recipe-seeds.ts (20 services)
  • seed script / deploy seeds to KV2026-04-19 全部

CLI (arcrun)

  • commands/auth-recipe.ts — list / info / scaffold
  • 更新 commands/parts.ts — scaffold fallback
  • 更新 index.ts — 註冊指令
  • 版本升 1.1.0
  • npm publisharcrun@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 實作統一 interfaceAuthenticate / NeedsRefresh / Refresh / Test)。 切換時 cypher-executorinjectFromAuthRecipe 改為呼叫對應 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 邏輯