Skip to content

fix(bluebubbles): restore inbound image attachments and accept updated-message events#67510

Merged
omarshahine merged 12 commits intomainfrom
fix/bluebubbles-inbound-attachments
Apr 16, 2026
Merged

fix(bluebubbles): restore inbound image attachments and accept updated-message events#67510
omarshahine merged 12 commits intomainfrom
fix/bluebubbles-inbound-attachments

Conversation

@omarshahine
Copy link
Copy Markdown
Contributor

@omarshahine omarshahine commented Apr 16, 2026

Summary

Four interconnected fixes for BlueBubbles inbound media that together restore image attachment handling on Node 22+:

  1. Strip bundled-undici dispatcher from non-SSRF fetch pathblueBubblesFetchWithTimeout's non-SSRF fallback was spreading a bundled-undici dispatcher into globalThis.fetch(), which Node 22's built-in undici rejects with TypeError: invalid onRequestStart method. This silently broke ALL inbound attachment downloads. (BlueBubbles attachment downloads silently fail on Node.js 22 (undici dispatcher mismatch) #64105, BlueBubbles inbound attachments broken after 2026.4.5 upgrade #61861, [Bug]: BlueBubbles attachment downloads fail silently on Node 22.20+ — invalid onRequestStart method #67241, [Fix] Inbound image attachments silently fail in BlueBubbles and Slack — SSRF guard dispatcher incompatible with Node native fetch #62530)

  2. Accept updated-message events carrying attachments — BlueBubbles fires updated-message when attachments are indexed after the initial new-message (which may arrive with attachments: []). The webhook handler was filtering these out as non-reaction events. ([Bug]: BlueBubbles inbound image webhooks arrive with attachments: [] and no follow-up -- OpenClaw never ingests the image #65430)

  3. Event-type-aware dedup key — the persistent GUID dedup now suffixes updated-message keys with :updated so follow-up attachment events aren't rejected as duplicates of the already-committed new-message. (Relates to [codex] fix(bluebubbles): dedupe webhook replays without dropping edits #52277)

  4. Retry attachment fetch for empty arrays — when the initial webhook arrives with empty attachments (image-only messages or updated-message events), waits 2s and re-fetches from the BB API as a fallback. (Relates to fix(bluebubbles): retry attachment fetch when webhook arrives with empty array (#65430) #67437)

Test plan

  • pnpm tsgo — no new type errors
  • pnpm test extensions/bluebubbles/ — 434 tests pass (9 new tests added)
  • Manual: send standalone image via iMessage → agent receives and processes it
  • Manual: send text + image together → agent receives both
  • pnpm build — verify no [INEFFECTIVE_DYNAMIC_IMPORT] warnings

Issues closed

Closes #64105, closes #61861, closes #65430, closes #67241, closes #62530.

Related issues (not fixed by this PR)

Superseded PRs

Related PRs

🤖 Generated with Claude Code

@omarshahine omarshahine added channel: bluebubbles Channel integration: bluebubbles maintainer Maintainer-authored PR labels Apr 16, 2026
@omarshahine omarshahine force-pushed the fix/bluebubbles-inbound-attachments branch from 1802d1e to b3363ee Compare April 16, 2026 05:29
@omarshahine
Copy link
Copy Markdown
Contributor Author

omarshahine commented Apr 16, 2026

Manual verification

Built locally from b3363ee, pnpm-linked, gateway restarted on the production Lobster instance (Node 25.6.0, macOS 26.3.1). Three inbound iMessage attachments sent from +12069106512 → Lobster.

Test A — cartoon lobster PNG (attachment-only bubble)

  • On-disk: 2× 600,047-byte files written to ~/.openclaw/media/inbound/ at the same second (different UUIDs, same bytes → new-message fetch + updated-message fetch both fired).
  • Agent session: received a 618,844-byte base64 image/png payload inline; model correctly described the image.

Test B — cooked-lobster JPG (attachment-only bubble)

  • On-disk: 2× 61,292-byte files at the same second.
  • Agent session: received 39,252-byte base64 image/jpeg; model correctly described the image.

Test C — live-lobster JPG with body text "And this" (caption + image bubble)

  • On-disk: 2× 113,566-byte files at the same second.
  • Agent session: received body text And this plus 82,100-byte base64 image/jpeg; model correctly described the image.

Fix signatures observed

Build

pnpm build completes clean with no [INEFFECTIVE_DYNAMIC_IMPORT] warnings.

@omarshahine omarshahine marked this pull request as ready for review April 16, 2026 05:53
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: b3363ee094

ℹ️ 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/bluebubbles/src/monitor.ts Outdated
Comment on lines +255 to +259
const dataRecord = asRecord(payload.data);
const dataAttachments = dataRecord?.attachments;
const isAttachmentUpdate =
eventType === "updated-message" &&
Array.isArray(dataAttachments) &&
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Detect attachment updates from normalized payload shape

isAttachmentUpdate only checks payload.data.attachments, but this webhook handler already supports wrapper formats (payload.message, payload.data.message, JSON-string payloads) through normalizeWebhookMessage/extractMessagePayload. For updated-message events where attachments are nested under one of those supported wrappers, this branch still treats the event as “updated without reaction” and returns early, so valid attachment updates are dropped and inbound media remains missing for those payload variants.

Useful? React with 👍 / 👎.

Comment on lines +181 to +183
const { dispatcher: _dispatcher, ...safeInit } = (init ?? {}) as RequestInit & {
dispatcher?: unknown;
};
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Preserve guarded dispatcher in non-SSRF fetch path

This removes init.dispatcher before calling fetch, which breaks the contract when this function is used as fetchImpl by guarded callers (for example attachment downloads via fetchRemoteMedia). fetchWithSsrFGuard passes a pinned dispatcher to enforce DNS pinning/SSRF policy per request; stripping it here silently disables that enforcement for this path and can allow requests to bypass the guarded network route (e.g., rebinding/proxy-policy mismatches).

Useful? React with 👍 / 👎.

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented Apr 16, 2026

Greptile Summary

This PR delivers four interconnected fixes for BlueBubbles inbound image attachment handling on Node 22+: stripping a bundled-undici dispatcher from the non-SSRF fetch path, accepting updated-message events that carry indexed attachments, giving those events a distinct dedup key (:updated suffix), and adding a 2-second API-refetch fallback when the initial webhook arrives with an empty attachment array.

Fixes #1, #2, and #3 are well-implemented and directly address the root causes. Fix #4 (the attachment retry in processMessageAfterDedupe) has a correctness gap: the retry block at line 1076 is placed after the !rawBody early-return guard at line 791. For pure image-only new-message events (the primary stated target of text.length === 0 in shouldRetryAttachments), rawBody evaluates to "" and the message is dropped before the retry can fire. In practice this gap is partially mitigated by fix #2 when BB reliably sends a follow-up updated-message, but for deployments where BB does not, image-only messages are still silently dropped.

Confidence Score: 3/5

Fixes #1/#2/#3 are correct and safe; fix #4 has a logic ordering bug that makes the image-only retry dead code — should be addressed before merging.

The P1 finding (attachment retry unreachable for text-empty image-only messages) means the fallback mechanism described in fix #4 is silently broken for its primary use-case. The primary path (fix #2, updated-message events) still works when BB sends follow-up webhooks, but the stated fallback for cases where BB does not send one does not work for pure image messages.

extensions/bluebubbles/src/monitor-processing.ts — the retry block at line 1076 needs to be moved before the !rawBody guard at line 791 to be reachable for image-only messages.

Comments Outside Diff (2)

  1. extensions/bluebubbles/src/monitor.ts, line 271-277 (link)

    P2 Stale log message after filter logic expanded

    The log message says without reaction but the filter now also passes through isAttachmentUpdate events. An operator debugging dropped updated-message events with empty attachments will see misleading output.

    Prompt To Fix With AI
    This is a comment left during a code review.
    Path: extensions/bluebubbles/src/monitor.ts
    Line: 271-277
    
    Comment:
    **Stale log message after filter logic expanded**
    
    The log message says `without reaction` but the filter now also passes through `isAttachmentUpdate` events. An operator debugging dropped `updated-message` events with empty attachments will see misleading output.
    
    
    
    How can I resolve this? If you propose a fix, please make it concise.
  2. extensions/bluebubbles/src/attachments.ts, line 30-32 (link)

    P2 blueBubblesPolicy in attachments.ts still returns {} (triggers SSRF guard)

    fetchBlueBubblesMessageAttachments (added in this PR, below) deliberately passes undefined instead of {} when private-network is not opted-in, with a comment explaining that {} routes through the SSRF guard and blocks localhost BB servers. The module-level blueBubblesPolicy helper at line 30 still returns {} for the non-private case, meaning sendBlueBubblesAttachment (which uses it via postMultipartFormData) takes the SSRF-guarded path for localhost deployments.

    This inconsistency is worth consolidating to avoid future confusion — monitor-processing.ts already has a local blueBubblesPolicy that returns undefined for the same reason.

    Prompt To Fix With AI
    This is a comment left during a code review.
    Path: extensions/bluebubbles/src/attachments.ts
    Line: 30-32
    
    Comment:
    **`blueBubblesPolicy` in `attachments.ts` still returns `{}` (triggers SSRF guard)**
    
    `fetchBlueBubblesMessageAttachments` (added in this PR, below) deliberately passes `undefined` instead of `{}` when private-network is not opted-in, with a comment explaining that `{}` routes through the SSRF guard and blocks localhost BB servers. The module-level `blueBubblesPolicy` helper at line 30 still returns `{}` for the non-private case, meaning `sendBlueBubblesAttachment` (which uses it via `postMultipartFormData`) takes the SSRF-guarded path for localhost deployments.
    
    This inconsistency is worth consolidating to avoid future confusion — `monitor-processing.ts` already has a local `blueBubblesPolicy` that returns `undefined` for the same reason.
    
    How can I resolve this? If you propose a fix, please make it concise.
Prompt To Fix All With AI
This is a comment left during a code review.
Path: extensions/bluebubbles/src/monitor-processing.ts
Line: 1076-1108

Comment:
**Attachment retry unreachable for image-only messages**

The retry condition at line 1083 explicitly targets `text.length === 0` (pure image-only messages), but those messages are dropped by the `!rawBody` guard at line 791 — which is evaluated before this retry block. When a `new-message` arrives with `text=""` and `attachments=[]`, `rawBody` is `text || placeholder = "" || "" = ""`, causing an early return. The retry is never reached.

The result: for a standalone image-only `new-message` that arrives before BB finishes attachment indexing, with BB not sending a follow-up `updated-message`, the message is silently dropped and fix #4's fallback does nothing.

The simplest repair is to move the attachment-retry block (or at minimum the `resolvedAttachments` reassignment) **before** the `!rawBody` guard, and recompute `placeholder`/`rawBody` afterward if `resolvedAttachments` was updated.

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

---

This is a comment left during a code review.
Path: extensions/bluebubbles/src/monitor.ts
Line: 271-277

Comment:
**Stale log message after filter logic expanded**

The log message says `without reaction` but the filter now also passes through `isAttachmentUpdate` events. An operator debugging dropped `updated-message` events with empty attachments will see misleading output.

```suggestion
            `webhook ignored ${eventType || "event"} (no reaction or attachment update)`,
```

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

---

This is a comment left during a code review.
Path: extensions/bluebubbles/src/attachments.ts
Line: 30-32

Comment:
**`blueBubblesPolicy` in `attachments.ts` still returns `{}` (triggers SSRF guard)**

`fetchBlueBubblesMessageAttachments` (added in this PR, below) deliberately passes `undefined` instead of `{}` when private-network is not opted-in, with a comment explaining that `{}` routes through the SSRF guard and blocks localhost BB servers. The module-level `blueBubblesPolicy` helper at line 30 still returns `{}` for the non-private case, meaning `sendBlueBubblesAttachment` (which uses it via `postMultipartFormData`) takes the SSRF-guarded path for localhost deployments.

This inconsistency is worth consolidating to avoid future confusion — `monitor-processing.ts` already has a local `blueBubblesPolicy` that returns `undefined` for the same reason.

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

Reviews (1): Last reviewed commit: "fix(bluebubbles): resolve review finding..." | Re-trigger Greptile

Comment thread extensions/bluebubbles/src/monitor-processing.ts Outdated
@aisle-research-bot
Copy link
Copy Markdown

aisle-research-bot Bot commented Apr 16, 2026

🔒 Aisle Security Analysis

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

# Severity Title
1 🟠 High BlueBubbles API password embedded in URL query string
2 🟠 High SSRF guard bypass by passing undefined policy to blueBubblesFetchWithTimeout
3 🟡 Medium SSRF protection bypass by passing undefined policy to blueBubblesFetchWithTimeout
1. 🟠 BlueBubbles API password embedded in URL query string
Property Value
Severity High
CWE CWE-201
Location extensions/bluebubbles/src/types.ts:128-138

Description

The BlueBubbles integration constructs API URLs by placing the account password directly into the URL query string (?password=...).

This is sensitive-data exposure because URLs are commonly recorded or propagated via:

  • application/server access logs
  • reverse proxies and load balancers
  • monitoring/APM tooling
  • exception messages (e.g., network errors that include the full request URL)
  • browser/network intermediaries (if ever used outside strictly server-side contexts)

In this change, the new fetchBlueBubblesMessageAttachments() code path also uses buildBlueBubblesApiUrl(..., password) to fetch message metadata, increasing the number of requests that will carry credentials in the URL.

Vulnerable code:

const url = new URL(params.path, `${normalized}/`);
if (params.password) {
  url.searchParams.set("password", params.password);
}
return url.toString();

Recommendation

Do not put secrets in URLs. Send the password via an HTTP header (or another non-URL channel supported by BlueBubbles), and ensure any logging redacts sensitive headers.

For example, refactor to return a URL without credentials and add a helper to build the request init:

export function buildBlueBubblesApiRequest(params: {
  baseUrl: string;
  path: string;
  password?: string;
}): { url: string; init: RequestInit } {
  const normalized = normalizeBlueBubblesServerUrl(params.baseUrl);
  const url = new URL(params.path, `${normalized}/`).toString();
  const headers = new Headers();
  if (params.password) headers.set("Authorization", `Bearer ${params.password}`);
  return { url, init: { headers } };
}

If the upstream API only supports password-in-query, implement redaction before any logging/telemetry (e.g., scrub password query param from URLs) and avoid including raw error strings that may contain the full URL.

2. 🟠 SSRF guard bypass by passing `undefined` policy to `blueBubblesFetchWithTimeout`
Property Value
Severity High
CWE CWE-918
Location extensions/bluebubbles/src/types.ts:149-190

Description

blueBubblesFetchWithTimeout only routes through the SSRF guard when ssrfPolicy !== undefined. This change intentionally returns undefined for the non-private-network case, which disables the SSRF guard entirely and falls back to an unguarded fetch().

Impact:

  • baseUrl is used to build BlueBubbles API URLs (e.g. /api/v1/message/:guid). If an attacker can influence serverUrl/baseUrl (via misconfiguration, config-write features, or any upstream injection), requests will be made to arbitrary hosts without SSRF protections.
  • The SSRF guard is also the natural place to enforce redirect restrictions, DNS/IP checks, and blocking private/link-local/metadata IP ranges. Bypassing it risks access to internal services.

Vulnerable code paths introduced/changed:

  • blueBubblesPolicy() now returns undefined when private-network access is not opted-in, explicitly selecting the unguarded path.
  • New API call fetchBlueBubblesMessageAttachments() uses this undefined policy, causing an unguarded GET request to the configured server URL.

Vulnerable code:

// attachments.ts
return allowPrivateNetwork ? { allowPrivateNetwork: true } : undefined;// types.ts
if (ssrfPolicy !== undefined) {
  return await _fetchGuard({ ..., policy: ssrfPolicy, ...});
}
return await fetch(url, { ...safeInit, signal: controller.signal });

Recommendation

Do not use undefined to mean “allow localhost”; keep the SSRF guard engaged by default and express exceptions as explicit policy.

Options:

  1. Preferred: extend policy to allow localhost explicitly while still enforcing SSRF protections for everything else (e.g., allowedHostnames: ["localhost"] and/or explicit loopback IP allowances), and always call _fetchGuard.
  2. If a fallback is required, gate it narrowly: only allow http://localhost / 127.0.0.1 / ::1 when opted-in, and still disallow redirects to private/link-local/metadata ranges.

Example (conceptual):

function blueBubblesPolicy(allowPrivateNetwork?: boolean): SsrFPolicy {
  if (allowPrivateNetwork) return { allowPrivateNetwork: true };// keep SSRF guard on, but allow common self-hosted hostnames safely
  return { allowedHostnames: ["localhost"] };
}// and in blueBubblesFetchWithTimeout: always guard
const { response, release } = await _fetchGuard({ url, init, timeoutMs, policy: ssrfPolicy ?? {}, auditContext: "bluebubbles-api" });
3. 🟡 SSRF protection bypass by passing undefined policy to blueBubblesFetchWithTimeout
Property Value
Severity Medium
CWE CWE-918
Location extensions/bluebubbles/src/attachments.ts:30-36

Description

blueBubblesFetchWithTimeout enforces SSRF protections only when a ssrfPolicy is provided; when it is undefined, it falls back to a raw fetch(url, ...) with no SSRF validation.

This change intentionally returns undefined in blueBubblesPolicy(...) for the default (non-private-network) case, and fetchBlueBubblesMessageAttachments also passes policy: undefined when allowPrivateNetwork is not opted-in.

Impact:

  • If an attacker can influence baseUrl (e.g., via tenant/account configuration, or any pathway that allows untrusted modification of BlueBubbles server settings), the server will perform arbitrary outbound HTTP requests without SSRF guardrails.
  • This can enable access to internal services (e.g., http://127.0.0.1, RFC1918, or cloud metadata endpoints depending on environment), turning the feature into an SSRF primitive.

Vulnerable code (policy intentionally disabled):

// Pass `undefined` (not `{}`) ... so the non-SSRF fallback path is used.
return allowPrivateNetwork ? { allowPrivateNetwork: true } : undefined;
const policy: SsrFPolicy | undefined = opts.allowPrivateNetwork
  ? { allowPrivateNetwork: true }
  : undefined;
const response = await blueBubblesFetchWithTimeout(url, { method: "GET" }, opts.timeoutMs, policy);

And the sink behavior:

if (ssrfPolicy !== undefined) {// SSRF-guarded fetch...
}// ...else: raw fetch
return await fetch(url, { ...safeInit, signal: controller.signal });

Recommendation

Do not disable SSRF protections by default.

Options:

  1. Keep SSRF guard enabled and explicitly allow only the BlueBubbles host:
    • Parse the configured baseUrl hostname and pass ssrfPolicy: { allowedHostnames: [hostname] }.
    • If you must support localhost deployments, allow it via an explicit opt-in flag (or a dedicated allowLocalhost setting), not by bypassing the guard.

Example:

const hostname = new URL(normalizeBlueBubblesServerUrl(opts.baseUrl)).hostname;
const policy: SsrFPolicy = opts.allowPrivateNetwork
  ? { allowPrivateNetwork: true }
  : { allowedHostnames: [hostname] };

const response = await blueBubblesFetchWithTimeout(url, { method: "GET" }, opts.timeoutMs, policy);
  1. If localhost must be supported by default, implement a narrow allowlist (e.g., localhost, 127.0.0.1, ::1) rather than fully unguarded fetch, and still block link-local/metadata ranges.

Analyzed PR: #67510 at commit 7806211

Last updated on: 2026-04-16T17:00:37Z

@openclaw-barnacle openclaw-barnacle Bot added the channel: whatsapp-web Channel integration: whatsapp-web label Apr 16, 2026
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: bea8f55c1c

ℹ️ 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/bluebubbles/src/monitor-processing.ts Outdated
@yfge
Copy link
Copy Markdown
Contributor

yfge commented Apr 16, 2026

Review follow-up triage:

I checked the current feedback and the two highest-signal code changes are:

  • move the attachment retry path before the !rawBody early return in extensions/bluebubbles/src/monitor-processing.ts, then recompute placeholder/body from the resolved attachments so attachment-only messages can survive the retry path
  • align the stale webhook log string in extensions/bluebubbles/src/monitor.ts and make extensions/bluebubbles/src/attachments.ts return undefined instead of {} for the non-private SSRF policy path, matching the newer fetch behavior

I prepared that patch locally, but this automation token cannot push to openclaw/openclaw (403 Permission denied to yfge).

One caveat from validation: the targeted BlueBubbles vitest run in my temp clone surfaced broad existing failures in the branch test suite, so I am not claiming a green branch from automation yet. The review points above are still valid and actionable.

@openclaw-barnacle openclaw-barnacle Bot added the scripts Repository scripts label Apr 16, 2026
omarshahine added a commit that referenced this pull request Apr 16, 2026
- monitor-processing: move attachment retry into the !rawBody guard so
  image-only new-message events that arrive with empty attachments and
  empty text are recovered via a BB API refetch before being dropped.
  The existing retry block at the end of processMessageAfterDedupe was
  unreachable for this case because the !rawBody early-return fired
  first. (Greptile)
- monitor: derive isAttachmentUpdate from the normalized message shape
  instead of raw payload.data.attachments so updated-message webhooks
  with attachments under wrapper formats (payload.message, JSON-string
  payloads) are correctly routed through for processing instead of
  silently filtered. (Codex)
- types: use bundled-undici fetch when init.dispatcher is present so
  the SSRF guard's DNS-pinning dispatcher is preserved when this
  function is called as fetchImpl from guarded callers (e.g. the
  attachment download path via fetchRemoteMedia). Falls back to
  globalThis.fetch when no dispatcher is present so tests that stub
  globalThis.fetch keep working. (Codex)
- attachments: blueBubblesPolicy returns undefined for the non-private
  case (matching monitor-processing's helper) so sendBlueBubblesAttachment
  stops routing localhost BB through the SSRF guard. (Greptile)
- scripts/check-no-raw-channel-fetch: bump the types.ts allowlist line
  to match the restructured non-SSRF branch.
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: ae97d0d9fb

ℹ️ 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/bluebubbles/src/types.ts Outdated
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: 4bfc2777b4

ℹ️ 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/bluebubbles/src/monitor-processing.ts Outdated
omarshahine and others added 6 commits April 16, 2026 14:57
…d-message events

Four interconnected fixes for BlueBubbles inbound media:

1. Strip bundled-undici dispatcher from non-SSRF fetch path so attachment
   downloads no longer silently fail on Node 22+ (#64105, #61861)

2. Accept updated-message webhook events that carry attachments instead of
   filtering them as non-reaction events (#65430)

3. Include eventType in the persistent GUID dedup key so updated-message
   follow-ups are not rejected as duplicates of the original new-message (#52277)

4. Retry attachment fetch from BB API (2s delay) when the initial webhook
   arrives with an empty attachments array — image-only messages and
   updated-message events only (#67437)

Closes #64105, closes #61861, closes #65430.
…tAttachments, add tests

- F1 (BLOCKER): pass undefined instead of {} for SSRF policy when
  allowPrivateNetwork is false, so localhost BB servers are not blocked.
- F2 (IMPORTANT): reuse exported extractAttachments() from monitor-normalize
  instead of duplicating field extraction logic.
- F3 (IMPORTANT): simplify asRecord(asRecord(payload)?.data) to
  asRecord(payload.data) since payload is already Record<string, unknown>.
- F4 (NIT): bind retryMessageId before the guard to eliminate non-null assertion.
- F5 (IMPORTANT): add 4 tests for fetchBlueBubblesMessageAttachments covering
  success, non-ok HTTP, empty data, and guid-less entries.
- Add CHANGELOG entry for the user-facing fix.
- monitor-processing: move attachment retry into the !rawBody guard so
  image-only new-message events that arrive with empty attachments and
  empty text are recovered via a BB API refetch before being dropped.
  The existing retry block at the end of processMessageAfterDedupe was
  unreachable for this case because the !rawBody early-return fired
  first. (Greptile)
- monitor: derive isAttachmentUpdate from the normalized message shape
  instead of raw payload.data.attachments so updated-message webhooks
  with attachments under wrapper formats (payload.message, JSON-string
  payloads) are correctly routed through for processing instead of
  silently filtered. (Codex)
- types: use bundled-undici fetch when init.dispatcher is present so
  the SSRF guard's DNS-pinning dispatcher is preserved when this
  function is called as fetchImpl from guarded callers (e.g. the
  attachment download path via fetchRemoteMedia). Falls back to
  globalThis.fetch when no dispatcher is present so tests that stub
  globalThis.fetch keep working. (Codex)
- attachments: blueBubblesPolicy returns undefined for the non-private
  case (matching monitor-processing's helper) so sendBlueBubblesAttachment
  stops routing localhost BB through the SSRF guard. (Greptile)
- scripts/check-no-raw-channel-fetch: bump the types.ts allowlist line
  to match the restructured non-SSRF branch.
…le log

Move the attachment retry block (2s BB API refetch for empty attachments)
before the !rawBody early-return guard. Previously, image-only messages
with text='' and attachments=[] would be dropped by the !rawBody check
before the retry could fire, making fix #4 dead code for its primary
use-case. Now the retry runs first and recomputes the placeholder from
resolved attachments so rawBody becomes non-empty when media is found.

Also fix stale log message that still said 'without reaction' after the
filter was expanded to pass through attachment updates.
…oach

Revert the @claude bot's undici import in types.ts — it introduced a
direct 'undici' dependency that is not declared in the BB extension's
package.json and would break isolated plugin installs. Restore the
original dispatcher-strip approach which is correct: the SSRF guard
already completed validation upstream before calling this function as
fetchImpl, so stripping the dispatcher does not weaken security.
@omarshahine omarshahine force-pushed the fix/bluebubbles-inbound-attachments branch from 4bfc277 to 279dba1 Compare April 16, 2026 15:00
@openclaw-barnacle openclaw-barnacle Bot removed the channel: whatsapp-web Channel integration: whatsapp-web label Apr 16, 2026
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: 279dba17d2

ℹ️ 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 scripts/check-no-raw-channel-fetch.mjs Outdated
Comment thread extensions/bluebubbles/src/monitor.ts
…uard

The empty-body attachment-recovery block added in the earlier PR revision
is now redundant because the main retry block was moved above the rawBody
computation in 0d7d1c4. Worse, that leftover block reassigned the
(now-const) placeholder variable, throwing `TypeError: Assignment to
constant variable` at runtime for image-only messages — breaking the very
recovery path it was meant to protect (flagged by Codex on 4bfc277).

Remove the dead block; the up-front retry already handles the image-only
case by recovering attachments before the rawBody computation, so once we
reach the !rawBody guard with an empty body it is genuinely empty and
should drop as before.
279dba1 reverted types.ts back to the dispatcher-strip approach,
which put the `fetch(url, ...)` call at line 189 instead of line 198.
Bump the allowlist entry to match so `lint:tmp:no-raw-channel-fetch`
stops failing check-additional.
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: b751956760

ℹ️ 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 on lines +156 to +157
if (message.eventType === "updated-message") {
return `${base}:updated`;
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 Prevent updated-message keys from replaying the same user prompt

Suffixing every updated-message dedupe key with :updated makes attachment follow-up webhooks for the same GUID bypass the existing new-message claim and run a full second inbound processing pass. When BlueBubbles sends new-message first (text, empty attachments) and then an updated-message later with attachments, this path can produce two independent assistant replies/tool runs for one user message once the update lands outside the 500ms debounce window.

Useful? React with 👍 / 👎.

`628b454eff feat: default Anthropic to Opus 4.7` bumped the bundled
anthropic image default to `claude-opus-4-7` but missed updating the
`ANTHROPIC_PDF_MODEL` constant in pdf-tool.model-config.test.ts. The
tests now fail on any PR that runs the `checks-node-agentic-agents-plugins`
shard because the resolver returns 4-7 while the test asserts 4-6.

Bump the constant to 4-7 to match the bundled default.
@openclaw-barnacle openclaw-barnacle Bot added the agents Agent runtime and tooling label Apr 16, 2026
@openclaw-barnacle openclaw-barnacle Bot removed the agents Agent runtime and tooling label Apr 16, 2026
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: 36aefb0feb

ℹ️ 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 on lines +128 to +130
const policy: SsrFPolicy | undefined = opts.allowPrivateNetwork
? { allowPrivateNetwork: true }
: undefined;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Preserve private-network opt-out for attachment fetches

When allowPrivateNetwork is false, this now sets policy to undefined, which routes blueBubblesFetchWithTimeout through the raw fetch path instead of the SSRF-guarded path. In practice this bypasses the channel’s private-network guardrails for message-attachment lookups, so deployments that explicitly set channels.bluebubbles.network.dangerouslyAllowPrivateNetwork=false can still make requests to private BlueBubbles hosts. This is a security/config-contract regression introduced by switching from {} to undefined here.

Useful? React with 👍 / 👎.

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

ℹ️ 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 on lines +30 to +35
function blueBubblesPolicy(allowPrivateNetwork: boolean | undefined): SsrFPolicy | undefined {
// Pass `undefined` (not `{}`) for the non-private case so the non-SSRF fallback path
// is used. An empty `{}` policy routes through the SSRF guard, which blocks the
// localhost BB deployments that are the most common self-hosted setup. The opt-in
// private-network branch keeps the explicit policy. (#64105, #67510)
return allowPrivateNetwork ? { allowPrivateNetwork: true } : undefined;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Keep attachment uploads on guarded fetch path

blueBubblesPolicy now returns undefined when private-network mode is not explicitly enabled, and that helper is also used by sendBlueBubblesAttachment for /api/v1/message/attachment requests. In this configuration blueBubblesFetchWithTimeout skips fetchWithSsrFGuard and falls back to raw fetch, so attachment uploads lose pinned-DNS/redirect/private-IP enforcement even when operators explicitly set network.dangerouslyAllowPrivateNetwork: false; this is a security regression on the outbound attachment path introduced by this helper change.

Useful? React with 👍 / 👎.

@omarshahine omarshahine merged commit 77d9fd6 into main Apr 16, 2026
44 checks passed
@omarshahine omarshahine deleted the fix/bluebubbles-inbound-attachments branch April 16, 2026 17:04
xudaiyanzi pushed a commit to xudaiyanzi/openclaw that referenced this pull request Apr 17, 2026
…d-message events (openclaw#67510)

* fix(bluebubbles): restore inbound image attachments and accept updated-message events

Four interconnected fixes for BlueBubbles inbound media:

1. Strip bundled-undici dispatcher from non-SSRF fetch path so attachment
   downloads no longer silently fail on Node 22+ (openclaw#64105, openclaw#61861)

2. Accept updated-message webhook events that carry attachments instead of
   filtering them as non-reaction events (openclaw#65430)

3. Include eventType in the persistent GUID dedup key so updated-message
   follow-ups are not rejected as duplicates of the original new-message (openclaw#52277)

4. Retry attachment fetch from BB API (2s delay) when the initial webhook
   arrives with an empty attachments array — image-only messages and
   updated-message events only (openclaw#67437)

Closes openclaw#64105, closes openclaw#61861, closes openclaw#65430.

* fix(bluebubbles): resolve review findings — SSRF policy, reuse extractAttachments, add tests

- F1 (BLOCKER): pass undefined instead of {} for SSRF policy when
  allowPrivateNetwork is false, so localhost BB servers are not blocked.
- F2 (IMPORTANT): reuse exported extractAttachments() from monitor-normalize
  instead of duplicating field extraction logic.
- F3 (IMPORTANT): simplify asRecord(asRecord(payload)?.data) to
  asRecord(payload.data) since payload is already Record<string, unknown>.
- F4 (NIT): bind retryMessageId before the guard to eliminate non-null assertion.
- F5 (IMPORTANT): add 4 tests for fetchBlueBubblesMessageAttachments covering
  success, non-ok HTTP, empty data, and guid-less entries.
- Add CHANGELOG entry for the user-facing fix.

* fix(ci): update raw-fetch allowlist line number after dispatcher strip

* fix(bluebubbles): resolve PR review findings (openclaw#67510)

- monitor-processing: move attachment retry into the !rawBody guard so
  image-only new-message events that arrive with empty attachments and
  empty text are recovered via a BB API refetch before being dropped.
  The existing retry block at the end of processMessageAfterDedupe was
  unreachable for this case because the !rawBody early-return fired
  first. (Greptile)
- monitor: derive isAttachmentUpdate from the normalized message shape
  instead of raw payload.data.attachments so updated-message webhooks
  with attachments under wrapper formats (payload.message, JSON-string
  payloads) are correctly routed through for processing instead of
  silently filtered. (Codex)
- types: use bundled-undici fetch when init.dispatcher is present so
  the SSRF guard's DNS-pinning dispatcher is preserved when this
  function is called as fetchImpl from guarded callers (e.g. the
  attachment download path via fetchRemoteMedia). Falls back to
  globalThis.fetch when no dispatcher is present so tests that stub
  globalThis.fetch keep working. (Codex)
- attachments: blueBubblesPolicy returns undefined for the non-private
  case (matching monitor-processing's helper) so sendBlueBubblesAttachment
  stops routing localhost BB through the SSRF guard. (Greptile)
- scripts/check-no-raw-channel-fetch: bump the types.ts allowlist line
  to match the restructured non-SSRF branch.

* fix(bluebubbles): move attachment retry before rawBody guard, fix stale log

Move the attachment retry block (2s BB API refetch for empty attachments)
before the !rawBody early-return guard. Previously, image-only messages
with text='' and attachments=[] would be dropped by the !rawBody check
before the retry could fire, making fix openclaw#4 dead code for its primary
use-case. Now the retry runs first and recomputes the placeholder from
resolved attachments so rawBody becomes non-empty when media is found.

Also fix stale log message that still said 'without reaction' after the
filter was expanded to pass through attachment updates.

* fix(bluebubbles): revert undici import, restore dispatcher-strip approach

Revert the @claude bot's undici import in types.ts — it introduced a
direct 'undici' dependency that is not declared in the BB extension's
package.json and would break isolated plugin installs. Restore the
original dispatcher-strip approach which is correct: the SSRF guard
already completed validation upstream before calling this function as
fetchImpl, so stripping the dispatcher does not weaken security.

* fix(bluebubbles): remove dead empty-body recovery block in !rawBody guard

The empty-body attachment-recovery block added in the earlier PR revision
is now redundant because the main retry block was moved above the rawBody
computation in 0d7d1c4. Worse, that leftover block reassigned the
(now-const) placeholder variable, throwing `TypeError: Assignment to
constant variable` at runtime for image-only messages — breaking the very
recovery path it was meant to protect (flagged by Codex on 4bfc2777).

Remove the dead block; the up-front retry already handles the image-only
case by recovering attachments before the rawBody computation, so once we
reach the !rawBody guard with an empty body it is genuinely empty and
should drop as before.

* fix(ci): update raw-fetch allowlist line after dispatcher-strip revert

279dba1 reverted types.ts back to the dispatcher-strip approach,
which put the `fetch(url, ...)` call at line 189 instead of line 198.
Bump the allowlist entry to match so `lint:tmp:no-raw-channel-fetch`
stops failing check-additional.

* test(pdf-tool): update stale opus-4-6 constant to opus-4-7

`628b454eff feat: default Anthropic to Opus 4.7` bumped the bundled
anthropic image default to `claude-opus-4-7` but missed updating the
`ANTHROPIC_PDF_MODEL` constant in pdf-tool.model-config.test.ts. The
tests now fail on any PR that runs the `checks-node-agentic-agents-plugins`
shard because the resolver returns 4-7 while the test asserts 4-6.

Bump the constant to 4-7 to match the bundled default.

---------

Co-authored-by: Lobster <10343873+omarshahine@users.noreply.github.com>
kvnkho pushed a commit to kvnkho/openclaw that referenced this pull request Apr 17, 2026
…d-message events (openclaw#67510)

* fix(bluebubbles): restore inbound image attachments and accept updated-message events

Four interconnected fixes for BlueBubbles inbound media:

1. Strip bundled-undici dispatcher from non-SSRF fetch path so attachment
   downloads no longer silently fail on Node 22+ (openclaw#64105, openclaw#61861)

2. Accept updated-message webhook events that carry attachments instead of
   filtering them as non-reaction events (openclaw#65430)

3. Include eventType in the persistent GUID dedup key so updated-message
   follow-ups are not rejected as duplicates of the original new-message (openclaw#52277)

4. Retry attachment fetch from BB API (2s delay) when the initial webhook
   arrives with an empty attachments array — image-only messages and
   updated-message events only (openclaw#67437)

Closes openclaw#64105, closes openclaw#61861, closes openclaw#65430.

* fix(bluebubbles): resolve review findings — SSRF policy, reuse extractAttachments, add tests

- F1 (BLOCKER): pass undefined instead of {} for SSRF policy when
  allowPrivateNetwork is false, so localhost BB servers are not blocked.
- F2 (IMPORTANT): reuse exported extractAttachments() from monitor-normalize
  instead of duplicating field extraction logic.
- F3 (IMPORTANT): simplify asRecord(asRecord(payload)?.data) to
  asRecord(payload.data) since payload is already Record<string, unknown>.
- F4 (NIT): bind retryMessageId before the guard to eliminate non-null assertion.
- F5 (IMPORTANT): add 4 tests for fetchBlueBubblesMessageAttachments covering
  success, non-ok HTTP, empty data, and guid-less entries.
- Add CHANGELOG entry for the user-facing fix.

* fix(ci): update raw-fetch allowlist line number after dispatcher strip

* fix(bluebubbles): resolve PR review findings (openclaw#67510)

- monitor-processing: move attachment retry into the !rawBody guard so
  image-only new-message events that arrive with empty attachments and
  empty text are recovered via a BB API refetch before being dropped.
  The existing retry block at the end of processMessageAfterDedupe was
  unreachable for this case because the !rawBody early-return fired
  first. (Greptile)
- monitor: derive isAttachmentUpdate from the normalized message shape
  instead of raw payload.data.attachments so updated-message webhooks
  with attachments under wrapper formats (payload.message, JSON-string
  payloads) are correctly routed through for processing instead of
  silently filtered. (Codex)
- types: use bundled-undici fetch when init.dispatcher is present so
  the SSRF guard's DNS-pinning dispatcher is preserved when this
  function is called as fetchImpl from guarded callers (e.g. the
  attachment download path via fetchRemoteMedia). Falls back to
  globalThis.fetch when no dispatcher is present so tests that stub
  globalThis.fetch keep working. (Codex)
- attachments: blueBubblesPolicy returns undefined for the non-private
  case (matching monitor-processing's helper) so sendBlueBubblesAttachment
  stops routing localhost BB through the SSRF guard. (Greptile)
- scripts/check-no-raw-channel-fetch: bump the types.ts allowlist line
  to match the restructured non-SSRF branch.

* fix(bluebubbles): move attachment retry before rawBody guard, fix stale log

Move the attachment retry block (2s BB API refetch for empty attachments)
before the !rawBody early-return guard. Previously, image-only messages
with text='' and attachments=[] would be dropped by the !rawBody check
before the retry could fire, making fix openclaw#4 dead code for its primary
use-case. Now the retry runs first and recomputes the placeholder from
resolved attachments so rawBody becomes non-empty when media is found.

Also fix stale log message that still said 'without reaction' after the
filter was expanded to pass through attachment updates.

* fix(bluebubbles): revert undici import, restore dispatcher-strip approach

Revert the @claude bot's undici import in types.ts — it introduced a
direct 'undici' dependency that is not declared in the BB extension's
package.json and would break isolated plugin installs. Restore the
original dispatcher-strip approach which is correct: the SSRF guard
already completed validation upstream before calling this function as
fetchImpl, so stripping the dispatcher does not weaken security.

* fix(bluebubbles): remove dead empty-body recovery block in !rawBody guard

The empty-body attachment-recovery block added in the earlier PR revision
is now redundant because the main retry block was moved above the rawBody
computation in 0d7d1c4. Worse, that leftover block reassigned the
(now-const) placeholder variable, throwing `TypeError: Assignment to
constant variable` at runtime for image-only messages — breaking the very
recovery path it was meant to protect (flagged by Codex on 4bfc2777).

Remove the dead block; the up-front retry already handles the image-only
case by recovering attachments before the rawBody computation, so once we
reach the !rawBody guard with an empty body it is genuinely empty and
should drop as before.

* fix(ci): update raw-fetch allowlist line after dispatcher-strip revert

279dba1 reverted types.ts back to the dispatcher-strip approach,
which put the `fetch(url, ...)` call at line 189 instead of line 198.
Bump the allowlist entry to match so `lint:tmp:no-raw-channel-fetch`
stops failing check-additional.

* test(pdf-tool): update stale opus-4-6 constant to opus-4-7

`628b454eff feat: default Anthropic to Opus 4.7` bumped the bundled
anthropic image default to `claude-opus-4-7` but missed updating the
`ANTHROPIC_PDF_MODEL` constant in pdf-tool.model-config.test.ts. The
tests now fail on any PR that runs the `checks-node-agentic-agents-plugins`
shard because the resolver returns 4-7 while the test asserts 4-6.

Bump the constant to 4-7 to match the bundled default.

---------

Co-authored-by: Lobster <10343873+omarshahine@users.noreply.github.com>
Mquarmoc pushed a commit to Mquarmoc/openclaw that referenced this pull request Apr 20, 2026
…d-message events (openclaw#67510)

* fix(bluebubbles): restore inbound image attachments and accept updated-message events

Four interconnected fixes for BlueBubbles inbound media:

1. Strip bundled-undici dispatcher from non-SSRF fetch path so attachment
   downloads no longer silently fail on Node 22+ (openclaw#64105, openclaw#61861)

2. Accept updated-message webhook events that carry attachments instead of
   filtering them as non-reaction events (openclaw#65430)

3. Include eventType in the persistent GUID dedup key so updated-message
   follow-ups are not rejected as duplicates of the original new-message (openclaw#52277)

4. Retry attachment fetch from BB API (2s delay) when the initial webhook
   arrives with an empty attachments array — image-only messages and
   updated-message events only (openclaw#67437)

Closes openclaw#64105, closes openclaw#61861, closes openclaw#65430.

* fix(bluebubbles): resolve review findings — SSRF policy, reuse extractAttachments, add tests

- F1 (BLOCKER): pass undefined instead of {} for SSRF policy when
  allowPrivateNetwork is false, so localhost BB servers are not blocked.
- F2 (IMPORTANT): reuse exported extractAttachments() from monitor-normalize
  instead of duplicating field extraction logic.
- F3 (IMPORTANT): simplify asRecord(asRecord(payload)?.data) to
  asRecord(payload.data) since payload is already Record<string, unknown>.
- F4 (NIT): bind retryMessageId before the guard to eliminate non-null assertion.
- F5 (IMPORTANT): add 4 tests for fetchBlueBubblesMessageAttachments covering
  success, non-ok HTTP, empty data, and guid-less entries.
- Add CHANGELOG entry for the user-facing fix.

* fix(ci): update raw-fetch allowlist line number after dispatcher strip

* fix(bluebubbles): resolve PR review findings (openclaw#67510)

- monitor-processing: move attachment retry into the !rawBody guard so
  image-only new-message events that arrive with empty attachments and
  empty text are recovered via a BB API refetch before being dropped.
  The existing retry block at the end of processMessageAfterDedupe was
  unreachable for this case because the !rawBody early-return fired
  first. (Greptile)
- monitor: derive isAttachmentUpdate from the normalized message shape
  instead of raw payload.data.attachments so updated-message webhooks
  with attachments under wrapper formats (payload.message, JSON-string
  payloads) are correctly routed through for processing instead of
  silently filtered. (Codex)
- types: use bundled-undici fetch when init.dispatcher is present so
  the SSRF guard's DNS-pinning dispatcher is preserved when this
  function is called as fetchImpl from guarded callers (e.g. the
  attachment download path via fetchRemoteMedia). Falls back to
  globalThis.fetch when no dispatcher is present so tests that stub
  globalThis.fetch keep working. (Codex)
- attachments: blueBubblesPolicy returns undefined for the non-private
  case (matching monitor-processing's helper) so sendBlueBubblesAttachment
  stops routing localhost BB through the SSRF guard. (Greptile)
- scripts/check-no-raw-channel-fetch: bump the types.ts allowlist line
  to match the restructured non-SSRF branch.

* fix(bluebubbles): move attachment retry before rawBody guard, fix stale log

Move the attachment retry block (2s BB API refetch for empty attachments)
before the !rawBody early-return guard. Previously, image-only messages
with text='' and attachments=[] would be dropped by the !rawBody check
before the retry could fire, making fix openclaw#4 dead code for its primary
use-case. Now the retry runs first and recomputes the placeholder from
resolved attachments so rawBody becomes non-empty when media is found.

Also fix stale log message that still said 'without reaction' after the
filter was expanded to pass through attachment updates.

* fix(bluebubbles): revert undici import, restore dispatcher-strip approach

Revert the @claude bot's undici import in types.ts — it introduced a
direct 'undici' dependency that is not declared in the BB extension's
package.json and would break isolated plugin installs. Restore the
original dispatcher-strip approach which is correct: the SSRF guard
already completed validation upstream before calling this function as
fetchImpl, so stripping the dispatcher does not weaken security.

* fix(bluebubbles): remove dead empty-body recovery block in !rawBody guard

The empty-body attachment-recovery block added in the earlier PR revision
is now redundant because the main retry block was moved above the rawBody
computation in 0d7d1c4. Worse, that leftover block reassigned the
(now-const) placeholder variable, throwing `TypeError: Assignment to
constant variable` at runtime for image-only messages — breaking the very
recovery path it was meant to protect (flagged by Codex on 4bfc2777).

Remove the dead block; the up-front retry already handles the image-only
case by recovering attachments before the rawBody computation, so once we
reach the !rawBody guard with an empty body it is genuinely empty and
should drop as before.

* fix(ci): update raw-fetch allowlist line after dispatcher-strip revert

279dba1 reverted types.ts back to the dispatcher-strip approach,
which put the `fetch(url, ...)` call at line 189 instead of line 198.
Bump the allowlist entry to match so `lint:tmp:no-raw-channel-fetch`
stops failing check-additional.

* test(pdf-tool): update stale opus-4-6 constant to opus-4-7

`628b454eff feat: default Anthropic to Opus 4.7` bumped the bundled
anthropic image default to `claude-opus-4-7` but missed updating the
`ANTHROPIC_PDF_MODEL` constant in pdf-tool.model-config.test.ts. The
tests now fail on any PR that runs the `checks-node-agentic-agents-plugins`
shard because the resolver returns 4-7 while the test asserts 4-6.

Bump the constant to 4-7 to match the bundled default.

---------

Co-authored-by: Lobster <10343873+omarshahine@users.noreply.github.com>
lovewanwan pushed a commit to lovewanwan/openclaw that referenced this pull request Apr 28, 2026
…d-message events (openclaw#67510)

* fix(bluebubbles): restore inbound image attachments and accept updated-message events

Four interconnected fixes for BlueBubbles inbound media:

1. Strip bundled-undici dispatcher from non-SSRF fetch path so attachment
   downloads no longer silently fail on Node 22+ (openclaw#64105, openclaw#61861)

2. Accept updated-message webhook events that carry attachments instead of
   filtering them as non-reaction events (openclaw#65430)

3. Include eventType in the persistent GUID dedup key so updated-message
   follow-ups are not rejected as duplicates of the original new-message (openclaw#52277)

4. Retry attachment fetch from BB API (2s delay) when the initial webhook
   arrives with an empty attachments array — image-only messages and
   updated-message events only (openclaw#67437)

Closes openclaw#64105, closes openclaw#61861, closes openclaw#65430.

* fix(bluebubbles): resolve review findings — SSRF policy, reuse extractAttachments, add tests

- F1 (BLOCKER): pass undefined instead of {} for SSRF policy when
  allowPrivateNetwork is false, so localhost BB servers are not blocked.
- F2 (IMPORTANT): reuse exported extractAttachments() from monitor-normalize
  instead of duplicating field extraction logic.
- F3 (IMPORTANT): simplify asRecord(asRecord(payload)?.data) to
  asRecord(payload.data) since payload is already Record<string, unknown>.
- F4 (NIT): bind retryMessageId before the guard to eliminate non-null assertion.
- F5 (IMPORTANT): add 4 tests for fetchBlueBubblesMessageAttachments covering
  success, non-ok HTTP, empty data, and guid-less entries.
- Add CHANGELOG entry for the user-facing fix.

* fix(ci): update raw-fetch allowlist line number after dispatcher strip

* fix(bluebubbles): resolve PR review findings (openclaw#67510)

- monitor-processing: move attachment retry into the !rawBody guard so
  image-only new-message events that arrive with empty attachments and
  empty text are recovered via a BB API refetch before being dropped.
  The existing retry block at the end of processMessageAfterDedupe was
  unreachable for this case because the !rawBody early-return fired
  first. (Greptile)
- monitor: derive isAttachmentUpdate from the normalized message shape
  instead of raw payload.data.attachments so updated-message webhooks
  with attachments under wrapper formats (payload.message, JSON-string
  payloads) are correctly routed through for processing instead of
  silently filtered. (Codex)
- types: use bundled-undici fetch when init.dispatcher is present so
  the SSRF guard's DNS-pinning dispatcher is preserved when this
  function is called as fetchImpl from guarded callers (e.g. the
  attachment download path via fetchRemoteMedia). Falls back to
  globalThis.fetch when no dispatcher is present so tests that stub
  globalThis.fetch keep working. (Codex)
- attachments: blueBubblesPolicy returns undefined for the non-private
  case (matching monitor-processing's helper) so sendBlueBubblesAttachment
  stops routing localhost BB through the SSRF guard. (Greptile)
- scripts/check-no-raw-channel-fetch: bump the types.ts allowlist line
  to match the restructured non-SSRF branch.

* fix(bluebubbles): move attachment retry before rawBody guard, fix stale log

Move the attachment retry block (2s BB API refetch for empty attachments)
before the !rawBody early-return guard. Previously, image-only messages
with text='' and attachments=[] would be dropped by the !rawBody check
before the retry could fire, making fix openclaw#4 dead code for its primary
use-case. Now the retry runs first and recomputes the placeholder from
resolved attachments so rawBody becomes non-empty when media is found.

Also fix stale log message that still said 'without reaction' after the
filter was expanded to pass through attachment updates.

* fix(bluebubbles): revert undici import, restore dispatcher-strip approach

Revert the @claude bot's undici import in types.ts — it introduced a
direct 'undici' dependency that is not declared in the BB extension's
package.json and would break isolated plugin installs. Restore the
original dispatcher-strip approach which is correct: the SSRF guard
already completed validation upstream before calling this function as
fetchImpl, so stripping the dispatcher does not weaken security.

* fix(bluebubbles): remove dead empty-body recovery block in !rawBody guard

The empty-body attachment-recovery block added in the earlier PR revision
is now redundant because the main retry block was moved above the rawBody
computation in 0d7d1c4. Worse, that leftover block reassigned the
(now-const) placeholder variable, throwing `TypeError: Assignment to
constant variable` at runtime for image-only messages — breaking the very
recovery path it was meant to protect (flagged by Codex on 4bfc2777).

Remove the dead block; the up-front retry already handles the image-only
case by recovering attachments before the rawBody computation, so once we
reach the !rawBody guard with an empty body it is genuinely empty and
should drop as before.

* fix(ci): update raw-fetch allowlist line after dispatcher-strip revert

279dba1 reverted types.ts back to the dispatcher-strip approach,
which put the `fetch(url, ...)` call at line 189 instead of line 198.
Bump the allowlist entry to match so `lint:tmp:no-raw-channel-fetch`
stops failing check-additional.

* test(pdf-tool): update stale opus-4-6 constant to opus-4-7

`b539120bf0 feat: default Anthropic to Opus 4.7` bumped the bundled
anthropic image default to `claude-opus-4-7` but missed updating the
`ANTHROPIC_PDF_MODEL` constant in pdf-tool.model-config.test.ts. The
tests now fail on any PR that runs the `checks-node-agentic-agents-plugins`
shard because the resolver returns 4-7 while the test asserts 4-6.

Bump the constant to 4-7 to match the bundled default.

---------

Co-authored-by: Lobster <10343873+omarshahine@users.noreply.github.com>
ogt-redknie pushed a commit to ogt-redknie/OPENX that referenced this pull request May 2, 2026
…d-message events (openclaw#67510)

* fix(bluebubbles): restore inbound image attachments and accept updated-message events

Four interconnected fixes for BlueBubbles inbound media:

1. Strip bundled-undici dispatcher from non-SSRF fetch path so attachment
   downloads no longer silently fail on Node 22+ (openclaw#64105, openclaw#61861)

2. Accept updated-message webhook events that carry attachments instead of
   filtering them as non-reaction events (openclaw#65430)

3. Include eventType in the persistent GUID dedup key so updated-message
   follow-ups are not rejected as duplicates of the original new-message (openclaw#52277)

4. Retry attachment fetch from BB API (2s delay) when the initial webhook
   arrives with an empty attachments array — image-only messages and
   updated-message events only (openclaw#67437)

Closes openclaw#64105, closes openclaw#61861, closes openclaw#65430.

* fix(bluebubbles): resolve review findings — SSRF policy, reuse extractAttachments, add tests

- F1 (BLOCKER): pass undefined instead of {} for SSRF policy when
  allowPrivateNetwork is false, so localhost BB servers are not blocked.
- F2 (IMPORTANT): reuse exported extractAttachments() from monitor-normalize
  instead of duplicating field extraction logic.
- F3 (IMPORTANT): simplify asRecord(asRecord(payload)?.data) to
  asRecord(payload.data) since payload is already Record<string, unknown>.
- F4 (NIT): bind retryMessageId before the guard to eliminate non-null assertion.
- F5 (IMPORTANT): add 4 tests for fetchBlueBubblesMessageAttachments covering
  success, non-ok HTTP, empty data, and guid-less entries.
- Add CHANGELOG entry for the user-facing fix.

* fix(ci): update raw-fetch allowlist line number after dispatcher strip

* fix(bluebubbles): resolve PR review findings (openclaw#67510)

- monitor-processing: move attachment retry into the !rawBody guard so
  image-only new-message events that arrive with empty attachments and
  empty text are recovered via a BB API refetch before being dropped.
  The existing retry block at the end of processMessageAfterDedupe was
  unreachable for this case because the !rawBody early-return fired
  first. (Greptile)
- monitor: derive isAttachmentUpdate from the normalized message shape
  instead of raw payload.data.attachments so updated-message webhooks
  with attachments under wrapper formats (payload.message, JSON-string
  payloads) are correctly routed through for processing instead of
  silently filtered. (Codex)
- types: use bundled-undici fetch when init.dispatcher is present so
  the SSRF guard's DNS-pinning dispatcher is preserved when this
  function is called as fetchImpl from guarded callers (e.g. the
  attachment download path via fetchRemoteMedia). Falls back to
  globalThis.fetch when no dispatcher is present so tests that stub
  globalThis.fetch keep working. (Codex)
- attachments: blueBubblesPolicy returns undefined for the non-private
  case (matching monitor-processing's helper) so sendBlueBubblesAttachment
  stops routing localhost BB through the SSRF guard. (Greptile)
- scripts/check-no-raw-channel-fetch: bump the types.ts allowlist line
  to match the restructured non-SSRF branch.

* fix(bluebubbles): move attachment retry before rawBody guard, fix stale log

Move the attachment retry block (2s BB API refetch for empty attachments)
before the !rawBody early-return guard. Previously, image-only messages
with text='' and attachments=[] would be dropped by the !rawBody check
before the retry could fire, making fix openclaw#4 dead code for its primary
use-case. Now the retry runs first and recomputes the placeholder from
resolved attachments so rawBody becomes non-empty when media is found.

Also fix stale log message that still said 'without reaction' after the
filter was expanded to pass through attachment updates.

* fix(bluebubbles): revert undici import, restore dispatcher-strip approach

Revert the @claude bot's undici import in types.ts — it introduced a
direct 'undici' dependency that is not declared in the BB extension's
package.json and would break isolated plugin installs. Restore the
original dispatcher-strip approach which is correct: the SSRF guard
already completed validation upstream before calling this function as
fetchImpl, so stripping the dispatcher does not weaken security.

* fix(bluebubbles): remove dead empty-body recovery block in !rawBody guard

The empty-body attachment-recovery block added in the earlier PR revision
is now redundant because the main retry block was moved above the rawBody
computation in 0d7d1c4. Worse, that leftover block reassigned the
(now-const) placeholder variable, throwing `TypeError: Assignment to
constant variable` at runtime for image-only messages — breaking the very
recovery path it was meant to protect (flagged by Codex on 4bfc2777).

Remove the dead block; the up-front retry already handles the image-only
case by recovering attachments before the rawBody computation, so once we
reach the !rawBody guard with an empty body it is genuinely empty and
should drop as before.

* fix(ci): update raw-fetch allowlist line after dispatcher-strip revert

279dba1 reverted types.ts back to the dispatcher-strip approach,
which put the `fetch(url, ...)` call at line 189 instead of line 198.
Bump the allowlist entry to match so `lint:tmp:no-raw-channel-fetch`
stops failing check-additional.

* test(pdf-tool): update stale opus-4-6 constant to opus-4-7

`3e103fc4d4 feat: default Anthropic to Opus 4.7` bumped the bundled
anthropic image default to `claude-opus-4-7` but missed updating the
`ANTHROPIC_PDF_MODEL` constant in pdf-tool.model-config.test.ts. The
tests now fail on any PR that runs the `checks-node-agentic-agents-plugins`
shard because the resolver returns 4-7 while the test asserts 4-6.

Bump the constant to 4-7 to match the bundled default.

---------

Co-authored-by: Lobster <10343873+omarshahine@users.noreply.github.com>
github-actions Bot pushed a commit to Desicool/openclaw that referenced this pull request May 9, 2026
…d-message events (openclaw#67510)

* fix(bluebubbles): restore inbound image attachments and accept updated-message events

Four interconnected fixes for BlueBubbles inbound media:

1. Strip bundled-undici dispatcher from non-SSRF fetch path so attachment
   downloads no longer silently fail on Node 22+ (openclaw#64105, openclaw#61861)

2. Accept updated-message webhook events that carry attachments instead of
   filtering them as non-reaction events (openclaw#65430)

3. Include eventType in the persistent GUID dedup key so updated-message
   follow-ups are not rejected as duplicates of the original new-message (openclaw#52277)

4. Retry attachment fetch from BB API (2s delay) when the initial webhook
   arrives with an empty attachments array — image-only messages and
   updated-message events only (openclaw#67437)

Closes openclaw#64105, closes openclaw#61861, closes openclaw#65430.

* fix(bluebubbles): resolve review findings — SSRF policy, reuse extractAttachments, add tests

- F1 (BLOCKER): pass undefined instead of {} for SSRF policy when
  allowPrivateNetwork is false, so localhost BB servers are not blocked.
- F2 (IMPORTANT): reuse exported extractAttachments() from monitor-normalize
  instead of duplicating field extraction logic.
- F3 (IMPORTANT): simplify asRecord(asRecord(payload)?.data) to
  asRecord(payload.data) since payload is already Record<string, unknown>.
- F4 (NIT): bind retryMessageId before the guard to eliminate non-null assertion.
- F5 (IMPORTANT): add 4 tests for fetchBlueBubblesMessageAttachments covering
  success, non-ok HTTP, empty data, and guid-less entries.
- Add CHANGELOG entry for the user-facing fix.

* fix(ci): update raw-fetch allowlist line number after dispatcher strip

* fix(bluebubbles): resolve PR review findings (openclaw#67510)

- monitor-processing: move attachment retry into the !rawBody guard so
  image-only new-message events that arrive with empty attachments and
  empty text are recovered via a BB API refetch before being dropped.
  The existing retry block at the end of processMessageAfterDedupe was
  unreachable for this case because the !rawBody early-return fired
  first. (Greptile)
- monitor: derive isAttachmentUpdate from the normalized message shape
  instead of raw payload.data.attachments so updated-message webhooks
  with attachments under wrapper formats (payload.message, JSON-string
  payloads) are correctly routed through for processing instead of
  silently filtered. (Codex)
- types: use bundled-undici fetch when init.dispatcher is present so
  the SSRF guard's DNS-pinning dispatcher is preserved when this
  function is called as fetchImpl from guarded callers (e.g. the
  attachment download path via fetchRemoteMedia). Falls back to
  globalThis.fetch when no dispatcher is present so tests that stub
  globalThis.fetch keep working. (Codex)
- attachments: blueBubblesPolicy returns undefined for the non-private
  case (matching monitor-processing's helper) so sendBlueBubblesAttachment
  stops routing localhost BB through the SSRF guard. (Greptile)
- scripts/check-no-raw-channel-fetch: bump the types.ts allowlist line
  to match the restructured non-SSRF branch.

* fix(bluebubbles): move attachment retry before rawBody guard, fix stale log

Move the attachment retry block (2s BB API refetch for empty attachments)
before the !rawBody early-return guard. Previously, image-only messages
with text='' and attachments=[] would be dropped by the !rawBody check
before the retry could fire, making fix openclaw#4 dead code for its primary
use-case. Now the retry runs first and recomputes the placeholder from
resolved attachments so rawBody becomes non-empty when media is found.

Also fix stale log message that still said 'without reaction' after the
filter was expanded to pass through attachment updates.

* fix(bluebubbles): revert undici import, restore dispatcher-strip approach

Revert the @claude bot's undici import in types.ts — it introduced a
direct 'undici' dependency that is not declared in the BB extension's
package.json and would break isolated plugin installs. Restore the
original dispatcher-strip approach which is correct: the SSRF guard
already completed validation upstream before calling this function as
fetchImpl, so stripping the dispatcher does not weaken security.

* fix(bluebubbles): remove dead empty-body recovery block in !rawBody guard

The empty-body attachment-recovery block added in the earlier PR revision
is now redundant because the main retry block was moved above the rawBody
computation in 0d7d1c4. Worse, that leftover block reassigned the
(now-const) placeholder variable, throwing `TypeError: Assignment to
constant variable` at runtime for image-only messages — breaking the very
recovery path it was meant to protect (flagged by Codex on 4bfc2777).

Remove the dead block; the up-front retry already handles the image-only
case by recovering attachments before the rawBody computation, so once we
reach the !rawBody guard with an empty body it is genuinely empty and
should drop as before.

* fix(ci): update raw-fetch allowlist line after dispatcher-strip revert

279dba1 reverted types.ts back to the dispatcher-strip approach,
which put the `fetch(url, ...)` call at line 189 instead of line 198.
Bump the allowlist entry to match so `lint:tmp:no-raw-channel-fetch`
stops failing check-additional.

* test(pdf-tool): update stale opus-4-6 constant to opus-4-7

`98a49ab365 feat: default Anthropic to Opus 4.7` bumped the bundled
anthropic image default to `claude-opus-4-7` but missed updating the
`ANTHROPIC_PDF_MODEL` constant in pdf-tool.model-config.test.ts. The
tests now fail on any PR that runs the `checks-node-agentic-agents-plugins`
shard because the resolver returns 4-7 while the test asserts 4-6.

Bump the constant to 4-7 to match the bundled default.

---------

Co-authored-by: Lobster <10343873+omarshahine@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment