Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .github/workflows/ci-lite.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)."
Expand Down
14 changes: 14 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,7 @@ GGML_NATIVE=OFF cargo check --manifest-path Cargo.toml \
| `skills` | ON | `openhuman::skills` + `openhuman::skill_runtime` + `openhuman::skill_registry` domains β€” SKILL.md discovery/parse/install, workflow execution + run logs, remote catalogs, the `skill_setup` / `skill_executor` builtin agents, and the 16 skill agent tools | none (see below) |
| `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) |
| `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.

Expand Down Expand Up @@ -306,6 +307,19 @@ Follows the voice facade+stub pattern for `mcp_server` / `mcp_registry` / `mcp_a

`src/core/all.rs` needs **no** `#[cfg]` for this gate: the stub aggregators return empty vecs, so the registration sites keep compiling unchanged.

#### 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/`)

Typed pub/sub + native request/response. Both singletons β€” use module-level functions.
Expand Down
33 changes: 31 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -347,7 +347,7 @@ tokio = { version = "1", features = ["test-util"] }
proptest = "1"

[features]
default = ["tokenjuice-treesitter", "voice", "web3", "media", "meet", "skills", "flows", "mcp"]
default = ["tokenjuice-treesitter", "voice", "web3", "media", "meet", "skills", "flows", "mcp", "channels"]
# 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 = [
Expand Down Expand Up @@ -458,14 +458,43 @@ skills = []
# `sanitize::sanitize_for_llm`). The gate follows the real dependency graph,
# not the directory name.
mcp = []
# 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"]
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.
Expand Down
1 change: 1 addition & 0 deletions app/src-tauri/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
14 changes: 13 additions & 1 deletion src/core/all.rs
Original file line number Diff line number Diff line change
Expand Up @@ -258,6 +258,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,
Expand Down Expand Up @@ -390,6 +392,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,
Expand Down Expand Up @@ -708,7 +715,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,
Expand Down Expand Up @@ -754,6 +762,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,
Expand Down Expand Up @@ -818,6 +828,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,
Expand Down
60 changes: 60 additions & 0 deletions src/core/all_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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> =
Expand All @@ -850,6 +852,7 @@ async fn harness_excludes_gated_namespaces() {
"skills",
"wallet",
"meet",
"channels",
"mcp_clients",
"health",
] {
Expand Down Expand Up @@ -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)"
);
}
10 changes: 10 additions & 0 deletions src/core/jsonrpc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -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
Expand Down
Loading
Loading