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>
20 KiB
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 改寫)|對應 issue:Arcrun#13 範圍宣告:本檔是既有 SDD
credential-primitives-wasm/的補充設計筆記(不是新 SDD 子系統,rule 02 §4.3 例外)。 詞彙(leo 堅持精確):零件/component=TinyGo WASM、recipe=http_request+固定設定(打最終 API)、 workflow=多步 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 設定。同機制,只是來源位置不同。
明確「保留、不可刪」
acr creds push保留 —— 手動推單一 credential 仍合法(只是不再是唯一/被迫的路徑)。- 最終 API recipe 保留 —— telegram_send / notion / gsheets(http_request + 固定設定)是打 API 的端點,不刪。改的是「credential 怎麼餵」,不是 recipe。
0.5 Q1 — list 是動態讀 store 還是 hardcoded?(已核實 code,2026-06-29)
結論:recipe/auth-recipe 的「正規清單」是動態的;但 acr parts 是 hardcoded 且把 5 個 recipe 混進去 → 那 5 個之外的 pushed recipe 在 acr parts 看不到。
| 指令 | 讀哪裡 | 動態? | pushed recipe 會出現? |
|---|---|---|---|
acr recipe push |
POST /recipes → KV store(recipe.ts:87) |
— | 寫入 store |
acr recipe list |
GET /recipes → RECIPES.list({prefix:'recipe:'})(recipe.ts:182 / recipes.ts:260) |
✅ 動態 | ✅ 會 |
acr auth-recipe list |
GET /auth-recipes → RECIPES.list({prefix:'auth_recipe:'})(auth-recipe.ts:42 / recipes.ts:547) |
✅ 動態 | ✅ 會 |
acr parts |
hardcoded BUILTIN_COMPONENTS 陣列(parts.ts:28-262),完全不 fetch store |
❌ 靜態 | ❌ 不會 |
- component(零件=WASM)hardcoded 是對的:零件只能 PR merge 新增(mindset §4 人類閘門),固定、慢增,靜態清單反映真實。
- 但
acr parts把 5 個 recipe 也 hardcode 進去(parts.ts:197-261:gmail_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)
acr parts移除 5 個 recipe 的 hardcode(parts.ts:197-261),只留真零件(logic/data/ai/http_request/cron…=WASM,PR-only,靜態正確)。acr parts末尾改成「指路」:列完真零件後,動態提示「整合類服務 →acr auth-recipe list(即時讀 store);API recipe →acr recipe list(即時讀 store)」。讓 AI/人從acr parts被導到動態清單,不在靜態清單找 recipe。- 進階(可選):
acr parts末段直接 fetchGET /recipes+GET /auth-recipes把 store 的 recipe 也列出來(標明「動態,來自 store」),徹底消除「submitted = invisible」。
- 進階(可選):
- 明確分流原則寫進 parts.ts 註解:零件=靜態(PR-only);recipe/auth-recipe=動態(從 store 讀)。不再把 recipe 釘進零件清單。
- 這 scope = #13「acr parts 殘留」的根治版(#13 只把 recipe 描述改對,沒解決「recipe 不該 hardcode 在 parts」的結構問題)。屬薄殼修正(rule 07)=把「列 recipe」這能力從介面靜態資料改成讀 API store,不違禁令。
1. 現狀(已核實 code + 實測 live,2026-06-29)
1.1 儲存:KV,不是 D1(B 的根因=未完成的遷移,非「設計即如此」)
- credential 只存
CREDENTIALS_KV,key={api_key}:cred:{name},AES-GCM。- 寫:
acr creds push→POST /credentials(cypher-executor/src/routes/credentials.ts:48)。 - 讀+解密:
auth_static_keyWASMkv_get(registry/components/auth_static_key/main.go:146)。 - binding:
cypher-executor/wrangler.toml:23;型別src/types.ts:31。
- 寫:
- 全 repo 無任何 credential 的 D1(D1 引用全是 KBDB graph)。
- leo 確認:採用 KBDB 後使用者已有自己的 D1,當時就要求 credential 也搬進「我的 D1」長久存 → 此遷移從未完成。 → B(KV→D1)確定要做,是 leo 明確要的「長久存我的 D1」層。
1.2 .env → credential:值不會自動進(C 的痛點,但前門不友善 ≠ 拆掉)
.env目前只供「設定」:ENCRYPTION_KEY/NAMESPACE/ARCRUN_*/CLOUDFLARE_*(cli/src/lib/config.ts:125)。- credential 的值只來自手寫
credentials.yaml→acr creds push(cli/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_send(api-recipe-seeds.ts:108)auth_service:'telegram',endpoint.../bot{{auth.bot_token}}/sendMessage。 - 但
auth-recipe-seeds.tssource 只有 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 層流,全部保留只連得更友善)
- 唯一打外部 API 的方式= recipe(保留)。telegram 與 notion/gsheets 同一條「workflow 按名指定 → 從 D1 取 → auth-recipe 注入 → recipe 打 API」鏈,無例外(A,本次已修 source)。
- credential 長久存使用者自己的 D1(如 n8n Credentials tab):填一次、長存、workflow 按名引用、永不重推(B)。
- 友善前門:使用者把 token 填進
.env(本機)/ code-on-web Environment(雲端)一次,acr creds push(保留)以此為值來源,client 端加密後存進 D1。不再被迫手寫credentials.yaml(C)。
3. 遷移步驟(A 已做;B/C 是設計變更,誠實標工作量)
Phase A — telegram/line_notify/kbdb 一致性(本次已做,commit 90777e3,未 push/merge)
auth-recipe-seeds.ts補 telegram(inject.path bot_token)/ line_notify(header Bearer)/ kbdb(header Bearer),形態取自 prod。recipes.tsAuthInjectSpec補path?(WASM/SDD §六早有、source 介面漏 → tsc 擋)。- google_user(oauth2)暫不回灌:內嵌 client_secret 不可進 git + 介面無 oauth2 欄 → 留 Phase D。
- 部署後 leo21c 跑
acr update(/init/seed) → 23→26,telegram 即與 notion 同鏈可用。 - 不刪任何 recipe;不碰
acr creds push。 純補種子 + 補型別欄位。
Phase B —(已翻案,見 §4.5)儲存:KV 維持,不搬 D1;高敏感可選補 Secrets Store
早先版本寫「KV→D1 遷移」。§4.5 核實史實 + leo 自己 2026-06-07 拍板(credential 留 KV)後翻案: credential=Redis-class(按 key 直接點查、密文不可 SQL 查),搬 D1 是淨負債。B 改為:
- 低敏感 credential 維持 KV(現狀 CREDENTIALS_KV,AES-GCM)。
acr creds list已可用(GET /credentialsKV.list)。 - (可選、後續)高敏感(service account JSON/private key)→ Cloudflare Secrets Store(credential_parts.md §8.2,KV 只存 ref)。唯一值得的儲存升級,與拍板不衝突。
- 不新增 D1 credentials 表、不新增
cred_gethost function、不做 KV→D1 migration——這些原計畫取消(好處 KV 已有)。 - 若 leo 仍要 D1:屬新需求(須更新 2026-06-07 拍板)。即便如此,密文本體仍留 KV/Secrets Store,D1 只放非機密 metadata。
- 風險:跨 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 按名取」。
acr creds push(保留)新增「從 .env/env-var 讀值」模式:掃 .env 中「已知 credential 名」 (由 auth-recipe 的required_secrets[].key反推,例notion_token/telegram_bot_token)→ client 端加密 → 存進 D1(Phase B 的 store)。- 仍保留現有「從 credentials.yaml 讀值」模式(向後相容,不刪)。yaml 變成可選輸入源之一,不再是被迫。
- 對稱:本機讀
.env(config.ts loadDotEnvOnce已會載入 .env 進process.env); 雲端=code-on-web 的 Environment 設定(同樣落到 env-var)。同一套讀法、不同來源位置。 - 名稱規約:.env 用大寫(
NOTION_TOKEN)→ normalize 成 credential 名notion_token(=auth-recipe required_secrets.key)。 - 薄殼界線(rule 07):這是「擴
acr creds push的輸入源」=介面慣例的輸入解析(§2 允許第 1 項), 不是在 CLI 拼裝 API 缺的能力(值來源是 .env/yaml 都只是「轉成 API 期望的加密 payload」)。加密仍 client 端(§2 第 4 項唯一例外)。store 仍是 API(D1)。 - 結果:填一次 .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 設計):
- 使用者把
TELEGRAM_BOT_TOKEN=123456:ABC...填進.env(本機)/ code-on-web Environment(雲端)一次。 acr creds push(保留;值來源=.env)→ client 加密 → 存進「我的 D1」credential store。之後永不重推。- 執行時:cypher-executor 見
telegram_send.auth_service='telegram'→auth_static_keyWASM 用cred_get從 D1 取telegram_bot_token密文 →crypto_decrypt→ 依auth_recipe:telegram的inject.path.bot_token輸出auth_path→makeRecipeRunner把 endpoint{{auth.bot_token}}換成真 token → POSThttps://api.telegram.org/bot<token>/sendMessage,body={chat_id,text}。
這條鏈與 notion(header 注入)/gsheets(service_account runtime token 注入)完全同型,只注入位置不同(path vs header)。
過渡期(B/C 未完成前,今天就能用的方式):在
credentials.yaml放telegram_bot_token+acr creds push(值進 KV)。 上面 yaml 的 workflow 寫法不變——只是 store 暫時是 KV、值來源暫時是手寫 yaml。B/C 完成後 store→D1、來源→.env,workflow yaml 一字不改(按名引用穩定)。
4.5 Q2 — KV vs D1:史實核實 + 三方調和 + CC 推薦(leo 授權我決定)
史實(git 核實,2026-06-29):本 repo 的 credential 從來沒在 D1
- 最早 MVP commit
2707fca:credential 即「AES-GCM decrypt from CREDENTIALS_KV」。 - 更早的 u6u-credentials ancestor worker(
credentials/已刪):CREDENTIALS_KV.put('cred:${id}')——也是 KV。 (它的CredentialRecord已有 name/type/created_at 結構欄位,但以 JSON 存進 KV。) - 全 history 的
CREATE TABLE只有 KBDB(entries/templates/entry_values),從無 credentials 表。 - → 找不到任何「credential D1→KV 撤退」的 commit。leo 記憶的「原本 D1 有 credentials 表、後來退 KV」 在本 repo 史實裡不存在(可能是 pre-repo 設計或別處,沒落地過 code)。
三方說法(全部攤開,none 寫 D1 存 credential)
| 來源 | 對 credential 儲存的說法 |
|---|---|
| leo 口頭(現在) | 存 D1(「長久存我的 D1」) |
| 現行 code | KV(CREDENTIALS_KV,MVP 至今未變) |
| kbdb-base SDD 拍板 2026-06-07(leo 親簽「Leo:同意」) | credential 留 KV(「session、verdict、credential 留 KV=短期高頻純取用,類比 Redis;workflow/recipe/成功記錄才進 D1=要層級/列舉/排序」)。kbdb-base/design.md:318、tasks.md:11、tasks.md:83 三處一致。 |
| credential_parts.md §8.2(長期需求源) | tenant KV + Cloudflare Secrets Store(低敏感→KV AES-GCM;高敏感如 service account JSON/private key→Secrets Store,KV 只存 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-off,mindset §7):
- 守 leo 自己的拍板:2026-06-07「credential 留 KV」是有理由的設計(Redis vs PostgreSQL 分工),非偷懶。推翻它要有新理由,目前沒有。
- D1 宣稱的好處在 KV 上已可達或不需要:
- 「
acr creds list」KV 已能做(GET /credentials用KV.list({prefix})列名,credentials.ts:68-78 已實作,今天就有)。 - 「按名引用(workflow 指定要哪把 cred)」是 key 設計(
{api_key}:cred:{name})就給的,與 KV/D1 無關——telegram_send 的auth_service→required_secrets.key已是按名取。A 修好後就具備,不需 D1。 - 「結構化欄位/sensitivity 分級」credential 是不可查詢的密文 blob(查詢明文=洩漏),D1 的 SQL 查詢優勢對 credential 用不上;分級需求由 credential_parts.md 的 Secrets Store(高敏感)解,不是 D1。
- 「
- 搬 D1 成本高且踩鐵律邊界:要新 host function
cred_get(WASM 改)、一次性 migration、跨 TS/WASM/host-function 三層 e2e——換來的好處 KV 已有。淨負債。 - 真正缺的不是 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.2,KV 只存 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 只放非機密的 metadata(cred 名稱/服務/建立時間/last_verified),與 workflow/recipe 進 D1 同模式。不要把密文搬進 D1 當主存。
5. 與既有鐵律/SDD 的對齊
- 不刪 recipe、不禁
acr creds push(leo spec)。A 純補種子 + 型別欄位;C 是擴acr creds push輸入源,非拆除。 - 不新建 component(mindset §1 / rule 02)。
- credential 解密仍在 WASM(rule 02 §2.2);KV→D1 只是密文來源從
kv_get換成新 host functioncred_get(屬 wasi-shim,rule 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.md(credential 長期規格)。