merge: credential 全取到時不再打 auth_static_key(Arcrun#124)——Mira 恢復可用

總管獨立複驗才併(不是採信自述):
· youlin 實測,用戶真正會走的那條路:
    改前 GET /q/yuga3bse/rag_chat → 500 credential resolve 回傳 404: error code: 1042
    改後 GET /q/yuga3bse/rag_chat → success: true
    /kbdb/map libraries 0 → 1(testkb,entry_count 4、triplet_count 1)
· 部署前後 binding 21→21,消失/多出/值變 各 0;secret 都在
· 修法保留 WASM 路徑(任一 name 沒取到就照舊走 T7 雙讀)⇒ 不縮減既有能力
· 註解明確警告「這個 1042 跟前兩輪的 1042 不是同一個病」——同碼兩因,值得留

根因:auth_static_key 不在首裝清單,而 cypher 遇 {{credential.X}} 就無條件 fetch 它。
影響:凡走網頁安裝器裝出來的實例,ingest 與查詢全死(youlin/leo21c 實測同死,
geek6688 因手工維護零件齊全而測不出來)。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

# Conflicts:
#	.worker-builds/manifest.json
This commit is contained in:
uncle6me-web
2026-08-15 00:06:51 +08:00
4 changed files with 132 additions and 5 deletions
@@ -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",
+5 -5
View File
@@ -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": [
@@ -245,6 +245,35 @@ export async function resolveCredentialRefs(
// 沒取到的 name 缺席 → WASM 的 resolve_credentials 分支 fallback 舊 KVT7)。
const resolvedSecrets = await resolveSecretsFromNewHome(env, apiKey, nameList);
// ── 全部取到 → 不必打 auth_static_key workerArcrun#1242026-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 跟 d2048e295a1462 那兩輪的 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<string, unknown>;
}
const url = wasmWorkerUrl('auth_static_key', env.WORKER_SUBDOMAIN);
const res = await fetch(url, {
method: 'POST',
@@ -615,3 +615,98 @@ leo 的判準(2026-07-30):
- **⏸ 等 leo 裁的兩題(票上 §七)**:① App 前端跑 Portal originES module)還是 iframe
——短期 App 是否都由我們自己寫?② v0 只開 `nav` 一個掛載點夠不夠?
- **狀態**:⏳ 待 confirm。沒 confirm 前照現行 SDD 走,不開新 SDD、不動功能程式碼。
---
## 提案:安裝器的狀態判斷一律看帳號,中心帳本降級成純優化資料(leo 2026-08-14
- **leo 原話(本提案的來源,也是判準)**:
> 「你吃飯了沒?是由我來判斷的嗎?我看到你沒有盤子,應該問你吃了嗎?
> 但我沒問,堅持說你吃過了,**因為我這裡登記你吃了,結果讓你不能買飯**。」
> 「我有**檢修孔**,需要去問問就好了,東西有沒有**一看就知道**,
> **根本不應該由中心判斷**。」
- **病根(同一天三次撞牆,同一個形狀)**:
安裝器用**自己的帳本**`INSTALLER_KV``deployed:<accountId>:<subdomain>`
決定「這台裝過了沒」,而不是去看**帳號上現在有什麼**——
但它手上一直握著那把 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 前不動功能程式碼。