Commit 9e586563f99a1994a4fec41d8562ec987c16ff39

Authored by yibo
1 parent 78c24226

fix(auth): bin/lemon.js 的 task report/dispatch 修复 401(38 号文档收尾)

commit b7b5096 给 agent-chat.js 挂 requireAuthOrInternal 后,只顾了「同进程内
require 拿 INTERNAL_TOKEN」这一种自环调用(company.js/task-settle.js),漏了
bin/lemon.js——它跑在 agent 的 claude CLI 子进程里,不共享内存,没法拿到这个
token,导致所有 agent 的 task report/dispatch 全部 401,没法自行汇报完成状态。

修法:jwt-auth.js 生成 INTERNAL_TOKEN 时顺带写进 process.env.LEMON_INTERNAL_TOKEN
——spawn 默认把 process.env 整份传给子进程,免费传给所有后代进程(claude CLI →
Bash 子进程 → bin/lemon.js)。bin/lemon.js 读到就带上 x-internal-token header。
顺带把硬编码的 API_BASE 端口改成可用 LEMON_API_BASE 覆盖,方便测试。

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
bin/lemon.js
... ... @@ -3,7 +3,7 @@
3 3 const { parseArgs } = require('util');
4 4 const http = require('http');
5 5  
6   -const API_BASE = 'http://localhost:3010/api';
  6 +const API_BASE = process.env.LEMON_API_BASE || 'http://localhost:3010/api';
