Skip to content

host: generic ChannelHost capability boundary; port whatsapp_web + telegram#3

Merged
senamakel merged 4 commits into
mainfrom
feat/host-boundary-provider-migration
Jul 5, 2026
Merged

host: generic ChannelHost capability boundary; port whatsapp_web + telegram#3
senamakel merged 4 commits into
mainfrom
feat/host-boundary-provider-migration

Conversation

@senamakel

Copy link
Copy Markdown
Member

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.

  • host module — one ChannelHost aggregator exposing each capability as an optional Arc<dyn …> accessor (lean providers use none, rich ones use many): TurnDispatcher, Transcriber, SpeechSynthesizer, ApprovalGate, ReactionGate, ConversationStore, EventSink, LifecycleRegistry, RunLedger, AllowlistStore, plus the existing context::Memory. Ships HostCapabilities, ChannelHostBuilder, NoopHost, and ProviderContext (the standardized construction seam).
  • security::PairingGuard — the portable first-run bind-code pairing/allowlist guard (moved from OpenHuman; it was already self-contained).
  • whatsapp_web ported behind a new whatsapp-web feature; its shutdown teardown now goes through LifecycleRegistry.
  • telegram transport ported (Bot API driver, 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-pub session_store.
  • Cargo: whatsapp-web feature + native WA stack; reqwest multipart; tokio fs; re-export tokio_tungstenite.

API Or Behavior Changes

Additive. New public modules host and security; new whatsapp-web feature (off by default); TelegramChannel/WhatsAppWebChannel gain with_* builders for host-capability injection. No behavior change for existing providers.

Tests

  • cargo fmt --check
  • cargo 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-targets
  • cargo build --all-targets --all-features
  • cargo test
  • cargo test --all-features975 passed

Documentation

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

senamakel added 3 commits July 5, 2026 04:01
- 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
@coderabbitai

coderabbitai Bot commented Jul 5, 2026

Copy link
Copy Markdown

Warning

Review limit reached

You’ve reached a temporary PR review limit under our Fair Usage Limits Policy.

Your recent review volume is higher than typical usage, so adaptive limits are currently applied.

Next review available in: 7 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

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 configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: fe351a8f-80b8-49f7-8ecc-b5aa311b76a1

📥 Commits

Reviewing files that changed from the base of the PR and between 1c51479 and 94fa9f5.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (36)
  • Cargo.toml
  • src/host/mod.rs
  • src/host/noop.rs
  • src/host/services.rs
  • src/lib.rs
  • src/providers/discord/api.rs
  • src/providers/email_channel.rs
  • src/providers/imessage.rs
  • src/providers/irc.rs
  • src/providers/lark.rs
  • src/providers/mattermost.rs
  • src/providers/mod.rs
  • src/providers/qq.rs
  • src/providers/telegram/attachments.rs
  • src/providers/telegram/channel.rs
  • src/providers/telegram/channel_core.rs
  • src/providers/telegram/channel_ops.rs
  • src/providers/telegram/channel_recv.rs
  • src/providers/telegram/channel_send.rs
  • src/providers/telegram/channel_tests.rs
  • src/providers/telegram/channel_types.rs
  • src/providers/telegram/mod.rs
  • src/providers/telegram/session_store.rs
  • src/providers/telegram/text.rs
  • src/providers/whatsapp_web.rs
  • src/providers/whatsapp_web_tests.rs
  • src/providers/yuanbao/connection.rs
  • src/providers/yuanbao/cos.rs
  • src/providers/yuanbao/inbound.rs
  • src/providers/yuanbao/media.rs
  • src/providers/yuanbao/mod.rs
  • src/providers/yuanbao/outbound.rs
  • src/providers/yuanbao/proto.rs
  • src/providers/yuanbao/types.rs
  • src/providers/yuanbao/wire.rs
  • src/security.rs

Comment @coderabbitai help to get the list of available commands.

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

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 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".

Comment on lines +119 to +123
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...)")

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge 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,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge 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,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge 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

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

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 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".

Comment on lines +375 to +376
if allowed_numbers.is_empty()
|| allowed_numbers.iter().any(|n| n == "*" || n == &normalized)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge 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 👍 / 👎.

Comment on lines +278 to +280
let path = Path::new(target);
if !path.exists() {
anyhow::bail!("Telegram attachment path not found: {target}");

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge 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 👍 / 👎.

@senamakel senamakel merged commit d829538 into main Jul 5, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant