feat(core): channels compile-time feature gate (#4801) — completes epic #4795 (#5029)

Co-authored-by: Steven Enamakel <enamakel@tinyhumans.ai>
This commit is contained in:
oxoxDev
2026-07-22 09:32:16 +03:00
committed by GitHub
co-authored by Steven Enamakel
parent 5fcde25320
commit 0eeee12753
19 changed files with 362 additions and 40 deletions
+2 -1
View File
@@ -447,6 +447,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
@@ -463,7 +464,7 @@ jobs:
openhuman/x402/stub.rs
EOF
)
ACTUAL=$(grep -rlE '#\[cfg\((not\()?feature = "(voice|media|web3|meet|mcp|skills|flows|desktop-automation)"' src --include='*.rs' \
ACTUAL=$(grep -rlE '#\[cfg\((not\()?feature = "(voice|media|web3|meet|mcp|skills|flows|channels|desktop-automation)"' 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)."
+13
View File
@@ -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/`)
+31 -2
View File
@@ -385,7 +385,7 @@ tokio = { version = "1", features = ["test-util"] }
proptest = "1"
[features]
default = ["tokenjuice-treesitter", "inference", "voice", "web3", "media", "documents", "meet", "skills", "flows", "mcp", "crash-reporting", "http-server", "desktop-automation", "tui"]
default = ["tokenjuice-treesitter", "inference", "voice", "web3", "media", "documents", "meet", "skills", "flows", "mcp", "crash-reporting", "http-server", "desktop-automation", "channels", "tui"]
# HTTP + Socket.IO server transport (#5048): the `/rpc` JSON-RPC endpoint and
# its auth middleware/CORS layer (`core::jsonrpc`, `core::auth`), the `/v1`
# OpenAI-compatible router (`inference::http`), the ad-hoc static-dir file
@@ -596,6 +596,33 @@ desktop-automation = ["dep:uiautomation"]
# 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", "dep:unicode-width"]
# 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
# "<explicit list without channels>"`. 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"]
@@ -603,7 +630,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.
-1
View File
@@ -5615,7 +5615,6 @@ dependencies = [
"tokio-tungstenite 0.24.0",
"tokio-util",
"toml 1.1.2+spec-1.1.0",
"tower",
"tracing",
"tracing-appender",
"tracing-log",
+1
View File
@@ -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",
"inference",
+13 -1
View File
@@ -259,6 +259,8 @@ fn build_registered_controllers() -> Vec<GroupedController> {
);
// 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,
@@ -388,6 +390,11 @@ fn build_registered_controllers() -> Vec<GroupedController> {
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,
@@ -700,7 +707,8 @@ fn build_registered_controllers() -> Vec<GroupedController> {
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,
@@ -746,6 +754,8 @@ fn build_registered_controllers() -> Vec<GroupedController> {
// 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,
@@ -810,6 +820,8 @@ fn build_internal_only_controllers() -> Vec<GroupedController> {
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,
+60
View File
@@ -858,6 +858,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> =
@@ -881,6 +883,7 @@ async fn harness_excludes_gated_namespaces() {
"skills",
"wallet",
"meet",
"channels",
"mcp_clients",
"health",
] {
@@ -1127,6 +1130,63 @@ 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)"
);
}
/// With the `http-server` feature on (the default), the HTTP + Socket.IO
/// transport is compiled in — `HTTP_SERVER_COMPILED_IN` reflects that, and
/// `socketioxide` is linked. `socketioxide` is the only dependency this gate
+10
View File
@@ -2092,6 +2092,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(
@@ -2112,6 +2118,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
+45 -20
View File
@@ -363,31 +363,56 @@ 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 {
// Each compile-time gate contributes the RPC namespaces whose controllers
// it unregisters. The frontend catalog is data (it always names the full
// shipped surface), so a slim build must ignore exactly those methods and
// keep asserting on everything else.
#[cfg(not(feature = "mcp"))]
// `mcp` OFF ⇒ the `mcp_clients` (dynamic registry) + `mcp_audit` (write
// log) controllers are unregistered.
if method.starts_with("openhuman.mcp_clients_")
|| method.starts_with("openhuman.mcp_audit_")
{
return true;
}
#[cfg(not(feature = "desktop-automation"))]
mcp_method_compiled_out(method)
|| channels_method_compiled_out(method)
|| desktop_automation_method_compiled_out(method)
}
#[cfg(feature = "mcp")]
fn mcp_method_compiled_out(_method: &str) -> bool {
false
}
#[cfg(not(feature = "mcp"))]
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_")
}
#[cfg(feature = "desktop-automation")]
fn desktop_automation_method_compiled_out(_method: &str) -> bool {
false
}
#[cfg(not(feature = "desktop-automation"))]
fn desktop_automation_method_compiled_out(method: &str) -> bool {
// `desktop-automation` OFF ⇒ the autocomplete / screen_intelligence /
// desktop-companion controllers are unregistered (#5049).
if method.starts_with("openhuman.autocomplete_")
method.starts_with("openhuman.autocomplete_")
|| method.starts_with("openhuman.screen_intelligence_")
|| method.starts_with("openhuman.companion_")
{
return true;
}
let _ = method;
false
}
#[test]
+8
View File
@@ -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
+6
View File
@@ -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`].
+1 -1
View File
@@ -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,
+76 -10
View File
@@ -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 controllers;
pub mod host;
pub mod proactive;
pub mod providers;
pub(crate) mod relay_runtime;
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;
#[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::<provider>` 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;
+2 -2
View File
@@ -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.
+2 -2
View File
@@ -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,
+4
View File
@@ -156,9 +156,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;
+1
View File
@@ -65,6 +65,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::*;
+5
View File
@@ -463,8 +463,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())),
+82
View File
@@ -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();