7 7  
8 8 function request(method, path, data) {
9 9 return new Promise((resolve, reject) => {
... ... @@ -13,6 +13,12 @@ function request(method, path, data) {
13 13 headers['Content-Type'] = 'application/json';
14 14 headers['Content-Length'] = Buffer.byteLength(dataStr);
15 15 }
  16 + // agent-chat.js 挂了 requireAuthOrInternal(38 号文档),这个 CLI 跑在 agent 自己的
  17 + // claude CLI 子进程里,没有登录 cookie;LEMON_INTERNAL_TOKEN 是服务端进程 spawn 子进程时
  18 + // 顺带传下来的(server/middleware/jwt-auth.js),带上就能过 internal 校验。
  19 + if (process.env.LEMON_INTERNAL_TOKEN) {
  20 + headers['x-internal-token'] = process.env.LEMON_INTERNAL_TOKEN;
  21 + }
16 22 const req = http.request(API_BASE + path, {
17 23 method,
18 24 headers
... ...
server/middleware/jwt-auth.js
... ... @@ -184,12 +184,20 @@ function extractUserFromToken(token) {
184 184 * company.js:82/506、task-settle.js:452 这三处 `fetch('http://127.0.0.1:.../api/agent/...')`
185 185 * 拿不到任何用户 cookie,会被 401 挡住——38 号文档 Phase 1)。
186 186 *
187   - * 只在内存里生成、不落盘、不读环境变量:调用方和校验方永远在同一个 Node 进程里
188   - * (自环 HTTP 只是"本进程调本进程",不是真的跨进程/跨机器),进程重启两边一起换新值,
189   - * 不存在"改了忘同步"的问题,也不需要额外的密钥管理。
  187 + * 只在内存里生成、不落盘:调用方和校验方永远在同一台机器的进程树里,进程重启两边一起
  188 + * 换新值,不存在"改了忘同步"的问题,也不需要额外的密钥管理。
  189 + *
  190 + * 写进 process.env 是为了让 `bin/lemon.js` 也能用上这个 token——它是 agent 用来
  191 + * `task report`/`task dispatch` 自我汇报的 CLI,跑在 claude CLI 子进程(乃至更深的 Bash
  192 + * 子进程)里,跟本进程不共享内存,没法直接 import 这个常量;但 spawn 默认把 process.env
  193 + * 整份传给子进程(ClaudeCliProtocol.js/ProcessLauncher.js 都是 `{...process.env}`),
  194 + * 写这一行就等于免费传给了所有后代进程,不用改任何 launcher 代码。之前只顾了「同进程内
  195 + * require 拿常量」这一种自环调用,漏了「agent 子进程里跑 CLI 工具」这一种,就是这次
  196 + * bug 的成因(commit b7b5096 之后 bin/lemon.js 的 task report/dispatch 全部 401)。
190 197 */
191 198 const INTERNAL_TOKEN = require('crypto').randomBytes(24).toString('hex');
192 199 const INTERNAL_TOKEN_HEADER = 'x-internal-token';
  200 +process.env.LEMON_INTERNAL_TOKEN = INTERNAL_TOKEN;
193 201  
194 202 /**
195 203 * Middleware: 放行本进程自环调用(带对的 INTERNAL_TOKEN),其余情况退回 requireAuth。
... ...
server/tests/unit/bin-lemon-internal-token.test.js 0 → 100644
  1 +/**
  2 + * bin/lemon.js — commit b7b5096(全站鉴权)之后,agent-chat.js 挂了 requireAuthOrInternal,
  3 + * 这个 CLI 跑在 agent 自己的 claude CLI 子进程里,没有登录 cookie,只能靠父进程通过
  4 + * process.env 传下来的 LEMON_INTERNAL_TOKEN 过 internal 校验(server/middleware/jwt-auth.js)。
  5 + *
  6 + * 用一个假的 HTTP 服务器代替真实后端,只验证 bin/lemon.js 发请求时带没带这个 header。
  7 + */
  8 +import { describe, it, expect, beforeAll, afterAll } from 'vitest';
  9 +import http from 'http';
  10 +import { execFile } from 'child_process';
  11 +import path from 'path';
  12 +
  13 +const LEMON_BIN = path.resolve(__dirname, '../../../bin/lemon.js');
  14 +
  15 +let server, port, lastHeaders;
  16 +
  17 +function runLemon(args, env) {
  18 + return new Promise((resolve) => {
  19 + execFile('node', [LEMON_BIN, ...args], { env }, () => resolve());
  20 + });
  21 +}
  22 +
  23 +describe('bin/lemon.js — LEMON_INTERNAL_TOKEN 透传', () => {
  24 + beforeAll(async () => {
  25 + server = http.createServer((req, res) => {
  26 + lastHeaders = req.headers;
  27 + res.writeHead(200, { 'Content-Type': 'application/json' });
  28 + res.end(JSON.stringify({ ok: true }));
  29 + });
  30 + // 用随机端口,避免跟本机真实跑着的 3010 dev server 撞车(LEMON_API_BASE 让 bin/lemon.js 指过来)
  31 + await new Promise((resolve) => server.listen(0, resolve));
  32 + port = server.address().port;
  33 + });
  34 +
  35 + afterAll(() => new Promise((resolve) => server.close(resolve)));
  36 +
  37 + it('设了 LEMON_INTERNAL_TOKEN 时,请求头带上 x-internal-token', async () => {
  38 + await runLemon(
  39 + ['task', 'report', '--taskId', 't1', '--from', 'a', '--to', 'b'],
  40 + { ...process.env, LEMON_API_BASE: `http://localhost:${port}/api`, LEMON_INTERNAL_TOKEN: 'test-token-abc' }
  41 + );
  42 + expect(lastHeaders['x-internal-token']).toBe('test-token-abc');
  43 + });
  44 +
  45 + it('没设 LEMON_INTERNAL_TOKEN 时,不带这个 header(不伪造身份)', async () => {
  46 + const env = { ...process.env, LEMON_API_BASE: `http://localhost:${port}/api` };
  47 + delete env.LEMON_INTERNAL_TOKEN;
  48 + await runLemon(['task', 'report', '--taskId', 't1', '--from', 'a', '--to', 'b'], env);
  49 + expect(lastHeaders['x-internal-token']).toBeUndefined();
  50 + });
  51 +});
... ...
server/tests/unit/jwt-auth.test.js
... ... @@ -40,4 +40,13 @@ describe(&#39;jwt-auth&#39;, () =&gt; {
40 40 expect(jwtAuth.extractUserFromToken(null)).toBeNull();
41 41 });
42 42 });
  43 +
  44 + // bin/lemon.js(agent 用来 task report/dispatch 自我汇报的 CLI)跑在 claude CLI 子进程
  45 + // 里,不跟主进程共享内存,只能靠 process.env 拿到 INTERNAL_TOKEN——commit b7b5096 之后
  46 + // 漏了这条路径,导致所有 agent 的 task report/dispatch 全部 401。
  47 + describe('INTERNAL_TOKEN 透传给子进程(bin/lemon.js 依赖这个)', () => {
  48 + it('require 本模块时应把 INTERNAL_TOKEN 写进 process.env.LEMON_INTERNAL_TOKEN', () => {
  49 + expect(process.env.LEMON_INTERNAL_TOKEN).toBe(jwtAuth.INTERNAL_TOKEN);
  50 + });
  51 + });
43 52 });
... ...