Files
Arcrun/system-dev/docs/3-specs/arcrun/credential-primitives-wasm/credential-store-migration.md
T
Claude 1d19d46161 feat(credentials): T8 回填端點 + T9 治理端點/CLI (credential-store-migration)
- POST /credentials/migrate-to-workers-secrets:舊 KV credential 逐筆解密回填 D1+Workers
  Secrets,冪等可審,重用 wasi-shim 唯一合法 crypto_decrypt 呼叫點
- GET /credentials 改讀 D1(與既有 /credentials/catalog 共用查詢);DELETE 改為新家優先、
  舊 KV fallback,避免孤兒資料
- acr creds list/replace/delete 三支 CLI 薄殼指令;順手修好過期的 acr creds push(舊
  client 端加密格式已被 T5 取代)
- 新增 cypher-executor/tests/credentials.test.ts + D1 test fixture

T6/T7(讀取/注入路徑、雙讀 fallback)需要重新編譯 registry/components/auth_static_key
的 TinyGo WASM,本環境無 tinygo 且 proxy 擋 github.com 下載,卡在工具鏈缺口,詳細分析
記錄在 credential-store-migration.md。

端到端驗證:部署到 leo21c 帳號真實跑過 GET/POST/DELETE 三分支 + migrate 端點(對真實
既存的兩筆 credential 跑,發現 cypher-executor 自己的 ENCRYPTION_KEY secret 疑似為空,
誠實記錄為待 leo/總管裁決的不可逆風險項,未擅自重設)。

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-04 23:00:07 +00:00

