Files
Leo 5cadc60e36 feat(workflow-discovery): /cypher/search 改為真查 registry——修「查詢回假信號」
📋 SDD:workflow-discovery(本 commit 同時執行 D35 交接:portal-auth 26/26 完成 → closed
   superseded_by workflow-discovery;workflow-discovery paused → active。單一活性已驗=1 份)
🎯 對應 task:3.x 搜尋端誠實化(CP2-B)

病灶(leo 2026-07-30 定性「腹語術」):
search-nodes.ts 無條件回 status:'found'、missingNodes 永遠 []——
型別宣告了 'missing' 但程式碼從不使用。實測「完全不存在的東西xyz」也回 found。
⇒ AI 拿到假信號 → 以為零件存在 → 部署才發現沒有 → 改寫 code
⇒ 正式 workflow 只用 2 個零件、8 個 code 節點含 if×61。

修法:
- 查 registry 判真實存在(走 HTTP,守 D28 禁新增 service binding;
  URL 用既有 wasmWorkerUrl() 慣例組,不自創)
- found 時附 input_schema/success_rate/stability
  ⇒ AI 才填得出 payload、才看得到「測過幾次」(leo:AI 只要填 payload)
- 查不通回 'unknown' 而非 'missing'——**誠實限制**:
  不能因查詢失敗就宣告零件不存在(那會讓 AI 誤判而重寫 code)
- missing 真的回傳出去(原本寫死 [])

⚠️ 實測發現 registry **沒有列表端點**(GET /components → 404,只有 /components/<id>)
⇒ 改為逐個查(節點數通常 <10、5s timeout)。補列表端點後可改抓一次=CP2-B 待辦。

驗:tsc 零錯誤;vitest 9 failed/179 passed=**與改動前 stash 對帳完全相同**(既有債非本次造成)。
 待部署到實例後跑 arcrun-usable/verify.sh 驗 01 那組轉綠。
2026-07-30 20:41:53 +08:00

18 KiB
Raw Permalink Blame History

status, superseded_by, closed_at, closed_reason
status superseded_by closed_at closed_reason
closed workflow-discovery 2026-07-30 26/26 任務全完成、0 未完成待搬;封測 portal 多人授權已上線

portal-auth — DesignRAG Portal 多人授權)

狀態:定案2026-07-14 總管審過+leo 裁 D-4=A/D-8=adminPR #49 建立:2026-07-13 | 最後更新:2026-07-14 對應:Gitea #24T1 Portal 拆分)+ #25T2 多用戶登入);rag-wave1 design §2/§3/§8 姊妹檔:requirements.md(範圍與驗收)、tasks.md(實作切分)


0. 現況地基(讀 code 核實過,設計都踩在這上面)

