host: generic ChannelHost capability boundary; port whatsapp_web + telegram#3
Conversation
- ShutdownHook is now async (FnOnce -> ShutdownFuture) to match OpenHuman's
async shutdown-hook contract.
- ReactionDecision gains `emoji` (presentation/telegram emoji ACK reactions),
mirroring OpenHuman's ReactionDecision { should_react, emoji }.
- ApprovalGate::request now defaults to an unsupported error so parse-only
channel adapters (the common case — channels observe the surface and parse
replies; the tool gate raises approvals host-internally) implement nothing.
- Export ReactionQuery.
Claude-Session: https://claude.ai/code/session_013NzEbXoeB3YzsbmpjVUTPW
Move the WhatsApp Web (multi-device) provider into tinychannels behind a new `whatsapp-web` feature (pulls the native whatsapp-rust / wacore / serde-big-array stack; off by default). Its sole host coupling — `core::shutdown::register` — is rewired onto `host::LifecycleRegistry`: the provider gains an optional `Arc<dyn LifecycleRegistry>` (set via `with_lifecycle`) and registers its WA-session teardown through it in `listen()`. Real and feature-off stub impls both carry the field so `startup` can call `.with_lifecycle(...)` unconditionally. 29 whatsapp_web tests pass under `--features whatsapp-web`. Claude-Session: https://claude.ai/code/session_013NzEbXoeB3YzsbmpjVUTPW
Host boundary:
- Add AllowlistStore capability (persist a newly-authorized identity into a
channel's configured allowlist) — the one new capability telegram needs.
Security:
- Add `security::PairingGuard` (+ constant_time_eq, token/code helpers, lockout
consts) — the portable first-run bind-code pairing guard, moved verbatim from
OpenHuman (it was already self-contained: parking_lot/sha2/uuid/rand/hex/tokio).
Telegram transport (providers/telegram/):
- Port the Bot API driver, session_store, and pairing usage into tinychannels.
Host couplings are inverted onto capabilities/injection:
- voice-note STT → injected `host::Transcriber`
- allowlist persist → injected `host::AllowlistStore`
- reaction fan-out → injected `host::EventSink` ("channel"/reaction_* events)
- proxy HTTP client → injected via `with_http_client`
- pairing → `crate::security::PairingGuard`
- The host-glue (remote_control, bus, approval_surface subscribers) stays in
OpenHuman; session_store is `pub` so those can consume it across the boundary.
- Cargo: reqwest `multipart`, tokio `fs`.
975 tests pass (`cargo test --all-features`), incl. 154 telegram + host tests.
Claude-Session: https://claude.ai/code/session_013NzEbXoeB3YzsbmpjVUTPW
|
Warning Review limit reachedYou’ve reached a temporary PR review limit under our Fair Usage Limits Policy. Next review available in: 7 minutes Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Run ID: ⛔ Files ignored due to path filters (1)
📒 Files selected for processing (36)
Comment |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: e1ea308d14
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| format!("{chunk}\n\n(continues...)") | ||
| } else if index == chunks.len() - 1 { | ||
| format!("(continued)\n\n{chunk}") | ||
| } else { | ||
| format!("(continued)\n\n{chunk}\n\n(continues...)") |
There was a problem hiding this comment.
Reserve room for Telegram continuation markers
When an outbound Telegram message is just over the 4096-unit limit, split_message_for_telegram can return a first chunk that is already at the platform maximum; appending (continues...) or (continued) after chunking makes that payload exceed Telegram's limit, so both the Markdown attempt and the plain-text retry send the same overlong text and the whole send fails. Reserve the marker length before splitting, or omit markers for chunks that would otherwise be at the limit.
Useful? React with 👍 / 👎.
| channel: "whatsapp".to_string(), | ||
| sender: normalized.clone(), | ||
| reply_target, | ||
| content: text, |
There was a problem hiding this comment.
Drop empty WhatsApp Web inbound messages
For WhatsApp events that are media/stickers/etc. with neither conversation nor extended_text_message.text, extract_message_text returns an empty string but the listener still forwards it as a ChannelMessage. In allowed chats this creates blank agent turns instead of ignoring unsupported non-text payloads (unlike the Cloud WhatsApp path, which skips non-text/empty content), so add a text.trim().is_empty() guard before sending.
Useful? React with 👍 / 👎.
| sender: normalized.clone(), | ||
| reply_target, | ||
| content: text, | ||
| timestamp: chrono::Utc::now().timestamp_millis() as u64, |
There was a problem hiding this comment.
Store WhatsApp Web timestamps in seconds
ChannelMessage::timestamp is treated as Unix seconds by the existing providers (for example Lark and Signal convert millisecond source timestamps by dividing by 1000), but this new WhatsApp Web path stores timestamp_millis() directly. Any downstream sorting, retention, or history logic that expects seconds will see these messages as far-future timestamps; use timestamp() or divide the millisecond value before assigning it.
Useful? React with 👍 / 👎.
The Rust SDK CI runs `cargo clippy --all-targets -- -D warnings` (and --all-features). Fix the lints carried over from the migrated providers: - Auto-fixed collapsible_if (×48), needless conversions, etc. - yuanbao/outbound: filter().next_back() → rfind(). - imessage: allow(dead_code) on macOS-only SQLite helpers (unused on Linux CI). - irc: allow(type_complexity) on a test-seam parse fn. - yuanbao/inbound: allow(large_enum_variant) on PipelineOutcome. - yuanbao: module-scoped allow(field_reassign_with_default) for test setup. - whatsapp_web: feature-gate the real-impl-only imports and the test module so the feature-off stub build has no unused imports and the feature-on build keeps them (the autofix had stripped feature-only imports). `cargo clippy --all-targets` and `--all-targets --all-features` are both clean; fmt clean; 975 tests pass. Claude-Session: https://claude.ai/code/session_013NzEbXoeB3YzsbmpjVUTPW
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 94fa9f5cb0
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| if allowed_numbers.is_empty() | ||
| || allowed_numbers.iter().any(|n| n == "*" || n == &normalized) |
There was a problem hiding this comment.
Deny empty WhatsApp Web allowlists
When allowed_numbers is left at its default empty value for the new WhatsApp Web backend, this condition treats every sender as authorized, unlike the existing Cloud WhatsApp provider/tests where an empty allowlist denies all unless "*" is configured. Enabling Web mode with only session_path therefore exposes the agent to any WhatsApp contact or group participant; require "*" for allow-all or mirror the Cloud deny-by-default behavior here and in is_number_allowed.
Useful? React with 👍 / 👎.
| let path = Path::new(target); | ||
| if !path.exists() { | ||
| anyhow::bail!("Telegram attachment path not found: {target}"); |
There was a problem hiding this comment.
Restrict Telegram attachment paths to safe files
Attachment targets come from ordinary outbound text markers (or a bare path), and any existing local path reaches the send_* helpers that read and upload the file. In a Telegram chat, a user can prompt the agent to emit something like [DOCUMENT:/etc/passwd] or another readable secret path and the bot will exfiltrate it; local attachments should be limited to host-issued/generated files under an approved workspace rather than arbitrary filesystem paths.
Useful? React with 👍 / 👎.
Summary
Introduces a generic host-capability boundary so rich channel providers can live in tinychannels without linking OpenHuman internals, and ports two more providers onto it.
hostmodule — oneChannelHostaggregator exposing each capability as an optionalArc<dyn …>accessor (lean providers use none, rich ones use many):TurnDispatcher,Transcriber,SpeechSynthesizer,ApprovalGate,ReactionGate,ConversationStore,EventSink,LifecycleRegistry,RunLedger,AllowlistStore, plus the existingcontext::Memory. ShipsHostCapabilities,ChannelHostBuilder,NoopHost, andProviderContext(the standardized construction seam).security::PairingGuard— the portable first-run bind-code pairing/allowlist guard (moved from OpenHuman; it was already self-contained).whatsapp-webfeature; its shutdown teardown now goes throughLifecycleRegistry.session_store, pairing). Host couplings inverted onto capabilities: voice STT →Transcriber, allowlist persist →AllowlistStore, reaction fan-out →EventSink, proxy client →with_http_client. Host glue (remote-control, bus/approval subscribers) stays in OpenHuman and consumes the now-pubsession_store.whatsapp-webfeature + native WA stack;reqwestmultipart;tokiofs; re-exporttokio_tungstenite.API Or Behavior Changes
Additive. New public modules
hostandsecurity; newwhatsapp-webfeature (off by default);TelegramChannel/WhatsAppWebChannelgainwith_*builders for host-capability injection. No behavior change for existing providers.Tests
cargo fmt --checkcargo clippy --all-targets -- -D warnings— pre-existing warnings in ported provider code (unused imports / dead_code carried over verbatim from the prior migration); not introduced here.cargo clippy --all-targets --all-features -- -D warnings— same.cargo build --all-targetscargo build --all-targets --all-featurescargo testcargo test --all-features— 975 passedDocumentation
Every capability trait and DTO is documented inline (including which provider needs it). Module docs on
host,security, and each provider explain the boundary.https://claude.ai/code/session_013NzEbXoeB3YzsbmpjVUTPW