Files
Arcrun/system-dev/docs/3-specs/archive/credential-primitives-wasm/credential-store-redesign.md
T
Leo 8d8b01d240 docs(sdd): credential-primitives-wasm 封存進 archive(T10 完成,卷已結案)
leo 2026-07-21 明令封存:卷已完成,留主目錄會讓未來 session 誤以為進行中。
最後一項 T10(廢除自管加密金鑰)已於 20c7610 完成(移除約 2500 行)。

- git mv 整卷 → system-dev/docs/3-specs/archive/credential-primitives-wasm/
  design.md status: closed;superseded_by 留空(據實:非被另一卷取代,是機制整個換掉)
- 卷首補「封存時仍未完成的項目」——逐條查 code 核實,不當作完成:
  真缺口=auth_mtls 從未實作、7.6 self-hosted auth 鏈端到端從未驗;
  另有勾選過期(auth_oauth2 其實已完成)與驗收條件已作廢(與現行 rule 07 牴觸)者
- 修好 14 處引用(原盤點 10 處,實際更多):session-start-load-sdd.sh 內容嚴重過期
  (把已完成 Phase 寫成進行中)→ 改為以 frontmatter 為判準;四份 rules、CLAUDE.md、
  BACKLOG、3-specs/README、deploy.ts、credentials.ts、wrangler.toml 逐一按性質處理
- 額外:system-dev/docs/2-architecture/ 有四個規則檔重複副本(06-08 遷移遺留)
  → 同步修好,否則留一份過期真相(今日第三次撞到「同一資訊兩份副本」的債)
- 02-forbidden.md §2.1 列的三個「待刪違規 TS」其實早已不存在
  → 改標刪除線+禁止重新引入(過期文件同時製造假待辦與假進行中)

驗證:sdd-active-check exit 0(active 仍恰一份=portal-auth);
session-start-load-sdd.sh exit 0;cypher-executor 與 cli typecheck 全綠;
測試 187/188(唯一 fail 為 pre-existing,stash 覆驗相同)。

未動:pre-write-guard.sh 的 KNOWN_SDDS 白名單不含 archive 路徑
(放寬 guardrail 需明確授權,留待決定)。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-21 01:57:34 +08:00

20 KiB
Raw Blame History

Credential Store 重設計提案(A telegram 一致性 + B KV→D1 + C 友善前門)

⚠️ 歷史記錄:本檔的儲存決策已被 credential-store-migration.md 取代並執行完畢 (2026-07-20)。文中提及的舊自管金鑰機制已不存在,僅供考古,勿依此操作。

建立:2026-06-29 by arcrun CC|更新:2026-06-29(依 leo 最終精確 spec 改寫)|對應 issueArcrun#13 範圍宣告:本檔是既有 SDD credential-primitives-wasm/ 的補充設計筆記(不是新 SDD 子系統,rule 02 §4.3 例外)。 詞彙(leo 堅持精確):零件/component=TinyGo WASMrecipe=http_request+固定設定(打最終 APIworkflow=多步 YAML(在這層「按名指定要哪個 credential」)auth-recipe=credential 怎麼注入


0. leo 的最終 spec(一句話):不是拆除,是把前門做友善 + 把 D1 收尾

保留全部既有零件,只改「credential 怎麼餵進去」,不改 recipe 本身。

人填 .env / 環境變數                  ← 友善前門,任何人都會(本機=.env;雲端=code-on-web 的 Environment 設定,對稱)
     ↓  acr creds push 保留,但「值的來源」= .env/env-var(不再強迫手寫 credentials.yaml
長久存「我的 D1」                      ← 存一次,存進使用者自己的 D1;之後自動,永不重推
     ↓  workflow YAML「按名」指定要哪個 credential"我要 google 那把")→ 自動從 D1 取
recipe 打最終 API                     ← telegram_send / notion / gsheets,保留,不刪

