Commit Graph

350 Commits

Author SHA1 Message Date
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
Leo 1e94f8451e Arcrun#80: commit tier2 worker 官方編譯成品到固定位置 .worker-builds/
從本 repo 的 cypher-executor / kbdb / .component-builds/http_request /
registry/components/code / mcp 五個原始碼目錄,用 scripts/build-worker-artifacts.mjs
編出 5 顆成品,commit 進 repo(與既有 .component-builds/*/component.wasm 同一套
「固定位置、任何人直接拿」慣例)。

每顆 manifest 條目自帶 source_commit(該原始碼目錄最後改動的 commit)與
content_sha256,答得出「我是哪個版本的原始碼編出來的」到單顆層級。

重現性已驗證(見 commit 說明外的驗證記錄):同一個 commit 在兩個獨立 clone
(不同磁碟路徑深度,刻意模擬雲端 vs 地端的目錄結構差異)分別裝依賴、分別編譯,
5 顆的 content_sha256 與 wasm module sha256 逐位元相同,manifest.json 唯一差異
是 generated_at 時間戳。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 13:45:33 +08:00
Leo dcb6ad693b fix(build-worker-artifacts): dirty 判斷排除自己的輸出目錄
.worker-builds/ 是本腳本的輸出,在成品寫出、commit 之前永遠是 untracked——
拿它判斷「原始碼乾不乾淨」是自己把自己判成髒的假陽性。git status pathspec
排除 .worker-builds 後才是「原始碼有沒有未 commit 變更」的真實訊號。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 13:45:33 +08:00
Leo 3b0238bb28 Arcrun#80: 新增 tier2 worker 官方編譯腳本(成品固定位置的地基)
背景:安裝器(arcrun-rag)過去自己對 Arcrun 原始碼跑 esbuild,導致同一份原始碼在
不同機器編出不同位元組(見 arcrun-rag changelog 1.4.33 段、arcrun-rag#72):
① esbuild 把入口路徑寫進產物內部註解,該路徑預設相依於執行時 cwd
② 各 worker 目錄用不同套件管理器裝 node_modules,夾帶不同版本間接依賴

本腳本解法:
- absWorkingDir 固定為「本腳本自己算出的 repo 根目錄」,entry 一律用相對路徑餵給
  esbuild——不管 clone 放在磁碟哪個絕對路徑,esbuild 內部產生的相對路徑字串相同
- 不自己跑 install,強制要求呼叫者先用該目錄既有 lockfile 裝好依賴(pnpm frozen /
  npm ci),避免「install 方式不同 → 依賴版本不同 → 位元組不同」
- 每顆 worker 的 manifest entry 自帶 source_commit(該目錄最後改動的 commit),
  答到單顆層級,不是整包一個 source 欄位

涵蓋 5 顆 tier2 worker(與 arcrun-rag bundle-components.mjs CORE_COMPONENTS 對齊):
cypher-executor / kbdb / http_request / code / mcp。

scripts/ 自帶 package.json + pnpm-lock.yaml 鎖 esbuild 版本(0.24.0)——
工具版本本身也是重現性的輸入之一。

驗證:本地跑通 5/5 build;byte-identical 重現性驗證見後續 commit(兩個獨立 clone
比對 sha256)。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 13:45:33 +08:00
uncle6me-web 788295ed71 merge: 忘記密碼的錯誤訊息改白話+健康檢查回報寄信有沒有接上(arcrun-rag#38/#69/#25)
總管 2026-08-11 審過後併入:
- 拿掉行話「實例」;拿掉「請管理員直接幫你改密碼」(單人使用者就是管理員=死路,#25 同病)
- 改成給一條他自己走得完的路:重新跑一次安裝/更新
- /health 增回報 mail_relay_configured,讓安裝器判斷得出這台要不要重推
2026-08-11 13:24:34 +08:00
uncle6me-web 797e7f751c fix(portal): 忘記密碼斷點——安裝器從沒注入 PORTAL_MAIL_RELAY_BASE(arcrun-rag#38/#69/#25)
leo 被鎖在自己的系統外面:1.4.35 已出 prod,「忘記密碼」畫面在線上,但按下去回
503「這台實例還沒有設定寄信服務」。根因在 products/arcrun-rag 的安裝器——
`grep -rn PORTAL_MAIL_RELAY installer/` 是零命中,即使 landing 那半(郵差)
D62 已經寫好。這個 repo 這邊改兩件配合修:

- /health 多回 `mail_relay_configured`(布林,不洩漏網址)——安裝器判斷「要不要
  重推」只比 bundle_version,但這個 var 是這次才第一次被注入,跟版本號無關;
  純比版本號的話,已經在最新版的實例(如 leo 那台)永遠不會因為「按更新」而
  重推,這個 var 永遠補不進去。安裝器那邊會讀這個欄位決定要不要強制重推。
- portal.ts 的 503 訊息改白話(不提「實例」)+給一條使用者自己走得完的路
  (重新執行安裝/更新),不再說「請管理員幫你改密碼」——單人使用者的管理員
  就是他自己,那是死路(同源病灶:arcrun-rag#25)。

安裝器那半的修法(PORTAL_MAIL_RELAY_BASE 注入+stale 判斷)在
products/arcrun-rag 同批修(installer/oauth-prototype/worker.js)。

測試:cypher-executor `npx vitest run tests/portal-auth.test.ts tests/health.test.ts`
37/37 綠(35+2,既有測試無回歸;本次未新增測試——改動是訊息文案與健康檢查欄位,
行為已由既有測試覆蓋的路徑保護)。
2026-08-11 12:59:57 +08:00
uncle6me-web d1c44a5878 merge: 忘記密碼與改密碼合成同一個機制(D62,arcrun-rag#69/#25)
總管 2026-08-11 逐筆審過後併入:
- 三筆:8d49d88 機制/9a29eb5 瀏覽器實測抓到的兩個前端缺陷/c76e10d 回歸測試
- #66 的修正 417d69c 在本分支血緣裡,併入不會弄丟已出貨的行為
- 試併零衝突;併入後 cypher-executor 測試 360 項、通過 346
  (14 個失敗與 main 併入前完全相同,是既有紅燈,非本次造成)
- 併入後多出的 8 項全數通過=D62 的回歸測試

⚠️ 併入 main ≠ 出貨。要送到用戶手上仍需走出貨管線並由 leo 解保險。
2026-08-11 11:14:13 +08:00
uncle6me-web a7e23badf2 WIP(kbdb): 關鍵字搜尋改成斷詞——⚠️ 未完成驗證,agent 被中途停止
【為什麼要這個】leo 2026-08-10:「沒有 MCP 你就是瞎的」。
總管實測(有對照組)證明搜尋對 AI 結構上不可用:

  kbdb_search("Gemini 逃生口")  → 0 筆      ← 兩個詞從不相鄰
  kbdb_search("Gemini")         → 864 行    ← 知識明明在
  kbdb_search("local arcrun")   → 5 筆      ← 這兩字在內容裡剛好相鄰

⇒ 現況是拿整個查詢字串去 LIKE,不拆詞。而 AI 問的永遠是問句 ⇒ 永遠回 0。

【這筆做了什麼】CJK/英數分段切詞、CJK 與 ASCII 停用詞、
單詞份量上限(避免長詞獨大)、相對分數截斷;附 search-tokenize.test.ts。

🔴 【誠實聲明】**這筆沒有驗完就被停了**(leo 要求全體停工專心 MCP 進安裝清單)。
未做:前後對照的實際輸出、回歸(原本查得到的不能變查不到)、相關性不崩壞。
**接手的人不要當成已驗證的東西**,先跑那三項再說。

分支保留於 gitea,隨時可接回去。相關:Leo/mira#4 總管留言。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
(cherry picked from commit 763de199b5)
2026-08-11 10:02:09 +08:00
Leo eebb691426 merge: 傳播空窗期不再銷毀 session(arcrun-rag#66)
leo 2026-08-10 本人被這個鎖在 stage 外面,並靠修好後的重設流程走回來——
這是「收的人自己驗到」,不是我們自己測過。

病:認證的家是 Workers Secret,改它會產生 worker 新版本,既有 isolate 讀到舊 env。
舊碼在那個空窗裡讀不到 record 就直接 SESSIONS_KV.delete()
⇒ 帶著完全有效的 token,登入狀態被當場銷毀,secret 鋪開也回不來。
#55 補的加速器重試只加在登入路徑,session 驗證這道門沒有。

實測空窗約 45 秒(不是 #55 記的 ≥15 秒):
改完密碼後舊密碼一路到 t+44.1s 仍登得進去,t+47.1s 才開始 401。

三段修法:
1. 先問加速器再判定(與登入路徑同一支 hydrateFromAccelerator)
2. 永不因「讀不到」刪 session——刪是 best-effort 清潔工,清掉的卻是使用者唯一的憑據
3. 仍讀不到且在空窗 → 503 auth_store_propagating,不是 401
   (前端看到 401 就清 localStorage,後端不刪也沒用)
前端 boot() 收斂成「只有 401 才算被登出」。

驗證:stage 改密碼後同一 token 打 90 秒 → 200x30 / 401x0;瀏覽器實跑仍在站內;
tests/portal-auth.test.ts 27/27 綠。
2026-08-10 14:19:04 +00:00
Claude c76e10d314 test(portal): 補 #66 與 D62 的回歸測試(8 條,35/35 綠)
#66(三條,斷言的是「session 有沒有被刪掉」而不只是狀態碼)
- 傳播空窗(加速器 key 還在)+讀不到 record → 503 auth_store_propagating,**KV 那筆還在**
- 非空窗+讀不到 record → 401 擋下,**KV 那筆仍然還在**(刪是清潔工,清掉的卻是唯一憑據)
- session 內容本身壞掉 → 401 且**該刪**(那是確定的事實,不是暫時讀不到)
  ⇒ 前兩條在舊碼上必紅:舊碼在這兩個情況都會 SESSIONS_KV.delete()

D62(五條)
- 帶 reset_token 呼叫 /portal/password/change:**不必登入、不必現有密碼**;
  且「先刪再回」=同一條連結第二次必定 400 reset_token_invalid,KV 裡也真的沒了
- 先「看一眼」票(GET /portal/password/reset)不會消耗它
- 亂猜 / 格式不對的 token → 400
- 沒 reset_token 又沒登入 → 401(修改密碼那一格仍要身分)
- 沒設代寄服務 → /portal/password/forgot 誠實 503 mail_relay_not_configured(不假裝寄出去了)

擺放順序有註解說明:#66 那組會故意把 per-isolate overlay 灌成空的(模擬空窗),
而 overlay 是模組級全域變數不隨 test 重置,故必須排在檔案最後。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R8vF2zS2XpaZjzkC75Fjss
2026-08-10 13:55:53 +00:00
Claude 9a29eb5af6 fix(portal): 瀏覽器實測抓到的兩個前端缺陷——連結被路由吃掉、.hide 從來沒有在藏東西
兩個都是「HTTP 200 看不出來、真的開瀏覽器才會現形」的(規則五點五③)。

① `#/reset?token=…` 被路由正規化吃掉
   route() 的第一行是「raw 不在 VIEWS 就換成 HOME」,而 reset 不是站內的 view
   ⇒ 使用者點信裡的連結,網址被改寫成 `#/search`、修改密碼畫面根本沒出現,
     **而且 token 一起被丟掉**——連結一次有效,等於這條連結就這樣廢了。
   改:route() 開頭先看有沒有 reset token,有就走 showReset 並 return。
   (boot() 那條只顧得到「整頁重新載入」;hash 變動這條路以前沒人守。)

② `.hide` 一直只有三條有 scope 的規則(#tabbar .tab / #sidenav .nav / .modebtn)
   沒有通用的 `.hide { display:none }` ⇒ 任何其他元素掛上 class="hide" **完全沒被藏起來**,
   看起來有藏、其實沒藏。實測畫面:登入頁還沒按「忘記密碼」就已經露出 email 欄與「寄出連結」;
   連結模式下「現有密碼」那一格也照樣顯示(D62 的「差別只有一格」當場破功)。
   補通用規則,帶 !important 以蓋過 inline display(#forgot-box 有)。
   既有三種用法意圖一致(都是要藏),行為不變。

stage 瀏覽器實測(youlin,playwright):忘記密碼 → 取連結 → 點進去 →
不輸入現有密碼設新密碼 → 自動回登入頁 → 用新密碼登入成功 → 進站 →
設定頁同一份表單(現有密碼那一格回來)改密碼 → **沒有被踢出去、可以繼續操作**。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R8vF2zS2XpaZjzkC75Fjss
2026-08-10 13:51:41 +00:00
Claude 8d49d883c0 feat(portal): 改密碼與忘記密碼合成同一個機制,用連結不用一次性密碼(D62)
leo 2026-08-10 拍板:「這兩個機制其實是一個機制,可以簡化。」
  修改密碼:輸入**現有的** → 輸入新的 → 覆蓋
  忘記密碼:收到**「修改密碼」連結** → **不輸入現有密碼(忽略)** → 輸入新的 → 覆蓋
⇒ 同一個畫面、同一條寫入路徑,差別只有「現有密碼」那一格。

後端(cypher-executor/src/routes/portal.ts)
- POST /portal/password/change =**那一支**。帶 reset_token 就走連結那一格(忽略 current),
  沒帶就要登入 + 正確的 current。兩條路在 writeNewPassword 之後完全相同。
  /portal/me/password 保留成**別名轉呼同一支**(不留第二份實作,兩份必然漂移)。
- POST /portal/password/forgot(公開)/GET /portal/password/reset(看票,不消耗)
- 連結的安全性(承 D50,不可退讓):**一次有效**(用掉即刪,先刪再回)、
  **會過期**(KV TTL 30 分鐘)、**與註冊辨識碼不同源**(現場 crypto 亂數,
  只活在本實例 SESSIONS_KV,與 landing SIGNUPS 那組安裝辨識碼毫無關係)。
  KV 存的是 token 的 sha256,不是 token 本身。
- 🔴 不做一次性密碼(leo:「不要發一次性密碼太麻煩」)。
- 🔴 已否決不准寫回來的三條(D50):console 密碼救援/重裝重設密碼/直接用固定辨識碼。

中央代寄(arcrun-rag landing 那半在該 repo)
leo 給的職責切法:實例產生連結、管一次性/過期;arcrun.dev 只是郵差。
必須這樣切的硬理由:用戶自己的實例**沒有 send_email binding**,根本寄不了信。
🔴 總管紅線:**絕不把整條 URL 交給郵差**——寄件網域帶 DKIM,肯收「任意 URL+任意 email」
就是一台開放的釣魚中繼,燒的是整個網域信譽、不可逆。
故只交出本實例 origin + 一張回呼票,並新增 POST /portal/password/relay-verify:
郵差**回頭打這個 origin** 問「這張票是你發的嗎」,冒用別人網域會被那台實例自己否認
⇒ 主機屬於呼叫方這件事由郵差親自確認,不是相信宣稱。
信裡的連結落點 GET /portal/password/reset-link(主機刻意=被確認過的那個 origin)。

前端(console-ui/public/portal/index.html)
- 登入頁「忘記密碼」入口,**在 portal 不在 console**(leo:「是對 portal 不是對 console,
  這樣 youlin 雖然忘記,我還是可以去 portal 忘記密碼。」)
- 拆掉登入頁原有的「用管理主控台密碼救援自己」連結與「忘記密碼請聯絡管理員」
  ——兩條都是 D50 已否決的做法(console 與 portal 是同一組帳密的兩個鑰匙孔)。
- #v-reset 殼**沒有自己的密碼欄位**:真正的表單是設定頁那唯一一份 #pw-form,
  進入連結模式時被原封不動搬過去,只切換「現有密碼」那一格顯不顯示。
  同一個表單元素、同一支送出函式 —— D62「同一個畫面」的字面落地。

驗證:tsc 與 baseline 同為 23 個既有錯誤(零新增);27/27 既有測試綠;前端 JS node --check 過。
stage 實測見交付回報。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R8vF2zS2XpaZjzkC75Fjss
2026-08-10 13:23:44 +00:00
Claude 417d69ceb3 fix(portal): 傳播空窗期不再銷毀 session——讀不到 ≠ 這個人不存在(arcrun-rag#66)
leo 2026-08-10 在 youlin stage 改完密碼被登出,之後怎麼登都回不去。
病灶是「這一瞬間讀不到」被當成「這個人不存在」,而且做的是**不可逆**的動作。

認證的家是 CF Workers Secret,改它會產生 worker 新版本,既有 isolate 讀到的
還是舊 env(#55 實測 ≥15 秒)。舊的 requirePortalUser 在那個空窗裡直接把
session 從 KV 刪掉——不是擋下讓你重試,是當場銷毀,等 secret 鋪開也回不來。
#55 補的「讀不到就再問一次加速器」只加在登入路徑(findAndVerifyUser),這道門沒有。

三處修法(缺一不可):

1. requirePortalUser 先問一次加速器再判定(與登入路徑同一招、同一支函式)
2. **永不因「讀不到」刪 session**;仍讀不到且正在傳播空窗 → 回 503
   `auth_store_propagating` 而不是 401(讀得到 record 的「已停用」照舊刪,那是確定的事實)
3. 前端 boot() 從「任何非 2xx 都 dropSession」收斂成**只有 401 才算被登出**
   ——後端不刪、前端卻自己丟掉 localStorage 的 token,症狀一模一樣

順帶修掉同一族的一個資料遺失路徑:mutateAuthStore 舊版拿「可能是舊版 env」當底稿做
read-modify-write,而 writeAuthStore 會重切分片並刪掉多出來的舊分片 ⇒ 底稿若是
「某帳號被建立之前」的版本,那個帳號會在這次寫入中被抹掉且無法還原(secret 是唯一真相源)。
改成先問加速器、再把 env 版與 overlay 版取聯集當底稿;刪除仍有效(fn() 在聯集之後才跑)。

驗證:tsc 與 baseline 同為 23 個既有錯誤(零新增);tests/portal-auth.test.ts 27/27 綠。
stage 實測見交付回報。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R8vF2zS2XpaZjzkC75Fjss
2026-08-10 13:22:34 +00:00
Leo 035e8b255b chore(mcp): stage build 標記對齊實際部署的 commit
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-10 09:51:36 +00:00
Leo 1c630ecfd4 docs(mcp): /health 註解誠實界定——404 只代表比本版舊,不等於舊世代
實測:leo21c 與 geek6688 的 /health 都回 404,但 geek6688 是新世代(email+password)。
原註解會讓下一個人拿 404 去判世代而誤判。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-10 09:51:36 +00:00
Leo a99d3e5a3e chore(mcp): 補 stage 部署設定(youlin)——升級 arcrun-mcp 終於有測試場
arcrun-mcp 不在安裝器出貨的那批裡,所以「升級某台的 arcrun-mcp」一直沒有地方先驗,
要驗只能拿 leo21c(唯一一份 47.9 萬筆知識的真身)冒險。本檔把 stage 補上。

順帶把「手動直推 arcrun-mcp 到自架帳號」的兩個坑從人的記性搬進檔案:
① 拿掉 mcp.arcrun.dev route(zone 在 uncle6,自架帳號沒有)
② OAUTH_KV 填真 id(出貨 toml 是佔位符,直推不填 → /authorize 503)

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-10 09:51:36 +00:00
Leo 894408181f feat(mcp): GET /health — 一條 curl 看出這台是新舊世代 MCP
判斷一台實例的 arcrun-mcp 是哪一代認證,原本只能打 /authorize 剖 HTML 有幾個欄位
(ops-facts 2026-08-10 的土法)。改成誠實版本面:200+auth=portal-login =新世代;
404 =舊世代(同意頁還要那把沒人拿得到的 MCP_OWNER_SECRET ⇒ 等於接不上)。
順帶 build 標記(MCP_BUILD var)讓「這台跑的是哪一版」看得到。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-10 09:51:36 +00:00
uncle6me-web d022ca067b merge: kbdb DELETE 誠實回報向量刪除結果(arcrun-rag#46)
總管複核:改法與同檔 /entries/deprecate-by-library 一致(同步 await 再回應),
不動 schema、不加表(守 D38),附 84 行測試。
舊版 fire-and-forget 會把向量刪除失敗靜默吞掉——呼叫端看到「刪除成功」
但語意搜尋還留著殘影,這正是 #46 回報的症狀。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-10 15:50:36 +08:00
uncle6me-web 6715402bcc fix(kbdb): DELETE /entries/:id 向量刪除改同步+誠實回報(arcrun-rag#46)
舊版把向量刪除包成 fire-and-forget(waitUntil(...).catch(()=>{}))——失敗被靜默
吞掉,呼叫端(rag_takedown_direct workflow/未來的 portal 刪除 UI)永遠不知道
向量沒清乾淨。與同檔 /entries/deprecate-by-library(同步 await + 回報
vectors_deleted)的做法不一致,補齊同款誠實回報:回應加 vector_deleted
(true/false/null),D1 本體刪除不因向量失敗而被擋下。

補上這支端點原本零覆蓋的測試(entries-delete.test.ts,3 案:模組未開/
向量刪除成功/向量刪除失敗)。

隨附 live e2e 驗證(youlin stage,4 筆測試知識,含批次 3 筆):刪除後
keyword/semantic 混合檢索與 rag_chat AI 問答皆不再讀到已刪內容,控制組
(未刪的既有知識)不受影響——確認端到端「刪除即在所有查詢路徑消失」成立,
細節見 Leo/arcrun-rag#46 留言。
2026-08-10 15:41:01 +08:00
uncle6me-web c4cee35adb feat(auth): 認證與資料分離——搬動知識資料時登入不再跟著壞掉(D61 / arcrun-rag#55)
leo 2026-08-10 下令:「登入認證資料要分離⋯⋯就算只有我一個人存在單獨的 json 檔也好,
它不能被改資料庫的連結導致無法登入。」「昨天不能登入 portal,今天不能登入 mcp,
這根本就是一個問題。」

病:portal 帳號住 KBDB(owner_id = {CONSOLE_TENANT}::portal),console 管理員帳密住
SESSIONS_KV。兩者都靠 binding 指過去,重裝/遷移一定會被重新指一次 ⇒ 保險箱的鑰匙
放在保險箱裡。2026-08-09 leo 資料一個位元組都沒動,卻被鎖在門外。

修:認證搬到 CF Workers per-script Secrets(掛在 script 上,與 bindings 兩套資源,
重部不會洗掉;journeys/gemini-key-lost-on-reinstall.md 與 installer worker.js:1148 皆有實證)。
- 新增 lib/portal-auth-store.ts:自足的 JSON,讀取零網路呼叫,>4.6KB 自動溢位分片
- portal.ts 的帳號讀寫全部改走它;KBDB 只留為舊實例的回退讀路徑,登入成功順手搬過去
- console-auth.ts 的第二份認證資料同樣搬離 KV
- 不牴觸 D38:KBDB 三張核心表不增不減,本案是把東西搬出去
- 沿用 credentials.ts 既有的 putWorkerSecret/deleteWorkerSecret,不另造第二套寫入路徑(D36)

明顯失敗(把 #10「寧可明顯失敗,不要靜默錯置」套到門鎖上):
- 「這台實例讀不到任何登入資料」回 503 + code=auth_store_empty,且**不計入 5 次鎖定**
  (08-09 leo 就是被系統自己的誤判鎖了 15 分鐘)
- /console/setup 遇既有帳號改說「你剛才輸入的密碼沒有被採用」,不再只說「已設定過」
- /health 與 /console/auth-status 吐 auth_store 狀態(住哪、寫不寫得進去)

stage 實測撞到並修掉的坑:改 secret 會產生 worker 新版本,既有 isolate 讀到的還是舊 env
⇒ 「建好帳號立刻登入」有 15 秒以上 401,還被算進鎖定。加一層短 TTL 的 KV 加速器
(非真相源,只在 secret 查不到/密碼對不上時問一次),換 KV 不影響不變量。

驗收:stage(youlin)把知識資料庫換成另一顆空的 + 換租戶代號 + SESSIONS_KV 換成空的,
三樣一起換之後 portal / MCP /authorize / console 三條登入路徑仍全綠(複跑 3 次)。
對照組(舊版程式碼同樣換庫):登入回「email 或密碼錯誤」,5 次後鎖 15 分鐘。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-10 14:13:37 +08:00
uncle6me-web ae81d22775 fix(harness): 世代閘的 ON_TRUE 判準過時,反過來擋下正確教材
引擎自 2026-08-01 起已支援條件邊(cypher-executor/src/graph-executor.ts
case 'ON_TRUE'/'ON_FALSE'/'ON_BRANCH',VALID_EDGE_TYPES 亦已列入;31 個
cypher-executor 測試全過)。registry/skills/write_intent_workflow.md(單一
真相源)也已在同日更正為教 ON_TRUE/ON_FALSE/ON_BRANCH 是合法邊。

但 cli/scripts/check-harness-generation.mjs 的世代閘還停在舊世代判準:
只要 SKILL.md 出現正面示範的 ON_TRUE 就擋——這道閘本身才是落後的一方,
把已經寫對的教材當錯誤攔下,害乾淨 `npm run build` 必敗。

同源的過時內容還藏在三個手動維護的 harness 原始檔(非腳本產物):
CLAUDE.block.md/commands/arcrun.md/hooks/arcrun-guard.sh 都寫著
「引擎沒有條件邊」「沒有 ON_TRUE/ON_FALSE/ON_FAILURE」,一併更正。

真正不存在的邊是 ON_FAILURE(VALID_EDGE_TYPES 只有 ON_FAIL),把 mustNot
判準從 ON_TRUE 換成 ON_FAILURE,並新增 must 規則要求 ON_TRUE 必須出現,
防止教材日後又被改回「條件邊不存在」的舊世代說法。

skills/arcrun-mindset/SKILL.md 是由 registry 於建置期重建的產物
(build-harness-skill.mjs),本次改動只跑 `npm run build:harness`
重建、不手改。

驗證:故意把 SKILL.md 的 ON_FAILURE 改成正面示範,確認閘仍會擋下
(exit 1),還原後 `npm run build` 連跑兩次皆全綠且冪等(SKILL.md
md5 不變)。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-10 11:03:43 +08:00
uncle6me-web 9740794050 docs(3-specs): 立案 Arcrun App ↔ Portal 掛載協定 v0 提案(Leo/Arcrun#82)
設計全文住在票上,本檔只留指針+影響分析,等 leo confirm。
病根實查:Portal「有哪些頁」在單檔 HTML 裡寫了四遍,出貨時整包內嵌成
單檔 worker,安裝器只會整顆換掉、無任何掛載點概念 ⇒ 加一個能力=改核心。

未 confirm 前不開新 SDD、不動功能程式碼(現行 active=workflow-discovery)。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-09 22:02:08 +08:00