Skip to content

fix(core): scope boolean coercion to boolean-typed schema fields#4618

Merged
tanzhenxin merged 1 commit into
QwenLM:mainfrom
Pluviobyte:fix/schema-validator-boolean-coercion
Jun 8, 2026
Merged

fix(core): scope boolean coercion to boolean-typed schema fields#4618
tanzhenxin merged 1 commit into
QwenLM:mainfrom
Pluviobyte:fix/schema-validator-boolean-coercion

Conversation

@Pluviobyte

Copy link
Copy Markdown
Contributor

What this PR does

When tool-parameter validation fails, the validator retries after coercing values, and the boolean-coercion step used to rewrite any string holding the text true/false into a real boolean, no matter what type the field was declared as. This makes that coercion schema-aware so it only fires for fields whose schema actually accepts a boolean (and does not also accept a string), leaving genuine string arguments untouched.

Why it's needed

Self-hosted / OpenAI-compatible models frequently emit booleans as the strings "true"/"false", so the validator coerces them back on a failed-validation retry. The problem is that the coercion was applied indiscriminately: if a tool call legitimately carried the literal text "true" or "false" in a string argument (an old_string, new_string, content, commit message, search pattern, etc.) and validation happened to fail for an unrelated reason, that string was silently turned into a boolean. The tool then operated on true/false instead of the text the model asked for — a silent, hard-to-debug corruption of the requested operation. The neighbouring fixStringifiedJsonValues helper already consults the schema (via getAcceptedTypes) precisely to avoid this class of over-coercion; the boolean path simply never got the same treatment.

Reviewer Test Plan

How to verify

Run the validator unit tests: cd packages/core && npx vitest run src/utils/schemaValidator.test.ts. All existing boolean-coercion tests still pass (genuine boolean fields, nested objects, case-insensitive True/FALSE, etc.), and new tests cover the regression. To confirm the new tests actually exercise the bug, revert only schemaValidator.ts and re-run: the two new "should not corrupt…" / "should not coerce… also accept string" tests fail, proving the guard is what fixes the corruption.

Evidence (Before & After)

Schema: { old_string: {type: string}, new_string: {type: string}, is_active: {type: boolean} }. Params from the model: { old_string: "true", new_string: "false", is_active: "false" } (note is_active arrives as a string, which triggers the coercion retry).

Before: validation still reports an error and params is mutated to { old_string: true, new_string: false, is_active: false } — the two string arguments are corrupted into booleans.

After: validation returns null and params is { old_string: "true", new_string: "false", is_active: false } — only the boolean-typed field is coerced; the string arguments are preserved.

UI/TUI: N/A (pure validation logic, no user-visible surface).

Tested on

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

The change is platform-independent (pure JSON-schema logic with no OS-specific code paths); verified locally on Linux with vitest, eslint, prettier --check and tsc --noEmit, and CI exercises macOS/Windows.

Environment (optional)

Unit tests only.

Risk & Scope

  • Main risk or tradeoff: the coercion is now narrower — a stringified boolean is only converted for a field whose schema accepts boolean. Fields with no schema information are left untouched (previously they were coerced), which is the safer default and matches fixStringifiedJsonValues.
  • Not validated / out of scope: this does not change the separate "stringified JSON" coercion, and is independent of fix: coerce non-string tool params to strings for self-hosted LLMs #2512 (which adds the opposite direction — non-string → string — as a new function and does not touch fixBooleanValues).
  • Breaking changes / migration notes: none. Genuine boolean fields (including ["boolean","null"] unions and arrays of booleans) are still coerced exactly as before.

Linked Issues

No existing issue — this is a latent correctness bug found while reviewing the tool-parameter validation path. Happy to open a tracking issue if preferred.

中文说明

这个 PR 做了什么

工具参数校验失败后会先做类型纠正再重试,其中“布尔纠正”这一步原本会把任何取值为 true/false 的字符串都改成真正的布尔值,完全不看该字段在 schema 里声明的类型。本 PR 让这步纠正变成“看 schema”的:只有当字段的 schema 确实接受布尔(且不同时接受字符串)时才纠正,真正的字符串参数保持原样。

为什么需要

自部署 / OpenAI 兼容模型经常把布尔输出成 "true"/"false" 字符串,所以校验失败重试时会把它们纠正回布尔。问题在于这个纠正是无差别执行的:如果某次工具调用的字符串参数(old_stringnew_stringcontent、commit message、搜索串等)本来就合法地包含文本 "true"/"false",而校验又恰好因为别的原因失败,那么这些字符串就会被悄悄改成布尔值,导致工具实际执行的操作和模型请求的不一致——一种很难排查的静默数据损坏。同文件里的 fixStringifiedJsonValues 已经通过 getAcceptedTypes 查 schema 来避免这类“过度纠正”,只是布尔这条路径一直没用上同样的保护。