對稱性:本機讀 .env;雲端讀 code-on-web 的 Environment 設定。同機制,只是來源位置不同。

明確「保留、不可刪」

  1. acr creds push 保留 —— 手動推單一 credential 仍合法(只是不再是唯一/被迫的路徑)。
  2. 最終 API recipe 保留 —— telegram_send / notion / gsheetshttp_request + 固定設定)是打 API 的端點,不刪。改的是「credential 怎麼餵」,不是 recipe。

0.5 Q1 — list 是動態讀 store 還是 hardcoded?(已核實 code2026-06-29

結論:recipe/auth-recipe 的「正規清單」是動態的;但 acr parts 是 hardcoded 且把 5 個 recipe 混進去 → 那 5 個之外的 pushed recipe 在 acr parts 看不到。

指令 讀哪裡 動態? pushed recipe 會出現?
acr recipe push POST /recipes → KV storerecipe.ts:87 寫入 store
acr recipe list GET /recipesRECIPES.list({prefix:'recipe:'})recipe.ts:182 / recipes.ts:260 動態
acr auth-recipe list GET /auth-recipesRECIPES.list({prefix:'auth_recipe:'})auth-recipe.ts:42 / recipes.ts:547 動態
acr parts hardcoded BUILTIN_COMPONENTS 陣列(parts.ts:28-262),完全不 fetch store 靜態 不會
  • component(零件=WASMhardcoded 是對的:零件只能 PR merge 新增(mindset §4 人類閘門),固定、慢增,靜態清單反映真實。
  • acr parts 把 5 個 recipe 也 hardcode 進去parts.ts:197-261gmail_send / google_sheets_append / telegram_send / line_notify_send / notion)——這是 component 與 recipe 的 conflation。recipe 是動態投稿的(任何人 acr recipe push),卻被釘進 acr parts 的靜態清單。
  • 後果(leo 的擔憂成立,但範圍精確):用戶 acr recipe push my_recipe → 進 store → acr recipe list / acr recipe search 看得到;但 acr parts 永遠看不到(除非改 source 重發 CLI)。「submitted = invisible」只發生在 acr parts 這個面,不是整個生態壞掉。
  • 與 telegram bug 的關係telegram bug 是另一條軸(seed source vs live KV 漂移,A 已修),但與此同病=「hardcoded 清單 ≠ live store」。acr parts 列退役/釘選 recipe(#13)也是這病的症狀。

Q1 修法 scope(建議,未在本批改 code)

  1. acr parts 移除 5 個 recipe 的 hardcodeparts.ts:197-261),只留真零件logic/data/ai/http_request/cron…=WASMPR-only,靜態正確)。
  2. acr parts 末尾改成「指路」:列完真零件後,動態提示「整合類服務 → acr auth-recipe list(即時讀 store);API recipe → acr recipe list(即時讀 store)」。讓 AI/人從 acr parts 被導到動態清單,不在靜態清單找 recipe。
    • 進階(可選):acr parts 末段直接 fetch GET /recipes + GET /auth-recipes 把 store 的 recipe 也列出來(標明「動態,來自 store」),徹底消除「submitted = invisible」。
  3. 明確分流原則寫進 parts.ts 註解:零件=靜態(PR-only);recipe/auth-recipe=動態(從 store 讀)。不再把 recipe 釘進零件清單。
  4. 這 scope #13「acr parts 殘留」的根治版(#13 只把 recipe 描述改對,沒解決「recipe 不該 hardcode 在 parts」的結構問題)。屬薄殼修正(rule 07)=把「列 recipe」這能力從介面靜態資料改成讀 API store,不違禁令。

