diff --git a/.worker-builds/arcrun-cypher-executor/worker.mjs b/.worker-builds/arcrun-cypher-executor/worker.mjs index dcaa5bc..f069da7 100644 --- a/.worker-builds/arcrun-cypher-executor/worker.mjs +++ b/.worker-builds/arcrun-cypher-executor/worker.mjs @@ -3791,6 +3791,9 @@ async function resolveCredentialRefs(data, env, apiKey) { if (names.size === 0) return data; const nameList = [...names]; const resolvedSecrets = await resolveSecretsFromNewHome(env, apiKey, nameList); + if (nameList.every((n) => Object.prototype.hasOwnProperty.call(resolvedSecrets, n))) { + return replaceCredentialRefs(data, resolvedSecrets); + } const url = wasmWorkerUrl("auth_static_key", env.WORKER_SUBDOMAIN); const res = await fetch(url, { method: "POST", diff --git a/.worker-builds/manifest.json b/.worker-builds/manifest.json index 8feb0f4..680dc05 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-14T05:27:06.148Z", - "repo_head": "94d5452425893ea9f2ed0b7166c7a87ca8866cec", + "generated_at": "2026-08-14T15:26:27.638Z", + "repo_head": "340b461f58ed6a9958850f8871cf05381ce95c9d", "repo_dirty": false, "workers": [ { "name": "arcrun-cypher-executor", "source_dir": "cypher-executor", - "source_commit": "4b6cc159b8b264ddca27da86922b2d4847ba0871", + "source_commit": "340b461f58ed6a9958850f8871cf05381ce95c9d", "main_module": "worker.mjs", "main_file": "arcrun-cypher-executor/worker.mjs", - "js_bytes": 589935, - "content_sha256": "8e6487478bc86bd289afaa5192cbd394b80b5235ea1eefb30ed716e6446a9312", + "js_bytes": 590085, + "content_sha256": "1da6a46e6a1781cb1a518635ff7e6b43a035710673066f4f764671c75441fdaf", "modules": [], "compat_date": "2025-02-19", "compat_flags": [ diff --git a/cypher-executor/src/actions/auth-dispatcher.ts b/cypher-executor/src/actions/auth-dispatcher.ts index b642fc3..50fa903 100644 --- a/cypher-executor/src/actions/auth-dispatcher.ts +++ b/cypher-executor/src/actions/auth-dispatcher.ts @@ -245,6 +245,35 @@ export async function resolveCredentialRefs( // 沒取到的 name 缺席 → WASM 的 resolve_credentials 分支 fallback 舊 KV(T7)。 const resolvedSecrets = await resolveSecretsFromNewHome(env, apiKey, nameList); + // ── 全部取到 → 不必打 auth_static_key worker(Arcrun#124,2026-08-14)──────────── + // + // 這條路徑上,那顆 worker 唯一比這裡多做的事,是「幫**沒取到**的 name 回頭翻舊 KV」 + // (T7 雙讀 fallback)。當 nameList 每一個都已經從新家取到明文時,它收到 + // `resolved_secrets` 後就是原樣回傳——回填本來就由下面的 `replaceCredentialRefs` + // 在本檔做。**所以那趟往返在「全取到」時不產生任何結果差異。** + // + // 為什麼這不是效能微調,是修 bug:`wasmWorkerUrl` 推導出的 + // `arcrun-auth-static-key.{WORKER_SUBDOMAIN}.workers.dev` **在網頁安裝器裝出來的實例上不存在** + // (首裝只裝 cypher-executor / kbdb / http_request / code 四顆,auth primitive 不在其中, + // 也沒有「用到才長」的機制)。於是 fetch 撞 CF 邊緣的 404 `error code: 1042` + // ⇒ 下面的 `!res.ok` throw ⇒ **凡是工作流用到 {{credential.X}} 的節點全部 500**。 + // 實測:leo21c 與全新安裝的 youlin 兩台,問答與寫入路徑一字不差地同死。 + // + // 🔴 給下一個人:**這個 1042 跟 d2048e2/95a1462 那兩輪的 1042 不是同一個病。** + // 那兩輪是「same-zone fetch 被 CF 擋」(解法=`global_fetch_strictly_public` flag, + // 且 service binding 的解法**已被 revert、不要再拿回來**)。本案是「**目標 worker 根本不存在**」 + // ——同一個錯誤碼、兩種原因。照舊帳去動 flag 或 binding 對本案完全無效(flag 早就在了)。 + // + // 邊界(rule 02 §2.2)不變:本檔仍然不解密、不展開模板、不組 JWT。 + // 明文的來源是 `secret_get`(host function,實作 = env[ref]),與改動前完全同一條路; + // 這裡只是不再為了「拿一份自己已經拿到的東西」去 fetch 一顆可能不存在的 worker。 + // + // 刻意保留 WASM 路徑:只要有任何一個 name 沒從新家取到(舊租戶、遷移過渡期的 KV 存量), + // 就照舊全量交給 WASM 走 T7 雙讀——**不縮減既有能力,只在等價時省掉那一跳**。 + if (nameList.every((n) => Object.prototype.hasOwnProperty.call(resolvedSecrets, n))) { + return replaceCredentialRefs(data, resolvedSecrets) as Record; + } + const url = wasmWorkerUrl('auth_static_key', env.WORKER_SUBDOMAIN); const res = await fetch(url, { method: 'POST', diff --git a/system-dev/docs/3-specs/pending-changes.md b/system-dev/docs/3-specs/pending-changes.md index 703f3a0..67d7f10 100644 --- a/system-dev/docs/3-specs/pending-changes.md +++ b/system-dev/docs/3-specs/pending-changes.md @@ -615,3 +615,98 @@ leo 的判準(2026-07-30): - **⏸ 等 leo 裁的兩題(票上 §七)**:① App 前端跑 Portal origin(ES module)還是 iframe ——短期 App 是否都由我們自己寫?② v0 只開 `nav` 一個掛載點夠不夠? - **狀態**:⏳ 待 confirm。沒 confirm 前照現行 SDD 走,不開新 SDD、不動功能程式碼。 + +--- + +## 提案:安裝器的狀態判斷一律看帳號,中心帳本降級成純優化資料(leo 2026-08-14) + +- **leo 原話(本提案的來源,也是判準)**: + > 「你吃飯了沒?是由我來判斷的嗎?我看到你沒有盤子,應該問你吃了嗎? + > 但我沒問,堅持說你吃過了,**因為我這裡登記你吃了,結果讓你不能買飯**。」 + > 「我有**檢修孔**,需要去問問就好了,東西有沒有**一看就知道**, + > **根本不應該由中心判斷**。」 + +- **病根(同一天三次撞牆,同一個形狀)**: + 安裝器用**自己的帳本**(`INSTALLER_KV` 的 `deployed::`) + 決定「這台裝過了沒」,而不是去看**帳號上現在有什麼**—— + 但它手上一直握著那把 CF token,一次 API 就問得到(=leo 說的檢修孔)。 + + | 撞牆 | 帳本說 | 現場 | 結果 | + |---|---|---|---| + | leo(`#120`) | 裝過了 → `update` | leo21c 已被清空,0 顆 worker | `rule.mjs:257` 停手「找不到任何要更新的 worker」,**畫面上沒有出口** | + | 封測者(`#123`) | 沒人綁這個名字 → 可以建 | 名字已被上次半殘安裝佔走 | CF 回 `title already exists`,**帳號永久卡死** | + | 08-14 早上的處置 | — | — | **人工去 uncle6 刪掉那筆紀錄**才能繼續=把帳本改回跟現場一致。**那是繞過,不是修法** | + +- **要達成什麼**:**帳本壞掉、被清掉、或根本不存在時,安裝器照樣裝得起來。** + +- **變更面(規格層,不是實作指定)**: + 1. **`mode`(init/update)的來源改成帳號現況**——「這個帳號上有沒有我們的 worker」。 + `worker.js:1750-1756` 現在讀兩條通道的 KV 前綴決定,改成問帳號。 + 2. **`rule.mjs:257` 那道「說是更新卻一顆 worker 都找不到 → 停手」不再是錯誤**。 + 在新判準下這個組合根本不會出現:沒有 worker ⇒ `mode` 本來就會是 `init`。 + (該閘原本是 `#97` 的另一道門,其防護目的由「2b 已部署綁定絕對優先」承接。) + 3. **那張紙搬到用戶自己的帳號上,中心不再保管**(leo 2026-08-14 第三次收斂,比「降級」更徹底): + + > 「**我覺得根本不用我 keep**,這次裝的 sha **記在一張紙放你家**, + > 下次又要裝我就把那張紙拿出來看你上次裝了什麼版本, + > **如果不符,以實際有幾顆為準**。」 + + ⇒ 紀錄(每顆 worker 的 `sha256`、`bundleBase`)**寫在使用者自己的 Cloudflare 帳號上**, + 不再寫進我們的 `INSTALLER_KV`。 + + 🔑 **為什麼這個比「降級」更好——它讓那個 bug 變成不可能發生,而不是被處理掉**: + 紙跟東西放在同一個地方 ⇒ **他把帳號清空時,紙跟著消失** + ⇒ 結構上不可能出現「東西沒了、而我們手上還留著一張說他裝過的紙」。 + `#120` 因此不是「被修好」,是**沒有地方可以發生**。 + + 附帶好處: + · 跨通道互蓋(t157)不必再讀「對方通道的 KV」——紙就在帳號上,兩條通道看的是同一張 + · 我們不再保管一份「誰裝了什麼」的中央登記簿(少一個要維護、會漂、會外洩的東西) + · 帳本壞掉/被清掉/從沒有過,安裝器照樣 work + + 🔗 **必須一併處理的勾連:辨識碼的「更新者免碼」**(leo 2026-08-14 問「辨識碼是用來找那張紙的嗎」, + 查證後方向是**反的**): + · 辨識碼=**邀請閘**(`verifyInviteCode` 打 landing 的 `/api/verify-code`),與帳本無關; + 真正認出使用者的是 **email**(`slugFromEmail` 決定所有資源名字),辨識碼不參與。 + · 但 `worker.js:3246`(t154)**用那張紙免掉辨識碼**:碼留空 → 放行進 OAuth → + 查此帳號有無部署紀錄 → 有=視同已核可免碼;無=退回要碼。 + ⇒ 紙搬到帳號上之後,「免碼」的判準自然從**查中心登記簿**變成**去帳號上看一眼**。 + ⇒ 附帶讓 `#120` 那個情境變**誠實**:帳號被清空 ⇒ 沒有 worker、沒有紙 ⇒ 你本來就是新裝, + **「請填辨識碼」是正確答案**;對照今天是「免碼放行 → 進去才說找不到 worker → 停手、沒有出口」。 + **同一個情境,一個是請你填個碼,一個是把你關在門外。** + ⚠️ 辨識碼**不可拿掉**——那是封測資格閘,與本提案無關。 + + ⏸ **要 leo 一併裁的兩題**(實作前必須有答案,但**做法由收工方判斷**): + ① 那張紙**貼在帳號的什麼位置**才會「跟著東西一起消失」—— + 要能被拆除工具連帶清掉,且不需要額外權限面 + ② `hasDeployRecordForToken`(t154:用一把 token 查「這些帳號有沒有裝過」) + 現在靠中心前綴掃描,搬走後要改成逐帳號去看——**這是預期內的成本,不是阻礙** + 4. 🔴 **鐵律:帳本永遠不得產生「停手」。** 帳本與現場不合時, + **以現場為準並順手把帳本改對**,不是把使用者擋在門外。 + + > leo 補充(2026-08-14,這句定義了帳本的身分): + > 「**我只是記錄上次的,這次去看看**,本來要記錄 sha, + > **一看東西都沒了,也不能硬說你裝過**。」 + > + > ⇒ **帳本記的是「上次」,不是「現在」。** 它是歷史紀錄,不是狀態宣告。 + > 每一趟都要去看現場;**看到的與帳本衝突時,看到的才算數**。 + > ⇒ 這也是為什麼「帳本不得停手」不是一條例外處理,而是它本來就沒有那個權限—— + > 一份講過去的紀錄,本來就不該否決現在觀察到的事實。 + +- **不動的牆**: + · 2b「已部署 worker 的綁定=事實,絕對優先」不得放寬(`#97` 的災情來源) + · 接管同名資源仍須呼叫端聲明 `createNameIsOurs`,預設 fail-closed(`#123` 的反向災情) + · 判斷仍只有**一份**,在 `shared/resource-rule/`;安裝器不得自建第二套 + +- **影響分析**: + · 動的是 `installer/oauth-prototype/worker.js` 的 mode 決定點 + `shared/resource-rule/rule.mjs` + 的 update-mode 前置閘;**鏡像兩份要同批**(`MIRROR.json` 守著) + · 多一次帳號查詢(`listAllScripts` 本來就會查,可共用,預期零額外成本) + · 不動 KBDB、無 schema 異動、無新金鑰面、無新公開端點 + · 會讓 `#120` 從「要人工清中心紀錄」變成不需要人介入 + +- **牽涉的票**:`inkstone/Arcrun#120`(重裝死結,本提案的主治對象) + |`inkstone/Arcrun#123`(已併 `9ad95ee`,是同一個病在資源層的那一半) + |`inkstone/arcrun-rag#78`(用戶版 uninstaller,受益但不因此結案) + +- **狀態**:⏳ **待 leo confirm 範圍**。方向已由 leo 當面給定;未 confirm 前不動功能程式碼。