Skip to content

fix: strip adjacent function_response sanitizer leak#82155

Merged
steipete merged 2 commits into
mainfrom
codex/fix-function-response-sanitizer-leak
May 15, 2026
Merged

fix: strip adjacent function_response sanitizer leak#82155
steipete merged 2 commits into
mainfrom
codex/fix-function-response-sanitizer-leak

Conversation

@steipete
Copy link
Copy Markdown
Contributor

@steipete steipete commented May 15, 2026

Summary

Fixes the remaining user-visible sanitizer leak where a stripped XML tool-call block can expose an immediately adjacent <function_response> block.

Root cause: stripToolCallXmlTags() already removed high-confidence <function_calls> / <tool_call> scaffolding, but it did not carry forward the fact that the next tag is adjacent to just-stripped internal workflow XML. That meant <function_response> could be evaluated as ordinary text after the preceding block disappeared, especially for compact same-line forms, chained response blocks, and dangling response starts.

This patch tracks the end of the last stripped XML tool-call block and strips adjacent <function_response> blocks across whitespace. It covers:

  • multiline adjacent responses after <function_calls>
  • same-line compact responses
  • leading-space same-line response payloads
  • dangling adjacent responses
  • chained adjacent response blocks
  • same-line visible reply tails after a response block

It keeps the literal-prose side tight: inline examples still preserve ordinary <function_response> mentions, and a narrow tag-explanation exception preserves prose such as <function_response> is the response wrapper after a stripped example block. The exception no longer treats generic prose-looking payloads such as is enabled as prose, because adjacent workflow output must strip even when output text starts like a sentence.

Fixes #47444.

Real behavior proof

Behavior addressed: adjacent <function_response> workflow output no longer survives after stripped XML tool-call scaffolding.

Real environment tested: local OpenClaw checkout on the patched branch, running the production sanitizeUserFacingText() path through node --import tsx.

Exact steps or command run after this patch: node --import tsx - <<'EOF' ... sanitizeUserFacingText(...) ... EOF

Evidence after fix: terminal output from the patched sanitizer probe:

case=adjacent multiline response
"After"
case=leading-space prose-like payload
"After"
case=literal wrapper prose
"<function_response> is the response wrapper; close it with </function_response>."

Observed result after fix: adjacent multiline and leading-space prose-like <function_response> payloads are removed from user-facing output, while literal wrapper explanation prose remains visible.

What was not tested: no live provider workflow was run; this is a deterministic text sanitizer change exercised through the production sanitizer function.

Verification

  • pnpm test src/shared/text/assistant-visible-text.test.ts src/agents/pi-embedded-helpers.sanitizeuserfacingtext.test.ts -- --reporter=verbose (203 tests passed)
  • git diff --check
  • /Users/steipete/Projects/agent-scripts/skills/codex-review/scripts/codex-review --mode local --full-access --output /tmp/codex-review-47444-r4.txt --parallel-tests "pnpm test src/shared/text/assistant-visible-text.test.ts src/agents/pi-embedded-helpers.sanitizeuserfacingtext.test.ts -- --reporter=dot" (clean, no accepted/actionable findings)
  • pnpm check:changed (Blacksmith Testbox tbx_01krnwp12a74pe8pvayaprmy7z, GitHub Actions run https://github.com/openclaw/openclaw/actions/runs/25920010654, exit 0)

@openclaw-barnacle openclaw-barnacle Bot added agents Agent runtime and tooling size: M maintainer Maintainer-authored PR labels May 15, 2026
@steipete steipete force-pushed the codex/fix-function-response-sanitizer-leak branch from b56868e to 83e9aa9 Compare May 15, 2026 13:30
@clawsweeper
Copy link
Copy Markdown
Contributor

clawsweeper Bot commented May 15, 2026

Codex review: needs maintainer review before merge.

Summary
The PR extends shared assistant-visible text sanitization to strip <function_response> blocks adjacent to stripped XML tool-call scaffolding, adds regression tests, and adds a changelog entry.

Reproducibility: yes. at source level. Current main strips standalone <function_response> blocks but does not carry forward stripped-block adjacency, while user-facing replies call that shared sanitizer with plural function-call XML stripping enabled.

Real behavior proof
Sufficient (terminal): The PR body includes after-fix terminal output from the production sanitizeUserFacingText() path showing the adjacent leak removed and literal wrapper prose preserved.

Next step before merge
No repair lane is appropriate because there are no actionable patch defects; the remaining action is maintainer review and normal merge handling for a protected-label PR.

Security
Cleared: The diff changes sanitizer code, focused tests, and changelog text only; it adds no dependencies, workflows, scripts, permissions, downloads, or secret-handling surface.

Review details

Best possible solution:

Land the shared-sanitizer fix after maintainer review and normal merge gates, keeping the cleanup channel-agnostic and preserving literal-prose safeguards.

Do we have a high-confidence way to reproduce the issue?

Yes, at source level. Current main strips standalone <function_response> blocks but does not carry forward stripped-block adjacency, while user-facing replies call that shared sanitizer with plural function-call XML stripping enabled.

Is this the best way to solve the issue?

Yes. Extending the shared sanitizer to track stripped-block adjacency is narrower and more maintainable than adding channel-specific cleanup around final reply delivery.

What I checked:

  • Current main leaves the adjacent response case unhandled: On current main, stripToolCallXmlTags() strips <function_response> only when isStandaloneOpeningTagLine(...) is true, so an immediately adjacent response tag after a stripped <function_calls> block is not covered by the existing condition. (src/shared/text/assistant-visible-text.ts:311, 9c389487002c)
  • User-facing replies use this shared sanitizer path: sanitizeUserFacingText() delegates to stripToolCallXmlTags(..., { stripFunctionCallsXmlPayloads: true }), so the shared text sanitizer is the right ownership point for user-visible reply leaks. (src/agents/pi-embedded-helpers/sanitize-user-facing-text.ts:409, 9c389487002c)
  • PR implements adjacency-aware stripping: The PR head tracks lastStrippedToolCallBlockEnd, tests whitespace adjacency, finds matching response close tags, and uses that state in shouldStripAdjacentResult. (src/shared/text/assistant-visible-text.ts:374, a52e7f2c2457)
  • PR adds focused regression coverage: The PR adds shared sanitizer and sanitizeUserFacingText() tests for adjacent multiline, compact, dangling, chained, prose-like, and visible-tail <function_response> cases. (src/shared/text/assistant-visible-text.test.ts:615, a52e7f2c2457)
  • Real behavior proof is present: The PR body includes after-fix terminal output from the production sanitizeUserFacingText() path showing adjacent response payloads stripped while literal wrapper prose remains visible; the PR also carries proof: sufficient. (a52e7f2c2457)
  • Relevant history shows this is a follow-up to earlier sanitizer work: Commit b180b8ae48335b698ab5f6887192875e9151f86d added standalone workflow <function_response> stripping, while 7ff90c516a6321ea004b3a771242997ff195bbf7 introduced the shared XML tool-call stripping path this PR extends. (src/shared/text/assistant-visible-text.ts:20, b180b8ae4833)

Likely related people:

  • steipete: Current blame and GitHub commit history tie the shared sanitizer and the prior standalone <function_response> fix to this person, and this PR extends that same path. (role: recent area contributor; confidence: high; commits: b180b8ae4833, d89732efca61, a52e7f2c2457; files: src/shared/text/assistant-visible-text.ts, src/agents/pi-embedded-helpers/sanitize-user-facing-text.ts, src/agents/pi-embedded-helpers.sanitizeuserfacingtext.test.ts)
  • oliviareid-svg: Authored the merged predecessor sanitizer work that introduced shared XML tool-call stripping, which is the path this PR extends. (role: related sanitizer contributor; confidence: medium; commits: 7ff90c516a63; files: src/shared/text/assistant-visible-text.ts, src/shared/text/assistant-visible-text.test.ts)
  • vincentkoc: Authored the commit that extracted the user-facing sanitizer module now delegating into the shared XML stripping logic. (role: sanitizer extraction contributor; confidence: medium; commits: 35664d5447a4; files: src/agents/pi-embedded-helpers/sanitize-user-facing-text.ts, src/agents/pi-embedded-helpers.ts)
  • joelnishanth: Authored the current-main follow-up for standalone <function> XML stripping in the same shared assistant-visible sanitizer surface. (role: adjacent sanitizer contributor; confidence: medium; commits: 78df859e15ac; files: src/shared/text/assistant-visible-text.ts, src/shared/text/assistant-visible-text.test.ts)

Remaining risk / open question:

  • No live provider or Telegram workflow was run in this review; the confidence comes from source inspection, focused regression coverage, and the PR's deterministic sanitizer proof.

Codex review notes: model gpt-5.5, reasoning high; reviewed against 9c389487002c.

Re-review progress:

@clawsweeper clawsweeper Bot added the proof: sufficient ClawSweeper judged the real behavior proof convincing. label May 15, 2026
@steipete steipete force-pushed the codex/fix-function-response-sanitizer-leak branch from 83e9aa9 to 5739e6e Compare May 15, 2026 14:15
@steipete steipete force-pushed the codex/fix-function-response-sanitizer-leak branch from 45ca0b8 to a52e7f2 Compare May 15, 2026 15:42
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: a52e7f2c24

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +370 to +373
const functionResponseCloseStart =
tag.tagName === "function_response"
? findMatchingToolCallCloseIndex(text, tag.end, tag.tagName)
: -1;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Avoid quadratic closing-tag scans per function_response tag

stripToolCallXmlTags() now calls findMatchingToolCallCloseIndex() for every <function_response> opening tag before checking whether adjacent-strip conditions even apply. That helper scans forward through the rest of the string each time, so messages containing many literal <function_response> examples (for example docs/tutorial-style output) become O(n²) to sanitize, which can add noticeable latency on user-visible reply paths. Gate the close-tag search behind the adjacency/standalone predicates (or reuse the existing single-pass state) to keep this pass linear.

Useful? React with 👍 / 👎.

@steipete
Copy link
Copy Markdown
Contributor Author

Verification for current head a52e7f2:

  • Local tests: node scripts/run-vitest.mjs src/shared/text/assistant-visible-text.test.ts src/agents/pi-embedded-helpers.sanitizeuserfacingtext.test.ts -- --reporter=verbose — 205 passed.
  • Whitespace: git diff --check origin/main...HEAD — clean.
  • Codex review: /Users/steipete/Projects/agent-scripts/skills/codex-review/scripts/codex-review --mode branch --base origin/main --full-access --output /tmp/codex-review-82155-rerun.txt --parallel-tests "node scripts/run-vitest.mjs src/shared/text/assistant-visible-text.test.ts src/agents/pi-embedded-helpers.sanitizeuserfacingtext.test.ts -- --reporter=verbose" — clean, no accepted/actionable findings.
  • Review fix added after the first Codex review finding: adjacent <function_response> payloads are stripped even when they contain explanation-like wording such as response wrapper.
  • GitHub Real behavior proof: https://github.com/openclaw/openclaw/actions/runs/25926897171 — success.

Known proof gap: broad GitHub CI/security gates are currently queued on GitHub runners for this head; auto-merge is armed and will merge only after required checks pass.

@steipete steipete merged commit 29b5563 into main May 15, 2026
88 of 93 checks passed
@steipete steipete deleted the codex/fix-function-response-sanitizer-leak branch May 15, 2026 15:48
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

agents Agent runtime and tooling maintainer Maintainer-authored PR proof: sufficient ClawSweeper judged the real behavior proof convincing. size: M

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Workflow responses leak internal <function_calls> scaffolding into user-visible chat

1 participant