地基 位置 現況
Console 單人登入 cypher-executor/src/routes/console-auth.ts 全站一組 email/password 存 KV console:credentialssalt3 輪 SHA-256);session KV console_sess:{token} TTL 30 天;登入後前端拿固定租戶字串 CONSOLE_TENANT 直打 /kbdb/*
KBDB 萬用表 kbdb/migrations/0001_base.sql 三表(entries/templates/entry_values)永不 ALTERrecordtemplateslotsslot 值是 entry_type='value' 的 entryowner_id 隔離租戶;records API 有 create/updatePATCH/get/by-template
查詢 API kbdb/src/routes/entries.tscypher-executor/src/routes/kbdb-proxy.ts keywordD1 LIKEowner_id/entry_type filter);semanticVectorize metadata filterowner_id/entry_type/source 已建 index);graphkbdb-graph-plugin /graph/neighbors/:name無任何 scope 概念);proxy 以 X-Arcrun-API-Key 當 owner_id 強制注入
MCP OAuth mcp/OAUTH.mdPR#15 access_tokenKV, hash key, TTL)→ 解出綁定 namespaceowner secret 在 CF Secrets
品牌/裁剪 console.ts #47/#48 CONSOLE_BRAND 字樣覆蓋;CONSOLE_PROFILE=rag 裁成 search/card/workflows/settings 四頁、落地搜尋——是 config 裁剪不是 auth
source 溯源 entry-crud.ts metadata_json.$.sourceingest envelope URI)可 json_extract 過濾,且已進 Vectorize metadata index

1. 架構總覽

同仁瀏覽器 ─► /portal(登入殼+搜尋+設定+admin帳號頁)─┐
                                                        │ cypher-executor(同一顆 worker
owner/導入者 ─► /consoleowner secret,現狀不動)────────┤   └─ /portal/data/* server-side enforce
客戶自家 AI ─► /mcpPR#15 OAuth,第一波不動)────────────┘        (owner_id+庫 filter 注入)
                                                        │
                                                   KBDB workerD1 萬用表+Vectorize
                                                   kbdb-graph-plugingraph 模式)

決策 D-1Portal 形態=cypher-executor 的 /portal 路由(非獨立 worker)【建議,待總管確認】

  • 選項 A(建議):同 worker 新增 routes/portal.tsroutes/portal-auth.tsroutes/portal-data.ts
  • 選項 B:獨立 worker。
  • 理由:Portal 查詢鐵律是「走既有 cypher/kbdb API、不開新資料路徑」——獨立 worker 得複製一整套 KBDB proxyKBDB_INTERNAL_TOKEN 持有面,多一個部署單位、多一個 secret 面;同 worker 重用 kbdbBase() 慣例零複製。企業實例本來就是「獨立部署的一套 cypher-executor」(帳號=環境模型),worker 級隔離已存在。
  • Mira 實例零影響:/portal 端點在未 bootstrap 任何 portal_user 時只回登入殼+「尚未啟用」,不影響既有頁面。
  • UI 重用:portal HTML 重用 console 的樣式系統與搜尋/卡片 view 的 render 片段(實作時抽共用 helper),但獨立 HTML 殼——不是在 console 上加 if,避免動到「Admin Console 現狀不動」鐵律。

2. 用戶模型(KBDB 萬用表,零新表)

2.1 templateportal_userseedcreated_by='system'

slot 內容 備註
email 登入帳號(存小寫)
display_name 顯示名
status active / disabled 停用=翻這個 slotupdateRecord PATCHD6 慣例同 deprecate-via-status-slot
role user / admin admin=多「管帳號+管權限」兩件事
password_hash KDF 輸出(見 §4),格式 pbkdf2-sha256$iters$salt$hash 自帶演算法前綴,未來換 KDF 可共存漸進遷移
libraries JSON array 字串,例 ["general","finance"];特殊值 ["*"]=全庫 可查庫集合(見 §3
created_at / updated_at ISO 時間 萬用表 entries 本有時間欄,slot 冗餘存一份供 admin 頁直讀

2.2 決策 D-2:帳號資料的 owner_id 系統子 namespace {tenant}::portal【建議,重要】

  • 問題(盤現況發現的真風險)record 的 slot 值是 entry_type='value' 的 entries、content 就是值本身;keyword 搜尋是 content LIKE、只按 owner_id 過濾(entry-crud.ts searchEntries 核實)。若 portal_user 存在 owner_id=CONSOLE_TENANT 底下,同仁的 email、甚至密碼雜湊會出現在搜尋結果裡
  • portal_user(含 §3 的 portal_library)一律寫 owner_id = "{CONSOLE_TENANT}::portal"。既有租戶查詢面(/kbdb/*、Portal 搜尋、MCP)都以 CONSOLE_TENANT 過濾 → 物理上撈不到帳號資料。零 KBDB 改動,純 caller 端約定。
  • 附帶盤出的既有小缺口:record-crud.ts updateRecord 的「grow 新 slot」路徑建 entry 時沒帶 owner_id(line 127)——對本設計影響小(getRecord 按 record_id 取不受影響),但 P2 實作時順手補上(帶 record 既有 owner_id),避免孤兒 entry。

2.3 email O(1) 查找(登入用)

record 本身沒有可索引的 head——另建一筆 head entryentry_type='portal_user'page_name=emailcontent=record_idowner_id={tenant}::portal。登入時走既有 indexed 查詢 GET /entries?page_name={email}&entry_type=portal_user&owner_id={tenant}::portal 一發命中。仍是 entries 表、零新表。(不用 KV 做 email index:帳號→record 映射是長效資料,進 KV 違鐵律。)

3. 「庫」是什麼(本 SDD 最核心的一題)

3.1 現況盤點:KBDB 語境裡可當「庫」的既有維度

候選 現況 當庫的問題
owner_idnamespace 租戶隔離主鍵,D1Vectorize 都有 index 語意是「租戶」不是「庫」;一庫一 namespace 會把「跨庫搜尋」變成 N 次查詢,且 proxy 強制單一 owner_id 注入、graph/ingest/MCP 全要跟著改,既有 45 萬筆資料要搬家
metadata_json.$.source ingest envelope 的檔案級 URIlogseq://vault/foo.md),D1 json_extractVectorize index 都可過濾 粒度是「單檔」不是「庫」;Vectorize filter 是等值比對,做不了前綴匹配
page_name / tags_json D1 可查 語意已被佔用(頁名/標籤),Vectorize 沒 index
parent_idproject 樹) project→workflow 樹 是任務樹不是知識分區

結論:現況沒有現成的「庫」維度——source 太細、owner_id 太粗。庫是新概念,但可以不動 schema 地長出來。

3.2 決策 D-3:庫=metadata_json.$.libraryingest 蓋章的一級 metadata 欄位)【建議,待總管/leo 裁——本 SDD 最大單一決策】

  • 定義:庫=知識條目上的 library 標記(如 general/finance/hr),ingest 寫入時蓋章ingest 端按「收集資料夾/repo → 庫名」的對照表蓋;對照表是 arcrun-rag 包的 ingest config,不歸本 repo)。
  • 儲存metadata_json.$.library——json_extract 可查(同 source 的 #5.1 先例),表不變
  • semanticVectorize upsert metadata 加 library 欄+建 metadata index(同 owner_id/source 先例)。既有向量要重推才進 indexembed.ts line 117 已知特性)——用既有 reindex backfill 機制補。
  • 庫目錄(admin 頁要列庫)template portal_libraryslotsname/display_name/description/status),owner_id 同 {tenant}::portal。零新表。
  • 未蓋章的舊資料:視同 library="general"(查詢端 fallbackjson_extract 為 NULL → 歸 general)。回填 job 非必要(fallback 已覆蓋),可列第二波清潔工。
  • 放棄的選項owner_id 子 namespace 當庫(3.1 表列問題);source 前綴當庫(Vectorize 做不了前綴)。

3.3 查詢怎麼 enforceserver-side,非前端藏)

  • KBDB base 擴充P1):/entries/search/entrieslibrary 參數(可多值,逗號分隔):
    • keywordD1):(json_extract(metadata_json,'$.library') IN (?,…) OR (json_extract(metadata_json,'$.library') IS NULL AND 'general' IN (?,…)))
    • semanticVectorize):filter { library: { $in: [...] } }Vectorize v2 metadata filter 支援 $in實作時以 wrangler/文件核實,若版本不支援 → fallback:每庫一次 topK 查詢、按 score 合併——庫數小,成本有界)
  • Portal 查詢面P3):前端只打 /portal/data/*server 端從 session→user record 取 libraries,注入 owner_id=CONSOLE_TENANTlibrary=<集合> 後轉發 KBDB。
  • 關鍵差異(vs 現行 consoleconsole 登入後把 CONSOLE_TENANT 交給前端當 API key 直打 /kbdb/*Portal 絕不下發租戶字串——portal_user 拿到租戶字串就能繞過庫 filter 直打 /kbdb/search。前端只有 portal session tokenenforce 全在 server。
  • ["*"](全庫)=不注入 library filter,只注入 owner_id。

3.4 graph 模式的庫權限(已知薄弱點,明寫)

kbdb-graph-plugin 的 /graph/neighbors/:name 只吃節點名,無 owner/庫概念(proxy 也只驗有 key)。三元組是跨文件萃取的衍生物,逐邊回查來源 entry 的 library=N+1 且語意模糊(一條邊可能來自多庫文件)。

  • 決策 D-4(定案:A 粗閘,leo 2026-07-14graph 模式=粗閘——只對「擁有 graph 來源庫權限」的用戶開放(portal_library 標記哪個庫是圖譜來源,預設 general;無權者搜尋頁不顯示 graph 模式、/portal/data/graph/* 回 403)。逐節點/逐邊細粒度過濾列第二波。已知殘餘(誠實記):客戶把機密文件餵進圖萃取時,粗閘擋不住節點名本身——導入文件要寫明「機密庫預設不進圖萃取來源」。

4. 密碼與登入

4.1 決策 D-5KDFPBKDF2-SHA256WebCrypto 原生,600k 迭代)【建議;issue 字面寫 bcrypt/argon2,偏離需總管點頭】

選項 評估
PBKDF2-SHA256 600k iters(建議) Workers crypto.subtle.deriveBits 原生、零依賴;OWASP 現行建議值;console-auth 已有「登入雜湊寫在 cypher TS」先例(rule 2.2 限的是 workflow credential 原語,不是 console/portal 登入)
argon2id 記憶體硬化更強,但 Workers 無原生,要引 WASM 模組(新依賴+冷啟成本);Workers 128MB 記憶體下參數也開不大
bcrypt 純 JS 實作燒 CPUWorkers CPU limit),無原生
  • 格式 pbkdf2-sha256$600000$<salt_b64>$<hash_b64>password_hash slot(見 D-6);驗證用常數時間比對(同 PR#15 慣例)。
  • 【修訂 2026-07-14T6-cloud 真雲實撞】迭代數 600k → 100kCF Workers 正式 runtime 的 PBKDF2 上限是 100,000crypto.subtle.deriveBits 超過直接拒絕)→ /portal/admin/bootstrap 真雲 500miniflare 無此限制=本機全綠假象(證據:arcrun-rag docs/manual/uncle6-deploy-record.md)。 OWASP 建議 600k 但平台封頂——儲存格式自帶迭代數(verify 從儲存值解析),未來平台放寬可無痛升。 誠實記:600k 舊 hash 的「漸進遷移」只在 miniflare/放寬後成立,真雲 deriveBits 同樣封頂; 所幸真雲 bootstrap 從未成功,雲端不存在 600k hash,無實際遷移面。
  • 順帶:不動 console-auth 既有 3 輪 SHA-256Admin Console 現狀不動鐵律);portal 是新面、直接用對的。

4.2 決策 D-6:雜湊放 slotKBDB),不放 CF Secrets【建議=定案傾向強】

  • CF Secrets 是部署期靜態wrangler secret put),admin 在 runtime CRUD 用戶時物理上寫不進去——多用戶動態資料放 Secrets 不可行。
  • KV 放長效帳號資料違「KV 只留暫存」。
  • slotKBDB D1 長效儲存,符合三層 by 用途(credential-storage-three-tier:長期→D1)。雜湊非明碼、不可逆;外洩風險靠強 KDF+D-2 子 namespace(搜不到)緩解。

4.3 登入/Session 流程

  • POST /portal/login {email,password} → head entry 查 record → status=active → KDF 驗證 → 發 portal_sess:{token}KV SESSIONS_KV,值={record_id}TTL PORTAL_SESSION_TTL 預設 7 天;issue 要求「短效」,比 console 30 天緊)。
  • 每個 /portal/data/* 請求都回讀 user recordsession 只存 record_id,權限/狀態以 record 為唯一真相源)→ 停用/改權限即時生效,不需 session 反向索引、不需等 TTL。停用時 admin API 同時盡力刪已知 sessionbest-effort),但正確性不依賴它。
  • 登入失敗節流(建議含,可裁):KV 計數 portal_lockfail:{email},5 次失敗鎖 15 分鐘(TTL 自然過期)。公開登入面的最低保險,工程量半天內。
  • 改自己密碼:POST /portal/me/password {current,new}(驗舊密);admin 重設他人:POST /portal/admin/users/:id/reset-password(回一次性新密碼,要求首登改密=slot 加 must_change:true——可裁,第一波可簡化為 admin 口頭轉交新密碼)。

4.4 決策 D-7ownerconsole secret)與 portal admin 的關係=並存【建議】

  • owner超級管理員Admin Consoleowner secret,機器/部署層);portal admin業務管理員(管同仁帳號與庫權限)。不取代:Admin Console 現狀不動是 #24 鐵律,且 owner secret 是安裝期人閘、portal admin 是 runtime 角色,層次不同。
  • BootstrapPOST /portal/admin/bootstrap 需帶 console owner session(重用 console-auth 的 validateConsoleSession)→ 建第一個 role=admin 的 portal_user。不引入新 secret、不開放無閘註冊。安裝器(rag-wave1 §6)把這步寫進一條龍。

5. API 面(全掛 cypher-executor/portal 前綴)

Method/Path auth 作用
GET /portal 無(回登入殼) Portal HTMLbrandCONSOLE_BRAND
POST /portal/login / POST /portal/logout — / session 登入/登出
GET /portal/session session 驗 session+回 {display_name, role, libraries}(前端據此渲染;不回租戶字串
POST /portal/me/password session 改自己密碼
GET /portal/data/search?q=&mode= session 三模式查詢(server 注入 owner_idlibrary;回應含 source 溯源欄)
GET /portal/data/entries/:id session 卡片詳頁(逐筆驗 libraryentry 的 library 不在用戶集合 → 404——防拿 id 直讀越庫)
GET /portal/data/graph/neighbors/:name sessionD-4 粗閘 graph 模式
GET /portal/data/workflows session(見 D-8 工作流顯示(唯讀 list+最近執行;不開 trigger
POST /portal/admin/bootstrap console owner session 建第一個 admin
GET/POST /portal/admin/usersPATCH /portal/admin/users/:idstatus/role/libraries/reset-password sessionrole=admin 帳號 CRUD+權限
GET/POST/PATCH /portal/admin/libraries sessionrole=admin 庫目錄登記

6. 頁面(leo 2026-07-13 頁面級拍板為綱)

  • 一般用戶=兩頁:搜尋頁(三模式切換+結果 source 溯源+卡片詳頁)+設定頁(改密碼、看自己的角色與可查庫、主題切換)。
  • admin 多一頁:帳號管理(同仁列表/新增/停用/重設密碼+每帳號勾選可查庫)+庫目錄管理。掛 Portal 內 admin-only nav 項( console settings 擴充——console 不動鐵律;且受眾不同:console 是 owner、這頁是 portal admin)。
  • 決策 D-8(定案:adminleo 2026-07-14):工作流頁 admin 可見、一般用戶不顯示——「工作流頁可以顯示(誠實系統狀態)」與「一般用戶只要兩頁」取交集;要改全員可見=config PORTAL_SHOW_WORKFLOWS=all 一行。唯讀、不開 triggertrigger 是 owner/console 的事)。
  • Mira 專屬頁(駕駛艙/分流台/專案管理/自動開發)一律不進

7. 設定與部署

  • [vars]PORTAL_SESSION_TTL(預設 604800)、PORTAL_SHOW_WORKFLOWSadmin/all/off,預設 admin);沿用 CONSOLE_TENANT/CONSOLE_BRAND。無新 secretbootstrap 走 console owner session)。
  • 部署:gated leo21c wrangler 直推(B 類流程);Vectorize library metadata index 建立+reindex backfill 是部署步驟(寫進 PR 的部署清單與 rag-wave1 安裝器)。
  • Mira 實例:不 bootstrap 即不啟用;CONSOLE_PROFILE 與 Portal 互不干涉(#48 是 console 的裁剪、本 SDD 是新面)。

8. 不做清單(第二波以後,明寫)

SSO/AD/SAML、Google OAuth2fast-follow)、per-user MCP token、審計 log、忘記密碼 email 自助、graph 逐節點細粒度過濾、舊資料 library 回填 job(fallback 已覆蓋)、密碼策略進階(複雜度規則/輪換)。

9. MCP 第二波預告(只預告,不實作)

PR#15 的 access_token KV record 現存「綁定 namespace」→ 擴成 {namespace, libraries}/authorize 同意頁讓 owner 勾庫集合;partner-auth 解 token 後把 libraries 帶進 MCP 工具的 KBDB 呼叫(同 §3.3 filter)。P1 的 KBDB library filter 地基即為此鋪路——第二波只動 mcp/,不再動 kbdb。

10. 風險

風險 緩解
Vectorize $in filter 版本支援不確定 P1 第一件事實測;fallback 每庫 fan-out 合併(設計已含)
graph 模式洩漏機密庫節點名 D-4 粗閘+可收緊成 admin-only;第二波細粒度
既有向量不重推就 filter 不到(embed.ts 已知特性) 部署清單強制 reindex backfill+驗收抽查 semantic 命中
Portal 時程拖住 pilot requirements 降級方案(rag profile+共享唯讀連結+IP 白名單),啟用回 leo 裁
帳號資料進搜尋結果 D-2 子 namespace(物理隔離)+P2 驗收明測「搜 email 搜不到」