Skip to content

feat(tools/bb): reply, edit, and unsend message actions#2583

Closed
maxtongwang wants to merge 90 commits intozeroclaw-labs:mainfrom
maxtongwang:feat/bb-issue5-upstream-pr
Closed

feat(tools/bb): reply, edit, and unsend message actions#2583
maxtongwang wants to merge 90 commits intozeroclaw-labs:mainfrom
maxtongwang:feat/bb-issue5-upstream-pr

Conversation

@maxtongwang
Copy link
Copy Markdown

@maxtongwang maxtongwang commented Mar 2, 2026

Summary

  • Base branch target: main
  • Problem: The LLM agent cannot reply to a specific iMessage thread, edit a sent message, or retract a sent message — limiting conversational accuracy.
  • Why it matters: Thread-aware reply, edit, and unsend are standard iMessage features required for full agent parity with OpenClaw's BlueBubbles extension.
  • What changed: New bluebubbles_message tool with a single action dispatch (reply, edit, unsend). Uses BB private-API endpoints: reply via selectedMessageGuid, edit via /message/{guid}/edit, unsend via /message/{guid}/unsend. Registered in the tool factory alongside group and send-attachment tools.
  • What did not change: No changes to the channel receive path, config schema, or existing tools.

Label Snapshot (required)

  • Risk label: risk: medium
  • Size label: size: S
  • Scope labels: tool
  • Module labels: tool: bluebubbles
  • Contributor tier label: (auto-managed)

Change Metadata

  • Change type: feature
  • Primary scope: tool

Linked Issue