1. 現狀(已核實 code + 實測 live2026-06-29

1.1 儲存:KV,不是 D1(B 的根因=未完成的遷移,非「設計即如此」)

  • credential 只存 CREDENTIALS_KVkey={api_key}:cred:{name}AES-GCM。
    • 寫:acr creds pushPOST /credentialscypher-executor/src/routes/credentials.ts:48)。
    • 讀+解密:auth_static_key WASM kv_getregistry/components/auth_static_key/main.go:146)。
    • bindingcypher-executor/wrangler.toml:23;型別 src/types.ts:31
  • 全 repo 無任何 credential 的 D1D1 引用全是 KBDB graph)。
  • leo 確認:採用 KBDB 後使用者已有自己的 D1,當時就要求 credential 也搬進「我的 D1」長久存 → 此遷移從未完成。 → BKV→D1確定要做,是 leo 明確要的「長久存我的 D1」層。

1.2 .env → credential值不會自動進C 的痛點,但前門不友善 ≠ 拆掉)

  • .env 目前只供「設定」:ENCRYPTION_KEY/NAMESPACE/ARCRUN_*/CLOUDFLARE_*cli/src/lib/config.ts:125)。
  • credential 的只來自手寫 credentials.yamlacr creds pushcli/src/commands/creds.ts)。
  • leo 記憶「notion/gsheets 只靠 .env 就通」與 code 不符——實際是早先 push 過、值已在 KV。
  • 痛點(北極星「拿 credential ≠ 攻打用戶」):每次要手寫 yaml;不像 n8n Credentials tab 填一次長存。 → C 不是發明「自動偵測魔法」,而是把 acr creds push值來源從「手寫 yaml」改成「讀 .env/env-var」,存進 D1,之後 workflow 按名取。

1.3 telegram 漂移(A)——已實測 smoking gun,已修 source

  • recipe telegram_sendapi-recipe-seeds.ts:108auth_service:'telegram'endpoint .../bot{{auth.bot_token}}/sendMessage
  • auth-recipe-seeds.ts source 只有 23 個 service,缺 telegram/line_notify/kbdb/google_user
  • 實測:prod = 27(手動 seed 過 telegram);self-host leo21c = 23,無 telegram → telegram_send 找不到 auth recipe → {{auth.bot_token}} 注入空 → URL 變 .../bot/sendMessage → 壞。notion/gsheets 通是因它們在那 23 裡。
  • 這是「source vs live 漂移=假綠」。telegram 不是走 legacy BUILTIN_CREDENTIALS_MAP,正確形態一直是 recipe+auth-recipe,只是 auth-recipe 沒進 source 種子。

2. 目標狀態(target,=leo 的 3 層流,全部保留只連得更友善)

  1. 唯一打外部 API 的方式 recipe(保留)。telegram 與 notion/gsheets 同一條「workflow 按名指定 → 從 D1 取 → auth-recipe 注入 → recipe 打 API」鏈,無例外(A,本次已修 source)。
  2. credential 長久存使用者自己的 D1(如 n8n Credentials tab):填一次、長存、workflow 按名引用、永不重推(B)。
  3. 友善前門:使用者把 token 填進 .env(本機)/ code-on-web Environment(雲端)一次acr creds push(保留)以此為值來源,client 端加密後存進 D1。不再被迫手寫 credentials.yamlC)。

3. 遷移步驟(A 已做;B/C 是設計變更,誠實標工作量)

Phase A — telegram/line_notify/kbdb 一致性(本次已做,commit 90777e3,未 push/merge

  • auth-recipe-seeds.ts 補 telegraminject.path bot_token/ line_notifyheader Bearer/ kbdbheader Bearer),形態取自 prod。
  • recipes.ts AuthInjectSpecpath?WASM/SDD §六早有、source 介面漏 → tsc 擋)。
  • google_useroauth2)暫不回灌:內嵌 client_secret 不可進 git + 介面無 oauth2 欄 → 留 Phase D。
  • 部署後 leo21c 跑 acr update(/init/seed) → 23→26telegram 即與 notion 同鏈可用。
  • 不刪任何 recipe;不碰 acr creds push 純補種子 + 補型別欄位。

