fix(plugins): apply output text transforms to toolcall_delta and toolcall_end events#97769
Conversation
|
Codex review: needs maintainer review before merge. Reviewed June 30, 2026, 8:08 PM ET / 00:08 UTC. Summary PR surface: Source +45, Tests +79. Total +124 across 2 files. Reproducibility: yes. Current main source shows provider output transforms skip structured ToolCall.arguments, and the PR body/comments include live DeepSeek V4 Pro proof where a masked read-tool path is restored before execution. Review metrics: 1 noteworthy metric.
Root-cause cluster Members:
Proposal only: this assessment does not dispatch repair, suppress jobs, mutate sibling items, close, or merge anything. Merge readiness Overall follows the weaker of proof and patch quality, so missing proof can cap an otherwise strong patch. Rank-up moves:
Risk before merge
Maintainer options:
Next step before merge
Security Review detailsBest possible solution: Land one canonical repair that restores tool-call argument text after repair and before execution without rewriting tool names; this PR is the narrower viable candidate if maintainers accept the behavior scope. Do we have a high-confidence way to reproduce the issue? Yes. Current main source shows provider output transforms skip structured ToolCall.arguments, and the PR body/comments include live DeepSeek V4 Pro proof where a masked read-tool path is restored before execution. Is this the best way to solve the issue? Yes, with maintainer acceptance of the compatibility-sensitive scope. The existing stream text-transform wrapper plus post-repair output placement is the narrowest maintainable fix; the broader sibling PR remains an alternative to choose or close. AGENTS.md: found and applied where relevant. Codex review notes: model internal, reasoning high; reviewed against 5e572dcf781a. Label changesLabel justifications:
Evidence reviewedPR surface: Source +45, Tests +79. Total +124 across 2 files. View PR surface stats
What I checked:
Likely related people:
What the crustacean ranks mean
Shiny media proof means a screenshot, video, or linked artifact directly shows the changed behavior. Runtime, network, CSP, and security claims still need visible diagnostics. How this review workflow works
|
|
@clawsweeper re-review Fixed both P1s:
|
|
🦞🧹 I asked ClawSweeper to review this item again. |
|
@clawsweeper re-review Fixed P1: result-path test fixture now contains an actual masked toolCall content block. The assertion previously tested against a plain text message that never contained toolCall blocks. |
|
🦞🧹 I asked ClawSweeper to review this item again. |
|
@clawsweeper re-review |
|
🦞🧹 I asked ClawSweeper to review this item again. |
|
@clawsweeper re-review Added:
|
|
@clawsweeper re-review Added real behavior proof: DeepSeek V4 Pro API call demonstrates the complete end-to-end pipeline:
|
…call_end events toolcall_delta and toolcall_end events bypassed the output replacement pipeline, leaking masked tokens (e.g. PII placeholders) into tool call arguments and external systems. Closes openclaw#97761
…ents Add recursive transformToolCallArgumentText to handle strings, arrays, and nested objects in tool call arguments, not just the name field.
- Remove toolCall.name transformation to prevent breaking tool routing - Add arguments transform to stream.result()/done.message via transformContentText for tool-call content blocks
Only transform arguments on content blocks with type=toolCall to avoid touching non-tool-call blocks that happen to have an arguments field.
…lock
The previous fixture used makeAssistantMessage('final') which produces
a plain text content block, not a toolCall block. The stream.result()
assertion was vacuously passing.
ff462d3 to
317e4ac
Compare
|
Maintainer repair and pre-merge proof are complete on exact head What changed during prepare:
Proof:
Maintainer judgment: the compatibility-sensitive expansion is intentional and bounded to structured tool-call argument string values. Contributor live DeepSeek proof covers the external execution path; I did not repeat that credentialed provider run locally. |
|
Merged via squash.
|
…call_end events (openclaw#97769) * fix(plugins): apply output text transforms to toolcall_delta and toolcall_end events toolcall_delta and toolcall_end events bypassed the output replacement pipeline, leaking masked tokens (e.g. PII placeholders) into tool call arguments and external systems. Closes openclaw#97761 * fix(plugins): transform nested tool call arguments in output replacements Add recursive transformToolCallArgumentText to handle strings, arrays, and nested objects in tool call arguments, not just the name field. * fix(plugins): keep tool names unchanged and cover result path - Remove toolCall.name transformation to prevent breaking tool routing - Add arguments transform to stream.result()/done.message via transformContentText for tool-call content blocks * fix(plugins): narrow argument transform to toolCall content blocks only Only transform arguments on content blocks with type=toolCall to avoid touching non-tool-call blocks that happen to have an arguments field. * test(plugins): assert stream.result() tool-call content block is transformed * test(plugins): fix result-path fixture to actually contain toolCall block The previous fixture used makeAssistantMessage('final') which produces a plain text content block, not a toolCall block. The stream.result() assertion was vacuously passing. * style(plugins): fix type cast in result-path test assertion * fix(agents): preserve tool argument transforms after repair * refactor(agents): keep tool transform boundary narrow --------- Co-authored-by: Peter Steinberger <steipete@golden-gate.local>
…call_end events (openclaw#97769) * fix(plugins): apply output text transforms to toolcall_delta and toolcall_end events toolcall_delta and toolcall_end events bypassed the output replacement pipeline, leaking masked tokens (e.g. PII placeholders) into tool call arguments and external systems. Closes openclaw#97761 * fix(plugins): transform nested tool call arguments in output replacements Add recursive transformToolCallArgumentText to handle strings, arrays, and nested objects in tool call arguments, not just the name field. * fix(plugins): keep tool names unchanged and cover result path - Remove toolCall.name transformation to prevent breaking tool routing - Add arguments transform to stream.result()/done.message via transformContentText for tool-call content blocks * fix(plugins): narrow argument transform to toolCall content blocks only Only transform arguments on content blocks with type=toolCall to avoid touching non-tool-call blocks that happen to have an arguments field. * test(plugins): assert stream.result() tool-call content block is transformed * test(plugins): fix result-path fixture to actually contain toolCall block The previous fixture used makeAssistantMessage('final') which produces a plain text content block, not a toolCall block. The stream.result() assertion was vacuously passing. * style(plugins): fix type cast in result-path test assertion * fix(agents): preserve tool argument transforms after repair * refactor(agents): keep tool transform boundary narrow --------- Co-authored-by: Peter Steinberger <steipete@golden-gate.local>
…call_end events (openclaw#97769) * fix(plugins): apply output text transforms to toolcall_delta and toolcall_end events toolcall_delta and toolcall_end events bypassed the output replacement pipeline, leaking masked tokens (e.g. PII placeholders) into tool call arguments and external systems. Closes openclaw#97761 * fix(plugins): transform nested tool call arguments in output replacements Add recursive transformToolCallArgumentText to handle strings, arrays, and nested objects in tool call arguments, not just the name field. * fix(plugins): keep tool names unchanged and cover result path - Remove toolCall.name transformation to prevent breaking tool routing - Add arguments transform to stream.result()/done.message via transformContentText for tool-call content blocks * fix(plugins): narrow argument transform to toolCall content blocks only Only transform arguments on content blocks with type=toolCall to avoid touching non-tool-call blocks that happen to have an arguments field. * test(plugins): assert stream.result() tool-call content block is transformed * test(plugins): fix result-path fixture to actually contain toolCall block The previous fixture used makeAssistantMessage('final') which produces a plain text content block, not a toolCall block. The stream.result() assertion was vacuously passing. * style(plugins): fix type cast in result-path test assertion * fix(agents): preserve tool argument transforms after repair * refactor(agents): keep tool transform boundary narrow --------- Co-authored-by: Peter Steinberger <steipete@golden-gate.local>
…call_end events (openclaw#97769) * fix(plugins): apply output text transforms to toolcall_delta and toolcall_end events toolcall_delta and toolcall_end events bypassed the output replacement pipeline, leaking masked tokens (e.g. PII placeholders) into tool call arguments and external systems. Closes openclaw#97761 * fix(plugins): transform nested tool call arguments in output replacements Add recursive transformToolCallArgumentText to handle strings, arrays, and nested objects in tool call arguments, not just the name field. * fix(plugins): keep tool names unchanged and cover result path - Remove toolCall.name transformation to prevent breaking tool routing - Add arguments transform to stream.result()/done.message via transformContentText for tool-call content blocks * fix(plugins): narrow argument transform to toolCall content blocks only Only transform arguments on content blocks with type=toolCall to avoid touching non-tool-call blocks that happen to have an arguments field. * test(plugins): assert stream.result() tool-call content block is transformed * test(plugins): fix result-path fixture to actually contain toolCall block The previous fixture used makeAssistantMessage('final') which produces a plain text content block, not a toolCall block. The stream.result() assertion was vacuously passing. * style(plugins): fix type cast in result-path test assertion * fix(agents): preserve tool argument transforms after repair * refactor(agents): keep tool transform boundary narrow --------- Co-authored-by: Peter Steinberger <steipete@golden-gate.local>
Closes #97761
What Problem This Solves
Plugin
textTransforms.outputrestored normal assistant text but skipped structured tool-call arguments. A model could therefore return a masked placeholder in a tool call and OpenClaw would execute or deliver that placeholder through Slack, Google Chat, email, or another external tool.Why This Change Was Made
The existing provider stream text-transform wrapper is the narrow owner boundary. This change:
ToolCall.arguments;This avoids the broader alternative of adding a new agent-loop finalizer API.
User Impact
Plugins that mask or rewrite provider-visible text can now rely on restored tool arguments before OpenClaw executes tools. Placeholder tokens should no longer leak into Slack messages, file paths, or other external tool inputs.
Evidence
Focused tests:
The regression covers a placeholder split across two
toolcall_deltachunks, reconstructed by the malformed-argument repair wrapper, and restored in the structured end/result arguments.Documentation and formatting:
Fresh repository autoreview after the maintainer repair:
Contributor live proof used DeepSeek V4 Pro with
"John Smith"masked to[MASKED]; the model returned a maskedreadtool call, the output transform restoredJohn Smith.txt, and the file read succeeded.Security Impact