如何验证

运行:cd packages/core && npx vitest run src/utils/schemaValidator.test.ts。所有原有布尔纠正用例仍然通过,新增用例覆盖该回归;只回退 schemaValidator.ts 重跑时,新增的两个用例会失败,证明正是这个守卫修复了损坏。

影响与风险

纠正范围变窄:只对 schema 接受布尔的字段生效;无 schema 信息的字段不再纠正(更安全,且与 fixStringifiedJsonValues 一致)。无破坏性变更,真正的布尔字段(含可空布尔、布尔数组)行为不变。与 #2512 相互独立(那个 PR 是反方向的“非字符串→字符串”,新增函数,不改 fixBooleanValues)。

Made with Cursor

SchemaValidator.validate() retries with coercion when initial validation fails. fixBooleanValues() rewrote every string "true"/"false" it found — regardless of the field's declared type — into a real boolean. When a tool call legitimately carries the text "true"/"false" in a string field (for example an old_string, new_string or content argument) and validation failed for an unrelated reason, that string was silently corrupted into a boolean, changing the operation the model requested.

Make fixBooleanValues schema-aware and only coerce a value when the field's schema accepts boolean and does not also accept string. This mirrors the sibling fixStringifiedJsonValues()/getAcceptedTypes() helpers, which already consult the schema for exactly this reason, and preserves the intended leniency for genuine boolean fields (including nullable booleans and arrays of booleans).

Co-authored-by: Cursor <[email protected]>

@wenshao wenshao 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.

No issues found. LGTM! ✅ — qwen3.7-max via Qwen Code /review

@wenshao

wenshao commented May 30, 2026

Copy link
Copy Markdown
Collaborator

🔍 Maintainer Verification Report

Verified locally on macOS (Darwin 25.4.0) — tmux-based test session

Results Summary

Check Status Details
Unit Tests (schemaValidator) ✅ PASS 43/43 tests passed
TypeScript (tsc --noEmit) ✅ PASS 0 errors in PR files (3 pre-existing errors in telemetry/sdk.ts — unrelated)
ESLint ✅ PASS No lint errors in schemaValidator.ts or schemaValidator.test.ts
Regression Proof ✅ CONFIRMED See below

Regression Test

Reverted only schemaValidator.ts to main (kept new tests in place) → exactly 2 new tests fail:

× should not corrupt string fields whose value is literally "true"/"false"
× should not coerce string booleans for fields that also accept string

Restoring the fix → all 43 tests pass again. This confirms the fix is both necessary and sufficient.

Code Review Notes

  • The fix mirrors the existing fixStringifiedJsonValues pattern which is already schema-aware via getAcceptedTypes — consistent approach.
  • Guard condition !accepted || accepted.has('string') || !accepted.has('boolean') correctly handles:
    • No schema info → skip coercion (safe default)
    • Field accepts string → skip (avoid corrupting legitimate string values)
    • Field does not accept boolean → skip (nothing to coerce to)
  • Array recursion correctly passes items schema to child elements.
  • The anyOf: [string, boolean] case is properly handled — string values are preserved when the field also accepts string.

Verification Environment

  • OS: macOS (Darwin 25.4.0)
  • Node: local
  • vitest: 3.2.4
  • Branch: fix/schema-validator-boolean-coercion @ 2e628316a

Verdict

Ready to merge. The change is minimal, well-scoped, properly tested, and the regression proof confirms the fix addresses the described corruption bug. No regressions observed.

@wenshao
wenshao requested a review from tanzhenxin June 7, 2026 23:16

@tanzhenxin tanzhenxin 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.

Approving — correct, net-positive fix.

Scoping boolean coercion to boolean-typed schema fields removes a real silent-corruption bug (genuine string args like old_string/content/commit messages that held the literal text "true"/"false" were being rewritten to booleans on the validation-retry path), and it's consistent with the sibling fixStringifiedJsonValues gate.

One known limitation worth a follow-up (not blocking): the schema-awareness is incomplete — boolean fields expressed via $ref / allOf / enum: [true,false] / const, and booleans nested inside anyOf/oneOf-wrapped objects, aren't recognized, so on the retry path they now fail validation loudly instead of being coerced. It's narrow (built-in tool schemas don't use these keywords) and a loud failure is better than silent corruption, but it disproportionately affects self-hosted / OpenAI-compatible model schemas (MCP/Pydantic commonly use $ref/allOf/nullable-object unions). Worth extending getAcceptedTypes (fold allOf, resolve $ref, treat boolean enum/const) and unwrapping unions when descending — or documenting the gap.

@tanzhenxin
tanzhenxin merged commit e7edb26 into QwenLM:main Jun 8, 2026
10 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