From 5d441aa7d3be3c5677a864ef5a1a865a5c54e477 Mon Sep 17 00:00:00 2001 From: uncle6me-web Date: Fri, 14 Aug 2026 19:51:31 +0800 Subject: [PATCH] =?UTF-8?q?fix(teardown):=20=E4=B8=AD=E5=BF=83=E5=81=B4?= =?UTF-8?q?=E8=A3=9C=E4=B8=8A=E5=B8=B3=E8=99=9F=E7=99=BD=E5=90=8D=E5=96=AE?= =?UTF-8?q?=E2=80=94=E2=80=94=E5=8E=9F=E6=9C=AC=20central-*=20=E7=A2=B0?= =?UTF-8?q?=E5=BE=97=E5=88=B0=20leo21c?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 實測破口(2026-08-14,修補前): central-plan --account-id 51a01bfa…(leo21c) → 一路暢通,列出 deployed:51a01bfa…:leo21c 加 --yes 就會刪掉 leo 的安裝紀錄 ⇒ 他下次重裝撞 Arcrun#120 死結。 根因是兩個問題被當成同一個: ① 動的是「誰的」紀錄 → assertAccountAllowed(帳號側有,中心側沒有) ② 以「誰的身分」動手 → assertWranglerIsCentralAccount(D88 補的那道) c543aba 的註解寫「這個不對稱現在補齊」,但只補了②。註解說補齊、程式補一半, 後來讀的人會相信註解——所以順手把那段註解改成實測講法。 另修 listAllScripts 沒翻頁(ops-facts 記過的同款坑)。在這支工具裡漏看一顆 worker 有兩層傷害:拆不乾淨,以及「共用資源保護」看不到那個 owner, 把還在用的資源判成沒人用而刪掉。 並補一條已知限制:資源清單是從 worker binding 反推、不掃帳號, 所以沒人綁的孤兒殘骸不會被列也不會被刪(這正是 drill-a/drill-b 不受影響的原因, 但代價是拆完重裝可能撞 Arcrun#123,要自己再列一次帳號)。 驗證(youlin,全唯讀,未執行 apply): - plan:6 顆 worker + 10 個資源(8 顆 yuga3bse KV/yuga3bse-db/embed-m3), drill-a、drill-b 不在清單 - leo21c 帳號側、中心側 兩條路徑皆 exit 1 拒絕 - youlin 中心側仍正常:staging 通道有 deployed:1129efd7…:youlin-hsieh-dev 一筆 Co-Authored-By: Claude Opus 5 --- scripts/teardown-instance.mjs | 49 +++++++++++++++++++++++++++++++---- 1 file changed, 44 insertions(+), 5 deletions(-) diff --git a/scripts/teardown-instance.mjs b/scripts/teardown-instance.mjs index f3754bd..3808f5e 100644 --- a/scripts/teardown-instance.mjs +++ b/scripts/teardown-instance.mjs @@ -58,6 +58,13 @@ * (是 → 一併排入拆除;不是 → 照紅線問人再決定要不要動它),**拆完後如果選了半殘留, * 要有心理準備下一步的 fail-closed 檢查會擋下重裝**。 * + * - 🔴 **只拆「有人綁著的」,看不見孤兒資源**:資源清單是從「要刪的 worker 的 binding」反推的, + * 不是掃帳號。所以**沒有任何 worker 綁著的殘骸**(例:上一次安裝到一半死掉留下的 KV/D1) + * 本工具**不會列、也不會刪**。這個取捨是刻意的——它同時是「不誤刪」的來源 + * (2026-08-14 youlin 上的 `arcrun-rag-drill-a`/`drill-b` 兩顆演練用 D1 因此原封不動)—— + * 但代價是:拆完之後帳號上可能仍留著同名殘骸,重裝時撞 Arcrun#123(同名資源已存在)。 + * ⇒ 拆完要重裝之前,**自己再列一次帳號**確認沒有同租戶前綴的孤兒。 + * * 用法: * node scripts/teardown-instance.mjs plan --account-id --api-token [--keep w1,w2] * node scripts/teardown-instance.mjs apply --account-id --api-token [--keep w1,w2] --yes @@ -161,11 +168,25 @@ function scanKnownWorkerNames() { // ── 二、帳號側 discovery ───────────────────────────────────────────────────── -/** 列出帳號上所有 worker script 名字。 */ +/** + * 列出帳號上所有 worker script 名字。 + * + * 🔴 一定要翻頁。漏看一顆 worker 在這支工具裡是**兩層**傷害: + * ① 那顆是實例的一部分卻沒被刪 ⇒ 拆不乾淨; + * ② 更糟——那顆若綁著某顆 KV/D1,「共用資源保護」就看不到這個 owner, + * 於是把還在用的資源判成「只有要刪的 worker 綁著」而**刪掉別人在用的東西**。 + * (同款坑 ops-facts 記過:`cf-resource-api.mjs` 寫死 per_page=100 又不翻頁。) + */ async function listAllScripts(api) { - const { ok, result, error } = await api.cfRaw('/workers/scripts?per_page=100'); - if (!ok) throw new Error(`列 worker scripts 失敗:${error}`); - return /** @type {Array<{id:string}>} */ (result).map((s) => s.id); + const names = []; + for (let page = 1; ; page++) { + const { ok, result, error } = await api.cfRaw(`/workers/scripts?per_page=100&page=${page}`); + if (!ok) throw new Error(`列 worker scripts 失敗(page ${page}):${error}`); + const batch = /** @type {Array<{id:string}>} */ (result) ?? []; + for (const s of batch) names.push(s.id); + if (batch.length < 100) break; + } + return names; } /** @@ -313,7 +334,15 @@ async function applyTeardown(plan, { accountId, apiToken }) { // 靠操作者自己每次手動 `wrangler whoami` 核對——那個核對動作是對的,只是長錯地方 // (長在人身上)。失敗模式是最壞的一種:wrangler 的 OAuth 登入態如果哪天換成別的帳號, // 會**靜默**對錯的帳號動中心 KV,沒有任何東西會叫。帳號側(youlin)有 ALLOWED_ACCOUNT_IDS -// 白名單擋,中心側原本完全沒有——這個不對稱本身就是設計缺口,現在補齊。 +// 白名單擋,中心側原本完全沒有——這個不對稱本身就是設計缺口。 +// +// ⚠️ 2026-08-14 稍晚訂正:上面那句原本寫「現在補齊」,**但當時只補了一半**—— +// 補的是「以誰的身分動手」(wrangler 身分),沒補「動的是誰的紀錄」(accountId 白名單)。 +// 實測證明破口是真的:`central-plan --account-id 51a01bfa…`(leo21c)一路暢通, +// 直接列出 `deployed:51a01bfa…:leo21c`,再加 `--yes` 就會刪掉 leo 的安裝紀錄。 +// ⇒ 現在 planCentralCleanup() 兩道都跑:assertAccountAllowed + assertWranglerIsCentralAccount。 +// 📌 教訓:**「補齊了」這句話要用實測講,不要用註解講**——註解說補齊,程式只補一半, +// 而後來讀的人(包括我)會相信註解。 // assertWranglerIsCentralAccount() 在任何中心 KV 讀寫前跑,核對 wrangler 現在的 OAuth // 登入態的帳號清單裡有沒有 CENTRAL_ACCOUNT_ID,沒有就直接丟錯、不放行。 @@ -384,6 +413,16 @@ function wranglerKvDelete(namespaceId, key) { } function planCentralCleanup(accountId) { + // 🔴 兩道,缺一不可,而且問的是**不同的問題**: + // ① assertAccountAllowed(accountId):「我要清掉的是**誰的**紀錄」—— + // 中心 KV 裡每個帳號一筆,`deployed::`。少了這道, + // `central-apply --account-id ` 會清掉 leo21c 的安裝紀錄, + // leo 下次重裝就會撞上 Arcrun#120 的死結。**帳號側擋得住 leo21c,中心側原本擋不住。** + // (2026-08-14 實測:`central-plan --account-id 51a01bfa…` 直接列出了 + // `deployed:51a01bfa…:leo21c`,一路暢通。) + // ② assertWranglerIsCentralAccount():「我現在**以誰的身分**在動中心 KV」(D88 那道)。 + // ①問對象、②問身分。原本只有②,於是「白名單」只保護了帳號側的一半路徑。 + assertAccountAllowed(accountId); assertWranglerIsCentralAccount(); const prefix = `deployed:${accountId}:`; const plan = {};