mirror of
https://github.com/tinyhumansai/openhuman.git
synced 2026-07-27 21:08:00 +00:00
Co-authored-by: Steven Enamakel <enamakel@tinyhumans.ai>
This commit is contained in:
co-authored by
Steven Enamakel
parent
5fcde25320
commit
0eeee12753
@@ -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)."
|
||||
|
||||
@@ -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
@@ -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.
|
||||
|
||||
Generated
-1
@@ -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",
|
||||
|
||||
@@ -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
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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]
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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`].
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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::*;
|
||||
|
||||
@@ -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())),
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user