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>
This commit is contained in:
-384
@@ -1,384 +0,0 @@
|
||||
# Credential Store 遷移 SDD — KV → D1(目錄)+ CF Workers per-script Secrets(密文)
|
||||
|
||||
> ⚠️ **歷史記錄(本卷 T1-T10 已全數完成,2026-07-20 收尾)**。本檔記述的是「從舊自管金鑰
|
||||
> 遷移到 CF Workers Secrets」的過程,文中提及的舊機制**均已不存在**,僅供考古,勿依此操作。
|
||||
> 現行做法見 `.claude/rules/01-tech-stack.md`「Credential 儲存規範」。
|
||||
|
||||
> 建立: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 確認)。
|
||||
- [x] T6 讀取/注入路徑:auth-dispatcher 改 D1 ref → `env[ref]` 取值(§2.5)+ 更新 last_used。
|
||||
〔2026-07-05 本機(有 TinyGo 0.40.1)接手,方案 A,leo 已拍板〕**接續 2026-07-04 [cloud-worker]
|
||||
揭露的正確落地方式**(cypher 先取值塞 payload,因 auth worker 讀不到 cypher 的 secrets):
|
||||
**WASM 端(T7 fallback 骨架,可獨立部署驗)**:
|
||||
- `registry/components/auth_static_key/main.go`:Input 加 `ResolvedSecrets map[string]string`;
|
||||
`authenticate` 的解密迴圈與 `handleResolveCredentials` 都改為「`input.ResolvedSecrets[key]`
|
||||
有值就用,沒有才 fallback 舊 `kvGet`+`cryptoDecrypt`」。default(此欄 nil)行為完全等於遷移前舊碼。
|
||||
- `registry/components/auth_service_account/main.go`:SA JSON 取得同型(有 resolved 就用,沒有才舊路徑)。
|
||||
- `tinygo build -target=wasi` 兩支皆通過(static_key 1.13MB / service_account 1.18MB,在 2MB 限制內),
|
||||
copy 到 `.component-builds/{name}/component.wasm`。
|
||||
**TS 端(T6 主路徑)**:`cypher-executor/src/actions/auth-dispatcher.ts` 新增
|
||||
`resolveSecretsFromNewHome(env, apiKey, names)`:查 D1 `credentials`(api_key+name IN names)拿
|
||||
`secret_ref` → `createArcrunHostFunctions(env, apiKey).secret_get(ref)`(T4,host 端 = `env[ref]`)
|
||||
取明文 → 組 `{name:value}` map,**取不到值的 name 缺席(不放空字串)** 讓 WASM fallback(T7)→
|
||||
取到值的 name `UPDATE credentials SET last_used_at`。`tryAuthDispatch` 取 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 0;`vitest 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 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 改動 + 重新編譯部署。
|
||||
- [x] 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 端此改動可獨立部署、零行為改變地驗「沒弄壞既有」。
|
||||
- [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 帳號驗證通過,非模擬。
|
||||
- [x] T10 回填驗證 + 觀察期 → 舊自管金鑰路徑全數移除(§4.4,leo 明示放行)。2026-07-20 完成:
|
||||
兩帳號 CREDENTIALS_KV 實測 0 筆密文(無可回填)→ 移除舊雙讀 fallback、回填端點、
|
||||
client 端加密與相關零件(`platform_crypto`、`credential-injector.ts`、`register.ts`)。
|
||||
`crypto_decrypt` host function 保留成永遠回失敗的 stub(現役三個 `auth_*` .wasm 仍宣告
|
||||
該 import,缺項會讓 WASM instantiate 失敗);三個零件重編後即可刪除。
|
||||
**本卷至此全數完成,屬歷史記錄。**
|
||||
|
||||
> **每個 cred 操作跨 TS / WASM / host-function / 兩個 store,必端到端實測**(防再假綠,mindset §7)。原「T1 不通則整案停」已兌現一輪:T1 負結果 → 整案停 → 總管裁決轉向 → T1.5 全過 → **T2-T9 解凍**。備援(若施工再撞死路):codegen binding + 自動重部署(Arcrun#2 總管裁決的方向 1)。
|
||||
-214
@@ -1,214 +0,0 @@
|
||||
# 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 設定。**同機制,只是來源位置不同。**
|
||||
|
||||
### 明確「保留、不可刪」
|
||||
1. **`acr creds push` 保留** —— 手動推單一 credential 仍合法(只是不再是唯一/被迫的路徑)。
|
||||
2. **最終 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)
|
||||
1. **`acr parts` 移除 5 個 recipe 的 hardcode**(parts.ts:197-261),只留**真零件**(logic/data/ai/http_request/cron…=WASM,PR-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 + 實測 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_key` WASM `kv_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.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.yaml`(C)。
|
||||
|
||||
---
|
||||
|
||||
## 3. 遷移步驟(A 已做;B/C 是設計變更,誠實標工作量)
|
||||
|
||||
### Phase A — telegram/line_notify/kbdb 一致性(本次已做,commit 90777e3,未 push/merge)
|
||||
- [x] `auth-recipe-seeds.ts` 補 telegram(inject.path bot_token)/ line_notify(header Bearer)/ kbdb(header Bearer),形態取自 prod。
|
||||
- [x] `recipes.ts` `AuthInjectSpec` 補 `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 改為:**
|
||||
1. **低敏感 credential 維持 KV**(現狀 CREDENTIALS_KV,AES-GCM)。`acr creds list` 已可用(`GET /credentials` KV.list)。
|
||||
2. **(可選、後續)高敏感(service account JSON/private key)→ Cloudflare Secrets Store**(credential_parts.md §8.2,KV 只存 ref)。唯一值得的儲存升級,與拍板不衝突。
|
||||
3. **不新增 D1 credentials 表、不新增 `cred_get` host function、不做 KV→D1 migration**——這些原計畫取消(好處 KV 已有)。
|
||||
4. 若 leo 仍要 D1:屬新需求(須更新 2026-06-07 拍板)。即便如此,密文本體仍留 KV/Secrets Store,D1 只放非機密 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 端加密 → 存進 D1(Phase B 的 store)。
|
||||
- 仍保留現有「從 credentials.yaml 讀值」模式(向後相容,不刪)。yaml 變成可選輸入源之一,不再是被迫。
|
||||
2. **對稱**:本機讀 `.env`(`config.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 層按名指定**:
|
||||
|
||||
```yaml
|
||||
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:telegram` 的 `inject.path.bot_token` 輸出 `auth_path`
|
||||
→ `makeRecipeRunner` 把 endpoint `{{auth.bot_token}}` 換成真 token → POST `https://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):
|
||||
1. **守 leo 自己的拍板**:2026-06-07「credential 留 KV」是有理由的設計(Redis vs PostgreSQL 分工),非偷懶。推翻它要有新理由,目前沒有。
|
||||
2. **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。
|
||||
3. **搬 D1 成本高且踩鐵律邊界**:要新 host function `cred_get`(WASM 改)、一次性 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.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 function `cred_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 長期規格)。
|
||||
@@ -1,255 +0,0 @@
|
||||
---
|
||||
status: paused
|
||||
superseded_by: ""
|
||||
---
|
||||
|
||||
# Design Document: Credential Primitives TS → WASM 改寫
|
||||
|
||||
> ⚠️ **本檔的 credential 儲存/解密段落已過時**(`crypto_decrypt` 現為永遠回失敗的 stub,
|
||||
> credential 值改由 CF Workers Secrets 託管、經 `secret_get(ref)` 取用)。遷移已於
|
||||
> 2026-07-20 完成,見 `credential-store-migration.md`;現行做法見
|
||||
> `.claude/rules/01-tech-stack.md`「Credential 儲存規範」。以下解密相關敘述僅供考古。
|
||||
|
||||
## Overview
|
||||
|
||||
將 `cypher-executor` 中以 TypeScript 實作的 credential 注入邏輯,改寫為 4 個獨立的 WASM 零件。這是 `credential_parts.md` 長期規格的實現,不再是「未來 Phase」。
|
||||
|
||||
**動機**:TS 實作無法在地端(workerd)和邊緣端(Wazero)執行。WASM 零件跨 runtime 可攜,符合 u6u 三層部署架構。
|
||||
|
||||
**嚴格規範(richblack 2026-04-19 確認)**:cypher-executor TS **完全不實作**任何 credential / auth / template / JWT / 解密邏輯。所有業務邏輯必須在 TinyGo WASM 零件內。TS 僅負責 HTTP routing + 呼叫 WASM + host function 提供 runtime primitive(crypto.subtle / KV / fetch)。
|
||||
|
||||
---
|
||||
|
||||
## 現有 TS 實作(要刪除的)
|
||||
|
||||
| 檔案 | 功能 | 對應 WASM Primitive |
|
||||
|------|------|---------------------|
|
||||
| `credential-injector.ts` — `injectFromAuthRecipe()` | static_key template 展開 | `auth_static_key` |
|
||||
| `credential-injector.ts` — service_account 分支 | JWT signing + token exchange | `auth_service_account` |
|
||||
| `credential-injector.ts` — `decryptCredential()` | AES-GCM 解密 | host function(所有 primitive 共用) |
|
||||
| `credential-injector.ts` — `interpolateTemplate()` | `{{secret.KEY}}` 替換 | 內建在各 primitive |
|
||||
| `jwt-signer.ts` — `exchangeGoogleJwt()` | PEM→PKCS8→RS256→token | `auth_service_account` |
|
||||
| `component-loader.ts` — BUILTIN_API_RECIPES | gmail/telegram/line/gsheets 寫死邏輯 | 刪除,改用 auth recipe + `http_request` 零件 |
|
||||
| `credential-injector.ts` — BUILTIN_CREDENTIALS_MAP | 舊路徑 flat injection | 刪除,統一走 auth recipe |
|
||||
| `arcrun/credentials/` | 重複的 credentials Worker | 刪除,路由已在 cypher-executor |
|
||||
|
||||
---
|
||||
|
||||
## 4 個 WASM Primitive 設計
|
||||
|
||||
### 統一 I/O 介面(stdin/stdout JSON)
|
||||
|
||||
```
|
||||
stdin(Worker → WASM):
|
||||
{
|
||||
"action": "authenticate" | "needs_refresh" | "refresh" | "test",
|
||||
"api_key": "ak_xxx", // 租戶識別,用來組 KV key
|
||||
"service": "openai", // 對應 auth_recipe:{service}
|
||||
"request": { "method": "GET", "url": "/path", "headers": {}, "body": null }
|
||||
}
|
||||
|
||||
WASM 內部流程:
|
||||
1. recipeJSON = kv_get("auth_recipe:" + service)
|
||||
2. 依 recipe.required_secrets 逐一 kv_get("{api_key}:cred:{name}") → {encrypted, iv}
|
||||
3. secrets[name] = crypto_decrypt(encrypted, iv)
|
||||
4. (service_account)crypto_sign_rs256(jwt, pkcs8) + http_request 換 token
|
||||
5. 展開 recipe.inject 的 {{secret.X}} / {{runtime.X}} 模板
|
||||
|
||||
stdout(WASM → Worker):
|
||||
{
|
||||
"success": true,
|
||||
"auth_headers": { "Authorization": "Bearer xxx" },
|
||||
"auth_query": {},
|
||||
"auth_body": {},
|
||||
"runtime": { ... updated runtime state,供下次 refresh 用 }
|
||||
}
|
||||
```
|
||||
|
||||
### auth_static_key
|
||||
|
||||
**位置**:`arcrun/registry/components/auth_static_key/`
|
||||
**語言**:TinyGo 或 AssemblyScript
|
||||
|
||||
功能:
|
||||
1. 讀取 `recipe.inject.header/query/body` 模板
|
||||
2. 用 `secrets` 展開 `{{secret.KEY}}` 模板
|
||||
3. 回傳 `auth_headers` / `auth_query` / `auth_body`
|
||||
|
||||
涵蓋:~80% 服務(Bearer token, API Key, Basic Auth, custom header)
|
||||
|
||||
### auth_service_account
|
||||
|
||||
**位置**:`arcrun/registry/components/auth_service_account/`
|
||||
**語言**:TinyGo 或 AssemblyScript
|
||||
|
||||
功能:
|
||||
1. 從 `secrets.service_account_json` 解析 private key
|
||||
2. JWT signing(RS256:PEM→PKCS8→sign)
|
||||
3. POST token exchange endpoint → 取得 access_token
|
||||
4. 展開 `{{runtime.access_token}}` 模板
|
||||
|
||||
**crypto 考量**:
|
||||
- TinyGo 的 `crypto/rsa` + `crypto/x509` 支援有限
|
||||
- 若 TinyGo 不支援 RS256:使用 host function 讓 Worker 的 `crypto.subtle` 代簽
|
||||
- 或改用 AssemblyScript(有 as-crypto 套件)
|
||||
|
||||
### auth_oauth2(新建)
|
||||
|
||||
**位置**:`arcrun/registry/components/auth_oauth2/`
|
||||
|
||||
功能:
|
||||
1. `needs_refresh`:檢查 `runtime.expires_at` 是否過期
|
||||
2. `refresh`:用 `runtime.refresh_token` + `secrets.client_secret` 換新 token
|
||||
3. `authenticate`:展開 `{{runtime.access_token}}` 到 headers
|
||||
|
||||
### auth_mtls(新建)
|
||||
|
||||
**位置**:`arcrun/registry/components/auth_mtls/`
|
||||
|
||||
功能:
|
||||
1. 從 `secrets` 讀取 client cert + key
|
||||
2. 回傳 TLS 設定(由 Worker runtime 執行實際 mTLS handshake)
|
||||
|
||||
---
|
||||
|
||||
## cypher-executor 改動
|
||||
|
||||
### 保留(TS routing 層)
|
||||
|
||||
- `routes/credentials.ts` — HTTP CRUD for credentials(接收加密的 payload)
|
||||
- `routes/recipes.ts` — HTTP CRUD for auth recipes
|
||||
- `routes/auth.ts` — OAuth flow routing
|
||||
- `graph-executor.ts` — workflow 執行排程
|
||||
- `lib/wasi-shim.ts` — WASM runtime + host functions(加解密 / KV / 簽章 / HTTP 實際由 `crypto.subtle` / env binding / fetch 執行,但**呼叫時機由 WASM 決定**)
|
||||
|
||||
### 修改
|
||||
|
||||
- `actions/credential-injector.ts` — **整檔刪除**,改為新檔 `actions/auth-dispatcher.ts`(約 30 行):
|
||||
1. 查 `resolveAuthRecipe(componentId)` 取得 `primitive` 名稱(static_key / service_account / oauth2 / mtls)
|
||||
2. 呼叫對應的 auth primitive Worker(見下「呼叫方式」)
|
||||
3. 送 `{ action, api_key, service, request }`(**不送 secrets、不送 recipe plaintext**)
|
||||
4. WASM 透過 host function 自行 `kv_get` 讀 recipe + 加密 secret,`crypto_decrypt` 解密
|
||||
5. 讀回傳 → 合併 `_auth_headers` / `_auth_query` / `_auth_body` 進 ctx
|
||||
|
||||
**呼叫方式(Phase 7,2026-06-06 演化)**:原設計走 `fetch(wasmWorkerUrl(auth_{primitive}, WORKER_SUBDOMAIN))`(workers.dev 公網 URL)。但壓測階段 11 證實:**self-hosted** cypher(`arcrun-cypher-executor.{sub}.workers.dev`)用 `fetch()` 打**同 `{sub}.workers.dev` zone** 的 auth worker,CF 回 **1042**(官方 docs:「fetch from another Worker on the **same zone**」)。官方不踩是因官方 cypher 在自訂域 `cypher.arcrun.dev`、打 `*.workers.dev` 屬**跨 zone**(非官方有 flag)。token/解密鏈本身正常,唯一卡點是同 zone fetch。
|
||||
- **解法 = `global_fetch_strictly_public` compatibility flag**(cypher wrangler.toml),auth-dispatcher **維持原本 `fetch(workers.dev)` 不改**。此 flag 讓 `fetch()` 走公網「前門」→ same-zone fetch 也通(官方 docs:「Worker-to-Worker fetch 可用 service binding **或** `global_fetch_strictly_public` flag」)。
|
||||
- **為何選 flag 不選 service binding**:service binding 靜態、加/改要重 deploy cypher(官方 docs);flag 一行解、用戶無感、不動 service binding 禁令。先做了 service binding(A)後 richblack 拍板廢、改 flag(B)。
|
||||
- **flag 安全(查證官方 docs)**:唯一副作用「Worker fetch 自己 hostname → self-loop」;cypher 只打外部 API + sibling auth worker(皆非自己 hostname)→ 不 self-loop。
|
||||
- **官方/self-host 共用同一份 toml**:官方 cypher 本就跨 zone(cypher.arcrun.dev → workers.dev),加 flag 行為不變;self-host 同 zone 被修好。`stripOfficialOnlyBindings()` 不碰 `compatibility_flags`,self-hosted 部署自動帶上。
|
||||
|
||||
- `lib/component-loader.ts` — **刪除 `BUILTIN_API_RECIPES`**(含 http_request / gmail / telegram / line_notify / google_sheets 的 TS 實作),全部改走 WASM runner。每個 `.wasm` 零件都已編譯並以獨立 Worker 部署(`{canonical-id-kebab}.arcrun.dev`)。loader 新增的「WASM runner」路徑就是「canonical_id → HTTP URL 查表後 fetch」,**不做** WASM instantiate。
|
||||
- **R2 動態注入 WASM 路徑作廢**(richblack 2026-04-19 確認:CF workerd 無法以 R2 物件臨時 instantiate WASM)。用戶自製零件(Phase 5)同樣走「產生獨立 Worker」流程,不從 R2 讀。
|
||||
|
||||
### 刪除
|
||||
|
||||
- `lib/jwt-signer.ts` — 整檔刪除,RS256 簽章移入 `auth_service_account` WASM(透過 host function `crypto_sign_rs256`)
|
||||
- `credential-injector.ts` 整檔刪除(見上)
|
||||
- `component-loader.ts` 的 `BUILTIN_API_RECIPES` 整段刪除
|
||||
- `BUILTIN_CREDENTIALS_MAP` 已在 `credential-injector.ts` 內,隨檔一併刪
|
||||
|
||||
---
|
||||
|
||||
## Host Functions(WASM ↔ Worker 的橋接)
|
||||
|
||||
auth primitive WASM 需要呼叫外部能力時,透過 host function。全部放 `u6u` namespace。**錯誤回傳非零 uint32;成功 = 0 且把結果寫入 `outPtr` 指向的 buffer**。
|
||||
|
||||
| Host Function | TinyGo 簽章 | 用途 |
|
||||
|---|---|---|
|
||||
| `http_request` | `(urlPtr/Len, methodPtr/Len, headersPtr/Len, bodyPtr/Len, outPtr, outLenPtr) uint32` | HTTP 請求(已實作) |
|
||||
| `kv_get` | `(keyPtr, keyLen, outPtr, outLenPtr) uint32` | 讀 KV。Worker 依 key 前綴路由到 `CREDENTIALS_KV` / `RECIPES` |
|
||||
| `crypto_decrypt` | `(encPtr, encLen, ivPtr, ivLen, outPtr, outLenPtr) uint32` | AES-GCM 解密。encryption key 由 Worker 從 `env.ENCRYPTION_KEY` 內部讀取,**永遠不暴露給 WASM** |
|
||||
| `crypto_sign_rs256` | `(dataPtr, dataLen, pkcs8Ptr, pkcs8Len, outPtr, outLenPtr) uint32` | Worker 用 `crypto.subtle.sign('RSASSA-PKCS1-v1_5' + SHA-256)`;private key 以 PKCS8 bytes 傳入 |
|
||||
|
||||
這些 host function 在 `lib/wasi-shim.ts` 中以 WASI import 提供。
|
||||
|
||||
### 安全邊界
|
||||
|
||||
- `ENCRYPTION_KEY` 只在 `crypto_decrypt` host function 內部使用,**絕不**經 stdin / 回傳值 / 任何路徑傳給 WASM
|
||||
- `api_key` 經 stdin 傳入 WASM(讓 WASM 自己組 `{api_key}:cred:{name}` KV key)
|
||||
- `kv_get` 在 Worker 側檢查 key 前綴:
|
||||
- `auth_recipe:*` → 讀 `RECIPES`
|
||||
- `{api_key}:cred:*` → 讀 `CREDENTIALS_KV`,且 `{api_key}` 必須等於 stdin 傳入的 api_key(防越權)
|
||||
- 其他前綴 → 回傳錯誤
|
||||
|
||||
---
|
||||
|
||||
## 關於解密位置
|
||||
|
||||
採用**方案 B(唯一方案)**:WASM 透過 host function `crypto_decrypt()` 自行解密。
|
||||
|
||||
- cypher-executor TS 完全不解密、不知道 plaintext
|
||||
- `ENCRYPTION_KEY` 永遠留在 Worker host function 內
|
||||
- WASM 知道要解哪份 ciphertext(經 `kv_get` 讀到的 `{encrypted, iv}`),但拿不到 encryption key
|
||||
- 這樣 TS 層完全沒有零件業務邏輯,符合 CLAUDE.md §禁止行為 1/6
|
||||
|
||||
(歷史註記:曾規劃方案 A「TS 先解密再送 stdin」,已廢棄 — 違反「TS 不得實作零件邏輯」。)
|
||||
|
||||
---
|
||||
|
||||
## §8 `{{credential.X}}` 用戶面注入語法(2026-06-10,richblack 確認 change)
|
||||
|
||||
### 問題(壓測實證的 401 根因)
|
||||
|
||||
Haiku 自主壓測時自然地在 workflow node.data 寫 `{{credential.openai_key}}` 引用已存的 credential,
|
||||
結果打目標 API 回 **401**;只有把 token **明文硬編碼**進 workflow 才 200。
|
||||
|
||||
**根因(系統客觀證據,三條 template 展開路徑都不認 `credential.` namespace)**:
|
||||
1. `graph-executor.ts` `interpolateString()`(node.data 展開)只認「context 內已存在的 key path」
|
||||
(`{{input.X}}` / `{{data.Y}}` / 任意 dot path),**無 `credential` namespace** → `{{credential.X}}`
|
||||
展不開 → header 帶字面值 `Bearer {{credential.X}}` 或空 → 401。
|
||||
2. auth recipe `inject` + auth primitive WASM 只認 `{{secret.X}}` / `{{runtime.X}}`,那條是 auth recipe
|
||||
**自動注入**路徑(recipe 宣告 `auth_service` 才觸發),不是用戶在 node.data 直接引用的入口。
|
||||
|
||||
**正解(auth recipe 自動注入)其實存在,但用戶不知道、直覺會寫 `{{credential.X}}`** → 這是
|
||||
**設計缺口**(缺一個直覺的用戶面 credential 引用入口),不是單純 bug。
|
||||
|
||||
### 決策:讓 `{{credential.X}}` 真的能用(richblack 2026-06-10)
|
||||
|
||||
新增「用戶在 workflow node.data 直接寫 `{{credential.NAME}}` → 自動解密回填」這條入口。
|
||||
|
||||
**硬約束(rule 02 §2.2)**:解密**絕不能**在 graph-executor TS 做(hook 擋 `crypto.subtle.decrypt`,
|
||||
且違反「TS 不實作 credential 邏輯」)。故 `{{credential.X}}` 的解密**復用既有 auth_static_key WASM**。
|
||||
|
||||
### 設計:auth_static_key 新增 `resolve_credentials` action
|
||||
|
||||
| 角色 | 職責 |
|
||||
|------|------|
|
||||
| graph-executor.ts(薄 routing) | 偵測 node.data 內 `{{credential.NAME}}` → 收集 names → 呼叫 auth_static_key WASM `resolve_credentials` → 拿回明文 → 回填 node.data。**不解密、不碰 ENCRYPTION_KEY**。 |
|
||||
| auth_static_key WASM(新 action) | 收 `{action:"resolve_credentials", api_key, names:[...]}` → 對每個 name `kv_get("{api_key}:cred:{name}")` + `crypto_decrypt` → 回 `{success, credentials:{name: plaintext}}`。**不查 recipe**(與 authenticate 分流)。 |
|
||||
|
||||
**stdin/stdout(resolve_credentials)**:
|
||||
```
|
||||
stdin: { "action": "resolve_credentials", "api_key": "ak_xxx", "names": ["openai_key", "..."] }
|
||||
stdout: { "success": true, "credentials": { "openai_key": "sk-...", ... } }
|
||||
缺某 name → success:false + error 指明缺哪個(誠實,不假綠)
|
||||
```
|
||||
|
||||
**展開時機(graph-executor)**:在 node.data interpolate(現有 `interpolateData`)**之後**、
|
||||
node 執行**之前**插一步:掃描展開結果裡殘留的 `{{credential.NAME}}` → 走 WASM resolve → 回填。
|
||||
放在 auth recipe 自動注入(`tryAuthDispatch`)**之外**,兩條入口並存但不重疊(一個是用戶顯式引用、
|
||||
一個是 recipe 宣告 `auth_service` 隱式注入)。
|
||||
|
||||
**安全邊界(同 authenticate)**:
|
||||
- `crypto_decrypt` host function 內部用 `ENCRYPTION_KEY`,**永不暴露給 WASM / 不經 graph-executor**。
|
||||
- `kv_get` host 側檢查 `{api_key}:cred:*` 的 api_key 必須等於 stdin api_key(防越權,現有機制)。
|
||||
- graph-executor 只傳 `names`(明文 credential 名)+ api_key,**不傳、不見** ciphertext / plaintext key。
|
||||
|
||||
### 為何不選「新建 credential_resolve 零件」
|
||||
|
||||
auth_static_key 已具備 `kvGet` + `cryptoDecrypt` 全部能力,新建零件 = 功能重疊 + 多一個 Worker
|
||||
部署成本。加一個 action 最貼近現有架構(richblack 2026-06-10 拍板)。
|
||||
|
||||
### 不做
|
||||
|
||||
- ❌ 不在 graph-executor 解密(rule 02 §2.2)
|
||||
- ❌ 不改 credential KV key 格式(仍 `{api_key}:cred:{name}`)
|
||||
- ❌ 不碰 auth recipe 自動注入路徑(`tryAuthDispatch` / `{{secret.X}}` 不變)
|
||||
|
||||
---
|
||||
|
||||
## 不做的事
|
||||
|
||||
- ❌ 不改 recipe YAML schema — 沿用現有格式
|
||||
- ❌ 不改 KV 儲存結構 — `auth_recipe:{service}` / `{api_key}:cred:{name}` 不變
|
||||
- ❌ 不改 SDK API — SDK 仍是 HTTP thin wrapper
|
||||
- ❌ 不建新的 Worker — 在 cypher-executor 內完成(§8 復用 auth_static_key,不新建零件)
|
||||
@@ -1,240 +0,0 @@
|
||||
# Implementation Tasks: Credential Primitives TS → WASM
|
||||
|
||||
> ⚠️ **歷史記錄**:文中 credential 加解密相關的完成記錄描述的是舊機制,該機制已於
|
||||
> 2026-07-20 完全移除(見 `credential-store-migration.md` T10)。僅供考古,勿依此操作。
|
||||
|
||||
**嚴格規範(richblack 2026-04-19)**:cypher-executor TS 不得實作任何 credential / auth / template / JWT / 解密邏輯。全部走 TinyGo WASM + host functions(方案 B)。
|
||||
|
||||
**封測狀態**:推遲(richblack 2026-04-19 決定)。先完成 Phase 1-3 清除違規 TS,再啟動封測。
|
||||
|
||||
---
|
||||
|
||||
## Phase 0:核心合併(u6u-core → arcrun)
|
||||
|
||||
- [x] 0.1 把 `u6u-core/builtins/` 搬到 `arcrun/builtins/`
|
||||
- [x] 0.2 確認 `arcrun/registry/components/` 21 個零件的 contract.yaml 完整(21/21)
|
||||
- [x] 0.3 刪除 `arcrun/credentials/` 整個目錄(重複,credential route 已在 cypher-executor)
|
||||
- [x] 0.4 更新 `arcrun/cypher-executor/wrangler.toml`:確認 CREDENTIALS_KV binding 存在
|
||||
- [x] 0.5 刪除 `matrix/u6u-core/` 整個目錄(2026-04-19 完成,只剩 credentials/ 已被 cypher-executor 取代)
|
||||
- [x] 0.6 在 `cypher-executor/src/lib/wasi-shim.ts` 新增 host functions:
|
||||
- `u6u.kv_get(keyPtr, keyLen, outPtr, outLenPtr) uint32` — 依 key 前綴路由到 `CREDENTIALS_KV` / `RECIPES`,越權檢查 api_key
|
||||
- `u6u.crypto_decrypt(encPtr, encLen, ivPtr, ivLen, outPtr, outLenPtr) uint32` — 用 `env.ENCRYPTION_KEY` + `crypto.subtle` AES-GCM 解密;key 不暴露給 WASM
|
||||
- `u6u.crypto_sign_rs256(dataPtr, dataLen, pkcs8Ptr, pkcs8Len, outPtr, outLenPtr) uint32` — `crypto.subtle.sign('RSASSA-PKCS1-v1_5' + SHA-256)`
|
||||
- 2026-04-19 完成:wasi-shim.ts 新增 `createArcrunHostFunctions(env, apiKey)` factory,集中 AES-GCM 解密 + RSA sign + KV 前綴路由越權檢查。WASI imports 的 u6u namespace wiring 本來就已接好(只是當時沒有實作 factory)。typecheck 通過。
|
||||
- [x] 0.7 在 `cypher-executor/src/lib/component-loader.ts` 新增 WASM runner 路徑:
|
||||
- 所有 WASM 零件(含 auth primitive、API 零件、未來用戶自製)一律走 HTTP URL(`{canonical-id-kebab}.arcrun.dev`)到獨立 Worker
|
||||
- **R2 動態注入路徑作廢**(richblack 2026-04-19 確認:CF workerd 不支援以 R2 物件臨時 instantiate WASM;用戶自製零件同樣走「產生獨立 Worker」流程,不走 R2)
|
||||
- cypher-executor 本身**不做** WASM instantiate,也不直接呼叫 `createArcrunHostFunctions`;那個 factory 是**零件 Worker 側**(`.component-builds/{name}/src/index.ts`)用的,在 Phase 1 建立 auth_static_key Worker 時接上
|
||||
- 2026-04-19 完成:`component-loader.ts` 新增 `WASM_HTTP_RUNNER_IDS`(10 個 canonical_id,6 個 API 零件 + 4 個 auth primitive)+ `wasmWorkerUrl()` URL 慣例輔助函數;解析鏈新增為第 8 層(放在 `BUILTIN_API_RECIPES` fallback 之後,避免 Phase 3 尚未完成時 API 零件 Worker 未部署造成 404;Phase 3 刪除 `BUILTIN_API_RECIPES` 後,API 零件會自然落到此層)。auth primitive 從此層進入。`tsc --noEmit` 通過。
|
||||
|
||||
---
|
||||
|
||||
## Phase 1:auth_static_key WASM(優先,涵蓋 80% 服務)
|
||||
|
||||
方案 B:WASM 自行讀 KV + 解密,TS 不碰 plaintext。
|
||||
|
||||
- [x] 1.1 建立 `arcrun/registry/components/auth_static_key/` 目錄
|
||||
- [x] 1.2 寫 `component.contract.yaml`(input: `{action, api_key, service, request}` → output: `{success, auth_headers, auth_query, auth_body, runtime}`)
|
||||
- [x] 1.3 實作 `main.go`(TinyGo):
|
||||
- 宣告 host imports:`kv_get` / `crypto_decrypt`(static_key 不需要 http_request)
|
||||
- 從 stdin 讀 `{action, api_key, service}`
|
||||
- `kv_get("auth_recipe:" + service)` → recipe JSON → 驗證 `primitive == "static_key"`
|
||||
- 對每個 non-optional `recipe.required_secrets`:`kv_get("{api_key}:cred:{name}")` → `{encrypted, iv}` → `crypto_decrypt` → plaintext
|
||||
- 展開 `{{secret.X}}` / `{{runtime.X}}` 模板於 `inject.header/query/body`;未知 key 展空字串(與 TS parity);其他 namespace 的 `{{...}}` 原樣保留
|
||||
- 輸出 stdout JSON `{success, auth_headers, auth_query, auth_body, runtime}`
|
||||
- [x] 1.4 `tinygo build -o auth_static_key.wasm -target=wasi main.go` — 2026-04-19 編譯通過(1.1MB,在 contract 限制 2MB 內)
|
||||
- [🔄] 1.5 建立 `.component-builds/auth_static_key/`(用 `component-worker-template`)並部署到 `auth-static-key.arcrun.dev`
|
||||
- 2026-04-20 完成**建置**部分:`.component-builds/auth_static_key/{wrangler.toml, package.json, tsconfig.json, src/index.ts, component.wasm}` 全數到位
|
||||
- 方案 A:`src/index.ts` 直接 import `../../../cypher-executor/src/lib/wasi-shim` 的 `createWasiShim` + `createArcrunHostFunctions`(以 `ArcrunHostEnv` 結構型別相容);AES 解密邏輯仍只存在於 wasi-shim.ts 一處(rule 02 §2.2)
|
||||
- 綁同組 KV:CREDENTIALS_KV (e7f4320f88d343f187e35e3543dd74c9) / RECIPES (9cf9db905c6241f78503199e58b2ffe0);ENCRYPTION_KEY 走 `wrangler secret put`
|
||||
- `wrangler deploy --dry-run` 通過(1192 KiB, 419 KiB gzip);實際 `wrangler deploy` + `secret put ENCRYPTION_KEY` 留給 richblack 執行
|
||||
- [x] 1.6 建立 `auth-dispatcher.ts`(取代 `credential-injector.ts`):查 auth recipe → HTTP POST 到對應 auth primitive URL → 合併 `_auth_headers` 進 ctx
|
||||
- 2026-04-20 完成:`cypher-executor/src/actions/auth-dispatcher.ts` 新建,export `tryAuthDispatch(componentId, input, env, apiKey)`
|
||||
- 流程:查 `resolveAuthRecipe` → primitive 在 `SUPPORTED_PRIMITIVES`(目前只有 `static_key`)→ fetch `wasmWorkerUrl('auth_static_key')` → 合併 `_auth_headers/_auth_query/_auth_body`
|
||||
- 自引用防護:`AUTH_PRIMITIVE_IDS` set 排除 4 個 `auth_*` componentId
|
||||
- `wasmWorkerUrl` 從 `component-loader.ts` export 出來共用
|
||||
- `graph-executor.ts` 改為:先試 `tryAuthDispatch`(新路徑),沒命中 fallback 到舊 `injectCredentials`(Phase 1.9 刪)
|
||||
- 檢查過 auth-dispatcher.ts 無 `crypto.subtle` / `interpolate` / `{{secret.` / hard-code API URL,符合 rule 02 §2.2
|
||||
- `tsc --noEmit` 通過
|
||||
- [ ] 1.7 端對端測試:openai recipe → 成功注入 `Authorization: Bearer <openai_key>`
|
||||
- [ ] 1.8 端對端測試:twilio recipe(Basic Auth)→ 成功注入
|
||||
- [ ] 1.9 **刪除 `credential-injector.ts` 整檔**(`decryptCredential` / `decryptSecrets` / `interpolateTemplate` / `BUILTIN_CREDENTIALS_MAP` 全刪)
|
||||
|
||||
---
|
||||
|
||||
## Phase 2:auth_service_account WASM
|
||||
|
||||
- [🔄] 2.1 建立 `arcrun/registry/components/auth_service_account/` 目錄
|
||||
- [🔄] 2.2 寫 `component.contract.yaml`
|
||||
- [🔄] 2.3 實作 `main.go`:
|
||||
- 從 stdin 讀 `{api_key, service}` + `kv_get` 拿 recipe + 解密 SA JSON
|
||||
- 解析 SA JSON 取 `client_email` / `private_key`(PEM)
|
||||
- PEM → PKCS8 bytes(純 Go,base64 decode + 去 header/footer)
|
||||
- 組 JWT header + payload(base64url),呼叫 `crypto_sign_rs256(signingInput, pkcs8)` 拿 signature
|
||||
- 組完整 JWT → `http_request` POST `token_uri` → 拿 `access_token`
|
||||
- 展開 `{{runtime.access_token}}` 模板
|
||||
- [x] 2.4 `tinygo build -o auth_service_account.wasm -target=wasi main.go` — 2026-04-20 編譯通過(1.1MB,在 contract 限制 2MB 內)
|
||||
- [x] 2.5 建立 `.component-builds/auth_service_account/` 並部署到 `auth-service-account.arcrun.dev`
|
||||
- 2026-04-20 完成**建置**部分:`.component-builds/auth_service_account/{wrangler.toml, package.json, tsconfig.json, src/index.ts, component.wasm}` 全數到位
|
||||
- 方案 A:`src/index.ts` 重用 `createArcrunHostFunctions` 提供 kv_get/crypto_decrypt/crypto_sign_rs256,**額外加 `http_request` host function**(token exchange 用,非 crypto 不受 §2.2 約束)。http_request 直接回 response body 原文(WASM 端 json.Unmarshal 找 access_token)
|
||||
- 綁同組 KV:CREDENTIALS_KV / RECIPES;ENCRYPTION_KEY 走 `wrangler secret put`
|
||||
- `wrangler deploy --dry-run` 通過(1248 KiB, 440 KiB gzip);實際 `wrangler deploy` + `secret put ENCRYPTION_KEY` 留給 richblack 執行
|
||||
- `auth-dispatcher.ts` 的 `SUPPORTED_PRIMITIVES` 加入 `'service_account'`,workflow 用 google SA recipe 會自動走新 WASM 路徑
|
||||
- [ ] 2.6 端對端測試:google_sheets_sa recipe → 成功取得 access_token → 注入 header
|
||||
- [x] 2.7 **刪除 `lib/jwt-signer.ts` 整檔** — 2026-04-20 完成
|
||||
- `cypher-executor/src/lib/jwt-signer.ts` 已刪除(RS256 JWT 邏輯移入 `auth_service_account.wasm`)
|
||||
- `credential-injector.ts` 原 line 23 `import { exchangeGoogleJwt }` 移除
|
||||
- `credential-injector.ts` 原 line 140-150 service_account 分支改為 throw(任何 service_account recipe 已被 auth-dispatcher 攔截;這條 TS fallback 若被觸發即表架構錯亂,直接爆錯比沈默解密更安全)
|
||||
- `cypher-executor` tsc --noEmit 通過
|
||||
|
||||
---
|
||||
|
||||
## Phase 3:清理 component-loader 的 TS 實作(全刪)
|
||||
|
||||
目標:`BUILTIN_API_RECIPES` 整段刪除,所有服務走 WASM runner(HTTP URL 路徑)。
|
||||
|
||||
- [x] 3.1 確認 `http_request.wasm` / `gmail.wasm` / `telegram.wasm` / `line_notify.wasm` / `google_sheets.wasm` 都在 `registry/components/` 且可執行 — 2026-04-20 驗證 6 個(含 cron)全數存在,main.go + .wasm 齊備
|
||||
- [x] 3.2 確認上述零件 Worker 都已部署(`{name}.arcrun.dev` 可用) — 2026-04-20 完成**建置**部分
|
||||
- 6 個 Worker 建置到位:`.component-builds/{http_request, gmail, telegram, line_notify, google_sheets, cron}/{wrangler.toml, package.json, tsconfig.json, src/index.ts, component.wasm}`
|
||||
- 方案 A:5 個需 http_request 的零件(http_request/gmail/telegram/line_notify/google_sheets)`src/index.ts` 共用模板;cron 是純計算不註冊 host function
|
||||
- 全部透過 `createWasiShim` 複用 cypher-executor/src/lib/wasi-shim.ts(rule 02 §2.2 邊界)
|
||||
- 6 個 `wrangler deploy --dry-run` 全通過(~1.17 MB / ~413 KB gzip 每個);實際 `wrangler deploy` 留給 richblack 執行
|
||||
- [x] 3.3 `component-loader.ts` 的內建路徑改為查對應 Worker URL → HTTP POST — 2026-04-20 完成
|
||||
- 原本第 7 層是 `BUILTIN_API_RECIPES` fallback、第 8 層是 `WASM_HTTP_RUNNER_IDS` (HTTP URL);兩層合併為第 7 層 `WASM_HTTP_RUNNER_IDS` 直接走 `makeHttpRunner(wasmWorkerUrl(id))`
|
||||
- 解析鏈新編號 1-8,順序不變(外部 URL → recipe hash → component hash → R2 → Service Binding → auth recipe runner → WASM HTTP runner → 找不到)
|
||||
- [x] 3.4 **刪除 `BUILTIN_API_RECIPES` 整個 Record**(`http_request` / `gmail` / `telegram` / `line_notify` / `google_sheets` / `cron` 的 TS 實作全刪) — 2026-04-20 完成
|
||||
- `cypher-executor/src/lib/component-loader.ts` 原 line 253-326 `BUILTIN_API_RECIPES` 常數 + fallback lookup 全刪(約 80 行)
|
||||
- 全域搜尋確認:`gmail.googleapis.com/...messages/send` / `api.telegram.org/bot.*sendMessage` / `sheets.googleapis.com/v4/spreadsheets` / `notify-api.line.me/api/notify` 在 cypher-executor TS 中已不存在(auth-recipe-seeds.ts 的 `base_url` 是 recipe 資料欄位,不是 hard-coded API call)
|
||||
- `cypher-executor` tsc --noEmit 通過
|
||||
- [ ] 3.5 端對端測試:workflow 用 gmail auth recipe + gmail.wasm Worker → 成功發信
|
||||
- [ ] 3.6 端對端測試:workflow 用 http_request.wasm Worker + auth_static_key 注入 → 成功呼叫任意 API
|
||||
|
||||
---
|
||||
|
||||
## Phase 4:auth_oauth2 + auth_mtls WASM(封測後)
|
||||
|
||||
- [ ] 4.1 建立 `arcrun/registry/components/auth_oauth2/`
|
||||
- [ ] 4.2 實作:`needs_refresh` / `refresh` / `authenticate` 三個 action
|
||||
- [ ] 4.3 建立 `arcrun/registry/components/auth_mtls/`
|
||||
- [ ] 4.4 實作:輸出 TLS cert/key(實際 mTLS handshake 由 Worker runtime 執行,WASM 無法做 socket)
|
||||
|
||||
---
|
||||
|
||||
## Phase 5:封測啟動門檻 — 核心穩定驗證
|
||||
|
||||
**全部通過才能啟動封測**。
|
||||
|
||||
- [ ] 5.1 所有 20 個 auth recipe seed 可正常運作(static_key 17 個 + service_account 3 個)
|
||||
- [ ] 5.2 `cypher-executor/src/actions/credential-injector.ts` **不存在**
|
||||
- [ ] 5.3 `cypher-executor/src/lib/jwt-signer.ts` **不存在**
|
||||
- [ ] 5.4 `cypher-executor/src/lib/component-loader.ts` 無 `BUILTIN_API_RECIPES` / `BUILTIN_CREDENTIALS_MAP`
|
||||
- [ ] 5.5 `cypher-executor/src/` 全域搜尋 `crypto.subtle.decrypt` 只出現在 `wasi-shim.ts` 的 `crypto_decrypt` host function
|
||||
- [ ] 5.6 `cypher-executor/src/` 全域搜尋 `crypto.subtle.sign` 只出現在 `wasi-shim.ts` 的 `crypto_sign_rs256` host function
|
||||
- [ ] 5.7 `cypher-executor/src/` 全域搜尋 `interpolate` 回傳 0 筆(template 展開全在 WASM)
|
||||
- [ ] 5.8 全域搜尋 `{{secret\.` / `{{runtime\.` 在 TS 檔案中回傳 0 筆
|
||||
|
||||
---
|
||||
|
||||
## Phase 6:通用 CI/CD deploy workflow
|
||||
|
||||
**背景**(2026-04-20 richblack 決定):現 `.github/workflows/deploy.yml` 只部署 cypher-executor + registry + 已刪除的 credentials,漏掉 Phase 1-3 產出的 8 個 Worker,且硬編碼每個 job 導致未來新增 Worker 都要改 CI。改為**通用掃描式 workflow**:任何含 `wrangler.toml` 的目錄 = 部署單位,改到該目錄下任何檔案 = 觸發重新 deploy。
|
||||
|
||||
**關鍵決策**:
|
||||
- 零件 `.wasm` 由 CI build(不 commit):`registry/components/{name}/main.go` 改動時才重 build,用 timestamp / content hash 判斷
|
||||
- `.component-builds/{name}/component.wasm` 由 CI 從 `registry/components/{name}/{name}.wasm` 複製產生(deploy 前一步)
|
||||
- 統一用 pnpm(`.component-builds/*` 本來就是;順勢把 cypher-executor 的 `package-lock.json` 砍了)
|
||||
- runtime secret(`ENCRYPTION_KEY`)不進 CI,由 richblack 一次性 `wrangler secret put`
|
||||
- registry Worker 的 `wrangler.toml` 現階段不改(職責是合約管理,與封測無關;`sandboxAcceptance.ts` 的 rule 02 §2.2 審查留到 Phase 5 用戶自製零件啟動時)
|
||||
|
||||
### Tasks
|
||||
|
||||
- [x] 6.1 改寫 `.github/workflows/deploy.yml`:動態掃描所有含 `wrangler.toml` 的目錄(排除 `node_modules/` + Pages 專案),用 matrix job fanout 部署;分兩層(tier1=`.component-builds/*`,tier2=其他),tier1 全綠後才 tier2(避免 service binding target 未存在)
|
||||
- [x] 6.2 加上 TinyGo build 步驟:tier1 matrix 一律 setup-tinygo + 從 `registry/components/{name}/main.go` rebuild `.wasm` → copy 到 `.component-builds/{name}/component.wasm`
|
||||
- [x] 6.3 diff-aware:push 到 main 時比對 `github.event.before..github.sha`,只 deploy 有 diff 的 Worker(含 `registry/components/{name}/` 連動 `.component-builds/{name}/`);`workflow_dispatch` 提供 `force_all` + `only` 選項
|
||||
- [x] 6.4 統一 pnpm:刪除 `cypher-executor/package-lock.json` + `registry/package-lock.json`;workflow 優先 `pnpm install --frozen-lockfile`,若該目錄無 `pnpm-lock.yaml` 則 fallback 到 `--no-frozen-lockfile`(混合期容錯)
|
||||
- [x] 6.5 加 `max-parallel: 5` 控制 Workers API rate limit(tier1 和 tier2 各自)
|
||||
- [x] 6.6 驗證:`workflow_dispatch` + `force_all=true` 手動跑一次,24 個 Worker 全綠 — 2026-04-20 完成
|
||||
- 最終綠色 run 24668903627(28/28 jobs,含 discover + summary):tier1 24 個零件 Worker + tier2 2 個 orchestration Worker(cypher-executor / registry)全 success
|
||||
- 過程中修兩輪:先修 `setup-node` 的 `cache: 'pnpm'` 對 legacy `package-lock.json` 目錄失效(改為不用 cache);再修 tier2 三個 package.json(cypher-executor/registry/builtins)遺漏 `wrangler` devDependency + regen pnpm-lock.yaml
|
||||
- ENCRYPTION_KEY secret 已由 richblack 授權、CC 從 .env pipe 到三個 Worker:`arcrun-auth-static-key`、`arcrun-auth-service-account`、`arcrun-cypher-executor`(不顯示內容)
|
||||
- [x] 6.7 文件:在 `.claude/rules/` 加一份 `05-deploy-convention.md`(「新增 Worker = 新目錄 + wrangler.toml,不用改 CI」)
|
||||
|
||||
---
|
||||
|
||||
## Phase 7:auth primitive 改走 service binding(解 self-hosted CF 1042)
|
||||
|
||||
> 2026-06-06 richblack 拍板。來源:壓測報告階段 11(`test_arcrun/docs/壓測報告.md §11`)。
|
||||
|
||||
**背景(架構演化)**:
|
||||
- 當初設想「用戶對每個服務建**零件**」→ 零件用 service binding 要 redeploy → 用戶建 workflow 要 redeploy(不可接受)→ 故禁 service binding、改 cypher binding(HTTP URL)。
|
||||
- 後來架構演化:用戶要的是 **recipe(資料,存 KV)+ 固定 primitive**,不是新零件。`acr recipe push` 進 KV **不 deploy**。用戶用「http_request primitive + 不同 recipe」打各種服務,永不新增 primitive。
|
||||
- 結論:「用戶建 workflow 要 deploy」這個禁令前提**在 recipe 模型下不成立**。primitive(http_request + 4 個 auth)是**平台固定基礎設施**,和 13 個邏輯零件同類,給它 service binding 的 deploy 成本只在平台部署時付一次,**不落用戶身上**。
|
||||
|
||||
**根因(壓測階段 11,唯讀證據)**:
|
||||
- `auth-dispatcher.ts:67-68` 用 `fetch(wasmWorkerUrl('auth_service_account', WORKER_SUBDOMAIN))` 打 workers.dev → CF 回 **404 + "error code: 1042"**(CF 邊緣頁,非 auth worker 回應)。
|
||||
- **1042 精確定義(官方 docs,2026-06-06 查證)**:「Worker tried to fetch from another Worker **on the same zone**, only supported when `global_fetch_strictly_public` flag is used」。關鍵是 **same zone**:
|
||||
- **self-hosted 踩**:cypher(`arcrun-cypher-executor.{sub}.workers.dev`)與 auth worker(`arcrun-auth-*.{sub}.workers.dev`)**同在 `{sub}.workers.dev` zone** → 同 zone fetch → 1042。
|
||||
- **官方不踩**:官方 cypher 在自訂域 `cypher.arcrun.dev`,打 auth 的 `*.uncle6-me.workers.dev` 是**跨 zone**,不觸發 same-zone 限制(非「官方有 flag」——官方 wrangler.toml 只有 `nodejs_compat`,無 `global_fetch_strictly_public`)。
|
||||
- 這是 P0#9「同 zone 522」的**另一形態**:之前解的是 `*.arcrun.dev` 同 zone,沒解到 `*.workers.dev` 同 zone。service binding 是內部 RPC 不經 zone → 徹底免疫(13 邏輯零件正是走 binding 才不踩)。
|
||||
- 直接從外部打 `arcrun-auth-service-account.leo21c.workers.dev` → 200 + 真 `ya29...` token → **JWT/解密/token 換取鏈本身全正常**,唯一卡點是 cypher 打不到自己帳號的 auth worker。
|
||||
- service binding 是 CF 內部 RPC,不經公網 → 同時繞開**同 zone 522(P0#9)**與**同帳號 workers.dev 子請求 1042**,比 workers.dev fetch 乾淨(13 邏輯零件正是用 binding 才不踩)。
|
||||
|
||||
**技術前提(已驗證 ✅)**:
|
||||
- CF API 查 leo21c(全新自架帳號)的 `arcrun-cypher-executor` settings → **實綁 13 個 service binding**(SVC_SET→arcrun-set …)。證明 `deploy.ts` 的 binding 注入在 self-hosted 生效,auth binding 走同路綁得上。
|
||||
|
||||
### 修法演進:A(service binding)→ 廢 → B(global_fetch_strictly_public flag)
|
||||
|
||||
**先做了 A(service binding)後評估廢棄,改用 B(flag)**——richblack 2026-06-06 拍板。
|
||||
|
||||
- **A(service binding)為何廢**:service binding 靜態,加/改要重 deploy cypher(官方 docs 證實)。richblack 判定不夠乾淨,且有更簡單的 B。
|
||||
- **B(`global_fetch_strictly_public` flag)為何對**:官方 docs——此 flag 讓 `fetch()` 走公網「前門」,**same-zone fetch 也能通**。cypher wrangler.toml 加一行即解,**用戶無感、不用域名、不用重 deploy、不動 service binding 禁令**。
|
||||
- **B 安全(查證官方 docs)**:唯一副作用是「Worker fetch 自己 hostname 會 self-loop」;cypher 只打外部 API + sibling auth worker(皆非自己 hostname)→ 不 self-loop。
|
||||
- **B 官方/self-host 共用**:官方 cypher 本就跨 zone,加 flag 行為不變;self-host 同 zone 被修好。同一份 toml 兩邊通用。
|
||||
- **評估但廢的他案**:arcrun.dev 子域給 self-host cypher——查證後子域同 zone(zone=註冊域名)照踩 1042,且跨帳號 route 落地等於 PaaS 轉向,廢。
|
||||
|
||||
### Tasks
|
||||
|
||||
- [x] 7.1 規範:`02-forbidden §3.1` / `03` / `CLAUDE.md` 鐵律4 / `pre-bash-guard 3.1`——**禁令維持原狀(不解禁 service binding)**,僅加註「same-zone 1042 用 flag 解」。(先前一度改成「平台 primitive 例外允許 binding」,因改用 B 已**全部還原**)
|
||||
- [x] 7.2 ~~hook 解禁~~ → **還原**:`pre-bash-guard.sh` 規則 3.1 回原本「禁止新增 service binding」(B 不需解禁)
|
||||
- [x] 7.3 `cypher-executor/wrangler.toml`:`compatibility_flags` 加 `global_fetch_strictly_public`(+ 註解病因/安全)。**移除先前加的 SVC_AUTH_***(grep SVC_AUTH=0)
|
||||
- [x] 7.4 `auth-dispatcher.ts` / `types.ts`:**還原**到 A 之前(移除 binding 優先邏輯與 SVC_AUTH_* 型別,回單純 `fetch(workers.dev)`——flag 讓它同 zone 也通);tsc exit 0
|
||||
- [x] 7.5 `cli/src/lib/deploy.ts`:**無需改**——flag 在 cypher wrangler.toml,`stripOfficialOnlyBindings()` 不碰 `compatibility_flags`,self-hosted 部署自動帶上
|
||||
- [ ] 7.6 驗收(客觀證據):全新自架帳號 `POST /webhooks/named/{ns}/{wf}/trigger` 讓 `append_row` 的 auth 注入回 200、真的寫進 Google Sheets(第一個端到端驗 self-hosted auth 鏈的測試);`tsc --noEmit` exit 0
|
||||
- [x] 7.7 design.md 同步(§修改 auth-dispatcher 補「binding 優先、fetch fallback」+ 架構演化 + 1042 根因):§修改 的 auth-dispatcher 補「binding 優先、fetch fallback」;記架構演化(recipe 取代零件 → primitive 可 binding)
|
||||
|
||||
---
|
||||
|
||||
## Phase 8:`{{credential.X}}` 用戶面注入語法(2026-06-10 richblack 確認 change,design §8)
|
||||
|
||||
> 根因:壓測 401 = workflow 寫 `{{credential.X}}` 但三條 template 展開路徑都不認此 namespace。
|
||||
> 修法:auth_static_key 加 `resolve_credentials` action(WASM 解密),graph-executor 偵測+回填(不解密)。
|
||||
|
||||
- [x] 8.1 `registry/components/auth_static_key/main.go`:input 加 `Names []string`;main 在 `service`
|
||||
必填檢查**之前**分流 `action == "resolve_credentials"` → 走新 `handleResolveCredentials`(不查 recipe、
|
||||
不要求 service),每個 name `kvGet("{api_key}:cred:{name}")` + `cryptoDecrypt` → 回
|
||||
`{success, credentials:{name: plaintext}}`;缺 name → success:false + error 指明(不假綠)
|
||||
- [x] 8.2 `tinygo build -target=wasi -o auth_static_key.wasm main.go` 編譯通過(1.1MB)+ copy 到
|
||||
`.component-builds/auth_static_key/component.wasm`
|
||||
- [x] 8.3 `cypher-executor/src/graph-executor.ts`:node.data interpolate 後、node 執行前,掃描殘留
|
||||
`{{credential.NAME}}` → 有則 POST `wasmWorkerUrl('auth_static_key')`
|
||||
`{action:"resolve_credentials", api_key, names}` → 回填 node.data。**不解密、不碰 ENCRYPTION_KEY**(rule 02 §2.2)
|
||||
實作放 `auth-dispatcher.ts` 的 `resolveCredentialRefs`(遞迴掃描+回填,無 {{credential.}} 則零開銷不打 WASM),graph-executor 只呼叫
|
||||
- [x] 8.4 `tsc --noEmit` exit 0 + grep 確認 graph-executor / auth-dispatcher 無 crypto.subtle / {{secret. / ENCRYPTION_KEY 業務邏輯
|
||||
- [x] 8.5 端對端驗收:機制已實證打通(2026-06-13 leo21c 壓測)。實際走 **Notion** 而非 OpenAI 路徑——
|
||||
node.data `{{credential.notion_token}}` → 注入解密後 token → 真讀到 Notion Recipes 資料
|
||||
(「蕃茄蘑菇燉雞」+ iCook 連結),同時 401 假綠根治全鏈驗證(host fn error envelope →
|
||||
零件 parsed["error"] → cypher isFailure(),401 回 success:false)。客觀證據:真服務 2xx + 真 body。
|
||||
(8.5 原訂 OpenAI 為驗收服務,實際以 Notion 達成同等證據;機制與服務無關,故視為驗收完成)
|
||||
- [x] 8.6 design.md / status.md / BACKLOG.md 同步(§8 設計 + 8.1-8.4 done 標記)
|
||||
|
||||
---
|
||||
|
||||
## Notes
|
||||
|
||||
- 方案 B 是唯一方案(方案 A 已廢棄,違反 CLAUDE.md §禁止行為)
|
||||
- Phase 8 的 `resolve_credentials` 是用戶面入口,與 authenticate(recipe 自動注入)並存不重疊
|
||||
- Phase 0.6(host functions)+ 0.7(WASM runner)是 Phase 1-3 的硬前置,必須先做
|
||||
- 若 TinyGo `encoding/base64` 可用就直接用;若不可用則自行實作(見 gmail/main.go 的 `base64URLEncode`)
|
||||
- `auth_mtls` 的 TLS handshake 無法在 WASM 內做(WASI preview1 沒 socket),只能輸出 cert/key 讓 Worker 在 fetch 時用
|
||||
- **每個 auth primitive WASM 都是獨立部署的 Worker**(透過 `component-worker-template/`),不是從 R2 動態載入
|
||||
- Cypher binding = workflow YAML 裡的 URL 清單,不是 Cloudflare service binding
|
||||
@@ -4,13 +4,13 @@
|
||||
|
||||
按 Design 的四個 Phase 實作。原則:修改不重建,SDK 是 HTTP API thin wrapper,加密只在 client 做 encrypt(不做 decrypt)。
|
||||
|
||||
**前置依賴**:必須先完成 `credential-primitives-wasm/tasks.md` 的 Phase 0-3(核心合併 + WASM primitives),確認核心穩定後才開始建三個介面。
|
||||
**前置依賴**:~~必須先完成 credential-primitives-wasm 的 Phase 0-3~~ → **依賴已解除**(該卷 Phase 0-3 已完成並於 2026-07-21 封存至 `system-dev/docs/3-specs/archive/credential-primitives-wasm/`)。
|
||||
|
||||
---
|
||||
|
||||
## Phase 0(前置):核心合併 + WASM 改寫
|
||||
|
||||
> 詳見 `.agents/specs/arcrun/credential-primitives-wasm/tasks.md`
|
||||
> ✅ 已完成並封存:`system-dev/docs/3-specs/archive/credential-primitives-wasm/tasks.md`
|
||||
>
|
||||
> 摘要:
|
||||
> - 合併 u6u-core → arcrun(搬 builtins、刪重複 credentials)
|
||||
|
||||
Reference in New Issue
Block a user