Skip to content

fix(whatsapp): canonicalize outbound media delivery#69813

Merged
mcaxtr merged 6 commits into
mainfrom
refactor/whatsapp-canonical-outbound-media
Apr 24, 2026
Merged

fix(whatsapp): canonicalize outbound media delivery#69813
mcaxtr merged 6 commits into
mainfrom
refactor/whatsapp-canonical-outbound-media

Conversation

@mcaxtr

@mcaxtr mcaxtr commented Apr 21, 2026

Copy link
Copy Markdown
Member

Summary

Describe the problem and fix in 2–5 bullets:

  • Problem: WhatsApp outbound media behavior had split ownership across extensions/whatsapp/src/channel-outbound.ts, extensions/whatsapp/src/outbound-adapter.ts, extensions/whatsapp/src/send.ts, and extensions/whatsapp/src/auto-reply/deliver-reply.ts, so media fixes kept landing in one path while another path still drifted.
  • Why it matters: gateway/channel sends, payload sends, and auto-reply sends could disagree on media detection, source selection, caption placement, retry behavior, and fallback behavior, causing dropped media, text downgrades, duplicate sends, or inconsistent delivery.
  • What changed in WhatsApp: introduced a canonical plugin-local outbound media contract in extensions/whatsapp/src/outbound-media-contract.ts and routed the outbound paths through it so media normalization, caption-first behavior, retry classification, logging shape, and listener/send resolution stay aligned.
  • What changed in the reply pipeline: preserved final assistant media directives through embedded-runner payload construction, made non-streaming captioned media use the final payload path once, kept media-only orphan blocks deliverable, and made media-drop fallback explicit without breaking NO_REPLY.
  • What did NOT change (scope boundary): transport-specific reply context handling still stays at the final send edge, and no gateway protocol changes were made.

Change Type (select all)

  • Bug fix
  • Feature
  • Refactor required for the fix
  • Docs
  • Security hardening
  • Chore/infra

Scope (select all touched areas)

  • Gateway / orchestration
  • Skills / tool execution
  • Auth / tokens
  • Memory / storage
  • Integrations
  • API / contracts
  • UI / DX
  • CI/CD / infra

Linked Issue/PR

Root Cause (if applicable)

  • Root cause: WhatsApp outbound media had overlapping behavior owners, and the embedded/final reply path could lose or duplicate assistant-generated media before it reached WhatsApp delivery.
  • Missing detection / guardrail: there was no parity regression suite forcing payload/gateway and auto-reply entrypoints to agree on media classification, caption-first behavior, retry behavior, fallback behavior, and single-send delivery.
  • Contributing context (if known): recent fixes addressed individual path-specific failures, but the multi-owner shape remained and kept allowing outbound-media drift to reappear.

Regression Test Plan (if applicable)

  • Coverage level that should have caught this:
    • Unit test
    • Seam / integration test
    • End-to-end test
    • Existing coverage already sufficient
  • Target test or file:
    • extensions/whatsapp/src/channel-outbound.test.ts
    • extensions/whatsapp/src/send.test.ts
    • extensions/whatsapp/src/outbound-base.test.ts
    • extensions/whatsapp/src/outbound-adapter.sendpayload.test.ts
    • extensions/whatsapp/src/outbound-payload.contract.test.ts
    • extensions/whatsapp/src/auto-reply/deliver-reply.test.ts
    • src/agents/pi-embedded-runner/run/payloads.test.ts
    • src/auto-reply/reply/agent-runner-payloads.test.ts
    • src/auto-reply/reply/reply-delivery.test.ts
    • src/auto-reply/reply/reply-media-paths.test.ts
  • Scenario the test should lock in: the same outbound media fixture should produce the same classification, primary source selection, caption behavior, retry handling, and single-send delivery across gateway/payload and auto-reply entrypoints.
  • Why this is the smallest reliable guardrail: the regression spans WhatsApp send normalization and final reply payload assembly, so the tests cover both the plugin-local contract and the reply-pipeline points where media could be lost or sent twice.
  • Existing test that already covers this (if any): extensions/whatsapp/src/auto-reply.web-auto-reply.connection-and-logging.e2e.test.ts covers one media reply logging/send path at a higher level.
  • If no new test is added, why not: N/A

