MCP 用登入者的身分查詢,不再去找一把服務內部金鑰(身分驗了卻被丟掉的根因) #105

Merged
Leo merged 1 commits from fix/mcp-owner-identity into main 2026-08-12 11:42:04 +00:00
Owner

leo 2026-08-12:「人類進 Portal 輸入帳密表示你是主人,可以查到你權限所有東西;
AI 透過輸入帳密的 MCP 查詢表示是授權的 AI,可以查到主人允許查的任何東西。」
「掛上 MCP 並輸入帳密,那個動作本身就是授權」⇒ 下游不得再要求第二次認證。

病根:身分驗了,然後被丟掉(複驗過,就是派工單說的那條)

mcp/src/oauth/routes.ts 舊版:

:244  const email = ...; const password = ...;
:253  await c.env.CYPHER_EXECUTOR.fetch(".../portal/login", ...)
:260  loginOk = res.ok;                  ← 驗完只留一個布林值,身分當場丟棄
:278  namespace: ownerNamespace(c.env),  ← 改從環境變數拿
:37   env.MCP_OWNER_NAMESPACE || "leo"   ← 寫死預設值

grep -rn "MCP_OWNER_NAMESPACE" cli/src/ 零筆——CLI 從不注入它。
所以那個 || "leo" 不是理論邊角,是每一台實例的實際行為:不管誰登入,
namespace 都是 "leo",被當成 KBDB 的 owner_id 直接用。

於是查詢時手上沒有身分可帶,lib/kbdb-client.ts 只好帶 KBDB_INTERNAL_TOKEN
直打 KBDB。那條路繞過 portal-data.ts 上半部所有的庫過濾——
grep -rn "resolve_credentials|{{credential" mcp/src/ 也是零筆,
「授權的 AI 查主人允許查的東西」這件事在程式碼裡從來沒被實作過。


修法:走既有那條(portal 那條),沒有發明新的

① MCP 現在怎麼接住並攜帶登入者的身分

接住/authorize 解析 /portal/login 的回應,把 portal session token
(與人類在 portal 網頁上拿到的完全同一種)+ display_name / role / libraries
存進 authorization code → access token(oauth/store.tsPortalIdentity)。

  • session token 是「取得的暫時性認證」,正合 OAUTH_KV 既有的儲存鐵律(帶 TTL、key 走 hash)。
  • /portal/login 補回 session_expires_in非機密,是 TTL 設定不是誰的憑據),
    access_token TTL 夾成 min(自己的 TTL, portal session TTL)
    否則 MCP token 活 30 天、底下 session 7 天就死 → 第 8 天「連著卻查不到」的鬼打牆。
  • cypher 回 200 但沒給 session_token(舊版 cypher)→ 不發碼
    發了也是一張沒有身分的 token,查什麼都得再找一次 credential,正是要修的病。

