Commit b7b50961f051a486cfc31c98f95810d9aaa4f863

Authored by yibo
1 parent 6f48c194

feat(auth): Phase 1 —— 全站要求登录(38 号文档)

给此前完全零防护的 company.js/okr.js/tasks.js/devloop.js/graph-run.js(两个挂载点)/
arena.js/config.js/evolution.js/whatsapp-auth.js 挂 requireAuth;dream.js 挂 requireAdmin
(系统级经验汇流视图,只有 admin 能看)。open-api.js(自己一套 OPEN_API_KEYS)/auth.js
自身/health 端点按计划排除。这一步只挡"没登录",公司归属过滤留到 Phase 2。

agent-chat.js 不能直接挂 requireAuth:company.js(自动建公司/招聘后自动训练)、
task-settle.js(goal 汇报回调)三处服务端自环调用会 fetch 自己的 /api/agent/* 端点,
拿不到任何用户 cookie,直接挂锁会打断这几条核心自动化链路。新增 requireAuthOrInternal +
jwt-auth.js 的进程内 INTERNAL_TOKEN(启动时随机生成,只活在内存里):自环调用带上
x-internal-token 头放行,其余情况仍要求真实登录;三处调用点同步改造。

17 个既有测试文件、67+ 处用例直接拿 supertest 打这些路由,从没设过登录 cookie(测的是
业务逻辑不是鉴权)。没有逐个补登录态,改为 requireAuth/requireAdmin 在 NODE_ENV=test
时默认放行,除非显式 FORCE_REAL_AUTH=1;新增 route-auth-gate.test.js 在放行关闭状态下
做结构检查(router.stack 里中间件是否真的排在所有路由前面),确保"锁真的挂上了"这件事
有测试兜底,不被业务测试的默认放行悄悄掩盖。

验证:真实 curl 走过全部路由(无 cookie 401、admin 登录全通、非 admin 能看公司但
dream.js 403);手工删过一行 requireAuth 确认守卫测试真的会红;全量 vitest 1427 passed,
另 3 个失败与本次改动无关(stash 验证过改前改后一致,系仓库并发工作遗留)。

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 已完成(2026-08-25),Phase 1-3 未开工**。远程部署前置需求:登录鉴权 + 公司级多租户隔离。现状 `auth.js`/`jwt-auth.js`(注册/登录/JWT-cookie/OAuth)+ WebSocket 鉴权都已可用,真正缺的是①把已有的锁挂到目前零防护的 `company.js`/`agent-chat.js`/`okr.js`/`tasks.js`/`devloop.js`/`graph-run.js`(含双挂载点)等路由上、②`company.json` 从 0 新增 `ownerId` 归属概念(无 SQL 表,纯文件)。已拍板:存量公司迁移统一划给种子账号 `admin`/`hex1234`(不强制改密);OKR 跟随 company 隔离(含独立的遗留 `/api/okr` 挂载点);`arena.js`/`evolution.js` 核实为疑似废弃代码(前端零引用、5 个月未改动),只挂登录不投入隔离;`dream.js` 系统级,挂 `requireAdmin`。**Phase 0**:种子脚本 + 登录页 + 路由守卫 + 401 拦截器 + 退出登录,真实浏览器走通(含顺手修复的 `MasterChatDrawer` 登录页泄漏)。Phase 1(全站要求登录)→ Phase 2(`router.param('companyId')` 归属校验,最大工作量)→ Phase 3(聊天隔离验证+前端收尾)待开工 |
  52 +| [38-auth-multi-tenant-isolation.md](./todo/system-design/38-auth-multi-tenant-isolation.md) | 📝 **Phase 0/1 已完成(2026-08-25),Phase 2-3 未开工**。Phase 1 给 9 个此前零防护的路由文件挂 `requireAuth`/`requireAdmin`;`agent-chat.js` 用新写的 `requireAuthOrInternal`(进程内 `INTERNAL_TOKEN`)——因为发现三处服务端自环调用(招聘/训练自动流水线、goal 汇报回调)拿不到用户 cookie,直接挂锁会打断核心自动化链路。17 个既有测试文件的 67+ 处用例从没设过登录态,改为测试环境默认放行 + 新增 `route-auth-gate.test.js` 结构化守卫测试专盯"锁真的挂上了没有"。远程部署前置需求:登录鉴权 + 公司级多租户隔离。现状 `auth.js`/`jwt-auth.js`(注册/登录/JWT-cookie/OAuth)+ WebSocket 鉴权都已可用,真正缺的是①把已有的锁挂到目前零防护的 `company.js`/`agent-chat.js`/`okr.js`/`tasks.js`/`devloop.js`/`graph-run.js`(含双挂载点)等路由上、②`company.json` 从 0 新增 `ownerId` 归属概念(无 SQL 表,纯文件)。已拍板:存量公司迁移统一划给种子账号 `admin`/`hex1234`(不强制改密);OKR 跟随 company 隔离(含独立的遗留 `/api/okr` 挂载点);`arena.js`/`evolution.js` 核实为疑似废弃代码(前端零引用、5 个月未改动),只挂登录不投入隔离;`dream.js` 系统级,挂 `requireAdmin`。**Phase 0**:种子脚本 + 登录页 + 路由守卫 + 401 拦截器 + 退出登录,真实浏览器走通(含顺手修复的 `MasterChatDrawer` 登录页泄漏)。Phase 1(全站要求登录)→ Phase 2(`router.param('companyId')` 归属校验,最大工作量)→ Phase 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
... ... @@ -96,10 +96,12 @@ router.param(&#39;companyId&#39;, (req, res, next, companyId) =&gt; {
96 96 - 顺手修了一处过程中发现的泄漏:`App.vue` 原来无条件渲染 `MasterChatDrawer`,未登录时登录页右上角也会露出这个悬浮入口——改成 `v-if="showChrome"`(`route.name !== 'Login' && !!user`),登录页干净了。
97 97 - **验收(真实浏览器走过一遍,非仅测试通过)**:未登录访问 `/company` 等任意页面 → 跳 `/login?redirect=/company`;`admin`/`hex1234` 登录成功 → 跳回原目标页,导航栏显示 `admin` + 退出按钮;直接刷新/新开页面访问受保护路由(非 SPA 内跳转)同样正确放行;点退出 → 清 cookie、跳回 `/login`;`npm run build` 产物构建通过,无报错。因为业务路由还没加锁,登录后依然能看到现状全部数据(等价于"登录墙搭好了,屋里还没上锁"),符合这一阶段的验收边界。
98 98  
99   -### Phase 1 — 全站要求登录(不做归属过滤)
100   -- 给 `agent-chat.js`/`company.js`/`okr.js`/`tasks.js`/`devloop.js`/`graph-run.js`(两个挂载点)/`arena.js`/`config.js`/`evolution.js`/`whatsapp-auth.js` 挂 `requireAuth`,显式排除 `open-api.js`、`auth.js` 自身、`/api/health`。
101   -- **`dream.js` 挂 `requireAdmin` 而不是 `requireAuth`**(已决策,见 §五)——它是系统级的经验汇流视图,只有 admin 能看,不是"登录即可看"。
102   -- **验收**:未登录状态下所有 API 都 401;已登录状态下行为跟 Phase 0 完全一致(任何登录用户依然能看到全部公司——归属过滤还没上);非 admin 登录用户访问 `dream.js` 下的接口 403。这一步的回归测试很简单:跑现有测试套件 + 确认没有新增的越权点。
  99 +### Phase 1 — 全站要求登录(不做归属过滤)✅ 已完成(2026-08-25)
  100 +- 给 `company.js`/`okr.js`/`tasks.js`/`devloop.js`/`graph-run.js`(两个挂载点:`router` + `companyRouter`)/`arena.js`/`config.js`/`evolution.js`/`whatsapp-auth.js` 挂 `requireAuth`,显式排除 `open-api.js`(自己一套 `OPEN_API_KEYS`)、`auth.js` 自身、`/api/health`。
  101 +- **`dream.js` 挂 `requireAdmin`**(已决策,见 §五)——系统级经验汇流视图,只有 admin 能看。
  102 +- **`agent-chat.js` 挂的是新写的 `requireAuthOrInternal`,不是普通 `requireAuth`**——实现过程中发现的真实风险:`company.js:82/506`(招聘/训练自动流水线)、`task-settle.js:452`(goal 汇报回调)三处**服务端自环调用**会 `fetch('http://127.0.0.1:.../api/agent/...')` 打这个文件,这三处根本拿不到任何用户 cookie,直接挂 `requireAuth` 会把公司自动创建、招聘训练、任务汇报这几条核心自动化链路全部打断成 401。解法:`jwt-auth.js` 新增进程内 `INTERNAL_TOKEN`(启动时随机生成、只活在内存里,不落盘不读环境变量——调用方和校验方永远在同一个 Node 进程),`requireAuthOrInternal` 先认这个头(`x-internal-token`),认不出再退回普通 `requireAuth`;三处自环调用改成带上这个头。
  103 +- **测试套件的连带问题**:仓库里已有 17 个测试文件、67+ 处用例直接拿 supertest 打这几个路由,从没设过登录 cookie(测的是业务逻辑,不是鉴权)——挂完锁后这批测试集体从 200/201 变 401。没有逐个文件补登录态(会引入 bcrypt 开销拖慢整个套件,且这些用例本来就不该关心鉴权),改为:`jwt-auth.js` 的 `requireAuth`/`requireAdmin` 在 `NODE_ENV==='test'`(vitest 自动设置)时默认放行,除非显式设了 `FORCE_REAL_AUTH=1`;新增 `server/tests/unit/route-auth-gate.test.js` 专门在 `FORCE_REAL_AUTH=1` 下做结构检查(`router.stack` 里有没有那个中间件函数本身、且排在所有具体路由前面),不发真实 HTTP、不碰 DB,确保"锁真的挂上了"这件事有测试兜底,而不是被 67 处业务测试的默认放行悄悄掩盖。
  104 +- **验收**:真实 `curl` 走过一遍——无 cookie 访问 `company.js`/`tasks.js`/`devloop.js`/`okr.js`/`graph-run.js`/`dream.js` 全部 401;`admin`/`hex1234` 登录后全部可访问;新注册的非 admin 用户能访问 `company.js` 等(Phase 1 还没做归属过滤,符合预期),但访问 `dream.js` 得到 403;`open-api.js`/`auth.js` 未受影响(前者仍是自己的 API Key 报错文案,后者登录本身不能被锁死)。手工临时删掉 `tasks.js` 的 `router.use(requireAuth)` 验证过守卫测试真的会红。全量 `npx vitest run`:1427 passed(含新增 13 条守卫测试),3 个失败与本次改动无关(`git stash` 验证过,改动前后同样失败:`agent-md-refs`/`graduation-criteria-reality`/`graph-engine-extend-budget`,是仓库里并发的其它工作留下的,非本次引入)。
103 105  
104 106 ### Phase 2 — 公司归属隔离(这次真正的硬骨头)
105 107 - `company.json` schema 加 `ownerId`;`create()` 接收 owner 参数(从 `req.user.id` 传入);`list()` 按 `req.user` 过滤(admin 角色不过滤)。
... ...
server/middleware/jwt-auth.js
... ... @@ -103,9 +103,30 @@ function extractUser(req) {
103 103 }
104 104  
105 105 /**
  106 + * 38 号文档 Phase 1 落地时发现:仓库里已有 17 个测试文件直接拿 supertest 打
  107 + * company.js/tasks.js/devloop.js/... 这些路由,从没设过登录 cookie——这些用例测的是
  108 + * 业务逻辑(队列/归档/CRUD),不是鉴权本身,一个个改成先登录再打显然是本末倒置
  109 + * (还会因为 bcrypt 而拖慢整个套件)。
  110 + *
  111 + * 所以测试环境默认让 requireAuth/requireAdmin 直接放行(`NODE_ENV=test` 是 vitest
  112 + * 自动设的,不需要每个测试文件单独配置),补一条真正验证"锁真的挂上了"的守卫测试
  113 + * (`server/tests/unit/route-auth-gate.test.js`),跑之前设 `FORCE_REAL_AUTH=1`
  114 + * 关掉这里的放行,其余测试不受影响。**不是每次调用都读一遍 env**:中间件内联判断,
  115 + * 保证同一个测试文件里前后设置的 env 值能立刻生效(不被模块加载时的一次性求值缓存)。
  116 + */
  117 +function isTestAuthBypass() {
  118 + return process.env.NODE_ENV === 'test' && process.env.FORCE_REAL_AUTH !== '1';
  119 +}
  120 +const TEST_BYPASS_USER = { id: -1, role: 'admin', display_name: 'test-bypass', email: 'test-bypass@local' };
  121 +
  122 +/**
106 123 * Middleware: require authentication (401 if not logged in)
107 124 */
108 125 function requireAuth(req, res, next) {
  126 + if (isTestAuthBypass()) {
  127 + req.user = TEST_BYPASS_USER;
  128 + return next();
  129 + }
109 130 const user = extractUser(req);
110 131 if (!user) {
111 132 return res.status(401).json({
... ... @@ -129,6 +150,10 @@ function optionalAuth(req, res, next) {
129 150 * Middleware: require admin role (403 if not admin)
130 151 */
131 152 function requireAdmin(req, res, next) {
  153 + if (isTestAuthBypass()) {
  154 + req.user = TEST_BYPASS_USER;
  155 + return next();
  156 + }
132 157 const user = extractUser(req);
133 158 if (!user) {
134 159 return res.status(401).json({ error: 'Unauthorized' });
... ... @@ -154,14 +179,41 @@ function extractUserFromToken(token) {
154 179 }
155 180 }
156 181  
  182 +/**
  183 + * 进程内令牌,只用于同一台机器上的服务端自环调用(agent-chat.js 挂 requireAuth 之后,
  184 + * company.js:82/506、task-settle.js:452 这三处 `fetch('http://127.0.0.1:.../api/agent/...')`
  185 + * 拿不到任何用户 cookie,会被 401 挡住——38 号文档 Phase 1)。
  186 + *
  187 + * 只在内存里生成、不落盘、不读环境变量:调用方和校验方永远在同一个 Node 进程里
  188 + * (自环 HTTP 只是"本进程调本进程",不是真的跨进程/跨机器),进程重启两边一起换新值,
  189 + * 不存在"改了忘同步"的问题,也不需要额外的密钥管理。
  190 + */
  191 +const INTERNAL_TOKEN = require('crypto').randomBytes(24).toString('hex');
  192 +const INTERNAL_TOKEN_HEADER = 'x-internal-token';
  193 +
  194 +/**
  195 + * Middleware: 放行本进程自环调用(带对的 INTERNAL_TOKEN),其余情况退回 requireAuth。
  196 + * 自环调用不代表任何真人用户,`req.user` 置一个可识别的系统身份,供下游判断。
  197 + */
  198 +function requireAuthOrInternal(req, res, next) {
  199 + if (req.headers[INTERNAL_TOKEN_HEADER] === INTERNAL_TOKEN) {
  200 + req.user = { id: 0, role: 'system', display_name: 'system', internal: true };
  201 + return next();
  202 + }
  203 + return requireAuth(req, res, next);
  204 +}
  205 +
157 206 module.exports = {
158 207 signAndSetCookie,
159 208 clearCookie,
160 209 requireAuth,
161 210 requireAdmin,
  211 + requireAuthOrInternal,
162 212 optionalAuth,
163 213 extractUser,
164 214 extractUserFromToken,
  215 + INTERNAL_TOKEN,
  216 + INTERNAL_TOKEN_HEADER,
165 217 JWT_SECRET,
166 218 COOKIE_NAME,
167 219 };
... ...
server/routes/agent-chat.js
... ... @@ -29,8 +29,15 @@ const { listCompanyAgents } = require(&#39;../services/company-service&#39;);
29 29 // 交付物归宿只有一份推导(28 §4.4),公司环境上下文与执行链路的运行时坐标共用它
30 30 const { companyWorkspaceDir } = require('../src/core/prompt-builder');
31 31 const systemConfig = require('../config');
  32 +const { requireAuthOrInternal } = require('../middleware/jwt-auth');
32 33 const router = express.Router();
33 34  
  35 +// 38 号文档 Phase 1:全站要求登录,但这个文件同时承接三处服务端自环调用
  36 +// (company.js:82/506、task-settle.js:452 的 fetch('.../api/agent/...'))——
  37 +// 那几处拿不到用户 cookie,用 requireAuthOrInternal 而不是 requireAuth,
  38 +// 放行带对内部令牌的自环请求,其余情况仍然要求真实登录。
  39 +router.use(requireAuthOrInternal);
  40 +
34 41 // ── 自唤醒回调:端口取实际监听端口,测试环境整体短路 ────────────────────
35 42 //
36 43 // 曾经这六处各自写死 `hostname:'localhost', port: systemConfig.port`(3010)。
... ...
server/routes/arena.js
... ... @@ -3,7 +3,12 @@
3 3 */
4 4 const express = require('express');
5 5 const router = express.Router();
  6 +const { requireAuth } = require('../middleware/jwt-auth');
6 7 const taxonomy = require('../config/taxonomy');
  8 +
  9 +// 38 号文档 Phase 1:疑似废弃代码(前端零引用、~5 个月未改动),最省事处理——只挂登录,
  10 +// 不深挖是否该删(不属于这次鉴权改造的范围)。
  11 +router.use(requireAuth);
7 12 const { ROLES, SCENES, PLATFORMS, autoTag, migrateOldScene } = taxonomy;
8 13 const config = require('../config');
9 14 const skillDb = require('../services/skill-db');
... ...
server/routes/company.js
... ... @@ -8,6 +8,12 @@ const express = require(&#39;express&#39;);
8 8 const path = require('path');
9 9 const fs = require('fs');
10 10 const router = express.Router();
  11 +const { requireAuth, INTERNAL_TOKEN, INTERNAL_TOKEN_HEADER } = require('../middleware/jwt-auth');
  12 +
  13 +// 38 号文档 Phase 1:全站要求登录。公司归属过滤(谁能看到哪家公司)留到 Phase 2 的
  14 +// router.param('companyId', ...) 统一收口,这一步只挡"没登录"。
  15 +router.use(requireAuth);
  16 +
11 17 const { CompanyService, listCompanyAgents, buildAgentDefinitionBrief } = require('../services/company-service');
12 18 const { ScheduleService } = require('../services/schedule-service');
13 19 const orgGraphService = require('../services/org-graph-service');
... ... @@ -75,7 +81,9 @@ function triggerOrgGraphPlanning(company, tag = &#39;OrgGraph&#39;) {
75 81  
76 82 fetch(`http://127.0.0.1:${PORT}/api/agent/${encodeURIComponent(namespacedId)}/chat`, {
77 83 method: 'POST',
78   - headers: { 'Content-Type': 'application/json' },
  84 + // 38 号文档 Phase 1:agent-chat.js 挂了 requireAuthOrInternal,自环调用要带这个头
  85 + // 才不会被 401 挡住(拿不到任何用户 cookie)。
  86 + headers: { 'Content-Type': 'application/json', [INTERNAL_TOKEN_HEADER]: INTERNAL_TOKEN },
79 87 body: JSON.stringify({
80 88 message: `公司 ${id}(${name})已创建。\n公司目标(company-goal):${goal || '(未填写)'}\n公司描述:${description || '(未填写)'}\n请按 orggraph-planning 工作流规划并建立这家公司的 workflow:读 system/org-graph-templates/ 候选骨架 + 可招角色清单,产出执行流图后调用 POST /api/company/${id}/org-graph 落库,再按拓扑序完成招聘。`,
81 89 conversationId: `orggraph-bootstrap-${id}-${Date.now().toString(36)}`,
... ... @@ -499,7 +507,7 @@ router.post(&#39;/:companyId/hire&#39;, async (req, res) =&gt; {
499 507 setTimeout(() => {
500 508 fetch(`http://127.0.0.1:${PORT}/api/agent/org-innovator/chat`, {
501 509 method: 'POST',
502   - headers: { 'Content-Type': 'application/json' },
  510 + headers: { 'Content-Type': 'application/json', [INTERNAL_TOKEN_HEADER]: INTERNAL_TOKEN },
503 511 body: JSON.stringify({
504 512 message: `公司 ${req.params.companyId} 的员工 ${role} 骨架和基础技能已注入完毕。请你使用 FDE pipeline 对该 Agent ${namespacedId} (特别是它的技能)进行评测和进化训练,直到 Benchmark 达标 (Score ≥ 95)。训练过程包括 Hunt(验证数据), Digest, Evolver 等。请立即开始评估和训练,并保持向我汇报。`,
505 513 conversationId: trainConversationId
... ...
server/routes/config.js
... ... @@ -8,6 +8,11 @@ const router = express.Router();
8 8 const fs = require('fs');
9 9 const path = require('path');
10 10 const os = require('os');
  11 +const { requireAuth } = require('../middleware/jwt-auth');
  12 +
  13 +// 38 号文档 Phase 1:现状未深入调研是否有公司维度数据,先按最保守的方式处理——只挂登录,
  14 +// 后续如发现内部数据需要按公司过滤,再提级到 Phase 2 的归属校验。
  15 +router.use(requireAuth);
11 16  
12 17 const ROOT = path.resolve(__dirname, '../..');
13 18  
... ...
server/routes/devloop.js
... ... @@ -17,6 +17,10 @@
17 17  
18 18 const express = require('express');
19 19 const router = express.Router();
  20 +const { requireAuth } = require('../middleware/jwt-auth');
  21 +
  22 +// 38 号文档 Phase 1:全站要求登录,公司归属过滤(经 :pid 反查 company_id)留到 Phase 2。
  23 +router.use(requireAuth);
20 24  
21 25 const loopController = require('../src/core/devloop/loop-controller');
22 26 const pathResolver = require('../services/devloop/path-resolver');
... ...
server/routes/dream.js
... ... @@ -7,8 +7,13 @@
7 7 */
8 8 const express = require('express');
9 9 const router = express.Router();
  10 +const { requireAdmin } = require('../middleware/jwt-auth');
10 11 const dream = require('../services/dream-service');
11 12  
  13 +// 38 号文档 Phase 1:Dream 是系统级的跨公司经验汇流视图,只有 admin 能看,
  14 +// 不是"登录即可看"(普通用户不该看到别的公司提炼出的经验)。
  15 +router.use(requireAdmin);
  16 +
12 17 // dreamSweep 串行跑几十个本体、每个可能起两次 LLM 调用,不能塞进一次 HTTP 请求里等。
13 18 // 用最小的内存态跟踪「在跑没在跑」+「上一轮结果」,够控制台轮询用。
14 19 let sweepState = { sweeping: false, startedAt: null, finishedAt: null, results: null, error: null };
... ...
server/routes/evolution.js
... ... @@ -4,6 +4,11 @@
4 4 */
5 5 const express = require('express');
6 6 const router = express.Router();
  7 +const { requireAuth } = require('../middleware/jwt-auth');
  8 +
  9 +// 38 号文档 Phase 1:疑似废弃代码(前端零引用、~5 个月未改动),最省事处理——只挂登录,
  10 +// 不深挖是否该删(不属于这次鉴权改造的范围)。
  11 +router.use(requireAuth);
7 12 const {
8 13 evolutionQueries, forageSkills, RunController,
9 14 crossoverGeneration, selectGeneration, evaluateRunFitness,
... ...
server/routes/graph-run.js
... ... @@ -41,10 +41,16 @@ const {
41 41 RunSlotBusyError,
42 42 } = require('../services/work-graph-db');
43 43 const { companyRootDir } = require('../services/company-paths');
  44 +const { requireAuth } = require('../middleware/jwt-auth');
44 45  
45 46 const router = express.Router();
46 47 const companyRouter = express.Router();
47 48  
  49 +// 38 号文档 Phase 1:全站要求登录(两个挂载点都要挂,§六风险 2 —— 这个文件以两种方式
  50 +// 挂载到 index.js,改的时候容易漏一个)。公司归属过滤留到 Phase 2。
  51 +router.use(requireAuth);
  52 +companyRouter.use(requireAuth);
  53 +
48 54 // ────────────────────────────────────────────────────────────────
49 55 // 小工具
50 56 // ────────────────────────────────────────────────────────────────
... ...
server/routes/okr.js
... ... @@ -16,11 +16,16 @@
16 16 */
17 17  
18 18 const { Router } = require('express');
  19 +const { requireAuth } = require('../middleware/jwt-auth');
19 20 const { OkrService } = require('../services/okr-service');
20 21  
21 22 const router = Router();
22 23 const okr = new OkrService();
23 24  
  25 +// 38 号文档 Phase 1:全站要求登录。这是独立于 company.js 的遗留全局 OKR 挂载点
  26 +// (目前无公司维度),公司归属改造留到 Phase 2(§四 Phase 2 / §五.1)。
  27 +router.use(requireAuth);
  28 +
24 29 // ── 列出所有目标 ──
25 30 router.get('/', (req, res) => {
26 31 res.json(okr.list());
... ...
server/routes/tasks.js
1 1 const express = require('express');
2 2 const router = express.Router();
  3 +const { requireAuth } = require('../middleware/jwt-auth');
3 4 const { TaskService } = require('../services/task-service');
4 5  
  6 +// 38 号文档 Phase 1:全站要求登录,公司归属过滤留到 Phase 2。
  7 +router.use(requireAuth);
  8 +
5 9 const taskService = new TaskService();
6 10  
7 11 // ── Task List 缓存 (10秒 TTL) ──
... ...
server/routes/whatsapp-auth.js
... ... @@ -9,6 +9,11 @@ const express = require(&#39;express&#39;);
9 9 const path = require('path');
10 10 const fs = require('fs');
11 11 const router = express.Router();
  12 +const { requireAuth } = require('../middleware/jwt-auth');
  13 +
  14 +// 38 号文档 Phase 1:这是本机管理员在浏览器里扫码连接 WhatsApp bot 的操作面板,
  15 +// 不是外部 WhatsApp 服务器回调的 webhook,挂登录门槛不会挡外部回调。
  16 +router.use(requireAuth);
12 17  
13 18 const AUTH_DIR = path.resolve(__dirname, '../../skills/whatsapp-bot/store/auth');
14 19  
... ...
server/services/task-settle.js
... ... @@ -449,9 +449,12 @@ async function reportGoalOutcome(goal, status, tasks) {
449 449 // 原来这里是**发射后不管**:`.then` 只对 !ok 打一行 error,函数立刻 return。
450 450 // 进程在这几十毫秒到几秒里被杀、或端点回 5xx,上级就永远不会被叫醒,而库里这条 goal
451 451 // 长得和一次完美收敛一模一样。现在等到结果再决定盖不盖「已汇报」的戳。
  452 + // 38 号文档 Phase 1:agent-chat.js 挂了 requireAuthOrInternal,自环调用要带这个头
  453 + // 才不会被 401 挡住(拿不到任何用户 cookie)。
  454 + const { INTERNAL_TOKEN, INTERNAL_TOKEN_HEADER } = require('../middleware/jwt-auth');
452 455 const ok = await fetch(`http://127.0.0.1:${PORT}/api/agent/task/report`, {
453 456 method: 'POST',
454   - headers: { 'Content-Type': 'application/json' },
  457 + headers: { 'Content-Type': 'application/json', [INTERNAL_TOKEN_HEADER]: INTERNAL_TOKEN },
455 458 body: JSON.stringify({
456 459 taskId: goal.id,
457 460 from,
... ...
server/tests/unit/route-auth-gate.test.js 0 → 100644
  1 +/**
  2 + * route-auth-gate.test.js —— 38 号文档 Phase 1 的守卫测试
  3 + *
  4 + * 只验证一件事:"这个 router 的 requireAuth/requireAdmin/requireAuthOrInternal 是不是真的
  5 + * 挂在最前面",不发真实 HTTP 请求、不碰 DB —— 结构检查:`router.stack` 里能不能找到那个
  6 + * 中间件函数本身(同一个函数引用,不是同名重写),且它排在所有具体路由(`layer.route` 有值
  7 + * 的层)前面。真正打 HTTP 会 401 由各业务测试文件顺带覆盖(jwt-auth.js 的
  8 + * FORCE_REAL_AUTH 默认在测试环境放行,这里显式打开验证真实拦截效果)。
  9 + *
  10 + * 这条测试存在的理由:67 处业务路由测试全靠 supertest 直打路由,从没设过登录 cookie——
  11 + * 如果只跑那些测试,`router.use(requireAuth)` 被误删也不会被任何测试发现(默认放行,
  12 + * 删不删都是 200)。这条测试专门盯"锁挂了没有"这一件事,跟业务逻辑测试互不重叠。
  13 + */
  14 +import { describe, it, expect, afterAll } from 'vitest';
  15 +
  16 +process.env.NODE_ENV = 'test';
  17 +process.env.FORCE_REAL_AUTH = '1'; // 关掉 jwt-auth.js 的测试环境默认放行
  18 +
  19 +const {
  20 + requireAuth,
  21 + requireAdmin,
  22 + requireAuthOrInternal,
  23 +} = require('../../middleware/jwt-auth');
  24 +
  25 +/** router.stack 里有没有这个中间件函数本身,且排在第一条具体路由之前 */
  26 +function gateIndex(router, middlewareFn) {
  27 + const stack = router.stack || [];
  28 + return stack.findIndex((layer) => layer.handle === middlewareFn);
  29 +}
  30 +function firstRouteIndex(router) {
  31 + const stack = router.stack || [];
  32 + return stack.findIndex((layer) => !!layer.route);
  33 +}
  34 +function expectGated(router, middlewareFn, label) {
  35 + const gateAt = gateIndex(router, middlewareFn);
  36 + const routeAt = firstRouteIndex(router);
  37 + expect(gateAt, `${label}: 没有挂上预期的鉴权中间件`).toBeGreaterThanOrEqual(0);
  38 + expect(gateAt, `${label}: 鉴权中间件排在了具体路由后面,前面那些路由没被拦到`)
  39 + .toBeLessThan(routeAt === -1 ? Infinity : routeAt);
  40 +}
  41 +
  42 +describe('route-auth-gate(38 号文档 Phase 1)', () => {
  43 + afterAll(() => {
  44 + delete process.env.FORCE_REAL_AUTH;
  45 + });
  46 +
  47 + it('company.js 挂了 requireAuth', () => {
  48 + expectGated(require('../../routes/company.js'), requireAuth, 'company.js');
  49 + });
  50 +
  51 + it('tasks.js 挂了 requireAuth', () => {
  52 + expectGated(require('../../routes/tasks.js'), requireAuth, 'tasks.js');
  53 + });
  54 +
  55 + it('devloop.js 挂了 requireAuth', () => {
  56 + expectGated(require('../../routes/devloop.js'), requireAuth, 'devloop.js');
  57 + });
  58 +
  59 + it('graph-run.js 两个挂载点都挂了 requireAuth(router + companyRouter)', () => {
  60 + const graphRun = require('../../routes/graph-run.js');
  61 + expectGated(graphRun, requireAuth, 'graph-run.js router');
  62 + expectGated(graphRun.companyRouter, requireAuth, 'graph-run.js companyRouter');
  63 + });
  64 +
  65 + it('okr.js 挂了 requireAuth', () => {
  66 + expectGated(require('../../routes/okr.js'), requireAuth, 'okr.js');
  67 + });
  68 +
  69 + it('arena.js 挂了 requireAuth', () => {
  70 + expectGated(require('../../routes/arena.js'), requireAuth, 'arena.js');
  71 + });
  72 +
  73 + it('config.js 挂了 requireAuth', () => {
  74 + expectGated(require('../../routes/config.js'), requireAuth, 'config.js');
  75 + });
  76 +
  77 + it('evolution.js 挂了 requireAuth', () => {
  78 + expectGated(require('../../routes/evolution.js'), requireAuth, 'evolution.js');
  79 + });
  80 +
  81 + it('whatsapp-auth.js 挂了 requireAuth', () => {
  82 + expectGated(require('../../routes/whatsapp-auth.js'), requireAuth, 'whatsapp-auth.js');
  83 + });
  84 +
  85 + it('dream.js 挂了 requireAdmin(系统级,只有 admin 能看,不是登录即可看)', () => {
  86 + expectGated(require('../../routes/dream.js'), requireAdmin, 'dream.js');
  87 + });
  88 +
  89 + it('agent-chat.js 挂了 requireAuthOrInternal(不是普通 requireAuth——三处服务端自环调用要放行)', () => {
  90 + expectGated(require('../../routes/agent-chat.js'), requireAuthOrInternal, 'agent-chat.js');
  91 + });
  92 +
  93 + it('open-api.js 不该挂 cookie 鉴权(它自己一套 OPEN_API_KEYS 体系,面向外部 Skill 市场调用方)', () => {
  94 + const openApi = require('../../routes/open-api.js');
  95 + expect(gateIndex(openApi, requireAuth)).toBe(-1);
  96 + expect(gateIndex(openApi, requireAuthOrInternal)).toBe(-1);
  97 + });
  98 +
  99 + it('auth.js 自身不挂全局鉴权(登录/注册端点必须对未登录用户可达)', () => {
  100 + const auth = require('../../routes/auth.js');
  101 + expect(gateIndex(auth, requireAuth)).toBe(-1);
  102 + });
  103 +});
... ...