User-visible / Behavior Changes

  • WhatsApp outbound media delivery now behaves consistently across gateway/channel sends, payload sends, and auto-reply sends.
  • Assistant auto-replies that emit media directives keep the attachment through final payload delivery.
  • Non-streaming captioned media replies send once with the caption attached.
  • Explicit mediaUrl stays primary when both mediaUrl and mediaUrls are present.
  • The first media item gets the caption and later text remains ordered after media across the shared paths.
  • Wrapped transient WhatsApp send failures retry again instead of failing immediately.
  • Auto-reply fallback remains explicit on media failure and later attachments are still attempted.
  • Channel outbound keeps trimming leading blank lines but preserves intentional leading indentation.

Diagram (if applicable)

Before:
[gateway send] ----> [path A]
[payload send] ----> [path B]
[auto-reply]  ----> [path C]
                     |
                     v
           [different media normalization / caption / retry / fallback]

After:
[gateway send] ----\
[payload send] -----> [canonical outbound media contract] -> [final transport call]
[auto-reply]  ----/             |
                                v
                  [shared media normalization / caption / retry / fallback]

Security Impact (required)

  • New permissions/capabilities? (No)
  • Secrets/tokens handling changed? (No)
  • New/changed network calls? (No)
  • Command/tool execution surface changed? (No)
  • Data access scope changed? (No)
  • If any Yes, explain risk + mitigation: N/A

Repro + Verification

Environment

  • OS: Linux
  • Runtime/container: Node 22 / pnpm workspace
  • Model/provider: N/A
  • Integration/channel (if any): WhatsApp plugin
  • Relevant config (redacted): default test config and WhatsApp plugin test fixtures

Steps

  1. Send the same outbound media payload through the gateway/payload path and the auto-reply path.
  2. Include mixed mediaUrl and mediaUrls, a caption/text body, and a wrapped transient send error case.
  3. Exercise assistant-generated MEDIA: replies through the embedded/final reply path.
  4. Verify source selection, caption placement, retry behavior, fallback behavior, and number of send calls.

Expected

  • All WhatsApp outbound entrypoints agree on media presence, primary source, caption-first behavior, retry classification, and fallback behavior.
  • Assistant-generated media replies survive final payload construction and are delivered once.

Actual

  • They now agree through the canonical helper, with regressions added for mixed-field precedence, wrapped retry errors, later-media-after-fallback, channel indentation preservation, final media directive preservation, non-streaming single-send ownership, and media-drop fallback behavior.

Evidence

Attach at least one:

  • Failing test/log before + passing after
  • Trace/log snippets
  • Screenshot/recording
  • Perf numbers (if relevant)

Human Verification (required)

What you personally verified (not just CI), and how:

  • Verified scenarios:
    • payload/gateway vs auto-reply parity on shared media fixtures
    • mixed mediaUrl + mediaUrls primary-source behavior
    • wrapped transient error retry behavior
    • later media still sends after first-media fallback
    • channel outbound preserves intentional indentation
    • live WhatsApp DM media reply sends one image with one caption
    • live WhatsApp missing-media reply sends one explicit text fallback and no attachment
  • Edge cases checked:
    • blank/whitespace mediaUrls entries
    • OGG Opus normalization
    • text-only short-circuit behavior
    • named/default account-scoped listener resolution through the shared path
  • What you did not verify:
    • every numbered live scenario after the final review-only dedupe adjustment; targeted unit coverage was added for that review case

Review Conversations

  • I replied to or resolved every bot review conversation I addressed in this PR.
  • I left unresolved only the conversations that still need reviewer or maintainer judgment.

If a bot review conversation is addressed by this PR, resolve that conversation yourself. Do not leave bot review conversation cleanup for maintainers.

Compatibility / Migration

  • Backward compatible? (Yes)
  • Config/env changes? (No)
  • Migration needed? (No)
  • If yes, exact upgrade steps: N/A

Risks and Mitigations

  • Risk:
    • Canonicalizing behavior or moving non-streaming captioned media to the final path could accidentally change an entrypoint-specific edge case.
  • Mitigation:
    • Added parity regressions across gateway/payload and auto-reply plus focused unit coverage around source selection, retry classification, fallback behavior, channel normalization, final media preservation, and duplicate-send prevention.

@aisle-research-bot

aisle-research-bot Bot commented Apr 21, 2026

Copy link
Copy Markdown

🔒 Aisle Security Analysis

We found 2 potential security issue(s) in this PR:

