From 8286c8afec1b225fcec21654cfd1cad3143231fe Mon Sep 17 00:00:00 2001 From: uncle6me-web Date: Fri, 14 Aug 2026 12:06:50 +0800 Subject: [PATCH] =?UTF-8?q?docs(pending-changes):=20=E6=8F=90=E6=A1=88?= =?UTF-8?q?=E2=80=94=E2=80=94=E8=AA=8D=E8=AD=89=E5=84=B2=E5=AD=98=E6=90=AC?= =?UTF-8?q?=E5=9B=9E=20D1/KV=EF=BC=88=E6=92=A4=20D61=20=E7=9A=84=E5=84=B2?= =?UTF-8?q?=E5=AD=98=E5=B1=A4=E6=B1=BA=E5=AE=9A=EF=BC=8C=E7=95=99=E5=85=B6?= =?UTF-8?q?=E6=98=8E=E9=A1=AF=E5=A4=B1=E6=95=97=E8=AA=9E=E6=84=8F=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .../arcrun-cypher-executor/worker.mjs | 2 +- .worker-builds/manifest.json | 10 +- cypher-executor/src/lib/portal-auth-store.ts | 5 +- system-dev/docs/3-specs/pending-changes.md | 99 +++++++++++++++++++ 4 files changed, 109 insertions(+), 7 deletions(-) diff --git a/.worker-builds/arcrun-cypher-executor/worker.mjs b/.worker-builds/arcrun-cypher-executor/worker.mjs index 27eddf6..49f39bc 100644 --- a/.worker-builds/arcrun-cypher-executor/worker.mjs +++ b/.worker-builds/arcrun-cypher-executor/worker.mjs @@ -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 = []; diff --git a/.worker-builds/manifest.json b/.worker-builds/manifest.json index 44486d3..c4177ba 100644 --- a/.worker-builds/manifest.json +++ b/.worker-builds/manifest.json @@ -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": [ diff --git a/cypher-executor/src/lib/portal-auth-store.ts b/cypher-executor/src/lib/portal-auth-store.ts index 84b13f1..349e0f8 100644 --- a/cypher-executor/src/lib/portal-auth-store.ts +++ b/cypher-executor/src/lib/portal-auth-store.ts @@ -215,8 +215,11 @@ export async function writeAuthStore(env: Bindings, data: AuthStoreData, tokenOv // 都不保證成立——安裝/更新從來不會把 CF_SECRETS_API_TOKEN 種成一個常駐的值。 // 對一個做不到的動作下指令=把人導向死路(他會以為自己操作錯誤,反覆重試)。 // 改成誠實描述現況+給得出去的下一步(回報支援),不再承諾一個我們自己都不確定會生效的動作。 + // 只列 CF_SECRETS_API_TOKEN:CF_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)。' + '這是平台端的已知限制,不是你操作錯誤——目前沒有你自己在畫面上能做的下一步,' + '請把這則訊息完整截圖/複製給支援,並註明你剛才在做什麼(例如:安裝精靈裡建立第一個帳號、' + '事後新增使用者、或修改密碼),會需要人工協助排除。', diff --git a/system-dev/docs/3-specs/pending-changes.md b/system-dev/docs/3-specs/pending-changes.md index fad654a..703f3a0 100644 --- a/system-dev/docs/3-specs/pending-changes.md +++ b/system-dev/docs/3-specs/pending-changes.md @@ -8,6 +8,105 @@ ## 待裁決 +### P?|認證儲存要不要搬回 D1/KV——resource-reuse 上線後,D61 想解的病可能已被更通用的機制解掉(2026-08-14) + +**觸發**:`arcrun-rag#99`/`inkstone/Arcrun#119`——全新安裝卡在「建立第一個帳號」,leo 本人與封測者 +都實撞。根因:D61(2026-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,不分是不是認證用的——不是為了本提案才要新造的機制,是已經在保護 +workflows/recipes/總圖的既有基礎設施。 + +核實證據(讀碼+讀已生成的成品,非猜測): +- `.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 前還是後的 bundle(merge 進 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 → 寫回 D1/KV),且要處理「這把 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 的反應非常慢。」