fix(core): scope boolean coercion to boolean-typed schema fields#4618
Conversation
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
left a comment
There was a problem hiding this comment.
No issues found. LGTM! ✅ — qwen3.7-max via Qwen Code /review
🔍 Maintainer Verification ReportVerified locally on macOS (Darwin 25.4.0) — tmux-based test session Results Summary
Regression TestReverted only Restoring the fix → all 43 tests pass again. This confirms the fix is both necessary and sufficient. Code Review Notes
Verification Environment
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. |
tanzhenxin
left a comment
There was a problem hiding this comment.
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.
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/falseinto 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 (anold_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 ontrue/falseinstead of the text the model asked for — a silent, hard-to-debug corruption of the requested operation. The neighbouringfixStringifiedJsonValueshelper already consults the schema (viagetAcceptedTypes) 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-insensitiveTrue/FALSE, etc.), and new tests cover the regression. To confirm the new tests actually exercise the bug, revert onlyschemaValidator.tsand 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" }(noteis_activearrives as a string, which triggers the coercion retry).Before: validation still reports an error and
paramsis mutated to{ old_string: true, new_string: false, is_active: false }— the two string arguments are corrupted into booleans.After: validation returns
nullandparamsis{ 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
The change is platform-independent (pure JSON-schema logic with no OS-specific code paths); verified locally on Linux with
vitest,eslint,prettier --checkandtsc --noEmit, and CI exercises macOS/Windows.Environment (optional)
Unit tests only.
Risk & Scope
boolean. Fields with no schema information are left untouched (previously they were coerced), which is the safer default and matchesfixStringifiedJsonValues.fixBooleanValues).["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_string、new_string、content、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