# Severity Title
1 🟡 Medium Unsanitized filename derived from mediaUrl can enable CRLF/header injection in WhatsApp document upload
2 🟡 Medium Non-idempotent retry of WhatsApp auto-reply sends can cause duplicate messages/media
1. 🟡 Unsanitized filename derived from mediaUrl can enable CRLF/header injection in WhatsApp document upload
Property Value
Severity Medium
CWE CWE-113
Location extensions/whatsapp/src/outbound-media-contract.ts:93-105

Description

deriveWhatsAppDocumentFileName() derives a document fileName from a user-controlled mediaUrl and applies decodeURIComponent without sanitizing control characters or dangerous separators.

  • Input: mediaUrl is taken from outbound payloads/auto-reply media URLs.
  • Transformation: path.posix.basename(parsed.pathname) then decodeURIComponent(fileName).
  • Sink: The resulting fileName is passed into WhatsApp document send payloads (document messages) via Baileys (@​whiskeysockets/baileys) in createWebSendApi, which typically becomes a multipart/form-data filename/metadata field.

A crafted URL such as https://host/%0d%0aX-Injected:1.pdf would produce a filename containing CRLF (\r\n), and %2F can decode to /. If downstream libraries do not strictly validate/escape fileName, this can lead to header/metadata injection or request corruption.

Vulnerable code:

const fileName = path.posix.basename(parsed.pathname);
return fileName ? decodeURIComponent(fileName) : undefined;

Recommendation

Sanitize derived filenames before passing them to messaging libraries.

Suggested approach:

  • Strip CR/LF and other ASCII control chars.
  • Remove path separators (/, \\) after decoding.
  • Enforce a conservative allowlist for characters and a max length.
  • Optionally, fall back to a fixed name (e.g. file) when sanitization changes the value significantly.

Example:

function sanitizeFileName(name: string): string {// remove control chars incl. CR/LF and DEL
  let n = name.replace(/[\u0000-\u001F\u007F]/g, "");// remove path separators
  n = n.replace(/[\\/]/g, "_");// collapse whitespace
  n = n.replace(/\s+/g, " ").trim();// allowlist (tune as needed)
  n = n.replace(/[^A-Za-z0-9._ -]/g, "_");// enforce length
  if (!n) return "file";
  return n.slice(0, 128);
}

const raw = fileName ? decodeURIComponent(fileName) : undefined;
return raw ? sanitizeFileName(raw) : undefined;

Additionally, consider validating sendOptions.fileName in createWebSendApi (defense-in-depth) before using it in Baileys payloads.

2. 🟡 Non-idempotent retry of WhatsApp auto-reply sends can cause duplicate messages/media
Property Value
Severity Medium
CWE CWE-841
Location extensions/whatsapp/src/auto-reply/deliver-reply.ts:72-91

Description

deliverWebReply wraps msg.reply(...) and msg.sendMedia(...) with sendWhatsAppOutboundWithRetry, retrying on a broad transient-error regex (including wrapped errors via formatError). WhatsApp sends are not idempotent at this layer (no idempotency key / no de-dupe on messageId), so a failure after the remote side already accepted the message (e.g., connection reset/closed after send) can result in the same reply being sent multiple times.

Why this is a security/business risk:

  • Spam/billing amplification: an attacker can intentionally induce transient transport failures (or exploit flaky connectivity) while triggering auto-replies, producing duplicate outbound messages/media.
  • Inconsistent behavior: the codebase explicitly asserts elsewhere that outbound sends should not retry “to avoid duplicate sends”, but auto-reply now retries even for wrapped transient errors.

Vulnerable code (auto-reply retries non-idempotent operations):

const sendWithRetry = async (fn: () => Promise<unknown>, label: string, maxAttempts = 3) => {
  return await sendWhatsAppOutboundWithRetry({ send: fn, maxAttempts, ... });
};
...
await sendWithRetry(() => msg.reply(chunk, quote), "text");
...
await sendWithRetry(() => msg.sendMedia({ ... }, quote), "media:..." );

Retry classification is based on error text and can match cases where the send may have succeeded:

return /closed|reset|timed\s*out|disconnect/i.test(formatError(error));

Recommendation

Avoid retrying non-idempotent outbound sends unless you can guarantee idempotency.

Options:

  1. Remove retries for msg.reply/msg.sendMedia in the auto-reply path (match sendMessageWhatsApp behavior).
  2. Implement de-duplication/idempotency:
    • Generate a deterministic idempotency key per auto-reply chunk/media (e.g., hash of replyToId + chunkIndex + payloadHash).
    • Store keys in a short-lived cache and skip re-sends when a key was already successfully sent.
    • Or, if the underlying WhatsApp API/library returns a messageId before errors can occur, persist it and check delivery status before retrying.
  3. Tighten retry conditions to only retry on errors that can be proven to occur before a message is accepted (e.g., local validation/serialization failures), rather than generic network errors.

