-
Notifications
You must be signed in to change notification settings - Fork 3k
fix(core): parse QWEN_SERVE_MCP_CLIENT_BUDGET strictly as a decimal integer #5752
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -513,6 +513,7 @@ import { | |
| normalizeCoreSettingValue, | ||
| extractFilesFromTarGz, | ||
| fetchAllowedGitHub, | ||
| createWorkspaceMcpBudget, | ||
| } from './acpAgent.js'; | ||
| import { gzipSync } from 'node:zlib'; | ||
| import type { Config } from '@qwen-code/qwen-code-core'; | ||
|
|
@@ -6713,3 +6714,40 @@ describe('sessionLanguage multi-session propagation', () => { | |
| await agentPromise; | ||
| }); | ||
| }); | ||
|
|
||
| describe('createWorkspaceMcpBudget — env parsing', () => { | ||
| const KEY = 'QWEN_SERVE_MCP_CLIENT_BUDGET'; | ||
| const MODE = 'QWEN_SERVE_MCP_BUDGET_MODE'; | ||
| const onEvent = vi.fn(); | ||
|
|
||
| afterEach(() => { | ||
| delete process.env[KEY]; | ||
| delete process.env[MODE]; | ||
| vi.clearAllMocks(); | ||
| }); | ||
|
|
||
| it('accepts a plain positive decimal integer', () => { | ||
| process.env[KEY] = '100'; | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [Suggestion] The "accepts" tests only assert Compare with the core test which asserts the actual parsed value ( const MockBudget = vi.mocked(WorkspaceMcpBudget);
createWorkspaceMcpBudget(onEvent);
expect(MockBudget).toHaveBeenCalledWith(
expect.objectContaining({ clientBudget: 100, mode: 'warn' }),
); |
||
| expect(createWorkspaceMcpBudget(onEvent)).toBeDefined(); | ||
| }); | ||
|
|
||
| it('accepts a trimmed decimal integer', () => { | ||
| process.env[KEY] = ' 42 '; | ||
| expect(createWorkspaceMcpBudget(onEvent)).toBeDefined(); | ||
| }); | ||
|
|
||
| // Mirrors McpClientManager.readBudgetFromEnv: a loose Number() would coerce | ||
| // these (0x10=16, 1e2=100, 1.0=1) and silently set a budget. The strict | ||
| // /^\d+$/ + isSafeInteger parse must reject them. | ||
| it.each(['0x10', '1e2', '1.0', '0b101', '5 abc', 'abc', '-5', '0', ' '])( | ||
| 'rejects non-decimal-integer value %j', | ||
| (raw) => { | ||
| process.env[KEY] = raw; | ||
| expect(createWorkspaceMcpBudget(onEvent)).toBeUndefined(); | ||
| }, | ||
| ); | ||
|
|
||
| it('returns undefined when the budget env var is unset', () => { | ||
| expect(createWorkspaceMcpBudget(onEvent)).toBeUndefined(); | ||
| }); | ||
| }); | ||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -2489,34 +2489,37 @@ function parsePoolDrainMs(envValue: string | undefined): number { | |||||
| * invokes `tryReserve`/`release`; this helper produces the controller | ||||||
| * and wires the event callback. | ||||||
| */ | ||||||
| function createWorkspaceMcpBudget( | ||||||
| export function createWorkspaceMcpBudget( | ||||||
| onEvent: (event: McpBudgetEvent) => void, | ||||||
| ): WorkspaceMcpBudget | undefined { | ||||||
| const rawBudget = process.env['QWEN_SERVE_MCP_CLIENT_BUDGET']; | ||||||
| const rawMode = process.env['QWEN_SERVE_MCP_BUDGET_MODE']; | ||||||
| // Match `McpClientManager.readBudgetFromEnv`'s parsing exactly. | ||||||
| // Use `Number(...)` + `Number.isInteger` so the pool and the manager | ||||||
| // Match `McpClientManager.readBudgetFromEnv`'s parsing exactly: only plain | ||||||
| // decimal digits set a budget. A loose `Number(...)` would silently accept | ||||||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [Suggestion] The comment says "Match
A future maintainer reading "exactly" may skip checking
Suggested change
|
||||||
| // `0x10`=16, `1e2`=100, and `1.0`=1 (all pass `isInteger`); the strict | ||||||
| // `/^\d+$/` + `isSafeInteger` check rejects them so the pool and the manager | ||||||
| // honor the same env values. | ||||||
| const budget = | ||||||
| rawBudget !== undefined && rawBudget !== '' ? Number(rawBudget) : undefined; | ||||||
| let budget: number | undefined; | ||||||
| if (rawBudget !== undefined && rawBudget !== '') { | ||||||
| const trimmed = rawBudget.trim(); | ||||||
| const parsed = Number(trimmed); | ||||||
| if (/^\d+$/.test(trimmed) && Number.isSafeInteger(parsed) && parsed > 0) { | ||||||
| budget = parsed; | ||||||
| } else { | ||||||
| process.stderr.write( | ||||||
| `qwen serve: ignoring invalid QWEN_SERVE_MCP_CLIENT_BUDGET=` + | ||||||
| `'${rawBudget}' (expected positive integer); ` + | ||||||
| `MCP budget enforcement disabled for this child.\n`, | ||||||
| ); | ||||||
| } | ||||||
| } | ||||||
| const mode: McpBudgetMode = (() => { | ||||||
| if (rawMode === 'enforce' || rawMode === 'warn' || rawMode === 'off') { | ||||||
| return rawMode; | ||||||
| } | ||||||
| return budget !== undefined && | ||||||
| Number.isFinite(budget) && | ||||||
| Number.isInteger(budget) && | ||||||
| budget > 0 | ||||||
| ? 'warn' | ||||||
| : 'off'; | ||||||
| return budget !== undefined ? 'warn' : 'off'; | ||||||
| })(); | ||||||
| if ( | ||||||
| mode === 'off' || | ||||||
| budget === undefined || | ||||||
| !Number.isFinite(budget) || | ||||||
| !Number.isInteger(budget) || | ||||||
| budget <= 0 | ||||||
| ) { | ||||||
| if (mode === 'off' || budget === undefined) { | ||||||
| return undefined; | ||||||
| } | ||||||
| return new WorkspaceMcpBudget({ | ||||||
|
|
||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -2627,6 +2627,25 @@ describe('McpClientManager — PR 14 guardrails', () => { | |
| } | ||
| }); | ||
|
|
||
| it('readBudgetFromEnv rejects non-decimal budget values (hex / scientific / float)', async () => { | ||
| const writeSpy = vi.spyOn(process.stderr, 'write').mockReturnValue(true); | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [Suggestion] The — qwen3.7-max via Qwen Code /review |
||
| try { | ||
| for (const bad of ['0x10', '1e2', '1.0']) { | ||
| process.env['QWEN_SERVE_MCP_CLIENT_BUDGET'] = bad; | ||
| const manager = mkManager({ config: configWithServers({}) }); | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [Suggestion] The stderr breadcrumb is the sole operator-facing signal for rejected values. Add an assertion after the rejection loop: const calls = writeSpy.mock.calls.map((c) => String(c[0]));
expect(calls.some(s => s.includes("'0x10'"))).toBe(true);— qwen3.7-max via Qwen Code /review |
||
| // Pre-fix Number('0x10')=16 / Number('1e2')=100 slipped through as a budget. | ||
| expect(manager.getMcpClientBudget()).toBeUndefined(); | ||
| } | ||
| // a plain decimal integer is still accepted. | ||
| process.env['QWEN_SERVE_MCP_CLIENT_BUDGET'] = '16'; | ||
| const ok = mkManager({ config: configWithServers({}) }); | ||
| expect(ok.getMcpClientBudget()).toBe(16); | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [Suggestion] The upgrade from — qwen3.7-max via Qwen Code /review |
||
| } finally { | ||
| writeSpy.mockRestore(); | ||
| delete process.env['QWEN_SERVE_MCP_CLIENT_BUDGET']; | ||
| } | ||
| }); | ||
|
|
||
| it('readResource rejects existing-but-now-disabled servers (wenshao R7 #5 line 1342)', async () => { | ||
| // Pre-fix: a server connected pre-disable and then operator- | ||
| // disabled mid-session via settings reload would still serve | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -269,7 +269,8 @@ export function mcpTransportOf(config: MCPServerConfig): McpTransportKind { | |
| * behavior, no enforcement. | ||
| * | ||
| * `QWEN_SERVE_MCP_CLIENT_BUDGET` — positive integer; non-numeric / | ||
| * zero / negative / NaN are silently ignored (treated as unset). | ||
| * zero / negative / NaN are rejected (treated as unset) and a | ||
| * stderr breadcrumb is written so the misconfiguration is visible. | ||
| * `QWEN_SERVE_MCP_BUDGET_MODE` — `enforce|warn|off`. Defaults to | ||
| * `warn` when a budget is set, `off` otherwise. | ||
| */ | ||
|
|
@@ -278,8 +279,12 @@ function readBudgetFromEnv(): McpBudgetConfig { | |
| const rawMode = process.env['QWEN_SERVE_MCP_BUDGET_MODE']; | ||
| let clientBudget: number | undefined; | ||
| if (rawBudget !== undefined && rawBudget !== '') { | ||
| const parsed = Number(rawBudget); | ||
| if (Number.isFinite(parsed) && Number.isInteger(parsed) && parsed > 0) { | ||
| // Parse strictly as a decimal integer: Number('0x10')=16, Number('1e2')=100 | ||
| // and Number('1.0')=1 all pass isInteger, so a loose parse would silently | ||
| // accept them. Only plain decimal digits should set a budget. | ||
| const trimmed = rawBudget.trim(); | ||
| const parsed = Number(trimmed); | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [Suggestion] The inline parsing logic ( Consider replacing the inline parse with: const budget = parsePositiveIntegerEnv('QWEN_SERVE_MCP_CLIENT_BUDGET');The stderr warning branch still works since — qwen3.7-max via Qwen Code /review |
||
| if (/^\d+$/.test(trimmed) && Number.isSafeInteger(parsed) && parsed > 0) { | ||
| clientBudget = parsed; | ||
| } else { | ||
| // operator typos | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[Suggestion]
MODEis declared and cleaned up inafterEachbut never set in any test case. The mode-resolution IIFE has four branches (enforce,warn,off, and the fallbackbudget !== undefined ? 'warn' : 'off') — all untested. Consider adding at least: