Skip to content

fix(msteams): sanitize internal tool-trace lines from outbound text (#90684)#95867

Open
jailbirt wants to merge 1 commit into
openclaw:mainfrom
jailbirt:fix/msteams-sanitize-outbound-90684
Open

fix(msteams): sanitize internal tool-trace lines from outbound text (#90684)#95867
jailbirt wants to merge 1 commit into
openclaw:mainfrom
jailbirt:fix/msteams-sanitize-outbound-90684

Conversation

@jailbirt

@jailbirt jailbirt commented Jun 22, 2026

Copy link
Copy Markdown
Contributor

What Problem This Solves

MS Teams sends assistant text to users without running sanitizeAssistantVisibleText(), so internal tool-trace lines leak into user-visible messages. The most visible case is the ⚠️ 🛠️ \ (agent)` failedbanner produced for a benign non-zero shell exit (e.g. agrep` that finds no match → exit 1). The turn completes successfully, but the user receives an alarming "failed" message from the bot.

msteamsChannelOutbound had no sanitizeText hook at all. This is the MS Teams instance of #90684. Discord, Google Chat (#95084, merged) and WhatsApp already run the canonical sanitizer on outbound; MS Teams was missing it.

Closes part of #90684. Related: #88332, #95084 (the merged Google Chat counterpart, identical fix shape).

Why This Change Was Made

Add sanitizeText: ({ text }) => sanitizeAssistantVisibleText(text) to msteamsChannelOutbound, reusing the canonical delivery sanitizer the other channels already use (rather than new regexes). The hook runs in the shared outbound deliver path, so it covers every text send — replies, sub-agent announces, and cron deliveries. msteamsChannelOutbound is now exported so the hook can be unit-tested (mirrors googlechatOutboundAdapter).

Out of scope: the Codex-native warning taxonomy (#88332) and threading/attachments. This is one focused MS Teams instance of the cross-channel umbrella.

User Impact

Internal trace/failure banners (e.g. ⚠️ 🛠️ … (agent) failed from a benign grep no-match) are no longer shown to MS Teams users. Legitimate assistant prose is unaffected. No config, auth, secrets, network, or tool-execution behavior changes.

Evidence

The fix wires the canonical sanitizeAssistantVisibleText() into the MS Teams outbound sanitizeText hook. Below is copied live output / console output (stdout) from running that exact function via node on a live OpenClaw 2026.6.8 deployment, with the leaked banner as input:

IN : "Listo, ya lo cargué.\n⚠️ 🛠️ `grep -E (PV-123) /tmp/out.txt (agent)` failed"
OUT: "Listo, ya lo cargué."

The banner line is removed; the real assistant prose is preserved.

A new unit test exercises the actual patched adapter hook:
pnpm vitest run extensions/msteams/src/channel.message-adapter.test.ts -t "sanitizeText"2 passed (msteams outbound sanitizeText): asserts msteamsChannelOutbound.sanitizeText strips the (agent) failed banner and leaves ordinary prose untouched.

Proof limitations: the MS Teams deployment available to me runs 2026.5.28, which predates the banner machinery (it does not generate the (agent) failed banner — (agent)/🛠️ are absent from its payload builder), so it cannot reproduce the end-to-end leak. The after-fix evidence therefore exercises the exact canonical sanitizer this PR wires into MS Teams, captured on a live 2026.6.8 deployment, plus the adapter-hook unit test. No MS Teams client screenshot; private space/account/user details omitted.

Risk

Highest-risk area is over-stripping legitimate content. Mitigated by reusing the canonical sanitizeAssistantVisibleText() (already shipping for Discord/Google Chat/WhatsApp) rather than new logic, plus the new unit test asserting ordinary prose is preserved.

AI-assisted change.


Real behavior proof

Local run against this branch, pnpm 11.2.2 (frozen lockfile), vitest.

Validation (re-runnable)

pnpm install --frozen-lockfile
node scripts/run-vitest.mjs run extensions/msteams/src/channel.message-adapter.test.ts -t "sanitizeText"
#  Test Files 1 passed (1)   Tests 2 passed | 2 skipped (4)
# full file: Tests 4 passed (4)

Before/after (patched MS Teams outbound path)

Input (identical both runs):

Here is your summary.
⚠️ 🛠️ `grep -E 'PV-123' /tmp/out.txt (agent)` failed
The report is ready.

A repro drives the real deliver.ts sanitize gate; BEFORE = adapter sanitizeText undefined (main), AFTER = the patched msteamsChannelOutbound.sanitizeText export.

[BEFORE main]  OUT: "Here is your summary.\n⚠️ 🛠️ `grep -E 'PV-123' /tmp/out.txt (agent)` failed\nThe report is ready."
[AFTER patch]  OUT: "Here is your summary.\nThe report is ready."

BEFORE: the ⚠️ 🛠️ … (agent) failed banner survives into outbound text. AFTER: the banner line is removed, both prose lines preserved.

Why main leaks

src/infra/outbound/deliver.ts applies handler.sanitizeText only when present (:795). On main msteamsChannelOutbound has no sanitizeText, so the shared gate is skipped and text passes through verbatim. This PR adds sanitizeText: ({ text }) => sanitizeAssistantVisibleText(text) at extensions/msteams/src/channel.ts:444, mirroring the merged Google Chat fix #95084. Canonical sanitizer: src/shared/text/assistant-visible-text.tsstripAssistantInternalTraceLines().

Proof limitation (consistent with the PR's existing note): the AFTER side exercises the hook through a faithful re-implementation of the deliver.ts gate (the real gate is module-internal, not exported), not a live MS Teams socket send.

@clawsweeper

clawsweeper Bot commented Jun 23, 2026

Copy link
Copy Markdown
Contributor

Codex review: needs real behavior proof before merge. Reviewed July 13, 2026, 1:40 PM ET / 17:40 UTC.

Summary
The branch exports the MS Teams outbound adapter, applies sanitizeAssistantVisibleText() through its sanitizeText hook, and adds focused tests for stripping a failed-tool banner while preserving ordinary prose.

PR surface: Source +5, Tests +18. Total +23 across 2 files.

Reproducibility: yes. Current main shows the MS Teams adapter lacks sanitizeText, and the shared delivery path only invokes channel sanitization when that hook exists. (github.com)

Review metrics: none identified.

Root-cause cluster
Relationship: fixed_by_candidate
Canonical: #90684
Summary: The umbrella issue is canonical; this PR is a Microsoft Teams candidate fix, while another open Teams PR requires comparison before selecting a landing branch.

Members:

Proposal only: this assessment does not dispatch repair, suppress jobs, mutate sibling items, close, or merge anything.

Merge readiness
Overall: 🦪 silver shellfish
Proof: 🦐 gold shrimp
Patch quality: 🦪 silver shellfish
Result: blocked until stronger real behavior proof is added.

Overall follows the weaker of proof and patch quality, so missing proof can cap an otherwise strong patch.

Rank-up moves:

Proof guidance:

  • [P1] Needs stronger real behavior proof before merge: The submitted output proves the older branch/deployment sanitizer behavior, but it does not prove the proposed hook after rebasing onto the current sanitizer contract; add current-main before/after terminal output or a real Teams round-trip and redact private identifiers or credentials. After adding proof, update the PR body; ClawSweeper should re-review automatically. If it does not, the PR author or someone with repository write access can comment @clawsweeper re-review.

Risk before merge

  • [P1] Merging after a routine rebase without updating the sanitizer dependency would leave the reported ⚠️ 🛠️ … (agent) failed text visible because the current shared sanitizer no longer removes that line. (github.com)
  • [P1] Changing outbound sanitization can suppress legitimate MS Teams prose if the replacement helper is broader than the established delivery contract.
  • [P1] Another open MS Teams PR appears to target the same behavior, creating duplicate or conflicting implementations unless maintainers select one landing path first.
  • [P1] Three exact-head checks remain failed without logs proving they are unrelated.

Maintainer options:

  1. Repair against the current sanitizer contract (recommended)
    Rebase and update the hook or shared sanitizer so a current-main regression test proves the target banner is removed while normal prose remains unchanged.
  2. Use the stronger overlapping implementation
    If fix(msteams): strip internal tool-trace banners from outbound text #103692 already handles the current sanitizer contract with stronger proof, preserve useful context here and proceed with only that branch.

Next step before merge

  • [P1] A maintainer should compare the overlapping Teams branches and confirm the current shared sanitizer owner before the contributor repairs and reproves one landing path.

Security
Cleared: The diff adds an existing SDK import, adapter hook, export, and tests without changing dependencies, secrets, permissions, workflows, downloaded artifacts, or package resolution.

Review findings

  • [P1] Target the current trace-line sanitizer contract — extensions/msteams/src/channel.ts:488
Review details

Best possible solution:

Rebase onto current main, use or restore a canonical shared trace-line sanitizer that demonstrably removes the failed-tool banner without channel-specific regexes, exercise it through the real outbound normalization path, and reconcile the overlapping MS Teams PR before landing.

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

Yes. Current main shows the MS Teams adapter lacks sanitizeText, and the shared delivery path only invokes channel sanitization when that hook exists. (github.com)

Is this the best way to solve the issue?

No, not at the current base. The adapter seam is correct, but the selected current-main sanitizer no longer removes the reported trace line; the fix must target the current shared contract and prove the real normalization result. (github.com)

Full review comments:

  • [P1] Target the current trace-line sanitizer contract — extensions/msteams/src/channel.ts:488
    After rebasing, this call no longer removes the banner named by the PR: current sanitizeAssistantVisibleText() only applies reasoning/model-token/XML and related scaffolding stages, with no failed-tool trace-line stage. The new hook would therefore leave ⚠️ 🛠️ … (agent) failed unchanged. Use the current canonical trace-line helper, or restore that behavior at the shared owner boundary, and add a current-main outbound-normalization regression test.
    Confidence: 0.97

Overall correctness: patch is incorrect
Overall confidence: 0.95

AGENTS.md: found and applied where relevant.

Codex review notes: model internal, reasoning high; reviewed against 3c417f791cc5.

Label changes

Label changes:

  • add status: 📣 needs proof: The PR needs real behavior proof before ClawSweeper can clear the contributor ask. Needs stronger real behavior proof before merge: The submitted output proves the older branch/deployment sanitizer behavior, but it does not prove the proposed hook after rebasing onto the current sanitizer contract; add current-main before/after terminal output or a real Teams round-trip and redact private identifiers or credentials. After adding proof, update the PR body; ClawSweeper should re-review automatically. If it does not, the PR author or someone with repository write access can comment @clawsweeper re-review.
  • remove proof: sufficient: Current real behavior proof status is insufficient, not sufficient.
  • remove status: ⏳ waiting on author: Current PR status label is status: 📣 needs proof.

Label justifications:

  • P2: This is a bounded MS Teams outbound-text bug with meaningful user-facing leakage but no evidence of outage, data loss, or unusable core runtime.
  • merge-risk: 🚨 message-delivery: The patch changes text immediately before delivery, and its current sanitizer dependency no longer performs the specific removal the PR promises.
  • rating: 🦪 silver shellfish: Overall readiness is 🦪 silver shellfish; proof is 🦐 gold shrimp and patch quality is 🦪 silver shellfish.
  • status: 📣 needs proof: The PR needs real behavior proof before ClawSweeper can clear the contributor ask. Needs stronger real behavior proof before merge: The submitted output proves the older branch/deployment sanitizer behavior, but it does not prove the proposed hook after rebasing onto the current sanitizer contract; add current-main before/after terminal output or a real Teams round-trip and redact private identifiers or credentials. After adding proof, update the PR body; ClawSweeper should re-review automatically. If it does not, the PR author or someone with repository write access can comment @clawsweeper re-review.
Evidence reviewed

PR surface:

Source +5, Tests +18. Total +23 across 2 files.

View PR surface stats
Area Files Added Removed Net
Source 1 6 1 +5
Tests 1 19 1 +18
Docs 0 0 0 0
Config 0 0 0 0
Generated 0 0 0 0
Other 0 0 0 0
Total 2 25 2 +23

What I checked:

Likely related people:

  • jailbirt: Authored the merged Google Chat implementation of the same outbound-adapter sanitizer pattern, giving them direct history with this behavior beyond the current proposal. (role: sibling sanitizer contributor; confidence: high; commits: 696c62400803; files: extensions/googlechat/src/channel.ts)
  • ZengWen-DT: Authored the merged Mattermost fix for the same root cause and added handler-to-transport regression coverage that is relevant to the corrected MS Teams proof shape. (role: adjacent channel contributor; confidence: medium; commits: 1ef45448712f; files: extensions/mattermost/src/channel.ts, extensions/mattermost/src/outbound-delivery.test.ts)
What the crustacean ranks mean
  • 🦀 challenger crab: rare, exceptional readiness with strong proof, clean implementation, and convincing validation.
  • 🦞 diamond lobster: very strong readiness with only minor maintainer review expected.
  • 🐚 platinum hermit: good normal PR, likely mergeable with ordinary maintainer review.
  • 🦐 gold shrimp: useful signal, but proof or patch confidence is still limited.
  • 🦪 silver shellfish: thin signal; proof, validation, or implementation needs work.
  • 🧂 unranked krab: not merge-ready because proof is missing/unusable or there are serious correctness or safety concerns.
  • 🌊 off-meta tidepool: rating does not apply to this item.

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 keeps one durable marker-backed review comment per issue or PR.
  • Re-runs edit this comment so the latest verdict, findings, and automation markers stay together instead of adding duplicate bot comments.
  • A fresh review can be triggered by eligible @clawsweeper re-review comments, exact-item GitHub events, scheduled/background review runs, or manual workflow dispatch.
  • PR/issue authors and users with repository write access can comment @clawsweeper re-review or @clawsweeper re-run on an open PR or issue to request a fresh review only.
  • Maintainers can also comment @clawsweeper review to request a fresh review only.
  • Fresh-review commands do not start repair, autofix, rebase, CI repair, or automerge.
  • Maintainer-only repair and merge flows require explicit commands such as @clawsweeper autofix, @clawsweeper automerge, @clawsweeper fix ci, or @clawsweeper address review.
  • Maintainers can comment @clawsweeper explain to ask for more context, or @clawsweeper stop to stop active automation.
Review history (1 earlier review cycle)
  • reviewed 2026-06-28T17:03:03.543Z sha 13a5ad7 :: needs changes before merge. :: [P2] Pass a payload to sanitizeText in the tests | [P3] Remove the literal-only concatenation

@clawsweeper clawsweeper Bot added rating: 🧂 unranked krab Not merge-ready due to missing proof or serious correctness/safety concerns. status: 📣 needs proof The PR needs real behavior proof before ClawSweeper can clear the contributor ask. P2 Normal backlog priority with limited blast radius. merge-risk: 🚨 message-delivery 🚨 May drop, duplicate, misroute, suppress, or wrongly target messages. labels Jun 23, 2026
@jailbirt

Copy link
Copy Markdown
Contributor Author

@clawsweeper re-review

@clawsweeper

clawsweeper Bot commented Jun 28, 2026

Copy link
Copy Markdown
Contributor

🦞🧹
ClawSweeper re-review requested.

I asked ClawSweeper to review this item again.
Action: item re-review queued (workflow sweep.yml, event repository_dispatch).
Result: the existing ClawSweeper review comment will be edited in place when the review finishes.

@clawsweeper clawsweeper Bot added proof: sufficient ClawSweeper judged the real behavior proof convincing. rating: 🦪 silver shellfish Thin PR readiness signal; proof, validation, or implementation needs work. status: ⏳ waiting on author ClawSweeper has contributor-facing work open and is waiting for author action. and removed rating: 🧂 unranked krab Not merge-ready due to missing proof or serious correctness/safety concerns. status: 📣 needs proof The PR needs real behavior proof before ClawSweeper can clear the contributor ask. labels Jun 28, 2026
jailbirt pushed a commit to jailbirt/openclaw that referenced this pull request Jul 13, 2026
…tests

Pass a ReplyPayload-compatible `payload` to the msteams outbound
`sanitizeText` test calls so the exact-head typecheck passes, and collapse
the literal-only string concatenation flagged by lint into a single string.

Addresses the ClawSweeper review findings on openclaw#95867 (P2 payload / P3 concat).

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
@jailbirt

Copy link
Copy Markdown
Contributor Author

Pushed a fix for the ClawSweeper findings: the msteams outbound sanitizeText test calls now pass a ReplyPayload-compatible payload (satisfies the { text; payload } param type at exact head), and the literal-only string concatenation is collapsed into a single string. Local: test 2/2 green, oxlint clean, tsgo:extensions:test clean.

@clawsweeper re-review

@clawsweeper

clawsweeper Bot commented Jul 13, 2026

Copy link
Copy Markdown
Contributor

🦞🧹
ClawSweeper re-review requested.

I asked ClawSweeper to review this item again.
Action: item re-review queued (workflow sweep.yml, event repository_dispatch).
Result: the existing ClawSweeper review comment will be edited in place when the review finishes.

Re-review progress:

…penclaw#90684)

Rebased onto current main. Adds a sanitizeAssistantVisibleText() hook to the
msteams outbound adapter so internal failed-tool trace banners are stripped
before delivery (mirrors the merged Google Chat fix openclaw#95084). The adapter tests
pass a ReplyPayload-compatible payload and use a single string literal so the
exact-head typecheck and lint pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
@jailbirt
jailbirt force-pushed the fix/msteams-sanitize-outbound-90684 branch from 5961c42 to 01243cc Compare July 13, 2026 16:19
@jailbirt

Copy link
Copy Markdown
Contributor Author

Also rebased onto current main so it no longer carries stale-base CI drift (check-dependencies/check-guards/scan-scope). checks-fast-loc-ratchet passes for this PR. Test 2/2 + lint clean locally.

@clawsweeper clawsweeper Bot added status: 📣 needs proof The PR needs real behavior proof before ClawSweeper can clear the contributor ask. and removed proof: sufficient ClawSweeper judged the real behavior proof convincing. status: ⏳ waiting on author ClawSweeper has contributor-facing work open and is waiting for author action. labels Jul 13, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

channel: msteams Channel integration: msteams merge-risk: 🚨 message-delivery 🚨 May drop, duplicate, misroute, suppress, or wrongly target messages. P2 Normal backlog priority with limited blast radius. rating: 🦪 silver shellfish Thin PR readiness signal; proof, validation, or implementation needs work. size: XS status: 📣 needs proof The PR needs real behavior proof before ClawSweeper can clear the contributor ask.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants