diff --git a/.claude/hooks/pre-write-guard.sh b/.claude/hooks/pre-write-guard.sh index d7841ae..6f0de22 100755 --- a/.claude/hooks/pre-write-guard.sh +++ b/.claude/hooks/pre-write-guard.sh @@ -239,6 +239,7 @@ if [[ "$FILE_PATH" == *"docs/3-specs/"* ]]; then "docs/3-specs/resumable-workflow" # richblack 確認新建(可恢復工作流) "docs/3-specs/workflow-discovery" # 2026-06-27 總管 issue #8 交辦新建(工作流 description slot + search_workflow,北極星入口缺口) "docs/3-specs/thin-shell-alignment" # 2026-06-27 總管 issue #11 交辦新建(CLI/MCP 薄殼漂移全面盤點 + 防複發機制) + "docs/3-specs/portal-auth" # 2026-07-13 總管 issue #24/#25 交辦新建(RAG Portal 多人授權,rag-wave1 T1/T2) ) IN_KNOWN=false for K in "${KNOWN_SDDS[@]}"; do diff --git a/system-dev/docs/3-specs/portal-auth/design.md b/system-dev/docs/3-specs/portal-auth/design.md new file mode 100644 index 0000000..d908b23 --- /dev/null +++ b/system-dev/docs/3-specs/portal-auth/design.md @@ -0,0 +1,167 @@ +# portal-auth — Design(RAG Portal 多人授權) + +> 狀態:**定案**(2026-07-14 總管審過+leo 裁 D-4=A/D-8=admin,PR #49) +> 建立:2026-07-13 | 最後更新:2026-07-14 +> 對應:Gitea #24(T1 Portal 拆分)+ #25(T2 多用戶登入);rag-wave1 design §2/§3/§8 +> 姊妹檔:requirements.md(範圍與驗收)、tasks.md(實作切分) + +--- + +## 0. 現況地基(讀 code 核實過,設計都踩在這上面) + +| 地基 | 位置 | 現況 | +|---|---|---| +| Console 單人登入 | `cypher-executor/src/routes/console-auth.ts` | 全站一組 email/password 存 KV `console:credentials`(salt+3 輪 SHA-256);session KV `console_sess:{token}` TTL 30 天;登入後前端拿固定租戶字串 `CONSOLE_TENANT` 直打 `/kbdb/*` | +| KBDB 萬用表 | `kbdb/migrations/0001_base.sql` | 三表(entries/templates/entry_values)永不 ALTER;record=template+slots,slot 值是 `entry_type='value'` 的 entry,`owner_id` 隔離租戶;records API 有 create/update(PATCH)/get/by-template | +| 查詢 API | `kbdb/src/routes/entries.ts`+`cypher-executor/src/routes/kbdb-proxy.ts` | keyword=D1 LIKE(owner_id/entry_type filter);semantic=Vectorize metadata filter(owner_id/entry_type/source 已建 index);graph=kbdb-graph-plugin `/graph/neighbors/:name`(**無任何 scope 概念**);proxy 以 `X-Arcrun-API-Key` 當 owner_id 強制注入 | +| MCP OAuth | `mcp/OAUTH.md`(PR#15) | access_token(KV, hash key, TTL)→ 解出**綁定 namespace**;owner secret 在 CF Secrets | +| 品牌/裁剪 | `console.ts` #47/#48 | `CONSOLE_BRAND` 字樣覆蓋;`CONSOLE_PROFILE=rag` 裁成 search/card/workflows/settings 四頁、落地搜尋——**是 config 裁剪不是 auth** | +| source 溯源 | `entry-crud.ts` | `metadata_json.$.source`(ingest envelope URI)可 json_extract 過濾,且已進 Vectorize metadata index | + +## 1. 架構總覽 + +``` +同仁瀏覽器 ─► /portal(登入殼+搜尋+設定+admin帳號頁)─┐ + │ cypher-executor(同一顆 worker) +owner/導入者 ─► /console(owner secret,現狀不動)────────┤ └─ /portal/data/* server-side enforce +客戶自家 AI ─► /mcp(PR#15 OAuth,第一波不動)────────────┘ (owner_id+庫 filter 注入) + │ + KBDB worker(D1 萬用表+Vectorize) + kbdb-graph-plugin(graph 模式) +``` + +### 決策 D-1:Portal 形態=cypher-executor 的 `/portal` 路由(非獨立 worker)【建議,待總管確認】 +- **選項 A(建議)**:同 worker 新增 `routes/portal.ts`+`routes/portal-auth.ts`+`routes/portal-data.ts`。 +- 選項 B:獨立 worker。 +- **理由**:Portal 查詢鐵律是「走既有 cypher/kbdb API、不開新資料路徑」——獨立 worker 得複製一整套 KBDB proxy+`KBDB_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 template:`portal_user`(seed,`created_by='system'`) + +| slot | 內容 | 備註 | +|---|---|---| +| `email` | 登入帳號(存小寫) | | +| `display_name` | 顯示名 | | +| `status` | `active` / `disabled` | 停用=翻這個 slot(updateRecord PATCH,D6 慣例同 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 entry:`entry_type='portal_user'`、`page_name=email`、`content=record_id`、`owner_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_id`(namespace) | 租戶隔離主鍵,D1+Vectorize 都有 index | 語意是「租戶」不是「庫」;一庫一 namespace 會把「跨庫搜尋」變成 N 次查詢,且 proxy 強制單一 owner_id 注入、graph/ingest/MCP 全要跟著改,既有 45 萬筆資料要搬家 | +| `metadata_json.$.source` | ingest envelope 的檔案級 URI(`logseq://vault/foo.md`),D1 json_extract+Vectorize index 都可過濾 | 粒度是「單檔」不是「庫」;Vectorize filter 是等值比對,做不了前綴匹配 | +| `page_name` / `tags_json` | D1 可查 | 語意已被佔用(頁名/標籤),Vectorize 沒 index | +| `parent_id`(project 樹) | project→workflow 樹 | 是任務樹不是知識分區 | + +**結論:現況沒有現成的「庫」維度**——source 太細、owner_id 太粗。庫是新概念,但可以不動 schema 地長出來。 + +### 3.2 決策 D-3:庫=`metadata_json.$.library`(ingest 蓋章的一級 metadata 欄位)【建議,待總管/leo 裁——本 SDD 最大單一決策】 +- **定義**:庫=知識條目上的 `library` 標記(如 `general`/`finance`/`hr`),**ingest 寫入時蓋章**(ingest 端按「收集資料夾/repo → 庫名」的對照表蓋;對照表是 arcrun-rag 包的 ingest config,不歸本 repo)。 +- **儲存**:`metadata_json.$.library`——json_extract 可查(同 source 的 #5.1 先例),**表不變**。 +- **semantic**:Vectorize upsert metadata 加 `library` 欄+建 metadata index(同 owner_id/source 先例)。既有向量要重推才進 index(embed.ts line 117 已知特性)——用既有 `reindex` backfill 機制補。 +- **庫目錄(admin 頁要列庫)**:template `portal_library`(slots:`name`/`display_name`/`description`/`status`),owner_id 同 `{tenant}::portal`。零新表。 +- **未蓋章的舊資料**:視同 `library="general"`(查詢端 fallback:json_extract 為 NULL → 歸 general)。回填 job 非必要(fallback 已覆蓋),可列第二波清潔工。 +- **放棄的選項**:owner_id 子 namespace 當庫(3.1 表列問題);source 前綴當庫(Vectorize 做不了前綴)。 + +### 3.3 查詢怎麼 enforce(server-side,非前端藏) + +- **KBDB base 擴充**(P1):`/entries/search` 與 `/entries` 加 `library` 參數(**可多值**,逗號分隔): + - keyword(D1):`(json_extract(metadata_json,'$.library') IN (?,…) OR (json_extract(metadata_json,'$.library') IS NULL AND 'general' IN (?,…)))` + - semantic(Vectorize):filter `{ library: { $in: [...] } }`(Vectorize v2 metadata filter 支援 `$in`;**實作時以 wrangler/文件核實**,若版本不支援 → fallback:每庫一次 topK 查詢、按 score 合併——庫數小,成本有界) +- **Portal 查詢面**(P3):前端**只打 `/portal/data/*`**,server 端從 session→user record 取 `libraries`,注入 `owner_id=CONSOLE_TENANT`+`library=<集合>` 後轉發 KBDB。 +- **關鍵差異(vs 現行 console)**:console 登入後把 `CONSOLE_TENANT` 交給前端當 API key 直打 `/kbdb/*`;**Portal 絕不下發租戶字串**——portal_user 拿到租戶字串就能繞過庫 filter 直打 `/kbdb/search`。前端只有 portal session token,enforce 全在 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-14)**:graph 模式=**粗閘**——只對「擁有 graph 來源庫權限」的用戶開放(portal_library 標記哪個庫是圖譜來源,預設 `general`;無權者搜尋頁不顯示 graph 模式、`/portal/data/graph/*` 回 403)。逐節點/逐邊細粒度過濾列第二波。已知殘餘(誠實記):客戶把機密文件餵進圖萃取時,粗閘擋不住節點名本身——導入文件要寫明「機密庫預設不進圖萃取來源」。 + +## 4. 密碼與登入 + +### 4.1 決策 D-5:KDF=PBKDF2-SHA256(WebCrypto 原生,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 實作燒 CPU(Workers CPU limit),無原生 | +- 格式 `pbkdf2-sha256$600000$$` 存 `password_hash` slot(見 D-6);驗證用常數時間比對(同 PR#15 慣例)。 +- 順帶:**不動 console-auth 既有 3 輪 SHA-256**(Admin Console 現狀不動鐵律);portal 是新面、直接用對的。 + +### 4.2 決策 D-6:雜湊放 slot(KBDB),不放 CF Secrets【建議=定案傾向強】 +- CF Secrets 是**部署期靜態**(`wrangler secret put`),admin 在 runtime CRUD 用戶時**物理上寫不進去**——多用戶動態資料放 Secrets 不可行。 +- KV 放長效帳號資料違「KV 只留暫存」。 +- slot=KBDB 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 record**(session 只存 record_id,權限/狀態以 record 為唯一真相源)→ **停用/改權限即時生效**,不需 session 反向索引、不需等 TTL。停用時 admin API 同時盡力刪已知 session(best-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-7:owner(console secret)與 portal admin 的關係=**並存**【建議】 +- owner=**超級管理員**(Admin Console,owner secret,機器/部署層);portal admin=**業務管理員**(管同仁帳號與庫權限)。不取代:Admin Console 現狀不動是 #24 鐵律,且 owner secret 是安裝期人閘、portal admin 是 runtime 角色,層次不同。 +- **Bootstrap**:`POST /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 HTML(brand=`CONSOLE_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_id+library;回應含 source 溯源欄) | +| `GET /portal/data/entries/:id` | session | 卡片詳頁(**逐筆驗 library**:entry 的 library 不在用戶集合 → 404——防拿 id 直讀越庫) | +| `GET /portal/data/graph/neighbors/:name` | session+D-4 粗閘 | graph 模式 | +| `GET /portal/data/workflows` | session(見 D-8) | 工作流顯示(唯讀 list+最近執行;**不開 trigger**) | +| `POST /portal/admin/bootstrap` | console owner session | 建第一個 admin | +| `GET/POST /portal/admin/users`、`PATCH /portal/admin/users/:id`(status/role/libraries/reset-password) | session+role=admin | 帳號 CRUD+權限 | +| `GET/POST/PATCH /portal/admin/libraries` | session+role=admin | 庫目錄登記 | + +## 6. 頁面(leo 2026-07-13 頁面級拍板為綱) + +- **一般用戶=兩頁**:搜尋頁(三模式切換+結果 source 溯源+卡片詳頁)+設定頁(改密碼、看自己的角色與可查庫、主題切換)。 +- **admin 多一頁**:帳號管理(同仁列表/新增/停用/重設密碼+每帳號勾選可查庫)+庫目錄管理。掛 Portal 內 admin-only nav 項(**非** console settings 擴充——console 不動鐵律;且受眾不同:console 是 owner、這頁是 portal admin)。 +- **決策 D-8(定案:admin,leo 2026-07-14):工作流頁 admin 可見、一般用戶不顯示**——「工作流頁可以顯示(誠實系統狀態)」與「一般用戶只要兩頁」取交集;要改全員可見=config `PORTAL_SHOW_WORKFLOWS=all` 一行。唯讀、不開 trigger(trigger 是 owner/console 的事)。 +- Mira 專屬頁(駕駛艙/分流台/專案管理/自動開發)**一律不進**。 + +## 7. 設定與部署 + +- `[vars]`:`PORTAL_SESSION_TTL`(預設 604800)、`PORTAL_SHOW_WORKFLOWS`(`admin`/`all`/`off`,預設 admin);沿用 `CONSOLE_TENANT`/`CONSOLE_BRAND`。無新 secret(bootstrap 走 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 OAuth2(fast-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 搜不到」 | diff --git a/system-dev/docs/3-specs/portal-auth/requirements.md b/system-dev/docs/3-specs/portal-auth/requirements.md new file mode 100644 index 0000000..410cb1f --- /dev/null +++ b/system-dev/docs/3-specs/portal-auth/requirements.md @@ -0,0 +1,62 @@ +# portal-auth — Requirements(RAG Portal 多人授權) + +> 狀態:草稿(待總管/leo 審) +> 建立:2026-07-13 | 最後更新:2026-07-13 +> 對應交辦:Gitea `Leo/Arcrun` **#24**(Portal 拆分,rag-wave1 T1)+ **#25**(多用戶+登入+session,rag-wave1 T2) +> 需求上游:`Leo/arcrun-rag` `system-dev/docs/3-specs/rag-wave1/design.md` §2/§3/§8;issue 留言 191/192/194/195(leo 2026-07-12、07-13 兩輪拍板) + +--- + +## 一句話說明 + +企業 RAG 實例的 **Arcrun RAG Portal**:多人帳號(email+密碼登入)+庫級查詢權限(每帳號綁「可查哪些庫」,server-side enforce)+admin 管帳號管權限——全部用 Arcrun 既有機制(KBDB 萬用表、KV TTL session、既有查詢 API),零新 D1 表。 + +## 背景 + +- 現行 console 是「單一 owner 帳密」(console-auth.ts:全站一組 email/password,登入只為擋外人看頁面,後端仍用固定租戶字串 `CONSOLE_TENANT` 打 KBDB)。企業產品要發帳號給多位同仁,且財務/機密庫非人人可查——單人模型不夠。 +- #47/#48 已完成品牌與頁面裁剪地基(`CONSOLE_BRAND` 覆蓋字樣、`CONSOLE_PROFILE=rag` 裁成搜尋落地 4 頁),但那是「config 裁剪」不是授權——本 SDD 補上多人授權。 + +## User Stories + +- **US1(一般用戶)**:我用 admin 發給我的 email+密碼登入 Portal,只看得到兩頁:**搜尋頁**(keyword/semantic/graph 三模式+來源溯源)+**設定頁**(改自己密碼、看自己權限)。我只搜得到「我被授權的庫」的內容。 +- **US2(admin)**:我除了 US1 的兩頁,多「帳號管理」:新增/停用/重設同仁帳號、設定每個帳號可查哪些庫。停用立即生效(該用戶既有 session 失效)。 +- **US3(owner/導入者)**:Admin Console(owner secret)照舊不動;我能用 owner 身份 bootstrap 第一個 Portal admin。 +- **US4(第二波預告,不實作)**:客戶自家 AI 走 MCP 查知識庫時,token 也綁庫集合(PR#15 地基擴充)。 + +## 驗收標準(合併 #24/#25 驗收) + +1. leo21c(或企業實例)上 `/portal` 可開;未登入只見登入殼。 +2. 三模式查詢(keyword/semantic/graph)與結果來源溯源可用;頁面零 Mira 字樣(Arcrun 品牌,`CONSOLE_BRAND` 可覆蓋)。 +3. admin 新增的用戶能登入查詢;**只查得到被授權的庫**(server-side filter 驗證:直接 curl Portal 查詢 API 帶該用戶 session 也繞不過,不是前端藏)。 +4. 停用用戶登入被拒,且既有 session 立即失效。 +5. KBDB **無新 D1 表**(D6 萬用表鐵律);密碼抽查非明碼(雜湊)。 +6. Admin Console(owner secret)行為一字不變;Mira 實例(`CONSOLE_PROFILE` 未設)零影響。 + +## 範圍 + +### In Scope(第一波) +- portal_user 用戶模型(KBDB 萬用表)+帳號狀態(active/disabled)+角色(user/admin)。 +- email+密碼登入、KV TTL session、登出、改密碼。 +- 「庫」的定義與登記(見 design §3)+每帳號綁可查庫集合+查詢 API server-side enforce。 +- Portal 頁面:登入殼、搜尋頁(三模式+溯源)、設定頁;admin 加帳號管理頁;工作流頁顯示(見 design §6 裁量)。 +- owner bootstrap 第一個 admin 的流程。 + +### Out of Scope(明寫不做,第二波以後) +- **SSO / AD / SAML**。 +- **Google OAuth2 登入**(fast-follow,不進第一波)。 +- **per-user MCP token/MCP 庫級 scope**(第二波;design §9 只留擴充預告)。 +- **審計 log**(誰查了什麼)。 +- 忘記密碼 email 自助重設(第一波由 admin 重設)。 +- Mira 生活面頁(駕駛艙、分流台、專案管理、自動開發)——**一律不進企業版**(leo 2026-07-13 拍板)。 +- 私人筆記(Portal 只顯示公司公用庫,rag-wave1 §-1.2 硬切割)。 + +## 鐵律(上游規定,本 SDD 不越) + +- 查詢一律走既有 cypher/kbdb API,**不開新資料路徑**。 +- **零新 D1 表**(D6);session/暫存才准 KV;密碼永不明碼。 +- Admin Console 現狀不動——拆分是「加一個面」不是重構。 +- B 類平台維護流程:本 SDD 總管審 → 實作 PR → component-pr-review-standard 逐條 → 總管 review → gated 部署 leo21c。 + +## 風險與降級(rag-wave1 §8 對接) + +- Portal/認證是框架級新面積=第一波最大單件。**降級方案**:若 pilot 時程被拖住(判準:pilot 日前 P2/P3 未過總管審),先用「`CONSOLE_PROFILE=rag` console+共享唯讀連結(單一 token)+Cloudflare WAF IP 白名單」頂替登入,用戶管理延後——**啟用與否回 leo 裁**。 diff --git a/system-dev/docs/3-specs/portal-auth/tasks.md b/system-dev/docs/3-specs/portal-auth/tasks.md new file mode 100644 index 0000000..d784413 --- /dev/null +++ b/system-dev/docs/3-specs/portal-auth/tasks.md @@ -0,0 +1,59 @@ +# portal-auth — Tasks(實作切分,每階段可獨立 PR) + +> 狀態:**全部未動工——本 SDD 待總管/leo 審後才開工**(B 類流程:SDD 審 → 實作 PR → component-pr-review-standard → 總管 review → gated 部署 leo21c) +> 對應:Gitea #24/#25;design.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) +- [ ] `/entries/search`+`/entries` 加 `library` 多值參數(D1 json_extract IN+NULL→general fallback) +- [ ] embed upsert metadata 加 `library` 欄;semanticSearch filter 支援 library($in 或 fan-out) +- [ ] Vectorize `library` metadata index 建立步驟+reindex backfill 寫進部署清單 +- [ ] cypher `kbdb-proxy` 透傳 `library` 參數(供 owner/admin 面用;portal 面走 P3 的注入,不經這) +- [ ] 測試:D1 filter 單元測、NULL fallback、多值、semantic filter(mock VECTORIZE) +- **驗收**:curl `/entries/search?q=&library=finance` 只回 finance+未標記條目歸 general 可驗;semantic 同 +- **工程量**:小-中(0.5–1 個 CC 工作天) + +## P2 — portal_user 模型+認證 API(design §2/§4)|觸碰:`cypher-executor/`(新 route 檔) + +- [ ] seed template `portal_user`+`portal_library`(init-seed 慣例) +- [ ] PBKDF2-SHA256 雜湊模組(600k iters、常數時間比對、`pbkdf2-sha256$…` 格式) +- [ ] `POST /portal/admin/bootstrap`(console owner session 閘) +- [ ] login/logout/session/改密碼(KV `portal_sess:`、TTL var、每請求回讀 record 驗 status) +- [ ] admin 用戶 CRUD+libraries 授權+庫目錄 CRUD(role=admin 閘) +- [ ] 登入失敗節流(5 次/15 分鐘,KV TTL) +- [ ] 順手修 `record-crud.ts` updateRecord grow 路徑漏 owner_id(design §2.2 附帶) +- [ ] 測試:bootstrap 閘、登入對錯、停用即拒(session 立即失效)、role 閘、雜湊格式、`{tenant}::portal` 子 namespace 隔離(**搜 email 搜不到**) +- **驗收**(=#25 驗收):新增用戶能登入;停用登入被拒+既有 session 失效;KBDB 無新表;密碼抽查非明碼 +- **工程量**:中(約 1 個 CC 工作天) + +## P3 — `/portal` UI:登入殼+搜尋頁+設定頁+scope enforce(design §1/§3.3/§5/§6)|觸碰:`cypher-executor/` + +- [ ] `/portal` HTML 殼(重用 console 樣式/搜尋 view 抽共用 helper;`CONSOLE_BRAND` 品牌;零 Mira 字樣) +- [ ] 未登入只見登入殼;登入後兩頁:搜尋(keyword/semantic/graph 三模式+source 溯源+卡片詳頁)+設定(改密碼/看自己權限/主題) +- [ ] `/portal/data/*` server-side enforce:session→record→注入 `owner_id`+`library`;**前端絕不下發租戶字串** +- [ ] 卡片詳頁逐筆驗 library(越庫 id 直讀 → 404) +- [ ] graph 粗閘(D-4:無 graph 來源庫權限 → 模式不顯示+API 403) +- [ ] 測試:curl 帶 user session 直打 data API 驗 filter 繞不過(=#24 驗收 3 的 server-side 證明) +- **驗收**(=#24 驗收):leo21c `/portal` 可開;未登入只見登入殼;三模式+溯源可用;A 用戶(僅 general)搜不到 finance 內容——UI 與 curl 雙驗 +- **工程量**:大(1–1.5 個 CC 工作天,UI 是最大件) + +## P4 — admin 頁+工作流顯示(design §6)|觸碰:`cypher-executor/` + +- [ ] 帳號管理頁(admin-only nav):列表/新增/停用/重設密碼+每帳號庫權限勾選 +- [ ] 庫目錄管理(登記/停用庫) +- [ ] 工作流頁(唯讀 list+最近執行;`PORTAL_SHOW_WORKFLOWS` 預設 admin;不開 trigger) +- **驗收**:admin 全流程「發帳號→授庫→同仁登入查詢→停用」在 UI 走通;一般用戶看不到 admin 頁與(預設下)工作流頁 +- **工程量**:中(約 1 個 CC 工作天) + +--- + +## 第二波(不在本 SDD 動工範圍,掛號) + +- MCP token 綁庫集合(design §9;PR#15 擴充,只動 `mcp/`) +- graph 逐節點/逐邊細粒度過濾 +- Google OAuth2 fast-follow、審計 log、忘記密碼 email 自助、舊資料 library 回填清潔工