Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

fix(serve): validate list maxEntries as a positive integer#5719

Merged
wenshao merged 1 commit into
QwenLM:mainQwenLM/qwen-code:mainfrom
he-yufeng:fix/list-validate-max-entrieshe-yufeng/qwen-code:fix/list-validate-max-entriesCopy head branch name to clipboard
Jun 23, 2026
Merged

fix(serve): validate list maxEntries as a positive integer#5719
wenshao merged 1 commit into
QwenLM:mainQwenLM/qwen-code:mainfrom
he-yufeng:fix/list-validate-max-entrieshe-yufeng/qwen-code:fix/list-validate-max-entriesCopy head branch name to clipboard

Conversation

@he-yufeng

@he-yufeng he-yufeng commented Jun 22, 2026

Copy link
Copy Markdown
Contributor

What this PR does

Validates the maxEntries option of the workspace file-system list() as a positive integer, rejecting Infinity / NaN / floats / 0 / negatives with a parse_error — the same guard readText() already applies to its limit and line options in the same file.

Why it's needed

list() caps results with if (entries.length >= opts.maxEntries) break. With an unvalidated maxEntries of Infinity or NaN, that comparison is always false, so the cap is silently ignored and the entire directory is returned; a float, 0, or a negative is equally meaningless. readText() in the same file already rejects bad limit / line values this way (#5639), so this closes the same validation gap for list()'s maxEntries and keeps the two entry points consistent.

Reviewer Test Plan

How to verify

Run the workspace file-system suite: npx vitest run packages/cli/src/serve/fs/workspace-file-system.test.ts. The new test rejects a non-positive-integer maxEntries with parse_error exercises Infinity, NaN, 0, -1, 1.5, and Number.MAX_SAFE_INTEGER + 1, asserting each is rejected with an FsError of kind parse_error. Expected: 77 passed. Stashing only the source guard makes that one test fail (the bad value is no longer rejected), which confirms the test reproduces the gap; restoring the guard makes it pass again. Valid positive-integer maxEntries is unchanged.

Evidence (Before & After)

N/A — non–user-visible change in the serve layer (input validation), no TUI surface. Verified by unit test (see above): 1 failed without the guard, 77 passed with it.

Tested on

OS Status
🍏 macOS ⚠️
🪟 Windows
🐧 Linux ⚠️

Environment (optional)

Unit tests only (vitest); no runtime / sandbox needed.

Risk & Scope

  • Main risk or tradeoff: Low — a previously-accepted-but-meaningless maxEntries (e.g. Infinity, a float, 0, a negative) now throws parse_error instead of silently returning the whole directory. Any caller relying on that silent no-cap behavior would now see an error, but that path was already broken (the cap was ignored).
  • Not validated / out of scope: Only list()'s maxEntries is touched; the other list() options and the rest of the file are unchanged.
  • Breaking changes / migration notes: None for valid input. Callers must pass a positive integer (or omit maxEntries), which matches readText()'s existing contract.

Linked Issues

No issue; mirrors the validation added for readText() in #5639.

中文说明

list()if (entries.length >= opts.maxEntries) break 来限制返回条数,但没有校验 maxEntries。传入 Infinity / NaN 时这个比较恒为 false,上限被静默忽略、整目录被返回;浮点数、0、负数同样没有意义。同文件的 readText() 早已对 limit / line 做了同样的校验(#5639),这个 PR 给 list()maxEntries 补上一致的「正整数」校验,非法值抛 parse_error。新增测试覆盖 Infinity/NaN/0/-1/1.5/超安全整数;去掉修复后该测试失败、加回后 77 通过。合法的正整数行为不变。

@wenshao

wenshao commented Jun 22, 2026

Copy link
Copy Markdown
Collaborator

@qwen-code /triage

@qwen-code-ci-bot qwen-code-ci-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey @he-yufeng — thanks for the fix! The code change itself looks clean and well-scoped.

However, the PR body is missing several required sections from the PR template:

  • ### How to verify, ### Evidence (Before & After), ### Tested on under Reviewer Test Plan
  • ## Risk & Scope (currently just ## Risk, and missing the sub-items: tradeoff, out of scope, breaking changes)
  • ## Linked Issues
  • <details>中文说明</details> — the bilingual summary

Could you fill those in? The test plan especially helps reviewers confirm the fix works as expected without having to run it themselves. Happy to take another look once updated.

@wenshao

wenshao commented Jun 22, 2026

Copy link
Copy Markdown
Collaborator

✅ Local verification report (maintainer)

Built and tested this PR locally to confirm the fix before merge.

Environment

  • macOS (Darwin 25.5), Node v22.22.2
  • PR head a3dfed9a9, base origin/main 2fd2104fa (1 commit on top)
  • Method: isolated git worktree at the PR head with a clean npm ci + fresh core/acp-bridge build, then verified each claim independently.

Results

Check Command Result
Target tests vitest run .../serve/fs/workspace-file-system.test.ts ✅ 77/77 pass
New test -t "rejects a non-positive-integer maxEntries" ✅ pass
Lint eslint on both changed files ✅ clean
Types tsc --noEmit (packages/cli) ✅ exit 0
Format prettier --check on both files ✅ clean

Mutation test (the key evidence). To confirm the new test actually guards the gap rather than passing vacuously, I reverted workspace-file-system.ts back to the main version (no maxEntries validation) while keeping the PR's new test. It fails immediately:

FAIL > list > rejects a non-positive-integer maxEntries with parse_error
  expect(isFsError(err)).toBe(true)  // got: false

With the unvalidated code, maxEntries: Infinity is silently accepted — entries.length >= Infinity never breaks, so list() resolves instead of throwing, and isFsError(err) is false. The test pins exactly that gap. It passes again once the guard is restored. The test covers [Infinity, NaN, 0, -1, 1.5, Number.MAX_SAFE_INTEGER + 1] — all six are rejected.

Correctness review

  • The guard !Number.isSafeInteger(opts.maxEntries) || opts.maxEntries < 1 (only when maxEntries !== undefined) is a faithful copy of the established pattern in the same file — readText() guards line / limit / maxBytes with the identical !Number.isSafeInteger(x) || x < 1 shape. undefined still means "no cap" and is correctly left untouched.
  • Error handling is consistent: the parse_error is thrown inside the existing try, so it flows through catch → recordAndWrap('list') and is audited/wrapped like every other error here. It sits after the trust gate and before any filesystem I/O (fail-fast).
  • No caller is broken (the one real risk for this kind of change): the only two callers pass a finite cap — dispatch.tsMAX_LIST + 1 and workspace-file-read.tsMAX_LIST_ENTRIES + 1, both = 2001, which passes the guard. Nothing relies on Infinity / 0 as an "unlimited" sentinel.

Verdict: LGTM. Minimal, correct, mutation-verified, consistent with the sibling readText guards, and breaks no callers. Safe to merge from my side.

🇨🇳 中文版

✅ 本地验证报告(维护者)

合并前在本地构建并测试了本 PR 以确认修复。

环境

  • macOS(Darwin 25.5),Node v22.22.2
  • PR head a3dfed9a9,base origin/main 2fd2104fa(其上 1 个提交)
  • 方法:在 PR head 上建独立 git worktree,做了干净的 npm ci 并重新构建 core/acp-bridge,再逐条独立验证 PR 的每个声明。

结果

检查项 命令 结果
目标测试 vitest run .../serve/fs/workspace-file-system.test.ts ✅ 77/77 通过
新增测试 -t "rejects a non-positive-integer maxEntries" ✅ 通过
Lint 对两个改动文件跑 eslint ✅ 干净
类型 tsc --noEmitpackages/cli ✅ exit 0
格式 对两个文件跑 prettier --check ✅ 干净

变异测试(关键证据)。 为确认新增测试是真的能挡住这个缺口、而不是空过,我把 workspace-file-system.ts 还原成 main 上的版本(没有 maxEntries 校验),同时保留 PR 的新测试。它立刻失败

FAIL > list > rejects a non-positive-integer maxEntries with parse_error
  expect(isFsError(err)).toBe(true)  // 实际: false

在没有校验的代码里,maxEntries: Infinity 被静默接受 —— entries.length >= Infinity 永远不会 break,于是 list() 正常返回而不是抛错,isFsError(err)false。这个测试精确锁定了该缺口。恢复守卫后它又通过。测试覆盖了 [Infinity, NaN, 0, -1, 1.5, Number.MAX_SAFE_INTEGER + 1] —— 六个值全部被拒。

正确性审查

  • 守卫 !Number.isSafeInteger(opts.maxEntries) || opts.maxEntries < 1(仅当 maxEntries !== undefined 时)忠实复刻了同文件里既有的模式 —— readText()line / limit / maxBytes 用的就是完全相同的 !Number.isSafeInteger(x) || x < 1 形态。undefined 仍然表示「不限制」,被正确地放行不动。
  • 错误处理保持一致: 这个 parse_error 是在既有的 try 内部抛出的,因此会经过 catch → recordAndWrap('list'),和这里其它错误一样被审计/包装。它位于 trust 校验之后、任何文件系统 I/O 之前(fail-fast)。
  • 没有破坏任何调用方(这类改动唯一的真实风险点):仅有的两个调用方传的都是有限的 cap —— dispatch.tsMAX_LIST + 1workspace-file-read.tsMAX_LIST_ENTRIES + 1,两者都 = 2001,能通过守卫。没有任何地方依赖 Infinity / 0 当作「无限制」的哨兵值。

结论:LGTM。 修复最小化、正确、经变异测试验证,与同文件 readText 的兄弟守卫一致,且不破坏任何调用方。从我这边看可以安全合并。

@he-yufeng

Copy link
Copy Markdown
Contributor Author

Filled in the missing template sections: How to verify / Evidence / Tested on, the full Risk & Scope sub-items, Linked Issues, and the 中文说明. Ready for another look — thanks!

@wenshao

wenshao commented Jun 22, 2026

Copy link
Copy Markdown
Collaborator

@qwen-code /triage

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Thanks for the PR, @he-yufeng!

Template looks good ✓ — all required sections present, including the bilingual summary and reviewer test plan.

On direction: this is a clean follow-up to #5639, which added the same Number.isSafeInteger guard to readText()'s limit/line options. Closing the same validation gap for list()'s maxEntries is a natural consistency fix. No product-direction concerns — it's exactly the kind of small defensive-validation PR that keeps the serve layer predictable.

On approach: the scope is tight — 12 lines of guard + 19 lines of test, touching exactly one method. The implementation is a direct copy of the readText() pattern (Number.isSafeInteger + < 1 check → FsError('parse_error', ...)), which is the right call for consistency. No scope creep, no drive-by refactors.

Moving on to code review and tests. 🔍

中文说明

感谢 @he-yufeng 的贡献!

模板完整 ✓ — 所有必填章节都在,包括双语说明和 Reviewer Test Plan。

方向:这是 #5639 的自然延续——那次给 readText()limit/line 加了 Number.isSafeInteger 校验,这个 PR 给 list()maxEntries 补上同样的一致性校验。没有产品方向问题,属于典型的防御性输入验证补漏。

方案:范围很紧——12 行校验 + 19 行测试,只动了一个方法。实现完全复用了 readText() 的模式(Number.isSafeInteger + < 1FsError('parse_error', ...)),保持一致性是正确的选择。没有范围蔓延或顺手重构。

进入代码审查和测试。🔍

Qwen Code · qwen3.7-max

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Code Review

The guard is a faithful mirror of the existing readText() validation at lines 386–401 — same Number.isSafeInteger + < 1 check, same FsError('parse_error', ...) shape, same error-message format. Placed right after assertTrustedForIntent, before the opendir call, which is the correct position (fail fast, before any I/O). No issues found.

One minor observation (not a blocker): the test asserts isFsError(err) + String(err).toContain('maxEntries') rather than checking err.kind === 'parse_error' directly (which some neighboring tests do). Both styles work — the String(err) approach is actually more thorough since it verifies the error message mentions the parameter name.

Test Results

With the guard (PR applied): 77/77 passed ✓

 ✓ src/serve/fs/workspace-file-system.test.ts (77 tests) 208ms

 Test Files  1 passed (1)
      Tests  77 passed (77)

Without the guard (source reverted, test kept): 1 failed, 76 passed — confirming the new test catches the gap.

 FAIL  src/serve/fs/workspace-file-system.test.ts > WorkspaceFileSystem - list > rejects a non-positive-integer maxEntries with parse_error
AssertionError: expected false to be true // Object.is equality
 ❯ src/serve/fs/workspace-file-system.test.ts:280:30
    280|       expect(isFsError(err)).toBe(true);

 Test Files  1 failed (1)
      Tests  1 failed | 76 passed (77)

Before/After (tmux): N/A — non–user-visible change in the serve layer (input validation), no TUI surface. Verified entirely by unit test.

中文说明

代码审查

校验逻辑完全复用了 readText() 在 386–401 行的模式——同样的 Number.isSafeInteger + < 1 检查、同样的 FsError('parse_error', ...) 格式。放在 assertTrustedForIntent 之后、opendir 之前,位置正确(在任何 I/O 之前快速失败)。没有发现问题。

一个小观察(不是 blocker):测试用了 isFsError(err) + String(err).toContain('maxEntries') 而不是直接检查 err.kind === 'parse_error'(周围的一些测试是这么做的)。两种方式都可以——String(err) 的方式实际上更全面,因为它验证了错误消息中包含了参数名。

测试结果

有校验(PR 已应用): 77/77 通过 ✓

去掉校验(回退源码,保留测试): 1 失败、76 通过——确认新测试确实能捕获这个漏洞。

Before/After(tmux): 不适用——serve 层的非用户可见变更(输入校验),无 TUI 界面。完全由单元测试验证。

Qwen Code · qwen3.7-max

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

This is a textbook consistency fix. readText() got the same guard in #5639; list() was the obvious gap. The PR closes it with the minimum possible code, following the exact same pattern, and the test genuinely catches the bug (confirmed: 1 fail without the guard, 77 pass with it).

My independent proposal before reading the diff would have been: add Number.isSafeInteger + < 1 check on opts.maxEntries right after assertTrustedForIntent, throw FsError('parse_error', ...), test with Infinity/NaN/0/-1/float/unsafe-integer. That's exactly what the PR does.

Nothing to cut, nothing missing, nothing to bikeshed. Approving. ✅

中文说明

这是一个标准的 consistency fix。readText()#5639 中加了同样的校验,list() 是显而易见的遗漏,这个 PR 用最少的代码、完全复用既有模式补上了。测试确实能捕获 bug(已确认:去掉校验 1 失败,加上 77 通过)。

独立提案和 PR 实现完全一致,没有可砍的范围、没有遗漏、没有争议点。通过。✅

Qwen Code · qwen3.7-max

@qwen-code-ci-bot qwen-code-ci-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, looks ready to ship. ✅

@wenshao

wenshao commented Jun 22, 2026

Copy link
Copy Markdown
Collaborator

✅ Runtime-surface follow-up — drove the real qwen serve daemon

Complementary to my test/mutation report above (which was vitest + mutation based). This one runs the actual daemon and checks the change at the only runtime surface that reaches WorkspaceFileSystem.list() — the serve HTTP API.

Method: built the PR head, launched qwen serve --workspace <tmp> --port 4179 --no-web (loopback, bearer auth off by default), and hit GET /list against a workspace holding a 3-file dir and a 2001-file dir.

No-regression at the surface

The guard runs on every list call with the hardcoded MAX_LIST_ENTRIES + 1 = 2001, which is a valid safe integer, so it passes and listing is unaffected:

Request Response
GET /list?path=small (3 files) 200 · truncated:false · 3 entries
GET /list?path=big (2001 files) 200 · truncated:true · 2000 entries — the cap machinery is intact

Reachability probe (the part worth adding)

I tried to feed a bad maxEntries from the client — the exact input the new guard exists to reject:

Client request Server response
GET /list?path=big&maxEntries=1 200 · truncated:true · count 2000
…&maxEntries=Infinity 200 · truncated:true · count 2000
…&maxEntries=NaN · -5 · 1.5 200 · truncated:true · count 2000

Every value is ignored — the query param never reaches list(). Both call sites (the GET /list route and the ACP qwen/file/list method) hardcode maxEntries server-side, and nowhere in the tree is ListOptions.maxEntries populated from client input. So the new parse_error guard cannot be reached from any external surface — it is pure defense-in-depth on the internal interface contract (faithfully mirroring the readText() guards), and the unit test is the right — and only — place to exercise it.

Verdict

PASS (runtime side) — confirms LGTM. Because the guard never fires on the real path, this PR is externally indistinguishable from base: same responses, breaks nothing. One scoping note for the merge log — the "entire directory is returned" scenario in the PR description is latent (no current caller passes a bad cap; both pass 2001), not a live bug. That makes this a consistency / future-proofing hardening, which is still a perfectly good reason to land it.

🇨🇳 中文版

✅ 运行时层面的补充验证 —— 实际拉起了 qwen serve 守护进程

作为上面那份「测试 + 变异」报告(基于 vitest)的补充。这一份跑的是真实守护进程,在唯一能触达 WorkspaceFileSystem.list() 的运行时入口 —— serve 的 HTTP API —— 上检查这次改动。

方法: 构建 PR head,启动 qwen serve --workspace <tmp> --port 4179 --no-web(回环地址,默认关闭 bearer 鉴权),然后对一个包含「3 文件目录」和「2001 文件目录」的工作区发 GET /list

接口层无回归

守卫在每次 list 调用时都会跑,用的是硬编码的 MAX_LIST_ENTRIES + 1 = 2001,这是个合法的安全整数,所以会通过、列目录不受影响:

请求 响应
GET /list?path=small(3 文件) 200 · truncated:false · 3 条
GET /list?path=big(2001 文件) 200 · truncated:true · 2000 条 —— 截断机制完好

可达性探针(值得补充的点)

我尝试从客户端塞一个非法的 maxEntries —— 也就是这个新守卫本来要拒绝的输入:

客户端请求 服务端响应
GET /list?path=big&maxEntries=1 200 · truncated:true · 共 2000
…&maxEntries=Infinity 200 · truncated:true · 共 2000
…&maxEntries=NaN · -5 · 1.5 200 · truncated:true · 共 2000

每个值都被忽略 —— 这个 query 参数根本传不到 list()。两个调用点(GET /list 路由和 ACP 的 qwen/file/list 方法)都在服务端硬编码 maxEntries,整个代码树里也没有任何地方用客户端输入去填 ListOptions.maxEntries。所以这个新的 parse_error 守卫从任何外部入口都触达不到 —— 它纯粹是对内部接口契约的纵深防御(忠实复刻了 readText() 的守卫),单元测试就是验证它的正确、且唯一的地方。

结论

通过(运行时层面)—— 确认 LGTM。 因为守卫在真实路径上永不触发,本 PR 在外部表现上与 base 无法区分:响应相同、不破坏任何东西。给合并记录补一条 scope 说明 —— PR 描述里「整目录被返回」的场景是潜在的(当前没有任何调用方传非法 cap,两个都传 2001),并非一个活跃 bug。这让本 PR 成为一次一致性 / 未来防护的加固 —— 这依然是个完全成立的合并理由。

Runtime-surface verification on macOS — real qwen serve daemon (loopback, no auth), GET /list + client-maxEntries injection probe. Complements the static/test/mutation report above.

@wenshao
wenshao merged commit 31dcb6e into QwenLM:main Jun 23, 2026
34 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

Morty Proxy This is a proxified and sanitized view of the page, visit original site.