Phase B —(已翻案,見 §4.5)儲存:KV 維持,不搬 D1;高敏感可選補 Secrets Store

早先版本寫「KV→D1 遷移」。§4.5 核實史實 + leo 自己 2026-06-07 拍板(credential 留 KV)後翻案 credentialRedis-class(按 key 直接點查、密文不可 SQL 查),搬 D1 是淨負債。B 改為:

  1. 低敏感 credential 維持 KV(現狀 CREDENTIALS_KVAES-GCM)。acr creds list 已可用(GET /credentials KV.list)。
  2. (可選、後續)高敏感(service account JSON/private key)→ Cloudflare Secrets Storecredential_parts.md §8.2KV 只存 ref)。唯一值得的儲存升級,與拍板不衝突。
  3. 不新增 D1 credentials 表、不新增 cred_get host function、不做 KV→D1 migration——這些原計畫取消(好處 KV 已有)。
  4. 若 leo 仍要 D1:屬新需求(須更新 2026-06-07 拍板)。即便如此,密文本體仍留 KV/Secrets StoreD1 只放非機密 metadata。
  5. 風險:跨 TS Worker / WASM / host function 三層,必端到端實測(防再假綠,mindset §7)。非小工程。

Phase C — 友善前門:acr creds push 值來源=.env/env-var → D1(小~中工程,依附 B)

精確(leo 修正):不是「自動 pickup 魔法」,是「acr creds push 保留,但值的來源改成 .env/env-var,存進 D1,之後 workflow 按名取」。

  1. acr creds push(保留)新增「從 .env/env-var 讀值」模式:掃 .env 中「已知 credential 名」 (由 auth-recipe 的 required_secrets[].key 反推,例 notion_token/telegram_bot_token)→ client 端加密 → 存進 D1Phase B 的 store)。
    • 仍保留現有「從 credentials.yaml 讀值」模式(向後相容,不刪)。yaml 變成可選輸入源之一,不再是被迫。
  2. 對稱:本機讀 .envconfig.ts loadDotEnvOnce 已會載入 .env 進 process.env); 雲端=code-on-web 的 Environment 設定(同樣落到 env-var)。同一套讀法、不同來源位置。
  3. 名稱規約:.env 用大寫(NOTION_TOKEN)→ normalize 成 credential 名 notion_token(=auth-recipe required_secrets.key)。
  4. 薄殼界線rule 07):這是「擴 acr creds push 的輸入源」=介面慣例的輸入解析(§2 允許第 1 項), 不是在 CLI 拼裝 API 缺的能力(值來源是 .env/yaml 都只是「轉成 API 期望的加密 payload」)。加密仍 client 端(§2 第 4 項唯一例外)。store 仍是 API(D1)。
  5. 結果:填一次 .env → acr creds push(或 deploy 末尾自動觸發一次)→ 長存 D1 → workflow 按名引用、零重複推。

Phase D(後續,非本批)— google_user(oauth2) 回灌 + oauth2 secret 部署期注入

  • AuthRecipeDefinition 介面補 oauth2 欄位;client_secret 走 wrangler secret/env 注入(不進 git),再回灌 source。

4. 正解:telegram 的 workflow / recipe yaml(給 mira 抄)

telegram 發訊=recipe telegram_send(不是 component、不是自建零件);credential 在 workflow 層按名指定

name: notify_done
flow:
  - "notify >> ON_SUCCESS >> end"   # 單節點時 flow 可省

config:
  notify:
    component: telegram_send         # ← 內建 recipe canonical_id(不是 component:telegram
    chat_id: "123456789"
    text: "✅ 任務完成"
    # bot_token 不寫這裡。recipe 的 auth_service='telegram' → 按名從 D1 取 telegram_bot_token
    #   → auth-recipe:telegram 的 inject.path 注入進 URL path。一致鏈,與 notion/gsheets 同型。