Supersede Attribution (required when Supersedes # is used)

N/A

Validation Evidence (required)

cargo fmt --all -- --check   # pass
cargo clippy --all-targets -- -D warnings  # pass (delta scope)
cargo test   # pass

Security Impact (required)

  • New permissions/capabilities? Yes — agent can now edit/delete previously sent iMessages.
  • New external network calls? Yes — PATCH/DELETE to BB server message endpoints.
  • Secrets/tokens handling changed? No
  • File system access scope changed? No
  • Risk: Edit/unsend could modify messages the user did not intend. Mitigated by the agent only acting on explicit user instruction.

Privacy and Data Hygiene (required)

  • Data-hygiene status: pass
  • Neutral wording confirmation: pass

Compatibility / Migration

  • Backward compatible? Yes — purely additive
  • Config/env changes? No
  • Migration needed? No

i18n Follow-Through (required when docs or user-facing wording changes)

  • i18n follow-through triggered? No — no user-facing docs updated.

Human Verification (required)

  • Verified scenarios: all three actions (reply, edit, unsend) schema validated against BB API docs; partIndex default 0 documented.
  • Edge cases: empty message_id, missing text for reply/edit, unknown action returns structured error.
  • What was not verified: live iMessage thread on physical hardware.

Side Effects / Blast Radius (required)

  • Affected subsystems: src/tools/ (new file + mod.rs registration).
  • Potential unintended effects: None — purely additive.

Agent Collaboration Notes (recommended)

  • Agent tools used: Claude Code
  • Confirmation: naming + architecture boundaries followed per AGENTS.md + CONTRIBUTING.md.

Rollback Plan (required)

  • Fast rollback: revert src/tools/bluebubbles_message.rs + three-line mod.rs change.
  • Feature flags: none.
  • Observable failure symptoms: BlueBubblesMessageTool absent from tool list; API failures surface as ToolResult { success: false }.

Risks and Mitigations

  • Risk: unsend on already-read messages may fail silently on older iOS.
    • Mitigation: BB server returns HTTP error; tool surfaces it as ToolResult { success: false, error: Some(...) }.

Summary by CodeRabbit

  • New Features

    • BlueBubbles iMessage channel integration with webhook support
    • Audio message transcription via local Whisper
    • Message reactions and tapback support
    • OAuth authentication system (Google with PKCE)
    • BlueBubbles group management, messaging, and attachment tools
    • DM and group chat policy controls (Open/Allowlist/Disabled)
    • Read receipt support for BlueBubbles
  • Documentation

    • Updated channel reference with BlueBubbles and ACP configuration
    • Extended configuration documentation with policy and transcription settings
    • Added internationalization support (Greek, Vietnamese)
  • Bug Fixes

    • Fixed HTTP redirect handling to properly validate policy for redirect targets

Review Fixes (Round 2)

Propagated docs fix from base branch feat/bluebubbles-transcription-tapback via rebase:

Chinese command text in English docs: docs/channels-reference.md natural-language approval command examples updated from 授权工具 shell to approve tool shell.

Review Fixes (Round 3)

No-panic constructor: bluebubbles_message.rs replaced .expect("valid reqwest client config") with .unwrap_or_else(|_| reqwest::Client::new()) to prevent panic on TLS misconfiguration.

Also propagated from base branches: transcription temp dir cleanup, error_page title HTML escaping, icon size check on decoded bytes.

Review Fixes (Round 6)

  • oauth.rs: propagate HTTP client build failure — no silent timeout loss
  • oauth.rs: validate PKCE state before consuming session file (CSRF fix)
  • oauth.rs: use POST form body for token revocation (RFC 7009 compliance)
  • transcription.rs: explicit fail on non-UTF-8 temp paths, not empty-string passthrough
  • transcription.rs: clean up partial ffmpeg WAV on conversion failure
  • bluebubbles.rs: remove attachment GUID from error log
  • claude-review.yml: fix step-level PR condition (!= ''!= null)

Review Fixes (Round 7)

  • bluebubbles_message.rs: explicit anyhow::Result<Self> constructor
  • bluebubbles_message.rs: all 3 transport error paths (reply, edit, unsend) return Ok(ToolResult { success: false }) instead of propagating Err
  • src/tools/mod.rs: registration updated to handle anyhow::Result constructor

Review Fixes (Round 8)

  • bluebubbles_message.rs: inject Arc<SecurityPolicy> — added can_act() and record_action() checks matching the pattern used by all other side-effect tools (ShellTool, FileReadTool, CronAddTool, etc.)

Review Fixes (cargo fmt)

  • Applied cargo fmt --all to files changed in this PR's diff — collapsed single-use iterator chains and reformatted match arm bodies to comply with project rustfmt settings

maxtongwang and others added 30 commits February 27, 2026 12:06
Adds @claude mention support on PRs using anthropics/claude-code-action@v1.
Mirrors the workflow from maxtongwang/openclaw.

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds use_sticky_comment: true to claude-review workflow so all review
updates are consolidated into a single comment instead of flooding the
PR with multiple comments.

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds BlueBubblesChannel — iMessage via self-hosted BlueBubbles macOS server.

- Webhook mode: receives new-message events at POST /bluebubbles
- Sender normalization: strips imessage:/sms:/auto: prefixes, lowercases emails
- fromMe caching: FIFO VecDeque+HashMap cache with reply context injection
- Optional inbound webhook auth: Authorization: Bearer via webhook_secret config
- Outbound send: POST /api/v1/message/text with ?password= query param + tempGuid
- Health check: GET /api/v1/ping (matches OpenClaw probeBlueBubbles)
- constant_time_eq() for secret comparison; manual Debug redacts password

Closes #1

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add a second BlueBubbles endpoint for a personal Apple ID that stores
incoming iMessages to shared memory but never calls the LLM or sends
a reply. This enables multi-tenant iMessage setups where one ZeroClaw
process handles both a bot account (POST /bluebubbles) and a personal
account (POST /bluebubbles-personal) from two BlueBubbles instances.

Changes:
- src/config/schema.rs: add bluebubbles_personal: Option<BlueBubblesConfig>
  to ChannelsConfig (reuses existing BlueBubblesConfig struct)
- src/gateway/mod.rs: add bluebubbles_personal field to AppState,
  wire construction, register POST /bluebubbles-personal route, and
  implement handle_bluebubbles_personal_webhook handler

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- [build] rustc-wrapper = "sccache" in .cargo/config.toml — caches
  compiled crates across clean builds; sccache auto-starts on first use
- [profile.dev] split-debuginfo = "unpacked" in Cargo.toml — skips
  dsymutil on macOS, eliminating post-link debug-symbol packaging delay

Note: mold 2.40.4 dropped Mach-O support so it cannot be used as a
macOS linker; split-debuginfo is the primary link-time win on macOS.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Convert Discord-style markdown in LLM responses to BB Private API
attributedBody segments for iMessage rendering:
- **bold** → {bold: true}
- *italic* → {italic: true}
- ~~strikethrough~~ → {strikethrough: true}
- __underline__ → {underline: true}
Markers nest arbitrarily; plain-text fallback strips markers.
Update system prompt to instruct LLM to use rich markdown + emoji.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add start_typing/stop_typing to BlueBubblesChannel using BB Private API
POST /api/v1/chat/{chatGuid}/typing. Indicator refreshes every 4s since
BB typing events expire in ~5s. Gateway handler starts indicator before
LLM call and stops it (both success and error paths) before sending reply.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- markdown_to_attributed_body: convert **bold**, *italic*, ~~strike~~,
  __underline__, `code` (→bold), # headers (→bold), ``` blocks (plain)
  into BB Private API attributedBody segments
- start_typing/stop_typing: background loop refreshing BB typing
  indicator every 4s while LLM processes; aborted on response
- EFFECT_MAP + extract_effect(): LLM can append [EFFECT:name] tags
  (slam, loud, gentle, invisible-ink, confetti, balloons, fireworks,
  lasers, love, celebration, echo, spotlight); stripped from content,
  passed as effectId to BB Private API
- Updated channel_delivery_instructions with full style + effect guide
- 38 unit tests passing

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add ignore_senders to BlueBubblesConfig and BlueBubblesChannel
- Personal webhook handler skips storing messages where sender is in ignore_senders
- Configure ignore_senders = ["tongtong901005@gmail.com"] in config.toml
- Fix release-fast profile: lto=false, codegen-units=16, opt-level=2 for fast local builds with sccache support

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Direct LLM to use web_fetch on canonical sources (ESPN, wttr.in, etc.)
  for sports, weather, news — not just web_search snippets
- Remove "be concise" instruction that was cutting the tool loop short
- Instruct bot to complete full research before replying
- Keep all text style and effect guidance

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…l endpoint

Port all three CodeRabbit improvements from feat/channel-bluebubbles to
fork main and extend them consistently to the bluebubbles_personal endpoint:

- typing_handles: replace single Mutex<Option<JoinHandle>> with
  Mutex<HashMap<String, JoinHandle>> keyed by chat GUID so concurrent
  conversations do not cancel each other's typing indicators
- is_sender_ignored(): new method checked before is_sender_allowed() in
  parse_webhook_payload so ignore_senders always takes precedence over
  the allowlist; removes the now-redundant inline ignore check from the
  personal handler since parse_webhook_payload handles it upstream
- Secret wiring: add bluebubbles and bluebubbles_personal password +
  webhook_secret to decrypt_channel_secrets and encrypt_channel_secrets
- Debug impl: add ignore_senders field to BlueBubblesConfig debug output
- Tests: add three unit tests covering ignore_senders exact match,
  precedence over allowlist, and empty-list no-op behaviour (41 total)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Conflicts resolved:
- .gitignore: keep fork's test-config.toml entry alongside upstream result
- Cargo.lock: accept upstream (no new deps from BB channel)
- src/config/schema.rs: keep BlueBubblesConfig + bluebubbles/personal fields
  alongside upstream GitHubConfig; wire both into encrypt/decrypt
- src/gateway/mod.rs: keep BB + personal handlers alongside upstream GitHub
  handler; update run_gateway_chat_with_tools calls to 3-arg signature;
  add missing BB fields to new GitHub test fixtures

Also fixes:
- src/providers/cursor.rs: add missing quota_metadata field (pre-existing
  upstream bug now caught by stricter ChatResponse struct)
- BB handler: update sanitize_gateway_response to 3-arg form matching
  upstream API change (adds leak_guard parameter)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
BlueBubbles channel now downloads and transcribes audio attachments when
`[transcription]` is configured. iMessage sends voice memos in Core Audio
Format (CAF) which Groq Whisper does not accept natively; CAF files are
converted to WAV via ffmpeg before upload.

Changes:
- transcription.rs: add `convert_caf_to_wav` — detects CAF by extension,
  writes to temp files (seekable input required), runs `ffmpeg -ar 16000
  -ac 1`, cleans up on all exit paths; returns Ok(None) for non-CAF so
  callers reuse original bytes
- bluebubbles.rs: add `transcription` field, `with_transcription` builder,
  `AudioAttachment` struct, `extract_audio_attachments`, `download_attachment`
  (percent-encoding inline), `download_and_transcribe`, and public async
  `parse_webhook_payload_with_transcription`; gracefully falls back to
  `<media:audio>` placeholder when transcription is disabled, ffmpeg is
  absent, or the API call fails
- gateway/mod.rs: wire `config.transcription.clone()` into both BB channel
  constructions; replace synchronous `parse_webhook_payload` call with
  async `parse_webhook_payload_with_transcription` in both webhook handlers

No new Cargo dependencies — uses tokio::process and tokio::fs already
present. No new config keys — wired through the existing `[transcription]`
block.

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Mirrors OpenClaw approach: read attachment from `original_path` in BB
webhook payload (local filesystem, no REST download) and shell out to
the `whisper` CLI. Python whisper handles CAF (iMessage voice memos)
natively via ffmpeg — no pre-conversion step needed.

- Remove convert_caf_to_wav + ffmpeg + Groq HTTP download path
- Add transcribe_audio_local: resolves whisper in PATH and common
  Homebrew/system paths, runs with --model turbo --output_format txt,
  reads <tmpdir>/<stem>.txt, cleans up on all exit paths
- extract_audio_attachments: use originalPath instead of guid
- transcribe_local: read file directly, no REST API call
- Fix pre-existing build break: add OauthConfig/GoogleOauthConfig
  schema types + re-export so oauth.rs compiles

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
BB webhook attachments do not include originalPath; extract guid instead
and download bytes via /api/v1/attachment/{guid}/download, then write to
a temp file and pass to the local whisper CLI for transcription.

BB converts CAF→MP3 before webhooking so the download is typically
audio/mpeg — no pre-conversion needed.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Two fixes:
1. Instead of REST API download, read the BB-converted MP3 directly from
   the local filesystem at the predictable path:
   ~/Library/Application Support/bluebubbles-server/Convert/{guid}/{name}.mp3
   BB converts CAF→MP3 before sending the webhook; since ZeroClaw runs on
   the same Mac as BB, no network call is needed.

2. Spawn webhook processing in background (tokio::spawn) so the handler
   returns 200 immediately. BB times out webhooks at 30 s, but whisper
   transcription takes longer — caused 408 timeouts with the previous
   synchronous approach.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
BB's transferName in webhooks is inconsistent — sometimes includes .mp3
already, causing triple-extension paths. Instead of constructing the
filename from transferName, scan the Convert/{guid}/ directory for the
first audio file (mp3/m4a/aac/wav/ogg). This is robust to any BB
filename convention.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace incorrect \!escaped.contains('&') assertion — HTML entity encoding uses & characters (&amp;, &lt;, etc.), so this always failed. Assert contains("&amp;") instead.
Replaces broken filesystem-based audio transcription with BB REST API download.
Adds dual-backend transcription (local whisper CLI primary, Groq API fallback),
inbound tapback surfacing via updated-message webhooks, and outbound reactions
via add_reaction/remove_reaction on the Channel trait.

Key changes:
- download_attachment_bytes: 25 MB cap (Content-Length + body), 30s HTTP timeout
- resolve_whisper_bin: OnceLock cache — binary search runs once at startup
- parse_tapback_event: surfaces ❤️/👍/👎/😂/‼️/❓ as [Tapback] system messages
- add_reaction/remove_reaction: BB Private API /api/v1/message/react
- 58 tests, all green
fetch_with_http_provider used Policy::none() but returned Ok(redirected_url) instead of fetching the target. Replace response with the fetched redirect target and fall through to shared body processing.
Brings in upstream commits through 3726d82.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

# Conflicts:
#	.cargo/config.toml
#	.gitignore
#	Cargo.toml
#	src/channels/bluebubbles.rs
#	src/channels/mod.rs
#	src/config/mod.rs
#	src/config/schema.rs
#	src/gateway/mod.rs
…nc parse

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ng indicator (#14)

- Replace slow Python whisper (~30–90 s) with whisper-cli (whisper.cpp,
  Metal-accelerated, ~1–2 s) for local audio transcription.
- Pre-convert all non-WAV audio to 16 kHz mono WAV via ffmpeg; the brew
  build of whisper-cli only reliably reads WAV.
- Use OnceLock for binary/model path resolution to avoid repeated lookups.
- Start typing indicator before transcription runs so users see feedback
  immediately rather than waiting for the full whisper phase.

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
…k reactions, and whisper-cli fast-path

Extends the BlueBubbles iMessage channel with the following capabilities
on top of the existing base implementation:

- REST API audio download: fetches voice memo attachments directly from
  the BlueBubbles server via its REST API, enabling reliable access
  without filesystem access.

- Tapback reactions: parses and surfaces reaction events (heart, thumbs
  up, thumbs down, ha ha, !!, ?) so the agent can understand user
  sentiment and react appropriately.

- Rich text / attributed body: parses Apple's attributedBody format to
  extract plain text from styled iMessage content, improving message
  fidelity for formatted messages and inline attachments.

- Typing indicator before transcription: sends a typing indicator
  immediately when a voice memo is detected, giving users real-time
  feedback before the potentially slower transcription step.

- Local whisper-cli fast-path (whisper.cpp): prefers the locally
  installed `whisper-cli` (from whisper.cpp, e.g. via `brew install
  whisper-cpp`) over the Groq API when available. On Apple Silicon with
  Metal acceleration this cuts transcription latency from 30-90 s to
  ~1-2 s. WAV files are passed directly; non-WAV audio is converted via
  ffmpeg before passing to whisper-cli. Falls back to the Groq API
  automatically when whisper-cli is not installed.

- Stderr diagnostics: surfaces whisper-cli stderr output in agent logs
  when transcription fails, making it easier to debug environment issues.

Files changed:
- src/channels/bluebubbles.rs: AudioAttachment struct, REST download,
  tapback parsing, rich text extraction, typing indicator wiring
- src/channels/transcription.rs: local whisper-cli fast-path with ffmpeg
  pre-conversion and graceful Groq API fallback
- src/gateway/mod.rs: wire .with_transcription() into channel
  construction; call parse_webhook_payload_with_transcription (async)
- Redact reqwest password URL from error messages with `.without_url()`
  in download_attachment_bytes, add_reaction, and remove_reaction
- Replace racy `as_nanos()` temp-file names with `uuid::Uuid::new_v4()`
  in bluebubbles.rs (audio download) and transcription.rs (ffmpeg/whisper)
- Gate typing indicator on fromMe/allowlist checks before `start_typing()`
  to avoid leaking presence for ignored/disallowed senders
- Add `tokio::time::timeout(120s)` + `kill_on_drop(true)` for ffmpeg,
  whisper-cli, and python whisper subprocesses to bound execution time
- Add Intel Homebrew model paths (`/usr/local/opt/whisper-cpp/...`) to
  MODELS array alongside Apple Silicon paths
- Move transcription+LLM loop into `tokio::spawn` background task and
  return 202 ACCEPTED immediately to stay within gateway timeout budget

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
maxtongwang and others added 7 commits March 3, 2026 17:32
Adds BlueBubblesSendAttachmentTool allowing the LLM to send images, audio,
and documents via iMessage through the BB Private API multipart endpoint
(/api/v1/message/attachment).

Parameters: chat_guid, filename, data_base64, mime_type, caption (optional),
as_voice (optional). Base64 is decoded before upload. Invalid base64 returns
a structured error without making a network call.

Registered in all_tools_with_runtime alongside BlueBubblesGroupTool when
channels_config.bluebubbles is present.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add 50 MiB size cap on data_base64 before base64 decode (OOM guard)
- MIME type failure is now a hard error (was: silent empty-file fallback)
- Remove mime_type from schema required[] — it defaults to octet-stream
- Update mime_type description to document the default

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Replace .expect() with .unwrap_or_else(|_| reqwest::Client::new()) so
  client-build failure (TLS misconfiguration) degrades gracefully instead
  of panicking at tool registration time
- Add .trim() to chat_guid and filename validation to reject whitespace-
  only inputs consistently with other BB tool validators

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@maxtongwang
Copy link
Copy Markdown
Author

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Mar 4, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

maxtongwang and others added 8 commits March 3, 2026 17:33
BlueBubbles' Private API voice memo flag expects an audio attachment.
Validate mime_type starts with "audio/" when as_voice=true and return
a clear ToolResult error instead of silently sending an invalid request.
Adds BlueBubblesMessageTool with action-dispatch pattern for three
iMessage mutation operations: reply (thread-aware), edit message text,
and retract/unsend a sent message. Registered in tool factory when
BlueBubbles channel is configured.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Require non-empty action with fail-fast guard (was: silent default)
- Trim message_id before empty check
- Make chat_guid optional in schema; validate + use only inside reply branch
  (edit and unsend only need message_id — forcing chat_guid confused LLM agents)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Trim text before empty check in reply and edit (consistent with
  action/message_id/chat_guid pattern)
- Add comment on partIndex: 0 to clarify multi-part unsend not supported

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace .expect() with .unwrap_or_else(|_| reqwest::Client::new()) so
client-build failure (TLS misconfiguration) degrades gracefully instead
of panicking at tool registration time.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@maxtongwang
Copy link
Copy Markdown
Author

@coderabbitai full review

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Mar 4, 2026

✅ Actions performed

Full review triggered.

@maxtongwang
Copy link
Copy Markdown
Author

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Mar 4, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

♻️ Duplicate comments (6)
src/tools/bluebubbles_send_attachment.rs (2)

161-165: ⚠️ Potential issue | 🟡 Minor

Trim caption before using it in the multipart form.

Whitespace-only captions currently pass through and are sent as message text. Trim at ingestion to keep sanitization consistent with other string inputs.

🧼 Suggested patch
         let caption = args
             .get("caption")
             .and_then(|v| v.as_str())
+            .map(str::trim)
             .unwrap_or("")
             .to_string();
As per coding guidelines: `src/tools/**/*.rs` requires validating and sanitizing all inputs.

Also applies to: 217-219

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/tools/bluebubbles_send_attachment.rs` around lines 161 - 165, The caption
string extracted via args.get("caption").and_then(|v|
v.as_str()).unwrap_or("").to_string() must be trimmed immediately to strip
whitespace-only values; replace the current assignment with one that calls
.trim() (and consider .to_string() after trimming) so empty/whitespace captions
become empty strings before being added to the multipart form, and apply the
same trimming/sanitization to the other caption extraction occurrences around
the code referenced (similar to lines 217-219) to keep input handling
consistent; update any downstream logic that checks for empty captions to rely
on the trimmed value (e.g., the local variable caption).

128-191: ⚠️ Potential issue | 🟡 Minor

Enforce a hard decoded-byte limit after base64 decode.

Line 128 checks encoded size only. Add an explicit file_bytes.len() cap after Line 182 so boundary behavior is exact and deterministic.

📏 Suggested patch
-        const MAX_ATTACHMENT_B64_LEN: usize = 34 * 1024 * 1024; // ~25 MiB decoded (base64 overhead ~4/3)
+        const MAX_ATTACHMENT_BYTES: usize = 25 * 1024 * 1024;
+        const MAX_ATTACHMENT_B64_LEN: usize = ((MAX_ATTACHMENT_BYTES + 2) / 3) * 4;
@@
         let file_bytes =
             match base64::Engine::decode(&base64::engine::general_purpose::STANDARD, data_b64) {
                 Ok(b) => b,
@@
             };
+        if file_bytes.len() > MAX_ATTACHMENT_BYTES {
+            return Ok(ToolResult {
+                success: false,
+                output: String::new(),
+                error: Some(format!(
+                    "decoded attachment exceeds maximum allowed size ({MAX_ATTACHMENT_BYTES} bytes)"
+                )),
+            });
+        }
As per coding guidelines: `src/{security,runtime,gateway,tools}/**/*.rs` changes should include boundary/failure-mode validation.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/tools/bluebubbles_send_attachment.rs` around lines 128 - 191, The code
only enforces an encoded-size cap (MAX_ATTACHMENT_B64_LEN) but not a hard
decoded-byte limit; after the base64 decode that produces file_bytes (the Ok(b)
arm in the match that assigns file_bytes) add a boundary check like a
MAX_ATTACHMENT_DECODED_LEN constant and if file_bytes.len() >
MAX_ATTACHMENT_DECODED_LEN return the same ToolResult failure shape with an
error message indicating the decoded attachment exceeds the allowed size; place
this check immediately after the decode success (where file_bytes is assigned)
so failure-mode is deterministic and consistent with other validations
(reference the file_bytes variable and the base64::Engine::decode match).
src/gateway/mod.rs (4)

2377-2378: ⚠️ Potential issue | 🟡 Minor

Fix stale response-code comment (200 vs 202).

The comment says “Returning 200 immediately”, but the handler returns StatusCode::ACCEPTED (202) at Line 2485.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/gateway/mod.rs` around lines 2377 - 2378, The comment above the handler
is stale: it says “Returning 200 immediately” but the code returns
StatusCode::ACCEPTED (202). Update the comment to reflect the actual response
code (202 / StatusCode::ACCEPTED) and/or reference "Accepted" semantics so it
matches the handler that returns StatusCode::ACCEPTED (e.g., in the async
request handler where the comment appears).

562-591: ⚠️ Potential issue | 🟠 Major

Do not silently disable BlueBubbles on constructor failure.

Line 590-Line 591 converts initialization errors into None, which can hide misconfiguration and continue with reduced capabilities.

Suggested fix
 let bluebubbles_channel: Option<Arc<BlueBubblesChannel>> = config
     .channels_config
     .bluebubbles
     .as_ref()
@@
         .transpose()
-        .map_err(|e| {
-            tracing::error!("Failed to initialize BlueBubbles channel: {e}");
-            e
-        })
-        .ok()
-        .flatten();
+        .map_err(|e| anyhow::anyhow!("Failed to initialize BlueBubbles channel: {e}"))?;

As per coding guidelines: src/**/*.rs: “Prefer explicit bail!/errors for unsupported or unsafe states; never silently broaden permissions/capabilities.”

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/gateway/mod.rs` around lines 562 - 591, The current chain around
BlueBubblesChannel::new maps initialization errors into None
(bluebubbles_channel) which silently disables the channel; instead, surface the
error so initialization fails loudly: remove the .ok().flatten() pattern and the
.map_err(...).ok() swallowing, and return or propagate the error (e.g., using
anyhow::bail! or ? from the surrounding function) when BlueBubblesChannel::new
(and its .with_policies/.with_transcription) fails, so misconfiguration does not
silently continue with reduced capabilities.

2381-2482: ⚠️ Potential issue | 🟠 Major

Add an outer timeout to bound semaphore permit lifetime.

The detached worker keeps the permit until the task finishes; if downstream work hangs, permits can be pinned indefinitely and eventually starve the pool.

Suggested fix
 tokio::spawn(async move {
     let _permit = permit; // released when this task completes
-    let messages = bb.parse_webhook_payload_with_transcription(&payload).await;
+    let work = async {
+        let messages = bb.parse_webhook_payload_with_transcription(&payload).await;
 
-    // Delivery receipt: send immediately for ALL inbound group messages, whether
-    // or not they pass the mention gate.  This shows "Delivered" on the sender's
-    // side as long as ZeroClaw is online.  DM delivery is handled by the OS/iMessage
-    // stack automatically; we only need to fire it explicitly for group chats.
-    {
+        // existing background processing body...
+        {
             // existing logic unchanged
-    }
+        }
 
-    for msg in &messages {
-        // existing logic unchanged
-    }
+        for msg in &messages {
+            // existing logic unchanged
+        }
+    };
+
+    if tokio::time::timeout(Duration::from_secs(180), work)
+        .await
+        .is_err()
+    {
+        tracing::error!("BlueBubbles background worker timed out");
+    }
 });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/gateway/mod.rs` around lines 2381 - 2482, The spawned worker currently
captures the semaphore permit via let _permit = permit and holds it until the
whole async task finishes, so add an outer timeout (using tokio::time::timeout)
around the task's main work to bound how long the permit can be held: wrap the
body that starts with messages =
bb.parse_webhook_payload_with_transcription(&payload).await and includes the for
msg in &messages loop and run_gateway_chat_with_tools(...) in a
tokio::time::timeout(duration, async move { ... }).await, handle the
Err(elapsed) case by logging the timeout and explicitly dropping the permit
(i.e., ensuring _permit is dropped) so the semaphore is released, and keep the
normal success path unchanged so permit is released when the task completes.

2358-2375: ⚠️ Potential issue | 🟠 Major

Please add explicit tests for the new BlueBubbles 202/503 boundary behavior.

This path introduces important queue-boundary semantics (accepted vs exhausted) but there is no direct assertion in this file’s tests for those outcomes.

Based on learnings: for src/security/**/*.rs|src/runtime/**/*.rs|src/gateway/**/*.rs|src/tools/**/*.rs, include at least one boundary/failure-mode validation and document rollback strategy.

Also applies to: 2484-2487

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/gateway/mod.rs` around lines 2358 - 2375, Add explicit unit/integration
tests that assert the BlueBubbles webhook path returns 202 when
BB_WORKER_SEMAPHORE.try_acquire_owned() succeeds and returns
StatusCode::SERVICE_UNAVAILABLE (503) with the JSON error payload when the
semaphore is exhausted; simulate both cases by mocking or temporarily replacing
BB_WORKER_SEMAPHORE to provide an owned permit and to force try_acquire_owned()
-> Err, then call the handler (the function invoking try_acquire_owned) and
assert the response status and body match the expected 202/JSON and
503/{"error":"worker pool exhausted, retry later"} outcomes; also add a short
doc comment or test metadata describing the rollback/retry strategy for the
queued work in failure mode so the boundary behavior is documented alongside the
tests.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@src/gateway/mod.rs`:
- Around line 2377-2378: The comment above the handler is stale: it says
“Returning 200 immediately” but the code returns StatusCode::ACCEPTED (202).
Update the comment to reflect the actual response code (202 /
StatusCode::ACCEPTED) and/or reference "Accepted" semantics so it matches the
handler that returns StatusCode::ACCEPTED (e.g., in the async request handler
where the comment appears).
- Around line 562-591: The current chain around BlueBubblesChannel::new maps
initialization errors into None (bluebubbles_channel) which silently disables
the channel; instead, surface the error so initialization fails loudly: remove
the .ok().flatten() pattern and the .map_err(...).ok() swallowing, and return or
propagate the error (e.g., using anyhow::bail! or ? from the surrounding
function) when BlueBubblesChannel::new (and its
.with_policies/.with_transcription) fails, so misconfiguration does not silently
continue with reduced capabilities.
- Around line 2381-2482: The spawned worker currently captures the semaphore
permit via let _permit = permit and holds it until the whole async task
finishes, so add an outer timeout (using tokio::time::timeout) around the task's
main work to bound how long the permit can be held: wrap the body that starts
with messages = bb.parse_webhook_payload_with_transcription(&payload).await and
includes the for msg in &messages loop and run_gateway_chat_with_tools(...) in a
tokio::time::timeout(duration, async move { ... }).await, handle the
Err(elapsed) case by logging the timeout and explicitly dropping the permit
(i.e., ensuring _permit is dropped) so the semaphore is released, and keep the
normal success path unchanged so permit is released when the task completes.
- Around line 2358-2375: Add explicit unit/integration tests that assert the
BlueBubbles webhook path returns 202 when
BB_WORKER_SEMAPHORE.try_acquire_owned() succeeds and returns
StatusCode::SERVICE_UNAVAILABLE (503) with the JSON error payload when the
semaphore is exhausted; simulate both cases by mocking or temporarily replacing
BB_WORKER_SEMAPHORE to provide an owned permit and to force try_acquire_owned()
-> Err, then call the handler (the function invoking try_acquire_owned) and
assert the response status and body match the expected 202/JSON and
503/{"error":"worker pool exhausted, retry later"} outcomes; also add a short
doc comment or test metadata describing the rollback/retry strategy for the
queued work in failure mode so the boundary behavior is documented alongside the
tests.

In `@src/tools/bluebubbles_send_attachment.rs`:
- Around line 161-165: The caption string extracted via
args.get("caption").and_then(|v| v.as_str()).unwrap_or("").to_string() must be
trimmed immediately to strip whitespace-only values; replace the current
assignment with one that calls .trim() (and consider .to_string() after
trimming) so empty/whitespace captions become empty strings before being added
to the multipart form, and apply the same trimming/sanitization to the other
caption extraction occurrences around the code referenced (similar to lines
217-219) to keep input handling consistent; update any downstream logic that
checks for empty captions to rely on the trimmed value (e.g., the local variable
caption).
- Around line 128-191: The code only enforces an encoded-size cap
(MAX_ATTACHMENT_B64_LEN) but not a hard decoded-byte limit; after the base64
decode that produces file_bytes (the Ok(b) arm in the match that assigns
file_bytes) add a boundary check like a MAX_ATTACHMENT_DECODED_LEN constant and
if file_bytes.len() > MAX_ATTACHMENT_DECODED_LEN return the same ToolResult
failure shape with an error message indicating the decoded attachment exceeds
the allowed size; place this check immediately after the decode success (where
file_bytes is assigned) so failure-mode is deterministic and consistent with
other validations (reference the file_bytes variable and the
base64::Engine::decode match).

ℹ️ Review info
Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 52da9d61-69fd-4db9-b69d-ba47be72d4e0

📥 Commits

Reviewing files that changed from the base of the PR and between 361c5f5 and 47699f9.

📒 Files selected for processing (8)
  • docs/channels-reference.md
  • docs/i18n/vi/channels-reference.md
  • src/channels/bluebubbles.rs
  • src/gateway/mod.rs
  • src/tools/bluebubbles_group.rs
  • src/tools/bluebubbles_message.rs
  • src/tools/bluebubbles_send_attachment.rs
  • src/tools/mod.rs
🚧 Files skipped from review as they are similar to previous changes (3)
  • src/tools/mod.rs
  • src/tools/bluebubbles_group.rs
  • docs/i18n/vi/channels-reference.md

maxtongwang and others added 3 commits March 4, 2026 02:01
…eturn bypass

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- channels/bluebubbles: write temp audio file with 0600 permissions to protect
  voice content on shared hosts; fail-fast validation for add/remove_reaction
- gateway/oauth: remove unused redirect field from OAuthStartQuery, fix TOCTOU
  race in revoke path, log Google token revocation failures
- gateway/mod: hash sender PII before logging, fire-and-forget mark_read calls
…-reference

- Add §4.1 BlueBubbles config/policy section to el/channels-reference.md
- Add §4.15 BlueBubbles config/policy section to vi/channels-reference.md
  with policy table matching EN runtime contract
- Addresses CodeRabbit finding: delivery matrix shows BlueBubbles transport
  but localized docs lacked matching config contract documentation
@maxtongwang
Copy link
Copy Markdown
Author

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Mar 5, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@maxtongwang
Copy link
Copy Markdown
Author

@coderabbitai review

@maxtongwang
Copy link
Copy Markdown
Author

Closing — no longer needed.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

channel Auto scope: src/channels/** changed. ci Auto scope: CI/workflow/hook files changed. config: core Auto module: config core files changed. core Auto scope: root src/*.rs files changed. dependencies Auto scope: dependency manifest/lock/policy changed. docs Auto scope: docs/markdown/template files changed. gateway: oauth Auto module: gateway/oauth changed. onboard: tui-3 Auto module: onboard/tui-3 changed. provider Auto scope: src/providers/** changed. risk: high Auto risk: security/runtime/gateway/tools/workflows. security: canary_guard-3 Auto module: security/canary_guard-3 changed. size: XL Auto size: >1000 non-doc changed lines. tool Auto scope: src/tools/** changed.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant