Commit 9e586563f99a1994a4fec41d8562ec987c16ff39
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>
Showing
4 changed files
with
78 additions
and
4 deletions
bin/lemon.js
| @@ -3,7 +3,7 @@ | @@ -3,7 +3,7 @@ | ||
| 3 | const { parseArgs } = require('util'); | 3 | const { parseArgs } = require('util'); |
| 4 | const http = require('http'); | 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 | function request(method, path, data) { | 8 | function request(method, path, data) { |
| 9 | return new Promise((resolve, reject) => { | 9 | return new Promise((resolve, reject) => { |
| @@ -13,6 +13,12 @@ function request(method, path, data) { | @@ -13,6 +13,12 @@ function request(method, path, data) { | ||
| 13 | headers['Content-Type'] = 'application/json'; | 13 | headers['Content-Type'] = 'application/json'; |
| 14 | headers['Content-Length'] = Buffer.byteLength(dataStr); | 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 | const req = http.request(API_BASE + path, { | 22 | const req = http.request(API_BASE + path, { |
| 17 | method, | 23 | method, |
| 18 | headers | 24 | headers |
server/middleware/jwt-auth.js
| @@ -184,12 +184,20 @@ function extractUserFromToken(token) { | @@ -184,12 +184,20 @@ function extractUserFromToken(token) { | ||
| 184 | * company.js:82/506、task-settle.js:452 这三处 `fetch('http://127.0.0.1:.../api/agent/...')` | 184 | * company.js:82/506、task-settle.js:452 这三处 `fetch('http://127.0.0.1:.../api/agent/...')` |
| 185 | * 拿不到任何用户 cookie,会被 401 挡住——38 号文档 Phase 1)。 | 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 | const INTERNAL_TOKEN = require('crypto').randomBytes(24).toString('hex'); | 198 | const INTERNAL_TOKEN = require('crypto').randomBytes(24).toString('hex'); |
| 192 | const INTERNAL_TOKEN_HEADER = 'x-internal-token'; | 199 | const INTERNAL_TOKEN_HEADER = 'x-internal-token'; |
| 200 | +process.env.LEMON_INTERNAL_TOKEN = INTERNAL_TOKEN; | ||
| 193 | 201 | ||
| 194 | /** | 202 | /** |
| 195 | * Middleware: 放行本进程自环调用(带对的 INTERNAL_TOKEN),其余情况退回 requireAuth。 | 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('jwt-auth', () => { | @@ -40,4 +40,13 @@ describe('jwt-auth', () => { | ||
| 40 | expect(jwtAuth.extractUserFromToken(null)).toBeNull(); | 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 | }); |