credential 解析(target 設計):

  1. 使用者把 TELEGRAM_BOT_TOKEN=123456:ABC... 填進 .env(本機)/ code-on-web Environment(雲端)一次
  2. acr creds push(保留;值來源=.env)→ client 加密 → 存進「我的 D1」credential store。之後永不重推。
  3. 執行時:cypher-executor 見 telegram_send.auth_service='telegram'auth_static_key WASM 用 cred_get 從 D1 取 telegram_bot_token 密文 → crypto_decrypt → 依 auth_recipe:telegraminject.path.bot_token 輸出 auth_pathmakeRecipeRunner 把 endpoint {{auth.bot_token}} 換成真 token → POST https://api.telegram.org/bot<token>/sendMessagebody={chat_id,text}

這條鏈與 notionheader 注入)/gsheetsservice_account runtime token 注入)完全同型,只注入位置不同(path vs header)。

過渡期(B/C 未完成前,今天就能用的方式):在 credentials.yamltelegram_bot_token + acr creds push(值進 KV)。 上面 yaml 的 workflow 寫法不變——只是 store 暫時是 KV、值來源暫時是手寫 yaml。B/C 完成後 store→D1、來源→.envworkflow yaml 一字不改(按名引用穩定)。


4.5 Q2 — KV vs D1:史實核實 + 三方調和 + CC 推薦(leo 授權我決定)

史實(git 核實,2026-06-29):本 repo 的 credential 從來沒在 D1

  • 最早 MVP commit 2707fcacredential 即「AES-GCM decrypt from CREDENTIALS_KV」。
  • 更早的 u6u-credentials ancestor workercredentials/ 已刪):CREDENTIALS_KV.put('cred:${id}')——也是 KV。 (它的 CredentialRecord 已有 name/type/created_at 結構欄位,但以 JSON 存進 KV。)
  • 全 history 的 CREATE TABLE 只有 KBDBentries/templates/entry_values),從無 credentials 表
  • 找不到任何「credential D1→KV 撤退」的 commit。leo 記憶的「原本 D1 有 credentials 表、後來退 KV」 在本 repo 史實裡不存在(可能是 pre-repo 設計或別處,沒落地過 code)。

三方說法(全部攤開,none 寫 D1 存 credential

來源 對 credential 儲存的說法
leo 口頭(現在) D1(「長久存我的 D1」)
現行 code KVCREDENTIALS_KVMVP 至今未變)
kbdb-base SDD 拍板 2026-06-07leo 親簽「Leo:同意」) credential 留 KV(「session、verdict、credential 留 KV=短期高頻純取用,類比 Redisworkflow/recipe/成功記錄才進 D1=要層級/列舉/排序」)。kbdb-base/design.md:318tasks.md:11tasks.md:83 三處一致。
credential_parts.md §8.2(長期需求源) tenant KV + Cloudflare Secrets Store(低敏感→KV AES-GCM;高敏感如 service account JSON/private key→Secrets StoreKV 只存 ref)。不是 D1。

leo 口頭「D1」與他自己 2026-06-07 的書面拍板(credential 留 KV)直接衝突,且兩份書面需求源都沒說 D1。 SDD 協議(00-sdd-protocol「規範互相矛盾→引原文、不自行猜」)要求攤開矛盾;但 leo 此次明確授權「你決定並推薦」,故下方給推薦(grounded in 上述證據),同時把矛盾標清楚讓 leo 有機會推翻。

「退 D1→KV 的理由還成立嗎?」(leo 的核心提問)

  • 本 repo 沒發生過該退守,故無「當初理由」可考。但 leo 假設的理由(「self-hosted 少依賴、免 D1」)今天確實已 moot—— self-hosted 用戶現在本來就有一顆 KBDB D1(kbdb/wrangler.toml:10 [[d1_databases]]deploy.ts 注入用戶自己的 id)。
  • 但「能用 D1」不等於「credential 該用 D1」。 2026-06-07 拍板把 KV 留給 credential 的理由不是「沒有 D1」, 而是**「credential =短期高頻純取用(by api_key+name 直接點查),是 Redis-class 工作負載,不需要 D1 的層級/列舉/排序」。 這個理由與有沒有 D1 無關,今天仍成立**。