Example (no retry for sends):

// do not retry WhatsApp sends to avoid duplicates
await msg.reply(chunk, quote);

Example (idempotency cache sketch):

if (await sentCache.has(key)) return;
await msg.reply(chunk, quote);
await sentCache.set(key, true, { ttlMs: 5 * 60_000 });

Analyzed PR: #69813 at commit 5ce22bc

Last updated on: 2026-04-24T03:46:54Z

@openclaw-barnacle openclaw-barnacle Bot added channel: whatsapp-web Channel integration: whatsapp-web size: L maintainer Maintainer-authored PR labels Apr 21, 2026
@greptile-apps

greptile-apps Bot commented Apr 21, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR introduces outbound-media-contract.ts as the single source of truth for WhatsApp outbound media normalization (URL deduplication, OGG codec normalization, transient retry), then routes send.ts, outbound-base.ts/sendPayload, and deliver-reply.ts through it. Regression tests cover mixed mediaUrl+mediaUrls precedence, wrapped-error retry classification, post-failure media continuation, and gateway/auto-reply parity.

Confidence Score: 5/5

Safe to merge; one minor defensive-guard suggestion with no impact on normal callers.

All remaining findings are P2 style/robustness suggestions. The canonical contract is correctly wired across all three outbound paths, normalization is idempotent and covered by parity tests, and the wrapped-error retry path is verified by a dedicated test.

No files require special attention.

Prompt To Fix All With AI
This is a comment left during a code review.
Path: extensions/whatsapp/src/outbound-media-contract.ts
Line: 94-118

Comment:
**`throw lastError` can throw `undefined` when `maxAttempts ≤ 0`**

`lastError` is initialized to `undefined` and only assigned inside the `catch` block. If a caller explicitly passes `maxAttempts: 0` (or negative), the loop body never executes and the fallthrough `throw lastError` silently propagates `undefined`, which produces a cryptic "undefined was thrown" at the call site instead of a meaningful error. A guard at the entry or a sentinel initial value would make the failure clearer.

```suggestion
}): Promise<T> {
  const maxAttempts = params.maxAttempts ?? 3;
  if (maxAttempts <= 0) {
    throw new Error(`sendWhatsAppOutboundWithRetry: maxAttempts must be >= 1, got ${maxAttempts}`);
  }
  let lastError: unknown;
```

How can I resolve this? If you propose a fix, please make it concise.

Reviews (1): Last reviewed commit: "fix(whatsapp): preserve indented channel..." | Re-trigger Greptile

Comment thread extensions/whatsapp/src/outbound-media-contract.ts
@mcaxtr

mcaxtr commented Apr 21, 2026

Copy link
Copy Markdown
Member Author

Follow-up on bot feedback:

  • Fixed the one concrete regression introduced by this PR in 9480ae0a65 by removing retry-based resend from the gateway/channel outbound path in extensions/whatsapp/src/send.ts and adding a regression test in extensions/whatsapp/src/send.test.ts that locks in single-attempt behavior on transient send failures.
  • The remaining bot items were not addressed in this PR because they fall outside the actual regression scope:
    • unbounded mediaUrls processing and verbose auto-reply retry logging were already present on the auto-reply path before this refactor
    • maxAttempts <= 0 is a defensive hardening edge case with no current non-positive callers
    • filename sanitization and retry-log redaction are plausible hardening follow-ups, but not concrete regressions introduced by this change

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

Copy link
Copy Markdown

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: 9480ae0a65

ℹ️ About Codex in GitHub

Codex has been enabled to automatically 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 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread extensions/whatsapp/src/outbound-base.ts
@mcaxtr mcaxtr force-pushed the refactor/whatsapp-canonical-outbound-media branch 2 times, most recently from c5ed37e to 4188dd5 Compare April 23, 2026 04:49

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

Copy link
Copy Markdown

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: d6be4895f4

ℹ️ About Codex in GitHub

Codex has been enabled to automatically 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 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread extensions/whatsapp/src/auto-reply/deliver-reply.ts Outdated
Comment thread extensions/whatsapp/src/outbound-base.ts Outdated

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

Copy link
Copy Markdown

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: 80cd3dbd22

ℹ️ About Codex in GitHub

