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>
This commit is contained in:
@@ -376,6 +376,23 @@
|
||||
真人用滑鼠點擊複製鈕(受限於自動化環境,上述已用既有鈕做過同構對照)。
|
||||
執行範圍:`console-ui/public/portal/index.html`(新增面板 + JS)。未動後端、未部署。
|
||||
|
||||
- [x] **Arcrun#108(任務層修正,2026-08-13):資料面租戶字串收斂到唯一產地**
|
||||
P3 的 `/portal/data/*` 一律用 `portalTenant(env) = env.CONSOLE_TENANT || 'leo'` 注 `owner_id`。
|
||||
那個字串是**部署環境變數**,而知識是 CLI/同步小幫手/MCP 用**實例 namespace**
|
||||
(`~/.arcrun/config.yaml` 的 `api_key`)寫進去的——兩個來源會漂。leo 實撞:
|
||||
藏書地圖回 0 個庫,同一分鐘 KBDB 裡有 1854 條三元組(他的在 `owner_id=bfezv28v`)。
|
||||
與 `Arcrun#105`(`env.MCP_OWNER_NAMESPACE || "leo"`)同形,低一層。
|
||||
**修法**:新增 `cypher-executor/src/lib/tenant.ts` 當唯一產地——
|
||||
`knowledgeOwner(env)` 回 branded `TenantId`(`ARCRUN_NAMESPACE` → `CONSOLE_TENANT` →
|
||||
誠實丟錯,**無字面預設值**),資料面過濾一律經 `ownerQuery()/ownerField()`;
|
||||
帳號子 namespace(design D-2 的 `{tenant}::portal`)改用 `accountTenant(env)`(回 `string`,
|
||||
型別上不可能流進資料面),**帳號落點一字不動**(動了舊實例登不進去)。
|
||||
`acr update` 先驗(`GET /kbdb/map?owner_id=<api_key>` 查得到庫)才注入 `ARCRUN_NAMESPACE`。
|
||||
空地圖改回四態(`no_library_grant`/`filtered_out`/`scope_mismatch`/`confirmed_empty`),
|
||||
沿 Arcrun#100「讀不到就說讀不到」。庫權限過濾一字未動(回歸測試釘住)。
|
||||
防複發:`scripts/build-worker-artifacts.mjs` 出貨前掃描,違規編不出成品。
|
||||
規範寫入 `.claude/rules/02-forbidden.md` 第六類、`system-dev/wiki/mistakes.md` #26。
|
||||
|
||||
## 第二波(不在本 SDD 動工範圍,掛號)
|
||||
|
||||
- MCP token 綁庫集合(design §9;PR#15 擴充,只動 `mcp/`)
|
||||
|
||||
@@ -547,6 +547,43 @@ repo 早已是 343,969 bytes 的新品牌世代,`Songti` 一處不剩。
|
||||
|
||||
---
|
||||
|
||||
## 26. 「身分來自環境變數」——同一句話寫錯兩次,因為規則有、機制沒有(2026-08-12,Arcrun#105/#108)
|
||||
|
||||
**症狀**:leo 打開藏書地圖回 **0 個庫**,同一分鐘 KBDB 裡有 **1854 條三元組**;
|
||||
`arcrun_whoami` 顯示 admin/全部知識庫,`kbdb_search` 也查得到東西——**只有地圖那格是空的**。
|
||||
|
||||
**根因**(不是資料掉了,是讀寫兩端各拿一個來源):
|
||||
|
||||
| | 寫入端用什麼當 owner_id | 讀取端用什麼過濾 |
|
||||
|---|---|---|
|
||||
| 之前 | `~/.arcrun/config.yaml` 的 `api_key`(CLI push/小幫手上傳/MCP,leo = `bfezv28v`) | `env.CONSOLE_TENANT \|\| "leo"`(repo toml 帶的**官方 prod 值**) |
|
||||
|
||||
`acr` 從來不注入 `CONSOLE_TENANT`,所以那個 `"leo"` 不是理論邊角,**是每台 self-hosted 實例的實際行為**。
|
||||
|
||||
**這是第二次**。`#105` 前一天才修掉 `env.MCP_OWNER_NAMESPACE || "leo"`——同一句話,換一個檔案。
|
||||
|
||||
**判準(下次照用)**:
|
||||
1. **「這個字串是用來決定誰的資料嗎?」** 是 → 它是身分,不是部署設定。
|
||||
身分要嘛來自請求(登入 session/`X-Arcrun-API-Key`),要嘛來自「寫入端用的那個值」,
|
||||
**不可以是一個各自抄一份的環境變數預設值**。
|
||||
2. **`|| '預設值'` 出現在身分解析路徑上=把「這台機器沒設定」偽裝成「你沒有資料」**。
|
||||
解析不到就誠實丟錯(#100 同一條:讀不到就說讀不到)。
|
||||
3. **「規則存在但沒有機制驗證」=它會再犯**。所以本次除了修 bug,還留下三道會擋的:
|
||||
- 型別閘:`TenantId` 只能由 `cypher-executor/src/lib/tenant.ts` 產出,
|
||||
資料面過濾只吃 `ownerQuery()/ownerField()` → 拿隨手一個 string 去過濾,`tsc` 當場不給過。
|
||||
- 出貨閘:`scripts/build-worker-artifacts.mjs` 編成品前先跑租戶來源檢查
|
||||
→ **違規的碼編不出成品、出不了貨**(不是「有人記得跑才會發現」)。
|
||||
- 這道閘自己可測:規則是純函式(`cypher-executor/scripts/tenant-source-rules.mjs`),
|
||||
`tests/tenant-gate.test.ts` 逐條驗「壞例子會擋、11 種合法寫法零誤攔」。
|
||||
**誤攔比漏攔更容易殺死一道閘**——被擋煩了就有人把它關掉。
|
||||
4. **修法不能比 bug 更危險**:`acr update` 注入 `ARCRUN_NAMESPACE` 前**先驗**
|
||||
(`GET /kbdb/map?owner_id=<api_key>` 查得到庫才寫)。無條件覆蓋會把「知識本來就在
|
||||
`CONSOLE_TENANT` 底下」的一鍵安裝實例指向空的那一格——那是 #97/#106 那類
|
||||
「更新一次把人家的東西弄不見」。
|
||||
|
||||
**順手挖出的同族**(同一道閘一次抓到):`console-dashboard.ts` 有 **4 處**、
|
||||
`console-auth.ts` 有 1 處相同寫法——console 首頁的規模數字與藏書地圖對 leo 也一直是空的。
|
||||
|
||||
## 快速檢查清單(做新功能前)
|
||||
|
||||
- [ ] 這是工作流還是零件?問「有必要嗎?」
|
||||
@@ -568,3 +605,6 @@ repo 早已是 343,969 bytes 的新品牌世代,`Songti` 一處不剩。
|
||||
- [ ] 本地/Gitea 改完 code 想 `acr update` 部署?先確認:它抓的是 GitHub codeload tarball,不是你剛改的目錄(#23)
|
||||
- [ ] 改完前端說「做完了」?先問**線上跑的是不是這一份**(`cd console-ui && npm run verify`)——組態綠不代表世代對(#25)
|
||||
- [ ] 要寫「含某關鍵字就擋」的閘?先想「有人寫一則說明它已被移除的註解時會怎樣」——關鍵字閘會腐爛,優先用指紋(#25)
|
||||
- [ ] 寫下 `env.X || '預設值'`?先問「這個字串是用來決定誰的資料嗎?」是 → 它是身分不是設定,不准有字面預設值(#26)
|
||||
- [ ] 要用某個字串過濾 owner_id?確認它與**寫入端**用的是同一個來源,不是另一份手抄的環境變數(#26)
|
||||
- [ ] 留了一道新的閘?它自己有測試嗎、誤攔案例驗過嗎、擋不擋得到自己?(#26)
|
||||
|
||||
Reference in New Issue
Block a user