Commit 5ceb8d02efa95e01ae3340f7541f829bdf642d57

Authored by yibo
1 parent 76d2a5ae

fix(auth): Phase 3 —— agent-chat.js 跨租户读会话漏洞(38 号文档收尾)

验证阶段真查出一个 Phase 1/2 都没顾上的真实漏洞:agent-chat.js(/api/agent/*)一直靠
:agentId 里的 "companyId:role" 命名空间前缀(locator.companyStateDir 解析)区分会话
属于哪家公司,但这个文件从只挂了 requireAuthOrInternal(登录即可),没有归属校验。
curl 验证:任意登录用户对 GET /api/agent/{别人的companyId}:{role}/conversations
依然 200,能跨租户读别家公司的 agent 会话。

修法跟 company.js 同一套思路:router.param('agentId', ...) 解析 : 前的 companyId 段,
过 canAccessCompany。三个例外:①没有 : 前缀的全局 agent(master/org-innovator 等,
落 _global)不做归属校验,本来就全系统共享;②内部自环调用(req.user.internal)直接
放行,招聘/训练自动流水线要能打到任意公司名下;③admin 不受限制。

新增 agent-chat-company-gate.test.js:直接拿 Express 暴露的 router.params.agentId[0]
单测这个 param handler,覆盖同公司放行/跨公司404/admin不受限/公司不存在404/全局agent
放行/内部自环放行六种情况。顺带修好一个连带失败的既有测试文件
(agent-chat-tenant-isolation.test.js,zz-tenant-alpha/beta 同样是从没真正建过的
虚构公司 id)。

验证:curl 复测跨租户读会话从 200 变 404,owner/admin 依然 200,全局 agent 不受影响;
真实浏览器用全新账号确认 CompanyDashboard.vue 对"没有可用公司"已有现成空状态,不需要
新写前端代码。全量 vitest 1433 passed,3 个无关既有失败不受影响。

至此 38 号文档 Phase 0/1/2/3 全部完成。

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
docs/README.md
... ... @@ -49,7 +49,7 @@ Graph Agent 架构重构(27 系列)。**已完成的阶段文档已移入 `d
49 49 | [29-dream-design.md](./todo/system-design/29-dream-design.md) | **dream(经验汇流)**— ✅ 已实现。定时整理所有 agent 在所有公司的记忆,提炼后跨公司沉淀(不泄漏公司信息)。四条取舍已拍板并实现:去敏独立判定、四落点分层写入(经验/`SKILL.md`/`soul.md`/`agent.md`,受管区块+三道闸门)、每日定时(串行+空转保护)、引用真伪自动查、REST+控制台入口(`/api/dream/*`、`/dream`)。`innovator-evolver` 真 CLI 跑通一次。**仍缺的**:「结论对不对」只能人抽查;「套进 org/work graph 来跑」评估后用户选了不做(作用域不匹配,见文内 §5.7) |
50 50 | [30-task-delegation-reliability-gaps.md](./todo/system-design/30-task-delegation-reliability-gaps.md) | **任务委派可靠性现状**— 📝 调研记录,非设计方案。DevLoop `runUnit`、Company 招聘装配、Graph 执行三条链路核对结论一致:都是「失败预算耗尽 → 停等人工」,没有「卡住了主动通知人」这一层;DevLoop 当前还默认走 mock,真实 CLI 成功率无实证数据。2026-08-23 追加真实实例并已修:`infra_flake` 原地重跑没有熔断线,真撞上过一次确定性死循环(节点把诊断结论误判成自己的执行结果,反复重跑却从不路由到诊断建议的目标节点);已加 `infra_flake_count` 列 + 硬顶 3 次熔断,连续判 infra_flake 超过硬顶就改判 blocked 交给人工 |
51 51 | [31-work-graph-fail-fallback-coverage.md](./todo/system-design/31-work-graph-fail-fallback-coverage.md) | work graph 兜底边覆盖不全 — ✅ 已完成(除入口节点外强制兜底边,`planner.json`/`executor-fanout.json` 补 6 条边),978/978 单测通过;2026-08-21 又用真实 LLM 起了一条新任务做端到端回归验证,`org-planner-sdd` 自判失败后自动打回 `org-planner-prd`(3 次,零人工介入),修复确认生效 |
52   -| [38-auth-multi-tenant-isolation.md](./todo/system-design/38-auth-multi-tenant-isolation.md) | 📝 **Phase 0/1/2 已完成(2026-08-25),Phase 3 未开工**。远程部署前置需求:登录鉴权 + 公司级多租户隔离。**Phase 0**:种子账号 `admin`/`hex1234` + 登录页 + 路由守卫 + 401 拦截器,真实浏览器走通。**Phase 1**:给 9 个此前零防护的路由文件挂 `requireAuth`/`requireAdmin`;`agent-chat.js` 用新写的 `requireAuthOrInternal`(进程内 `INTERNAL_TOKEN`)——三处服务端自环调用(招聘/训练自动流水线、goal 汇报回调)拿不到用户 cookie,直接挂锁会打断核心自动化链路;17 个既有测试文件的 67+ 处用例改为测试环境默认放行 + 新增 `route-auth-gate.test.js` 结构化守卫测试。**Phase 2**:`company.json` 加 `ownerId`(无 SQL 表),`canAccessCompany` 作为全仓库唯一归属判据;`company.js`/`graph-run.js`(两个挂载点参数形状不同,`companyRouter` 按 companyId 直查、主 router 按 `:id`=run 反查 company_id,设计阶段的描述不准确,实现时才发现)/`devloop.js`(`:pid` 反查 company_id)分别接入;存量公司迁移脚本(`npm run backfill:company-owner`)已跑;`okr.js` 最终没有按原计划接入 companyId(发现是全站零导航入口的孤儿页面,改挂 `requireAdmin` 而非投入前端"选公司"工作量,与设计阶段方案的偏离已如实记录);8 个既有测试文件因为用纯字符串公司 id、从未真正建过公司,新增共享 helper `ensure-test-company.js` 修复。全量 `vitest` 1427 passed(3 个无关既有失败不受影响)。Phase 3(聊天隔离验证 + 前端收尾)待开工 |
  52 +| [38-auth-multi-tenant-isolation.md](./todo/system-design/38-auth-multi-tenant-isolation.md) | ✅ **Phase 0/1/2/3 全部完成(2026-08-25)**。远程部署前置需求:登录鉴权 + 公司级多租户隔离。**Phase 0**:种子账号 `admin`/`hex1234` + 登录页 + 路由守卫 + 401 拦截器。**Phase 1**:9 个此前零防护的路由文件挂 `requireAuth`/`requireAdmin`;`agent-chat.js` 用新写的 `requireAuthOrInternal`(进程内 `INTERNAL_TOKEN`)放行三处服务端自环调用(招聘/训练自动流水线、goal 汇报回调);新增 `route-auth-gate.test.js` 结构化守卫测试。**Phase 2**:`company.json` 加 `ownerId`,`canAccessCompany` 作为全仓库唯一归属判据;`company.js`/`graph-run.js`(两个挂载点参数形状不同,设计阶段描述不准确,实现时才发现)/`devloop.js` 分别接入;`okr.js` 发现是零导航入口的孤儿页面,改挂 `requireAdmin` 而非投入前端"选公司"工作量(与设计阶段方案的偏离);存量公司迁移脚本已跑。**Phase 3**:验证阶段真查出一个漏洞——`agent-chat.js` 靠 `:agentId` 里的 `"companyId:role"` 前缀区分会话归属,但一直没做归属校验,任意登录用户能跨租户读别家公司的 agent 会话;已修复(`router.param('agentId')` + `canAccessCompany`,全局 agent/内部自环调用例外放行),新增 `agent-chat-company-gate.test.js`。全程新增/修复 9 个既有测试文件(用纯字符串公司 id 从未真正建过公司,新增共享 helper `ensure-test-company.js`)。全量 `vitest` 1433 passed,3 个无关既有失败不受影响 |
53 53 | [37-org-graph-department-scoping.md](./todo/system-design/37-org-graph-department-scoping.md) | 📝 **设计讨论,调用点扫描已完成,尚未立项实现**。现状 org graph 按 `company_id` 单例(`27-P2` 标题原文即"公司级"),塞不下 HR/财务/市场这类跟"交付流水线"无关的公司常设职能——`system/agents/hr/`("招·留·管·退")本体已存在却从没被装进任何图,全靠 fire-and-forget chat 招聘、无图执行审计轨迹。提议把 org graph 作用域从 `company_id` 升级为 `(company_id, department_id)`,一家公司可装配多张部门图,每张仍保持人工固定/慢变/可审计。全仓库调用点扫描结论:改造面是最初估算的 4 倍——真正的单点真相在 `work-graph-db.js:readDefGraph` 的裸 SQL 不变式(不在服务层),还牵涉两个易混淆服务文件间必须字节级一致的重复唯一索引、独立的 `graph-seed-service.js` 对账子系统、`company-hire-org-layer.test.js` 专属测试、`orggraph-planning`/`task-planning` 两份 agent 人设文档的自我认知改写 |
54 54 | [36-prd-agent-methodology-training-feasibility.md](./todo/system-design/36-prd-agent-methodology-training-feasibility.md) | 📝 **调研 + 阶段一/二均已完成**。回应"落地页太单薄是不是该用 Innovator 先训练专业 agent、再用 Dream 持续迭代"——认同方向。**阶段一**:`graph-engine.js` 结算节点后把 `success`/`fail:<reason>` 写进本体 `memory.md`(此前只有人类对话路径会写,Dream 天天空转)。**阶段二**:新建独立试验节点 `prd-methodology-trial` 走真实 Hunt→Digest→Evolve,2 轮收敛(96/97 分,真实基线 50 分),三个真实历史项目反推验证方法论有效;过程中连带修复同一个 2026-08-16 改名批次遗留的 5 处 `innovator`→`org-innovator` 死链(后端两处硬编码、前端"重新训练"按钮、`/api/agent/list` 展示 id 来源的四个 `config.json`),以及 `AgentScaffold` 默认模板一处裸相对路径缺陷。结论:FDE 对方法论类角色确实管用,但收敛速度/验收严谨度依赖任务书显式写明的自纠正要求,非免人工黑盒。环节 3(训练产物 → 雇佣本体转换)尚未开工 |
55 55 | [35-validation-rigor-and-body-readiness-proposal.md](./todo/system-design/35-validation-rigor-and-body-readiness-proposal.md) | 📝 **复盘 + 改进方案,四条提案全部已实施**。承接 31/32/33/34 号的复盘结论:任务结果差主要是架构问题(完整性闸门 `missingBodyDefinition` 只在招聘时查、图执行侧完全没有第二道防线——新发现)+ 一个具体的招聘装配可靠性 bug,不是 agent 能力问题。提案 A:本体不完整时图执行前置判 `fail_reason=body_not_ready`,不扣预算。提案 D:`goal-contract.js` 的 `evidence_hint` 命中浏览器类关键词的维度必须有证据支撑,否则判不过。提案 C:新增 `scripts/visual-acceptance-check.mjs` 复用检查工具。提案 B:控制台「验收证据」小节展示每个维度有没有证据支撑 |
... ...
docs/todo/system-design/38-auth-multi-tenant-isolation.md
... ... @@ -4,7 +4,7 @@
4 4 > **触发**:代码要部署到远程服务器,现状零鉴权——任何人打开域名直接进系统,`:companyId` 全部路由零权限校验,谁都能看/改任何一家公司。用户要求:登录注册(种子一个默认账号)+ 全站 cookie 鉴权 + 每个用户只看得到自己的 company(system agent 全局共享不受影响)+ 每个用户的聊天记录互相隔离。
5 5 > **前置结论(已在同一会话里做过一次调研,详见对话记录,不重复展开)**:这不是从零建鉴权系统。`server/routes/auth.js` + `server/middleware/jwt-auth.js` 已经是一套完整可用的注册/登录/JWT-cookie/GitHub+Google OAuth 实现,`/ws/claude` 的 WebSocket 鉴权也已经做完了。真正缺的是两件事:①把这套已有的锁**挂到**目前完全不设防的路由上;②公司归属这个概念**从 0 到 1** 新建(`company.json` 目前连 `owner` 字段都没有)。
6 6 > **已拍板的产品决策**:存量公司(`data/companies/` 下现有的全部)迁移时统一划给种子默认账号;种子账号 `admin` / `hex1234`(首次部署后建议改密,见 §七)。
7   -> **状态**:实现计划,未开工。
  7 +> **状态**:✅ Phase 0/1/2/3 全部完成(2026-08-25)。四个阶段的实现细节、跟设计阶段的偏离(`graph-run.js` 双挂载点参数形状不同、`okr.js` 改成 admin-only 而非归属校验)、以及 Phase 3 真查出的一个漏洞(`agent-chat.js` 之前可跨租户读会话),都记在下面对应 Phase 小节里。
8 8  
9 9 ---
10 10  
... ... @@ -113,10 +113,13 @@ router.param(&#39;companyId&#39;, (req, res, next, companyId) =&gt; {
113 113 - **测试套件的连带修复,比预想的更细碎**:8 个既有测试文件(`agent-transcript`/`graph-run-route-io`/`graph-run-route-slot-busy`/`p3c-recovery-http`/`task-slots-parallel`/`company-files-path-guard`/`heartbeat-company-scoped`/`devloop` 下 3 个文件)用的公司 id 都是纯字符串标签,从没通过 `companyService.create()` 建过真公司——新增共享测试 helper `server/tests/helpers/ensure-test-company.js`(`ensureTestCompany`/`removeTestCompany`),在每个文件生成测试公司 id 的唯一入口处补一次 `company.json`;两个文件里原有的"跳过清理已存在 `company.json` 的目录(那是真公司)"防御逻辑,判据从"有没有 `company.json`"改成"`ownerId` 是不是 `-1` 这个测试专用标记",否则会把这批新出现的测试夹具误判成真公司、永远不清理。
114 114 - **验收**:全部真实 `curl` 走过(种子账号登录看到全部存量公司;新注册账号登录看到空列表;新账号直接拼 URL 访问种子账号的 company id → 404;新建 company 后只有创建者自己的 `list` 能看到、admin 两个都看到、`ownerId` 落的确实是创建者的 `users.id`)。全量 `npx vitest run`:1427 passed,同样 3 个无关的既有失败(未受影响)。
115 115  
116   -### Phase 3 — 聊天记录隔离验证 + 前端收尾体验
117   -- LemonBoss 侧会话(`data/companies/{id}/agents/{agentId}/{convId}`)理论上 Phase 2 做完就自动隔离了(会话挂在 company 目录下),这一步只是**验证**,不是新写代码:确认没有遗漏的、绕过 company 归属校验就能读会话的接口(比如某个直接按 `convId` 查、不经过 `companyId` 校验的路由)。
118   -- 前端补全登出后的空状态、加载态、错误提示;确认 18 个路由页面在未登录/无权访问公司时的表现符合预期。
119   -- **验收**:跨账号真实操作一遍——种子账号和新账号分别登录,交叉尝试访问对方的会话记录,确认全部拒绝。
  116 +### Phase 3 — 聊天记录隔离验证 + 前端收尾体验 ✅ 已完成(2026-08-25)
  117 +- **验证不是走过场,真查出了一个之前没预料到的真实漏洞。** `agent-chat.js`(`/api/agent/*`)一直靠 `:agentId` 里的命名空间前缀(`"{companyId}:{role}"`,`locator.companyStateDir` 解析)区分会话属于哪家公司,但 Phase 1 只给它挂了登录门槛(`requireAuthOrInternal`),Phase 2 的归属收口只覆盖了 `company.js`/`graph-run.js`/`devloop.js`,完全没顾上这个文件。真实 `curl` 验证:任意登录用户对 `GET /api/agent/{别人的companyId}:{role}/conversations` 依然 200——能跨租户读别家公司的 agent 会话。
  118 +- 修法跟 `company.js` 同一套思路:`router.param('agentId', ...)`,解析 `:` 前的 `companyId` 段,过 `canAccessCompany`。三个例外都要照顾到:①没有 `:` 前缀的**全局 agent**(`master`/`org-innovator` 等,落 `_global`)不做归属校验——它们本来就是全系统共享,不属于任何公司;②**内部自环调用**(`req.user.internal`,见 Phase 1 的 `INTERNAL_TOKEN`)直接放行,招聘/训练自动流水线要能打到任意公司名下;③管理员一如既往不受限制。
  119 +- 新增单元测试 `server/tests/unit/agent-chat-company-gate.test.js`:直接拿 Express 暴露的 `router.params.agentId[0]` 单测这个 param handler(不起真 HTTP server),覆盖同公司放行/跨公司 404/admin 不受限/公司不存在 404/全局 agent 放行/内部自环放行六种情况。
  120 +- 顺带修好一个既有测试文件的连带失败:`agent-chat-tenant-isolation.test.js`(测另一类更早的租户隔离 bug——todo/plan 落点、CLI cwd)用的 `zz-tenant-alpha`/`zz-tenant-beta` 也是从没真的建过公司的字符串标签,同样用 `ensure-test-company.js` 补上。
  121 +- 前端:直接拿一个新注册、不属于任何公司的账号在真实浏览器里跑一遍——`CompanyDashboard.vue` 对"当前没有可用公司"这个状态已经有现成的空状态处理("自治架构观测站 · 没有任何正在运行中的公司或工作舱"),不需要为这次改造专门新写空状态;导航栏/退出登录跟 Phase 0 验证过的行为一致。
  122 +- **验收**:`curl` 复测跨租户读 `agent-chat.js` 会话从 200 变 404,admin/所有者依然 200,全局 agent 不受影响,公司不存在依然 404;真实浏览器用全新账号访问一家不属于自己的公司的 URL,看到的是既有的、体验良好的空状态而不是报错/崩溃/白屏。全量 `npx vitest run`:1433 passed(含新增 6 条),同样 3 个无关既有失败。
120 123  
121 124 ---
122 125  
... ...
server/routes/agent-chat.js
... ... @@ -25,7 +25,8 @@ const locator = require(&#39;../services/agent-locator&#39;);
25 25 const { OkrService } = require('../services/okr-service');
26 26 const { saveSessionSummary, loadRecentMemory, retrieveRelevantPlans, autoReflect, consolidateMemory, generateDailyReport } = require('../services/memory-service');
27 27 // 「这家公司雇了谁」的唯一真相来源(28 §4.2):公司侧不再有 agent 目录可扫
28   -const { listCompanyAgents } = require('../services/company-service');
  28 +const { listCompanyAgents, CompanyService, canAccessCompany } = require('../services/company-service');
  29 +const companyService = new CompanyService();
29 30 // 交付物归宿只有一份推导(28 §4.4),公司环境上下文与执行链路的运行时坐标共用它
30 31 const { companyWorkspaceDir } = require('../src/core/prompt-builder');
31 32 const systemConfig = require('../config');
... ... @@ -38,6 +39,25 @@ const router = express.Router();
38 39 // 放行带对内部令牌的自环请求,其余情况仍然要求真实登录。
39 40 router.use(requireAuthOrInternal);
40 41  
  42 +// 38 号文档 Phase 3:agent-chat.js 一直靠 :agentId 里的命名空间前缀("{companyId}:{role}",
  43 +// locator.companyStateDir 解析)区分会话属于哪家公司,但 Phase 1/2 都没顾上这个文件的
  44 +// 归属校验——只要求登录,没查公司归不归你。核实过是真的能跨租户读会话(GET .../conversations
  45 +// 对不是自己公司的 companyId:role 依然 200),这里补上。
  46 +// 没有 `:` 前缀的是全局 agent(master/org-innovator 等,落 _global),本来就是全系统共享,
  47 +// 不属于任何公司,不做归属校验——跟 system/agents/ 本体全局共享是同一件事的两个层面。
  48 +// 内部自环调用(招聘/训练自动流水线)持有 req.user.internal,直接放行,不能对它做归属校验
  49 +// (自环调用没有"当前用户"这个概念,company.js/task-settle.js 那三处会主动打到任意公司名下)。
  50 +router.param('agentId', (req, res, next, agentId) => {
  51 + if (req.user?.internal) return next();
  52 + const colonIdx = agentId.indexOf(':');
  53 + if (colonIdx === -1) return next(); // 全局 agent,无公司维度
  54 + const companyId = agentId.slice(0, colonIdx);
  55 + if (!canAccessCompany(companyService.get(companyId), req.user)) {
  56 + return res.status(404).json({ error: `Agent ${agentId} 不存在` });
  57 + }
  58 + next();
  59 +});
  60 +
41 61 // ── 自唤醒回调:端口取实际监听端口,测试环境整体短路 ────────────────────
42 62 //
43 63 // 曾经这六处各自写死 `hostname:'localhost', port: systemConfig.port`(3010)。
... ...
server/tests/unit/agent-chat-company-gate.test.js 0 → 100644
  1 +/**
  2 + * agent-chat-company-gate.test.js —— 38 号文档 Phase 3
  3 + *
  4 + * agent-chat.js(/api/agent/*)一直靠 :agentId 里的命名空间前缀("{companyId}:{role}",
  5 + * locator.companyStateDir 解析)区分会话属于哪家公司,但 Phase 1/2 都没顾上这个文件的
  6 + * 归属校验——只要求登录,没查公司归不归当前用户。真实 curl 验证过是可以跨租户读会话的
  7 + * (GET .../conversations 对不是自己公司的 companyId:role 依然 200)。
  8 + *
  9 + * 补的是 router.param('agentId', ...):这里直接拿 Express 暴露的 `router.params.agentId`
  10 + * 数组单元测(不发真实 HTTP、不起 server),比照 company.js 那道 companyId 收口的判据
  11 + * canAccessCompany,行为应该完全对称。
  12 + */
  13 +process.env.LEMONBOSS_CLAUDE_MODE = 'mock'; // 必须在 require 路由之前
  14 +
  15 +import { describe, it, expect, beforeAll, afterAll } from 'vitest';
  16 +import path from 'path';
  17 +
  18 +const REPO_ROOT = path.resolve(__dirname, '../../..');
  19 +const { ensureTestCompany, removeTestCompany } = require('../helpers/ensure-test-company');
  20 +
  21 +const agentChatRouter = require('../../routes/agent-chat.js');
  22 +const paramHandler = agentChatRouter.params.agentId[0];
  23 +
  24 +const OWNER = { id: 9001, role: 'user' };
  25 +const OTHER = { id: 9002, role: 'user' };
  26 +const ADMIN = { id: 9003, role: 'admin' };
  27 +const INTERNAL = { id: 0, role: 'system', internal: true };
  28 +const CID = 'gate-test-co';
  29 +
  30 +function runParam(agentId, user) {
  31 + return new Promise((resolve) => {
  32 + const req = { user };
  33 + const res = {
  34 + _status: null,
  35 + status(code) { this._status = code; return this; },
  36 + json(body) { resolve({ status: this._status, body }); },
  37 + };
  38 + const next = (err) => resolve({ status: null, next: err || true });
  39 + paramHandler(req, res, next, agentId);
  40 + });
  41 +}
  42 +
  43 +describe('agent-chat.js 的 :agentId 归属校验(38 号文档 Phase 3)', () => {
  44 + beforeAll(() => {
  45 + ensureTestCompany(CID, { ownerId: OWNER.id });
  46 + });
  47 +
  48 + afterAll(() => {
  49 + removeTestCompany(CID);
  50 + });
  51 +
  52 + it('公司自己的用户能过(next 被调用,不是 404)', async () => {
  53 + const r = await runParam(`${CID}:some-role`, OWNER);
  54 + expect(r.next).toBe(true);
  55 + });
  56 +
  57 + it('别的登录用户访问 → 404(不暴露这个公司/agent 存在)', async () => {
  58 + const r = await runParam(`${CID}:some-role`, OTHER);
  59 + expect(r.status).toBe(404);
  60 + expect(r.body.error).toMatch(/不存在/);
  61 + });
  62 +
  63 + it('admin 不受归属限制', async () => {
  64 + const r = await runParam(`${CID}:some-role`, ADMIN);
  65 + expect(r.next).toBe(true);
  66 + });
  67 +
  68 + it('公司不存在 → 404(不是 next 放行)', async () => {
  69 + const r = await runParam('no-such-company:role', OWNER);
  70 + expect(r.status).toBe(404);
  71 + });
  72 +
  73 + it('没有 companyId 前缀的全局 agent(如 master)不做归属校验,任何登录用户都放行', async () => {
  74 + const r = await runParam('master', OTHER);
  75 + expect(r.next).toBe(true);
  76 + });
  77 +
  78 + it('内部自环调用(req.user.internal)直接放行,不查归属', async () => {
  79 + const r = await runParam(`${CID}:some-role`, INTERNAL);
  80 + expect(r.next).toBe(true);
  81 + const r2 = await runParam('no-such-company:role', INTERNAL);
  82 + expect(r2.next).toBe(true);
  83 + });
  84 +});
... ...
server/tests/unit/agent-chat-tenant-isolation.test.js
... ... @@ -98,6 +98,11 @@ describe(&#39;agent-chat · 公叿®µä¸èƒ½ä¸¢ï¼ˆtodo/plan è½ç‚¹ã€æ–‡ä»¶å†™å…¥æ ¹ã
98 98  
99 99 beforeAll(async () => {
100 100 for (const a of [CEO, BOSS]) seedBody(a);
  101 + // 38 å·æ–‡æ¡£ Phase 3:agent-chat.js çš„ :agentId 现在会按 "companyId:role" å‰ç¼€å查公å¸
  102 + // æ˜¯ä¸æ˜¯çœŸå®žå­˜åœ¨ã€å½“å‰ç”¨æˆ·ç¢°ä¸ç¢°å¾—到——ALPHA/BETA åªæ˜¯è¿™ä»½æµ‹è¯•里的字符串标签,
  103 + // 从没真的建过公å¸ï¼Œä¸è¡¥çš„è¯è¯·æ±‚会在归属校验这一关就被 404 挡下,走ä¸åˆ°æœ¬æ–‡ä»¶çœŸæ­£
  104 + // è¦æµ‹çš„"公叿®µæœ‰æ²¡æœ‰ä¸¢"è¿™å‡ æ¡æ–­è¨€ã€‚
  105 + for (const cid of [ALPHA, BETA]) require_('../helpers/ensure-test-company').ensureTestCompany(cid);
101 106  
102 107 const app = express();
103 108 app.use(express.json());
... ...