diff --git a/scripts/teardown-instance.mjs b/scripts/teardown-instance.mjs index d3e91cd..f3754bd 100644 --- a/scripts/teardown-instance.mjs +++ b/scripts/teardown-instance.mjs @@ -31,7 +31,18 @@ * 「這個 worker 是不是正在被別人使用」——那是 main/coordinator 層級的協調,不是機器能判斷的)。 * - 中心 KV 清理走 `wrangler kv key`,用的是本機既有的 wrangler OAuth 登入態(不是 CF API * token),因為 INSTALLER_KV 住在另一個帳號(uncle6,官方安裝器所在),本工具沒有那把帳號 - * 的 API token。呼叫前務必 `npx wrangler whoami` 確認登入態是 uncle6,不是別的帳號。 + * 的 API token。**2026-08-14 起機械強制**:`assertWranglerIsCentralAccount()` 在任何 + * `central-plan`/`central-apply` 動作前跑 `wrangler whoami --json`,核對登入態看得到 + * uncle6 帳號才放行,看不到就丟錯拒絕(D88「閘要長在機器上,不是長在誰的記性上」—— + * 原本這是操作者每次手動核對的紀律,不是結構保證,現在補成機械閘)。 + * - `apply` 對「worker 被別的 worker service binding 卡住刪不掉」**不會自動重試/拓樸排序**。 + * 這是 fail-safe(失敗就是刪不掉,不會刪錯東西),但需要操作者自己重跑 `apply` 到收斂。 + * 2026-08-14 兩次實測的收斂輪數:第一次(帳號上有 kbdb-graph-plugin 這類外部依賴殘留) + * 跑了 2 輪(16 個失敗 → mcp 已刪,cypher-executor 解鎖 → 其餘 13 個跟著解鎖,僅 + * arcrun-kbdb 卡在外部依賴,需另外處理);第二次(帳號上沒有外部依賴殘留,純內部 + * service binding 鏈)跑了 3 輪(16 個失敗 → cypher-executor 解鎖、13 個一次過 → + * 只剩 array-ops 卡 cypher-executor 剛好晚一步刪掉 → 第 3 輪清空)。**下一個人看到 + * 第一輪一半失敗不是工具壞掉,是預期行為,重跑 `apply` 即可,通常 2–3 輪收斂。** * - 🔴 **偵測不到「不屬於這台實例的 worker 反過來依賴這台實例的 worker」這種形狀** * (2026-08-14 實撞,兩層咬合):`kbdb-graph-plugin`(不認得,正確被排除、不動)身上有 * service binding 指著 `arcrun-kbdb`(要刪的)。CF 因此拒絕刪 `arcrun-kbdb`—— @@ -296,14 +307,58 @@ async function applyTeardown(plan, { accountId, apiToken }) { // ── 三、中心 KV(安裝器 deployed 紀錄)清理 ──────────────────────────────────── // 這兩顆 KV 住在 uncle6 帳號(官方安裝器 arcrun-installer 所在),不是 youlin 的帳號。 -// 沒有這顆帳號的 CF API token,走本機已登入的 wrangler OAuth(`npx wrangler whoami` 需顯示 -// uncle6.me@gmail.com's Account,58309bb90fd93ad6d0fe0aae99170e9d)。 +// 沒有這顆帳號的 CF API token,走本機已登入的 wrangler OAuth。 +// +// 🔴 2026-08-14 leo 審核裁示(D88「閘要長在機器上,不是長在誰的記性上」):這裡原本 +// 靠操作者自己每次手動 `wrangler whoami` 核對——那個核對動作是對的,只是長錯地方 +// (長在人身上)。失敗模式是最壞的一種:wrangler 的 OAuth 登入態如果哪天換成別的帳號, +// 會**靜默**對錯的帳號動中心 KV,沒有任何東西會叫。帳號側(youlin)有 ALLOWED_ACCOUNT_IDS +// 白名單擋,中心側原本完全沒有——這個不對稱本身就是設計缺口,現在補齊。 +// assertWranglerIsCentralAccount() 在任何中心 KV 讀寫前跑,核對 wrangler 現在的 OAuth +// 登入態的帳號清單裡有沒有 CENTRAL_ACCOUNT_ID,沒有就直接丟錯、不放行。 const CENTRAL_KV = { prod: { id: '2ff9a845e7f646dab7b1e3dd6707b77f', label: 'INSTALLER_KV(prod 通道)' }, staging: { id: '750c23faf0794d3db317c32f596df9a9', label: 'PEER_INSTALLER_KV(staging 通道,t157 雙通道問題)' }, }; const CENTRAL_ACCOUNT_ID = '58309bb90fd93ad6d0fe0aae99170e9d'; // uncle6 +const CENTRAL_ACCOUNT_LABEL = "uncle6.me@gmail.com's Account"; + +/** 核對本機 wrangler 的 OAuth 登入態確實看得到 uncle6 帳號,不對就丟錯、不放行任何中心 KV 操作。 */ +function assertWranglerIsCentralAccount() { + let out; + try { + out = execFileSync('npx', ['wrangler', 'whoami', '--json'], { + cwd: REPO_ROOT, + encoding: 'utf8', + stdio: ['ignore', 'pipe', 'pipe'], + }); + } catch (e) { + throw new Error( + `拒絕執行中心 KV 操作:無法確認 wrangler 登入身分(\`wrangler whoami --json\` 失敗:` + + `${e instanceof Error ? e.message : e})。這是刪除工具,身分核不對就不准動中心 KV。` + ); + } + const jsonStart = out.indexOf('{'); + let who; + try { + who = JSON.parse(jsonStart >= 0 ? out.slice(jsonStart) : out); + } catch { + throw new Error(`拒絕執行中心 KV 操作:解析 \`wrangler whoami --json\` 輸出失敗:${out.slice(0, 300)}`); + } + if (!who.loggedIn) { + throw new Error('拒絕執行中心 KV 操作:wrangler 目前未登入(loggedIn:false)。'); + } + const ids = (who.accounts ?? []).map((a) => a.id); + if (!ids.includes(CENTRAL_ACCOUNT_ID)) { + throw new Error( + `拒絕執行中心 KV 操作:wrangler 目前的 OAuth 登入態(${who.email ?? '未知 email'})看得到的帳號是` + + ` [${ids.join(', ') || '無'}],裡面沒有中心帳號 ${CENTRAL_ACCOUNT_ID}(${CENTRAL_ACCOUNT_LABEL})。` + + ` 這代表你現在的 wrangler 登入態不是 uncle6——INSTALLER_KV/PEER_INSTALLER_KV 只住在那個帳號,` + + ` 繼續執行極可能是對錯的帳號動手(或單純連不到)。請先 \`npx wrangler login\` 切回 uncle6 帳號再重跑。` + ); + } +} function wranglerKvList(namespaceId, prefix) { const out = execFileSync( @@ -329,6 +384,7 @@ function wranglerKvDelete(namespaceId, key) { } function planCentralCleanup(accountId) { + assertWranglerIsCentralAccount(); const prefix = `deployed:${accountId}:`; const plan = {}; for (const [chan, kv] of Object.entries(CENTRAL_KV)) {