CC 推薦(leo 授權我決定):credential 主存維持 KV;不為「列表/按名引用」而搬 D1

理由(誠實 trade-offmindset §7):

  1. 守 leo 自己的拍板2026-06-07「credential 留 KV」是有理由的設計(Redis vs PostgreSQL 分工),非偷懶。推翻它要有新理由,目前沒有。
  2. D1 宣稱的好處在 KV 上已可達或不需要
    • acr creds list」KV 已能做(GET /credentialsKV.list({prefix}) 列名,credentials.ts:68-78 已實作,今天就有)。
    • 「按名引用(workflow 指定要哪把 cred)」是 key 設計{api_key}:cred:{name})就給的,與 KV/D1 無關——telegram_send 的 auth_servicerequired_secrets.key 已是按名取。A 修好後就具備,不需 D1。
    • 「結構化欄位/sensitivity 分級」credential 是不可查詢的密文 blob(查詢明文=洩漏),D1 的 SQL 查詢優勢對 credential 用不上;分級需求由 credential_parts.md 的 Secrets Store(高敏感)解,不是 D1。
  3. 搬 D1 成本高且踩鐵律邊界:要新 host function cred_getWASM 改)、一次性 migration、跨 TS/WASM/host-function 三層 e2e——換來的好處 KV 已有。淨負債。
  4. 真正缺的不是 D1,是「友善前門 C」:leo 真痛點是「每 cred 手寫 yaml + 重推」,那是 C.env 當值來源 + 一次存) 解的,與 store 是 KV 還 D1 無關

推薦的目標儲存(取代本檔早先「搬 D1」設計):

  • 低敏感 credential(多數 token/api_key)→ 維持 KV(現狀,AES-GCM)。
  • 高敏感(service account JSON / private key)→ Cloudflare Secrets Store(依 credential_parts.md §8.2KV 只存 ref)——這是唯一值得做的儲存升級,且與 leo 拍板不衝突(拍板說 credential 留 KV,未禁高敏感走 Secrets Store;兩者互補)。
  • C(友善前門)照做acr creds push 值來源=.env/env-var、存一次、workflow 按名取。store 仍 KV(+Secrets Store)
  • B 從「KV→D1 遷移」改為「KV 維持 + 高敏感補 Secrets Store(可選、後續)」。telegram/notion/gsheets 的一致鏈(A)與一次填(C)完全不依賴 D1

若 leo 看完仍要 D1(例如想未來在同一顆 DB 做 cred 審計/輪替記錄):那是新需求,需更新 2026-06-07 拍板。 屆時 credential 本體仍建議 KV/Secrets Store(密文不需 SQL),D1 只放非機密的 metadatacred 名稱/服務/建立時間/last_verified),與 workflow/recipe 進 D1 同模式。不要把密文搬進 D1 當主存。


5. 與既有鐵律/SDD 的對齊

  • 不刪 recipe、不禁 acr creds pushleo spec)。A 純補種子 + 型別欄位;C 是擴 acr creds push 輸入源,非拆除。
  • 不新建 componentmindset §1 / rule 02)。
  • credential 解密仍在 WASMrule 02 §2.2);KV→D1 只是密文來源從 kv_get 換成新 host function cred_get(屬 wasi-shimrule 02 §2.3 合法)。
  • 薄殼(rule 07):acr creds push 讀 .env 仍只做「輸入解析 + client 加密 + 呼叫 API」,能力(store)在 API/D1,不在介面層。
  • 設計權威:auth-recipe.md §六(telegram path 注入 line 70-71)、§七(kbdb 共用 line 150-151); 需求源 docs/user_requirements/credential_parts.mdcredential 長期規格)。