feat(channel/bluebubbles): dm_policy, group_policy, and send_read_receipts#2495
feat(channel/bluebubbles): dm_policy, group_policy, and send_read_receipts#2495maxtongwang wants to merge 74 commits intozeroclaw-labs:mainfrom
Conversation
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 (&, <, etc.), so this always failed. Assert contains("&") 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>
✅ Actions performedReview triggered.
|
There was a problem hiding this comment.
♻️ Duplicate comments (5)
src/gateway/mod.rs (3)
2377-2378:⚠️ Potential issue | 🟡 MinorFix stale status-code comment (200 vs 202).
Line 2377 says “Returning 200 immediately” while the handler returns
StatusCode::ACCEPTEDat 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, Update the stale comment that says "Returning 200 immediately" so it correctly reflects the actual status being returned (StatusCode::ACCEPTED / 202); locate the comment near the handler that ultimately returns StatusCode::ACCEPTED and change the wording to mention 202/ACCEPTED and that the request is accepted for background processing, ensuring the comment matches the code's behavior.
2366-2374:⚠️ Potential issue | 🟠 MajorAdd boundary tests for Accepted vs exhausted worker-pool paths.
The new semaphore branch introduces a critical boundary (
202 acceptedvs503 exhausted), but there’s no corresponding validation in the provided tests for this handler behavior.Based on learnings: "Applies to src/gateway/**/*.rs : Include threat/risk notes and rollback strategy. Add/update tests or validation evidence for failure modes and boundaries."
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 2366 - 2374, Add unit/integration tests for the handler that acquires BB_WORKER_SEMAPHORE to assert both success (when try_acquire_owned returns Ok) yields StatusCode::ACCEPTED (202) and failure (when semaphore is exhausted / try_acquire_owned returns Err) yields StatusCode::SERVICE_UNAVAILABLE (503) with the JSON error body; mock or configure BB_WORKER_SEMAPHORE to a controllable semaphore in tests, exercise the code path that calls Arc::clone(&*BB_WORKER_SEMAPHORE).try_acquire_owned() (the handler/function containing that match) for both scenarios, and include assertions for response status and body; add a test case documenting the failure mode and a brief rollback note in comments for reverting the change if boundary behavior regresses.
585-591:⚠️ Potential issue | 🟠 MajorDon’t silently disable BlueBubbles on init failure.
Line 590 converts initialization errors into
None, which hides misconfiguration and weakens fail-fast startup behavior.Suggested fix
- let bluebubbles_channel: Option<Arc<BlueBubblesChannel>> = config + let bluebubbles_channel: Option<Arc<BlueBubblesChannel>> = config .channels_config .bluebubbles .as_ref() .map(|bb| { BlueBubblesChannel::new( bb.server_url.clone(), bb.password.clone(), bb.allowed_senders.clone(), bb.ignore_senders.clone(), ) .map(|ch| { Arc::new( ch.with_policies( bb.dm_policy, bb.group_policy, bb.group_allow_from.clone(), bb.send_read_receipts, ) .with_transcription(config.transcription.clone()), ) }) }) .transpose() .map_err(|e| { tracing::error!("Failed to initialize BlueBubbles channel: {e}"); e - }) - .ok() - .flatten(); + })?;As per coding guidelines: "Prefer explicit
bail!/errors for unsupported or unsafe states; never silently broaden permissions/capabilities (Fail Fast principle)".🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/gateway/mod.rs` around lines 585 - 591, The current chain ending with `.transpose().map_err(|e| { tracing::error!("Failed to initialize BlueBubbles channel: {e}"); e }).ok().flatten()` swallows initialization failures by converting errors to None; instead propagate the error (fail-fast). Remove the `.ok().flatten()` and return/propagate the error from the `map_err` (or replace `map_err` with a `map_err(|e| { tracing::error!(...); anyhow::bail!(...); })` or simply use `...?` on the result) so the calling function receives a Result::Err; keep the trace log but do not convert the error into None. Ensure the enclosing function's signature returns Result and update callers if necessary.src/channels/bluebubbles.rs (2)
271-287:⚠️ Potential issue | 🟠 MajorEnforce
allowed_sendersin groupAllowlistmode to avoid permission widening.
BlueBubblesGroupPolicy::Allowlistcurrently checks onlygroup_allow_from, so configured sender restrictions are bypassed in that mode. This broadens access relative to existing sender-gated behavior.🔧 Suggested fix
fn is_group_allowed(&self, chat_guid: &str, sender: &str) -> bool { + let sender_allowed = self.is_sender_allowed(sender); match self.group_policy { BlueBubblesGroupPolicy::Disabled => false, BlueBubblesGroupPolicy::Allowlist => self .group_allow_from .iter() - .any(|g| g == "*" || g.eq_ignore_ascii_case(chat_guid)), + .any(|g| g == "*" || g.eq_ignore_ascii_case(chat_guid)) + && sender_allowed, BlueBubblesGroupPolicy::Open => { - if self.allowed_senders.is_empty() { - true - } else { - self.allowed_senders - .iter() - .any(|a| a == "*" || a.eq_ignore_ascii_case(sender)) - } + sender_allowed } } }As per coding guidelines: “Do not silently weaken security policy or access constraints; never silently broaden permissions/capabilities.”
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/channels/bluebubbles.rs` around lines 271 - 287, is_group_allowed's BlueBubblesGroupPolicy::Allowlist branch currently only checks group_allow_from and thus ignores allowed_senders; change the Allowlist branch to require both that the chat_guid matches group_allow_from AND that the sender is permitted by allowed_senders (treating an empty allowed_senders as "no sender restriction" same as Open). In other words, inside is_group_allowed, update the Allowlist arm to return self.group_allow_from.iter().any(... for chat_guid) && (self.allowed_senders.is_empty() || self.allowed_senders.iter().any(... for sender)) so both group and sender checks are enforced.
303-339:⚠️ Potential issue | 🟠 Major
mark_readblocks despite the “fire-and-forget” contract.
mark_readawaits the HTTP call, so BB slowness can delay the caller up to the request timeout. If non-blocking is intended, dispatch the POST in a detached task (or enforce a short per-request timeout).🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/channels/bluebubbles.rs` around lines 303 - 339, The mark_read function currently awaits the HTTP POST which violates the intended “fire-and-forget” behavior; change it to dispatch the POST in a detached task so it won’t block the caller (or, alternatively, wrap the request in a short tokio::time::timeout). Concretely, move the request logic that builds the URL and makes client.post(...).query(...).send().await into a tokio::spawn async move closure (cloning self.client, self.password and the computed url/encoded/chat_guid as needed) and perform the same logging inside that spawned task; if you prefer a safety bound, apply tokio::time::timeout around the send() inside the task to enforce a short per-request timeout. Ensure mark_read returns immediately after spawning the task.
🤖 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/channels/bluebubbles.rs`:
- Around line 271-287: is_group_allowed's BlueBubblesGroupPolicy::Allowlist
branch currently only checks group_allow_from and thus ignores allowed_senders;
change the Allowlist branch to require both that the chat_guid matches
group_allow_from AND that the sender is permitted by allowed_senders (treating
an empty allowed_senders as "no sender restriction" same as Open). In other
words, inside is_group_allowed, update the Allowlist arm to return
self.group_allow_from.iter().any(... for chat_guid) &&
(self.allowed_senders.is_empty() || self.allowed_senders.iter().any(... for
sender)) so both group and sender checks are enforced.
- Around line 303-339: The mark_read function currently awaits the HTTP POST
which violates the intended “fire-and-forget” behavior; change it to dispatch
the POST in a detached task so it won’t block the caller (or, alternatively,
wrap the request in a short tokio::time::timeout). Concretely, move the request
logic that builds the URL and makes client.post(...).query(...).send().await
into a tokio::spawn async move closure (cloning self.client, self.password and
the computed url/encoded/chat_guid as needed) and perform the same logging
inside that spawned task; if you prefer a safety bound, apply
tokio::time::timeout around the send() inside the task to enforce a short
per-request timeout. Ensure mark_read returns immediately after spawning the
task.
In `@src/gateway/mod.rs`:
- Around line 2377-2378: Update the stale comment that says "Returning 200
immediately" so it correctly reflects the actual status being returned
(StatusCode::ACCEPTED / 202); locate the comment near the handler that
ultimately returns StatusCode::ACCEPTED and change the wording to mention
202/ACCEPTED and that the request is accepted for background processing,
ensuring the comment matches the code's behavior.
- Around line 2366-2374: Add unit/integration tests for the handler that
acquires BB_WORKER_SEMAPHORE to assert both success (when try_acquire_owned
returns Ok) yields StatusCode::ACCEPTED (202) and failure (when semaphore is
exhausted / try_acquire_owned returns Err) yields
StatusCode::SERVICE_UNAVAILABLE (503) with the JSON error body; mock or
configure BB_WORKER_SEMAPHORE to a controllable semaphore in tests, exercise the
code path that calls Arc::clone(&*BB_WORKER_SEMAPHORE).try_acquire_owned() (the
handler/function containing that match) for both scenarios, and include
assertions for response status and body; add a test case documenting the failure
mode and a brief rollback note in comments for reverting the change if boundary
behavior regresses.
- Around line 585-591: The current chain ending with `.transpose().map_err(|e| {
tracing::error!("Failed to initialize BlueBubbles channel: {e}"); e
}).ok().flatten()` swallows initialization failures by converting errors to
None; instead propagate the error (fail-fast). Remove the `.ok().flatten()` and
return/propagate the error from the `map_err` (or replace `map_err` with a
`map_err(|e| { tracing::error!(...); anyhow::bail!(...); })` or simply use
`...?` on the result) so the calling function receives a Result::Err; keep the
trace log but do not convert the error into None. Ensure the enclosing
function's signature returns Result and update callers if necessary.
|
@coderabbitai full review |
✅ Actions performedFull review triggered. |
There was a problem hiding this comment.
Actionable comments posted: 4
♻️ Duplicate comments (12)
docs/i18n/el/channels-reference.md (1)
40-50:⚠️ Potential issue | 🟠 MajorSplit native iMessage and BlueBubbles rows in the EL delivery table.
At Line 49,
iMessageis currently documented as BlueBubbles webhook mode, which misstates native iMessage behavior. Please separate them into distinct rows (native AppleScript bridge vs BlueBubbles webhook) to keep receive-mode and inbound-port semantics accurate.Based on learnings: "Applies to docs/channels-reference.md : Treat as runtime-contract reference. Track behavior changes to channel integrations. Update corresponding localized docs for supported locales when wording changes."
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@docs/i18n/el/channels-reference.md` around lines 40 - 50, The EL docs table incorrectly conflates native iMessage and BlueBubbles into one row; split the single "iMessage" row into two rows: one for native iMessage (describe mechanism as "AppleScript bridge" or "Native bridge", set Port Forwarding to "Όχι" and appropriate receive-mode semantics) and a separate "BlueBubbles" row (mechanism "BlueBubbles webhook (`/bluebubbles`)", Port Forwarding "Ναι" and note that the BlueBubbles server must be reachable). Update the table header alignment and ensure the new rows preserve the same column order and formatting as the other rows (use the unique labels "iMessage" and "BlueBubbles webhook (`/bluebubbles`)").docs/channels-reference.md (1)
555-593:⚠️ Potential issue | 🟠 MajorBlueBubbles runtime-contract expansion still needs full locale follow-through.
This section adds new shared user-facing contract content (
dm_policy,group_policy,group_allow_from,send_read_receipts) but the update set shown here does not cover all supported locales. Please sync remaining localized channel references (or explicitly document deferral with a tracked follow-up).As per coding guidelines, "If a change touches docs IA, runtime-contract references, or user-facing wording in shared docs, perform i18n follow-through for currently supported locales (en, zh-CN, ja, ru, fr, vi, el) in the same PR."
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@docs/channels-reference.md` around lines 555 - 593, The BlueBubbles channel docs add new runtime-contract fields (dm_policy, group_policy, group_allow_from, send_read_receipts) but the PR only updates the English doc; update the corresponding localized channel reference files for all supported locales (en, zh-CN, ja, ru, fr, vi, el) to include the new fields and policy table for BlueBubbles, or if you cannot update them now, add a clear note in each locale file pointing to a tracked follow-up issue/PR number that defers i18n follow-through; ensure you update the same section/title used for BlueBubbles so the runtime-contract wording is consistent across locales.docs/i18n/vi/channels-reference.md (1)
118-118:⚠️ Potential issue | 🟠 MajorBlueBubbles runtime-contract coverage in VI doc is still incomplete.
Line 118 adds BlueBubbles to the delivery table, but the VI reference still lacks matching BlueBubbles policy/config detail and a BlueBubbles diagnostics keyword row in the appendix table. Please sync this section with the EN runtime-contract coverage so behavior and triage docs stay consistent.
Based on learnings: "Applies to docs/channels-reference.md : Treat as runtime-contract reference. Track behavior changes to channel integrations. Update corresponding localized docs for supported locales when wording changes."
Also applies to: 415-430
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@docs/i18n/vi/channels-reference.md` at line 118, The Vietnamese channels-reference entry added "BlueBubbles" to the delivery table but lacks the corresponding runtime-contract policy/config details and the diagnostics keyword row present in the English runtime-contract docs; update the VI channels-reference.md by copying the BlueBubbles policy/config paragraph and the diagnostics keyword row from the EN runtime-contract coverage (channels-reference.md EN version) into the Vietnamese document, ensure the delivery table entry text matches the EN wording and add a localized diagnostics keyword row in the appendix table so behavior, triage, and configuration guidance are consistent across locales.src/channels/transcription.rs (1)
90-93:⚠️ Potential issue | 🟠 Majorffmpeg timeout/spawn failures can leak temporary WAV files.
At Line 90-93, timeout/spawn errors return early via
?before cleanup oftmp, so conversion artifacts can accumulate on failure paths.♻️ Cleanup-safe conversion handling
- let conv = tokio::time::timeout(std::time::Duration::from_secs(120), ffmpeg_cmd.output()) - .await - .map_err(|_| anyhow::anyhow!("ffmpeg CAF→WAV conversion timed out after 120s"))? - .context("ffmpeg CAF→WAV conversion failed")?; + let conv = match tokio::time::timeout(std::time::Duration::from_secs(120), ffmpeg_cmd.output()).await { + Ok(Ok(o)) => o, + Ok(Err(e)) => { + let _ = tokio::fs::remove_file(&tmp).await; + return Err(anyhow::anyhow!("ffmpeg CAF→WAV conversion failed: {e}")); + } + Err(_) => { + let _ = tokio::fs::remove_file(&tmp).await; + anyhow::bail!("ffmpeg CAF→WAV conversion timed out after 120s"); + } + };Based on learnings: "Applies to **/*.rs : Keep error paths obvious and localized; prefer explicit
bail!/errors for unsupported or unsafe states."🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/channels/transcription.rs` around lines 90 - 93, The ffmpeg timeout/spawn failure path can return early and leak the temporary WAV file `tmp`; change the `tokio::time::timeout(...).await.map_err(...)?` usage around `ffmpeg_cmd.output()` so errors always run cleanup: either use a scoped match/if-let to handle the timeout/error (capturing the `Output` or error into `conv`), remove the `tmp` file (or rely on a `tempfile::NamedTempFile`/RAII wrapper) before returning/bailing, and ensure `tmp` is deleted in every error branch (timeout, spawn failure, and conversion failure) so `conv`, `ffmpeg_cmd`, and `tmp` cleanup is explicit and not bypassed by `?`.src/gateway/mod.rs (3)
2377-2379:⚠️ Potential issue | 🟡 MinorStale inline comment: status code says 200 but handler returns 202.
Please align the comment text with the actual
StatusCode::ACCEPTEDresponse.🤖 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 - 2379, The inline comment above the background-task spawn is stale: it references returning HTTP 200 but the handler actually returns StatusCode::ACCEPTED (202). Update the comment text near the Arc::clone(bluebubbles) / background task block to mention 202 (StatusCode::ACCEPTED) instead of 200 so the comment matches the actual response code and intent.
562-591:⚠️ Potential issue | 🟠 MajorBlueBubbles init failures are swallowed and silently disable the channel.
The
.ok().flatten()conversion drops initialization errors after logging, which weakens fail-fast startup behavior for a configured integration.As per coding guidelines: `src/**/*.rs` — “Prefer explicit `bail!`/errors for unsupported or unsafe states; never silently broaden permissions/capabilities; document fallback behavior when fallback is intentional and safe”.🔧 Propagate initialization failures
let bluebubbles_channel: Option<Arc<BlueBubblesChannel>> = config .channels_config .bluebubbles .as_ref() .map(|bb| { BlueBubblesChannel::new( bb.server_url.clone(), bb.password.clone(), bb.allowed_senders.clone(), bb.ignore_senders.clone(), ) .map(|ch| { Arc::new( ch.with_policies( bb.dm_policy, bb.group_policy, bb.group_allow_from.clone(), bb.send_read_receipts, ) .with_transcription(config.transcription.clone()), ) }) }) .transpose() .map_err(|e| { tracing::error!("Failed to initialize BlueBubbles channel: {e}"); e - }) - .ok() - .flatten(); + })?;🤖 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 code currently logs BlueBubblesChannel initialization errors but then swallows them via .ok().flatten(), silently disabling the channel; change the chain to propagate the error instead. Replace the trailing .ok().flatten() with a propagated Result by using the existing .transpose().map_err(|e| { tracing::error!( "Failed to initialize BlueBubbles channel: {e}"); e })? so the function returns an Err on failure; keep the use of BlueBubblesChannel::new, ch.with_policies and .with_transcription and ensure bluebubbles_channel remains an Option<Arc<BlueBubblesChannel>> after the ? propagation.
2358-2487:⚠️ Potential issue | 🟠 MajorPlease add direct tests for 202 accepted vs 503 saturation behavior.
This new async boundary is critical: one test should assert 202 +
{"status":"accepted"}when a permit exists, and another should assert 503 +{"error":"worker pool exhausted, retry later"}when permits are exhausted.Based on learnings: “Applies to src/gateway/**/*.rs : Include threat/risk notes and rollback strategy. Add/update tests or validation evidence for failure modes and boundaries. Keep observability useful but non-sensitive.”
🤖 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 - 2487, Add two direct tests around the BlueBubbles webhook path that exercise the try_acquire_owned branch on BB_WORKER_SEMAPHORE: one test (e.g., test_bluebubbles_returns_accepted_when_permit_exists) should ensure a permit is available (ensure BB_WORKER_SEMAPHORE has at least one free permit), invoke the webhook handler with a minimal payload, and assert the HTTP response is StatusCode::ACCEPTED and body JSON {"status":"accepted"}; the other test (e.g., test_bluebubbles_returns_503_when_semaphore_exhausted) should exhaust BB_WORKER_SEMAPHORE (by acquiring all permits or otherwise blocking try_acquire_owned), invoke the same handler, and assert StatusCode::SERVICE_UNAVAILABLE and JSON {"error":"worker pool exhausted, retry later"}; make sure tests cleanup/release any acquired permits so other tests are unaffected and reference the BB_WORKER_SEMAPHORE, try_acquire_owned, and the handler invocation that returns the ACCEPTED/SERVICE_UNAVAILABLE pair.src/gateway/oauth.rs (5)
557-561:⚠️ Potential issue | 🟠 MajorRevocation transport failures are still silently swallowed.
let _ = revoke_google_token(...)drops network/TLS errors entirely, so the endpoint can report success while remote revocation never happened.🔧 Minimal hardening patch
if service == "google" { if let Some(token) = read_token(&path).await { - let _ = revoke_google_token(&token.access_token).await; + if let Err(e) = revoke_google_token(&token.access_token).await { + tracing::warn!("Google token revoke request failed before local cleanup: {e}"); + } } }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/gateway/oauth.rs` around lines 557 - 561, The revocation call currently swallows all errors via `let _ = revoke_google_token(&token.access_token).await;`; update the `if service == "google"` block that uses `read_token(&path).await` to handle the Result from `revoke_google_token` instead of discarding it: await the call into a `match` or `if let Err(e)` and either log the error (including `e` and context like `path`/`service`) or propagate it to the caller so transport/TLS failures are not ignored; ensure you reference `revoke_google_token` and `read_token` in the change and do not lose the original success path.
116-127:⚠️ Potential issue | 🟡 Minor
callback_urlhardcodeshttp://, which breaks HTTPS-exposed OAuth deployments.When the gateway is exposed via an HTTPS tunnel/proxy, this can produce
redirect_urimismatches with provider settings.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/gateway/oauth.rs` around lines 116 - 127, The callback_url function currently hardcodes "http://" causing redirect_uri mismatches for HTTPS deployments; update callback_url (involving AppState and callback_url) to derive the URL scheme from configuration instead of hardcoding: read a scheme field (e.g., cfg.gateway.scheme) or a TLS flag (e.g., cfg.gateway.tls_enabled) from cfg and set scheme = "https" when TLS is enabled (fallback to "http" otherwise), keep the existing display_host logic for localhost normalization, and format the URL as "{scheme}://{display_host}:{port}/auth/{service}/callback" so HTTPS-exposed gateways produce correct redirect_uri values.
76-92:⚠️ Potential issue | 🟠 MajorPre-existing OAuth directory/token files may keep permissive Unix permissions.
mode(0o700)/mode(0o600)protects create-time only. If the path already exists with broader permissions, this code path does not re-harden it.#!/bin/bash # Verify whether oauth path hardening includes post-create chmod for existing paths. # Expected: explicit set_permissions logic near ensure_oauth_dir/write_token. rg -n -C4 'async fn ensure_oauth_dir|mode\(0o700\)|set_permissions|async fn write_token|mode\(0o600\)' src/gateway/oauth.rsAs per coding guidelines:
src/{gateway,security,tools,runtime}/**/*.rs— “Deny-by-default for access and exposure boundaries; never log secrets, raw tokens, or sensitive payloads; keep network/filesystem/shell scope as narrow as possible unless explicitly justified”.Also applies to: 99-113
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/gateway/oauth.rs` around lines 76 - 92, ensure_oauth_dir currently only sets mode during creation, so pre-existing directories with permissive permissions are not re-hardened; update ensure_oauth_dir to always enforce restrictive permissions on the path after creation (and on errors when path already exists) by applying std::fs::set_permissions (or set_permissions via PermissionsExt on unix to 0o700) inside tokio::task::spawn_blocking, and likewise ensure write_token enforces file permissions (0o600) after writing the token (again using spawn_blocking and PermissionsExt on unix); locate and modify the functions ensure_oauth_dir and write_token to add these explicit post-create chmod steps so existing files/dirs are hardened regardless of creation path.
129-132:⚠️ Potential issue | 🟠 MajorOAuth management endpoints are effectively unauthenticated when pairing is off.
auth_checkcurrently returnstruewhen pairing is disabled, which opens/auth/{service},/auth/status, andDELETE /auth/{service}without any auth boundary.As per coding guidelines:
src/{gateway,security,tools,runtime}/**/*.rs— “Deny-by-default for access and exposure boundaries; never log secrets, raw tokens, or sensitive payloads; keep network/filesystem/shell scope as narrow as possible unless explicitly justified”.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/gateway/oauth.rs` around lines 129 - 132, auth_check currently returns true when state.pairing.require_pairing() is false, effectively disabling auth; change auth_check so it is deny-by-default: if pairing is enabled keep current behavior, but when pairing is disabled do NOT return true unconditionally — instead validate the incoming headers using the service's normal auth validation path (e.g., call the existing token/session validation routine or an AppState method that verifies auth headers) and only return true when that validation succeeds; update auth_check (and any callers of auth_check) to reference AppState’s proper auth validator rather than short-circuiting on pairing.require_pairing().
705-738:⚠️ Potential issue | 🟠 MajorSecurity-critical handler failure boundaries still need direct tests.
Current coverage is helper-level only; please add handler-level tests for unauthorized management access, missing/mismatched OAuth state, and revoke failure behavior.
Based on learnings: “Applies to src/gateway/**/*.rs : Include threat/risk notes and rollback strategy. Add/update tests or validation evidence for failure modes and boundaries. Keep observability useful but non-sensitive.”
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/gateway/oauth.rs` around lines 705 - 738, The repo lacks handler-level tests for failure boundaries: add tests that exercise the management access handler, the OAuth callback state validation, and the revoke flow to cover unauthorized access, missing/mismatched state, and revoke errors. Specifically, add tests that call the management handler (e.g., handle_manage or manage_authorization) with an unauthenticated/unauthorized request and assert the handler returns the expected 401/403 and does not perform privileged actions; call the OAuth callback handler (e.g., oauth_callback or handle_oauth_callback) with missing and with mismatched state values and assert it rejects the request and does not create sessions; and mock the revocation backend for the revoke handler (e.g., revoke_token or handle_revoke) to return an error and assert the handler surfaces the failure, performs any defined rollback/cleanup, and emits the expected observable logs/metrics. Ensure tests use the same public interfaces/handlers in src/gateway/oauth.rs, mock external dependencies (token store, HTTP client), and assert both HTTP response codes and side-effect (no session persisted, no management change) to validate boundaries.
🧹 Nitpick comments (1)
src/config/schema.rs (1)
14325-14369: Add whitespace-only allowlist regression tests.Current tests assert empty vectors, but Line 7635 and Line 7643 also reject whitespace-only entries. Adding explicit
vec![" "]cases for both DM and group allowlists would lock that branch behavior.🧪 Suggested test additions
+ #[test] + async fn validation_rejects_bluebubbles_dm_allowlist_with_whitespace_only_senders() { + let mut config = Config::default(); + config.channels_config.bluebubbles = Some(BlueBubblesConfig { + server_url: "http://localhost:1234".to_string(), + password: String::new(), + allowed_senders: vec![" ".to_string()], + webhook_secret: None, + ignore_senders: vec![], + dm_policy: BlueBubblesDmPolicy::Allowlist, + group_policy: BlueBubblesGroupPolicy::Open, + group_allow_from: vec![], + send_read_receipts: true, + }); + let err = config.validate().expect_err( + "allowlist dm_policy with whitespace-only allowed_senders should fail validation", + ); + assert!(err.to_string().contains("dm_policy")); + } + + #[test] + async fn validation_rejects_bluebubbles_group_allowlist_with_whitespace_only_entries() { + let mut config = Config::default(); + config.channels_config.bluebubbles = Some(BlueBubblesConfig { + server_url: "http://localhost:1234".to_string(), + password: String::new(), + allowed_senders: vec![], + webhook_secret: None, + ignore_senders: vec![], + dm_policy: BlueBubblesDmPolicy::Open, + group_policy: BlueBubblesGroupPolicy::Allowlist, + group_allow_from: vec![" ".to_string()], + send_read_receipts: true, + }); + let err = config.validate().expect_err( + "allowlist group_policy with whitespace-only group_allow_from should fail validation", + ); + assert!(err.to_string().contains("group_policy")); + }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/config/schema.rs` around lines 14325 - 14369, Add two regression tests mirroring validation_rejects_bluebubbles_dm_allowlist_with_empty_senders and validation_rejects_bluebubbles_group_allowlist_with_empty_list but use whitespace-only entries (e.g., allowed_senders: vec![" "] and group_allow_from: vec![" "]) in the BlueBubblesConfig; call Config::default(), set channels_config.bluebubbles with those whitespace-only vectors, call config.validate().expect_err(...), and assert the error string contains "dm_policy" for the DM test and "group_policy" for the group test to ensure whitespace-only allowlist entries are rejected the same as empty vectors.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@docs/config-reference.md`:
- Line 1247: Update the `send_read_receipts` config description so it reflects
actual runtime behavior: instead of saying receipts are posted "after each
processed message", state that read receipts are sent only after a successful
reply is sent (i.e., when the outbound response completes successfully). Locate
the `send_read_receipts` entry in the config reference and change the wording to
mention "after successful replies" and keep the default value (`true`)
unchanged.
In `@src/channels/bluebubbles.rs`:
- Around line 818-821: In the Err(e) branch of the audio transcription flow (the
match arm that currently does tracing::warn!("BB audio transcription failed:
{e}")), avoid logging raw error text which may contain attachment GUIDs or
filenames from transcribe_local / download_attachment_bytes; instead log a
sanitized message and only non-sensitive context (e.g., "BB audio transcription
failed" plus error kind or a scrubbed e.to_string() with GUID/filename patterns
removed) and then continue to call self.parse_webhook_payload(payload); update
the tracing::warn call in that Err(e) arm to use the sanitized/error-kind form
rather than printing the full error chain.
In `@src/channels/transcription.rs`:
- Around line 114-119: The owner-only permission hardening currently swallows
errors for the set_permissions call on out_dir and is not applied to the second
temp directory; change the unix-only permission block so the
tokio::fs::set_permissions(&out_dir, Permissions::from_mode(0o700)).await result
is checked and propagated (use anyhow::bail! or propagate the Err) instead of
ignoring it, apply the same Permissions::from_mode(0o700) hardening to the other
temp directory created later (the second temp dir variable), and make this
fail-closed on non-unix targets (return an explicit error or bail! if permission
hardening cannot be enforced) so neither out_dir nor the later temp directory
continues with broader access when hardening fails.
---
Duplicate comments:
In `@docs/channels-reference.md`:
- Around line 555-593: The BlueBubbles channel docs add new runtime-contract
fields (dm_policy, group_policy, group_allow_from, send_read_receipts) but the
PR only updates the English doc; update the corresponding localized channel
reference files for all supported locales (en, zh-CN, ja, ru, fr, vi, el) to
include the new fields and policy table for BlueBubbles, or if you cannot update
them now, add a clear note in each locale file pointing to a tracked follow-up
issue/PR number that defers i18n follow-through; ensure you update the same
section/title used for BlueBubbles so the runtime-contract wording is consistent
across locales.
In `@docs/i18n/el/channels-reference.md`:
- Around line 40-50: The EL docs table incorrectly conflates native iMessage and
BlueBubbles into one row; split the single "iMessage" row into two rows: one for
native iMessage (describe mechanism as "AppleScript bridge" or "Native bridge",
set Port Forwarding to "Όχι" and appropriate receive-mode semantics) and a
separate "BlueBubbles" row (mechanism "BlueBubbles webhook (`/bluebubbles`)",
Port Forwarding "Ναι" and note that the BlueBubbles server must be reachable).
Update the table header alignment and ensure the new rows preserve the same
column order and formatting as the other rows (use the unique labels "iMessage"
and "BlueBubbles webhook (`/bluebubbles`)").
In `@docs/i18n/vi/channels-reference.md`:
- Line 118: The Vietnamese channels-reference entry added "BlueBubbles" to the
delivery table but lacks the corresponding runtime-contract policy/config
details and the diagnostics keyword row present in the English runtime-contract
docs; update the VI channels-reference.md by copying the BlueBubbles
policy/config paragraph and the diagnostics keyword row from the EN
runtime-contract coverage (channels-reference.md EN version) into the Vietnamese
document, ensure the delivery table entry text matches the EN wording and add a
localized diagnostics keyword row in the appendix table so behavior, triage, and
configuration guidance are consistent across locales.
In `@src/channels/transcription.rs`:
- Around line 90-93: The ffmpeg timeout/spawn failure path can return early and
leak the temporary WAV file `tmp`; change the
`tokio::time::timeout(...).await.map_err(...)?` usage around
`ffmpeg_cmd.output()` so errors always run cleanup: either use a scoped
match/if-let to handle the timeout/error (capturing the `Output` or error into
`conv`), remove the `tmp` file (or rely on a `tempfile::NamedTempFile`/RAII
wrapper) before returning/bailing, and ensure `tmp` is deleted in every error
branch (timeout, spawn failure, and conversion failure) so `conv`, `ffmpeg_cmd`,
and `tmp` cleanup is explicit and not bypassed by `?`.
In `@src/gateway/mod.rs`:
- Around line 2377-2379: The inline comment above the background-task spawn is
stale: it references returning HTTP 200 but the handler actually returns
StatusCode::ACCEPTED (202). Update the comment text near the
Arc::clone(bluebubbles) / background task block to mention 202
(StatusCode::ACCEPTED) instead of 200 so the comment matches the actual response
code and intent.
- Around line 562-591: The code currently logs BlueBubblesChannel initialization
errors but then swallows them via .ok().flatten(), silently disabling the
channel; change the chain to propagate the error instead. Replace the trailing
.ok().flatten() with a propagated Result by using the existing
.transpose().map_err(|e| { tracing::error!( "Failed to initialize BlueBubbles
channel: {e}"); e })? so the function returns an Err on failure; keep the use of
BlueBubblesChannel::new, ch.with_policies and .with_transcription and ensure
bluebubbles_channel remains an Option<Arc<BlueBubblesChannel>> after the ?
propagation.
- Around line 2358-2487: Add two direct tests around the BlueBubbles webhook
path that exercise the try_acquire_owned branch on BB_WORKER_SEMAPHORE: one test
(e.g., test_bluebubbles_returns_accepted_when_permit_exists) should ensure a
permit is available (ensure BB_WORKER_SEMAPHORE has at least one free permit),
invoke the webhook handler with a minimal payload, and assert the HTTP response
is StatusCode::ACCEPTED and body JSON {"status":"accepted"}; the other test
(e.g., test_bluebubbles_returns_503_when_semaphore_exhausted) should exhaust
BB_WORKER_SEMAPHORE (by acquiring all permits or otherwise blocking
try_acquire_owned), invoke the same handler, and assert
StatusCode::SERVICE_UNAVAILABLE and JSON {"error":"worker pool exhausted, retry
later"}; make sure tests cleanup/release any acquired permits so other tests are
unaffected and reference the BB_WORKER_SEMAPHORE, try_acquire_owned, and the
handler invocation that returns the ACCEPTED/SERVICE_UNAVAILABLE pair.
In `@src/gateway/oauth.rs`:
- Around line 557-561: The revocation call currently swallows all errors via
`let _ = revoke_google_token(&token.access_token).await;`; update the `if
service == "google"` block that uses `read_token(&path).await` to handle the
Result from `revoke_google_token` instead of discarding it: await the call into
a `match` or `if let Err(e)` and either log the error (including `e` and context
like `path`/`service`) or propagate it to the caller so transport/TLS failures
are not ignored; ensure you reference `revoke_google_token` and `read_token` in
the change and do not lose the original success path.
- Around line 116-127: The callback_url function currently hardcodes "http://"
causing redirect_uri mismatches for HTTPS deployments; update callback_url
(involving AppState and callback_url) to derive the URL scheme from
configuration instead of hardcoding: read a scheme field (e.g.,
cfg.gateway.scheme) or a TLS flag (e.g., cfg.gateway.tls_enabled) from cfg and
set scheme = "https" when TLS is enabled (fallback to "http" otherwise), keep
the existing display_host logic for localhost normalization, and format the URL
as "{scheme}://{display_host}:{port}/auth/{service}/callback" so HTTPS-exposed
gateways produce correct redirect_uri values.
- Around line 76-92: ensure_oauth_dir currently only sets mode during creation,
so pre-existing directories with permissive permissions are not re-hardened;
update ensure_oauth_dir to always enforce restrictive permissions on the path
after creation (and on errors when path already exists) by applying
std::fs::set_permissions (or set_permissions via PermissionsExt on unix to
0o700) inside tokio::task::spawn_blocking, and likewise ensure write_token
enforces file permissions (0o600) after writing the token (again using
spawn_blocking and PermissionsExt on unix); locate and modify the functions
ensure_oauth_dir and write_token to add these explicit post-create chmod steps
so existing files/dirs are hardened regardless of creation path.
- Around line 129-132: auth_check currently returns true when
state.pairing.require_pairing() is false, effectively disabling auth; change
auth_check so it is deny-by-default: if pairing is enabled keep current
behavior, but when pairing is disabled do NOT return true unconditionally —
instead validate the incoming headers using the service's normal auth validation
path (e.g., call the existing token/session validation routine or an AppState
method that verifies auth headers) and only return true when that validation
succeeds; update auth_check (and any callers of auth_check) to reference
AppState’s proper auth validator rather than short-circuiting on
pairing.require_pairing().
- Around line 705-738: The repo lacks handler-level tests for failure
boundaries: add tests that exercise the management access handler, the OAuth
callback state validation, and the revoke flow to cover unauthorized access,
missing/mismatched state, and revoke errors. Specifically, add tests that call
the management handler (e.g., handle_manage or manage_authorization) with an
unauthenticated/unauthorized request and assert the handler returns the expected
401/403 and does not perform privileged actions; call the OAuth callback handler
(e.g., oauth_callback or handle_oauth_callback) with missing and with mismatched
state values and assert it rejects the request and does not create sessions; and
mock the revocation backend for the revoke handler (e.g., revoke_token or
handle_revoke) to return an error and assert the handler surfaces the failure,
performs any defined rollback/cleanup, and emits the expected observable
logs/metrics. Ensure tests use the same public interfaces/handlers in
src/gateway/oauth.rs, mock external dependencies (token store, HTTP client), and
assert both HTTP response codes and side-effect (no session persisted, no
management change) to validate boundaries.
---
Nitpick comments:
In `@src/config/schema.rs`:
- Around line 14325-14369: Add two regression tests mirroring
validation_rejects_bluebubbles_dm_allowlist_with_empty_senders and
validation_rejects_bluebubbles_group_allowlist_with_empty_list but use
whitespace-only entries (e.g., allowed_senders: vec![" "] and
group_allow_from: vec![" "]) in the BlueBubblesConfig; call Config::default(),
set channels_config.bluebubbles with those whitespace-only vectors, call
config.validate().expect_err(...), and assert the error string contains
"dm_policy" for the DM test and "group_policy" for the group test to ensure
whitespace-only allowlist entries are rejected the same as empty vectors.
ℹ️ Review info
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: cd5ccaac-6f20-496b-a262-69e2dbbaff2f
📒 Files selected for processing (12)
Cargo.tomldocs/actions-source-policy.mddocs/channels-reference.mddocs/config-reference.mddocs/i18n/el/channels-reference.mddocs/i18n/vi/channels-reference.mdsrc/channels/bluebubbles.rssrc/channels/transcription.rssrc/config/schema.rssrc/gateway/mod.rssrc/gateway/oauth.rssrc/tools/web_fetch.rs
docs/config-reference.md
Outdated
| | `dm_policy` | Optional | `"open"` (default) \| `"allowlist"` \| `"disabled"` — gates direct messages | | ||
| | `group_policy` | Optional | `"open"` (default) \| `"allowlist"` \| `"disabled"` — gates group chats | | ||
| | `group_allow_from` | Optional | Chat GUIDs allowed when `group_policy = "allowlist"`. Use `["*"]` for all groups | | ||
| | `send_read_receipts` | Optional | Post read receipt after each processed message. Default: `true` | |
There was a problem hiding this comment.
Clarify send_read_receipts trigger timing to match runtime behavior.
At Line 1247, the description says receipts are posted after each processed message, but runtime wiring is after successful replies. Tightening this wording avoids false expectations during incident triage.
✏️ Suggested doc correction
-| `send_read_receipts` | Optional | Post read receipt after each processed message. Default: `true` |
+| `send_read_receipts` | Optional | Post read receipt after each successful reply. Default: `true` |Based on learnings: "Applies to docs/config-reference.md : Treat as runtime-contract reference. Track behavior changes to configuration schema."
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| | `send_read_receipts` | Optional | Post read receipt after each processed message. Default: `true` | | |
| | `send_read_receipts` | Optional | Post read receipt after each successful reply. Default: `true` | |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@docs/config-reference.md` at line 1247, Update the `send_read_receipts`
config description so it reflects actual runtime behavior: instead of saying
receipts are posted "after each processed message", state that read receipts are
sent only after a successful reply is sent (i.e., when the outbound response
completes successfully). Locate the `send_read_receipts` entry in the config
reference and change the wording to mention "after successful replies" and keep
the default value (`true`) unchanged.
| Err(e) => { | ||
| tracing::warn!("BB audio transcription failed: {e}"); | ||
| self.parse_webhook_payload(payload) | ||
| } |
There was a problem hiding this comment.
Sanitize error before logging to avoid leaking attachment identifiers.
Line 819 logs {e} directly, which may contain attachment GUIDs or filenames from transcribe_local error chain (e.g., from download_attachment_bytes). This could expose identifiers in logs.
🔧 Suggested fix
Err(e) => {
- tracing::warn!("BB audio transcription failed: {e}");
+ tracing::warn!("BB audio transcription failed");
+ tracing::debug!("Transcription error details: {e}");
self.parse_webhook_payload(payload)
}As per coding guidelines: "Never commit personal or sensitive data in … logs."
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/channels/bluebubbles.rs` around lines 818 - 821, In the Err(e) branch of
the audio transcription flow (the match arm that currently does
tracing::warn!("BB audio transcription failed: {e}")), avoid logging raw error
text which may contain attachment GUIDs or filenames from transcribe_local /
download_attachment_bytes; instead log a sanitized message and only
non-sensitive context (e.g., "BB audio transcription failed" plus error kind or
a scrubbed e.to_string() with GUID/filename patterns removed) and then continue
to call self.parse_webhook_payload(payload); update the tracing::warn call in
that Err(e) arm to use the sanitized/error-kind form rather than printing the
full error chain.
|
@coderabbitai review |
✅ Actions performedReview triggered.
|
…aw-labs#2495 - Fix send_read_receipts doc description to clarify it sends to the sender, not "posts a receipt" (imprecise wording) - Sanitize BB transcription error log: use structured tracing field `error = %e` to avoid embedding paths in the interpolated string - Propagate set_permissions errors in transcribe_with_whisper_cpp out_dir: clean up and return Err instead of silently discarding - Apply 0o700 restriction to Python whisper tmp_dir immediately after creation, propagating failure with cleanup before any writes
|
@coderabbitai review |
✅ Actions performedReview triggered.
|
There was a problem hiding this comment.
♻️ Duplicate comments (6)
src/channels/bluebubbles.rs (3)
271-288:⚠️ Potential issue | 🟠 Major
group_policy = Allowlistcurrently bypasses sender allowlist checks.In
Allowlistmode, onlygroup_allow_fromis enforced. A sender not inallowed_senderscan still pass if chat GUID is allowlisted.🔒 Suggested fix
fn is_group_allowed(&self, chat_guid: &str, sender: &str) -> bool { + let sender_allowed = self.is_sender_allowed(sender); match self.group_policy { BlueBubblesGroupPolicy::Disabled => false, BlueBubblesGroupPolicy::Allowlist => self .group_allow_from .iter() - .any(|g| g == "*" || g.eq_ignore_ascii_case(chat_guid)), + .any(|g| g == "*" || g.eq_ignore_ascii_case(chat_guid)) + && sender_allowed, BlueBubblesGroupPolicy::Open => { - if self.allowed_senders.is_empty() { - true - } else { - self.allowed_senders - .iter() - .any(|a| a == "*" || a.eq_ignore_ascii_case(sender)) - } + sender_allowed } } }As per coding guidelines, “Do not silently weaken security policy or access constraints.”
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/channels/bluebubbles.rs` around lines 271 - 288, The Allowlist branch in is_group_allowed currently returns true if chat_guid is in group_allow_from without checking allowed_senders; change BlueBubblesGroupPolicy::Allowlist logic to require both the chat GUID be allowlisted (group_allow_from contains "*" or equals chat_guid) AND the sender is allowed (allowed_senders is empty or contains "*" or equals sender), so only when both conditions hold return true; update the block inside is_group_allowed to perform these two checks and return false otherwise.
303-338:⚠️ Potential issue | 🟠 Major
mark_readis not actually fire-and-forget and can block up to HTTP timeout.The method is documented as fire-and-forget, but it awaits the POST. During BB outages this can stall processing per message.
⚙️ Non-blocking pattern
- match self - .client - .post(&url) - .query(&[("password", &self.password)]) - .send() - .await - { - Err(e) => tracing::warn!("BlueBubbles mark_read failed: {}", e.without_url()), - Ok(resp) => { - let status = resp.status(); - if status.is_success() { - tracing::info!("BlueBubbles mark_read ok status={status}"); - } else { - tracing::warn!("BlueBubbles mark_read got {status}"); - } - } - } + let client = self.client.clone(); + let password = self.password.clone(); + tokio::spawn(async move { + match client.post(&url).query(&[("password", password.as_str())]).send().await { + Err(e) => tracing::warn!("BlueBubbles mark_read failed: {}", e.without_url()), + Ok(resp) => { + let status = resp.status(); + if status.is_success() { + tracing::info!("BlueBubbles mark_read ok status={status}"); + } else { + tracing::warn!("BlueBubbles mark_read got {status}"); + } + } + } + });🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/channels/bluebubbles.rs` around lines 303 - 338, mark_read currently awaits the POST and can block; make it truly fire-and-forget by moving the HTTP call into a spawned background task. Keep the early check of send_read_receipts and compute normalised/encoded/url synchronously, then clone/self-contain all values needed for the request (clone self.client, password string, url) and call tokio::spawn(async move { ... }) to perform the post and logging (use the same tracing.warn/info logic inside the task). Reference mark_read, self.client, self.password, self.api_url and ensure no await on the spawned task so the function returns immediately.
818-822:⚠️ Potential issue | 🟠 MajorAvoid logging raw transcription error chains at warn level.
error = %ecan leak attachment/file identifiers from nested error messages. Emit sanitized summary instead.🧹 Suggested sanitization
Err(e) => { - tracing::warn!(error = %e, "BB audio transcription failed"); + let sanitized = crate::providers::sanitize_api_error(&e.to_string()); + tracing::warn!("BB audio transcription failed: {sanitized}"); self.parse_webhook_payload(payload) }As per coding guidelines, “Never commit personal or sensitive data in … logs.”
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/channels/bluebubbles.rs` around lines 818 - 822, The current tracing::warn!(error = %e, "BB audio transcription failed") can leak attachment/file identifiers from nested error chains; replace this direct error interpolation with a sanitized summary: remove the %e from the warn call and instead pass a short, scrubbed string (e.g., the error type or a call to a helper sanitize_error(e) that strips file paths/IDs and sensitive tokens) or use std::any::type_name_of_val(&e) to log only the error class; keep the existing fallback to self.parse_webhook_payload(payload) and ensure no raw error chain is emitted at warn level.src/channels/transcription.rs (1)
90-94:⚠️ Potential issue | 🟠 MajorClean up temp WAV on ffmpeg timeout/spawn failures.
The timeout/spawn-error path returns before removing
tmp, so failed conversions can leak temporary files.♻️ Cleanup-safe conversion pattern
- let conv = tokio::time::timeout(std::time::Duration::from_secs(120), ffmpeg_cmd.output()) - .await - .map_err(|_| anyhow::anyhow!("ffmpeg CAF→WAV conversion timed out after 120s"))? - .context("ffmpeg CAF→WAV conversion failed")?; + let conv = match tokio::time::timeout( + std::time::Duration::from_secs(120), + ffmpeg_cmd.output(), + ) + .await + { + Ok(Ok(output)) => output, + Ok(Err(e)) => { + let _ = tokio::fs::remove_file(&tmp).await; + return Err(anyhow::anyhow!("ffmpeg CAF→WAV conversion failed: {e}")); + } + Err(_) => { + let _ = tokio::fs::remove_file(&tmp).await; + anyhow::bail!("ffmpeg CAF→WAV conversion timed out after 120s"); + } + };As per coding guidelines, “Keep error paths obvious and localized; prefer explicit
bail!/errors for unsupported or unsafe states.”🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/channels/transcription.rs` around lines 90 - 94, The ffmpeg timeout/spawn-error branch can return early and leak the temporary WAV file `tmp`; update the conversion call around tokio::time::timeout(std::time::Duration::from_secs(120), ffmpeg_cmd.output()) so that any early error path removes the temp file before returning—e.g., in the map_err closure and any spawn/output error handling, call std::fs::remove_file(&tmp).unwrap_or_else(|_| ()) (or use a small drop guard/TempDir wrapper) and then return the anyhow error, ensuring the cleanup happens for both timeout and ffmpeg spawn/output failures that currently return before the subsequent success check on `conv`.docs/config-reference.md (2)
1247-1247:⚠️ Potential issue | 🟡 Minor
send_read_receiptsdescription still mismatches runtime trigger.The text says receipts are sent after each processed incoming message, but behavior is after successful replies. Please align wording to avoid operator confusion.
✏️ Suggested wording fix
-| `send_read_receipts` | Optional | Send read receipt to the sender after each incoming message is processed. Default: `true` | +| `send_read_receipts` | Optional | Send read receipt after each successful reply. Default: `true` |Based on learnings: “Applies to docs/config-reference.md : Treat as runtime-contract reference. Track behavior changes to configuration schema.”
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@docs/config-reference.md` at line 1247, Update the `send_read_receipts` config description in the table row (the entry currently reading "Send read receipt to the sender after each incoming message is processed") to reflect the actual runtime trigger: state that read receipts are sent after a reply is successfully sent (e.g., "Send a read receipt to the sender after a reply is successfully sent. Default: `true`"). Locate the `send_read_receipts` table row and replace the sentence so wording matches runtime behavior.
1233-1254:⚠️ Potential issue | 🟠 MajorUpdate related runtime-contract docs for BlueBubbles operations/troubleshooting.
This adds new channel behavior/config surface, but related operator references should be updated in the same PR (or explicitly deferred):
docs/operations-runbook.mdanddocs/troubleshooting.md(plus localized counterparts for supported locales).As per coding guidelines, “When runtime surfaces change, update related references (
commands/providers/channels/config/runbook/troubleshooting).”🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@docs/config-reference.md` around lines 1233 - 1254, The new channels_config.bluebubbles config keys (server_url, password, allowed_senders, ignore_senders, webhook_secret, dm_policy, group_policy, group_allow_from, send_read_receipts) were added but related runtime/operator docs weren’t updated; update docs/operations-runbook.md and docs/troubleshooting.md (and all localized equivalents) to document BlueBubbles webhook endpoint (POST /bluebubbles), authentication (webhook_secret / Authorization: Bearer), policies (dm_policy, group_policy and group_allow_from semantics), allowed_senders/ignore_senders behavior, send_read_receipts default, and common troubleshooting steps for connectivity/auth failures and message routing.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Duplicate comments:
In `@docs/config-reference.md`:
- Line 1247: Update the `send_read_receipts` config description in the table row
(the entry currently reading "Send read receipt to the sender after each
incoming message is processed") to reflect the actual runtime trigger: state
that read receipts are sent after a reply is successfully sent (e.g., "Send a
read receipt to the sender after a reply is successfully sent. Default:
`true`"). Locate the `send_read_receipts` table row and replace the sentence so
wording matches runtime behavior.
- Around line 1233-1254: The new channels_config.bluebubbles config keys
(server_url, password, allowed_senders, ignore_senders, webhook_secret,
dm_policy, group_policy, group_allow_from, send_read_receipts) were added but
related runtime/operator docs weren’t updated; update docs/operations-runbook.md
and docs/troubleshooting.md (and all localized equivalents) to document
BlueBubbles webhook endpoint (POST /bluebubbles), authentication (webhook_secret
/ Authorization: Bearer), policies (dm_policy, group_policy and group_allow_from
semantics), allowed_senders/ignore_senders behavior, send_read_receipts default,
and common troubleshooting steps for connectivity/auth failures and message
routing.
In `@src/channels/bluebubbles.rs`:
- Around line 271-288: The Allowlist branch in is_group_allowed currently
returns true if chat_guid is in group_allow_from without checking
allowed_senders; change BlueBubblesGroupPolicy::Allowlist logic to require both
the chat GUID be allowlisted (group_allow_from contains "*" or equals chat_guid)
AND the sender is allowed (allowed_senders is empty or contains "*" or equals
sender), so only when both conditions hold return true; update the block inside
is_group_allowed to perform these two checks and return false otherwise.
- Around line 303-338: mark_read currently awaits the POST and can block; make
it truly fire-and-forget by moving the HTTP call into a spawned background task.
Keep the early check of send_read_receipts and compute normalised/encoded/url
synchronously, then clone/self-contain all values needed for the request (clone
self.client, password string, url) and call tokio::spawn(async move { ... }) to
perform the post and logging (use the same tracing.warn/info logic inside the
task). Reference mark_read, self.client, self.password, self.api_url and ensure
no await on the spawned task so the function returns immediately.
- Around line 818-822: The current tracing::warn!(error = %e, "BB audio
transcription failed") can leak attachment/file identifiers from nested error
chains; replace this direct error interpolation with a sanitized summary: remove
the %e from the warn call and instead pass a short, scrubbed string (e.g., the
error type or a call to a helper sanitize_error(e) that strips file paths/IDs
and sensitive tokens) or use std::any::type_name_of_val(&e) to log only the
error class; keep the existing fallback to self.parse_webhook_payload(payload)
and ensure no raw error chain is emitted at warn level.
In `@src/channels/transcription.rs`:
- Around line 90-94: The ffmpeg timeout/spawn-error branch can return early and
leak the temporary WAV file `tmp`; update the conversion call around
tokio::time::timeout(std::time::Duration::from_secs(120), ffmpeg_cmd.output())
so that any early error path removes the temp file before returning—e.g., in the
map_err closure and any spawn/output error handling, call
std::fs::remove_file(&tmp).unwrap_or_else(|_| ()) (or use a small drop
guard/TempDir wrapper) and then return the anyhow error, ensuring the cleanup
happens for both timeout and ffmpeg spawn/output failures that currently return
before the subsequent success check on `conv`.
ℹ️ Review info
Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: d67ab25a-7808-43e5-bc3b-3ac5fe182d57
📒 Files selected for processing (3)
docs/config-reference.mdsrc/channels/bluebubbles.rssrc/channels/transcription.rs
Add dm_policy, group_policy, group_allow_from, and send_read_receipts config contract to the VI localized channels-reference. The English doc (§4.20) introduced these keys in PR zeroclaw-labs#2495 but VI was not updated to include the BlueBubbles section at all.
|
@coderabbitai review |
✅ Actions performedReview triggered.
|
…I nav parity Add BlueBubbles channel-specific section to operations-runbook.md covering server requirements, webhook setup, config keys, health check, and upgrade steps. Add BlueBubbles troubleshooting entry to troubleshooting.md with diagnosis table for common failures (credential errors, webhook registration, signature mismatch, policy denials) and a network connectivity check command. Add missing Nextcloud Talk Setup link to docs/i18n/vi/channels-reference.md quick-paths section to match English navigation parity. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
@coderabbitai review |
✅ Actions performedReview triggered.
|
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@docs/troubleshooting.md`:
- Around line 185-211: The new BlueBubbles webhook troubleshooting section added
to docs/troubleshooting.md must be synchronized to all localized troubleshooting
docs (zh-CN, ja, ru, fr, vi, el) or explicitly deferred in the PR; either add
translated copies of the section into each locale file (matching title
"BlueBubbles: webhook not receiving messages" and the same
symptom/diagnosis/fixes content adapted for language) or, if translations cannot
be completed in this PR, update the PR description to include a clear deferral
note naming the owner responsible and a follow-up issue/PR reference that will
add the localized versions (per docs/i18n-guide.md checklist item 3). Ensure the
change references the exact English section in docs/troubleshooting.md so
reviewers can verify synchronization or the deferral plan.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 9cc4c863-edea-4492-a921-0c1b79feeff0
📒 Files selected for processing (3)
docs/i18n/vi/channels-reference.mddocs/operations-runbook.mddocs/troubleshooting.md
✅ Files skipped from review due to trivial changes (1)
- docs/operations-runbook.md
🚧 Files skipped from review as they are similar to previous changes (1)
- docs/i18n/vi/channels-reference.md
| ### BlueBubbles: webhook not receiving messages | ||
|
|
||
| Symptom: BlueBubbles is configured but ZeroClaw receives no inbound messages. | ||
|
|
||
| Diagnosis: | ||
|
|
||
| ```bash | ||
| zeroclaw channel doctor | ||
| ``` | ||
|
|
||
| Common causes and fixes: | ||
|
|
||
| | Symptom | Likely cause | Fix | | ||
| | -------------------------------------- | ------------------------------------------------ | ---------------------------------------------------------------- | | ||
| | `BlueBubbles` shows unhealthy | Wrong `server_url` or `password` | Verify URL is reachable and password matches BB server | | ||
| | No webhook events arrive | Webhook not registered in BlueBubbles UI | Add `POST <host>/bluebubbles` as a webhook in BB server settings | | ||
| | Signature verification failure in logs | `webhook_secret` mismatch | Copy the exact secret from BlueBubbles server UI into config | | ||
| | Events arrive but no reply | `dm_policy` or `group_policy` denying the sender | Check policy settings and `allowed_senders` / `group_allow_from` | | ||
|
|
||
| Network check — verify the ZeroClaw gateway is reachable from the BlueBubbles host: | ||
|
|
||
| ```bash | ||
| curl -v http://<zeroclaw-host>:<port>/bluebubbles -X POST -d '{}' -H 'Content-Type: application/json' | ||
| ``` | ||
|
|
||
| Expected: `400` or `401` (not a connection error). | ||
|
|
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "=== 1. List all changed files to understand PR scope ==="
git diff --name-only HEAD~1..HEAD 2>/dev/null | head -30 || echo "(Could not get git diff)"
echo
echo "=== 2. Explore docs directory structure ==="
find docs -type d -maxdepth 2 | sort | head -20
echo
echo "=== 3. Check for locale-specific directory patterns ==="
fd -t d -H '^(i18n|locales|zh-CN|ja|ru|fr|vi|el)$' docs 2>/dev/null || echo "(No locale dirs found)"
echo
echo "=== 4. List troubleshooting files across all locations ==="
fd -H 'troubleshooting' docs 2>/dev/null | sort || echo "(No troubleshooting files found)"
echo
echo "=== 5. Check for i18n-related docs ==="
fd -H 'i18n|locali' docs -type f 2>/dev/null | head -10 || true
echo
echo "=== 6. Search for deferral notes in common locations ==="
rg -l 'deferr|follow.?up|future.*localiz|i18n.*follow' docs README.md 2>/dev/null | head -10 || echo "(No deferral notes found)"
echo
echo "=== 7. Check docs/troubleshooting.md history ==="
git log --oneline docs/troubleshooting.md 2>/dev/null | head -5 || echo "(Could not get git log)"Repository: zeroclaw-labs/zeroclaw
Length of output: 1487
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "=== 1. Read docs/i18n-guide.md (mandatory completion checklist) ==="
head -100 docs/i18n-guide.md
echo
echo "=== 2. Check if BlueBubbles section exists in EN troubleshooting ==="
grep -n "BlueBubbles" docs/troubleshooting.md | head -5
echo
echo "=== 3. Check BlueBubbles presence in each locale ==="
for locale in zh-CN ja ru fr vi el; do
count=$(grep -c "BlueBubbles" "docs/i18n/$locale/troubleshooting.md" 2>/dev/null || echo "0")
echo " $locale: $count occurrences"
done
echo
echo "=== 4. Show line counts for comparison (EN vs locales) ==="
wc -l docs/troubleshooting.md docs/i18n/*/troubleshooting.md | tail -10
echo
echo "=== 5. Search for deferral note in PR-related docs ==="
rg -n "BlueBubbles|deferr.*localiz|follow.*PR" docs/pr-workflow.md docs/README.md docs/SUMMARY.md 2>/dev/null | head -20 || echo "(No deferral found)"Repository: zeroclaw-labs/zeroclaw
Length of output: 6094
Sync BlueBubbles troubleshooting section to all localized docs or document deferral in PR.
The BlueBubbles webhook troubleshooting section (lines 185–211) was added to the English docs but is missing from all 7 localized troubleshooting files (zh-CN, ja, ru, fr, vi, el). Per the i18n-guide.md mandatory completion checklist (item 3), top-level runtime docs changes must sync to all locales in the same PR. If localization cannot complete in this PR, add an explicit deferral note to the PR description with owner and follow-up issue/PR reference; silent deferral is not permitted. Currently neither sync nor deferral is present.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@docs/troubleshooting.md` around lines 185 - 211, The new BlueBubbles webhook
troubleshooting section added to docs/troubleshooting.md must be synchronized to
all localized troubleshooting docs (zh-CN, ja, ru, fr, vi, el) or explicitly
deferred in the PR; either add translated copies of the section into each locale
file (matching title "BlueBubbles: webhook not receiving messages" and the same
symptom/diagnosis/fixes content adapted for language) or, if translations cannot
be completed in this PR, update the PR description to include a clear deferral
note naming the owner responsible and a follow-up issue/PR reference that will
add the localized versions (per docs/i18n-guide.md checklist item 3). Ensure the
change references the exact English section in docs/troubleshooting.md so
reviewers can verify synchronization or the deferral plan.
…ent flow collisions Include the OAuth state token in the PKCE filename so concurrent /auth/google starts each get their own session file. Validate state format before using it in the path to prevent path traversal. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…g disabled Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
theonlyhennygod
left a comment
There was a problem hiding this comment.
Blocking merge for now: this PR is currently in a merge-conflict state with main (DIRTY/CONFLICTING). Please rebase or resolve conflicts against main, then I can re-review and merge while preserving contributor attribution.
…2495 gateway/mod.rs: - Hash sender identifier before INFO log to avoid raw PII exposure (phone numbers / Apple IDs); remove content fragment from log line - Mark-read calls wrapped in tokio::spawn (fire-and-forget) to prevent BlueBubbles 30 s timeout from blocking message-processing task gateway/oauth.rs: - Log Google token revocation failures instead of silently discarding them (let _ → if let Err(e)); local deletion still proceeds - Remove OAuthStartQuery.redirect field that was declared but never consumed by the handler (YAGNI; eliminates contract mismatch) docs i18n follow-through: - docs/i18n/vi/channels-reference.md: add BlueBubbles dm_policy exception note; add BlueBubbles to allowed_senders field list - docs/i18n/el/channels-reference.md: add BlueBubbles allowlist exception note to security-policy section - docs/i18n/vi/config-reference.md: add [channels_config.bluebubbles] section with dm_policy, group_policy, group_allow_from, send_read_receipts
…abs#2495 Remove path.exists() pre-check; handle NotFound directly from remove_file to eliminate the race between existence check and the actual delete.
Replace blocking std::fs::remove_dir_all inside .map_err() with async tokio::fs::remove_dir_all in a match block so cleanup does not block the async executor on error. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
@coderabbitai review |
✅ Actions performedReview triggered.
|
There was a problem hiding this comment.
Actionable comments posted: 1
♻️ Duplicate comments (4)
src/gateway/mod.rs (2)
2377-2379:⚠️ Potential issue | 🟡 MinorFix stale status-code comment (200 vs actual 202).
The comment says “Returning 200 immediately,” but this handler returns
202 Accepted(Line 2496).🤖 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 - 2379, Update the stale comment that reads "Returning 200 immediately" to reflect the actual response code sent by this handler (202 Accepted); locate the comment near the Arc::clone(bluebubbles) usage (e.g., the bb = Arc::clone(bluebubbles) context) and change the wording to something like "Returning 202 Accepted immediately keeps the HTTP lifecycle within the timeout budget while the background task handles transcription, LLM inference, and reply." Ensure the comment aligns with the response value returned by the handler.
585-591:⚠️ Potential issue | 🟠 MajorDon’t silently drop BlueBubbles when initialization fails.
Line 590–Line 591(.ok().flatten()) converts a real init error intoNone, so startup proceeds with the channel silently disabled.✅ Fail-fast fix
- .transpose() - .map_err(|e| { - tracing::error!("Failed to initialize BlueBubbles channel: {e}"); - e - }) - .ok() - .flatten(); + .transpose() + .map_err(|e| { + tracing::error!("Failed to initialize BlueBubbles channel: {e}"); + e + })?;As per coding guidelines,
src/**/*.rsshould prefer explicit errors over silent fallback.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/gateway/mod.rs` around lines 585 - 591, The current chain (.transpose().map_err(|e| { tracing::error!("Failed to initialize BlueBubbles channel: {e}"); e }).ok().flatten()) swallows initialization errors by turning Err into None; instead remove the .ok().flatten() and propagate the error so startup fails fast—i.e., keep the .transpose().map_err(...) and then use the ? operator (or return the Result) to propagate the error from the BlueBubbles init call; ensure the surrounding function returns a Result so the error can bubble up rather than being silently ignored.src/channels/transcription.rs (1)
90-93:⚠️ Potential issue | 🟠 MajorTimeout/spawn failure path can leak the temp WAV file.
Line 90–Line 93returns early via?beforetmpcleanup, so partial conversion output can remain on disk.♻️ Cleanup-safe handling
- let conv = tokio::time::timeout(std::time::Duration::from_secs(120), ffmpeg_cmd.output()) - .await - .map_err(|_| anyhow::anyhow!("ffmpeg CAF→WAV conversion timed out after 120s"))? - .context("ffmpeg CAF→WAV conversion failed")?; + let conv = match tokio::time::timeout( + std::time::Duration::from_secs(120), + ffmpeg_cmd.output(), + ) + .await + { + Ok(Ok(o)) => o, + Ok(Err(e)) => { + let _ = tokio::fs::remove_file(&tmp).await; + return Err(anyhow::anyhow!("ffmpeg CAF→WAV conversion failed: {e}")); + } + Err(_) => { + let _ = tokio::fs::remove_file(&tmp).await; + anyhow::bail!("ffmpeg CAF→WAV conversion timed out after 120s"); + } + };As per coding guidelines,
src/**/*.rsshould keep error paths obvious and localized.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/channels/transcription.rs` around lines 90 - 93, The timeout/spawn failure path can leave the temporary WAV file (tmp) on disk because the early return from the tokio::time::timeout / ffmpeg_cmd.output() block (`let conv = ...?;`) occurs before tmp is cleaned up; ensure tmp is removed on all error/timeout paths by either using a RAII guard (e.g., wrap tmp in a type that deletes on Drop or use tempfile::NamedTempFile) or explicitly remove the temp file before returning when the timeout returns Err or when ffmpeg_cmd.output() fails; update the code around ffmpeg_cmd.output(), timeout handling, and the tmp variable to guarantee cleanup on both timeout and conversion error paths.src/gateway/oauth.rs (1)
126-137:⚠️ Potential issue | 🟡 MinorOAuth routes remain open when pairing is disabled.
When
require_pairing()is false,is_authenticatedreturns true for any token (including empty), leaving OAuth connect/status/revoke endpoints accessible without authentication. While this respects the operator's choice, OAuth credential management is sensitive and should arguably require explicit opt-in for unpaired access.As per coding guidelines: "Deny-by-default for access and exposure boundaries."
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/gateway/oauth.rs` around lines 126 - 137, auth_check currently treats empty tokens as valid when PairingGuard.is_authenticated returns true for unpaired mode; change it to deny-by-default for OAuth endpoints by rejecting empty or missing Bearer tokens unless there is an explicit operator opt-in flag. Update auth_check to (1) extract the token as now, (2) if token is empty, return false unless a dedicated config/flag (e.g. state.pairing.allow_unpaired_oauth or state.config.allow_unpaired_oauth) is true, and (3) otherwise call state.pairing.is_authenticated(token); reference auth_check, state.pairing.is_authenticated, and add or use a clear opt-in symbol like allow_unpaired_oauth to gate unpaired OAuth access.
🧹 Nitpick comments (2)
src/gateway/oauth.rs (2)
73-89: Consider hardening pre-existing directories.
DirBuilder::mode(0o700)only applies when creating a new directory. If the OAuth directory already exists with broader permissions (e.g., from a previous version or manual creation), those permissions remain unchanged. This was noted in a previous review cycle.🔒 Optional: Re-apply permissions after creation
#[cfg(unix)] { + use std::os::unix::fs::PermissionsExt; use std::os::unix::fs::DirBuilderExt; let dir = dir.clone(); tokio::task::spawn_blocking(move || { - std::fs::DirBuilder::new().recursive(true).mode(0o700).create(&dir) + std::fs::DirBuilder::new().recursive(true).mode(0o700).create(&dir)?; + // Harden pre-existing directory + std::fs::set_permissions(&dir, std::fs::Permissions::from_mode(0o700)) }) .await .map_err(|e| anyhow::anyhow!("spawn_blocking panicked creating oauth dir: {e}"))??; }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/gateway/oauth.rs` around lines 73 - 89, The current ensure_oauth_dir function uses DirBuilder::mode(0o700) which only affects newly created directories and won't tighten permissions on an already-existing oauth dir; update ensure_oauth_dir to, after creation (or if create fails because dir exists), explicitly set the directory permissions to 0o700 (on Unix) using std::fs::set_permissions or nix/chmod wrapper so pre-existing loose permissions are hardened; keep the existing spawn_blocking approach for the blocking ops and apply the permission change inside that Unix branch (referencing ensure_oauth_dir, DirBuilder::mode, and the create call) and skip/adjust for non-Unix platforms accordingly.
711-716: Consider escaping single quotes for defense-in-depth.The
html_escapefunction handles&,<,>,"but not single quotes ('). While current template usage appears safe, unescaped single quotes can be an XSS vector in attribute contexts using single-quoted values.🛡️ Add single quote escaping
fn html_escape(s: &str) -> String { s.replace('&', "&") .replace('<', "<") .replace('>', ">") .replace('"', """) + .replace('\'', "'") }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/gateway/oauth.rs` around lines 711 - 716, The html_escape function currently replaces &, <, >, and " but omits single quotes; update the html_escape function to also replace the single-quote character (') with an HTML entity (e.g., ' or ') so single-quoted attribute contexts are safe—add the .replace for '\'' -> "'" (or "'") alongside the existing replaces in the html_escape function to ensure single quotes are escaped.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@docs/i18n/vi/config-reference.md`:
- Around line 479-480: Update the enum documentation for dm_policy and
group_policy to match the new schema by replacing the outdated `"disabled"`
option with the correct `"block"` value; locate the table rows that mention
`dm_policy` and `group_policy` and change the enum list to `"open"` (default) |
`"allowlist"` | `"block"` so the docs reflect the runtime config contract and
prevent invalid configurations.
---
Duplicate comments:
In `@src/channels/transcription.rs`:
- Around line 90-93: The timeout/spawn failure path can leave the temporary WAV
file (tmp) on disk because the early return from the tokio::time::timeout /
ffmpeg_cmd.output() block (`let conv = ...?;`) occurs before tmp is cleaned up;
ensure tmp is removed on all error/timeout paths by either using a RAII guard
(e.g., wrap tmp in a type that deletes on Drop or use tempfile::NamedTempFile)
or explicitly remove the temp file before returning when the timeout returns Err
or when ffmpeg_cmd.output() fails; update the code around ffmpeg_cmd.output(),
timeout handling, and the tmp variable to guarantee cleanup on both timeout and
conversion error paths.
In `@src/gateway/mod.rs`:
- Around line 2377-2379: Update the stale comment that reads "Returning 200
immediately" to reflect the actual response code sent by this handler (202
Accepted); locate the comment near the Arc::clone(bluebubbles) usage (e.g., the
bb = Arc::clone(bluebubbles) context) and change the wording to something like
"Returning 202 Accepted immediately keeps the HTTP lifecycle within the timeout
budget while the background task handles transcription, LLM inference, and
reply." Ensure the comment aligns with the response value returned by the
handler.
- Around line 585-591: The current chain (.transpose().map_err(|e| {
tracing::error!("Failed to initialize BlueBubbles channel: {e}"); e
}).ok().flatten()) swallows initialization errors by turning Err into None;
instead remove the .ok().flatten() and propagate the error so startup fails
fast—i.e., keep the .transpose().map_err(...) and then use the ? operator (or
return the Result) to propagate the error from the BlueBubbles init call; ensure
the surrounding function returns a Result so the error can bubble up rather than
being silently ignored.
In `@src/gateway/oauth.rs`:
- Around line 126-137: auth_check currently treats empty tokens as valid when
PairingGuard.is_authenticated returns true for unpaired mode; change it to
deny-by-default for OAuth endpoints by rejecting empty or missing Bearer tokens
unless there is an explicit operator opt-in flag. Update auth_check to (1)
extract the token as now, (2) if token is empty, return false unless a dedicated
config/flag (e.g. state.pairing.allow_unpaired_oauth or
state.config.allow_unpaired_oauth) is true, and (3) otherwise call
state.pairing.is_authenticated(token); reference auth_check,
state.pairing.is_authenticated, and add or use a clear opt-in symbol like
allow_unpaired_oauth to gate unpaired OAuth access.
---
Nitpick comments:
In `@src/gateway/oauth.rs`:
- Around line 73-89: The current ensure_oauth_dir function uses
DirBuilder::mode(0o700) which only affects newly created directories and won't
tighten permissions on an already-existing oauth dir; update ensure_oauth_dir
to, after creation (or if create fails because dir exists), explicitly set the
directory permissions to 0o700 (on Unix) using std::fs::set_permissions or
nix/chmod wrapper so pre-existing loose permissions are hardened; keep the
existing spawn_blocking approach for the blocking ops and apply the permission
change inside that Unix branch (referencing ensure_oauth_dir, DirBuilder::mode,
and the create call) and skip/adjust for non-Unix platforms accordingly.
- Around line 711-716: The html_escape function currently replaces &, <, >, and
" but omits single quotes; update the html_escape function to also replace the
single-quote character (') with an HTML entity (e.g., ' or ') so
single-quoted attribute contexts are safe—add the .replace for '\'' -> "'"
(or "'") alongside the existing replaces in the html_escape function to
ensure single quotes are escaped.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 09bf57a8-0aa5-40e6-bf01-e1f8af34d24b
📒 Files selected for processing (6)
docs/i18n/el/channels-reference.mddocs/i18n/vi/channels-reference.mddocs/i18n/vi/config-reference.mdsrc/channels/transcription.rssrc/gateway/mod.rssrc/gateway/oauth.rs
🚧 Files skipped from review as they are similar to previous changes (2)
- docs/i18n/el/channels-reference.md
- docs/i18n/vi/channels-reference.md
| | `dm_policy` | Tùy chọn | `"open"` (mặc định) \| `"allowlist"` \| `"disabled"` — kiểm soát tin nhắn trực tiếp | | ||
| | `group_policy` | Tùy chọn | `"open"` (mặc định) \| `"allowlist"` \| `"disabled"` — kiểm soát chat nhóm | |
There was a problem hiding this comment.
dm_policy / group_policy enum docs are inconsistent with the new schema.
These rows document "disabled", but this PR’s config contract introduces "block". Using "disabled" from docs will lead to invalid configuration.
🛠️ Suggested doc fix
-| `dm_policy` | Tùy chọn | `"open"` (mặc định) \| `"allowlist"` \| `"disabled"` — kiểm soát tin nhắn trực tiếp |
-| `group_policy` | Tùy chọn | `"open"` (mặc định) \| `"allowlist"` \| `"disabled"` — kiểm soát chat nhóm |
+| `dm_policy` | Tùy chọn | `"open"` (mặc định) \| `"allowlist"` \| `"block"` — kiểm soát tin nhắn trực tiếp |
+| `group_policy` | Tùy chọn | `"open"` (mặc định) \| `"allowlist"` \| `"block"` — kiểm soát chat nhóm |Based on learnings: Applies to docs/config-reference.md — treat as runtime-contract reference and track configuration schema behavior changes.
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| | `dm_policy` | Tùy chọn | `"open"` (mặc định) \| `"allowlist"` \| `"disabled"` — kiểm soát tin nhắn trực tiếp | | |
| | `group_policy` | Tùy chọn | `"open"` (mặc định) \| `"allowlist"` \| `"disabled"` — kiểm soát chat nhóm | | |
| | `dm_policy` | Tùy chọn | `"open"` (mặc định) \| `"allowlist"` \| `"block"` — kiểm soát tin nhắn trực tiếp | | |
| | `group_policy` | Tùy chọn | `"open"` (mặc định) \| `"allowlist"` \| `"block"` — kiểm soát chat nhóm | |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@docs/i18n/vi/config-reference.md` around lines 479 - 480, Update the enum
documentation for dm_policy and group_policy to match the new schema by
replacing the outdated `"disabled"` option with the correct `"block"` value;
locate the table rows that mention `dm_policy` and `group_policy` and change the
enum list to `"open"` (default) | `"allowlist"` | `"block"` so the docs reflect
the runtime config contract and prevent invalid configurations.
|
Closing — no longer needed. |
Summary
dm_policyandgroup_policyconfig fields to[channels_config.bluebubbles]withopen,allowlist, andblockvariantssend_read_receiptsbool (defaulttrue) to control iMessage read-receipt deliverygroup_allow_fromallowlist forgroup_policy = "allowlist"modesrc/gateway/oauth.rs) with Google OAuth flow, PKCE, token storage, and revocationLabel Snapshot
Change Metadata
src/channels/bluebubbles.rs,src/config/schema.rs,src/gateway/mod.rs,src/gateway/oauth.rs, docsLinked Issue
No upstream Linear key — external contributor.
Validation Evidence
cargo fmt --all -- --check: passcargo clippy --all-targets -- -D warnings: passcargo test: pass./scripts/ci/rust_quality_gate.sh: pass./scripts/ci/rust_strict_delta_gate.sh: pass./scripts/ci/docs_quality_gate.sh: passSecurity Impact
dm_policy/group_policygate inbound message processing — deny-by-default paths addedmode(0o600), directorymode(0o700); token revocation on logoutsend_read_receipts: no security impact; controlled by operator configallowed_senders/group_allow_fromvalidated at startup — empty allowlist withallowlistpolicy is a startup errorPrivacy and Data Hygiene
mark_readlogsCompatibility
dm_policy = "open",group_policy = "open",send_read_receipts = true)oauth.rsis a new module; no existing behavior changedi18n Follow-Through
channels-reference.mdanddocs/README.mdupdated for BlueBubbles §4.15 paritychannels-reference.mdupdateddocs/troubleshooting.mdlines 185-211) tozh-CN,ja,ru,fr,vi,elis deferred to a follow-up PR. Content is ops/troubleshooting only and does not block functionality. Follow-up will add translated copies perdocs/i18n-guide.mdchecklist item 3.Human Verification
dm_policy/group_policyenforcement tested with actual BlueBubbles serversend_read_receipts = falsedisables read receipts in group chatsSide Effects
oauth.rsadds/auth/google,/auth/google/callback,/auth/status,/auth/revokeroutes to the gateway routerallowlistpolicy with empty list is now a hard startup errorAgent Notes
claude-review.ymlworkflow removed from branch (fork-only, not for upstream)cargo fmt --allapplied after each fix roundRollback Plan
Revert the PR. All changes are additive/isolated — no data migrations required.
Risks
risk: highlabel — gateway + security surface (OAuth, policy enforcement)/auth/googlestarts could race; acceptable for current usage (single-user deployment)Review Fixes (Round 1–11)
Multiple rounds of CodeRabbit findings addressed including: OAuth security hardening, PII removal from logs, allowlist validation, CSRF state token, token revocation, chunking docs, format fixes, VI nav parity, troubleshooting section, and runbook additions.
Review Fixes (i18n Deferral)
i18n follow-through for BlueBubbles webhook troubleshooting section (
docs/troubleshooting.mdlines 185-211):The English section "BlueBubbles: webhook not receiving messages" added in this PR has not been translated to the supported locales (
zh-CN,ja,ru,fr,vi,el).Deferral: Translation deferred to a follow-up PR. Content is troubleshooting/ops and does not block functionality. Follow-up will add translated copies per
docs/i18n-guide.mdchecklist item 3.Summary by CodeRabbit
Release Notes
New Features
Improvements
Documentation