docs(pending-changes): 提案——認證儲存搬回 D1/KV(撤 D61 的儲存層決定,留其明顯失敗語意)

arcrun-rag#99 止血修法(安裝精靈傳 OAuth token 給 cypher 建第一個帳號)只解一格,
之後的寫入(換密碼/加使用者/存金鑰)仍永久 writable:false,是留債。

查證(讀碼+讀本機重打的 .worker-builds/manifest.json,非猜測):D61 想解的病根
(重裝時 binding 被照名字重新指到新建空資源)已在 2026-08-13 被更通用的
shared/resource-rule(Arcrun#97)解掉——SESSIONS_KV、KBDB 的 D1 都在 cypher 的
requires 清單裡,且 runInstall:1762 每次安裝/更新都會走這條規則。技術前提成立。

提案:認證資料搬回 D1/KV(走 binding,零外部 CF token、零過期、零新 OAuth scope、
零手動步驟),保留 D61 加的三個明顯失敗機制。附影響分析、遷移考量、信心水位
(靜態推論,未動態實測)、D84 三段式驗法(③ uninstall 目前做不到,如實標記)。

⏸ 等 leo/總管 confirm,本 commit 不動任何架構程式碼。

順手修:writeAuthStore 的錯誤訊息移除誤導的「/ CF_ACCOUNT_ID」(已查證全新實例
一定有這個 var,installer worker.js:1354 無條件注入,列兩項只會讓下一個人白工)。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
uncle6me-web
2026-08-14 12:06:50 +08:00
parent ca1ed2aaf6
commit 8286c8afec
4 changed files with 109 additions and 7 deletions
@@ -9248,7 +9248,7 @@ function newAuthUserId() {
async function writeAuthStore(env, data, tokenOverride) {
if (!authStoreWritable(env, tokenOverride)) {
throw new AuthStoreWriteError(
"\u9019\u53F0\u5BE6\u4F8B\u9084\u4E0D\u80FD\u5BEB\u5165\u8A8D\u8B49\u5132\u5B58\uFF08\u7F3A CF_SECRETS_API_TOKEN / CF_ACCOUNT_ID\uFF09\u3002\u8A8D\u8B49\u5206\u96E2\u9700\u8981\u9019\u5169\u9805\u624D\u5BEB\u5F97\u9032 Workers Secrets\u2014\u2014\u8ACB\u91CD\u65B0\u57F7\u884C\u5B89\u88DD\uFF0F\u66F4\u65B0\u8B93\u5B83\u5C31\u7DD2\u3002"
"\u9019\u53F0\u5BE6\u4F8B\u76EE\u524D\u5BEB\u4E0D\u9032\u8A8D\u8B49\u5132\u5B58\uFF08\u7F3A\u53EF\u7528\u7684 Cloudflare \u5BEB\u5165\u6191\u8B49\uFF1ACF_SECRETS_API_TOKEN\uFF09\u3002\u9019\u662F\u5E73\u53F0\u7AEF\u7684\u5DF2\u77E5\u9650\u5236\uFF0C\u4E0D\u662F\u4F60\u64CD\u4F5C\u932F\u8AA4\u2014\u2014\u76EE\u524D\u6C92\u6709\u4F60\u81EA\u5DF1\u5728\u756B\u9762\u4E0A\u80FD\u505A\u7684\u4E0B\u4E00\u6B65\uFF0C\u8ACB\u628A\u9019\u5247\u8A0A\u606F\u5B8C\u6574\u622A\u5716\uFF0F\u8907\u88FD\u7D66\u652F\u63F4\uFF0C\u4E26\u8A3B\u660E\u4F60\u525B\u624D\u5728\u505A\u4EC0\u9EBC\uFF08\u4F8B\u5982\uFF1A\u5B89\u88DD\u7CBE\u9748\u88E1\u5EFA\u7ACB\u7B2C\u4E00\u500B\u5E33\u865F\u3001\u4E8B\u5F8C\u65B0\u589E\u4F7F\u7528\u8005\u3001\u6216\u4FEE\u6539\u5BC6\u78BC\uFF09\uFF0C\u6703\u9700\u8981\u4EBA\u5DE5\u5354\u52A9\u6392\u9664\u3002"
);
}
const shards = [];
+5 -5
View File
@@ -1,18 +1,18 @@
{
"schema": 1,
"built_for": "arcrun-tier2-worker-artifacts",
"generated_at": "2026-08-14T03:45:54.247Z",
"repo_head": "c3856470ad5616b55aff846839581c15ed0975ed",
"generated_at": "2026-08-14T04:06:12.523Z",
"repo_head": "ca1ed2aaf6525df247e8bbac5277bb77dc78ba6b",
"repo_dirty": true,
"workers": [
{
"name": "arcrun-cypher-executor",
"source_dir": "cypher-executor",
"source_commit": "b223a698844be289c1b01f99eb34a8e2ac85bb74",
"source_commit": "ca1ed2aaf6525df247e8bbac5277bb77dc78ba6b",
"main_module": "worker.mjs",
"main_file": "arcrun-cypher-executor/worker.mjs",
"js_bytes": 588919,
"content_sha256": "d28f500a0d70f6e9364958dd7b4d446ceb7f3ce96584d6ef509d286fc2e86e86",
"js_bytes": 589408,
"content_sha256": "2c2fa5e2e0240ea27da424d543091b02d61b36ecc37310ca513220fe60c6096a",
"modules": [],
"compat_date": "2025-02-19",
"compat_flags": [
+4 -1
View File
@@ -215,8 +215,11 @@ export async function writeAuthStore(env: Bindings, data: AuthStoreData, tokenOv
// 都不保證成立——安裝/更新從來不會把 CF_SECRETS_API_TOKEN 種成一個常駐的值。
// 對一個做不到的動作下指令=把人導向死路(他會以為自己操作錯誤,反覆重試)。
// 改成誠實描述現況+給得出去的下一步(回報支援),不再承諾一個我們自己都不確定會生效的動作。
// 只列 CF_SECRETS_API_TOKENCF_ACCOUNT_ID 已查證全新實例一定有
//installer/oauth-prototype/worker.js:1354 無條件注入每一顆部署的 worker),
// 列兩項只會讓下一個人以為兩項都要查,白工一次。
throw new AuthStoreWriteError(
'這台實例目前寫不進認證儲存(缺可用的 Cloudflare 寫入憑證:CF_SECRETS_API_TOKEN / CF_ACCOUNT_ID)。' +
'這台實例目前寫不進認證儲存(缺可用的 Cloudflare 寫入憑證:CF_SECRETS_API_TOKEN)。' +
'這是平台端的已知限制,不是你操作錯誤——目前沒有你自己在畫面上能做的下一步,' +
'請把這則訊息完整截圖/複製給支援,並註明你剛才在做什麼(例如:安裝精靈裡建立第一個帳號、' +
'事後新增使用者、或修改密碼),會需要人工協助排除。',
@@ -8,6 +8,105 @@
## 待裁決
### P?|認證儲存要不要搬回 D1/KV——resource-reuse 上線後,D61 想解的病可能已被更通用的機制解掉(2026-08-14)
**觸發**`arcrun-rag#99``inkstone/Arcrun#119`——全新安裝卡在「建立第一個帳號」,leo 本人與封測者
都實撞。根因:D612026-08-10`c4cee35`)把認證資料搬到 CF Workers per-script Secrets,需要 cypher
自己持有 `env.CF_SECRETS_API_TOKEN`;這把 token 從安裝那天起就沒被種過(07-29 已知缺口),於是**每
一台全新實例**永遠 `writable:false`
**已做的止血只解掉一格,其餘留債**:先做的修法(安裝精靈把裝機當下自己還有效的 OAuth token 隨
`/console/setup``/portal/admin/bootstrap` 請求傳給 cypher,用這一次、不落地)讓「建立第一個帳號」
能通,但 `console-auth.ts:216``/console/setup/reset`)、`/portal/admin/users``POST /credentials`
(存 API 金鑰)、`/portal/password/change`——**這些路徑安裝精靈都不在場,永遠拿不到那個表頭**,裝完
之後這台實例對這些操作永遠 `writable:false`。leo 的判準是「別人不再出這個問題」且「後面不能留
債」,這不合格。
**排除掉的兩個止血選項(留給下一個人看,避免重新踩一次)**
- **A. 用 OAuth 權限「代鑄」一把不過期的 CF API Token**(呼叫 `POST /user/tokens`)——需要新的
OAuth scope(目前 `OAUTH_SCOPES` 六項裡沒有「管理 API Tokens」這項),會讓用戶授權畫面多一條
同意項,是產品面決定,未經查證 CF 的 OAuth scope 目錄裡那個 scope 確切叫什麼。
- **B. 讓用戶自己去 CF Dashboard 建一把 Token 貼進安裝精靈**——不需要新 scope,但打破「一鍵安裝」,
也是產品面決定。
- **(已明確排除)把 OAuth access_token 直接當長效 secret 存**——access_token 16 小時過期,
refresh_token 是 rotation 制、單次有效,cypher 沒有能力自己做 OAuth refresh(那等於要 cypher 自己
再長一份 OAuth client,且會跟安裝器搶同一條 refresh 鏈的競態)。存了會變成「`writable` 顯示
`true`,但 16 小時後開始靜默 401,且沒有任何機制會發現」——比現在「明確顯示 `writable:false`」更
糟,直接違反 D61 commit 自己引用的 #10「寧可明顯失敗,不要靜默錯置」。
**本提案(C):查證結果——技術前提成立,有具體證據,不是推論**
D61 的病根是「重裝時 binding 被安裝器照名字重新指到新建的空資源」(console 帳密住 `SESSIONS_KV`
portal_user 住 KBDB 的 D1)。**這個病根本身已經在 2026-08-13 被一個更早、更通用、範圍更廣的機制
解掉**——`shared/resource-rule``Arcrun#97``arcrun-rag#87`:「已部署的 worker 綁著什麼,那就是
事實 → 原封不動沿用,不管叫什麼名字」)。現在**每一次**安裝或更新都會先跑這條規則,涵蓋所有
`kv_namespace``d1` binding,不分是不是認證用的——不是為了本提案才要新造的機制,是已經在保護
workflowsrecipes/總圖的既有基礎設施。
核實證據(讀碼+讀已生成的成品,非猜測):
- `.worker-builds/manifest.json`(本次為驗證這個提案,用 `scripts/build-worker-artifacts.mjs`
重打的真實成品):`arcrun-cypher-executor.requires.kv``"SESSIONS_KV"``requires.d1` =
`{binding:"CREDENTIALS_DB", database_name:"arcrun-kbdb"}`——與 `arcrun-kbdb` worker 自己的 `DB`
binding 指向同一個資料庫名(portal_user 的家)。
- `installer/oauth-prototype/worker.js:1762``runInstall` 真的呼叫
`resolveResourcesByRule(token, accountId, manifestRequirements(manifest, baseName, true), mode)`
`mode ∈ {'init','update'}`——**每次安裝/更新都會走這條路,不是選配**。
- `manifestRequirements()``:818-843`)把 `requires.kv``requires.d1` 轉成規則輸入,
`SESSIONS_KV``CREDENTIALS_DB` 均未被排除。
- `shared/resource-rule/rule.mjs``kv_namespace``d1` 一視同仁,不分 binding 名字;同
`createName``arcrun-kbdb`)的 d1 需求會被 `shareSameResource` 收斂成同一顆。
**時序細節(誠實揭露,降低但不推翻結論)**`Arcrun#97` 的觸發事故發生在 2026-08-12(比 D61 晚兩
天),當時症狀含「portal 登出」——代表 D61 上線後、resource-reuse 落地前,這類事故確實還發生過一
次。無法確認 leo21c 那次事故當下實際跑的是 D61 前還是後的 bundlemerge 進 main ≠ 部署到任何一台
實例,這個落差在別處也反覆出現)。不論是哪一種:**resource-reuse 落地之後(08-13 起)**,這個機制
`SESSIONS_KV`KBDB D1 的保護,與 D61 對認證儲存的保護,範圍是重疊的——這件事在程式碼層級是確
定的,不是本節不確定的部分。
**建議做法**(confirm 後才動,這裡先寫方向不寫實作細節):
1. 認證資料搬回 D1(走 KBDB base API,比照 D61 之前的舊機制)+ console 帳密搬回 `SESSIONS_KV`——
寫入走 Workers binding,不需要任何外部 CF token,零過期、零新 OAuth scope、零手動步驟。
2. D61 加的三個「明顯失敗」機制**保留**,只是底層儲存換回 binding:讀不到不算密碼錯且不計入鎖定
`auth_store_empty`)、`/console/setup` 遇既有帳號說清楚密碼沒被採用、`/health`
`/console/auth-status` 吐儲存狀態。
3. `portal-auth-store.ts` 整份(CF Secrets 分片機制)——需要人決定留著當可切換的備援路徑,還是直
接移除單一化;本提案不預設答案。
**影響分析(依 SDD-LIFECYCLE 第 3 條格式)**
- **現行 active SDD**`system-dev/docs/3-specs/portal-auth/`——本提案不影響該 SDD 已完成的任務
P2 bootstrap/登入/role 閘等),只改「認證資料住哪」這一層的實作,功能行為(登入/bootstrap/
改密碼的 API 契約)不變。
- **尚未完成、待搬移**:本提案本身沒有現成 tasks 可搬,confirm 後在新工作項目下處理。
- **既有相關**`arcrun-rag#99``inkstone/Arcrun#119`(本次 P0 症狀單,confirm 後統一收斂修
法);D61`Leo/arcrun-rag#55`)本身——confirm 即等於部分撤銷 D61 的儲存層決定(保留其明顯失敗
語意),需要在 ADR 記一筆「D61 補充/取代」。
- **已裝好、正在跑 D61(CF Secrets 認證儲存)的實例怎麼辦**:需要一次性遷移(讀出 CF Secrets 裡的
JSON → 寫回 D1KV),且要處理「這把 CF Secrets 還留著沒人管」的收尾(刪除或忽略均可,留著不影
響正確性只是佔用 secret 額度)。confirm 後另立遷移步驟,不在本提案本身寫死做法。
**信心水位(誠實標,不假裝已驗證)**:上面「技術前提成立」的證據全部來自**靜態推論**——讀原始
碼、讀本機重打出的 `.worker-builds/manifest.json`、讀呼叫鏈——**沒有跑過一次真的部署+動態驗
證**。要把信心水位補到「實測過」,得先過 D20(部署 stage)才做得到,而部署本身要 leo 開閘 ⇒
不能把它當成「先驗證才准寫提案」的前置,否則變成「等閘才能寫提案、寫了提案才要得到閘」的死結
(總管裁決:先寫提案,把這段誠實揭露)。
**怎麼驗(confirm 後、真正動工前必跑,依 D84 之二三段式,不能只驗 update)**
1. **測 update**:已裝好、正在跑 D61 的實例,走過一次性遷移後,`/console/auth-status`
`writable:true`,既有帳密登入不受影響。
2. **uninstall 拆掉後從乾淨狀態模擬第一次安裝**:全新帳號走一次安裝精靈,`/console/setup` 建帳密
成功、`/portal/admin/bootstrap` 建第一個 admin 成功,**且不需要安裝精靈傳任何表頭**(因為寫入
走 binding,不再依賴 CF_SECRETS_API_TOKEN 這條路)。
3. **③ 目前做不到,如實標記**:D84 之二要求「除了測 update 還要用 uninstaller 刪除後再模擬第一次
安裝」——**uninstaller 現在還不存在**(另一條 subagent 正在做,`Arcrun#120` 家族)。本提案的驗
收要等 uninstaller 落地才補得齊第 2 步的「先真的拆過一次」那個誠實版本;在那之前,第 2 步只能
用「全新 CF 帳號」代替「先 uninstall 再裝」,兩者不完全等價(後者才驗得到「舊資源殘留時是否誤
沿用」這個 resource-rule 專門處理的情境)。
**⏸ 等 leo/總管 confirm 才動**(依 SDD 生命週期鐵律第 3 條)。confirm 前,止血用的表頭修法
`fix/install-bootstrap-cf-token-99``fix/installer-pass-install-token-99`,已 push 分支未合併)
繼續作為短期止血候選,兩者不衝突——表頭修法讓「建第一個帳號」現在能用,即使本提案要花時間走完整
個遷移流程也不擋這條路先出。
### P2|fan-out 並行執行(一個節點的多條出邊目前是循序跑)— 2026-08-03
**觸發**:leo 08-03 原話——「這是在測試中的計畫,**希望體驗很好**,我發現用 gemma4 的反應非常慢。」