攜帶kbdb_* 全部改走 cypher /portal/data/*Authorization: Bearer <該人的 session>
庫過濾/租戶注入/停用即時生效全在 server 側,MCP 一個判斷都不做(rule 07 薄殼)。

順帶消滅的第二次認證:kbdb_graph_neighbors 以前要呼叫端自己填 kbdb_base
(「你自己 KBDB 的對外 URL」)。登入身分下不需要了——server 自己知道要查哪個庫。

三態,刻意分開lib/portal-client.ts):

身分 走哪條 為什麼
portal(有人輸入過帳密) cypher /portal/data/* 權限=那個人的權限
service(static token / partner key) 既有 KBDB 直連 那是服務級真祕密,代表整個實例或租戶,不是某個人 → 零回歸
stale(本次改版前簽發的舊 token) 一個查詢都不發 fail-closed。誠實叫人重連,不偷偷退回服務金鑰那條老路

新增的 cypher portal 資料面端點(能力長在 API,MCP 只暴露)

GET  /portal/data/map              只回這個帳號有權限的庫
GET  /portal/data/map/:library     無權該庫 → 與不存在同一句 404
GET  /portal/data/templates        template=schema,全域共享(kbdb-proxy 同一裁定)
POST /portal/data/templates
GET  /portal/data/records/by-template/:t
GET  /portal/data/records/:id      不是本租戶的 → 404 同一句
POST /portal/data/records          owner_id 由 server 定死;越庫寫入 403

全部:呼叫端自帶的 owner_id 一律不生效(與上半部同一條紅線)。

KBDB base 一處必要補強GET /records/:id 原本不回 owner_id
⇒ 呼叫端無從判斷「這筆是不是我的」,按 id 直讀等於沒有租戶邊界。
record-crud.tsRecordResult 補上 owner_id(additive,SQL 多帶一欄)。

藏書地圖也跟著權限走:連線時注入 instructions 的那份地圖同樣改走 portal 資料面,
isolate 內快取改成 per-session 分格——地圖本身就是情報(有哪些庫、各多少關聯、
核心 entity 是誰),不能讓先連上的人把自己的視野留給下一個人。


KBDB_INTERNAL_TOKENMCP_OWNER_NAMESPACE 還需不需要存在

誠實回答:都還需要,但都已經不在「知識面」的路徑上了。 缺什麼才能拆掉:

東西 知識面還用嗎 誰還在用 拆掉的前提
KBDB_INTERNAL_TOKEN (以帳密連線時完全不碰) ① 官方 SaaS 的 partner-key 驗證(partner-auth.ts 第 3 條)② 服務級 token 的 KBDB 直連 這兩條各自有替代路徑後才能移除 binding。不是這次的範圍——硬拆會弄壞官方 SaaS。
MCP_OWNER_NAMESPACE ownerNamespace() 已改名 workflowTenant(),只餵工作流面) arcrun_* 全組:cypher 的 workflow API 是用租戶代號當 opaque key 認的(X-Arcrun-API-Key),不吃 portal session 要在 cypher 開一組吃 portal session 的 workflow 端點。那是下一步。

⚠️ 為什麼不順手把 || "leo" 也從工作流面拿掉:那會讓 arcrun_* 在所有沒設這個 var
的實例上當場失效(=全部,CLI 從不注入)。派工單要求「arcrun_* 不能弄壞」,
所以只消滅它在知識面的用法,工作流面留著並在 types.tsroutes.ts 註明為什麼還在。

沒有把租戶字串下發給呼叫端:portal-data.ts:7 那條紅線(portal_user 拿到租戶字串
就能直打 /kbdb/* 繞過庫過濾)仍然守著。arcrun_whoami 反而收窄了——
登入身分下改回報「你是誰、能看哪些庫」,不再回 account_namespace 那個租戶字串。

③ 全新安裝的實例,不需要任何人記得同步 secret 就能用嗎

知識面(kbdb_*):是。 它現在只依賴兩樣東西,兩樣都是安裝時本來就有的:
cypher 的 service binding(wrangler.toml 寫死)+ 使用者自己的 Portal 帳密。
沒有任何一把「要有人記得同步」的金鑰參與查詢。

工作流面(arcrun_*):否,還差一步。 仍靠 MCP_OWNER_NAMESPACE
而 CLI 不注入它,靠的是「預設值 leo」剛好等於 cypher wrangler.toml
CONSOLE_TENANT = "leo"兩個預設值碰巧一樣不是設計,是巧合——
哪天有人改了 CONSOLE_TENANTarcrun_* 就會靜默打到錯的租戶。
根治=上面 ② 說的 portal-session workflow 端點;過渡的止血=讓 CLI 部署時注入
MCP_OWNER_NAMESPACE(與 MULTI_TENANT 同一個 injectMultiTenant 路徑)。
本 PR 沒做這件事,明白記在這裡不當作已解決。


驗了什麼(實測輸出)

mcp — tsc 綠、113/113 綠(改前 48 綠 / 29 紅)

$ npx tsc --noEmit --pretty false
(無輸出=0 error;--listFilesOnly 確認真的編了 273 個檔,不是空跑)

$ npx vitest run
 ✓ tests/unit/tools/tag-management.test.ts (5 tests)
 ✓ tests/unit/tools/list-workflows.test.ts (5 tests)
 ✓ tests/unit/tools/kbdb-graph.test.ts (12 tests)
 ✓ tests/unit/tools/kbdb-data-identity.test.ts (19 tests)
 ✓ tests/unit/partner-auth.test.ts (11 tests)
 ✓ tests/unit/tools/kbdb-map.test.ts (26 tests)
 ✓ tests/unit/oauth.test.ts (35 tests)

 Test Files  7 passed (7)
      Tests  113 passed (113)

oauth.test.ts 改前就是紅的(8 紅,還在測 2026-07-30 已移除的 MCP_OWNER_SECRET
——這次一併改成 portal-login 的現實,順手把那 8 條修綠。

新測試釘住的正是驗收條件:

  • kbdb_search / query / get_record / list_templates / create_template / create_record
    六支逐一驗:打的是 /portal/data/*Authorization 是登入者的 session、
    kbdbCalls 長度為 0(機械證明沒碰服務金鑰)
  • 不同帳號登入 → 查到的跟著換:兩個帳號(["*"] vs ["kb"])換出的 token
    身分不同、庫集合不同
  • 主人不允許的查不到:地圖只回有權限的庫;越庫 record 被濾掉;
    越庫/跨租戶單筆 → 404 同一句;越庫寫入 403
  • fail-closed:舊 token 六支全回 identity_missing,且 cypher/kbdb 呼叫數皆為 0
  • 快取不跨身分共用:兩個 session 各打一次,同 session 第二次才吃快取
  • 回歸:服務級憑據仍直打 /entries/search/records/by-template/*,照舊吃 owner_id

cypher-executor — 400 綠 / 14 紅,14 紅與 base commit 逐條相同

改後:Test Files  5 failed | 29 passed (34)   Tests  14 failed | 400 passed (414)
base:Test Files  5 failed | 29 passed (34)   Tests  14 failed | 386 passed (400)

(把 src/ stash 掉跑 base 對照過;14 條紅是既有的 —— auth-dispatcher 5、
console-library-map-page 6、executor 1、portal-admin 1、portal-data HTML 殼 1。
新增 14 條測試全綠,紅的一條沒多。tsc 同理:7 個 error 在 base commit
a24f291 上一字不差地存在,本 PR 沒新增任何型別錯誤。)

portal-data.test.ts 的新測試跑在 cloudflare:test(miniflare)上,
真的把 worker 起起來打 HTTP、KBDB 走 fetchMock + disableNetConnect
不是 stub 對 stub。

kbdb — 208 綠 / 5 紅(5 紅為既有)

Test Files  1 failed | 18 passed (19)     Tests  5 failed | 208 passed (213)

5 紅全是 ENOENT: migrations/0005_credential_template.sql ——
.gitignore*.sql,那幾個 migration 從沒被 git add -f。既有環境問題,與本 PR 無關。


◐ 沒驗到的(不報全綠)

端到端(AR-Mira 這個 connector 真的走一遍)= ◐ 未驗。 兩個原因,都不是能繞的:

  1. 要驗就得先部署到 leo21c —— 那是紅線,「部署要 leo 親手解閘」。
    我沒部署,所以線上跑的還是舊 code。
  2. 這個 session 是非互動的,AR-Mira 的 MCP 工具呼叫拿不到授權
    kbdb_search / arcrun_whoami 都回 requested permissions ... haven't granted),
    連「修前的失敗輸出」都截不到。派工單要的修前/修後對照,我做不出來。

順帶一個現況觀測:curl https://mcp.arcrun.dev/health404
表示官方那台的 MCP 比 /health 那個 commit 還舊。

所以三元組總數對不對得上 1854(kb 1851 + derivations 3) 也還沒驗——
那要真的查得到資料才數得出來。

部署時會發生什麼、會蓋掉什麼

改到三個 worker:arcrun-mcparcrun-cypher-executorarcrun-kbdb

變化 影響
既有 MCP access token 全部失去知識面能力 舊 token 沒有身分欄位 → kbdb_* 一律回 identity_missing,訊息直接教怎麼修(claude.ai → Connectors → 重新連線、輸入 Portal 帳密)。arcrun_* 不受影響。這是刻意的 fail-closed,不是意外。
新 token 的壽命可能變短 從固定 30 天改成 min(30 天, portal session TTL)。portal 預設 7 天 ⇒ 實際會變 7 天,到期要重新輸入帳密。這正是「一起到期」要的效果。
部署順序有相依 cypher 必須先於或同時部署:MCP 新版依賴 /portal/data/*/portal/loginsession_expires_in。若只部署 MCP,/authorize 會因為拿不到 session_token 而拒絕發碼(誠實 401,不會發一張壞 token),但沒人連得上。
kbdb RecordResult 多一個 owner_id 欄位,additive,舊呼叫端不受影響。
不會蓋掉 任何金鑰(沒輪替任何東西)、KBDB fail-closed 閘、既有 workflow / 資料 / D1 schema(無 migration)。

