Files
Arcrun/system-dev/docs/3-specs/arcrun/credential-primitives-wasm/credential-store-migration.md
T
uncle6me-web ed2e42e007 feat(credentials): T6 讀取/注入接新家 + T7 雙讀 fallback (credential-store-migration 方案 A)
D19「擁有目錄不擁有內容物」遷移的讀取斷點。密文住 cypher per-script secrets、
D1 只存 secret_ref、auth worker 讀不到 cypher 的 secrets → cypher 先取值塞 payload。

WASM 端(T7 fallback 骨架,零行為改變可獨立部署驗):
- auth_static_key/main.go + auth_service_account/main.go:Input 加 resolved_secrets,
  解密處改「有 resolved 就用、沒有才 fallback 舊 kv_get+crypto_decrypt」。default 等於舊碼。
- tinygo build 兩支通過,copy 到 .component-builds/*/component.wasm。

TS 端(T6 主路徑):
- auth-dispatcher.ts 新增 resolveSecretsFromNewHome:查 D1 拿 secret_ref →
  secret_get(env[ref], T4) 取明文 → 組 map(取不到的 name 缺席,不放空字串) →
  更新 last_used_at。tryAuthDispatch + resolveCredentialRefs 都塞 resolved_secrets。
- rule 02 §2.2 對齊:只查 ref/取值/塞字串,不解密不展開模板不組 JWT。

驗證:cypher-executor + cli tsc exit 0;vitest 41/42(新增 auth-dispatcher.test.ts 6 案例全過,
剩 1 pre-existing 無關失敗)。待 leo21c 部署驗 WASM 端到端(不由本任務部署,acr update
硬綁 GitHub codeload=mistakes #23)。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-05 13:44:26 +08:00

39 KiB
Raw Blame History

Credential Store 遷移 SDD — KV → D1(目錄)+ CF Workers per-script Secrets(密文)

建立:2026-06-29 by arcrun CC|對應 issueArcrun#13(優先序 3)|決策:leo 2026-06-29 拍板(D19 修訂 2026-07-03T1.5 spike,證據 Arcrun#2:密文的家由「CF Secrets Store 產品」改為「CF Workers per-script Secrets」(wrangler secret put / API PUT /accounts/{id}/workers/scripts/{script}/secrets 那套)。原因:T1 spike 實證 Secrets Store 的 Worker binding 部署時靜態宣告、.get(ref) 參數被忽略,不支援「runtime 依 D1 ref 動態查任意 secret」;T1.5 spike 四驗證全過(API 動態加免重部署/env[ref] 動態索引可行/14 把未觸上限/重部署後存活),方向定案。§2.3、§2.5、T3、T4 已依此改寫;雙讀過渡、回填、回滾、治理(D1 只存 metadata+secret_ref 不存密文)設計不動。D212026-07-02 leo 拍板):當作 SaaS 不存在,按單用戶自架設計,多租戶段落標「future SaaS 再議」。 範圍宣告:本檔是既有 SDD credential-primitives-wasm/ 的補充設計(rule 02 §4.3 例外:現有 SDD 目錄內新增單檔)。不施工,先 SDD,總管審對齊後放行。 取代關係:本檔的儲存決策取代 credential-store-redesign.md §4.5 的「維持 KV」推薦——leo 看完該推薦後拍板走 D1+Secrets StoreD19;密文家其後依 T1.5 spike 修訂為 Workers per-script Secrets,見上方修訂註,D19 原則不變),故 Phase B 翻回「搬」。redesign.md 的 Atelegram 一致性,已做)/ C(友善前門)/ Q1(acr parts)不受影響。 詞彙:component=TinyGo WASMrecipe=http_request+固定設定|workflow=多步 YAMLauth-recipe=credential 怎麼注入。


0. leo 的最終決策(D19,一句話)

「擁有目錄,不擁有內容物」arcrun 持有 credential 的目錄(看得到存了哪些、最後何時用),但不持有、也讀不回密文明文——連 owner 自己的界面都讀不到值。

動機(leo 親述):用戶 API key 外泄被盜刷的經濟損失 + 商譽風險(「就算不是我們過失也會怪在我們身上」)。我們不持有明文、不持有金鑰 = 不背這個鍋。

密文值(token / SA JSON / private key  →  CF Workers per-script SecretsCF 託管,API 唯寫,arcrun 拿不到明文)
credential 目錄(name/service/metadata →  使用者自己的 D1(不含密文,只含指向 Workers Secrets 的 secret_refenv var 名)
舊自管 ENCRYPTION_KEY + 密文存 KV         →  廢掉(回填完成後);KV 降為暫存/熱讀快取,非真相源
界面 acr creds list                      →  讀 D1(顯示清單 + last_used),看得到但讀不回值

n8n credentials UXD19 定案)

  • 界面顯示很多 credentials(名字 / 服務 / metadata / last_used)。
  • 不能 read 既有值、不能 edit 既有值(連 owner 都不行)。只能:
    • 整筆 replace(重貼新值覆蓋舊的)
    • delete
  • read 不回值「不是限制、是設計」——值在 Workers SecretsAPI 唯寫,T1.5 spike 實證讀不回),arcrun 拿不到。

1. 現狀(已核實 code2026-06-29

項目 現狀 檔案
密文儲存 CREDENTIALS_KVkey={api_key}:cred:{name},值={encrypted, iv}AES-GCM cypher-executor/src/routes/credentials.ts:48
加密金鑰 自管全域 ENCRYPTION_KEYclient 端加密、WASM crypto_decrypt 解密) rule 01 加解密;wasi-shim.ts
POST /credentials { name, encrypted, iv } credentials.ts:24-53
讀+解密 auth_static_key WASM kv_getcrypto_decrypt registry/components/auth_static_key/main.go:146
列表 GET /credentialsKV.list({prefix}) 列名(不含值) credentials.ts:68-78
D1 全 repo 無 credential D1D1 只有 KBDB graph kbdb/wrangler.toml:10

風險點(leo 要廢的):自管單一 ENCRYPTION_KEY → 漏一把,全部 credential 裸。


2. 目標架構(D19

2.1 三方職責

元件 存什麼 誰拿得到明文
CF Workers per-script Secrets(掛在 cypher worker 上的 secrets 密文值本體(token/SA JSON/private key 只有 CF runtime 在執行期以 env var 注入時。管理 API 唯寫讀不回值(T1.5 spike 實證),arcrun TS/WASM/owner 界面都拿不回
使用者的 D1(與 KBDB 共用那顆 arcrun-kbdb credential 目錄api_key / name / service / sensitivity / created_at / last_used_at / secret_ref無密文 全員可讀目錄(非機密),無人從這拿到值
CREDENTIALS_KV 過渡期:舊自管密文;遷移後:降為可選熱讀快取或廢用 (遷移後不再是真相源)

2.2 D1 schema(明確不含密文欄)

-- migrations/0002_credentials.sqlkbdb 同一顆 D1IF NOT EXISTS 冪等)
CREATE TABLE IF NOT EXISTS credentials (
  api_key      TEXT NOT NULL,           -- 租戶
  name         TEXT NOT NULL,           -- credential 名(= auth-recipe required_secrets[].key,如 telegram_bot_token
  service      TEXT,                    -- 對應 servicetelegram / notion …),可空
  sensitivity  TEXT NOT NULL DEFAULT 'standard',  -- 'standard' | 'high'high = SA JSON/private key
  secret_ref   TEXT NOT NULL,           -- 指向 Workers Secrets 的 env var 名(如 CRED_TELEGRAM_BOT_TOKEN;不是密文本體)
  created_at   INTEGER NOT NULL,
  last_used_at INTEGER,                 -- 執行注入時更新(治理面 last_used 顯示用)
  PRIMARY KEY (api_key, name)
);
CREATE INDEX IF NOT EXISTS idx_cred_apikey ON credentials(api_key);

⚠️ 「name 是名字標籤不是密文」leo 防誤解):name/secret_ref 都是字串標籤,密文值永不進 D1

2.3 Workers per-script Secrets 整合(密文存 cypher worker 的 secrets、D1 存 ref

依據:T1.5 spike 2026-07-03,證據 Arcrun#2curl/wrangler 原始輸出)。本節取代原「CF Secrets Store 產品」設計——T1 spike 實證該產品的 binding 部署時靜態宣告、.get(ref) 參數被忽略、REST 唯寫,不支援「runtime 依 D1 ref 動態查任意 secret」,整案曾停等重新設計;總管裁決轉向 Workers per-script SecretsT1.5 四驗證全過後定案。

已實證的 Workers per-script Secrets 形態(T1.5 spikeArcrun#2):

  • 寫入(動態、免重部署)CF API PUT /accounts/{account_id}/workers/scripts/{script_name}/secretsbody {"name":"<secret_ref>","text":"<值>","type":"secret_text"}(等價 CLIwrangler secret put <NAME>,不需本地源碼)。不改 wrangler.toml、不重上傳 code;spike ① 實證寫入後下一次 invocation 立即可讀(連續 5 次呼叫一致,無延遲、無需等 CF roll 版本)。
  • 讀取(runtime 動態 by refsecret 以 env var 形式進 workerenv 是普通 JS 物件 → env[secret_ref] 用字串動態索引spike ② 實證,?which=SECRET_B 正確取回對應值)。這正是 Secrets Store binding 做不到的事(T1 spike ④-b.get(ref) 參數被忽略)。
  • 唯寫:管理 API 只能 create/update/delete/list 名字,讀不回值(與 Secrets Store 同性質)→ D19「不擁有內容物」保持。
  • secret_ref 命名規則照舊secret_refenv var 名(如 CRED_TELEGRAM_BOT_TOKEN),D1 目錄存 ref 不存值(§2.2)。命名需避開 cypher worker 既有 vars/bindings 名(施工時以 CRED_ 前綴隔離命名空間)。
  • 數量上限(D21 降級:記錄即可,不做阻擋設計)spike ③ 實測 14 把全部 HTTP 201、讀取正常,未觸上限;官方書面上限數字未取得(文件頁被 proxy 擋)。self-hosted 單用戶場景不視為阻擋項;多租戶天花板=future SaaS 再議,不為它增加複雜度。
  • 重部署存活spike ④ 實證 wrangler deploy 重上傳 code 後 14 把 secret 全數存活、值不變——secrets 是掛在 script 外部的獨立資源,不走 wrangler.toml,理論上不受 stripOfficialOnlyBindings 注入邏輯影響(不重演 [ai] binding 被清的坑)。⚠️ 誠實註記:spike 測的是 wrangler deploy 非實際 acr update CLI(底層同一支 PUT /workers/scripts/{name} API,推論成立但非直接實測)——正式導入時於非關鍵 worker 跑一次 acr update 全流程驗證(併入 T3)。

寫入路徑的 token 需求cypher worker 需持有一把「能打 Workers Scripts secrets API 的 CF API token」+ account id(寫入/刪除用;本身高敏感,走 wrangler secret put 注入 cypher worker,非進 git)。這仍比「自管全域 ENCRYPTION_KEY」好——CF 託管值、token 可輪替、且該 token 讀不回任何 secret 值(API 唯寫)。

self-hostedleo21cper-script secrets 是 Workers 原生能力(就是現行 ENCRYPTION_KEY 住的機制),任何 CF 帳號皆支援、無 store 資源需 ensure(對比原 Secrets Store 設計省去「acr init/update ensure store + 注入 store_id」整步)。acr init/update 只需確保上述寫入 token/account id 設定就緒(見 T3)。

註:這與「Anthropic Routine env-var 型 self-hosted 不支援」是不同層(那是 Anthropic 雲端;這是 CF 帳號能力)——別混(leo 提醒)。

2.4 寫入流程(client 加密 → Workers Secrets

⚠️ 鐵律對齊:現行是 client 端加密 + 自管 ENCRYPTION_KEY。走 Workers Secrets 後,密文交給 CF 託管arcrun 不再自管金鑰。client 端是否仍加密一層 = 待決(見 §6 開放問題 Q-b):

  • 選項甲:client 不再加密,明文值經 TLS 送到 cypher → cypher 用「能打 secrets API 的 token」PUT 進 Workers SecretsCF 端託管)。arcrun TS 短暫經手明文(記憶體中,不落地)。
  • 選項乙:保留 client 加密當「傳輸期保護」,但這需 arcrun 持有解密金鑰才能寫進 secrets 明文槽 → 又回到自管金鑰,違 D19。 → 傾向選項甲(符合「不持有金鑰」),但「TS 短暫經手明文」需 leo 接受(mindset §7 誠實標:不是零接觸,是不落地、不持久、不持金鑰)。

2.5 讀取/注入流程(執行期 by ref)

依據:T1.5 spike 2026-07-03Arcrun#2)——env[ref] 動態字串索引實證可行(spike ②),取值不再需要任何對外 REST 呼叫(值已由 CF runtime 以 env var 注入 worker)。

  1. cypher 執行 workflow,遇 recipe auth_service='telegram'
  2. 查 D1 credentials where api_key+name=telegram_bot_token → 取 secret_ref + sensitivity
  3. secret_ref 在執行期動態取值:env[secret_ref]Workers Secrets 以 env var 注入,字串索引原生可行;零網路呼叫、零延遲)。
  4. auth_recipe:telegraminject.path/header 注入(與現行 auth-dispatcher 同型,只是值來源從「KV 密文+WASM 解密」換成「env[ref] 取值」)。
  5. 更新 D1 last_used_at

⚠️ rule 02 §2.2/§2.3 對齊:解密/注入仍不在 cypher TS 實作業務邏輯。env[ref] 取值是「host 能力」(類比 kv_get/crypto_decrypt),落在 wasi-shim host function(新增 secret_get(ref)host 端實作=env[ref]spike ② 已證此場景成立),WASM 零件仍是注入邏輯的所在。施工時須守此界線(見 §5)。


3. 治理 / UX 端點(n8n 模式)

端點 行為 對齊 D19
GET /credentials D1,回 [{name, service, sensitivity, created_at, last_used_at}]不含 secret_ref 對外、不含值 顯示清單 + last_used
PUT /credentials/:namereplace 整筆覆寫:寫新值進 Workers SecretsAPI PUT 同 ref 覆蓋或新 ref+ 更新 D1 metadata 只能 replace
DELETE /credentials/:name 刪 D1 row + 刪 Workers Secrets secretAPI DELETE .../secrets/{ref} 可 delete
GET /credentials/:name/value 不存在 / 移除任何讀回值的路徑 讀不回值 = 設計

CLI 薄殼(rule 07):acr creds list(讀 D1 顯示)、acr creds replace <name>(覆寫)、acr creds delete <name>移除任何「印出 credential 值」的路徑


4. 遷移:雙讀過渡 + 回填 + 回滾

4.1 雙讀過渡(不停機)

執行期取 credential 值的順序:

  1. 先查 D1 有無 secret_ref → 有 → 走 Workers Secrets 取值(env[ref],新家)。
  2. 找不到 → fallback 舊路徑:CREDENTIALS_KV {api_key}:cred:{name} + WASM crypto_decrypt(舊自管)。 → 新寫入一律走新家;舊資料未回填前仍可讀。

4.2 回填(一次性、冪等、可審)

POST /credentials/migrate-to-workers-secrets(類比現有 migrate-cron-index 冪等端點;端點名隨 2026-07-03 修訂由 migrate-to-secrets-store 更名,設計不變):

  • 對每個 {api_key}:cred:{name} KV rowWASM 解密取明文 → PUT 進 Workers Secrets(領 secret_refenv var 名)→ 在 D1 建 row(含 ref + metadata)。
  • 冪等:D1 已有該 (api_key,name) row 且 ref 可解析 → 跳過。
  • 誠實回報逐筆 ok/fail(不假綠,mindset §7)。
  • 回填完成且驗證通過後,才執行 §4.4 廢 ENCRYPTION_KEY。

4.3 回滾錨點

  • 回填不刪 KV 舊密文(保留為回滾錨點),只新增 D1+Workers Secrets。
  • 出問題 → 雙讀 fallback 自動回到 KV 路徑;D1 row 可刪、Workers Secrets secret 可刪 → 回到純 KV 狀態。
  • 只有在「全租戶回填驗證綠 + 觀察期無 fallback 命中」後,才進 §4.4。

4.4 廢自管 ENCRYPTION_KEY(最後一步)

  • 移除 crypto_decrypt 對 credential 的依賴路徑(KV 密文路徑停用)。
  • wrangler secret delete ENCRYPTION_KEYcypher / auth_static_key / auth_service_account)。
  • 清理 KV 舊密文(確認 Workers Secrets 是唯一真相源後)。
  • ⚠️ 此步不可逆,須 leo 明示放行 + 回填觀察期通過。

5. 與鐵律 / 既有架構對齊(施工硬約束)

  • rule 02 §2.2/§2.3cypher TS 不實作 credential 解密/注入業務邏輯。Workers Secrets 取值走 host function 邊界(新增 secret_get(ref) 於 wasi-shimhost 端實作=env[ref]),注入仍在 WASM auth primitive。
  • rule 01 加解密:自管 AES-GCM 路徑在回填後廢除;新家由 CF Workers Secrets 託管(arcrun 不持金鑰,管理 API 唯寫讀不回值)。
  • 不新增 componentmindset §1);不新增 service bindingrule 03 §3.1)。
  • 薄殼(rule 07CLI/MCP 只做「list 顯示 / replace 覆寫 / delete」介面轉換,secrets 能力在 API。
  • 部署繞開 GitHub ActionsD1 migration + secrets 寫入設定走 acr init/updatewrangler 直推),不掛 Actionssecrets 本體是掛在 script 外部的獨立資源,重部署不受影響(T1.5 spike ④)。
  • D1 同源:用使用者既有 arcrun-kbdb D1(不新建第二顆),migration 0002_credentials.sql 走現有 deploy.ts migration 注入路徑(與 0001_base.sql 同模式)。

6. 開放問題(施工前須 leo / 總管拍板)

  • Q-a(核心) 已解(T1 + T1.5 spike,證據 Arcrun#2。原問「Secrets Store 動態 by-ref 取值路徑是否可行」→ T1 spike 負結果(binding 靜態、.get(ref) 參數被忽略、REST 唯寫),該產品路徑不成立;總管裁決轉向 Workers per-script SecretsT1.5 spike 四驗證全過(§2.3),硬前置解除。
  • Q-b:寫入時 client 是否仍加密一層(§2.4 甲/乙)。傾向甲(不持金鑰,TS 短暫經手明文不落地)——需 leo 接受此誠實 trade-off。
  • Q-csensitivity 分級的判定(誰標 high/standard)——auth-recipe 可加 sensitivity 欄宣告該 service 的 credential 等級,或一律 high。
  • Q-d:與 redesign.md C(友善前門:.env → 一次填)整合——C 的「值來源 .env」在新架構下是「.env 明文 → cypher PUT 進 Workers Secrets」,與 §2.4 甲一致。確認 C 依附本 SDD(值的家是 Workers Secrets 不是 D1)。

7. 任務分解(T1.5 全過,T2-T9 解凍施工——2026-07-03,依總管 Arcrun#2 裁決)

  • T1 spikeleo21c CF 帳號 Secrets Store create store + worker by-ref 取值驗證(解 Q-a)。結果:負結果,路徑不成立binding 靜態宣告、.get(ref) 參數被忽略、REST 唯寫;2026-07-02,證據 Arcrun#2)→ 轉向 T1.5。
  • T1.5 spikeWorkers per-script Secrets 四驗證全過(① API 動態加免重部署即讀 ② env[ref] 動態索引可行 ③ 14 把未觸上限(D21:記錄即可)④ 重部署後存活;2026-07-03,證據 Arcrun#2)。方向定案,硬前置解除。
  • T2 D1 migration 0002_credentials.sql(§2.2+ deploy.ts 注入。2026-07-03 完成: kbdb/migrations/0002_credentials.sql 新增(IF NOT EXISTS 冪等,schema 對齊 §2.2); cli/src/lib/deploy.ts §3.6 新增區塊,重用既有 applyD1Migration helper,緊接在 0001_base.sql 套用之後、同一顆 D1ctx.d1DatabaseId)、同一支 CF D1 query API,機制與 0001 完全一致。 cli package tsc --noEmit exit 0(無輸出)。端到端證據leo21c arcrun-kbdb D1 uuid 1099d0f3-d062-4202-984c-5a57b0a8c031):POST .../d1/database/{id}/query 送整份 0002_credentials.sql → {"success":true,...}SELECT name FROM sqlite_master WHERE type='table' AND name='credentials' → 回 {"name":"credentials"}schema dump 確認欄位與 idx_cred_apikey index 完全對齊 §2.2;重跑同一份 migration 第二次 → success:true(冪等驗證)。
  • T3 acr init/update 確保 secrets 寫入設定就緒:cypher worker 持有「能打 Workers Scripts secrets API 的 CF API token」+ account id(§2.3 寫入路徑;per-script secrets 無 store 資源需 ensure,原「ensure store + 注入 store_id」整步取消)+ 於非關鍵 worker 跑一次 acr update 全流程驗證 secrets 存活(補 spike ④ 的誠實缺口)。〔依 T1.5 spike 2026-07-03 改寫,證據 Arcrun#22026-07-03 完成(比照 ENCRYPTION_KEY 既有模式,未發明新模式): 1. cli/src/lib/deploy.ts injectWranglerConfig 新增 CF_ACCOUNT_ID 注入(非機密帳號 id 比照 WORKER_SUBDOMAIN 同一套機制,ctx.accountId 是 init/update 早就有的值)。 2. cli/src/commands/init.ts 加「下一步 ③」印手動指令 wrangler secret put CF_SECRETS_API_TOKEN --name arcrun-cypher-executorCF_SECRETS_API_TOKEN 是機密,比照 ENCRYPTION_KEY 走 手動 wrangler secret put,不自動代設)。 驗證:cli tsc --noEmit exit 0。誠實缺口 closure(不重複 T1.5 spike ④,而是對本次真的 改動過的 committed code 做一次真實驗證)acr update 目前仍受 mistakes #23 限制(硬綁 GitHub codeload uncle6me-web/Arcrun@main,會抓不到 Gitea 本次改動、且會把 leo21c 的 24 個 worker 全部拉回 GitHub 上的舊版覆蓋掉剛部署的 T4/T5 成果)——判斷後刻意不跑,跑了只會摧毀 本次測試環境又證明不了任何新東西,同 mistakes #23 已記錄的已知限制。改採 SDD 本身列的 替代選項「lighter confirmation... on real committed code」:對已經是本次真實委的 code 的 leo21c arcrun-cypher-executor 做第二次 wrangler deploy(同 T5 手法:本地 patch wrangler.toml 真實 id → deploy → 立刻 restore),部署後 CF API 確認 CF_SECRETS_API_TOKEN / ENCRYPTION_KEY 兩個 secret 都還在(GET .../workers/scripts/.../secrets 清單不變), 並以真實 curl POST /credentials 打一次確認寫入路徑仍正常運作(回 {"success":true, "secret_ref":"CRED_REDEPLOY_PROBE_..."} )——證明「secrets 在對本次改動過的 code 重部署後 存活且可用」,非僅沿用 T1.5 spike 對 throwaway worker 的舊證據。測試資料已清理, wrangler.toml 已 restore 回 git 追蹤版本(grep 確認無 leo21c 帳號專屬 id 殘留)。 懸而未決(誠實記錄,非本次能力範圍解決)acr update 硬綁 GitHub codeload 與 「cloud-worker 只碰 Gitea」的架構前提仍有結構性衝突(mistakes #23 原文),T3 的 「acr init/update 確保 secrets 設定就緒」邏輯本身(CF_ACCOUNT_ID 注入 + 印 token 指令) 已可用,但只要走 acr update 這條指令本身,部署物來源仍是 GitHub 舊碼,這個更深的問題 留給 leo 裁決(非 T3 範圍能解)。
  • T4 wasi-shim 新增 secret_get(ref) host function(守 rule 02 邊界;host 端實作=env[ref] 動態索引,spike ② 已證可行,零網路呼叫)。〔依 T1.5 spike 2026-07-03 改寫,證據 Arcrun#22026-07-03 完成: cypher-executor/src/lib/wasi-shim.tsArcrunHostEnv 加 index signature[secretRef: string]: unknown)讓 host function 能對任意 secret_ref 動態取值;WasiHostFunctionssecret_get?: (ref: string) => Promise<string | null>u6u WASI imports 加 secret_get wiring(與 kv_get 同款 pointer/memory-write 機制:0 成功/1 錯誤/2 找不到);host 端實作 secretGet(env, ref)env[ref] 動態索引,只允許 CRED_ 前綴(比照 routedKvGet 的 前綴檢查精神,拒絕讀 ENCRYPTION_KEY/CF_SECRETS_API_TOKEN 等非 credential 機密,我方判斷的 安全邊界,SDD 未明文但符合 §2.3「CRED_ 前綴隔離命名空間」精神);createArcrunHostFunctions 掛上 secret_get。驗證:tsc --noEmit exit 0(無輸出);vitest run tests/wasi-shim.test.ts 18/18 通過(新增 createArcrunHostFunctions — secret_get 4 案例對 fake env 物件測 CRED_ 前綴放行 /不存在回 null/非 CRED_ 前綴拒絕/型別不符回 null;u6u.secret_get wiring 1 案例測未注入時 同步 fallback)。誠實撞牆記錄:原規劃另外 2 個測 pointer/memory-write 機制的案例 (直接呼叫 .imports.u6u.secret_get(...))失敗,查證後發現 vitest-pool-workers 環境的 WebAssembly 支援 JSPIhostWrap() 把所有 async host function(含既有 kv_get)包成 WebAssembly.Suspending 物件,此物件設計上只能當 WASM import 綁定用、不能在 JS 端直接 fn(...) 呼叫——用 probe 測試證實 kv_get 的 import 同樣不可直接呼叫,確認是既有架構的 環境限制非 secret_get 缺陷,已移除該 2 案例並在測試檔內註記;pointer 機制的真實驗證改由 T5 wrangler 部署端到端涵蓋。
  • T5 寫入路徑:POST/PUT /credentials 改寫 Workers SecretsAPI PUT+ D1 ref(§2.4)。2026-07-03 完成: cypher-executor/src/routes/credentials.ts 整檔改寫 POST(建立)+ 新增 PUT /credentials/:name (覆寫):兩者共用 writeCredential() → 1) putWorkerSecret() PUT 進 CF Workers Scripts secrets 管理 API(唯寫)2) upsertCredentialRow() D1 upsert(不含密文,覆寫時保留原 created_at)。 secret_ref 命名=CRED_<NAME 大寫>_<sha256(api_key) 前 8 碼大寫>cypher-executor/src/lib/hash.ts 匯出既有 sha256Prefix,避免跨租戶同名 credential 撞名覆蓋彼此的 secret;純函式衍生非解密/簽章邏輯, 不違 rule 02 §2.2)。DELETE/GET /credentials 本次刻意不動(仍讀寫舊 KV,T9 治理端點範圍), 已在檔頭與 §3 comment 誠實標注此缺口。types.ts Bindings 加 CREDENTIALS_DB: D1Database (與 KBDB base 共用同一顆 arcrun-kbdb D1+ CF_SECRETS_API_TOKEN? / CF_ACCOUNT_ID?wrangler.toml[[d1_databases]] binding="CREDENTIALS_DB"(沿用 kbdb 同款 database_id 注入機制,deploy.ts 不用改)+ [vars] CF_ACCOUNT_ID="" 佔位(T3 由 deploy.ts 自動注入非機密 帳號 id,同 WORKER_SUBDOMAIN 模式)。§2.4 選項甲落地client 不再 AES-GCM 加密,明文值經 TLS 傳輸,cypher 短暫在記憶體經手明文不落地不持金鑰)——與舊版 01-tech-stack.md 記載的 {name,encrypted,iv} 傳輸格式不同,此為本 SDD 對舊格式的刻意取代,§6 Q-b 仍列需 leo 明確 接受的誠實 trade-off(先落地,若不接受選項甲需回頭改,見任務完成報告「撞牆」段)。 驗證:tsc --noEmit exit 0cli + cypher-executor 皆是);vitest run 26/27 通過(1 個 pre-existing 無關失敗,見 T4 記錄,non-regression 已用 git stash 驗證)。端到端證據(真實 leo21c arcrun-cypher-executor worker,非測試替身——寫入端點本身就是要驗的東西,無替代): 比照 mistakes #23 手法本地 patch wrangler.tomlKV/D1 id、CF_ACCOUNT_ID、WORKER_SUBDOMAIN、 KBDB_BASE_URL 換 leo21c 真實值,strip [ai]/[[routes]])→ wrangler deploy --dry-run 核對 binding 表(env.CREDENTIALS_DB (arcrun-kbdb) D1 Database 等全對)→ 真部署 → wrangler secret put CF_SECRETS_API_TOKEN(沿用 CLOUDFLARE_API_TOKEN)→ curl POST /credentials{"success":true,"secret_ref":"CRED_TELEGRAM_BOT_TOKEN_9BB9FB82",...} → CF API GET .../workers/scripts/arcrun-cypher-executor/secrets 確認該 secret 真的存在 → D1 query 確認 對應 rowapi_key/name/service/sensitivity/secret_ref/created_at 全對)→ curl PUT /credentials/telegram_bot_token 覆寫值,確認 secret_ref 不變、created_at 不變(覆寫語意 正確)→ 清理:CF API DELETE .../secrets/{ref} + D1 DELETE FROM credentials,二次查詢確認 皆已清空。部署完成後立刻 restore wrangler.toml 備份git 追蹤版本無 leo21c 帳號專屬 id 殘留(已 grep 確認)。
  • T6 讀取/注入路徑:auth-dispatcher 改 D1 ref → env[ref] 取值(§2.5+ 更新 last_used。 2026-07-05 本機(有 TinyGo 0.40.1)接手,方案 Aleo 已拍板〕接續 2026-07-04 [cloud-worker] 揭露的正確落地方式cypher 先取值塞 payload,因 auth worker 讀不到 cypher 的 secrets): WASM 端(T7 fallback 骨架,可獨立部署驗) - registry/components/auth_static_key/main.goInput 加 ResolvedSecrets map[string]string authenticate 的解密迴圈與 handleResolveCredentials 都改為「input.ResolvedSecrets[key] 有值就用,沒有才 fallback 舊 kvGet+cryptoDecrypt」。default(此欄 nil)行為完全等於遷移前舊碼。 - registry/components/auth_service_account/main.goSA JSON 取得同型(有 resolved 就用,沒有才舊路徑)。 - tinygo build -target=wasi 兩支皆通過(static_key 1.13MB / service_account 1.18MB,在 2MB 限制內), copy 到 .component-builds/{name}/component.wasmTS 端(T6 主路徑)cypher-executor/src/actions/auth-dispatcher.ts 新增 resolveSecretsFromNewHome(env, apiKey, names):查 D1 credentialsapi_key+name IN names)拿 secret_refcreateArcrunHostFunctions(env, apiKey).secret_get(ref)T4host 端 = env[ref] 取明文 → 組 {name:value} map取不到值的 name 缺席(不放空字串) 讓 WASM fallbackT7)→ 取到值的 name UPDATE credentials SET last_used_attryAuthDispatch 取 recipe 非 optional required_secrets[].key 當 names、resolveCredentialRefs 用已收集的 names,兩者都把 resolved_secrets 塞進送 auth WASM 的 POST body。rule 02 §2.2 對齊:只查 ref/取值/塞字串, 不解密、不展開模板、不組 JWT(grep 確認無 crypto.subtle/interpolate/{{secret./BUILTIN_*)。 驗證cypher-executor + cli tsc --noEmit 皆 exit 0vitest run 41/42(新增 tests/auth-dispatcher.test.ts 6 案例全過:D1 有 ref+新家有值→進 map/無 ref→缺席走 fallback 有 ref 但新家沒值 secret_get 回 null→缺席不放空字串/混合/空 names/取值後更新 last_used_at 剩 1 個 pre-existing 無關失敗 executor.test.ts「不存在」措辭,見 T8 記錄,非本次退步—— git baseline 35/36 → 41/42,只增不減)。 待 leo21c 部署驗(本任務不部署,acr update 硬綁 GitHub codeload=mistakes #23,交總管/後續) WASM 端到端(resolved_secrets 命中新家的真實注入)在 vitest 測不到(JSPI import 限制,同 T4 記錄), secret_get 的 pointer/memory-write 也待部署端到端涵蓋。TS 側 resolveSecretsFromNewHome 的 D1 查詢 + secret_get + last_used 更新已用真實 D1 binding + fake env(模擬 per-script secret 注入) 單元覆蓋。 2026-07-04 [cloud-worker] 卡在框架級工具缺口,非設計問題〕T6 需要修改 registry/components/auth_static_key/main.go(與 auth_service_account/main.go 這兩個 auth primitive 是獨立部署的 TinyGo WASM Worker,密文值現在住在 cypher-executor 自己的 per-script secretsT5 putWorkerSecret 寫入的 script 是 arcrun-cypher-executor,不是 auth_static_key worker)——env[secret_ref] 動態索引 只在「secret 所在的那個 worker 自己的 runtime」內可讀,auth_static_key worker 讀不到 cypher-executor 的 env。故 T6 的正確落地方式是:cypher-executorauth-dispatcher.ts 先查 D1 拿 secret_ref、呼叫 secret_get(ref)(T4 已備妥,讀的正是 cypher 自己的 env)取得明文,再把已解析好的值放進送給 auth_static_key WASM 的 payload 一個新欄位 (如 resolved_secrets: {name: value}),main.go 收到後優先用它、沒有才 fallback 舊 kv_get+crypto_decrypt(這個 fallback 分支同時就是 T7 雙讀)。這個 main.go 改動 需要 TinyGo 編譯——本次雲端環境核實:tinygo 未安裝(which tinygo 找不到)、 apt-cache search tinygo 查無套件、且 proxy 擋 github.comcurl -sI https://github.com/tinygo-org/tinygo/releasesHTTP/1.1 403 Forbidden tinygo 官方無 apt/npm 分發管道,只能從 GitHub Releases 下載),無法在本環境安裝 TinyGo 工具鏈,因此無法重新編譯/部署 auth_static_key.wasm。誠實結論:T6/T7 的 TS 側前置(D1 查詢+secret_get 呼叫)在架構上可行且不衝突 rule 02(cypher 短暫經手 明文、只轉送給 WASM 做 template 注入,同 §2.4 選項甲的既有精神),但卡在需要 TinyGo 編譯環境這一步,非本次雲端工人能力範圍,需總管本機(有 TinyGo)或 leo 決定是否要在雲端環境補裝工具鏈(例如允許 proxy 放行 github.com release 下載,或 改用其他 TinyGo 取得管道)。整案不硬繞(不改用「TS 直接組 header」這種違反 rule 02 的捷徑),留給有 TinyGo 環境的一方接手 main.go 改動 + 重新編譯部署。
  • T7 雙讀 fallback(§4.1)。〔2026-07-05 本機,隨 T6 一併落地。〕如原設計預期: 不需額外的「fallback 判斷」程式碼——T6 的 resolved_secrets 就是「有給才用、沒給 就照舊 kv_get+crypto_decrypt」的設計(WASM 端 input.ResolvedSecrets[key]ok 判斷即分流)。 雙讀在兩層自然成立: (1) cypher 層——resolveSecretsFromNewHome 查 D1 查不到 ref(從未回填)或 secret_get 回 null (新家還沒值)→ 該 name 不進 resolved_secrets map (2) WASM 層——收到的 resolved_secrets 缺該 key → 走既有 kvGet+cryptoDecrypt 舊路徑。 新寫入(T5)一律新家;舊資料未回填(T8)前仍可讀(舊 KV 不刪,§4.3 回滾錨點不動)。 單元測試涵蓋「無 ref→缺席」「有 ref 但新家沒值 secret_get 回 null→缺席」兩條 fallback 分流 (見 T6 的 tests/auth-dispatcher.test.ts)。default(無 resolved_secrets 欄)行為 = 遷移前舊碼, 故 WASM 端此改動可獨立部署、零行為改變地驗「沒弄壞既有」。
  • T8 回填端點 POST /credentials/migrate-to-workers-secrets(§4.2,冪等可審)。 2026-07-04 [cloud-worker] 完成:cypher-executor/src/routes/credentials.ts 新增 端點,重用 createArcrunHostFunctions(env, apiKey).crypto_decryptwasi-shim.ts 唯一合法 crypto.subtle.decrypt 呼叫點,本檔不重新實作解密,grep 確認全 repo crypto.subtle 呼叫點未增加)解出舊 KV 明文 → putWorkerSecret → D1 upsert。 冪等:D1 已有可解析 secret_ref 的 row 就跳過(不重打 CF API)。逐筆誠實回報 ok/skipped/fail。單元測試tests/credentials.test.tsvitest-pool-workers + D1 mocktests/setup.ts 建表):401 檢查、冪等 skip、空結果、以及「假造密文在 測試環境缺 CF token 時誠實回報 fail 不假綠」4 案例全過。tsc --noEmit exit 0 vitest run 35/361 個 pre-existing 無關失敗,見完成記錄)。 端到端真實驗證leo21c arcrun-cypher-executor,非測試替身):patch wrangler.toml 真實 id(同 T5 手法)→ wrangler deploy --dry-run 核對 binding 表全對 → 真部署 → 打 POST /credentials/migrate-to-workers-secrets 對真實租戶 leo 名下兩筆真實既存的舊 KV credentialgoogle_service_accountnotion_token,非測試殘留,KV 內容確認是合法 {encrypted, iv} JSON 結構, base64 長度正常)→ 端點誠實回報兩筆皆 failImported AES key length must be 128, 192, or 256 bits but provided 0),未寫入任何部分資料到 D1 或 Workers Secrets(已查證 D1 credentials 表對這兩筆 name 完全無 row,非部分髒寫)重大誠實發現(非本次程式碼缺陷,是既有基礎設施缺口):這個錯誤代表 env.ENCRYPTION_KEYcypher-executor 這個 worker 自己的 runtime 內讀到空字串 hexToUint8Array('').length === 0)——過去 crypto_decrypt 只在 auth_static_key/ auth_service_account 這兩個獨立 worker 上被呼叫過(各自有自己設定好的 ENCRYPTION_KEY secret),cypher-executor 自己的 ENCRYPTION_KEY secret 從未被 任何程式路徑真正呼叫驗證過Phase 6.6 CI 記錄有把同一份 .env 值 pipe 給三個 worker,理論上應該一致,但 CF Workers Secrets 管理 API 本質唯寫、無法讀回比對, 無法排除某個環節曾經漏設或設成空值)。T8 是史上第一個在 cypher-executor 自己 的 runtime 呼叫 crypto_decrypt 的程式路徑,因此第一次揭露這個潛在缺口。 不可逆風險,本次不處理,等 leo:重設 cypher-executor 的 ENCRYPTION_KEY 這件事 本身不可逆——如果現在的空值就是問題根源,重設一把新 key 沒問題;但如果問題其實 出在別處(例如 env binding 讀取路徑有 bug),錯誤地假設「重設 key 就會好」而 去 wrangler secret put 可能導致這兩把真實 credentialGoogle Service Account + Notion token)從此永久無法解密(唯寫 API 讀不回舊值比對,猜錯就回不了頭)。 故本次刻意不猜、不重設,只誠實回報現象+兩種可能成因,交給有本機環境能 實際比對三個 worker ENCRYPTION_KEY 是否一致的一方(總管/leo)診斷後裁決。 測試資料已清理(探測用 t89_probe_secrett89_legacy_probe 皆已刪除,D1 與 KV 復原至只剩原本兩筆真實資料的乾淨狀態,已 curl 覆核);部署完成後 wrangler.tomlgit checkout 復原,grep 確認無 leo21c 帳號專屬 id 殘留。
  • T9 治理端點 list/replace/delete + 移除任何 read-value 路徑(§3+ CLI 薄殼。 2026-07-04 [cloud-worker] 完成:GET /credentials 改讀 D1(與既有 /credentials/ catalog 共用同一份 query/catalog 保留給 Console 既有呼叫不受影響); DELETE /credentials/:name 改為「D1 有 secret_ref → 刪 Workers Secret + D1 row 沒有(從未回填過,只存在舊 KV)→ fallback 刪舊 KV key」,避免 GET 改讀 D1 後 「查不到卻刪不掉」的孤兒資料。全程無任何回傳密文值的路徑GET 只回 name/service/sensitivity/created_at/last_used_atDELETE 不讀值只刪)。CLI 薄殼 cli/src/commands/creds.tsacr creds list(讀 D1 顯示)/acr creds replace <name> <value>PUT 整筆覆寫)/acr creds delete <name>;順手修好 acr creds push(原實作對應 T5 之前的 {name,encrypted,iv} 格式已過期,改直接 PUT 明文 {name,value},否則會 400——這是既有 CLI 的誠實缺口修正,非新增範圍外功能)。 驗證tsc --noEmitcli + cypher-executorexit 0vitest run credentials.test.ts 全過(見 T8 記錄,同一檔涵蓋兩個任務);CLI tsc build 後 node dist/index.js creds --help/creds replace --help 手動核對指令走線正確。 端到端真實驗證leo21c arcrun-cypher-executor): POST /credentialsT5 既有路徑)建一筆 → GET /credentials 立刻讀到(D1 新讀 路徑生效,與 /catalog 回傳一致)→ DELETE 新路徑分支:CF API 直接核對 arcrun-cypher-executor secrets 清單,刪除前有 CRED_T89_PROBE_SECRET_*、刪除後 該 secret 真的消失(只剩 CF_SECRETS_API_TOKEN/ENCRYPTION_KEY)+ D1 row 真的 清空 → 另外手工在 KV 塞一筆「只存在舊 KV、無 D1 row」的假資料,DELETE 走 legacy-kv fallback 分支,CF API 核對 KV key 真的被刪除。兩分支皆對真實 leo21c 帳號驗證通過,非模擬。
  • T10 回填驗證 + 觀察期 → 廢 ENCRYPTION_KEY(§4.4leo 明示放行)。

每個 cred 操作跨 TS / WASM / host-function / 兩個 store,必端到端實測(防再假綠,mindset §7)。原「T1 不通則整案停」已兌現一輪:T1 負結果 → 整案停 → 總管裁決轉向 → T1.5 全過 → T2-T9 解凍。備援(若施工再撞死路):codegen binding + 自動重部署(Arcrun#2 總管裁決的方向 1)。