藏書地圖看得到自己的知識——租戶字串改從「寫入端」來,不再拿環境變數預設值(Arcrun#108) #112
Reference in New Issue
Block a user
Delete Branch "fix/library-map-108"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
修
Arcrun#108。leo 的 1854 條三元組在owner_id=bfezv28v,cypher 拿 repo toml 帶的官方預設值CONSOLE_TENANT = "leo"去過濾 ⇒ 藏書地圖回 0 個庫。① 租戶字串改成從哪裡來、為什麼
答:從「寫入這批知識的那一方」來——也就是實例 namespace(使用者
~/.arcrun/config.yaml的api_key)——而不是從一份手抄的環境變數預設值來。為什麼不是「掛到登入者身上」
票上問要不要收斂到
#105的答案。我的判斷是收斂到同一條原則,但不是同一個載體,理由:在這個架構裡租戶不是每人一個。Portal 帳號共用同一台實例的知識庫(design D-2:帳號自己住
{tenant}::portal子 namespace),帳號之間的差別是libraries權限,不是owner_id。把owner_id複製一份到每個帳號上,只會多一個可以各自過期的副本——那正是本票的病本身,不是解藥。#105真正的教訓不是「一律搬到帳號上」,而是三句話:#105的權威來源是登入者(因為 MCP 那條路的身分本來就在登入時拿到了);#108的權威來源是寫入端用的那個 namespace(因為知識是 CLI/同步小幫手/MCP 寫進去的)。兩張票收斂的是這條原則,落點不同是因為「誰擁有這個事實」不同。實際怎麼做
1. 唯一產地:
cypher-executor/src/lib/tenant.ts|| 'leo'從解析路徑上整個拿掉:那個預設值就是把「這台機器沒設定」偽裝成「你沒有資料」的元凶。解析不到 →TenantUnresolvedError→ 誠實回「讀不到」(#100同一條)。2. 值的正解由 CLI 從真相源導出,而且先驗再寫
acr update把 config 的api_key注入成 cypher 的ARCRUN_NAMESPACE。但只在確認那個 namespace 底下真的查得到知識時才寫(GET /kbdb/map?owner_id=<api_key>,查得到庫 → 寫;查不到/問不到 → 一個字都不動,既有值原封保留)。沒注入時回退
CONSOLE_TENANT⇒ 對官方 prod 與尚未更新的實例,這次改動是惰性的(行為一字不變)。CONSOLE_TENANT本身一個字都沒動——它同時是帳號子 namespace 的組成,改它會讓舊實例登不進去。兩個不同的事實,兩個 var。3. 空地圖分四態(沿
#100「讀不到就說讀不到」)no_library_grant/filtered_out/scope_mismatch/confirmed_empty。scope_mismatch這一格以前不存在,所以「設定錯誤」被畫成「你沒有資料」。回應仍不含租戶字串(design §3.3 紅線)。4. 同族一起修(下面那道閘一次抓到的)
console-dashboard.ts4 處、console-auth.ts1 處是同一句話——console 首頁的規模數字與藏書地圖對 leo 也一直是空的。② 我留的那道閘是什麼、怎麼證明它會擋
「靜態租戶字串不得用於資料面過濾」,三條規則,全部盯行為不盯字詞:
CONSOLE_TENANT/ARCRUN_NAMESPACE)只能在src/lib/tenant.ts讀取as TenantId只能在tenant.ts,且不得套在字面字串上owner_id、值又來自portalTenant()/accountTenant()外加型別閘:
ownerQuery()/ownerField()只吃TenantId,而TenantId只能由tenant.ts產出 ⇒ 想把隨手一個 string 丟進去,tsc當場不給過。會擋,不是只提醒
擋在出貨路徑上:
scripts/build-worker-artifacts.mjs(.worker-builds/的產地)編成品前先掃,違規 → 建置中止。違規的碼編不出成品、出不了貨,不是「有人記得跑檢查才會發現」。實跑(把
#108那一行放回portal-data.ts再編):還原後同一支指令:
逐行檢查(
--stdin模式,給日後接 PreToolUse hook 用)餵#108原句:判準看「有沒有在做那件事」
owner_id這個詞出現在註解、JSDoc、型別宣告、property 讀取(x.owner_id)、帳號層過濾(owner_id: ns)、請求自帶租戶(owner_id: apiKey)都不擋——tests/tenant-gate.test.ts有 11 條專門的零誤攔案例。誤攔比漏攔更容易殺死一道閘(被擋煩了就有人把它關掉)。閘自己能被測試
規則本體是純函式、零 node 相依(
cypher-executor/scripts/tenant-source-rules.mjs),走檔案系統的那半分開(check-tenant-source.mjs)。tests/tenant-gate.test.ts直接餵字串驗 5 種壞例子會擋、11 種合法寫法零誤攔,另外用?raw讀真原始碼驗現況乾淨。掃描範圍只有cypher-executor/src/,測試與 fixture 都在範圍外 ⇒ 這道閘擋不到自己。③ 部署到 leo21c 時要注意什麼(給總管拿去跟 leo 要部署閘)
1. 光部署新版還不夠——
ARCRUN_NAMESPACE必須真的進到 cypher worker 的[vars]。沒有它就回退
CONSOLE_TENANT,地圖還是空的(只是這次會誠實告訴你「命名空間對不上」而不是「你沒有資料」)。acr update:會自動驗 + 注入(leo 的 configapi_key應為bfezv28v;先驗GET /kbdb/map?owner_id=bfezv28v回得到 9 個庫才寫)。wrangler直推單顆 cypher:要自己帶--var ARCRUN_NAMESPACE:bfezv28v(wrangler deploy 是整份覆蓋,toml 沒寫的 var 會消失——#106那個坑)。2. 部署前先跑這條核對,貼輸出:
回 9 → 注入
bfezv28v是安全的。回 0 → 先不要注入,回來查(可能 08-11 之後又搬過)。3. 帳號那半不要動。
CONSOLE_TENANT維持現值——它是帳號子 namespace({tenant}::portal)的組成,改了 leo 會登不進 Portal。本 PR 沒有動它,部署時也不要順手改。4. 部署後的驗收(票上那四條):
kbdb_get_map(AR-Mira)→kb1851/合計 1854/portal/data/graph/overview這條也改用同一個命名空間了,之前一樣是空的)kbdb_search/kbdb_query回歸5. 這次改動對「沒注入
ARCRUN_NAMESPACE」的實例是惰性的(官方 prod、其他 self-hosted 未更新者行為一字不變),所以先部 leo21c、其他實例慢慢跟是安全的。沒動
庫權限過濾(一字未改,回歸測試釘住)、帳號資料落點、
CONSOLE_TENANT、任何金鑰、租戶字串仍不下發前端。leo 的資料沒有被改名回leo。驗證
◐ 未驗(誠實標記):端到端——
kbdb_get_map回 1851、Portal 總圖畫面、部分授權帳號實測,全都需要部署到 leo21c,而那道閘要 leo 親手解(紅線)。上面 ③ 就是為了讓那一趟一次過。新增測試涵蓋:
knowledgeOwner解析三態、/portal/data/map真的用注入的 namespace 查詢(bfezv28v不是leo)、庫權限只看得到授權的庫、空地圖四態、回應不含租戶字串、CLI 先驗再寫的三態 + 不覆蓋既有值。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 commite05518a逐字相同(既有) 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>