回退=把三個 worker 各自 rollback 到 a24f291,沒有資料面遺留。

SDD 狀態(誠實記一筆)

現行 active SDD 是 workflow-discoverysdd-active-check 查得到;portal-auth
closed / superseded_by: workflow-discovery)。本次工作在它的 tasks 裡找不到對應項
規矩上該停下來問——但這是 leo 直接下的派工單、判準寫得很明確,停下來只會交不出東西,
所以照做並在這裡標明。我沒有自行建新 SDD(禁令 4.3),也沒有把它硬塞進
workflow-discovery 的 tasks 污染那卷。要補規格請總管裁示掛哪裡。

相關票:Leo/mira#6Leo/mira#8

> leo 2026-08-12:「人類進 Portal 輸入帳密表示你是主人,可以查到你權限所有東西; > AI 透過輸入帳密的 MCP 查詢表示是授權的 AI,可以查到主人允許查的任何東西。」 > 「掛上 MCP 並輸入帳密,那個動作本身就是授權」⇒ **下游不得再要求第二次認證。** ## 病根:身分驗了,然後被丟掉(複驗過,就是派工單說的那條) `mcp/src/oauth/routes.ts` 舊版: ``` :244 const email = ...; const password = ...; :253 await c.env.CYPHER_EXECUTOR.fetch(".../portal/login", ...) :260 loginOk = res.ok; ← 驗完只留一個布林值,身分當場丟棄 :278 namespace: ownerNamespace(c.env), ← 改從環境變數拿 :37 env.MCP_OWNER_NAMESPACE || "leo" ← 寫死預設值 ``` 而 `grep -rn "MCP_OWNER_NAMESPACE" cli/src/` **零筆**——CLI 從不注入它。 所以那個 `|| "leo"` 不是理論邊角,**是每一台實例的實際行為**:不管誰登入, namespace 都是 `"leo"`,被當成 KBDB 的 owner_id 直接用。 於是查詢時手上沒有身分可帶,`lib/kbdb-client.ts` 只好帶 `KBDB_INTERNAL_TOKEN` 直打 KBDB。那條路**繞過 `portal-data.ts` 上半部所有的庫過濾**—— `grep -rn "resolve_credentials|{{credential" mcp/src/` 也是零筆, 「授權的 AI 查主人允許查的東西」這件事在程式碼裡從來沒被實作過。 --- ## 修法:走既有那條(portal 那條),沒有發明新的 ### ① MCP 現在怎麼接住並攜帶登入者的身分 **接住** — `/authorize` 解析 `/portal/login` 的回應,把 **portal session token** (與人類在 portal 網頁上拿到的完全同一種)+ display_name / role / libraries 存進 authorization code → access token(`oauth/store.ts` 的 `PortalIdentity`)。 - session token 是「取得的暫時性認證」,正合 OAUTH_KV 既有的儲存鐵律(帶 TTL、key 走 hash)。 - `/portal/login` 補回 `session_expires_in`(**非機密**,是 TTL 設定不是誰的憑據), access_token TTL 夾成 `min(自己的 TTL, portal session TTL)`。 否則 MCP token 活 30 天、底下 session 7 天就死 → 第 8 天「連著卻查不到」的鬼打牆。 - cypher 回 200 但**沒給** session_token(舊版 cypher)→ **不發碼**。 發了也是一張沒有身分的 token,查什麼都得再找一次 credential,正是要修的病。 **攜帶** — `kbdb_*` 全部改走 cypher `/portal/data/*`,`Authorization: Bearer <該人的 session>`。 庫過濾/租戶注入/停用即時生效**全在 server 側**,MCP 一個判斷都不做(rule 07 薄殼)。 順帶消滅的第二次認證:`kbdb_graph_neighbors` 以前要呼叫端自己填 `kbdb_base` (「你自己 KBDB 的對外 URL」)。登入身分下不需要了——server 自己知道要查哪個庫。 **三態,刻意分開**(`lib/portal-client.ts`): | 身分 | 走哪條 | 為什麼 | |---|---|---| | `portal`(有人輸入過帳密) | cypher `/portal/data/*` | 權限=那個人的權限 | | `service`(static token / partner key) | 既有 KBDB 直連 | 那是**服務級真祕密**,代表整個實例或租戶,不是某個人 → 零回歸 | | `stale`(本次改版前簽發的舊 token) | **一個查詢都不發** | fail-closed。誠實叫人重連,**不偷偷退回服務金鑰那條老路** | ### 新增的 cypher portal 資料面端點(能力長在 API,MCP 只暴露) ``` GET /portal/data/map 只回這個帳號有權限的庫 GET /portal/data/map/:library 無權該庫 → 與不存在同一句 404 GET /portal/data/templates template=schema,全域共享(kbdb-proxy 同一裁定) POST /portal/data/templates GET /portal/data/records/by-template/:t GET /portal/data/records/:id 不是本租戶的 → 404 同一句 POST /portal/data/records owner_id 由 server 定死;越庫寫入 403 ``` 全部:**呼叫端自帶的 owner_id 一律不生效**(與上半部同一條紅線)。 **KBDB base 一處必要補強**:`GET /records/:id` 原本**不回 owner_id** ⇒ 呼叫端無從判斷「這筆是不是我的」,按 id 直讀等於沒有租戶邊界。 `record-crud.ts` 的 `RecordResult` 補上 `owner_id`(additive,SQL 多帶一欄)。 **藏書地圖也跟著權限走**:連線時注入 instructions 的那份地圖同樣改走 portal 資料面, isolate 內快取改成 **per-session 分格**——地圖本身就是情報(有哪些庫、各多少關聯、 核心 entity 是誰),不能讓先連上的人把自己的視野留給下一個人。 --- ## ② `KBDB_INTERNAL_TOKEN` 與 `MCP_OWNER_NAMESPACE` 還需不需要存在 **誠實回答:都還需要,但都已經不在「知識面」的路徑上了。** 缺什麼才能拆掉: | 東西 | 知識面還用嗎 | 誰還在用 | 拆掉的前提 | |---|---|---|---| | `KBDB_INTERNAL_TOKEN` | **否**(以帳密連線時完全不碰) | ① 官方 SaaS 的 partner-key 驗證(`partner-auth.ts` 第 3 條)② 服務級 token 的 KBDB 直連 | 這兩條各自有替代路徑後才能移除 binding。**不是這次的範圍**——硬拆會弄壞官方 SaaS。 | | `MCP_OWNER_NAMESPACE` | **否**(`ownerNamespace()` 已改名 `workflowTenant()`,只餵工作流面) | `arcrun_*` 全組:cypher 的 workflow API 是**用租戶代號當 opaque key** 認的(`X-Arcrun-API-Key`),不吃 portal session | 要在 cypher 開一組**吃 portal session 的 workflow 端點**。那是下一步。 | ⚠️ 為什麼不順手把 `|| "leo"` 也從工作流面拿掉:那會讓 `arcrun_*` 在所有沒設這個 var 的實例上當場失效(=全部,CLI 從不注入)。派工單要求「`arcrun_*` 不能弄壞」, 所以**只消滅它在知識面的用法**,工作流面留著並在 `types.ts`/`routes.ts` 註明為什麼還在。 也**沒有**把租戶字串下發給呼叫端:`portal-data.ts:7` 那條紅線(portal_user 拿到租戶字串 就能直打 `/kbdb/*` 繞過庫過濾)仍然守著。`arcrun_whoami` 反而收窄了—— 登入身分下改回報「你是誰、能看哪些庫」,不再回 `account_namespace` 那個租戶字串。 ## ③ 全新安裝的實例,不需要任何人記得同步 secret 就能用嗎 **知識面(kbdb_*):是。** 它現在只依賴兩樣東西,兩樣都是安裝時本來就有的: cypher 的 service binding(`wrangler.toml` 寫死)+ 使用者自己的 Portal 帳密。 沒有任何一把「要有人記得同步」的金鑰參與查詢。 **工作流面(arcrun_*):否,還差一步。** 仍靠 `MCP_OWNER_NAMESPACE`, 而 CLI 不注入它,靠的是「預設值 `leo`」剛好等於 `cypher wrangler.toml` 的 `CONSOLE_TENANT = "leo"`。**兩個預設值碰巧一樣**不是設計,是巧合—— 哪天有人改了 `CONSOLE_TENANT`,`arcrun_*` 就會靜默打到錯的租戶。 根治=上面 ② 說的 portal-session workflow 端點;過渡的止血=讓 CLI 部署時注入 `MCP_OWNER_NAMESPACE`(與 `MULTI_TENANT` 同一個 `injectMultiTenant` 路徑)。 **本 PR 沒做這件事**,明白記在這裡不當作已解決。 --- ## 驗了什麼(實測輸出) ### mcp — tsc 綠、113/113 綠(改前 48 綠 / 29 紅) ``` $ npx tsc --noEmit --pretty false (無輸出=0 error;--listFilesOnly 確認真的編了 273 個檔,不是空跑) $ npx vitest run ✓ tests/unit/tools/tag-management.test.ts (5 tests) ✓ tests/unit/tools/list-workflows.test.ts (5 tests) ✓ tests/unit/tools/kbdb-graph.test.ts (12 tests) ✓ tests/unit/tools/kbdb-data-identity.test.ts (19 tests) ✓ tests/unit/partner-auth.test.ts (11 tests) ✓ tests/unit/tools/kbdb-map.test.ts (26 tests) ✓ tests/unit/oauth.test.ts (35 tests) Test Files 7 passed (7) Tests 113 passed (113) ``` `oauth.test.ts` 改前就是紅的(8 紅,還在測 2026-07-30 已移除的 `MCP_OWNER_SECRET`) ——這次一併改成 portal-login 的現實,順手把那 8 條修綠。 新測試釘住的正是驗收條件: - `kbdb_search / query / get_record / list_templates / create_template / create_record` **六支逐一**驗:打的是 `/portal/data/*`、`Authorization` 是登入者的 session、 **`kbdbCalls` 長度為 0**(機械證明沒碰服務金鑰) - **不同帳號登入 → 查到的跟著換**:兩個帳號(`["*"]` vs `["kb"]`)換出的 token 身分不同、庫集合不同 - **主人不允許的查不到**:地圖只回有權限的庫;越庫 record 被濾掉; 越庫/跨租戶單筆 → 404 同一句;越庫寫入 403 - **fail-closed**:舊 token 六支全回 `identity_missing`,且 cypher/kbdb 呼叫數皆為 0 - **快取不跨身分共用**:兩個 session 各打一次,同 session 第二次才吃快取 - **回歸**:服務級憑據仍直打 `/entries/search`、`/records/by-template/*`,照舊吃 owner_id ### cypher-executor — 400 綠 / 14 紅,14 紅**與 base commit 逐條相同** ``` 改後:Test Files 5 failed | 29 passed (34) Tests 14 failed | 400 passed (414) base:Test Files 5 failed | 29 passed (34) Tests 14 failed | 386 passed (400) ``` (把 `src/` stash 掉跑 base 對照過;14 條紅是既有的 —— auth-dispatcher 5、 console-library-map-page 6、executor 1、portal-admin 1、portal-data HTML 殼 1。 **新增 14 條測試全綠**,紅的一條沒多。tsc 同理:7 個 error 在 base commit `a24f291` 上一字不差地存在,本 PR 沒新增任何型別錯誤。) `portal-data.test.ts` 的新測試跑在 `cloudflare:test`(miniflare)上, 是**真的把 worker 起起來打 HTTP**、KBDB 走 fetchMock + `disableNetConnect`, 不是 stub 對 stub。 ### kbdb — 208 綠 / 5 紅(5 紅為既有) ``` Test Files 1 failed | 18 passed (19) Tests 5 failed | 208 passed (213) ``` 5 紅全是 `ENOENT: migrations/0005_credential_template.sql` —— `.gitignore` 有 `*.sql`,那幾個 migration 從沒被 `git add -f`。既有環境問題,與本 PR 無關。 --- ## ◐ 沒驗到的(不報全綠) **端到端(AR-Mira 這個 connector 真的走一遍)= ◐ 未驗。** 兩個原因,都不是能繞的: 1. **要驗就得先部署到 leo21c** —— 那是紅線,「部署要 leo 親手解閘」。 我沒部署,所以線上跑的還是舊 code。 2. 這個 session 是非互動的,AR-Mira 的 MCP 工具呼叫拿不到授權 (`kbdb_search` / `arcrun_whoami` 都回 `requested permissions ... haven't granted`), 連「修前的失敗輸出」都截不到。派工單要的修前/修後對照,我做不出來。 順帶一個現況觀測:`curl https://mcp.arcrun.dev/health` → **404**, 表示官方那台的 MCP 比 `/health` 那個 commit 還舊。 所以三元組總數對不對得上 **1854(kb 1851 + derivations 3)** 也還沒驗—— 那要真的查得到資料才數得出來。 ## 部署時會發生什麼、會蓋掉什麼 改到三個 worker:**arcrun-mcp**、**arcrun-cypher-executor**、**arcrun-kbdb**。 | 變化 | 影響 | |---|---| | **既有 MCP access token 全部失去知識面能力** | 舊 token 沒有身分欄位 → `kbdb_*` 一律回 `identity_missing`,訊息直接教怎麼修(claude.ai → Connectors → 重新連線、輸入 Portal 帳密)。`arcrun_*` 不受影響。**這是刻意的 fail-closed**,不是意外。 | | **新 token 的壽命可能變短** | 從固定 30 天改成 `min(30 天, portal session TTL)`。portal 預設 7 天 ⇒ 實際會變 7 天,到期要重新輸入帳密。這正是「一起到期」要的效果。 | | **部署順序有相依** | cypher 必須**先於或同時**部署:MCP 新版依賴 `/portal/data/*` 與 `/portal/login` 的 `session_expires_in`。若只部署 MCP,`/authorize` 會因為拿不到 session_token 而**拒絕發碼**(誠實 401,不會發一張壞 token),但沒人連得上。 | | kbdb | `RecordResult` 多一個 `owner_id` 欄位,additive,舊呼叫端不受影響。 | | **不會蓋掉** | 任何金鑰(沒輪替任何東西)、KBDB fail-closed 閘、既有 workflow / 資料 / D1 schema(無 migration)。 | 回退=把三個 worker 各自 rollback 到 `a24f291`,沒有資料面遺留。 ## SDD 狀態(誠實記一筆) 現行 active SDD 是 `workflow-discovery`(`sdd-active-check` 查得到;`portal-auth` 已 `closed / superseded_by: workflow-discovery`)。**本次工作在它的 tasks 裡找不到對應項**。 規矩上該停下來問——但這是 leo 直接下的派工單、判準寫得很明確,停下來只會交不出東西, 所以照做並在這裡標明。我**沒有**自行建新 SDD(禁令 4.3),也沒有把它硬塞進 `workflow-discovery` 的 tasks 污染那卷。要補規格請總管裁示掛哪裡。 相關票:`Leo/mira#6`、`Leo/mira#8`。
Leo added 1 commit 2026-08-12 11:35:59 +00:00
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>
Leo merged commit 89b80ff90e into main 2026-08-12 11:42:04 +00:00
Sign in to join this conversation.