From d7236debb62c6479c4a6058b526fd11393992f84 Mon Sep 17 00:00:00 2001 From: oxoxDev Date: Sat, 18 Jul 2026 02:44:28 +0530 Subject: [PATCH 1/9] feat(core): add channels compile-time feature gate + forward to desktop shell (#4801) --- Cargo.toml | 33 +++++++++++++++++++++++++++++++-- app/src-tauri/Cargo.toml | 1 + 2 files changed, 32 insertions(+), 2 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index aacb8d574b..5e6537d426 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -355,7 +355,7 @@ tokio = { version = "1", features = ["test-util"] } proptest = "1" [features] -default = ["tokenjuice-treesitter", "voice", "web3", "media", "meet", "skills", "flows", "mcp", "tui"] +default = ["tokenjuice-treesitter", "voice", "web3", "media", "meet", "skills", "flows", "mcp", "channels", "tui"] # AST-aware code compression (tree-sitter Rust/TS/Python grammars; C build). # On by default; disable to fall back to the brace-depth heuristic. tokenjuice-treesitter = [ @@ -479,6 +479,33 @@ mcp = [] # build-fact "tui feature disabled at compile time" error from the untouched # `"tui" | "chat"` CLI arm (mirrors the `mcp` stub pattern). tui = ["dep:ratatui", "dep:crossterm"] +# Channels domain: `openhuman::channels` (external-messaging providers — Telegram, +# Discord, Slack, Signal, WhatsApp, iMessage, IRC, … — plus the channel runtime, +# controllers, host, proactive messaging and inbound dispatch) together with the +# `webview_accounts` / `webview_apis` / `webview_notifications` / `whatsapp_data` +# webview-bridge domains. Default-ON — the desktop app always ships with +# channels. Slim / headless builds opt out via `--no-default-features --features +# ""`. Composes with the runtime +# `DomainSet::Channels` flag (#4796): this feature narrows the compile-time +# surface, `DomainSet` gates it at runtime. +# +# INTENTIONALLY EMPTY — do NOT "fix" this by adding `dep:` entries. This gate +# sheds ZERO dependencies: `tinychannels` stays load-bearing regardless +# (`config/schema/channels.rs` re-exports its config types, `event_bus/events.rs` +# `DomainEvent` embeds `tinychannels::ChannelInboundEnvelope`, and +# `security/pairing.rs` re-exports its pairing helpers), so it is always +# compiled. The gate's value is compile-time surface + binary size, not the dep +# tree. (`whatsapp-web` refines this gate — see below — forwarding to the +# `tinychannels/whatsapp-web` provider feature.) +# +# NOTE: the in-app web chat (`openhuman::web_chat`, RPC namespace `channel`) is +# NOT gated by this feature even though its runtime tag is +# `DomainGroup::Channels` — it is core product surface and stays always +# compiled (decoupled from `channels/` in #5002). The `channels::{traits, cli}` +# carve-outs also stay ungated — `traits` is a 1-line tinychannels re-export and +# `cli::CliChannel` is the dep-free local REPL used by the always-on agent +# harness interactive loop. See AGENTS.md "channels gate" + `channels/mod.rs`. +channels = [] sandbox-landlock = ["dep:landlock"] sandbox-bubblewrap = [] peripheral-rpi = ["dep:rppal"] @@ -486,7 +513,9 @@ browser-native = ["dep:fantoccini"] fantoccini = ["browser-native"] landlock = ["sandbox-landlock"] # The WhatsApp Web provider now lives in tinychannels; forward to its feature. -whatsapp-web = ["tinychannels/whatsapp-web"] +# It is a refinement INSIDE the `channels` gate — enabling it pulls in the whole +# channels domain (the provider re-exports live under `channels/providers/`). +whatsapp-web = ["channels", "tinychannels/whatsapp-web"] # Exposes the destructive `openhuman.test_reset` RPC. Off by default; the E2E # build (app/scripts/e2e-build.sh) flips it on. Shipped binaries never have # this feature so the wipe RPC isn't even registered, let alone reachable. diff --git a/app/src-tauri/Cargo.toml b/app/src-tauri/Cargo.toml index 2e69451e6d..dcbc62c3ad 100644 --- a/app/src-tauri/Cargo.toml +++ b/app/src-tauri/Cargo.toml @@ -161,6 +161,7 @@ cef = { version = "=146.4.1", default-features = false } # `scripts/ci/check-feature-forwarding.mjs` fails CI when this list drifts from # the core's `[features] default` (#4919) — do not hand-maintain it from memory. openhuman_core = { path = "../..", package = "openhuman", default-features = false, features = [ + "channels", "media", "tokenjuice-treesitter", "voice", From 778fbbb60526a472f2f7ce0653f866dcfe5727c7 Mon Sep 17 00:00:00 2001 From: oxoxDev Date: Sat, 18 Jul 2026 02:44:38 +0530 Subject: [PATCH 2/9] refactor(channels): retarget mis-housed imports to tinychannels (#4801) --- src/openhuman/audio_toolkit/ops.rs | 2 +- src/openhuman/cron/bus.rs | 4 ++-- src/openhuman/memory_conversations/bus.rs | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/openhuman/audio_toolkit/ops.rs b/src/openhuman/audio_toolkit/ops.rs index fb77b99a5b..4675bb341a 100644 --- a/src/openhuman/audio_toolkit/ops.rs +++ b/src/openhuman/audio_toolkit/ops.rs @@ -4,10 +4,10 @@ use base64::{engine::general_purpose::STANDARD as BASE64, Engine}; use lettre::message::{header::ContentType, Attachment, Mailbox, MultiPart, SinglePart}; use lettre::Message; -use crate::openhuman::channels::email_channel::EmailChannel; use crate::openhuman::config::Config; use crate::openhuman::voice::{create_tts_provider, DEFAULT_PIPER_VOICE}; use crate::rpc::RpcOutcome; +use tinychannels::providers::email_channel::EmailChannel; use super::types::{ AudioEmailDeliveryResult, AudioFormat, AudioGenerateRequest, AudioGeneratedArtifact, diff --git a/src/openhuman/cron/bus.rs b/src/openhuman/cron/bus.rs index afc49bac27..73d0fbac3f 100644 --- a/src/openhuman/cron/bus.rs +++ b/src/openhuman/cron/bus.rs @@ -7,10 +7,10 @@ //! channel construction out of the scheduler. use crate::core::event_bus::{DomainEvent, EventHandler}; -use crate::openhuman::channels::{Channel, SendMessage}; use async_trait::async_trait; use std::collections::HashMap; use std::sync::Arc; +use tinychannels::{Channel, SendMessage}; /// Subscribes to `CronDeliveryRequested` events and dispatches /// the output to the named channel. @@ -98,8 +98,8 @@ impl EventHandler for CronDeliverySubscriber { #[cfg(test)] mod tests { use super::*; - use crate::openhuman::channels::traits::ChannelMessage; use std::sync::atomic::{AtomicUsize, Ordering}; + use tinychannels::ChannelMessage; use tokio::sync::mpsc; /// Minimal mock channel that tracks send() calls. diff --git a/src/openhuman/memory_conversations/bus.rs b/src/openhuman/memory_conversations/bus.rs index 6c83263942..de6122ef3e 100644 --- a/src/openhuman/memory_conversations/bus.rs +++ b/src/openhuman/memory_conversations/bus.rs @@ -10,8 +10,8 @@ use chrono::Utc; use serde_json::json; use crate::core::event_bus::{DomainEvent, EventHandler, SubscriptionHandle}; -use crate::openhuman::channels::context::conversation_history_key; -use crate::openhuman::channels::traits::ChannelMessage; +use tinychannels::context::conversation_history_key; +use tinychannels::ChannelMessage; use super::{ append_message, ensure_thread, get_messages, ConversationMessage, CreateConversationThread, From 8487e24eecba769e42afdb03a24d2bfe52c48b59 Mon Sep 17 00:00:00 2001 From: oxoxDev Date: Sat, 18 Jul 2026 02:44:48 +0530 Subject: [PATCH 3/9] feat(channels): gate the channels domain behind the channels feature (#4801) --- src/openhuman/channels/mod.rs | 78 ++++++++++++++++++++++++++++++++--- 1 file changed, 72 insertions(+), 6 deletions(-) diff --git a/src/openhuman/channels/mod.rs b/src/openhuman/channels/mod.rs index f28dc7d379..6135175cf3 100644 --- a/src/openhuman/channels/mod.rs +++ b/src/openhuman/channels/mod.rs @@ -1,67 +1,133 @@ //! Channel implementations and runtime orchestration. +//! +//! Compile-time gate (`channels`, default-ON — #4801): the whole domain lives +//! behind the feature EXCEPT two dependency-free carve-outs that always-on code +//! reaches directly: +//! +//! * `traits` — a one-line re-export of the `tinychannels` `Channel` / +//! `SendMessage` traits, named by the always-on agent-harness interactive +//! loop (`agent::harness::session::runtime::run_interactive`). +//! * `cli` — `CliChannel`, the dependency-free local stdin/stdout REPL the same +//! interactive loop drives in every build. +//! +//! Everything else (`providers`, `host`, `controllers`, `runtime`, `bus`, +//! `proactive`, `commands`, `context`, `routes`, `relay_runtime`, the provider +//! re-exports, `doctor_channels`, `start_channels`, the `build_system_prompt` +//! re-export and the `test_support` re-export) is `#[cfg(feature = "channels")]`. +//! Nothing INSIDE those submodules changes when the gate flips. +//! +//! The gate sheds ZERO dependencies — `tinychannels` stays load-bearing for the +//! config schema, the `DomainEvent` inbound envelope and security pairing — so +//! its value is compile-time surface + binary size, not the dep tree. See +//! AGENTS.md "channels gate". -pub mod bus; +// Always-compiled carve-outs (see module docs). pub mod cli; +pub mod traits; + +pub use cli::CliChannel; +pub use traits::{Channel, ChannelSendExt, SendMessage}; + +#[cfg(feature = "channels")] +pub mod bus; +#[cfg(feature = "channels")] pub mod controllers; +#[cfg(feature = "channels")] pub mod host; +#[cfg(feature = "channels")] pub mod proactive; +#[cfg(feature = "channels")] pub mod providers; +#[cfg(feature = "channels")] pub(crate) mod relay_runtime; -pub mod traits; +#[cfg(feature = "channels")] mod commands; +#[cfg(feature = "channels")] pub(crate) mod context; +#[cfg(feature = "channels")] mod routes; +#[cfg(feature = "channels")] mod runtime; -#[cfg(test)] +#[cfg(all(feature = "channels", test))] mod tests; // Stable `channels::` paths (implementation lives under `providers/`). +#[cfg(feature = "channels")] pub use providers::dingtalk; +#[cfg(feature = "channels")] pub use providers::discord; +#[cfg(feature = "channels")] pub use providers::email_channel; +#[cfg(feature = "channels")] pub use providers::imessage; +#[cfg(feature = "channels")] pub use providers::irc; +#[cfg(feature = "channels")] pub use providers::lark; +#[cfg(feature = "channels")] pub use providers::linq; +#[cfg(feature = "channels")] pub use providers::mattermost; +#[cfg(feature = "channels")] pub use providers::qq; +#[cfg(feature = "channels")] pub use providers::signal; +#[cfg(feature = "channels")] pub use providers::slack; +#[cfg(feature = "channels")] pub use providers::telegram; +#[cfg(feature = "channels")] pub use providers::whatsapp; #[cfg(feature = "whatsapp-web")] pub use providers::whatsapp_web; +#[cfg(feature = "channels")] pub use providers::yuanbao; -pub use cli::CliChannel; +#[cfg(feature = "channels")] pub use dingtalk::DingTalkChannel; +#[cfg(feature = "channels")] pub use discord::DiscordChannel; +#[cfg(feature = "channels")] pub use email_channel::EmailChannel; +#[cfg(feature = "channels")] pub use imessage::IMessageChannel; +#[cfg(feature = "channels")] pub use irc::IrcChannel; +#[cfg(feature = "channels")] pub use lark::LarkChannel; +#[cfg(feature = "channels")] pub use linq::LinqChannel; +#[cfg(feature = "channels")] pub use mattermost::MattermostChannel; +#[cfg(feature = "channels")] pub use qq::QQChannel; +#[cfg(feature = "channels")] pub use signal::SignalChannel; +#[cfg(feature = "channels")] pub use slack::SlackChannel; +#[cfg(feature = "channels")] pub use telegram::TelegramChannel; -pub use traits::{Channel, ChannelSendExt, SendMessage}; +#[cfg(feature = "channels")] pub use whatsapp::WhatsAppChannel; #[cfg(feature = "whatsapp-web")] pub use whatsapp_web::WhatsAppWebChannel; +#[cfg(feature = "channels")] pub use yuanbao::YuanbaoChannel; -#[cfg(any(test, debug_assertions))] +#[cfg(all(feature = "channels", any(test, debug_assertions)))] pub use runtime::test_support; +#[cfg(feature = "channels")] pub use commands::doctor_channels; +#[cfg(feature = "channels")] pub use controllers::{ChannelAuthMode, ChannelDefinition}; // Channel system-prompt assembly lives in // `crate::openhuman::context::channels_prompt` alongside the rest of // the prompt-building code. Re-exported here for callers that used the // old `channels::build_system_prompt` path. +#[cfg(feature = "channels")] pub use crate::openhuman::context::channels_prompt::build_system_prompt; +#[cfg(feature = "channels")] pub use runtime::start_channels; From 617388de3444de70a281b6413b68a2b3caa0a8bb Mon Sep 17 00:00:00 2001 From: oxoxDev Date: Sat, 18 Jul 2026 02:44:48 +0530 Subject: [PATCH 4/9] feat(core): leaf-gate webview_* + whatsapp_data behind channels (#4801) --- src/openhuman/mod.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/openhuman/mod.rs b/src/openhuman/mod.rs index dc26574017..885be25902 100644 --- a/src/openhuman/mod.rs +++ b/src/openhuman/mod.rs @@ -150,9 +150,13 @@ pub mod wallet; pub mod web3; pub mod web_chat; pub mod webhooks; +#[cfg(feature = "channels")] pub mod webview_accounts; +#[cfg(feature = "channels")] pub mod webview_apis; +#[cfg(feature = "channels")] pub mod webview_notifications; +#[cfg(feature = "channels")] pub mod whatsapp_data; pub mod workspace; pub mod x402; From 59dd1c809a4987bf67198fb17069fee2713a30e4 Mon Sep 17 00:00:00 2001 From: oxoxDev Date: Sat, 18 Jul 2026 02:44:56 +0530 Subject: [PATCH 5/9] feat(core): gate channel/webview/whatsapp_data controller registration (#4801) --- src/core/all.rs | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/src/core/all.rs b/src/core/all.rs index b5821c13da..acdb6fb1a7 100644 --- a/src/core/all.rs +++ b/src/core/all.rs @@ -258,6 +258,8 @@ fn build_registered_controllers() -> Vec { ); // Webview APIs bridge — proxies connector calls (Gmail, …) through // a WebSocket to the Tauri shell so curl reaches the live webview. + // Gated behind the `channels` feature. + #[cfg(feature = "channels")] push( &mut controllers, DomainGroup::Channels, @@ -384,6 +386,11 @@ fn build_registered_controllers() -> Vec { DomainGroup::Channels, crate::openhuman::web_chat::all_web_channel_registered_controllers(), ); + // External messaging channels (Telegram, Discord, Slack, …). + // Gated behind the `channels` feature. NOTE: the web_chat push above stays + // ungated — the in-app chat is core product surface (decoupled in #5002), + // even though its runtime tag is DomainGroup::Channels. + #[cfg(feature = "channels")] push( &mut controllers, DomainGroup::Channels, @@ -702,7 +709,8 @@ fn build_registered_controllers() -> Vec { DomainGroup::Threads, crate::openhuman::todos::all_todos_registered_controllers(), ); - // Embedded webview native notifications + // Embedded webview native notifications. Gated behind the `channels` feature. + #[cfg(feature = "channels")] push( &mut controllers, DomainGroup::Channels, @@ -748,6 +756,8 @@ fn build_registered_controllers() -> Vec { // The write-path ingest controller is registered separately in build_internal_only_controllers. // Classified Channels (WhatsApp Web messaging surface) — not enumerated in the // spec Platform list; grouped with the other channel/webview domains. + // Gated behind the `channels` feature. + #[cfg(feature = "channels")] push( &mut controllers, DomainGroup::Channels, @@ -812,6 +822,8 @@ fn build_internal_only_controllers() -> Vec { let mut controllers = Vec::new(); // whatsapp_data ingest: scanner-side write path. Callable over RPC by the // Tauri scanner but excluded from agent-facing schema discovery. + // Gated behind the `channels` feature. + #[cfg(feature = "channels")] push( &mut controllers, DomainGroup::Channels, From 311edd8d7f8136a19241c70a6143d13179180a49 Mon Sep 17 00:00:00 2001 From: oxoxDev Date: Sat, 18 Jul 2026 02:44:56 +0530 Subject: [PATCH 6/9] feat(core): gate channels runtime wiring behind the feature (#4801) --- src/core/jsonrpc.rs | 10 ++++++++++ src/core/runtime/context.rs | 8 ++++++++ src/core/runtime/services.rs | 6 ++++++ 3 files changed, 24 insertions(+) diff --git a/src/core/jsonrpc.rs b/src/core/jsonrpc.rs index 2b87bdcb2d..82fe7ea2af 100644 --- a/src/core/jsonrpc.rs +++ b/src/core/jsonrpc.rs @@ -2007,6 +2007,12 @@ fn register_domain_subscribers( } // Channels: inbound dispatch + web-only proactive messaging. + // The `plan.channels` runtime guard cannot stand in for the compile-time + // gate: the `channels::bus::ChannelInboundSubscriber` + + // `channels::proactive` type paths below must still resolve for this to + // compile, so the whole block is `#[cfg]`-gated too (mirrors the flows + // dual-gate below). + #[cfg(feature = "channels")] if plan.channels { if group_first_time(DomainGroup::Channels) { if let Some(handle) = crate::core::event_bus::subscribe_global(Arc::new( @@ -2027,6 +2033,10 @@ fn register_domain_subscribers( "[event_bus] Channels subscribers (inbound + web-only proactive) SKIPPED — Channels domain disabled" ); } + #[cfg(not(feature = "channels"))] + log::debug!( + "[event_bus] Channels subscribers (inbound + web-only proactive) SKIPPED — channels feature disabled at compile time" + ); // Flows trigger dispatch (issue B2): maps FlowScheduleTick / // ComposioTriggerReceived / WebhookIncomingRequest onto enabled flows and diff --git a/src/core/runtime/context.rs b/src/core/runtime/context.rs index 8bf9357f21..99b9e97769 100644 --- a/src/core/runtime/context.rs +++ b/src/core/runtime/context.rs @@ -376,6 +376,10 @@ pub async fn init_stores( } // Initialize the WhatsApp data store so scanner ingest calls // can write data without requiring a lazy-init fallback. + // Compile-time `channels` gate: the body names `whatsapp_data::global`, + // which is compiled out with the feature off, so the block is `#[cfg]`-gated. + // The `plan.whatsapp_data` field + its runtime tests stay. + #[cfg(feature = "channels")] if plan.whatsapp_data { match crate::openhuman::whatsapp_data::global::init(cfg.workspace_dir.clone()) { Ok(_) => log::info!( @@ -387,6 +391,10 @@ pub async fn init_stores( } else { log::debug!("[boot] whatsapp_data::global init SKIPPED — Channels domain disabled"); } + #[cfg(not(feature = "channels"))] + log::debug!( + "[boot] whatsapp_data::global init SKIPPED — channels feature disabled at compile time" + ); // Seed the people store so people controllers + `people_*` // tools can read/write. Without this the process-global stays // empty and every call fails with "people store not diff --git a/src/core/runtime/services.rs b/src/core/runtime/services.rs index 3b814b1d37..272dec4a2c 100644 --- a/src/core/runtime/services.rs +++ b/src/core/runtime/services.rs @@ -154,6 +154,10 @@ pub fn spawn_cron_service() { /// `OPENHUMAN_DISABLE_CHANNEL_LISTENERS` is set to `1`/`true`, and returns early /// when no channel integrations are configured. pub fn spawn_channels_service() { + // Compile-time `channels` gate: the body names `channels::start_channels`, + // so the whole thing is `#[cfg]`-gated. With the feature off there are no + // realtime listeners to spawn. + #[cfg(feature = "channels")] if std::env::var("OPENHUMAN_DISABLE_CHANNEL_LISTENERS") .ok() .filter(|s| s == "1" || s.eq_ignore_ascii_case("true")) @@ -181,6 +185,8 @@ pub fn spawn_channels_service() { } else { log::info!("[channels] OPENHUMAN_DISABLE_CHANNEL_LISTENERS set — skipping start_channels"); } + #[cfg(not(feature = "channels"))] + log::debug!("[channels] channels feature disabled at compile time — not spawning listeners"); } /// Starts legacy bootstrap loops that predate [`ServiceSet`]. From e4d0353c5b7f5c672fba303b8105bda73b3a54c3 Mon Sep 17 00:00:00 2001 From: oxoxDev Date: Sat, 18 Jul 2026 02:44:56 +0530 Subject: [PATCH 7/9] feat(core): gate whatsapp_data agent tools behind channels (#4801) --- src/openhuman/tools/mod.rs | 1 + src/openhuman/tools/ops.rs | 5 +++++ 2 files changed, 6 insertions(+) diff --git a/src/openhuman/tools/mod.rs b/src/openhuman/tools/mod.rs index 9326370ef5..6a3e977327 100644 --- a/src/openhuman/tools/mod.rs +++ b/src/openhuman/tools/mod.rs @@ -64,6 +64,7 @@ pub use crate::openhuman::tinyplace::tools::*; pub use crate::openhuman::todos::tools::*; #[cfg(feature = "web3")] pub use crate::openhuman::wallet::tools::*; +#[cfg(feature = "channels")] pub use crate::openhuman::whatsapp_data::tools::*; pub use crate::openhuman::workspace::tools::*; pub use implementations::*; diff --git a/src/openhuman/tools/ops.rs b/src/openhuman/tools/ops.rs index a92b314c80..5bb0b4a351 100644 --- a/src/openhuman/tools/ops.rs +++ b/src/openhuman/tools/ops.rs @@ -458,8 +458,13 @@ pub fn all_tools_with_runtime( // The matching `whatsapp_data_ingest` write-path stays internal-only // (registered in `src/core/all.rs::build_internal_only_controllers`) // and is intentionally NOT wrapped here. + // Gated behind the `channels` feature (the tool types live in the + // gated `whatsapp_data` domain). + #[cfg(feature = "channels")] Box::new(WhatsAppDataListChatsTool), + #[cfg(feature = "channels")] Box::new(WhatsAppDataListMessagesTool), + #[cfg(feature = "channels")] Box::new(WhatsAppDataSearchMessagesTool), Box::new(ScheduleTool::new(security.clone(), root_config.clone())), Box::new(ProxyConfigTool::new(config.clone(), security.clone())), From 9ceaee9ff1ea0202bc50f6e2af28763f13b8a7db Mon Sep 17 00:00:00 2001 From: oxoxDev Date: Sat, 18 Jul 2026 02:45:08 +0530 Subject: [PATCH 8/9] test(core): pin channels gate both-ways + ignore gated methods in alias drift (#4801) --- src/core/all_tests.rs | 60 +++++++++++++++++++++++ src/core/legacy_aliases.rs | 29 +++++++++-- src/openhuman/tools/ops_tests.rs | 82 ++++++++++++++++++++++++++++++++ 3 files changed, 168 insertions(+), 3 deletions(-) diff --git a/src/core/all_tests.rs b/src/core/all_tests.rs index a562c711e5..15f46fa6bb 100644 --- a/src/core/all_tests.rs +++ b/src/core/all_tests.rs @@ -827,6 +827,8 @@ async fn harness_excludes_gated_namespaces() { // convention; gate it like its siblings so the disabled build passes (#5022). #[cfg(feature = "voice")] assert!(full_ns.contains("voice"), "full() must expose voice"); + #[cfg(feature = "channels")] + assert!(full_ns.contains("channels"), "full() must expose channels"); let ctx = CoreContext::for_test(DomainSet::harness(), None); let harness_ns: BTreeSet<&'static str> = @@ -850,6 +852,7 @@ async fn harness_excludes_gated_namespaces() { "skills", "wallet", "meet", + "channels", "mcp_clients", "health", ] { @@ -1095,3 +1098,60 @@ fn meet_controllers_absent_when_feature_off() { ); } } + +/// The channel + webview-bridge namespaces register when the `channels` feature +/// is on (#4801). +/// +/// Paired with `channels_controllers_absent_when_feature_off` below to pin both +/// directions of the compile-time gate. `webview_notifications` has no +/// controllers (v1 toggle lives shell-side), so it is not asserted here. +#[cfg(feature = "channels")] +#[test] +fn channels_controllers_registered_when_feature_on() { + let namespaces: Vec<&str> = all_controller_schemas() + .iter() + .map(|s| s.namespace) + .collect(); + for ns in ["channels", "webview_apis", "whatsapp_data"] { + assert!( + namespaces.contains(&ns), + "with the `channels` feature ON the `{ns}` controllers must be registered" + ); + } +} + +/// With `channels` compiled out the channel + webview-bridge domains leave zero +/// trace in the registry (#4801) — while the in-app web chat (`channel` +/// namespace) stays present, pinning the #5002 decoupling: turning off external +/// messaging must NOT take down core in-app chat. +/// +/// This is the half that proves the gate does something. The 3 `whatsapp_data` +/// agent tools are pinned separately in `tools::ops_tests` (that module has the +/// full-tool-list machinery); here we assert the controller surface. +#[cfg(not(feature = "channels"))] +#[test] +fn channels_controllers_absent_when_feature_off() { + let namespaces: Vec<&str> = all_controller_schemas() + .iter() + .map(|s| s.namespace) + .collect(); + for ns in [ + "channels", + "webview_apis", + "webview_notifications", + "whatsapp_data", + ] { + assert!( + !namespaces.contains(&ns), + "with the `channels` feature OFF the `{ns}` controllers must be absent \ + (unknown-method over /rpc, omitted from /schema)" + ); + } + // #5002 decoupling: the in-app web chat controllers (RPC namespace `channel`) + // are core product surface and must survive the `channels` gate being off. + assert!( + namespaces.contains(&"channel"), + "the in-app web_chat controllers (`channel` namespace) must stay registered \ + even with the `channels` feature OFF (#5002 decoupling)" + ); +} diff --git a/src/core/legacy_aliases.rs b/src/core/legacy_aliases.rs index f08bf5850d..a558aaec45 100644 --- a/src/core/legacy_aliases.rs +++ b/src/core/legacy_aliases.rs @@ -363,19 +363,42 @@ mod tests { /// failure by deleting live frontend methods from the catalog). /// /// Mirrors how the agent loader tolerates the orchestrator TOML's dangling - /// `mcp_agent` subagent id (#4799). + /// `mcp_agent` subagent id (#4799). Composed from one predicate per gate so + /// each new gate adds a self-contained pair (keeps the attribute-`#[cfg]` + /// form the feature-gate smoke lane's coverage guard tracks). + fn is_compiled_out_method(method: &str) -> bool { + mcp_method_compiled_out(method) || channels_method_compiled_out(method) + } + #[cfg(feature = "mcp")] - fn is_compiled_out_method(_method: &str) -> bool { + fn mcp_method_compiled_out(_method: &str) -> bool { false } #[cfg(not(feature = "mcp"))] - fn is_compiled_out_method(method: &str) -> bool { + fn mcp_method_compiled_out(method: &str) -> bool { // `mcp` feature OFF ⇒ the `mcp_clients` (dynamic registry) and // `mcp_audit` (write log) controllers are unregistered. method.starts_with("openhuman.mcp_clients_") || method.starts_with("openhuman.mcp_audit_") } + #[cfg(feature = "channels")] + fn channels_method_compiled_out(_method: &str) -> bool { + false + } + + #[cfg(not(feature = "channels"))] + fn channels_method_compiled_out(method: &str) -> bool { + // `channels` feature OFF ⇒ the channels + webview_apis + + // webview_notifications + whatsapp_data controllers are unregistered + // (#4801). NOTE: the in-app web chat (`openhuman.channel_*`) is NOT + // gated (core product surface, #5002) — do not add that prefix here. + method.starts_with("openhuman.channels_") + || method.starts_with("openhuman.webview_apis_") + || method.starts_with("openhuman.webview_notifications_") + || method.starts_with("openhuman.whatsapp_data_") + } + #[test] fn quoted_value_extracts_single_quoted_string() { assert_eq!(quoted_value(": 'hello'"), "hello"); diff --git a/src/openhuman/tools/ops_tests.rs b/src/openhuman/tools/ops_tests.rs index bd1b0d7ae2..48b31b7ff1 100644 --- a/src/openhuman/tools/ops_tests.rs +++ b/src/openhuman/tools/ops_tests.rs @@ -149,6 +149,88 @@ fn all_tools_includes_spawn_subagent() { ); } +/// The three read-only WhatsApp-data agent tools are registered when the +/// `channels` feature is on (#4801). Paired with the absent-variant below to +/// pin both directions of the compile-time gate. +#[cfg(feature = "channels")] +#[test] +fn whatsapp_data_tools_present_when_channels_on() { + let tmp = TempDir::new().unwrap(); + let security = Arc::new(SecurityPolicy::default()); + let mem = test_memory(&tmp); + let browser = BrowserConfig { + enabled: false, + allowed_domains: vec![], + session_name: None, + ..BrowserConfig::default() + }; + let http = crate::openhuman::config::HttpRequestConfig::default(); + let cfg = test_config(&tmp); + let tools = all_tools( + Arc::new(Config::default()), + &security, + AuditLogger::disabled(), + mem, + &browser, + &http, + tmp.path(), + &HashMap::new(), + &cfg, + ); + let names = tool_names(&tools); + for expected in [ + "whatsapp_data_list_chats", + "whatsapp_data_list_messages", + "whatsapp_data_search_messages", + ] { + assert!( + names.iter().any(|n| n == expected), + "`{expected}` must be registered when the `channels` feature is on; got: {names:?}" + ); + } +} + +/// With `channels` compiled out the three WhatsApp-data agent tools are absent +/// from the registry (not degraded to an error) — the tool types live in the +/// gated `whatsapp_data` domain (#4801). +#[cfg(not(feature = "channels"))] +#[test] +fn whatsapp_data_tools_absent_when_channels_off() { + let tmp = TempDir::new().unwrap(); + let security = Arc::new(SecurityPolicy::default()); + let mem = test_memory(&tmp); + let browser = BrowserConfig { + enabled: false, + allowed_domains: vec![], + session_name: None, + ..BrowserConfig::default() + }; + let http = crate::openhuman::config::HttpRequestConfig::default(); + let cfg = test_config(&tmp); + let tools = all_tools( + Arc::new(Config::default()), + &security, + AuditLogger::disabled(), + mem, + &browser, + &http, + tmp.path(), + &HashMap::new(), + &cfg, + ); + let names = tool_names(&tools); + for absent in [ + "whatsapp_data_list_chats", + "whatsapp_data_list_messages", + "whatsapp_data_search_messages", + ] { + assert!( + !names.iter().any(|n| n == absent), + "`{absent}` must be absent when the `channels` feature is off; got: {names:?}" + ); + } +} + #[test] fn all_tools_includes_spawn_async_subagent() { let tmp = TempDir::new().unwrap(); From 51f0f499b5ae4adab5bf4c315d8275bfd909b005 Mon Sep 17 00:00:00 2001 From: oxoxDev Date: Sat, 18 Jul 2026 02:45:08 +0530 Subject: [PATCH 9/9] ci(feature-gate-smoke): cover the channels gate + document it (#4801) --- .github/workflows/ci-lite.yml | 3 ++- AGENTS.md | 13 +++++++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci-lite.yml b/.github/workflows/ci-lite.yml index 4ae6ae2029..788403e662 100644 --- a/.github/workflows/ci-lite.yml +++ b/.github/workflows/ci-lite.yml @@ -446,6 +446,7 @@ jobs: core/cli_tests.rs core/jsonrpc_tests.rs core/legacy_aliases.rs + core/runtime/context.rs openhuman/agent/harness/builtin_definitions.rs openhuman/agent/harness/definition_tests.rs openhuman/agent/harness/session/tests.rs @@ -462,7 +463,7 @@ jobs: openhuman/x402/stub.rs EOF ) - ACTUAL=$(grep -rlE '#\[cfg\((not\()?feature = "(voice|media|web3|meet|mcp|skills|flows)"' src --include='*.rs' \ + ACTUAL=$(grep -rlE '#\[cfg\((not\()?feature = "(voice|media|web3|meet|mcp|skills|flows|channels)"' src --include='*.rs' \ | xargs grep -lE '#\[test\]|#\[tokio::test\]|fn .*_test' 2>/dev/null | sed 's|^src/||' | sort -u) if ! diff <(echo "$EXPECTED" | sed 's/^ *//' | sort -u) <(echo "$ACTUAL"); then echo "::error::Gated-test file set changed. Update the EXPECTED allowlist in the rust-feature-gate-smoke lane, and extend the scoped 'cargo test' filter if the new module can carry an ungated-assert regression (see #5022)." diff --git a/AGENTS.md b/AGENTS.md index a9e637719a..b4625e091d 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -244,6 +244,7 @@ GGML_NATIVE=OFF cargo check --manifest-path Cargo.toml \ | `flows` | ON | `openhuman::flows` (saved automation graphs — create/run/schedule, the `workflow_builder` + `flow_discovery` agents), `openhuman::tinyflows` (engine seam), `openhuman::rhai_workflows` (`.ragsh` language-workflow tool) | `tinyflows`, `jaq-core`, `jaq-std`, `jaq-json`, `rhai` | | `mcp` | ON | `openhuman::mcp_server` (the `openhuman mcp` stdio/HTTP server), `openhuman::mcp_registry` (dynamic Smithery installs — `mcp_clients` RPC namespace, SQLite, boot spawn, supervisor, OAuth), `openhuman::mcp_audit` (write-audit log), and the static config-declared server set in `openhuman::mcp_client`. ~19 agent tools, ~20k LOC | **none** (see scope note) | | `tui` | ON | `openhuman::tui` — the `openhuman tui` (alias `chat`) CLI subcommand: a ratatui/crossterm terminal chat UI onto the `web_chat` surface, running the core in-process. No controllers, no agent tools. **Intentionally NOT forwarded to the desktop shell** (allowlisted in `check-feature-forwarding.mjs`). | `ratatui`, `crossterm` | +| `channels` | ON | `openhuman::channels` (external-messaging providers — Telegram/Discord/Slack/Signal/WhatsApp/iMessage/IRC/… — plus the channel runtime, controllers, host, proactive messaging + inbound dispatch) and the `webview_accounts` / `webview_apis` / `webview_notifications` / `whatsapp_data` webview-bridge domains (incl. the 3 `whatsapp_data_*` agent tools). **Carve-outs `channels::{traits, cli}` stay ungated.** | **none** (`tinychannels` is load-bearing) | **Facade pattern (pathfinder for the other gates).** `pub mod voice;` is **always compiled** as a facade: the real submodules are `#[cfg(feature = "voice")]`, and a `#[cfg(not(feature = "voice"))] mod stub;` (`src/openhuman/voice/stub.rs`) re-exposes the same public surface that always-on / other-gated callers use (`server`, `dictation_listener`, `streaming`, `reply_speech`, `cloud_transcribe`, `cli`, `create_stt_provider`, `effective_stt_provider`, `publish_ptt_transcript_committed`) with no-op / `None` / disabled-error bodies. Callers therefore do **not** need per-call `#[cfg]`. When voice is off: the voice/audio controllers are unregistered (unknown-method over `/rpc`, absent from `/schema`), the `audio_generate_podcast` agent tools are absent, and `openhuman voice` returns a "voice disabled" error. Stub signatures must match the real ones exactly — the disabled build (`--no-default-features --features tokenjuice-treesitter`) is the **only** thing that catches drift, so run it before pushing any change to the voice surface. @@ -317,6 +318,18 @@ The terminal chat UI (`openhuman tui` / alias `chat`) lives in `src/openhuman/tu - **Intentionally NOT forwarded to the desktop shell** (the app ships its own Tauri UI). It carries the only current entry in `INTENTIONALLY_NOT_FORWARDED` in `scripts/ci/check-feature-forwarding.mjs`; the pure reducer lives in `src/openhuman/tui/state.rs` (`TranscriptState::apply_event`) with unit tests, so most behaviour is testable without a terminal. Drops the exclusive `ratatui` + `crossterm` deps when off. Verify with `cargo tree -i ratatui --no-default-features --features tokenjuice-treesitter` (must return nothing). +#### The `channels` gate (#4801 — last child of #4795) + +Leaf-gate pattern with **two ungated carve-outs and no stub file** — the reach-map put every gated symbol at a *registration/leaf* call site, so absence (unknown-method / omitted tool), not a disabled-error stub, is the correct off-state (same rationale as `flows` / `meet`). + +- **Sheds ZERO dependencies — do NOT re-litigate.** `tinychannels` stays always-compiled regardless of the gate: `config/schema/channels.rs` re-exports its config types, `event_bus/events.rs`'s `DomainEvent` embeds `tinychannels::ChannelInboundEnvelope`, and `security/pairing.rs` re-exports its pairing helpers. The `channels = []` feature list is intentionally empty. The gate's value is compile-time surface + binary size. (`whatsapp-web` is a **refinement inside** the gate — `whatsapp-web = ["channels", "tinychannels/whatsapp-web"]`.) +- **Two ungated carve-outs.** `pub mod traits;` (a one-line `tinychannels` `Channel`/`SendMessage` re-export) and `pub mod cli;` (`CliChannel`, a dependency-free local stdin/stdout REPL) stay compiled in **all** builds — both are reached by the always-on agent-harness interactive loop (`agent::harness::session::runtime::run_interactive`). Same shape as the `meet_agent::wav` carve-out. `channels::mod.rs` `#[cfg(feature = "channels")]`s everything else; nothing inside the gated submodules changes. +- **The in-app web chat is NOT gated.** `openhuman::web_chat` (RPC namespace `channel`, decoupled from `channels/` in #5002 + #5003 which also moved `learning` out) is core product surface and stays always-compiled even though its runtime tag is `DomainGroup::Channels`. Its registration push in `src/core/all.rs` is deliberately left ungated; the both-ways test pins `channel` present with the feature OFF. +- **Three mis-housed imports were retargeted to `tinychannels` (no stub needed).** `cron/bus.rs` (`Channel`/`SendMessage`/`ChannelMessage`), `memory_conversations/bus.rs` (`ChannelMessage` + `context::conversation_history_key`), and `audio_toolkit/ops.rs` (`providers::email_channel::EmailChannel`) reached the gated domain only to pick up symbols that actually live in `tinychannels`; pointing them straight at the crate removes the always-on → gated edge (and the voice→channels cross-gate edge). The old `channels::` paths were 1-line delegations / `pub use` re-exports of exactly these. +- **Leaf-gated call sites** (each carries its own `#[cfg]`): the 5 controller-registration pushes in `src/core/all.rs` (channels controllers, `webview_apis`, `webview_notifications`, public + internal `whatsapp_data`), the `ChannelInboundSubscriber` + web-only-proactive block in `src/core/jsonrpc.rs`, `spawn_channels_service` in `src/core/runtime/services.rs`, the `whatsapp_data::global::init` block in `src/core/runtime/context.rs`, and the `whatsapp_data::tools::*` glob + 3 `WhatsAppData*Tool` registrations in `src/openhuman/tools/{mod,ops}.rs`. The `webview_accounts` / `webview_apis` / `webview_notifications` / `whatsapp_data` `pub mod` declarations in `src/openhuman/mod.rs` are leaf-gated too. String-match arms (`"channels" =>` descriptions, `whatsapp_data_` in `group_for_namespace`) stay **ungated** — they are data. +- **`start_bootstrap_jobs`' `services.channels` block keeps running slim** — it drives composio sync / workspace-memory sync / orchestration drain and names **no** `channels::` symbol, so it stays ungated by design. +- **No CLI change.** There is no `openhuman channels` subcommand; generic namespace resolution yields "unknown namespace" when off (the `flows` precedent — acceptable). +- **Both-ways tests.** `channels_controllers_{registered_when_feature_on,absent_when_feature_off}` in `src/core/all_tests.rs` pin the controller surface (the OFF half also asserts `channel`/web_chat survives), and `whatsapp_data_tools_{present_when_channels_on,absent_when_channels_off}` in `src/openhuman/tools/ops_tests.rs` pin the 3 agent tools (that module has the full-tool-list machinery). CI's smoke lane runs `cargo check` only, so run `cargo test --lib --no-default-features --features tokenjuice-treesitter core::all::tests` locally after touching any gated surface. ### Event bus (`src/core/event_bus/`)