封測者 1.4.45 實撞: a namespace with this account ID and title already exists ⇒ 那個帳號從此永遠裝不起來,而錯誤訊息對用戶完全無法行動。 根因:rule.mjs 第 2c 段只問「有沒有已部署的 worker 綁著它」, 不問「這個名字在帳號上是不是已經存在」。註解裡「本來就沒有東西可丟」 漏掉一種狀態——資源已建、worker 還沒部署就中斷(逾時/關掉分頁/斷網)。 拆除 youlin 時親眼看到的 8 顆空殼 KV 是同一個形狀。 修法:2c 在 create 之前先查同名。找到同名資源時: · 呼叫端聲明了 createNameIsOurs → 接回那一顆(adopt + reclaimed 標記) · 沒聲明 → 停手,訊息帶 RES-NAME-TAKEN 錯誤碼讓用戶回報 createNameIsOurs 是接管的唯一依據,預設 false(fail-closed)。只有名字 推導自使用者自己的身分時才准聲明——安裝器的 arcrun-rag-<slugFromEmail(email)>-kv-<binding> 合格;acr 從 toml 讀到的 裸 binding 名(WEBHOOKS)不合格,因為用戶自己也可能用那個名字。 這不是把 #97 刪掉的「照名字 ensure」搬回來: ① #97 找不到就新建一顆頂上去(會弄丟資料);這裡找到才沿用, 找不到照舊新建,永遠不拿新的空資源頂替既有的 ② 排在「已部署綁定=事實」之後,名字只在沒有任何綁定可看時才有發言權 ③ #97 無條件相信名字;這裡要呼叫端先證明名字推導自用戶身分 順手補上假帳號的保真度:FakeCloudflare.createKvNamespace 原本不擋同名, 所以半殘帳號在測試裡看起來只是「多幾顆孤兒」,實際上是裝不起來—— 少了那一行,這個 bug 測不出來。fixture-account.mjs 也把 resourcesExist 與 deployed 拆開,才表達得出這個狀態。 驗證(皆為實跑): node shared/resource-rule/tests/half-finished-install.mjs → 全部通過(零依賴) cd cli && npm test → 73/73 pass sync-resource-rule --check → 三份副本皆與原稿一致 ⚠️ 只有規則這一半。安裝器要在 manifestRequirements 聲明 createNameIsOurs 才會生效,那一半在 arcrun-rag(D85)。 Refs: inkstone/Arcrun#123
shared/resource-rule — 「這個實例該用哪些資源」的唯一一份規則
leo 2026-08-12: ①「如果你沒有裝,就是新的;如果你已經有,原來叫什麼名字就繼續用下去。」 ②「根本就不應該在 CLI,我要的是一個大家都可以用到的規則。」
① 是規則本身,② 是它該住哪裡。這個目錄就是 ②。
1. 規則(三句話)
判準是「這顆 worker 現在綁著誰」,不是「有沒有叫這個名字的資源」。
- 已部署的 worker 上綁著什麼,那就是事實 → 原封不動沿用,不管那顆資源叫什麼名字。
- 只有「確定沒有任何人綁過它」才准新建(新版本新增的 binding、或真的全新帳號)。
- 只要有一點說不準就整趟停手——讀不到綁定/綁著的資源不見了/同一個 binding 指向兩顆/ 該更新的 worker 一顆都不在 ⇒ 什麼都不建、什麼都不部署,把話說清楚讓人來判斷。
planResources()(不寫入,只出計畫)與 applyResourcePlan()(有 blocker 就拒絕執行)分兩段,
所以「被擋下的時候一顆資源都不會被建出來」是結構上的保證,不是靠誰記得寫 early return。
1.1 第 2 條的例外:上一次裝到一半死掉(Arcrun#123)
「沒有人綁過它」有兩種成因,第 2 條原本只想到第一種:
| 名字在帳號上 | 有 worker 綁著 | 該怎麼做 | |
|---|---|---|---|
| 新版本新增的 binding/全新帳號 | ❌ | ❌ | 新建(照舊) |
| 上次安裝建到一半就中斷 | ✅ | ❌ | 接回那一顆(否則 CF 回「title already exists」,這個帳號永遠裝不起來) |
接管同名資源的唯一依據是呼叫端在 BindingRequirement 上聲明 createNameIsOurs: true,
意思是「這個名字是我用使用者自己的身分可重現地算出來的」——安裝器的
arcrun-rag-<slugFromEmail(email)>-kv-<binding> 合格;acr 從 wrangler.toml 讀到的裸 binding 名
(WEBHOOKS)不合格,因為使用者自己也可能拿那個名字去建東西。
沒聲明就撞名 ⇒ 停手(訊息帶 RES-NAME-TAKEN 錯誤碼讓使用者回報,
不叫他自己去 Cloudflare 後台動手)。
🔴 這不是把 #97 刪掉的「照名字 ensure」搬回來。差別:#97 是找不到就新建一顆頂上去 (會把活著的實例洗成空的);這裡是找到才沿用、找不到才照舊新建,而且排在 「已部署的綁定=事實」之後——名字永遠只在「確定沒有任何綁定可看」時才有發言權。
2. 為什麼在這裡,不在 cypher-executor 的 API
.claude/rules/07-thin-shell.md 的標準答案是「能力放 API」。這一條不走那條路,理由是自舉:
| 問題 | 說明 |
|---|---|
| cypher 可能還不存在 | 這條規則要在「決定怎麼裝」的當下就用得到,而安裝器的工作正是把 cypher 生出來。把規則放進 cypher = 要先有雞才能有蛋。 |
| 輸入是使用者自己的帳號狀態 | 判斷的依據是使用者 Cloudflare 帳號上的綁定。送去平台託管的 worker 換一個答案 ⇒ ①「能不能安裝」綁在平台是否活著,②使用者的帳號拓撲交給第三方。 |
| 它根本不需要是服務 | 這是純函式:唯一的 IO 由呼叫端注入(ResourceApi)。薄殼原則要求「能力只實作一次」,不是「能力一定要是 HTTP」。 |
所以形態是一份零依賴的 ESM——Node 18+ 與 Cloudflare Workers runtime 都能直接 import, 不必編譯、不必連網、不必先有任何 arcrun 元件活著。
其他評估過的形態:共用 npm 套件 → 要多發一個 package + token,且安裝器得先 npm i 才能判斷,
自舉問題只是換個位置;做成一顆零件 → 得用 TinyGo/AssemblyScript 重寫一次,那正是「第二份實作」。
3. 檔案
| 檔案 | 內容 |
|---|---|
rule.mjs |
規則本體:planResources / applyResourcePlan / parseWranglerRequirements + 把 CF 回應讀成事實的 normalizeLiveBindings / normalizeLiveVars |
cf-resource-api.mjs |
ResourceApi 的 CF REST 實作(只用 global fetch)。眼睛也要共用——見下 §5 |
installer-entry.mjs |
安裝器唯一該碰的入口:resolveInstanceResources() |
tests/fixture-account.mjs |
假 Cloudflare 帳號(fetch 替身)+三種情境 |
tests/demo.mjs |
node shared/resource-rule/tests/demo.mjs——零依賴、零建置就能跑的示範 |
🔴 零依賴是硬規則:只准 import 同目錄的兄弟檔,不准碰 node:*。
有外部依賴就會有某條路吃不到它。cli/tests/single-implementation.test.ts ③ 會擋。
4. 兩條路怎麼取用
安裝器 / 任何 Worker(不需要副本)
安裝器本來就會下載本 repo 的 archive 當部署來源(.claude/rules/05-deploy-convention.md
「WASM 來源」),shared/resource-rule/ 就在那份 archive 裡:
import { resolveInstanceResources } from './shared/resource-rule/installer-entry.mjs';
const r = await resolveInstanceResources({
accountId, apiToken,
wranglerTomls: [cypherToml, registryToml, mcpToml, kbdbToml], // toml 的「內容」,不是路徑
mode: isUpdate ? 'update' : 'init',
});
if (r.blocked) {
// 🔴 一顆資源都沒被建。把 r.blockers 原文顯示給使用者,**不要自己「試著繼續」**。
return showAndStop(r.blockers);
}
// r.bindings : { 'kv_namespace:WEBHOOKS': 'kvid-…', 'd1:DB': 'uuid-…', … }
// r.origin : { 'kv_namespace:WEBHOOKS': 'adopted' | 'created', … }
// r.liveVars : { 'arcrun-cypher-executor': { ARCRUN_BUNDLE_VERSION: '1.4.33', … } } ← #106
安裝器不准自己判斷要不要建資源,也不准自己解讀 CF 的 binding 回應。只呼叫這一支。
acr CLI(需要一份鏡射)
arcrun 是獨立 npm 套件,npm pack 打不進套件目錄外的檔案 ⇒ 套件裡必須自帶一份。
cli/src/lib/resource-rule/ 就是本目錄的逐位元組鏡射,由
node scripts/sync-resource-rule.mjs 產生。
要改規則就改這個目錄,然後重跑 sync。 手改鏡射會被擋下:
npm run build 與 npm test 都先跑 sync-resource-rule.mjs --check,
差一個位元組就 exit 1(同 cli/harness/ 的產生物+世代閘慣例)。
5. 為什麼連 CF client 也共用
判斷一致還不夠,看到的東西也要一致。
「已部署的 worker 綁著什麼」是從 GET /workers/scripts/{script}/settings 讀來的。
兩條路各自寫一份 client,只要有一邊把 404 當錯誤、漏了 per_page、少認一種欄位名
(namespace_id vs id),那一邊就會「看不到既有綁定」——
而看不到既有綁定的下一步,依規則就是新建。
Arcrun#97 不需要規則寫錯,眼睛不一樣就足以重演。
所以 cli/src/lib/cf-api.ts 的 CfAccountClient 把 ResourceApi 那七個方法全部委派
給 cf-resource-api.mjs,自己不留實作。
6. 驗收
cd cli && npm test # 58 項,含下列三組
node shared/resource-rule/tests/demo.mjs # 安裝器那條路,零依賴獨立跑
| 測試 | 證的事 |
|---|---|
cli/tests/two-paths-agree.test.ts |
同一個帳號狀態餵給 acr 那條與安裝器那條,選出的 resource id 相同、建的東西相同、停手的理由相同 |
cli/tests/single-implementation.test.ts |
①規則的 7 支函式全 repo 只有這裡有實作 ②鏡射逐位元組相同 ③共用層零依賴 |
cli/tests/resource-adoption.test.ts |
#97 本身的迴歸(沿用/不多建/四種停手情境),改共用層後照樣全過 |
三種情境(tests/fixture-account.mjs 的 SCENARIOS):
fresh— 沒裝過 → 正常建新的(不能為了沿用而變成永遠不建)installed— 裝過了 → 沿用原本那幾顆,工作流與登入 session 都還在renamed— 資源在但名字與預期完全不同 → 仍然沿用(#97 的病根,專門驗)
7. 相關
Arcrun#97— 「我按了更新,工作流和登入全不見了」:CLI 那條已修,本目錄是把同一條規則交給所有路徑Arcrun#106— 重部署把plain_textvar(含版本標籤)洗掉:liveVars就是那些標籤Arcrun#80/arcrun-rag#39— 同一個「重複做 Arcrun 的工作」家族;Arcrun 是唯一編譯點的既有慣例.claude/rules/07-thin-shell.md— 本目錄存在的依據