fix(agents): prevent malformed HTML entities from breaking tool calls#99564
Merged
Conversation
Contributor
|
Codex review: stale review; fresh review needed. Summary Next step Review history (5 earlier review cycles)
|
Contributor
Author
|
@clawsweeper re-review |
steipete
force-pushed
the
fix/decode-surrogate-entities
branch
from
July 6, 2026 00:35
44127ca to
f01db73
Compare
Contributor
|
Merged via squash.
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What Problem This Solves
OpenClaw's HTML-entity repair wrapper mutates the actual streamed tool-call
argumentsobject before tool execution. A provider payload that is still valid ASCII, such asbad � end, could previously be repaired intobad \uD800 end, introducing a lone UTF-16 surrogate into live tool input. That malformed text can break downstream JSON/URI handling, transcript persistence, or tool-specific string processing. This PR keeps surrogate numeric entities escaped while preserving normal decoding for valid HTML entities.Evidence
OpenAI-compatible provider/tool execution proof
I ran a local OpenAI-compatible HTTP/SSE provider through OpenClaw's real
openai-completionsprovider adapter, with model compat set totoolCallArgumentsEncoding: "html-entities"and the production embedded-runner HTML-entity tool-call wrapper installed. The provider emitted a streamedtool_callsresponse for a registeredprobe_tool; after the OpenClaw stream produced atype: "toolCall"block, the proof invoked the probe tool and logged the arguments immediately before execution.Command form:
Redacted terminal excerpt after this patch:
This exercises provider transport, streamed OpenAI-compatible tool-call parsing, the embedded-runner HTML-entity wrapper, and the pre-tool-execution argument boundary. It shows surrogate numeric entities remain ASCII entity text before the tool executes, while the valid astral numeric entity still decodes to
😀; the resulting query contains no lone surrogate and can be URI-encoded.Focused wrapper proof
I also ran a provider-shaped tool-call stream through the production
createHtmlEntityToolCallArgumentDecodingWrapper, then consumed the wrapped stream the same way downstream tool execution can inspect the final assistant message.Observed output after this patch:
{ "query": "bad � and � end", "ok": "ok 😀", "hasLoneSurrogate": false, "uriEncoding": "bad%20%26%23xD800%3B%20and%20%26%2355296%3B%20end" }Focused regression test
pnpm exec vitest run --reporter=verbose --config test/vitest/vitest.unit-fast.config.ts src/agents/embedded-agent-runner/tool-call-argument-decoding.test.tsThe focused suite reports
Test Files 1 passed (1)andTests 5 passed (5). The regression now covers�and�preservation, plus a positive astral scalar case (😀->😀).Review findings addressed
Real behavior proofworkflow passing (run 28670300540), and the PR body keeps the OpenAI-compatible provider/tool execution proof as real OpenClaw adapter + streamed tool-call + pre-tool-execution boundary coverage. I still do not have credentialed xAI or Venice live-provider credentials in this environment, so live third-party provider escaping remains explicitly disclosed as not tested and a maintainer-decision proof boundary rather than hidden or replaced with unit-only evidence.native-command-session-target(passed=24 failed=1, job 85015792777). The samenative-command-session-targetscenario also fails on latestorigin/mainat48e8965b1060d61beda1a52d572373a409317498(passed=24 failed=1, run 28671452150/job 85035716021). This PR's diff only changessrc/agents/embedded-agent-runner/tool-call-argument-decoding.tsand its focused test, so the QA Smoke failure is treated as a main-baseline CI blocker, not a PR-introduced code failure.Summary
�and�as their original ASCII entity text instead of converting them into lone UTF-16 surrogates.argumentsobject before tools execute. Producing malformed UTF-16 there can break JSON/URI handling, transcript recording, or downstream tool processing even though the original provider payload was valid ASCII text.Root Cause
String.fromCodePoint(). That parser invariant missed the Unicode scalar-value rule: UTF-16 surrogate code points in the 0xd800-0xdfff range are numeric code points but must not be introduced as standalone text, so provider ASCII entity text could become malformed UTF-16 tool input.Real behavior proof
v22.17.0and pnpm11.2.2.openai-completionsadapter over HTTP/SSE, emitted a streamed tool call, produced OpenClaw stream events throughtoolcall_end, executed the probe tool, and recorded pre-execution arguments withquerypreserved asbad � and � end, valid astral entity decoded took 😀,hasLoneSurrogate: false, and successful URI encoding. The focused Vitest rerun on the current head also passedTest Files 1 passed (1)andTests 5 passed (5).Regression Test Plan
src/agents/embedded-agent-runner/tool-call-argument-decoding.test.ts�), a decimal surrogate entity (�), and a valid astral scalar (😀). The wrapper tests also lock the shared-arguments object path through streamed partial/message events andresult()so double-decoding does not corrupt already-repaired arguments.Merge risk
merge-risk:low🤖 Generated with Claude Code