Codex has been enabled to automatically 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 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread src/auto-reply/reply/reply-media-paths.ts Outdated
@openclaw-barnacle openclaw-barnacle Bot added the agents Agent runtime and tooling label Apr 23, 2026

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

Copy link
Copy Markdown

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: bfc00aec16

ℹ️ About Codex in GitHub

Codex has been enabled to automatically 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 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread extensions/whatsapp/src/outbound-media-contract.ts Outdated
@mcaxtr mcaxtr force-pushed the refactor/whatsapp-canonical-outbound-media branch 2 times, most recently from f4b900e to 7506049 Compare April 24, 2026 02:42

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

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

https://github.com/openclaw/openclaw/blob/7506049d7af31e0ff1524770c0c66b5616a75469/src/auto-reply/reply/reply-delivery.ts#L147-L150
P1 Badge Deliver media-only block replies when streaming is disabled

When blockStreamingEnabled is false, this handler now never forwards block payloads to onBlockReply, so media-only block replies are dropped. That breaks the lifecycle path where consumePendingToolMediaReply emits attachments at the end of a run: those payloads are not guaranteed to be reconstructible from final assistant text, so they can disappear entirely instead of being sent once. Please restore the media-bearing fallback send path for the non-streaming branch.

ℹ️ About Codex in GitHub

Codex has been enabled to automatically 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 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread src/auto-reply/reply/reply-delivery.ts
@mcaxtr mcaxtr force-pushed the refactor/whatsapp-canonical-outbound-media branch from 7506049 to 1e5744a Compare April 24, 2026 03:02

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

Copy link
Copy Markdown

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: 1e5744a3a6

ℹ️ About Codex in GitHub

Codex has been enabled to automatically 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 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread src/auto-reply/reply/reply-media-paths.ts
@mcaxtr mcaxtr force-pushed the refactor/whatsapp-canonical-outbound-media branch from 1e5744a to fd9e485 Compare April 24, 2026 03:24

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

Copy link
Copy Markdown

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: fd9e485882

ℹ️ About Codex in GitHub

