Commit Graph

371 Commits

Author SHA1 Message Date
uncle6me-web 4b6cc159b8 feat(auth): 認證儲存搬回 D1/KV——落實方案C(leo confirm「走C」,arcrun-rag#99)
實作 pending-changes.md「認證儲存要不要搬回 D1/KV」提案(commit 8286c8a):
D61 的病根「重裝時 binding 被安裝器照名字重新指到新建空資源」已被更通用的
shared/resource-rule(Arcrun#97,2026-08-13)解掉,故不再需要繞開 binding
去躲這個病——而繞開的代價正是這次要收的債:Workers Secrets 寫入需要外部
CF_SECRETS_API_TOKEN,這把 token 從安裝那天起就沒被種過,止血版只解掉
「建第一個帳號」這一格,之後的每一次寫入(換密碼/加帳號/改權限)仍卡死。

改動:
- console 管理員帳密:家改回 SESSIONS_KV(binding,console-auth.ts)
- portal 多人帳號:家改回 KBDB(binding,走 base HTTP API,D38 零 SQL,portal.ts)
- D61 認證儲存(CF Workers Secrets)留為舊實例的唯讀回退路徑:讀取零成本、
  零外部憑證需求(只有寫入才要 token);登入成功即 best-effort 自動搬進新家,
  且**這次登入發出的 session 就直接指向新 record_id**(不必等下一次登入)
- D61 的三項「明顯失敗」語意全部保留:auth_store_empty(讀不到不算密碼錯、
  不計入鎖定)、/console/setup 遇既有帳號說清楚密碼沒被採用、/health 與
  /console/auth-status 吐儲存狀態
- 移除止血版的 x-arcrun-install-token 表頭傳遞機制(installToken 參數)——
  帳號寫入從此不需要任何外部 CF token,這個結構性缺口已從根拔除

測試:cypher-executor 全套 vitest 439/453(14 個既存失敗與本改動無關,已用
git stash 對照 clean checkout 逐一比對檔名確認完全相同);tsc --noEmit
無新增錯誤(3 個既存錯誤同上核實無關)。已跑 build-worker-artifacts.mjs
重打 tier2 bundle,grep 複驗 createKbdbUserRecord/promoteToKbdb 進了成品、
promoteLegacyUser/x-arcrun-install-token 完全從成品消失。

未覆蓋:POST /credentials(一般 workflow API 金鑰儲存)仍依賴
CF_SECRETS_API_TOKEN——這是 01-tech-stack.md 既有的、獨立於 D61 之外的
credential 儲存架構(D19「擁有目錄不擁有內容物」),本提案範圍只涵蓋「認證」
(登入帳密),不涵蓋一般 credential 儲存;07-29 已知缺口仍待另案處理。

不准 merge 進 main(SDD 鐵律③,等總管審過再併);不准部署(D20 出貨閘)。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-14 12:52:07 +08:00
uncle6me-web 8286c8afec docs(pending-changes): 提案——認證儲存搬回 D1/KV(撤 D61 的儲存層決定,留其明顯失敗語意)
arcrun-rag#99 止血修法(安裝精靈傳 OAuth token 給 cypher 建第一個帳號)只解一格,
之後的寫入(換密碼/加使用者/存金鑰)仍永久 writable:false,是留債。

查證(讀碼+讀本機重打的 .worker-builds/manifest.json,非猜測):D61 想解的病根
(重裝時 binding 被照名字重新指到新建空資源)已在 2026-08-13 被更通用的
shared/resource-rule(Arcrun#97)解掉——SESSIONS_KV、KBDB 的 D1 都在 cypher 的
requires 清單裡,且 runInstall:1762 每次安裝/更新都會走這條規則。技術前提成立。

提案:認證資料搬回 D1/KV(走 binding,零外部 CF token、零過期、零新 OAuth scope、
零手動步驟),保留 D61 加的三個明顯失敗機制。附影響分析、遷移考量、信心水位
(靜態推論,未動態實測)、D84 三段式驗法(③ uninstall 目前做不到,如實標記)。

⏸ 等 leo/總管 confirm,本 commit 不動任何架構程式碼。

順手修:writeAuthStore 的錯誤訊息移除誤導的「/ CF_ACCOUNT_ID」(已查證全新實例
一定有這個 var,installer worker.js:1354 無條件注入,列兩項只會讓下一個人白工)。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-14 12:06:50 +08:00
uncle6me-web ca1ed2aaf6 fix(auth): 錯誤訊息不再叫用戶做做不到的事(arcrun-rag#99,leo 傳實測畫面點破)
leo 傳了封測者實際看到的畫面:撞牆訊息叫他「重新執行安裝/更新讓它就緒」,
但安裝/更新從來不會把 CF_SECRETS_API_TOKEN 種成一個常駐的值——這句話在
訊息會出現的所有情況下都不保證成立(尤其是安裝精靈本身之外的路徑,如事後
新增使用者、改密碼)。使用者照做 → 回到同一畫面 → 以為是自己操作錯誤 →
無限迴圈。這比「訊息不夠白話」嚴重一個級數:不是難懂,是死路。

改:不再承諾一個我們自己都不確定會生效的動作,改成誠實描述現況+給得出去
的下一步(回報支援並附上下情境)。訊息集中在 writeAuthStore 單一出處,
console-auth.ts/portal.ts 的 502 回應共用同一份文字,不會各說各話。

測試:全套 vitest 443/457(14 個既存失敗與本檔無關,已用 git stash 對照
clean checkout 確認);tsc --noEmit 無新增錯誤。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-14 11:56:59 +08:00
uncle6me-web 88f308642e fix(auth): 全新安裝補上 CF_SECRETS_API_TOKEN 的缺口——第一個帳號建得起來了(arcrun-rag#99)
根因:D61(認證與資料分離,c4cee35)把 /console/setup、/portal/admin/bootstrap
的寫入全部改走 CF Workers Secrets(env.CF_SECRETS_API_TOKEN),但這把 token
從安裝那天起就沒被種過——07-29 已知缺口(pending-changes.md「credential 走
n8n 模式」),當時只降級某個功能;D61 之後升級成「連第一個帳號都建不起來」
的硬斷點,每台全新安裝必中(leo 本人+封測者實撞:裝得起來但卡在註冊)。

修法:putWorkerSecret/deleteWorkerSecret/authStoreWritable/writeAuthStore/
mutateAuthStore 新增可選的 tokenOverride 參數(呼叫端提供 > worker 自身 env)。
/console/setup、/portal/admin/bootstrap 讀取 x-arcrun-install-token 表頭,
只有安裝精靈(裝機當下手上有一把自己還有效的 OAuth token,workers-scripts.write
scope,同一把已用於 putWorkerSecretDirect/seedCredential)會帶這個表頭;
一般使用者自己在瀏覽器操作不受影響。沿用既有「D36 安裝器代寫」precedent,
不是新開一條路;bootstrap 本身已被「已有 admin → 409」擋成只能成功一次,
不會被拿來反覆濫用。

測試:cypher-executor vitest 443/457(14 個既存失敗與本改動無關,已用
git stash 對照確認);新增 2 則直接證明「缺 token→502 auth_store_not_writable/
帶 token→200」。tsc --noEmit 無新增錯誤。已跑 build-worker-artifacts.mjs
重打 tier2 bundle 供驗證(工作區未 commit 前提下的本地驗證版)。

未完成:安裝器(products/arcrun-rag/installer/oauth-prototype/worker.js)
端的 x-arcrun-install-token 表頭傳遞已另外修好,但兩邊都還沒部署——需要
①重打正式 worker artifact ②install.arcrun.dev 的安裝器 wrangler deploy
③已卡住的封測者要再走一次安裝精靈讓他的 cypher worker 拿到新 bundle。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-14 11:46:56 +08:00
uncle6me-web c3856470ad chore(builds): 重編成品——把 #117 地圖 entry_count 與 #118 讀不到≠不存在 放進執行檔 2026-08-13 18:28:25 +08:00
claude-code 1ccee0055e Merge PR #118: 「讀不到」不准長得像「不存在」+開場就講得出有知識庫(總管審過) 2026-08-13 10:02:27 +00:00
claude-code 302e024804 Merge PR #117: 藏書地圖分開講「有幾筆內容」與「萃出幾條關係」(總管審過) 2026-08-13 10:01:34 +00:00
uncle6me-web c2897ba68b fix(mcp): AI 一連上就知道「這裡有主人的知識庫」+「讀不到」不再講成「不存在」
leo 2026-08-13:「它應該是**你的知識來源**⋯⋯**沒有它你是瞎的**,
從你對 Arcrun 的認知就知道你的內建 Memory 是沒用的。」

## 病灶一:指路的話被綁在一個會無聲消失的段落上

instructions 裡唯一提到「這裡有知識可查」的,是 buildLibraryMapInstructions
拼上去的【藏書地圖】——而那段是**選配的**(1500ms 逾時、catch 回 null、失敗無聲)。
必定出現的那段靜態文字(startHere)從頭到尾只講工作流、零件、recipe、邊的語法,
`kbdb_search`/`kbdb_get_map` 一個字都沒有。

實害:同一天,一條 portal 連線的 session 為了回答「Arcrun 是什麼」去讀了 682 行
原始碼——而 `kbdb_search(q="Arcrun 是什麼")` 當下回得出 33 筆真答案。

修法:
- 新增 `KNOWLEDGE_FIRST` 靜態段(與 startHere 同級,KBDB 掛了照樣出現),
  排在最前面:這條連線後面有主人的知識庫、第一個動作是 kbdb_search、
  工具怎麼呼叫、以及「沒查到/沒查/地圖沒取到」三件事不可以講成同一句。
- 地圖失敗**不再靜默**:拿不到就印【藏書地圖:這次沒取到】,
  stale token 另印身分版(要使用者重新連線)——「沒取到」與「這裡沒有知識」
  不可以長得一樣(Arcrun#109 同族)。鐵律不變:失敗仍然不擋連線。
- 抽出 `buildServerInstructions(env, identity)` 供測試印出完整字串。

## 病灶二:一個 401 被逐字翻譯成「不存在」

`arcrun_get_skill('write_intent_workflow')` 回「skill ... 不存在」,
但 `kbdb_search` 撈得到 `page_name: "skill-write_intent_workflow"`
(entry_type: agent-skill、source: installer-seed)——**與本工具查的鍵逐字相符**。
真兇是 `kbdbGetByPageName` 的 `if (!resp.ok) return null;`。
而 401 的來源是該實例的 arcrun-mcp 沒有 KBDB_INTERNAL_TOKEN
(kbdb-client 沒 token 就匿名送出)=部署面的事,但這支 code 把它講成了假話。

修法:
- 非 2xx 一律拋 KbdbAccessError(帶真 status)→ kbdb_unauthorized/kbdb_unreachable,
  訊息明說「這是讀不到,不是不存在」,並交出還走得通的那條路(kbdb_search)。
- `not_found` 只留給「KBDB 正常回應但真的沒這張卡」,並說明是「這台實例沒 seed」。
- 五支工具吃 identity:stale → identity_missing;portal 撞 401 時額外說明
  「是這批工具還走服務憑據,不是你的帳號讀不到」。
- instructions 步驟 4 不再指名 `arcrun_get_skill('INDEX')`(leo21c 實測根本沒 seed
  這支)——改成先 `arcrun_list_skills()` 看這台實例真的有哪幾支。

## 驗

- `mcp` 測試 130 passed(新增 mcp-instructions 7 項+skills-examples-honesty 10 項),tsc exit 0。
- 三種情境(地圖抓得到/抓不到/舊 token)各印一份完整 instructions 實測,
  三份的開場都指向 kbdb_search。

⚠️ 未部署(部署是 leo 的手動閘)。上線後才會到用戶手上。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-13 14:09:35 +08:00
uncle6me-web 614fe44812 fix(kbdb): 藏書地圖加 entry_count——triplet_count=0 不再被誤讀成「沒有知識」(Arcrun#87 三次收尾)
leo21c 實測:kbdb_get_map() 9 庫裡 8 庫 triplet_count:0/top_entities:[](kb 以外全部),
一個全新 session 讀到這份地圖會合理但錯誤地判定「這些庫沒有知識」而放棄查詢——但
kbdb_search(mode=keyword) 找得到 42 筆真內容(來源 gitea:Leo/Arcrun@…/gitea:Leo/mira@…等),
其中 15 筆 entries 的 metadata_json.library 已正確標成 arcrun/mira/arcrun-harness/arcrun-rag。

查證(唯讀,未動 leo21c 任何寫入):
- entries(原始 ingest 內容)與 triplet(從 entries 萃取出的三元組)是兩個不同存放處。
- kb 以外 7 庫:entries 有、library 標記正確;triplet 一筆都沒有——不是「三元組沒貼標」,
  是「三元組從沒被萃取」(另一條偵察線在查斷在哪一段,跨 repo,本 PR 不處理)。
- general 庫(未標 library 的三元組兜底分類)即時計數也是 0 ⇒ 沒有任何「有三元組但沒標庫」
  的候選 ⇒ Leo/Arcrun#87 先前那條批次補標通道(PR #114)在目前資料現況下會補到 0 筆,
  它解的是另一個問題,不是這次「地圖看起來是空的」的真因。

本次修法(純讀端加欄位,不碰任何寫入/部署/線上資源):
- kbdb/src/actions/library-map.ts:新增 liveEntryCountsByLibrary(),依 entries 自己的
  metadata_json.$.library 分組即時計數(排除 entry_type='value' 儲存碎片與地圖自己的歷史
  摘要 block,避免自我膨脹)。listLibraryMaps/getLibraryMapDetail/recomputeLibraryMap
  的回傳都加上 entry_count,與 triplet_count 並排、互不覆蓋。
- mcp/src/lib/library-map.ts:renderLibraryMapLines(MCP 連線開場注入的那份地圖原文)
  triplet_count=0 但 entry_count>0 時改印「0 triplets/N 筆原始內容(尚未萃取關係,
  kbdb_search 查得到)」,不再只印「0 triplets」。
- mcp/src/tools/kbdb_map.ts:kbdb_get_map 工具的全館/單庫回應都帶 entry_count,並在符合
  條件時附加提示,明講「triplet_count=0 不代表沒有知識」。
- console-ui/public/console/index.html:藏書地圖看板卡片同步顯示,人類看的畫面同一件事。

順手修掉一個真 SQL bug:entry_count 排除條件原寫
`NOT (entry_type='block' AND json_extract(...)='library_map')`,SQL 三值邏輯下
metadata_json 沒有 $.kind 欄位時 json_extract 回 NULL、`NULL = 'library_map'` 為 NULL
(非 false),整條 WHERE 判定 NULL 而把所有列濾掉——改用 COALESCE(...,'') 修正
(新增測試以 sqlite 實跑驗證抓到並鎖住這個修法)。

測試:kbdb 21/21(新增 2 案,全套 215/215);mcp kbdb-map 33/33(新增 7 案,全套 120/120)。
cypher-executor 既有 map/portal-data 相關測試(library-map-scope-108/kbdb-map-proxy/
portal-data)103/104 綠,唯一失敗(/portal HTML 殼 404)在未動過的 main checkout 上同樣
失敗,環境既有問題、與本次改動無關。

CP:◐ 半通。程式碼在此分支,測試綠燈,未併 main、未部署 leo21c——entry_count 要讓
leo21c 的真實使用者看到,需部署 kbdb+mcp(cypher-executor 未改動,portal-data.ts
是透明轉發不需重部署)。三元組萃取斷在哪一段(真因)與 23/42 entries 未 embed
(語意搜尋覆蓋率)兩件不在本 PR 範圍,分別交給另一條偵察線與 embed reconcile 管線。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-13 12:36:03 +08:00
uncle6me-web 9ff933ba98 chore(builds): 重編成品——把 #108 的租戶修法放進執行檔
cypher source 10d150a → b223a698(#108 併入)。
重編時新的租戶來源閘第一行就報:
  ✔ 租戶來源檢查通過:cypher-executor 資料面 owner_id 全部來自 src/lib/tenant.ts

⇒ 那道閘現在長在建置線上:以後有人再寫 env.XXX || 'leo',成品編不出來。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-13 00:25:46 +08:00
Leo 2fcae722e7 Merge PR #112: 藏書地圖看得到自己的知識——租戶字串改從寫入端來(Arcrun#108)
總管複驗(不聽自述,自己重跑並與 base 逐條比對):
  cypher  14 failed / 441 passed(base 14 / 400)⇒ 多 41 條新測試全過
          失敗清單 diff 無輸出=逐字元相同、零回歸
  閘實測  照 #105/#108 的原句形狀種一個違規(env.CONSOLE_TENANT || leo)
          → 掃描器當場抓到並指名道姓;移除後恢復綠 ⇒ 不是假綠

這道閘的價值超過本票:它擋的是「身分來自環境變數」整族,
#105 那句話今天再寫一次也會被擋。符合 leo 08-12 立的
「做平台要減少 hotfix」——修掉 bug 不算完成,要留下下次再犯會被擋住的機制。

agent 誠實標的殘項(不擋併):
  · kbdb_get_map 回 1851、Portal 總圖畫面、部分授權帳號實測——都要部署 leo21c,紅線沒碰
  · 想把閘也接進 .claude/hooks/pre-write-guard.sh(寫入前就擋),該檔受保護改不動;
    檢查器已備妥 --stdin 模式,總管代接
2026-08-12 16:24:45 +00:00
uncle6me-web cac874601f chore(builds): 重編 tier2 成品——把 #108 放進執行檔(出貨路徑 A 的第 0 步)
原始碼修好不等於使用者拿得到:`.worker-builds/` 是安裝器與 `acr update`
實際部署的那份執行檔。本次一併重編,repo_dirty=false、repo_head=b223a698。

  arcrun-cypher-executor  sha256=e0026a23792f  source=b223a698 ← #108
  arcrun-kbdb             sha256=9c6d41895d78  source=f87d0e92(未變)
  arcrun-http-request     sha256=9a9dcb71879a  source=1e85dfb4(未變)
  arcrun-code             sha256=285a7406ec69  source=621cb8d9(未變)
  arcrun-mcp              sha256=3ebc0d441bc0  source=10d150ac(未變)

其餘四顆的 js 位元組有變是 esbuild 版本/相依重裝造成的重編,source commit 未動。
本次起這條路徑多一道閘:編成品前先跑租戶來源檢查,違規直接「建置中止」
(見同 PR 的 scripts/build-worker-artifacts.mjs)——實跑輸出貼在 PR。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-13 00:16:55 +08:00
uncle6me-web b223a69884 fix(portal): 藏書地圖看得到自己的知識——租戶字串改從「寫入端」來,不再拿環境變數預設值(Arcrun#108)
leo 2026-08-12 實撞:藏書地圖回 0 個庫,同一分鐘 KBDB 裡有 1854 條三元組,
`arcrun_whoami` 顯示 admin/全部知識庫、`kbdb_search` 也查得到——只有地圖那格是空的。

病根(不是資料掉了,是讀寫兩端各拿一個來源):
  寫入端 owner_id = `~/.arcrun/config.yaml` 的 `api_key`(CLI push/小幫手上傳/MCP,
    leo = `bfezv28v`)
  讀取端過濾 = `portalTenant(env) = env.CONSOLE_TENANT || "leo"`
    ——repo toml 帶的**官方 prod 值**,而 `acr` 從來不注入 CONSOLE_TENANT
  ⇒ 那個 `"leo"` 不是理論邊角,是每台 self-hosted 實例的實際行為,1854 條全被濾掉。

與 #105(`env.MCP_OWNER_NAMESPACE || "leo"`)同一句話,換一個檔案。

租戶字串該從哪裡來(本票的核心判斷):
  **從「寫入這批知識的那一方」來,不是從一份手抄的環境變數預設值來。**
  不是「掛到每個帳號上」——portal 帳號共用同一台實例的知識庫(design D-2),
  帳號之間的差別是 libraries 權限不是 owner_id;複製一份到帳號上只是多一個會過期的副本。
  #105 真正的教訓是:過濾用的租戶字串要有單一權威來源、解析不到要誠實失敗、且要能機械驗證。

修法:
1. 唯一產地 `cypher-executor/src/lib/tenant.ts`
   - `knowledgeOwner(env)` → branded `TenantId`:`ARCRUN_NAMESPACE` → `CONSOLE_TENANT` →
     丟 `TenantUnresolvedError`。**沒有字面預設值**——`|| 'leo'` 正是把「這台機器沒設定」
     偽裝成「你沒有資料」的元凶。
   - `accountTenant(env)` → 普通 `string`(帳號子 namespace `{tenant}::portal` 與 cypher
     自己寫的設定用它)。**回 string 是刻意的**:型別上就不可能流進知識資料面。
   - 資料面過濾一律經 `ownerQuery()` / `ownerField()`,只吃 `TenantId`。
2. 值的正解由 CLI 從真相源導出:`acr update` 把 config 的 `api_key` 注入成 `ARCRUN_NAMESPACE`,
   但**先驗再寫**(`GET /kbdb/map?owner_id=<api_key>` 查得到庫才寫;查不到/問不到就一個字
   都不動)。無條件覆蓋會把「知識本來就在 CONSOLE_TENANT 底下」的一鍵安裝實例指向空的那一格
   ——那是 #97/#106 那類「更新一次把人家的東西弄不見」,比原本的 bug 更糟。
   未注入時回退 CONSOLE_TENANT ⇒ 對官方 prod 與未更新的實例,這次改動是惰性的。
3. 空地圖分四態(沿 #100「讀不到就說讀不到」):no_library_grant/filtered_out/
   scope_mismatch/confirmed_empty。scope_mismatch 以前不存在,所以設定錯誤被畫成
   「你沒有資料」。回應仍不含租戶字串(design §3.3 紅線)。
4. 同族一起修(同一道閘一次抓到):console-dashboard 4 處、console-auth 1 處
   ——console 首頁的規模數字與藏書地圖對 leo 也一直是空的。

留下的閘(規則存在但沒機制驗證=會再犯第三次):
  · 型別閘:TenantId 只能由 tenant.ts 產出 → 拿隨手一個 string 去過濾,tsc 當場不給過。
  · 出貨閘:scripts/build-worker-artifacts.mjs 編 tier2 成品前先掃,違規 → 編不出成品。
  · 閘自己可測:規則是純函式(tenant-source-rules.mjs),tests/tenant-gate.test.ts
    逐條驗「5 種壞例子會擋」+「11 種合法寫法零誤攔」;掃描範圍只有 src/,擋不到自己。
  規範寫入 .claude/rules/02-forbidden.md 第六類、system-dev/wiki/mistakes.md #26。

沒動:庫權限過濾(一字未改,回歸測試釘住)、帳號資料落點、任何金鑰、租戶字串仍不下發前端。

驗證:
  cypher   vitest 441 綠 / 14 紅,14 紅與 base commit e05518a 逐字相同(既有)
           tsc 5 個既有錯誤,零新增
  cli      node:test 60/60 綠(含本次新增 12 條);tsc 零錯誤
  閘       壞例子實跑 exit 1;build 實跑「建置中止」;乾淨時實跑通過
  端到端   ◐ 未驗:需部署到 leo21c,那道閘要 leo 親手解(見 PR ③)

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-13 00:15:58 +08:00
uncle6me-web fad5da0e17 rules/07 §3.6:自舉例外——能力只實作一次,但不一定要是 HTTP API
代補 PR #111 的 agent 想加但改不動的段落(該檔受保護)。

立這條的原因不是理論:#97 的修法一開始寫在 cli/src/lib/resource-resolver.ts
(能力住在介面層,違反 §0)⇒ 安裝器拿不到它 ⇒ 同一個 bug 只修了一半,
走 acr 的人有保護、走 install.arcrun.dev 的人沒有——而所有真實用戶走後者。

leo 2026-08-12:「根本就不應該在 CLI,我要的是一個大家都可以用到的規則。」

記三件,都是為了不讓下一個人「修正」回去:
① 為什麼不放 cypher API(自舉/輸入是使用者自己的帳號狀態/它是純函式)
② §0 的正確讀法是「只准有一份、不准住在單一介面裡」,放 API 只是常見手段
③ npm 打包例外:cli/ 下的逐位元組副本由 sync --check 機械擋漂移,不是第二份實作

📍 repo:matrix/arcrun(shared/resource-rule/、cli/、.claude/rules/07)
   + products/arcrun-rag(安裝器待接上,已派工)

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-12 23:50:53 +08:00
Leo 13155a1d7b Merge PR #111: 把「該用哪些資源」搬出 CLI——一份實作,兩條路吃同一條規則
leo 2026-08-12:「根本就不應該在 CLI,我要的是一個大家都可以用到的規則。」

總管複驗(不聽自述,自己重跑):
  兩份 rule.mjs 逐位元組相同;--check 綠
  故意加一行製造漂移 → --check 當場擋下;還原後恢復綠 ⇒ 閘是真的,不是假綠
  cli 測試 58 pass / 0 fail(main 上是 49 ⇒ 多 9 條全過)
  邊界乾淨:沒動 cypher/kbdb/mcp;resource-resolver.ts 減 461 行變薄殼

兩條新測試比我寫的收工標準更嚴:
  two-paths-agree 斷言「一邊停手、一邊照做 = 最危險的分歧」與「停手的理由也要一樣」
  single-implementation 帶 assert.ok(files.length > 0) 防「檢查 0 個卻通過」
  ——後者正是總管當晚剛撞過的假綠形狀

殘項(agent 誠實標的,不擋併):
  · youlin 上走安裝器那條路的實測沒跑成——安裝器在 arcrun-rag,不在它的工作副本裡,
    且沙箱無 CF 憑證。它依紅線「不確定時寧可中止」沒有猜著跑。
  · 安裝器那一行 import 尚未接上(規格在 shared/resource-rule/README.md §4)
  · rules/07 想補「自舉例外」正例,該檔受保護,內容留在 README §2 待總管代補
2026-08-12 15:44:50 +00:00
uncle6me-web bb548b6fdf refactor(shared): 「該用哪些資源」搬出 CLI——一份實作,acr 與安裝器吃同一條規則
leo 2026-08-12:「根本就不應該在 CLI,我要的是一個大家都可以用到的規則。」

「這個實例該用哪些資源」換到安裝器就要重寫一次 ⇒ 依 rules/07-thin-shell.md 的判準
它是**能力**,而它原本住在 cli/src/lib/resource-resolver.ts ⇒ 那本身就是違規。
後果已經真的發生:acr 那條有 Arcrun#97 的修法、安裝器那條沒有,於是安裝器照名字
找、找不到就建一顆空的綁上去 ⇒「我按了更新,工作流和登入全不見了」。

規則搬到 shared/resource-rule/(零依賴 ESM,Node 與 Workers runtime 都直接跑):

  · rule.mjs           規則本體+把 CF 回應讀成事實的 normalizeLive*
  · cf-resource-api.mjs ResourceApi 的 CF REST 實作——**眼睛也共用**:
                        兩條路各自解讀 CF 回應,只要一邊看不到既有綁定就會去新建,
                        #97 不需要規則寫錯就能重演
  · installer-entry.mjs 安裝器唯一該碰的入口 resolveInstanceResources()

不是做成 cypher 端點的理由(自舉):這條規則要在「決定怎麼裝」的當下就用得到,
而那時 cypher 可能還不存在(安裝器的工作正是把它生出來);且輸入是使用者自己帳號的
綁定狀態,不該送去平台換答案。它是純函式,用不著變成服務。

只有一份,機械看守:
  · 安裝器直接 import repo archive 裡的原稿,**不需要副本**
  · acr 因為 npm pack 打不進套件目錄外的檔案,帶一份逐位元組鏡射
    (scripts/sync-resource-rule.mjs 產生;build/test 先跑 --check,差一位元組就紅)
    ——同 cli/harness/ 產生物+世代閘的既有慣例
  · cli/tests/single-implementation.test.ts 掃全 repo:7 支規則函式的實作只有一處

CLI 淨 -496 行(邏輯是搬走,不是複製)。cf-api.ts 的 CfAccountClient 保留公開介面,
ResourceApi 那七個方法全部委派共用 client。

驗證:cli 58/58 綠(含新增的兩條路一致性 fixture + 三種情境),tsc --noEmit 乾淨。
2026-08-12 23:37:01 +08:00
uncle6me-web e05518a2b4 chore(builds): 重編成品——把 #107 的版本標籤修法放進執行檔
出貨線第 1 站的 #93 新鮮度閘擋下:cypher-executor 成品記 10d150a,
而 HEAD 已是 2129356(#107 改了 routes/health.ts 與 types.ts)。

這是該閘今晚第三次擋對——沒有它,1.4.42 會送出一個「版本標籤修法只在源碼裡」的成品。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-12 22:22:57 +08:00
Leo 21293568d5 Merge PR #107: 更新完還看得到版本號——CLI 重部署不再把版本標籤洗掉(Arcrun#106)
總管複驗(不聽自述,自己重跑並與 base 逐條比對):
  cli      main 是 0 pass / 3 fail(測試根本跑不起來)→ PR 49 pass / 0 fail
           ⇒ agent 那句「#97 的守衛測試一直沒在跑」是真的,又一個假綠
  cypher   14 failed / 402 passed(base 14 / 400);失敗清單 diff 無輸出=零回歸

設計比我建議的對:版本標籤每趟重烙、其他 vars 才沿用。
我原本建議「一律沿用」會讓版本號永遠停在安裝當時。

殘項(agent 誠實標的,不擋併):沒在真實例上跑過、沒有真 Portal 截圖
(它的環境指向 leo21c 紅線、無 youlin 憑證、瀏覽器未授權)。總管接手補這段。
2026-08-12 14:05:57 +00:00
uncle6me-web 53b05c6d3d fix(cli): 更新完還看得到版本號——CLI 重部署不再把版本標籤(和你的設定)洗掉
leo 08-12 實撞:更新完 leo21c,Portal 設定頁的「版本」變成
「無法讀取目前版本(知識庫服務可能正在啟動)」。版本號是 leo 唯一的驗收介面,
看不到就等於他無法自己確認任何一次更新有沒有生效。

根因(Arcrun#106):`bundle_version` 來自部署時注入的 plain_text var
`ARCRUN_BUNDLE_VERSION`,而**只有安裝器會注入**。wrangler deploy 是整份覆蓋,
toml 沒寫的 var 直接消失 ⇒ CLI 更新那條路每跑一次就把標籤洗掉一次。
#97 修好了「櫃子」(KV/D1/Vectorize 沿用既有),沒修「櫃子上的標籤」。

修法(兩種 var 走相反的規則,這是本次的判斷):
· 設定類 var = 使用者實例的事實 → **沿用**(讀綁定時同一份回應就帶回來,不多打 API)
  ——把 #97「已部署的 worker 上綁著什麼就是事實」原封不動套用到 plain_text var。
· 版本標籤 = 這份成品的屬性 → **每趟重烙,絕不沿用舊值**。
  沿用舊值會得到一個永遠停在安裝當天的假標籤——比沒有標籤更糟,
  因為它會讓人以為驗收過了。
  版號取部署當下發行頻道公告的 release(Portal/daemon 就是拿它當「最新版」比),
  另外把**真正部署的 commit** 一起烙上去(/health 多吐 `bundle_commit`)→ 漂掉查得出來。
  查不到 release 就誠實退成 `YYYY-MM-DD+<commit7>`,不掰一個 semver 假裝已是最新。

順帶(都是同一條路上的東西):
· ref 先解析成 commit sha 再用 sha 下載 archive——不可變,順手解掉 branch tarball 被快取的老病
· Portal 版本行接受帶 build metadata 的 semver(`1.4.41+d61` 這種先前一律被當成「較舊版本」)
· cli 測試在 node 22 上本來一支都跑不起來(.js→.ts 解析 + parameter property),補上 resolve hook
  ——#97 那份「使用者的東西還在不在」的迴歸守衛也在其中,跑不起來的守衛等於沒有守衛
· types.ts 的 ARCRUN_BUNDLE_VERSION 重複宣告(TS2300)併回一處

驗證見 PR:cli 49/49 綠、cypher health 4/4 綠、Portal 版本行原始碼實跑五種情境、
對真實已部署 worker 的唯讀 dry-run。**未做**:真實實例上的 acr update 端到端
(本機唯一有憑證的帳號是 leo21c=紅線禁碰,youlin 無憑證)。

Refs: Leo/Arcrun#106, #97, #95

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-12 21:58:43 +08:00
uncle6me-web f87d0e92f4 fix(migrations): 0005/0006 從來沒進過版控——被 *.sql 規則吃掉,每個用戶都收到「部署物缺」
leo 更新 leo21c 時撞到(他問「部分失敗?」):
  ✗ D1 migration: 部署物缺 kbdb/migrations/0005_credential_template.sql
  ✗ D1 migration: 部署物缺 kbdb/migrations/0006_drop_credentials_table.sql
(四顆 worker cypher/registry/kbdb/mcp 全部 ✓,失敗的只有這兩個檔)

根因不是誰忘了推:.gitignore:53 的 `*.sql` 是為了擋 D1 匯出備份(整庫全量=機敏),
但它連 migration 一起吃掉。0001-0004 還在,只因為它們在該規則之前就 commit 了
(gitignore 不影響已追蹤檔案)⇒ 0005/0006 從產生那天起就不在任何 clone 裡。

⇒ 這不是 leo 一台的事:更新指令從 Gitea 抓 main,那兩個檔不在那裡
   ⇒ **任何人裝/更新都會收到同一組失敗**,包含全新安裝。

修法照 rules/05-deploy-convention.md「WASM 來源」段已有的慣例
(`.component-builds/**/component.wasm` 就是用否定規則放行的):
  !kbdb/migrations/*.sql

範圍實測(沒開太大):
  kbdb/migrations/0005、0006      → 放行
  backup-2026.sql / kbdb/backup-x.sql / dump.sql / cypher-executor/export.sql → 仍被擋

進版控前確認過無機敏值:grep 命中的 token/secret/api_key 全是欄位名
(api_key、secret_ref)與註解;無 >=20 位英數的疑似真值。

殘項:leo21c 實查 templates 9 個、credential 不在其中 ⇒ 0005 從未套用,
那台仍停在 D38 之前(credentials 走 0002 的獨立表)。要補套需另跑一次更新。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-12 21:10:12 +08:00
uncle6me-web ba152bc83a chore(builds): 重編 tier2 成品——把 #105 放進執行檔(出貨路徑 A 的第 0 步)
leo 選 A(把出貨線推到能出一版含 #105 的 bundle)。查證後發現最前面還有一層:

  cypher 源碼最後動 10d150a(#105)  / 成品最後動 a24f291(更早)
  mcp    源碼最後動 10d150a(#105)  / 成品最後動 8e10f1d(更早)

⇒ #105 改了 cypher 與 mcp 兩邊源碼,但沒有重編成品。
   這正是 Arcrun#93 那道「源碼比執行檔新就停下來」的閘要擋的狀態。

用官方唯一編譯點 scripts/build-worker-artifacts.mjs(Arcrun#80 的機制,已存在)
重編五顆,全部 5/5:
  arcrun-cypher-executor  571KB  source=10d150ac
  arcrun-kbdb             146KB  source=10d150ac
  arcrun-mcp             1152KB  source=10d150ac
  arcrun-http-request      78KB  source=1e85dfb4(未變)
  arcrun-code             150KB  source=621cb8d9(未變)

交叉驗證(不只信它自記的 commit):/portal/data/ 這條 #105 才有的路徑
在 arcrun-mcp 成品裡出現 8 次。

下一步(等 leo 解閘):把修好的引擎部署到 geek6688 當出貨機
(ARCRUN_SHIP_BASE 可覆寫,預設是 leo21c——arcrun-rag#79 要搬離的正是這個),
再從那台跑出貨線,第 17 站 purge 的 wait 節點才有帶修法的引擎可跑。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-12 20:03:30 +08:00
Leo 89b80ff90e Merge PR #105: MCP 用登入者的身分查詢,不再去找服務內部金鑰
總管複驗(不聽自述,自己重跑,並與 base a24f291 逐條比對):
  mcp     tsc 乾淨;vitest 113/113 綠
  cypher  14 紅與 base 逐字元相同(diff 無輸出);passed 386→400,新增 14 條全過
  kbdb    5 紅與 base 相同
  安全邊界 11 條全綠(跨租戶 404/越庫 404/owner_id 由 server 定死)

殘項(已記,不擋併):
  · kbdb 的 owner_id 欄位改動沒有新測試守著(213 總數未變)
  · 工作流面仍靠 MCP_OWNER_NAMESPACE || leo 那個巧合,本 PR 刻意沒動(拔了 arcrun_* 會全面失效)
  · 端到端  未驗:要部署到 leo21c,那道閘要 leo 親手解
2026-08-12 11:42:03 +00:00
uncle6me-web 10d150ac2b fix(mcp): MCP 用登入者的身分查詢,不再去找一把服務內部金鑰
leo 2026-08-12:「人類進 Portal 輸入帳密表示你是主人,可以查到你權限所有東西;
AI 透過輸入帳密的 MCP 查詢表示是授權的 AI,可以查到主人允許查的任何東西。」
「掛上 MCP 並輸入帳密,那個動作本身就是授權」⇒ 下游不得再要求第二次認證。

病根(不是金鑰沒同步,是身分沒接住):
  oauth/routes.ts 驗完 Portal 帳密只留下 `loginOk = res.ok` 一個布林值,身分當場丟棄,
  namespace 改從 `MCP_OWNER_NAMESPACE || "leo"` 拿。於是查詢時手上沒有身分可帶,
  只好用 KBDB_INTERNAL_TOKEN 直打 KBDB——那條路繞過 portal 所有庫過濾,
  而且不管誰登入都看到同一格、看到全部。CLI 也從不注入 MCP_OWNER_NAMESPACE,
  所以那個 "leo" 預設值是每台實例的實際行為,不是理論上的邊角。

修法(走既有那條路,不發明新的):
1. 接住身分:/authorize 解析 /portal/login 回應,把 portal session token +
   display_name/role/libraries 存進 authorization code → access token。
   /portal/login 補回 session_expires_in,access_token TTL 夾成
   min(自己的 TTL, portal session TTL)——不讓「MCP 還連著、底下 session 早死」。
   cypher 回 200 但沒給 session_token(舊版)→ 不發碼,不簽一張沒有身分的 token。
2. 攜帶身分:kbdb_* 全部改走 cypher `/portal/data/*`,Authorization 帶登入者的
   session。庫過濾/租戶注入/停用即時生效全在 server 側,與人類走 portal 網頁同一道閘。
   kbdb_graph_neighbors 因此不再需要 kbdb_base(server 自己知道查哪個庫)。
   藏書地圖(含連線時注入 instructions 的那份)同樣只回有權限的庫,快取改 per-session
   分格——地圖本身就是情報,不能讓先連上的人把視野留給下一個。
3. fail-closed:舊 token 沒有身分 → 誠實要求重新連線,不偷偷退回服務金鑰那條老路。
   服務級憑據(static token / partner key)維持既有 KBDB 直連,arcrun_* 零回歸。

新增 cypher portal 資料面端點(能力長在 API,MCP 只暴露;rule 07):
  GET  /portal/data/map、/portal/data/map/:library
  GET  /portal/data/templates、POST /portal/data/templates
  GET  /portal/data/records/by-template/:t、GET /portal/data/records/:id
  POST /portal/data/records
全部:呼叫端自帶 owner_id 一律不生效;越權與不存在同回 404;寫入 owner_id 由 server 定死。

KBDB base:`GET /records/:id` 與 by-template 補回 owner_id 欄位——原本不回,
呼叫端無從判斷「這筆是不是我的」,按 id 直讀等於沒有租戶邊界。

沒動:KBDB fail-closed 閘、任何金鑰、租戶字串仍不下發給呼叫端。

驗證:
  mcp        tsc 綠;vitest 113/113 綠(改前 48 綠 29 紅)
  cypher     vitest 400 綠 / 14 紅,14 紅與 base commit a24f291 逐條相同(既有)
  kbdb       vitest 208 綠 / 5 紅,5 紅同為既有(migrations/*.sql 被 gitignore)
  端到端     ◐ 未驗:需部署到 leo21c,那道閘要 leo 親手解(見 PR)

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-12 19:33:12 +08:00
uncle6me-web a24f2912eb chore(builds): 重編執行檔——把 wait 的修法真的放進引擎成品(Arcrun#93 的閘抓到的)
#101 併進 main 後,.worker-builds/arcrun-cypher-executor 還記著 a5e4caf,
比源碼 HEAD 少了 f1370e2(wait 搬回引擎那筆)。

⇒ 這正是 #93 那道閘存在的理由:**沒有它,這次出貨會送出一個
「引擎裡沒有 wait 修法」的成品**——leo 更新完照樣燒 CPU,而出貨線全綠。
閘寫出來的隔天就抓到一次,抓到的還是我。

arcrun-cypher-executor  source a5e4caff1370e22  sha256=8411ed59b7ad

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-12 15:30:26 +08:00
Leo 1791ffa497 Merge pull request '#101 等待搬回引擎——WASI 沙箱裡沒有「不花 CPU 地等」這種東西' (#103) from fix/wait-not-in-wasm-101 into main 2026-08-12 07:26:00 +00:00
Leo 296fb01247 Merge pull request 'portal 安裝完成清單拿掉「去 Google 申請 AI 金鑰」那一項(arcrun-rag#81)' (#102) from fix/portal-onboarding-drop-gemini-key-81 into main 2026-08-12 07:24:47 +00:00
uncle6me-web f1370e2275 fix(engine): 等待搬回引擎——WASI 沙箱裡沒有「不花 CPU 地等」這種東西(Arcrun#101)
leo 在 youlin stage 實測(只有 input >> wait 兩個節點):
  ms=3000 → 38.9s 後 503(1102) / ms=20000 → 34.0s / ms=30000 → 34.9s / 寫死 3000 → 34.8s
四個值同一種死法、與 ms 無關 ⇒ 病不是「等待很貴」,是「等待從來沒成功過」。

修法:wait 移進 BUILTIN_COMPONENTS,由引擎 await 一個 timer。
只花 wall-clock、不記 CPU ⇒ 等 30 秒與等 3 秒同價(皆 ≈0)。
I/O 契約沿用 component.contract.yaml,既有 workflow 的 wait 節點定義不必改。

🔴 誠實標明:原本註解斷言「Workers 時鐘在同步執行期間凍結,所以自旋永不結束」。
寫測試去證,反而被打臉——workerd 裡自旋 2553 圈後 Date.now() 就前進了。
那條假斷言已刪除(不是改鬆),完整機制降級為推測。修法不依賴它:
純 WASI 沙箱本來就沒有睡覺這個手段,會等的只有宿主。

實測:
  npx vitest run tests/wait-builtin.test.ts  → 12 passed (12)
  npx vitest run(全套)                      → 386 passed / 14 failed
                                              (14 = 動工前的既有紅燈數,未新增)

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-12 15:15:08 +08:00
uncle6me-web d58a6e152d fix(portal): 安裝完成清單拿掉「去 Google 申請 AI 金鑰」那一項(arcrun-rag#81)
leo 2026-08-12:「已經改用 workers AI,刪掉。」

那一項長在**安裝完成清單裡而且是勾選項** ⇒ 使用者會以為不做這步就沒裝完,
而它要人離開流程、去第三方網站申請帳號、把金鑰貼進表單
——**這是整條安裝路徑上最重的一個動作,而它現在是白做的**。

連帶:步數不再寫死「還差 2 步」,改成由剩下的項目算出來
(少一項卻還寫 2,是另一種說謊)。
2026-08-12 14:44:38 +08:00
uncle6me-web 793a94ecb5 chore(worker-builds): 重編——#100 的修法要進執行檔才會送到用戶那台
cypher-executor source 525faaf → a5e4caf(sha 49d59597 → d1765930)。
今天第三次在同一件事上被提醒:改完源碼沒重編,出貨與安裝送出去的還是舊的。
2026-08-12 13:36:39 +08:00
uncle6me-web cbeddf7535 merge: 讀不到就說讀不到——總圖不再把「讀不到」畫成「你沒有」(Arcrun#100)
總管審過並自己跑了驗證:
  cypher-executor 374 pass(原 357,+17 條),14 個失敗與 main 基準線完全相同
  反向驗證:拿掉帶認證那一行 ⇒ 14 → 17 failed;還原 ⇒ 回到 14

它挖到兩件我沒交辦、而且比原題更嚴重的:
  · /triplets/stats 的 total 是**分頁長度不是總數**(預設 limit=100)
    ⇒ 只修 401 的話畫面會從「0」變成「100」——一樣是假的
  · 數 entries 原本不帶 owner 過濾 ⇒ **會混到別的租戶**(實測 459,137 vs leo 的 458,732)
2026-08-12 13:36:17 +08:00
uncle6me-web d7c6bd0680 merge: 更新不再照名字找資源——已部署 worker 綁著什麼就是什麼(Arcrun#97)
總管審過並自己跑了驗證(那條線被權限閘擋住,沒能執行):
  新測試 20 條全過(含四種「說不準就停手」情境)|CLI 全套 38 pass / 0 fail
  反向驗證:把「照名字 ensure」原語加回去 ⇒ 紅線測試當場變紅(0 pass / 1 fail)
  重編 dist:舊的 ensureKvNamespace/ensureD1Database 在產物裡 0 個

它做的比交辦的多:加了兩條紅線測試(cf-api 不得再提供「找不到同名就順手建一顆」的原語、
只有 resource-resolver 能決定要不要建),以及一條我沒想到的——
**部署出去的 toml 不得殘留官方帳號的資源 id**(自架寫進官方庫=跨租戶外洩)。

誠實標記:驗證是本機模擬(假 CF API),**沒有在真機上跑過**。
2026-08-12 13:36:17 +08:00
uncle6me-web a5e4caf5cb fix(portal): 讀不到就說讀不到——總圖不再把「讀不到」畫成「你沒有」(Arcrun#100)
leo 2026-08-12 打開總圖看到:「0 個實體 · 0 條關聯/知識庫還沒有任何關聯——
上傳文件後 AI 會自動織網」。**而他庫裡有 1854 條三元組。**
那句話會叫他去做一件不需要做的事。

三個獨立的洞疊起來才變成那句謊:
  ① console-dashboard.ts 打 kbdb-graph-plugin 沒帶認證(同段落打 kbdb 的兩支都有帶)
  ② 那顆 worker 不在更新的部署清單裡 ⇒ token 一換它就落單
  ③ **畫面把 null 畫成 0**——後端已經誠實回 null 了,是前端把它變成謊話

為什麼一直沒被發現:graph plugin 原本身上沒有 token ⇒ 門開著 ⇒ 沒帶也進得去。
2026-08-12 輪替後它有了 token,門關上,401 才浮出來。

📍 repo:matrix/arcrun(cypher-executor/src/routes/、console-ui/public/)
📍 票:Leo/Arcrun#100
2026-08-12 13:33:53 +08:00
uncle6me-web 45a546a686 fix(cli): 更新不再照名字找資源——已部署 worker 綁著什麼就是什麼(Arcrun#97)
病根:更新會「確保」它需要的資源存在,而它是**照名字找**的。
使用者的資源是安裝器建的(`arcrun-rag-<x>-kv-webhooks`),更新找的是 `WEBHOOKS`
⇒ 找不到 ⇒ 新建一顆空的並綁上去。

2026-08-12 實撞(leo21c):一次例行更新後
  KV 9 顆 → 18 顆、D1 1 顆 → 2 顆,worker 全綁到新建的空的
  ⇒ 工作流一支都看不到、portal 登出、總圖空的、80 把 recipe 解不出來
  leo 原話:「leo21c 是掛掉的」。資料沒掉,但從他的角度就是東西全不見了。

修法方向:**已經部署上去的 worker 綁著什麼,那就是事實**——
名字是使用者那側的事,不是更新指令可以決定的。

📍 repo:matrix/arcrun(cli/)|📍 票:Leo/Arcrun#97
2026-08-12 13:30:17 +08:00
uncle6me-web e69d6bbc03 chore(worker-builds): 重編——kbdb 的跳脫修法要進執行檔才算數(Arcrun#94)
今天已經在這件事上被咬過一次(8e10f1d):源碼改了、執行檔沒重編,
出貨會全綠地送出舊的。這次併完就重編,不留同一個坑。

kbdb source 525faaf → c497ec4(sha256 7bc66656 → ffb8d434)。
其餘四顆源碼沒動、位元組也沒動。
2026-08-12 12:22:10 +08:00
uncle6me-web b302c03ea8 merge: 搜尋框打的 % 和 _ 是字,不是萬用字元(Arcrun#94)
總管逐筆審過兩筆,並自己跑了測試(那條線被權限閘擋住沒能執行):
  前後對照  查 100% 改前撈回「共有 100 個待辦項目」→ 改後只回真的含 100% 的
            查 owner_id 改前連 ownerXid 也中 → 改後只回字面命中
            單打 % 或 _ 改前整個庫都回來 → 改後只回字面命中
  反向驗證  拿掉跳脫 → 8 failed(紅的正是病徵本身)
            拿掉 ESCAPE → 5 failed(含既有 search-long-query 兩條)
  既有測試  18 檔 197 pass → 19 檔 213 pass

它連帶處理了一個我沒想到的:跳脫會讓 pattern 變長,所以長度上限改用「跳脫後」的
長度算——否則打 48 個 % 會產生 98 bytes 的 pattern,退回 08-03 修掉的那個 HTTP 500。

誠實標記:只在本機真 SQLite 上驗過,沒對任何線上實例跑過。
同款漏洞另有兩處未動(library-map.ts:169、library-backfill.ts:75-76,吃維運端前綴參數)。
2026-08-12 12:21:48 +08:00
uncle6me-web c497ec418e test(kbdb): 用真 SQLite 釘住 #94 的前後對照與跳脫邊界
只驗 SQL 形狀不算數——「% 被當成萬用字元」這件事,只有真的跑一次 LIKE 才看得見。
用 node:sqlite(與 library-map/embed-backfill 同款治具)跑真 SQL,每組驗收同時跑
「舊寫法」與「現行寫法」,讓前後對照直接長在測試裡。

守五件事:
  ① 前後對照   —— 100% / owner_id / 單打符號,改前撈回不相干的、改後只回字面命中
  ② 跳脫邊界   —— `\` 不跳脫會漏掉真正的 `C:\`,也會讓 `_` 的跳脫失效、萬用字元漏回來
  ③ ESCAPE 齊全 —— 沒有任何 `content LIKE ?` 是裸的(漏一個就等於那條路沒修)
  ④ 不退化     —— 不含 % _ \ 的查詢 pattern 逐字不變,最熱路徑仍是單一 LIKE
  ⑤ byte 預算  —— 滿是 % 的長查詢仍在 D1 的 50 bytes 上限內(承 2026-08-03 的 500 修復)

反向驗證(把修法拿掉,兩半分別驗,兩半都是承重的):
  拿掉跳脫      → 8 failed / 204 passed,紅的正是病徵本身
  拿掉 ESCAPE   → 5 failed / 207 passed(含既有 search-long-query 2 條)
  復原後        → 19 檔 213 pass(基線 18 檔 197 pass)
2026-08-12 11:59:36 +08:00
uncle6me-web 6985bf4850 fix(kbdb): 搜尋框打的 % 和 _ 是字,不是萬用字元(Arcrun#94)
使用者在搜尋框打 `%` 或 `_`,搜出來一堆跟他打的字無關的東西。

病根:pattern 一直是 `'%' + 使用者輸入 + '%'` 直接內插,而 SQLite 的 LIKE 有
兩個萬用字元(`%` 任意長度、`_` 任意一字)且**沒有預設跳脫字元**——不寫
ESCAPE 就沒有任何辦法表示「字面上的 %」。所以他打的符號被當成 pattern 語法:

    `100%`     → `%100%%`  → 「100」後面接什麼都算 ⇒ 撈回一堆不相干的
    `owner_id` → `%owner_id%` → `_` 匹配任一字元 ⇒ ownerXid 也中
    單打 `%`/`_` → `%%%`/`%_%` → 整個庫都回來

舊病,不是 08-10 斷詞(#84)引進的:pattern 從來就是這樣拼的。之前關鍵字搜尋
幾乎恆為 0 命中,這個洞被那個洞蓋住;斷詞讓搜尋真的會回東西之後才浮出來。
斷詞那段一個字都沒動。

修法:pattern 產生點全部收斂到兩支 helper——
  · escapeLikeLiteral():跳脫 `%` `_` `\` 三個字元
  · CONTENT_LIKE 常數:每個 `content LIKE ?` 一律帶 `ESCAPE '\'`

為什麼跳脫字元本身(`\`)也要處理:宣告 ESCAPE 之後 `\` 就變成 pattern 裡有
意義的字元,而且它會把後面那個字吃掉、且不報錯——打 `C:\` 會變成去找 `C:%`
(真正的 `C:\` 反而漏掉);只跳脫 %/_ 而漏掉 `\`,打 `C:\_temp` 時 `\\` 先被
讀成字面 `\`、後面的 `_` 變回萬用字元 ⇒ 撈到 `C:\Xtemp`。三個是一組的。
`[` `]` `?` `*` 不需要跳脫(那是 GLOB/別的方言,LIKE 不吃),多跳只會白白吃掉
pattern 的 byte 預算。

連帶:跳脫會變長(`%`→`\%`),所以 50 bytes 上限改用「跳脫後」的長度算
(likeBytes),否則打 48 個 `%` 會產生 98 bytes 的 pattern ⇒ 退回 2026-08-03
修掉的那個 HTTP 500。不含這三個字元的查詢 likeBytes ≡ utf8Len ⇒ 既有查詢逐字不變。

search-long-query.test.ts 兩條「逐字比對謂詞字串」的斷言跟著新字串更新——改的是
比對用的常數、不是放寬檢查(仍逐字相等比對),pattern 本身一個字都沒變。

沒有引進第三種搜尋機制,沒有動資料層(三表不變),沒有動斷詞。
2026-08-12 11:59:36 +08:00
uncle6me-web 8e10f1d83e chore(worker-builds): 重編官方成品——kbdb/cypher-executor 的修法之前只在源碼裡
為什麼要有這一筆:`.worker-builds/` 是**出貨與安裝真正拿去部署的執行檔**,
而它記的 source_commit 在剛併完 #85/#88 之後對不上源碼:

    arcrun-cypher-executor  成品記的 797e7f7  源碼已經是 525faaf  ⚠️
    arcrun-kbdb             成品記的 a7e23ba  源碼已經是 3eb8b31  ⚠️

⇒ 這時候去出貨或 `init`,送出去的是**舊的執行檔**,測試全綠也沒用
(wiki/mistakes.md「修的東西在執行檔裡,改完源碼+測試綠 ≠ 送到用戶手上」)。

🔴 build-bundles.mjs 的「落後閘」抓不到這個:它比的是「.worker-builds 這個目錄有沒有
落後 main 的 commit」,不是「成品記的 source_commit 有沒有落後那顆 worker 的源碼」。
這次是人工比對才發現的。修這道閘另開票。

重編後:cypher-executor source=525faaf、kbdb source=3eb8b31,兩顆的新程式碼都在產物裡
(kbdb 找得到 embed_backfill_usage/backfillEntryLibraryTags,cypher 找得到 builtin 那條路)。

誠實標記:另外三顆(code/http_request/mcp)源碼沒動,位元組卻變了——是 esbuild
版本漂移換了它產生的 helper(`__esm` 多了一段 try/catch)。這正是 #77 記的
「build 不可跨機器重現」那個結構性斷點的實例。
2026-08-12 09:50:50 +08:00
uncle6me-web 3eb8b31f2b merge: 補算向量的節奏、額度、世代核對+標庫共用同一顆閘(Arcrun#85/D68/D69)
總管逐筆審過 1d6dde4/674e1b4/37e13fc:全部落在 kbdb/,沒有新表(額度用量寄生在
既有 entries 表單一列,D38)、沒有動實例。標庫與時間分層共用一套 SelectionCriteria
與同一顆每日 D1 額度計數器——兩者若各記各的,其中一個會把另一個的閘繞過去。
實測:kbdb 全套 196 pass / 0 fail(含反向驗證:拿掉 cap 那個 case 會變紅)。

紅線仍在:這只是併進 main,還沒部署到任何實例。
2026-08-12 09:40:30 +08:00
uncle6me-web 8cee9c9f76 merge: /cypher/search 不再誤報執行期原生零件 not_found(Arcrun#88)
總管逐筆審過 525faaf:改動只在 cypher-executor 的 search-nodes.ts(查 registry 前先比對
執行期白名單)與 component-loader.ts(把既有三份白名單的聯集匯出),沒有新清單、
沒有掃目錄灌死碼。實測基準線:本分支 357 pass / 14 fail,main 350 pass / 14 fail
——同樣的 14 個既有失敗(portal-data 等 5 檔),新增的 7 個全綠。
2026-08-12 09:40:18 +08:00
uncle6me-web 7e3ca4c1a1 Revert "WIP(kbdb): 語意搜尋零命中的排查——⚠️ 被總管中途叫停,未完成驗證"
This reverts commit af3edff856.
2026-08-11 23:18:38 +08:00
uncle6me-web af3edff856 WIP(kbdb): 語意搜尋零命中的排查——⚠️ 被總管中途叫停,未完成驗證
leo 2026-08-11 判斷「如果是 Vectorize 沒完成就不用查了」,總管據此停線。
真因已經寫在 repo 自己的註解裡(kbdb/wrangler.toml:43-51,Arcrun#11):
metadata index 只收「建立後 upsert」的向量,既有向量須 reindex,
否則帶 owner_id filter 一律 0 命中——與實測每一格吻合
(805 筆在、關鍵字搜得到、語意 0、拿自己查自己也 0 ⇒ 不是分數門檻)。

⚠️ 這批改動是排查途中的產物,**沒有走完驗證**,不要當成可用的修法。
保留只是不讓它憑空消失(總管中斷造成,不是它做壞)。
接手的人請先讀 Arcrun#85 上的結論再決定要不要用。

真正的補救是 reindex,而 reindex 要燒 AI 額度
⇒ 卡在 Arcrun#85 的每日額度閘上線之後才能做。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 23:05:20 +08:00
uncle6me-web c5d696556e 保管:#89/#90/#91 卡在人類閘前的產物,從 session 暫存目錄搶進版控
三樣都做完並實測過,但落地的最後一步是終端機裡等人親手打字的互動閘。
它們原本只存在於某個 session 的 scratchpad——那種目錄一關就沒了。

· recipes/gitea_put_file.yaml     出貨線 7 站等它
· recipes/cf_worker_deploy_simple.yaml  ⚠️ 只適用無 bindings 的簡單情形(見 #90)
· hash-component/                 sha256/sha1/md5,已與系統原生指令逐位元核對
  (.wasm 是 1.3MB 編譯產物,不進版控,README 附重編指令)

README 寫了落地指令與各自的注意事項。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 22:38:55 +08:00
uncle6me-web 5919c6b90f fix(kbdb): 藏書地圖讀端每次都觸發重算寫 D1(Arcrun#87 止血)
liveTripletCountsByLibrary 沒有過濾 status,recomputeLibraryMap 只算
COALESCE(status,'active')='active'——兩邊判準不一致,只要庫裡有一筆
superseded triplet 就永遠判定 stale,導致每次讀地圖(GET /map、GET /map/:library、
kbdb_get_map MCP 工具)都觸發重算並新建一筆 library_map record,無止盡寫 D1,
且加劇既有的非原子 supersede 競態(kb 44 筆全 superseded、notes 兩筆同時 active
即 arcrun-rag#50 的共同根因之一)。

修法:liveTripletCountsByLibrary 的 SQL 改成 pivot 出 status 再套用與
recomputeLibraryMap 逐字一致的過濾,兩邊判準對齊。

新增迴歸測試釘住此 bug(反向驗證:跑在修前的 SQL 上會失敗,非空氣測試);
獨立用 leo21c MCP 連線連讀兩次 kbdb_get_map() 重現修前症狀
(general 庫 updated_at 從 1786457080 前進到 1786457114,中間無寫入)。

kbdb/tests/library-map.test.ts 19/19 全綠。既有殘骸(100 筆 library_map record)
未清——目前沒有可用的 DELETE 通道,待部署後另行處理。

kbdb-sql-ok:liveTripletCountsByLibrary 的 .prepare 呼叫是牆內本體
(kbdb/src/actions/),本次 checkout 開在巢狀 worktree
matrix/arcrun/.worktree-fix-87/(避免打斷另一 session 佔用中的
matrix/arcrun 主 checkout),kbdb-api-wall-guard.sh 的字面路徑比對
*matrix/arcrun/kbdb/src/* 吃不到中間多出的 worktree 目錄層,非真的繞牆。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 22:16:30 +08:00
uncle6me-web 525faaf5d0 fix(cypher-executor): /cypher/search 不再誤報執行期原生零件 not_found(Arcrun#88)
病因:/cypher/search 只查 component registry(SUBMISSIONS_KV,經 submitComponent/
index-only 才有記錄);而 component-loader.ts 能直接解析、從不查 registry 的一整類
零件(trigger_workflow/BUILTIN_COMPONENTS/LOGIC_BINDING_MAP/WASM_HTTP_RUNNER_IDS,
如 if_control/http_request/switch)從未被 submit 過。leo21c 實例實測:
GET /components/catalog 回 404「零件 catalog 不存在」,search 因此對這些零件誠實地
回「兩庫都查過沒有」——但它們其實跑得動(leo 08-11 探測工作流已證)。

修法:從 component-loader.ts 匯出 RUNTIME_NATIVE_COMPONENT_IDS(既有三份執行期
解析白名單的聯集,非新清單),search-nodes.ts 在查 registry 之前先比對,
不受 registry 是否可達/是否已 backfill 影響。真正不存在的零件仍誠實回
not_found/unknown,not_found 的分型建議與相似候選機制不變。

刻意不做:不掃描 registry/components/* 目錄當清單來源——那含已標記待刪的死碼
(km_writer/kbdb_upsert_block),07-30 曾把這類死碼誤灌進 registry;也不投資
SUBMISSIONS_KV 的 backfill 腳本——decisions-summary.md D29 已定調 SUBMISSIONS_KV
併入「KV 退休戰」,不宜再加投資。

測試:cypher-executor/tests/search-nodes-runtime-native.test.ts 7 case 全綠,
複現 registry unreachable/registry 可達但目錄空(leo21c 實例的真實症狀)兩種情境。
全 suite 迴歸:357 pass(較修前 350 pass 多 7 個新測試),既有 14 個失敗與修前
數量、內容完全相同(pre-existing,與本次改動無關)。
2026-08-11 21:51:21 +08:00
uncle6me-web 37e13fc9bf merge: 併入 main 最新(b6ef0f0 三元組 library 補標 proxy+測試)保持分支不落後 2026-08-11 18:46:03 +08:00
uncle6me-web 674e1b4fa2 fix(kbdb): D69 節流世代核對+標庫共用 D1 額度,補「挑哪一批」的統一 SelectionCriteria(Arcrun#85)
leo 逐行複核 fix/embed-backfill-d68 後點出的破口+二次裁決(票上全文見 Leo/Arcrun#85):

一、向量化優先序(今天寫的立刻/本週在跑的先跑/有查詢紀錄的庫優先/半年前慢慢跑)表達
不出來——策略要能從外面(工作流)指定,不能焊死在資料層。新增 embed.ts 的
`SelectionCriteria`(owner_id/source/library/since/until),backfillEmbeddings 與
reconcileEmbedGeneration 共用同一套形狀;「按庫」那一層現在有資料可用即可運作(見下)。

二、世代核對(reconcileEmbedGeneration)不打 AI 但逐筆寫 D1,47 萬筆候選 ≈ 4.7 倍 D1
100,000 rows/日免費額度,先前零保護。新增 actions/maintenance-quota.ts(單一 entries
列/日的共用計數器,精神同 execution-log.ts/embed.ts 既有慣例,不新增表)。

三、leo 二度裁決:「標庫」與「時間分層」其實是一件事,判定標準要從第一天同時容納兩者,
不能先做一半再回頭改。新增 actions/library-backfill.ts 的 backfillEntryLibraryTags——
呼叫端(ingest/daemon/Arcrun#87)決定要貼哪個庫、用 page_names(Gitea 原稿卡名精準
點名,leo 定案的正解)或 source_prefix/page_name_prefix 過渡 fallback 篩選候選,base
只負責安全、節流地寫入。owner_id 刻意必填(leo 點出「補錯 owner 等於白做」——實查卡片
掛在 owner_id=bfezv28v,換成 'leo' 查卻是空的)。

D69:reconcile 與標庫 backfill 共用同一顆「今天還剩多少 D1 寫入額度」計數器(不共用的話
其中一個會把另一個的閘繞過去);新增 POST /entries/backfill-library + GET .../status,
擴充 POST /embed/backfill 與 /embed/reconcile 吃 library/since/until 參數。

測試:92 → 39 個新增/擴充案例覆蓋 since/until/library 篩選、reconcile 額度真的擋
(含「拿掉 cap 會變紅」反向驗證)、標庫 backfill 冪等/owner_id 必填/page_names 精準比對、
以及兩個操作共用同一顆額度計數器的跨模組驗證(雙向:先 reconcile 耗盡再標庫、反之亦然)。
kbdb 全套 192 個測試綠燈,tsc --noEmit 除既有 auth.test.ts 舊缺陷外無新增錯誤。

紅線:未併 main、未部署、未動任何實例的 is_embedded 旗標(只在本地 SQLite 測試治具跑過)。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 18:39:55 +08:00
uncle6me-web b6ef0f07dc fix(kbdb): 開通 PATCH /kbdb/records/:id proxy + 補三元組 library 補標測試
leo 2026-08-11:貼 wiki 卡「這裏就有 3 個三元組,如果三元組是 0 一定是有錯」——
三元組沒有不見(新式 1,633 筆/舊式 636 筆都還在),問題是地圖看不到:新式三元組
只有 171 筆填了 library slot,其餘因寫入時 template 還沒有該 slot 而被靜默丟棄
(record-crud.ts createRecord 只替「已宣告」的 slot 建值,不是替 caller 傳的
values 建值——順序錯了值就消失,不報錯)。

本次改動:
- cypher-executor/src/routes/kbdb-proxy.ts:補 PATCH /kbdb/records/:recordId。
  基本盤(kbdb/src/routes/records.ts)早有這個端點(mira-dissolve T2.1 的
  updateRecord),但這條 proxy 之前只轉發 GET/POST /kbdb/records,外部(工作流/
  CLI/補標腳本)打不到——能力在,通道沒開,補標三元組只能繞去改表(違 D38)。
  純轉發、無業務邏輯,比照既有 PATCH /kbdb/entries/:id 慣例。
- kbdb/tests/triplet-library-backfill.test.ts:真 node:sqlite 驗三件事——
  ①源頭順序(ensure-slot 必須在 write 之前,事後補救不了已寫的那筆,重現+
  驗證正解)②存量補標(地圖輸出前後對照)③不會重複做(同批跑兩次,第二次
  touch 0 筆)。順手記錄一個相關但不在本次範圍的小落差:liveTripletCountsByLibrary
  的 'general' fallback 桶與 recomputeLibraryMap 的精確比對語意沒對齊。
- cypher-executor/tests/kbdb-records-patch-proxy.test.ts:新端點的租戶閘/
  參數驗證/轉發/404 透傳。

全數綠燈:kbdb 181/181、cypher-executor 新增 8 案全過(既有 14 案失敗為
pre-existing,已用 git stash 比對確認與本次改動無關)。

kbdb-graph-plugin(實際的三元組寫入端 createTriplet/ingestEnvelope)需要同款
修正(library 補進 TRIPLET_SLOTS + 從 source_uri 推導),但該 repo 現行
0 份 active SDD,其 sdd-guard 明確要求人決定要不要開啟——本次不越界代為決定,
留在報告中交回。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 16:48:31 +08:00
uncle6me-web 1d6dde4a01 fix(kbdb/embed): D68 補算向量照新到舊排序+每日額度上限+修復舊世代 is_embedded 誤判
leo 2026-08-11 拍板(D68,system-dev/wiki/decisions-summary.md):補算向量要照時間
由新到舊、且每天有額度上限,不能一次把 Workers AI 每日免費 10,000 neurons 燒光
(與萃取共用同一份額度,見 ops-facts.md)。對應 Leo/Arcrun#85 列出的三個缺口:
① 補算是由舊到新(ORDER BY created_at ASC)② 沒有每日額度上限 ③ 沒有任何自動觸發。

改動:
- backfillEmbeddings:ORDER BY created_at DESC(新到舊),並在打 AI 前依
  env.EMBED_BACKFILL_DAILY_LIMIT(未設用推導出的預設值 1800,算式見 embed.ts 註解)
  截斷候選、額度用完即停手不再打 AI。額度用量存在 entries 表單一列
  (entry_type='embed_backfill_usage',UTC 日期切),不新增表(D38)。
- embedOnWrite / backfillEmbeddings 成功嵌入後在既有 content_hash 欄位蓋上
  現行模型名(世代戳記),修復 leo21c 資料還原案:從備份整批灌回的列帶著對已退役
  768 維索引的 is_embedded=1,現行 1024 維索引永遠不會補到它們。
- 新增 reconcileEmbedGeneration + POST /embed/reconcile:對 is_embedded=1 但
  content_hash 非現行世代的候選,問 Vectorize.getByIds 是否真的在現行 index——
  在→只補 content_hash 不打 AI;不在→重置 is_embedded=0 交回正常 backfill 佇列。
- 新增 kbdb/tests/embed-backfill.test.ts(改走真 SQLite,比舊版手刻假 DB 更硬):
  14 個測試涵蓋新到舊排序、額度真的擋(含「拿掉 cap 會變紅」的反向驗證)、
  世代核對端到端(reconcile → 重置 → backfill 真的補回來)、既有行為不迴歸。

現況誠實回報:目前沒有任何東西會自動觸發補算(無 cron/scheduled handler)——
唯一的「自動」路徑是 entries.ts 的語意搜尋回 0 命中時 fire-and-forget 觸發一次
(既有行為,本次未改動),仍需人或 CC 主動呼叫 /embed/backfill 或掛排程。

紅線:未動 leo 正式實例 leo21c;未動資料層形狀(三表不變,仍走既有 content_hash
bookkeeping 欄);未 push main,本 commit 在獨立分支 fix/embed-backfill-d68。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 16:42:54 +08:00
uncle6me-web 507620e313 merge: Arcrun 統一編譯零件,成品放固定位置 .worker-builds/(Arcrun#80)
總管 2026-08-11 審過後併入:
- 照本 repo 既有慣例(.component-builds 底下的 wasm 本來就 commit 進來),不新造第二套
- 每顆成品自帶 source_commit + content_sha256 ⇒ 答得出「我是哪個版本編出來的」到單顆層級
- 重現性已驗:兩個不同路徑深度的獨立 clone 分別裝依賴分別編譯,
  5 顆的 content_sha256 與 wasm sha256 逐位元相同(manifest 只差 generated_at)
  ⇒ 這正是 arcrun-rag#39 的驗收條件,也是 #72 那個死結的解

待觀察(不擋本次):每次編譯會 commit 數萬行成品進原始碼 repo,長期會脹。
2026-08-11 13:54:19 +08:00