Files
Arcrun/system-dev/docs/3-specs/portal-auth/tasks.md
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

37 KiB
Raw Permalink Blame History

portal-auth — Tasks(實作切分,每階段可獨立 PR)

狀態:全部未動工——本 SDD 待總管/leo 審後才開工(B 類流程:SDD 審 → 實作 PR → component-pr-review-standard → 總管 review → gated 部署 leo21c 對應:Gitea #24/#25design.md 各 §

依賴鏈:P1 → P2 → P3 → P4(P2 不依賴 P1 可並行起,但 P3 要兩者都齊)。


P1 — KBDB「庫」filter 地基(design §3.2/§3.3)|觸碰:kbdb/

  • 開工第一件事:實測 Vectorize metadata filter $in 支援與否(決定主路徑 vs fan-out fallback
    • 核實結論(2026-07-14支援,走主路徑 $in,不需 fan-out fallback。證據:① 官方文件 developers.cloudflare.com/vectorize/reference/metadata-filtering/ 明列 8 運算子 $eq/$ne/$in/$nin/$lt/$lte/$gt/$gte,「For $in and $nin, filter object values can be arrays of string, number, boolean, or null values」;② 本 repo @cloudflare/workers-types@4.20260702.1 原生 typing VectorizeVectorMetadataFilterCollectionOp = "$in" | "$nin"index.d.ts:15782), wrangler 4.98.0。限制:metadata filtering 只對 2023-12-06 之後建的 index 有效(本專案 index 皆是)。 本機無 Vectorize runtime$in線上實跑併入部署排練驗(見部署清單)。
  • /entries/search/entrieslibrary 多值參數(D1 json_extract INNULL→general fallback
    • 實作註:SQL 用 COALESCE(json_extract(metadata_json,'$.library'),'general') IN (…)——與 design §3.3 的 OR 形狀語意完全等價(單組佔位符較簡)。本機 miniflare+local D1 十案例實跑全過(PR 附證據)。
  • embed upsert metadata 加 library 欄;semanticSearch filter 支援 library$in 主路徑fan-out 不需)
    • 實作註:Vectorize 端在寫入時正規化(未標記→'general')——Vectorize filter 做不了 COALESCE, 寫入端蓋章後查詢端單純 $in;與 D1 查詢端 fallback 語意對齊。
  • Vectorize library metadata index 建立步驟+reindex backfill 寫進部署清單
    • 部署清單(待雲端排練驗——本機無 Vectorize runtimesemantic 路徑 code 完成、線上實跑未驗)
      1. wrangler vectorize create-metadata-index arcrun-kbdb-embed --property-name library --type string
      2. POST /embed/backfill {"reindex":true} 分批到 remaining=0index 只收建立後 upsert 的向量)
      3. 驗收抽查:semantic 帶 library= 過濾命中/不帶行為不變
      4. 掛號:cli/src/lib/deploy.ts ensureVectorizeMetadataIndexes() 待補 library(cli/ 屬 P1 派工範圍外「只動 kbdb」,隨 P3 或部署 PR 補——kbdb/wrangler.toml 註解已標)
  • cypher kbdb-proxy 透傳 library 參數(供 owner/admin 面用;portal 面走 P3 的注入,不經這)
    • ⚠️ 範圍註(2026-07-14):P1 派工紅線「只動 kbdb、不碰 cypher-executor」與本項矛盾——照窄範圍 執行,本項順延(一行 query 透傳,隨 P3 動 cypher 時一併)。
    • 已隨 P3 補上(2026-07-14 P3 PR):/kbdb/search/kbdb/entries 透傳白名單各加 library
  • 測試:D1 filter 單元測、NULL fallback、多值、semantic filtermock VECTORIZE
    • kbdb/tests/library-filter.test.ts 11 項(SQL 形狀/route 解析/向後相容/降級仍 enforce/$in 構造/ 寫入端正規化)+既有 6 項全綠(17/17);tsc exit 0。
  • 驗收curl /entries/search?q=&library=finance 只回 finance+未標記條目歸 general 可驗;semantic 同
  • 工程量:小-中(0.5–1 個 CC 工作天)

P2 — portal_user 模型+認證 APIdesign §2/§4)|觸碰:cypher-executor/(新 route 檔)

  • seed template portal_userportal_libraryinit-seed 慣例)
    • 實作註(2026-07-14):種子資料 cypher-executor/src/lib/portal-seeds.ts*-seeds.ts 慣例類別); 冪等 ensure 在 routes/portal.ts ensurePortalTemplates(),掛進 POST /init/seed(回應加 portal_templates 區塊)+ bootstrap 前自動 ensure(沒跑過 seed 也能 bootstrap)。
  • PBKDF2-SHA256 雜湊模組(600k iters、常數時間比對、pbkdf2-sha256$… 格式)
    • cypher-executor/src/lib/portal-auth.tsWebCrypto deriveBitsverify 從儲存格式解析 iterations → 換 KDF/參數可共存漸進遷移;constantTimeEqual 同 mcp PR#15 實作)。
  • POST /portal/admin/bootstrapconsole owner session 閘)
  • login/logout/session/改密碼(KV portal_sess:、TTL var、每請求回讀 record 驗 status
    • PORTAL_SESSION_TTL 進 wrangler.toml [vars]types.ts(預設 604800)。
  • admin 用戶 CRUDlibraries 授權+庫目錄 CRUDrole=admin 閘)
    • 安全註:admin 操作目標 record 需過「email head entry 指回同 record_id」成員驗證(防拿任意 record_id 改到不相干 KBDB record);列表/回應一律剝除 password_hash;未帶密碼建帳號 → server 產一次性密碼隨回應回傳一次(不落地明碼)。
  • 登入失敗節流(5 次/15 分鐘,KV TTL)
  • 順手修 record-crud.ts updateRecord grow 路徑漏 owner_iddesign §2.2 附帶)
    • 修法:首查 JOIN entries 帶回 record 既有 owner_idgrow 的新 entry 沿用。回歸測試 kbdb/tests/record-grow-owner.test.ts3 項:帶 owner/原無 owner 維持 null/非 grow 不 INSERT)。
  • 測試:bootstrap 閘、登入對錯、停用即拒(session 立即失效)、role 閘、雜湊格式、{tenant}::portal 子 namespace 隔離(搜 email 搜不到
    • 單元:cypher-executor/tests/portal-auth.test.ts 20 項(fetchMock 假 KBDB host,絕不外連); cypher 全套 133/134(唯一失敗=executor「不存在的零件」pre-existingstash 複驗);kbdb 20/20 兩包 tsc exit 0。端到端:本機雙 worker(隔離 persist33/33 全綠,含搜尋隔離實測 (租戶 ns 搜 email/雜湊 0 筆+對照組 leo::portal 搜得到=非假綠)與明碼落地抽查 0 筆。
  • 驗收(=#25 驗收):新增用戶能登入;停用登入被拒+既有 session 失效;KBDB 無新表;密碼抽查非明碼
  • 工程量:中(約 1 個 CC 工作天)

P3 — /portal UI:登入殼+搜尋頁+設定頁+scope enforcedesign §1/§3.3/§5/§6)|觸碰:cypher-executor/

  • /portal HTML 殼(重用 console 樣式/搜尋 view 抽共用 helperCONSOLE_BRAND 品牌;零 Mira 字樣)
    • 實作註(2026-07-14):routes/portal-ui.ts 單檔 HTML+原生 JSconsole 同形態)。UI 重用取捨= 複製 console 樣式子集而非抽共用模組——抽共用要動 console.ts(違「console 現狀不動」鐵律), 代價=樣式雙份、改版要兩邊同步(PR 說明誠實記)。零 Mira 字樣由測試機械斷言。
  • 未登入只見登入殼;登入後兩頁:搜尋(keyword/semantic/graph 三模式+source 溯源+卡片詳頁)+設定(改密碼/看自己權限/主題)
    • graph 模式按 session graph_allowed 顯示(D-4);工作流頁 admin 額外可見(D-8, PORTAL_SHOW_WORKFLOWS 預設 admin,唯讀不開 trigger)——前端藏只是 UX,真閘在路由層。
  • /portal/data/* server-side enforcesession→record→注入 owner_idlibrary前端絕不下發租戶字串
    • routes/portal-data.ts(安全核心):caller 自帶 owner_id/library 一律被 server 值靜默覆蓋 (e2e 攻擊實測);["*"]=不注 library;HTML 殼經測試機械斷言零租戶字串/零 X-Arcrun-API-Key/不打 /kbdb/*。
  • 卡片詳頁逐筆驗 library(越庫 id 直讀 → 404
    • 雙重驗證:owner_id=CONSOLE_TENANT(防拿跨租戶 id 直讀)+library∈用戶集合(NULL→general); 越庫/跨租戶/不存在同一句 404(不洩存在性,e2e 逐字比對)。
  • graph 粗閘(D-4:無 graph 來源庫權限 → 模式不顯示+API 403)
    • 來源庫=portal_library graph_source='true' 集合(seed 補 slotensurePortalTemplates 冪等 grow 舊 template 缺的 slotsadmin libraries PATCH 可標記/取消),沒有任何標記→預設 ['general'](D-4 定案);無權 403、前端模式鈕不顯示;停用庫不算來源。
  • 測試:curl 帶 user session 直打 data API 驗 filter 繞不過(=#24 驗收 3 的 server-side 證明)
    • 單元:cypher-executor/tests/portal-data.test.ts 21 項(fetchMock 假 host,絕不外連); cypher 全套 154/155(唯一失敗=executor「不存在的零件」pre-existingstash 複驗); kbdb 20/20;兩包 tsc exit 0。端到端:本機隔離雙 worker9790/9791demo 8787/8788 不碰) 47/47 全綠——含越庫 id 直讀 404、攻擊參數被覆蓋、graph 403→授 general 後放行、 admin 全庫+跨租戶不可見、權限 PATCH 即時生效、HTML 殼斷言+inline JS node --check、 改密碼全鏈。wrangler deploy --dry-run 打包過。
  • 驗收=#24 驗收):leo21c /portal 可開;未登入只見登入殼;三模式+溯源可用;A 用戶(僅 general)搜不到 finance 內容——UI 與 curl 雙驗
    • 本機端到端已全數驗過(上項證據);leo21c 線上面=部署後驗gated wrangler 直推,B 類流程; semantic 線上路徑仍依 P1 部署清單的 Vectorize indexreindex)。
  • 工程量:大(1–1.5 個 CC 工作天,UI 是最大件)

P4 — admin 頁+工作流顯示(design §6)|觸碰:cypher-executor/

  • 帳號管理頁(admin-only nav):列表/新增/停用/重設密碼+每帳號庫權限勾選
    • 實作註(2026-07-14):portal-ui.ts 加「管理」頁(v-adminnav 只對 role=admin 顯示, 真閘=/portal/admin/* role 閘 403)。新增未帶密碼 → server generatePassword 一次性 密碼只在畫面顯示一次otpbox,關閉即逝;server 只存 hash);每帳號庫勾選含 ["*"] 全庫選項(勾選連動停用個別庫框);可勾清單=庫目錄啟用中 ∪ general ∪ 該帳號 既有授權(目錄外舊授權不憑空消失)。admin API P2 已交付不重做;本波補 last-admin 鎖死保護(總管派工明定,design 未明寫——PATCH 停用/降級「最後一個 active admin」→ 409;只在目標是 active admin 且 patch 會使其失格時多打一次 list)。
  • 庫目錄管理(登記/停用庫)
    • 同頁第二區:登記(name/display_name/description)+停用/啟用+ graph_source 圖譜來源標記(D-4,P3 已有 API,本波補 UI)。
  • 工作流頁(唯讀 list+最近執行;PORTAL_SHOW_WORKFLOWS 預設 admin;不開 trigger)
    • P3 已交付/portal/data/workflows routev-workflows 頁+D-8 閘),P4 沿用零改動。
  • 驗收:admin 全流程「發帳號→授庫→同仁登入查詢→停用」在 UI 走通;一般用戶看不到 admin 頁與(預設下)工作流頁
    • 單元:tests/portal-admin.test.ts 14 項(鎖死保護含「另一 admin 是 disabled 不算數」 「停用一般 user 不多打 list」;一次性密碼/reset 明碼不進 KBDB bodyHTML 殼加 admin 頁後 仍零租戶字串/零 /kbdb//零 API key/零 Mira)。cypher 全套 168/169(唯一失敗=executor 「不存在的零件」pre-existing);kbdb 20/20;兩包 tsc 0dry-run 打包過;inline JS node --check 過(2 段)。
    • 端到端:本機隔離雙 worker9790/9791demo 8787/8788 不碰)41/41 全綠——bootstrap→ admin 建帳號拿一次性密碼→新 user 登入→勾庫後同一 session 搜尋範圍即時變化1 筆→2 筆、 越庫 404→200)→停用 session 立即失效→重設密碼舊密 401 新密 200→last-admin 409(含降級 即時生效:原 admin session 打 admin API 變 403)→一般 user 打 admin API 403。
    • leo21c 線上面=部署後驗(gated wrangler 直推,B 類流程)。
  • 工程量:中(約 1 個 CC 工作天)

BugfixP1-P4 交付後)

  • PBKDF2 600k → 100kT6-cloud 部署抓到的框架蟲,2026-07-14,分支 fix-pbkdf2-cf-limit CF Workers 正式 runtime PBKDF2 上限 100,000 iterations600k 時 crypto.subtle.deriveBits 直接拒絕 → /portal/admin/bootstrap 真雲 500uncle6 實撞,證據 arcrun-rag docs/manual/uncle6-deploy-record.md);miniflare 無此限制=本機測試全綠假象。 修:portal-auth.ts PBKDF2_ITERATIONS 100_000+註明平台封頂;design D-5 已加修訂註。 verify 相容性:迭代數從儲存值解析,補測試「600k 舊 hash 仍通」(miniflare 造);誠實邊界 =真雲 deriveBits 同樣封頂、600k hash 真雲仍拒,但真雲 bootstrap 從未成功過 → 無存量 hash、 無遷移面。同 PR 順手:.component-builds/http_request/wrangler.tomlglobal_fetch_strictly_public flagself-hosted 同帳號 fetch 1042 正解,Arcrun#33 同族, 官方行為不變原理同 cypher 前例 rules/03 1042 段)。

  • Portal 來源回溯超連結(2026-07-18,任務層小改,分支 feat/portal-source-link 來源=leo RAG demo 客戶測試回饋「來源回溯 gitea:// 沒辦法點」) 新增可選 var PORTAL_SOURCE_WEB_BASEtypes.ts 註解含語意);portal-ui 卡片詳頁「來源回溯」 與 AI 問答出處列在「var 有設+source 為 gitea://」時渲染 <a target=_blank rel=noopener> gitea://#n → {base}/chunk 錨點捨棄、路徑逐段 encodeURIComponent、href 過 esc());未設 var 行為一字不變(Mira 零影響)。驗證:tsc 0、portal-auth/portal-data 測試 48/48 綠、渲染後 client JS new Function 語法檢查過。部署:uncle6 重部+設 var(leo 閘)。

  • 搜尋濾內部型別+總圖頁(2026-07-18 晚,任務層小改,分支 feat/portal-library-overview; 來源=leo 客戶測試回饋「無標題雜項列」+「書庫總圖」指示(原案=Arcrun#39 藏書地圖的 GUI 切片))filterDeprecatedEntries 加內部型別過濾(value=slot 值外漏雜項列、workflow=工作流定義), #46 上游修好後隨整段拔掉;測試補一案(49/49 綠)。 ② 新增 GET /portal/data/graph/overview(全租戶 active 三元組 → nodes/edgesD-4 graph 粗閘同 neighborskbdbFetch 直讀不經 plugin,邊數上限 500 誠實截斷)+portal「總圖」頁 nav 隨 graph_allowed 顯示;手刻力導向佈局、固定種子確定性、紙感樣式、點節點跳圖譜搜尋; 頁尾連 00-MAP.md=#39「人機共用同一份地圖」的文字版)。 ③ #39 本體(library_map Template/ingest 重算/MCP instructions+get_map)不在本次範圍,仍歸 #39 SDD。

  • t95 搜尋 CJK 正規化(2026-07-28,任務層小改) 來源=leo 實測 geek6688 實例「搜『AI協作』(無空白)0 結果,搜『AI 協作』有結果,中文習慣不是每個人都會加空白」。 修:portal-data.ts 新增 normalizeCjkQuery()(CJK/ASCII 邊界自動插空白,純函式可 export 單測); /portal/data/search 路由的 q 參數先過正規化再查 KBDB;只動查詢端不動索引端。 驗證:pure function 6 案(純 CJK/ASCII 不動、邊界插空白、已有空白不重複、全形符號不觸發)+ integration 2 案(AI%E5%8D%94%E4%BD%9C → KBDB 收到 AI 協作)。

  • t96 圖譜節點模糊比對 fuzzy fallback2026-07-28,任務層小改): 來源=leo 實測「graph 模式搜『AI 協作』回鄰居 0 關聯 0,但總圖上明明有節點『AI 協作規範書』—精確比對太嚴」。 修:portal-data.ts 新增 findBestNodeMatch()(contains 比對+最短名優先,純函式)+ fuzzyFindNode()(查 KBDB triplet records 找最佳節點名); graph neighbors 路由 plugin fallback 路徑(②):精確命中 0 鄰居+0 邊 → 以 fuzzyFindNode 找最佳節點名重查; 工作流路徑(①)套 CJK 正規化但不加 fuzzy fallbackworkflow 自管節點解析)。 B5 分支(work/b5-graph-library-filter-0726)只動同一行的 libraries 欄位,衝突面最小。 驗證:findBestNodeMatch 6 案(空清單/無命中/精確子字串/多命中取最短/CJK 未正規化/大小寫)+ integration 4 案(有鄰居直接回、0 鄰居 fuzzy 命中、0 鄰居 fuzzy 無命中誠實回 0、t95+t96 連動)。

  • t97 庫目錄只顯示用戶同步進來的(2026-07-28,任務層小改): 來源=leo 裁定「用戶沒加上的庫,不要自作主張給它加上」。 t97a:bootstrap 後不再預埋 kb 庫(index.html firstsetup 移除 POST /portal/admin/libraries 種子 call)。 t97bGET /portal/admin/libraries auto 段新增 n === 'general' 過濾——general 是系統「未標庫」 fallback 桶,不是用戶加的,不在目錄露臉;資料照舊、B5 權限語意不動。 驗證:portal-admin.test.ts 新增「auto 過濾 general」1 案(mock KBDB /entries/libraries → ['kb','general','notes'] 回應 names 含 kb/notes,不含 general)。

  • t114 拿掉「登記到目錄」兩段式(2026-07-28,任務層小改): 來源=leo 裁定「掃進來的就是要進目錄…加入目錄這件小事還要分兩段做?是在攻打用戶嗎」。 修:renderAdminLibs() 拿掉 auto/已登記視覺分岔,auto 庫直接以完整卡片顯示(保留「同步自動出現」tag, 去掉「登記到目錄後可以改顯示名」描述文字);移除「登記到目錄」按鈕與 lib-adopt event handler; 空狀態文字改「同步小幫手還沒送來任何庫…庫會自動出現在這裡」。 顯示名:此輪 auto 庫唯讀(與已登記庫現行行為一致,實作成本最低;後續若需可補 auto-adopt on PATCH)。 搜尋不依賴登記(搜尋走 /portal/data/search → server 注入 library filter,與庫目錄登記簿無關)。 驗證:HTML 殼測試補「無 lib-adopt、無登記到目錄」斷言;既有 HTML 斷言(零租戶字串等)不回退。

  • t115 kbdb 全域認證中介層(2026-07-28,安全洞熱修;二修 2026-07-28) 實證:不帶任何憑證直打 arcrun-kbdb.<sub>.workers.dev/entries → 200 回真實知識;POST 直寫 成功。B5 權限做在 cypher/portal 層,繞過 portal 直打 kbdb 全破,隱私賣點(原文不出機)形同虛設。 一修(已在樹上)兩個缺陷:① if (env.KBDB_INTERNAL_TOKEN) 才擋 = 沒設 secret 洞照開(fail-open); ② workflows/rag-ingest-card.local.yamlarcrun-rag repo)有 3 處 __KBDB_BASE__ post_block/post_triplet 走 http_request 直打 kbdb),rag-chat/graph-neighbors/takedown 同樣; token 生效後收卡與查詢全 401。 二修(本次): ① kbdb/src/index.ts middleware 改 fail-closed

    • 未設 token → POST/PATCH/DELETE/PUT 直接 401fail-closed for writes); GET 記 console.warn 後放行(讀取升級窗口,老實例不整個炸)。
    • 已設 token → 非 health 路由全部要求 Bearer(行為同一修)。 ② kbdb/tests/auth.test.ts 改 12 項(含 PATCH/DELETE 無 token→401;原「無 token POST 過」改為 401)。 ③ kbdb/src/types.tskbdb/wrangler.toml 說明改為 fail-closed 語意。 ④ workflow http_request 節點(arcrun-rag 側):規格見本項末「給安裝器的規格」段,由總管派 arcrun-rag。 cypher 不需改(五處已有 if (KBDB_INTERNAL_TOKEN) headers[Authorization]=Bearer)。 老實例升級路徑:安裝器部署時自動生成同一把 token → wrangler secret put KBDB_INTERNAL_TOKEN 注入 kbdb 與 cypher 兩個 workerworkflow yaml 同批替換 __KBDB_TOKEN__(見規格)即封口。 kbdb_upsert_block WASM 指向死路由 /blocks(設 token 前後都壞,不新增破壞)。
  • t116 圖搜尋靜默失敗——portal 補傳 kbdb_base2026-07-29,任務層小改) 診斷:graph_neighbors workflow 的 fetch_triplets url 含 {{input.kbdb_base}}/records/... 但 portal 呼叫 executeWebhookGraph 時未傳 kbdb_base → URL 解析失敗 → HTTP request failed。 修法(b 案 雙保險):portal-data.ts 補傳 kbdb_base: env.KBDB_BASE_URL ?? '' yaml 端(__KBDB_BASE__ 安裝期替換)由總管派 arcrun-rag,詳見本項末規格段。 執行範圍:cypher-executor/src/routes/portal-data.ts(補傳 kbdb_base)。

  • t117 三元組寫入靜默失敗——FOREACH 全失敗浮出水面(2026-07-29,任務層小改) 診斷:youlin 實例 triplet=0,懷疑 post_triplet http_request 401 被靜默吞掉; FOREACH 收集到全 success:false 的 iterResults 後回 {success:true, data:{results:[...]}} —— 呼叫端不知失敗。 兩層修法: ① wasi-shim.ts http_request catch:改寫錯誤 envelope(含 fetch 原始 message)到 WASM 輸出, 取代只 return 1WASM 寫 "HTTP request failed"); ② graph-executor.ts FOREACH:若全部 iterResults 均 success===false,拋 Error(含首項 status code → catch 轉 ExecutionError → executeWebhookGraph 回 {success:false, error:"..."}。 執行範圍:cypher-executor/src/lib/wasi-shim.tscypher-executor/src/graph-executor.ts。 測試:vitest 新增兩條(FOREACH 全失敗→error 含 statuswasi-shim catch 路徑)。

  • t122 萃取引擎金鑰雲端下發(2026-07-29) 來源=總管確認:daemon/config 回傳 extractor:'claude' 寫死且不含金鑰,封測者萃取全滅。 修:① POST /portal/admin/extractoradmin 閘)存 {engine, gemini_api_key?, llm_model?} 到 KV key={tenant}:portal:extractor_config,金鑰不落 logGET /portal/admin/extractor(回 engine + has_key:bool,不回明文); ③ POST /portal/daemon/config 回傳改讀上述設定(未設→預設 gemma;gemma+有 key → 含金鑰下發); ④ console-ui/public/portal/index.html 設定頁新增「萃取引擎」區塊(引擎選擇+金鑰輸入+提示重連)。 測試:vitest 新增 3 案(未設定→下發 gemma 無金鑰;設定後→下發含金鑰;GET 不回明文)。

  • t128 圖搜尋仍回 0——graph_neighbors 補傳 template2026-07-29,任務層小改) 診斷:t116 只補了 kbdb_base,但 fetch_triplets.url 同時含 {{input.template}} portal 呼叫 executeWebhookGraph 未傳 template → URL 變 /records/by-template/?owner_id=... → count=0。 總管實測:手動補 "template":"triplet" → count=1(企業版功能解鎖)。 修:portal-data.ts graph_neighbors 呼叫補 template: 'triplet'。 執行範圍:cypher-executor/src/routes/portal-data.ts 一行。 測試:portal-data.test.ts 新增整合案(t128 describeworkflow 執行不崩即代表 template 進到 context)。 workflow input 完整核對清單(portal-data.ts 所有 executeWebhookGraph 呼叫):

    • graph_neighborsnode 、depth 、namespace 、owner 、kbdb_base t116)、template (本次)
    • rag_chatquestion namespace/kbdb_base 未核實(rag_chat workflow yaml 在 arcrun-rag,非本 repo
    • takedown:不在 portal-data.ts,在 arcrun-rag 端(dashboard/安裝器),超出本 repo 範圍
  • t130 新實例缺 triplet template → 總圖永遠空(2026-07-29,任務層小改) 真因:PORTAL_TEMPLATE_SEEDS 只有 portal_userportal_libraryrag_ingest_card.post_tripletPOST /records {template:'triplet'} → 新實例回 400「template not found: triplet」→ 三元組全滅。 修:portal-seeds.tstriplet seedslots 來源:2026-07-19 kbdb_list_templates 核實 15 槽 + library)。 呼叫路徑驗證(ensurePortalTemplates 全四掛點): ① POST /init/seed(acr init 觸發)← 主線,新裝必經 ② POST /portal/admin/bootstrap(首次 admin 設定) ③ POST /portal/daemon/librariesdaemon 登記資料夾→庫) ④ POST /portal/admin/libraries(admin 手動新增庫) 既有壞實例補救:任一掛點重跑即補建(冪等),e.g. 重跑 acr init 或 bootstrap。 執行範圍:cypher-executor/src/lib/portal-seeds.ts(新增 triplet seed); cypher-executor/tests/portal-auth.test.tsmockTemplatesExist 補 triplet + t130 3 條新測試); cypher-executor/tests/portal-admin.test.tsmockTemplatesExist 補 triplet)。 測試:純資料驗證(seed 含 triplet + 必要 slots)+冪等驗(已存→existing)+自動補建(404→POST→created)。

  • t129 AI 問答出處重複一整頁——後端按 page_name 去重(2026-07-29,任務層小改): 診斷:一張卡拆成 3-5 block,每 block 各回一筆 source(同 page_name)→ 前端列一整頁重複。 選後端修(/portal/data/chat)理由:出處去重是資料清潔,跟前端渲染無關;改後端不需動 HTML。 修:portal-data.ts 新增純函式 dedupeSourcesByPage()Map 按 page_name/page 去重,hit_count > 1 時附計數); chat 路由 rawSources 先過去重再回。 執行範圍:cypher-executor/src/routes/portal-data.ts(新增 export 函式 + chat route)。 測試:portal-data.test.ts 新增 5 案(t129 describe:合併計數/各保一筆/page 備用/空陣列/page_name 優先)。

  • t135 庫目錄移除(2026-07-29,任務層小改): 來源=leo 裁定「需要加移除按鈕,別人裝錯我沒辦法幫他弄,需要可以自主」。 後端:DELETE /portal/admin/libraries/:id(刪登記簿 record,資料不動)+ DELETE /portal/admin/libraries/by-name/:nameauto 庫;body confirm:<庫名> 才執行; 呼叫 KBDB PATCH /entries/deprecate-by-libraryentries 標 deprecated → 從 auto 清單消失)。 KBDB:deleteRecord action + DELETE /records/:id route deprecateEntriesByLibrary action + PATCH /entries/deprecate-by-library route(此路由在 /:id 之前)。 Daemon hint(根治只增不減):POST /portal/daemon/libraries 存 active lib names 到 KVTTL 48h); GET /portal/admin/libraries 讀 KV 後對登記庫標 daemon_watching: false(若 daemon 有回報但未含該庫)。 前端:每張庫卡片加「移除」按鈕(登記庫=confirm() 對話框;auto 庫=prompt() 輸入庫名確認); 移除後即時從列表消失(不必重整);daemon_watching=false → 顯示灰字「小幫手目前沒有在同步這個資料夾」。 測試:portal-admin.test.ts 新增 7 案(DELETE/:id 成功/404/非admin403by-name 成功/無confirm/confirm不符/非admin403)。

  • t131 金鑰設定合併(2026-07-29,任務層小改): 來源=leo 裁定「一律規定先輸入 gemini api key,聊天和萃都一次設好;如果有 claude,勾選加強版」。 後端:新增 POST /portal/admin/ai(同時做 chat-keyextractor 的事)+ GET /portal/admin/ai(回 has_key/use_claude_for_extract/claude_available)+ POST /portal/daemon/report-capabilities(收 daemon 回報的 has_claude); 舊 /portal/admin/chat-key/portal/admin/extractor 保留相容。 前端:設定頁兩區塊合一,Gemini Key 必填欄+Claude 選填 checkbox(依 daemon 回報 enable/disable)。 ⚠️ daemon 端(arcrun-rag repo)需實作 report-capabilities 呼叫,規格:

    • 連線成功後 POST {cypher_url}/portal/daemon/report-capabilities
    • body: {email, password, has_claude: bool, daemon_version: string, os: string}
    • has_claude 由現有 FindClaudeBin() 決定(t92 已實作)
    • 時機:連線成功後一次+每次啟動時一次(不加新 timer) KV key 設計:{tenant}:portal:ai_config(合併設定){tenant}:portal:daemon_caps(能力回報,TTL 7 天) 測試:portal-admin.test.ts 新增 7 案(全通;全套 238 tests 229 passed9 failed 皆 pre-existing)。
  • t142 庫目錄顯示同步張數+關聯數(2026-07-29,任務層小改): 來源=leo 裁定(政府專案驗收)「雲端子庫顯示同步了幾個 wiki+幾個三元組,一眼看出這個庫真的有東西」。 後端(kbdb): ① GET /entries/library-stats?owner_id={stats: [{library, card_count}]} SQLCOUNT(DISTINCT page_name) GROUP BY library(僅 entry_type='block',排 deprecated)。 ⚠️ 卡數=distinct page_name(一張卡 3-5 個 block),不是 COUNT(*)(防膨脹 3-5 倍)。 路由在 /libraries 之後、/ 之前(避免被 /:id 吃掉)。 ② GET /records/triplet-stats?owner_id={stats: [{library, triplet_count}]} SQLsubquery DISTINCT triplet record IDs + LEFT JOIN library slot(無 slot→general), 一次 SQL 完成,不 N+1。路由在 /by-template/:template 之前。 後端(portal.ts):GET /portal/admin/libraries 改並行撈三端點(Promise.all + .catch(→null)); 已登記庫與 auto 庫各補 card_count + triplet_countMap O(1) 查找),任一失敗不炸主流程。 前端(index.html renderAdminLibs):每張庫卡片補一行 「N 張知識卡・M 條關聯」(只顯示非零項);兩者均 0 → 顯示「還沒有內容」,不顯示「0 張」。 測試:kbdb/tests/library-stats.test.ts14 新案:SQL 形狀/COUNT DISTINCT/entry_type filter/ deprecated filter/COALESCE general fallback/參數傳入/回傳結構/空陣列不炸); portal-admin.test.ts(t97 既有測試補三端點 mock + t142 describe 3 案: 已登記庫帶 stats/auto 庫帶 stats/空庫 card_count=triplet_count=0)。 vitest + node --check 待 leo 驗收環境跑(本機無 Workers runtime)。

  • P8-資源搭配:節點輸出 KV 寫入只服務 PIPE 讀者(2026-08-09,任務層小改) 來源=arcrun-rag system-dev/docs/3-specs/pending-changes.md P8「短板齊平」(leo 08-08 裁決通過) + leo 08-09「不需要換模型,要調整每個 CF 數字搭配」(模型切換已 revert 894d9ab,本項不碰模型)。 病:BUILD-006 讓每個節點(含 FOREACH 每一圈)把輸出 put 進 EXEC_CONTEXT KV 但全 codebase 唯一讀點是 PIPE 邊的 kvGetNodeOutput——rag 系工作流(ON_SUCCESS+對每個) 完全沒有 PIPE 邊 ⇒ 一張卡 15 次 put 全是寫了沒人讀的。 KV 免費層 1,000 write/日 ÷ 15 ≈ 66 檔/日=全系統真正最短的板(比 neurons 的 119 檔/日更短, 且免金鑰路與 Gemini 路都吃這條)。§3.8.2 原盤點只算到 execution-logger 那 1 筆,漏了這 15 筆。 修:graph-executor.ts 節點輸出寫 KV 前檢查「該節點有 PIPE 出邊」才寫; PIPE 工作流(含「完成後」與未知語意詞預設 PIPE)行為不變,resume 路徑(resumeFromPaused)不動。 實測(youlin stage,兩張同構測試卡 5 blocks6 triplets): 修前 run rag_ingest_card-1786208572245 留 6 個 node key15 次 put); 修後同形狀執行 EXEC_CONTEXT 零 keyblocks/triplets 照樣寫入成功(KBDB record id 全數回傳)。 單元測試:tests/executor.test.ts「P8:節點輸出 KV 寫入只服務 PIPE 讀者」—— 無 PIPE 圖 put=0PIPE 圖照舊寫且 _kv_outputs 傳遞不變(12 passed;既有 1 failed 為 「零件不存在」文案舊測試,修法前後皆失敗,與本項無關)。 一併修復:08-08 deploy-all 重部把 youlin 的 [ai] binding 洗掉(/portal/daemon/extract 501 ——本次以 KEEP_AI=true 重部復原(501→200 實測,模型仍 scout 未動)。

  • t218 設定頁補顯示 MCP 連接網址(2026-08-09,任務層小改,arcrun-rag#7: 來源=封測者原話「說明叫我把 MCP 加進 claude.ai connector,但我找不到網址」。文件 (docs-site/use/mcp.md)一直寫「登入 portal → 設定頁,那裡可以直接複製」,但畫面上 從沒真的顯示過——用戶照著文件走一定撲空(總管實查:console-ui/public/portal/index.html grep mcp|MCP 0 命中)。 修:console-ui/public/portal/index.html 設定頁新增一塊面板「接上你的 AI(MCP)」, 純前端字串轉換(不需後端新端點、不需安裝器多寫設定):MCP 網址=apiBase 把 worker 名字從 arcrun-cypher-executor 換成 arcrun-mcp,同一顆自架帳號的 workers.dev 子網域(與 cli/src/lib/deploy.ts:386-392 部署時組出這兩個網址的邏輯完全對齊,非另立 一套猜法);不是這個網址形狀時誠實顯示「尚未偵測到」+停用複製鈕,不亂猜一個連不到的網址。 複製按鈕沿用既有 st-copy-url 的 clipboard 邏輯,抽出共用 copyText(btn, url)(原 copyOriginUrl 只能複製 location.origin,MCP 網址不是 origin,需要能傳任意字串)。 文件(products/arcrun-rag docs-site use/mcp.md)核對過:"不確定是哪一串?登入 portal → 設定頁,那裡可以直接複製" 這句在本次修完後才是真的,故文件本身不必改字。 端到端實測(本機真瀏覽器,非 curl):起 kbdb + cypher-executor 兩個真 wrangler dev local D1 套 migrations 0001-0006,兩邊 KBDB_INTERNAL_TOKEN 對齊解開 t115 fail-closed 閘)+本機靜態伺服 portal(UI_ORIGINS 解 CORS);真的走 /console/setup/console/login/portal/admin/bootstrap 產生第一個 admin,瀏覽器真登入 /portal/login,導到設定頁—— 面板正確顯示、apiBase 是 localhost(不符自架網址形狀)時誠實顯示「尚未偵測到」+複製鈕 停用(沒有亂猜一個假網址)。再用瀏覽器 console 把 window.ARCRUN_API_BASE 換成貼近真實 自架形狀的字串(https://arcrun-cypher-executor.fake9xyz.workers.dev,故意用假 subdomain, 不使用任何真實租戶字串以免誤觸真實實例),對照組獨立驗算與 index.html 內同一份公式, 結果與 deploy.ts:391 的組法逐字相同(https://arcrun-mcp.<sub>.workers.dev/mcp); 真的把這個值餵回頁面 DOM#st-mcp-url),畫面正確顯示、複製鈕從停用變可按。 複製鈕點擊後 clipboard 沒有變化——經比對既有、本次未改動st-copy-url(知識庫網址 複製鈕)在同一自動化瀏覽器環境下點擊也是同樣結果,確認是這套瀏覽器自動化工具本身的 clipboard-write 權限限制(非真人操作,navigator.clipboard.writeText 需要真使用者手勢), 不是本次新增程式碼的迴歸——兩顆鈕行為一致。 未測:真實自架 workers.dev 網址(未取得任何測試帳密,且紅線禁碰 youlin/uncle6); 真人用滑鼠點擊複製鈕(受限於自動化環境,上述已用既有鈕做過同構對照)。 執行範圍:console-ui/public/portal/index.html(新增面板 + JS)。未動後端、未部署。

  • Arcrun#108(任務層修正,2026-08-13):資料面租戶字串收斂到唯一產地 P3 的 /portal/data/* 一律用 portalTenant(env) = env.CONSOLE_TENANT || 'leo'owner_id。 那個字串是部署環境變數,而知識是 CLI/同步小幫手/MCP 用實例 namespace ~/.arcrun/config.yamlapi_key)寫進去的——兩個來源會漂。leo 實撞: 藏書地圖回 0 個庫,同一分鐘 KBDB 裡有 1854 條三元組(他的在 owner_id=bfezv28v)。 與 Arcrun#105env.MCP_OWNER_NAMESPACE || "leo")同形,低一層。 修法:新增 cypher-executor/src/lib/tenant.ts 當唯一產地—— knowledgeOwner(env) 回 branded TenantIdARCRUN_NAMESPACECONSOLE_TENANT → 誠實丟錯,無字面預設值),資料面過濾一律經 ownerQuery()/ownerField() 帳號子 namespacedesign D-2 的 {tenant}::portal)改用 accountTenant(env)(回 string 型別上不可能流進資料面),帳號落點一字不動(動了舊實例登不進去)。 acr update 先驗(GET /kbdb/map?owner_id=<api_key> 查得到庫)才注入 ARCRUN_NAMESPACE。 空地圖改回四態(no_library_grantfiltered_outscope_mismatchconfirmed_empty), 沿 Arcrun#100「讀不到就說讀不到」。庫權限過濾一字未動(回歸測試釘住)。 防複發:scripts/build-worker-artifacts.mjs 出貨前掃描,違規編不出成品。 規範寫入 .claude/rules/02-forbidden.md 第六類、system-dev/wiki/mistakes.md #26。

第二波(不在本 SDD 動工範圍,掛號)

  • MCP token 綁庫集合(design §9PR#15 擴充,只動 mcp/
  • graph 逐節點/逐邊細粒度過濾
  • Google OAuth2 fast-follow、審計 log、忘記密碼 email 自助、舊資料 library 回填清潔工