1d19d46161
- 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>
342 lines
35 KiB
Markdown
342 lines
35 KiB
Markdown
# Credential Store 遷移 SDD — KV → D1(目錄)+ CF Workers per-script Secrets(密文)
|
||
|
||
> 建立:2026-06-29 by arcrun CC|對應 issue:Arcrun#13(優先序 3)|決策:leo 2026-06-29 拍板(D19)
|
||
> **修訂 2026-07-03(T1.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 不存密文)設計不動**。D21(2026-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 的 A(telegram 一致性,已做)/ C(友善前門)/ Q1(acr parts)不受影響。
|
||
> 詞彙:component=TinyGo WASM|recipe=http_request+固定設定|workflow=多步 YAML|auth-recipe=credential 怎麼注入。
|
||
|
||
---
|
||
|
||
## 0. leo 的最終決策(D19,一句話)
|
||
|
||
**「擁有目錄,不擁有內容物」**:arcrun 持有 credential 的**目錄**(看得到存了哪些、最後何時用),但**不持有、也讀不回密文明文**——連 owner 自己的界面都讀不到值。
|
||
|
||
動機(leo 親述):用戶 API key 外泄被盜刷的經濟損失 + 商譽風險(「就算不是我們過失也會怪在我們身上」)。**我們不持有明文、不持有金鑰 = 不背這個鍋。**
|
||
|
||
```
|
||
密文值(token / SA JSON / private key) → CF Workers per-script Secrets(CF 託管,API 唯寫,arcrun 拿不到明文)
|
||
credential 目錄(name/service/metadata) → 使用者自己的 D1(不含密文,只含指向 Workers Secrets 的 secret_ref=env var 名)
|
||
舊自管 ENCRYPTION_KEY + 密文存 KV → 廢掉(回填完成後);KV 降為暫存/熱讀快取,非真相源
|
||
界面 acr creds list → 讀 D1(顯示清單 + last_used),看得到但讀不回值
|
||
```
|
||
|
||
### n8n credentials UX(D19 定案)
|
||
- 界面顯示**很多 credentials**(名字 / 服務 / metadata / last_used)。
|
||
- **不能 read 既有值、不能 edit 既有值**(連 owner 都不行)。只能:
|
||
- **整筆 replace**(重貼新值覆蓋舊的)
|
||
- **delete**
|
||
- read 不回值「不是限制、是設計」——值在 Workers Secrets(API 唯寫,T1.5 spike 實證讀不回),arcrun 拿不到。
|
||
|
||
---
|
||
|
||
## 1. 現狀(已核實 code,2026-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 D1(D1 只有 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.sql(kbdb 同一顆 D1,IF 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, -- 對應 service(telegram / 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#2(curl/wrangler 原始輸出)。本節取代原「CF Secrets Store 產品」設計——T1 spike 實證該產品的 binding 部署時靜態宣告、`.get(ref)` 參數被忽略、REST 唯寫,不支援「runtime 依 D1 ref 動態查任意 secret」,整案曾停等重新設計;總管裁決轉向 Workers per-script Secrets,T1.5 四驗證全過後定案。
|
||
|
||
**已實證的 Workers per-script Secrets 形態(T1.5 spike,Arcrun#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-hosted(leo21c)**: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 Secrets(CF 端託管)。**arcrun TS 短暫經手明文**(記憶體中,不落地)。
|
||
> - 選項乙:保留 client 加密當「傳輸期保護」,但這需 arcrun 持有解密金鑰才能寫進 secrets 明文槽 → 又回到自管金鑰,違 D19。
|
||
> → 傾向**選項甲**(符合「不持有金鑰」),但「TS 短暫經手明文」需 leo 接受(mindset §7 誠實標:不是零接觸,是不落地、不持久、不持金鑰)。
|
||
|
||
### 2.5 讀取/注入流程(執行期 by ref)
|
||
|
||
> 依據:T1.5 spike 2026-07-03(Arcrun#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 Secrets(API `PUT` 同 ref 覆蓋或新 ref)+ 更新 D1 metadata | 只能 replace |
|
||
| `DELETE /credentials/:name` | 刪 D1 row + 刪 Workers Secrets secret(API `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 row:WASM 解密取明文 → `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-shim,host 端實作=`env[ref]`),注入仍在 WASM auth primitive。
|
||
- **rule 01 加解密**:自管 AES-GCM 路徑在回填後廢除;新家由 CF Workers Secrets 託管(arcrun 不持金鑰,管理 API 唯寫讀不回值)。
|
||
- **不新增 component**(mindset §1);不新增 service binding(rule 03 §3.1)。
|
||
- **薄殼(rule 07)**:CLI/MCP 只做「list 顯示 / replace 覆寫 / delete」介面轉換,secrets 能力在 API。
|
||
- **部署繞開 GitHub Actions**:D1 migration + secrets 寫入設定走 `acr init/update`(wrangler 直推),不掛 Actions;secrets 本體是掛在 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 spike:leo21c 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#2〕2026-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#2〕2026-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 Secrets(API `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 0(cli + 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 確認
|
||
對應 row(api_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/36(1 個 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` 可能導致**這兩把真實 credential(Google 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_at,DELETE 不讀值只刪)。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-executor)exit 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.4,leo 明示放行)。
|
||
|
||
> **每個 cred 操作跨 TS / WASM / host-function / 兩個 store,必端到端實測**(防再假綠,mindset §7)。原「T1 不通則整案停」已兌現一輪:T1 負結果 → 整案停 → 總管裁決轉向 → T1.5 全過 → **T2-T9 解凍**。備援(若施工再撞死路):codegen binding + 自動重部署(Arcrun#2 總管裁決的方向 1)。
|