342 lines
35 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 Store**D19;密文家其後依 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_KV`key=`{api_key}:cred:{name}`,值=`{encrypted, iv}`AES-GCM | `cypher-executor/src/routes/credentials.ts:48` |
| 加密金鑰 | 自管全域 `ENCRYPTION_KEY`client 端加密、WASM `crypto_decrypt` 解密) | rule 01 加解密;`wasi-shim.ts` |
| 寫 | `POST /credentials` { name, encrypted, iv } | `credentials.ts:24-53` |
| 讀+解密 | `auth_static_key` WASM `kv_get``crypto_decrypt` | `registry/components/auth_static_key/main.go:146` |
| 列表 | `GET /credentials``KV.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(明確不含密文欄)
```sql
-- 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}/secrets`body `{"name":"<secret_ref>","text":"<值>","type":"secret_text"}`(等價 CLI`wrangler secret put <NAME>`,不需本地源碼)。**不改 wrangler.toml、不重上傳 code**spike ① 實證寫入後下一次 invocation 立即可讀(連續 5 次呼叫一致,無延遲、無需等 CF roll 版本)。
- **讀取(runtime 動態 by ref**secret 以 env var 形式進 worker`env` 是普通 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_ref`env 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-hostedleo21c**per-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:telegram``inject.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/:name`(replace | 整筆覆寫:寫新值進 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_ref`env 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_KEY`cypher / auth_static_key / auth_service_account)。
- 清理 KV 舊密文(確認 Workers Secrets 是唯一真相源後)。
- ⚠️ **此步不可逆**,須 leo 明示放行 + 回填觀察期通過。
---
## 5. 與鐵律 / 既有架構對齊(施工硬約束)
- **rule 02 §2.2/§2.3**cypher 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 唯寫讀不回值)。
- **不新增 component**mindset §1);不新增 service bindingrule 03 §3.1)。
- **薄殼(rule 07**CLI/MCP 只做「list 顯示 / replace 覆寫 / delete」介面轉換,secrets 能力在 API。
- **部署繞開 GitHub Actions**D1 migration + secrets 寫入設定走 `acr init/update`wrangler 直推),不掛 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 Secrets**T1.5 spike 四驗證全過(§2.3),硬前置解除。
- **Q-b**:寫入時 client 是否仍加密一層(§2.4 甲/乙)。傾向甲(不持金鑰,TS 短暫經手明文不落地)——需 leo 接受此誠實 trade-off。
- **Q-c**`sensitivity` 分級的判定(誰標 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 裁決)
- [x] T1 spikeleo21c CF 帳號 Secrets Store create store + worker by-ref 取值驗證(解 Q-a)。**結果:負結果,路徑不成立**(binding 靜態宣告、`.get(ref)` 參數被忽略、REST 唯寫;2026-07-02,證據 Arcrun#2)→ 轉向 T1.5。
- [x] T1.5 spike**Workers per-script Secrets 四驗證全過**(① API 動態加免重部署即讀 ② `env[ref]` 動態索引可行 ③ 14 把未觸上限(D21:記錄即可)④ 重部署後存活;2026-07-03,證據 Arcrun#2)。**方向定案,硬前置解除。**
- [x] 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
套用之後、同一顆 D1`ctx.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`(冪等驗證)。
- [x] 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-executor``CF_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 範圍能解)。
- [x] 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.ts` — `ArcrunHostEnv` 加 index signature`[secretRef: string]:
unknown`)讓 host function 能對任意 secret_ref 動態取值;`WasiHostFunctions` 加
`secret_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 支援 JSPI`hostWrap()` 把所有 async host function(含既有 `kv_get`)包成
`WebAssembly.Suspending` 物件,此物件設計上只能當 WASM import 綁定用、不能在 JS 端直接
`fn(...)` 呼叫——用 probe 測試證實 `kv_get` 的 import 同樣不可直接呼叫,確認是既有架構的
環境限制非 secret_get 缺陷,已移除該 2 案例並在測試檔內註記;pointer 機制的真實驗證改由
T5 wrangler 部署端到端涵蓋。
- [x] 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.toml`KV/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-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 secrets**T5 `putWorkerSecret` 寫入的 script 是
`arcrun-cypher-executor`,不是 auth_static_key worker)——`env[secret_ref]` 動態索引
只在「secret 所在的那個 worker 自己的 runtime」內可讀,auth_static_key worker 讀不到
cypher-executor 的 env。故 T6 的正確落地方式是:cypher-executor`auth-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.com``curl -sI
https://github.com/tinygo-org/tinygo/releases` → `HTTP/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)。〔同上,隨 T6 main.go 改動一併落地——D1 查得到 secret_ref
走新路徑,查不到就是 WASM 原有的 `kv_get`+`crypto_decrypt` 分支,本來就會自然發生,
不需要額外的「fallback 判斷」程式碼,只要 T6 的 `resolved_secrets` 是「有給才用、沒給
就照舊」的設計即可。同樣卡在 T6 的 TinyGo 前置。〕
- [x] T8 回填端點 `POST /credentials/migrate-to-workers-secrets`(§4.2,冪等可審)。
2026-07-04 [cloud-worker] 完成:`cypher-executor/src/routes/credentials.ts` 新增
端點,重用 `createArcrunHostFunctions(env, apiKey).crypto_decrypt`wasi-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.ts`vitest-pool-workers +
D1 mock`tests/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 credential`google_service_account`、
`notion_token`,非測試殘留,KV 內容確認是合法 `{encrypted, iv}` JSON 結構,
base64 長度正常)→ **端點誠實回報兩筆皆 fail**(`Imported AES key length must be
128, 192, or 256 bits but provided 0`),**未寫入任何部分資料到 D1 或 Workers
Secrets(已查證 D1 `credentials` 表對這兩筆 name 完全無 row,非部分髒寫)**。
**重大誠實發現(非本次程式碼缺陷,是既有基礎設施缺口)**:這個錯誤代表
`env.ENCRYPTION_KEY` 在 **cypher-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_secret``t89_legacy_probe` 皆已刪除,D1 與 KV
復原至只剩原本兩筆真實資料的乾淨狀態,已 curl 覆核);部署完成後 `wrangler.toml`
已 `git checkout` 復原,grep 確認無 leo21c 帳號專屬 id 殘留。
- [x] 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.ts` 加 `acr 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 --noEmit`cli + cypher-executorexit 0`vitest run`
credentials.test.ts 全過(見 T8 記錄,同一檔涵蓋兩個任務);CLI `tsc` build 後
`node dist/index.js creds --help`/`creds replace --help` 手動核對指令走線正確。
**端到端真實驗證**leo21c `arcrun-cypher-executor`):
`POST /credentials`T5 既有路徑)建一筆 → `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)。