Codex has been enabled to automatically 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 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread src/auto-reply/reply/reply-delivery.ts
@mcaxtr mcaxtr force-pushed the refactor/whatsapp-canonical-outbound-media branch from fd9e485 to 6f20898 Compare April 24, 2026 03:37
@mcaxtr mcaxtr force-pushed the refactor/whatsapp-canonical-outbound-media branch from 6f20898 to 5ce22bc Compare April 24, 2026 03:40
@mcaxtr mcaxtr merged commit 18c9831 into main Apr 24, 2026
66 of 69 checks passed
@mcaxtr mcaxtr deleted the refactor/whatsapp-canonical-outbound-media branch April 24, 2026 04:04
vincentkoc added a commit that referenced this pull request Apr 24, 2026
* 'main' of https://github.com/openclaw/openclaw:
  fix(whatsapp): canonicalize outbound media delivery (#69813)
masatohoshino added a commit to masatohoshino/openclaw that referenced this pull request Apr 25, 2026
Fixes openclaw#66053. The WhatsApp adapter previously dropped the audioAsVoice
flag from ChannelOutboundContext, so replies carrying the
[[audio_as_voice]] directive were delivered as document attachments
instead of PTT voice notes.

Layers

- sendMessageWhatsApp: adds audioAsVoice option. When true and the
  loaded media is a verified audio source (isVerifiedAudioSource),
  mediaType is rebuilt from a sanitized MIME with opus codec
  preservation/fallback so the outbound frame reaches WhatsApp as a
  voice note. Non-audio-verified payloads stay on the document path.
- createWhatsAppOutboundBase (outbound-base.ts): forwards audioAsVoice
  through the sendMedia factory option so outbound-adapter.ts no
  longer needs per-adapter wiring (upstream canonicalization landed
  in openclaw#69813).
- deliverWebReply (auto-reply/deliver-reply.ts): routes kind='audio'
  and audioAsVoice-verified media to PTT with sanitized opus
  mimetype; image/video/document branches apply sanitized allowlisted
  mimetypes; document fileName is passed through sanitizeFileName.

Security hardening (applied on top of upstream's
normalizeWhatsAppLoadedMedia helper)

- sanitizeMediaMime rejects control characters (CWE-93) and preserves
  codecs params only in the audio path.
- sanitizeFileName strips ASCII control and bidi/invisible Unicode to
  prevent filename UI spoofing (CWE-451).
- isVerifiedAudioSource gates forceVoiceDelivery so an audioAsVoice
  reply cannot coerce non-audio bytes into a voice-note payload.

Tests (extensions/whatsapp)

- send.test.ts: audioAsVoice=true+audio routing, forceVoiceDelivery
  override guard, mimetype sanitization across kinds, document
  fileName sanitization.
- deliver-reply.test.ts: voice-coercion rejection, control-character
  fallbacks for audio/image/video/document, fileName sanitization,
  audioAsVoice-unset document path.
- outbound-base.test.ts / outbound-adapter.sendpayload.test.ts:
  audioAsVoice propagation through the factory.

Rebased onto upstream/main after openclaw#69813. Audio mimetype canonicalization
("audio/ogg" -> "audio/ogg; codecs=opus") is now owned by
normalizeWhatsAppLoadedMedia; this change layers PTT routing and
security hardening on top.

Closes openclaw#66053
Angfr95 pushed a commit to Angfr95/openclaw that referenced this pull request Apr 25, 2026
* fix(whatsapp): normalize outbound media payloads

* fix(embedded-runner): preserve final media directives

* fix(auto-reply): keep non-streaming media on final path

* fix(auto-reply): warn when reply media is dropped

* fix(whatsapp): align auto-reply media delivery

* docs(changelog): note whatsapp media normalization
ogt-redknie pushed a commit to ogt-redknie/OPENX that referenced this pull request May 2, 2026
* fix(whatsapp): normalize outbound media payloads

* fix(embedded-runner): preserve final media directives

* fix(auto-reply): keep non-streaming media on final path

* fix(auto-reply): warn when reply media is dropped

* fix(whatsapp): align auto-reply media delivery

* docs(changelog): note whatsapp media normalization
ogt-redknie pushed a commit to ogt-redknie/OPENX that referenced this pull request May 2, 2026
github-actions Bot pushed a commit to Desicool/openclaw that referenced this pull request May 9, 2026
* fix(whatsapp): normalize outbound media payloads

* fix(embedded-runner): preserve final media directives

* fix(auto-reply): keep non-streaming media on final path

* fix(auto-reply): warn when reply media is dropped

* fix(whatsapp): align auto-reply media delivery

* docs(changelog): note whatsapp media normalization
github-actions Bot pushed a commit to Desicool/openclaw that referenced this pull request May 9, 2026
globalcaos pushed a commit to globalcaos/tinkerclaw that referenced this pull request May 13, 2026
* fix(whatsapp): normalize outbound media payloads

* fix(embedded-runner): preserve final media directives

* fix(auto-reply): keep non-streaming media on final path

* fix(auto-reply): warn when reply media is dropped

* fix(whatsapp): align auto-reply media delivery

* docs(changelog): note whatsapp media normalization
globalcaos pushed a commit to globalcaos/tinkerclaw that referenced this pull request May 13, 2026
github-actions Bot pushed a commit to Desicool/openclaw that referenced this pull request May 24, 2026
* fix(whatsapp): normalize outbound media payloads

* fix(embedded-runner): preserve final media directives

* fix(auto-reply): keep non-streaming media on final path

* fix(auto-reply): warn when reply media is dropped

* fix(whatsapp): align auto-reply media delivery

* docs(changelog): note whatsapp media normalization
github-actions Bot pushed a commit to Desicool/openclaw that referenced this pull request May 24, 2026
jameslcowan pushed a commit to jameslcowan/openclaw that referenced this pull request Jun 2, 2026
* fix(whatsapp): normalize outbound media payloads

* fix(embedded-runner): preserve final media directives

* fix(auto-reply): keep non-streaming media on final path

* fix(auto-reply): warn when reply media is dropped

* fix(whatsapp): align auto-reply media delivery

* docs(changelog): note whatsapp media normalization
jameslcowan pushed a commit to jameslcowan/openclaw that referenced this pull request Jun 2, 2026
sablehead pushed a commit to sablehead/openclaw that referenced this pull request Jun 10, 2026
* fix(whatsapp): normalize outbound media payloads

* fix(embedded-runner): preserve final media directives

* fix(auto-reply): keep non-streaming media on final path

* fix(auto-reply): warn when reply media is dropped

* fix(whatsapp): align auto-reply media delivery

* docs(changelog): note whatsapp media normalization
sablehead pushed a commit to sablehead/openclaw that referenced this pull request Jun 10, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

agents Agent runtime and tooling channel: whatsapp-web Channel integration: whatsapp-web maintainer Maintainer-authored PR size: XL

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant