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
2566f3b538
commit
56841ebaea
@@ -235,6 +235,7 @@ GGML_NATIVE=OFF cargo check --manifest-path Cargo.toml \
|
||||
| `voice` | ON | `openhuman::voice` + `openhuman::audio_toolkit` domains — STT/TTS providers, dictation server, always-on listening, podcast audio + email | `hound`, `lettre` |
|
||||
| `web3` | ON | `openhuman::wallet` + `openhuman::web3` + `openhuman::x402` domains — crypto wallet (multi-chain sign/broadcast), swaps/bridges/dapp calls, x402 machine payments | `bitcoin`, `curve25519-dalek` |
|
||||
| `media` | ON | `openhuman::media_generation` (the `media_generate_*` agent tools) + `openhuman::image` scaffold | none (surface-only) |
|
||||
| `meet` | ON | `openhuman::meet` (join-URL validation) + `openhuman::meet_agent` (live STT/LLM/TTS loop) + `openhuman::agent_meetings` (backend-delegated Meet bot over Socket.IO) | none — see note |
|
||||
|
||||
**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.
|
||||
|
||||
@@ -243,6 +244,17 @@ GGML_NATIVE=OFF cargo check --manifest-path Cargo.toml \
|
||||
**`web3` gate — first gate that sheds real crypto deps.** Same facade pattern: `pub mod wallet;` / `pub mod web3;` / `pub mod x402;` stay always-compiled, real submodules are `#[cfg(feature = "web3")]`, and each domain's `stub.rs` re-exposes the always-on caller surface with disabled-error / empty bodies. When off, the wallet/web3/x402 controllers are unregistered, the web3 swap/bridge/dapp agent tools are absent (via `all_web3_agent_tools()` → empty), and the exclusive `bitcoin` (BTC P2WPKH PSBT) + `curve25519-dalek` (Solana off-curve ATA) deps are dropped. **tinyplace on-chain payments + Polymarket writes degrade to graceful "wallet disabled" errors** (the tinyplace comms path and the core itself are unaffected — `tinyplace::signer` still works via ed25519). The stubs cover `WALLET_NOT_CONFIGURED_MESSAGE`, `status`, `secret_material`, `WalletChain`, `prepare_transfer`/`execute_prepared` (+ param/result types), `solana_cluster`/`SolanaCluster`/`tinyplace_solana_rpc_endpoints`, `tinyplace_signer_seed`, `wallet::rpc::{redact_rpc_url, with_tinyplace_solana_endpoints}`, and the `all_*_registered_controllers`/`all_*_controller_schemas`/`all_web3_agent_tools` entry points. Two caller families still need per-call `#[cfg(feature = "web3")]` because they name concrete gated types rather than a stubbable aggregator: the six `Wallet*Tool` + `X402RequestTool` registrations in `tools/ops.rs`, the `wallet::tools::*` glob in `tools/mod.rs`, and the x402 402-retry path in `tools/impl/network/http_request.rs` (with the feature off a 402 returns to the caller unpaid). **Does NOT drop `ethers-core` / `ethers-signers` / `coins-bip39` / `bs58` / `ed25519-dalek` / `ripemd`** — those are shared with the Polymarket tools (`tools/impl/network/polymarket*`, `clob_auth`) + tinyplace + orchestration and stay always-on. Run the disabled build (`--no-default-features --features tokenjuice-treesitter`) before pushing any change to the wallet/web3/x402 surface — it is the only drift catcher.
|
||||
|
||||
**Leaf-gate variant (`media`, #4804).** Unlike `voice`, the `media` gate needs **no** stub facade: `media_generation` has a single caller (the `build_media_tools` call in `src/openhuman/tools/ops.rs`, itself `#[cfg(feature = "media")]`) and `openhuman::image` is unwired scaffold (#2997), so both modules are simply `#[cfg(feature = "media")] pub mod …`. It is a **surface-only** gate: media generation is backend-proxied (`reqwest`, shared) and the `image` crate is shared with channel upload, so no exclusive deps are shed — the issue's "sheds media processing dependencies" / "controllers unregistered" DoD lines are superseded (Media is agent-tools-only; no controller/store/subscriber is tagged `Media`). When a gated domain is a true leaf, prefer this over the facade+stub.
|
||||
**`meet` gate (#4800)** — uses all three module patterns, one per domain, chosen by the rule *"does always-compiled code reach a non-registration symbol in here?"*:
|
||||
|
||||
- **`meet` → leaf-gate.** `#[cfg(feature = "meet")] pub mod meet;` outright. Its only outside reference is the registration site in `src/core/all.rs`, so no facade is needed (same shape as `audio_toolkit`).
|
||||
- **`meet_agent` → facade + carve-out, no stub file.** Every submodule is gated **except `wav`** (below). Nothing outside the Meet domain calls the gated submodules.
|
||||
- **`agent_meetings` → facade + stub.** Three always-compiled call sites reach in — the heartbeat planner (`calendar::handle_calendar_meeting_candidate`) and two subscriber registrations (`core::jsonrpc`, `channels::runtime::startup`) — so `src/openhuman/agent_meetings/stub.rs` supplies no-op equivalents and those callers need no `#[cfg]`.
|
||||
|
||||
**No deps to shed (do not re-litigate).** Unlike `voice`, this gate drops **zero** dependencies — the Meet domains have no exclusive crates. `meet_agent::wav` is a hand-rolled 79-line RIFF writer with no `use` statements, written precisely so Meet never needed `hound` (which `voice` already owns and sheds). The dependency shed was pre-paid; this gate's value is compile-time surface and binary size, not the dep tree.
|
||||
|
||||
**⚠ The `wav` carve-out is load-bearing.** `meet_agent::wav::pack_pcm16le_mono_wav` is called by `desktop_companion::pipeline::stt`, which is `DomainGroup::Platform` and compiled in **every** build. `pub mod wav;` must stay **ungated** so that call site keeps its real implementation — it costs nothing, since `wav.rs` is dependency-free. If someone gates it, the `--no-default-features` build fails loudly at `desktop_companion`; that failure is correct. Do **not** "fix" it by stubbing `pack_pcm16le_mono_wav` — that trades a compile error for green CI while silently corrupting desktop-companion STT (the STT backend would receive a malformed WAV). Revert the cfg instead.
|
||||
|
||||
**Both-ways tests.** `src/core/all_tests.rs` pins the gate in both directions (`meet_controllers_registered_when_feature_on` / `meet_controllers_absent_when_feature_off`). The negative half is the one that proves the gate removes anything. Note CI's smoke lane runs `cargo check` only and never compiles test code, so a disabled-build **test** break is invisible to it — 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/`)
|
||||
|
||||
|
||||
+15
-1
@@ -341,7 +341,7 @@ tokio = { version = "1", features = ["test-util"] }
|
||||
proptest = "1"
|
||||
|
||||
[features]
|
||||
default = ["tokenjuice-treesitter", "voice", "web3", "media"]
|
||||
default = ["tokenjuice-treesitter", "voice", "web3", "media", "meet"]
|
||||
# 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 = [
|
||||
@@ -384,6 +384,20 @@ web3 = ["dep:bitcoin", "dep:curve25519-dalek"]
|
||||
# controllers / stores / subscribers tagged `Media` (agent tools only), and
|
||||
# `openhuman::image` is currently unwired scaffold (added #2997).
|
||||
media = []
|
||||
# Meet domains: `meet` (join-URL validation), `meet_agent` (live STT/LLM/TTS
|
||||
# loop over an open call), and `agent_meetings` (backend-delegated Meet bot via
|
||||
# Socket.IO). Default-ON — the desktop app always ships with Meet. Slim /
|
||||
# headless builds opt out via `--no-default-features --features "<list without
|
||||
# meet>"`. Composes with the runtime `DomainSet::meet` flag (#4796): the feature
|
||||
# narrows the compile-time surface, `DomainSet` gates it at runtime.
|
||||
# NOTE: unlike `voice`, this gate drops NO dependencies — the Meet domains have
|
||||
# zero exclusive crates. `meet_agent::wav` is a hand-rolled RIFF writer
|
||||
# precisely so Meet never needed `hound` (which `voice` already owns), so the
|
||||
# dependency shed was pre-paid. The gate's value is compile-time surface +
|
||||
# binary size, not the dep tree.
|
||||
# CARVE-OUT: `meet_agent::wav` stays compiled in ALL builds — the always-on
|
||||
# `desktop_companion` STT path depends on it. See `meet_agent/mod.rs`.
|
||||
meet = []
|
||||
sandbox-landlock = ["dep:landlock"]
|
||||
sandbox-bubblewrap = []
|
||||
peripheral-rpi = ["dep:rppal"]
|
||||
|
||||
@@ -160,6 +160,7 @@ openhuman_core = { path = "../..", package = "openhuman", default-features = fal
|
||||
"voice",
|
||||
"tokenjuice-treesitter",
|
||||
"web3",
|
||||
"meet",
|
||||
] }
|
||||
tinyjuice = { version = "0.2.1", default-features = false }
|
||||
|
||||
|
||||
+8
-2
@@ -712,19 +712,25 @@ fn build_registered_controllers() -> Vec<GroupedController> {
|
||||
DomainGroup::Platform,
|
||||
crate::openhuman::notifications::all_notifications_registered_controllers(),
|
||||
);
|
||||
// Google Meet call-join request validation (shell handles the webview)
|
||||
// Google Meet call-join request validation (shell handles the webview).
|
||||
// Gated behind the `meet` feature.
|
||||
#[cfg(feature = "meet")]
|
||||
push(
|
||||
&mut controllers,
|
||||
DomainGroup::Meet,
|
||||
crate::openhuman::meet::all_meet_registered_controllers(),
|
||||
);
|
||||
// Agent meetings — backend-delegated Meet bot via Socket.IO
|
||||
// (gated with meet).
|
||||
#[cfg(feature = "meet")]
|
||||
push(
|
||||
&mut controllers,
|
||||
DomainGroup::Meet,
|
||||
crate::openhuman::agent_meetings::all_agent_meetings_registered_controllers(),
|
||||
);
|
||||
// Live meet-agent loop: STT/LLM/TTS over the open call's audio.
|
||||
// Live meet-agent loop: STT/LLM/TTS over the open call's audio
|
||||
// (gated with meet).
|
||||
#[cfg(feature = "meet")]
|
||||
push(
|
||||
&mut controllers,
|
||||
DomainGroup::Meet,
|
||||
|
||||
@@ -909,7 +909,44 @@ fn group_mapping_smoke() {
|
||||
assert_eq!(group_for_namespace("voice"), Some(DomainGroup::Voice));
|
||||
#[cfg(feature = "web3")]
|
||||
assert_eq!(group_for_namespace("wallet"), Some(DomainGroup::Web3));
|
||||
// `meet` is compiled out under `--no-default-features`, so the registry has
|
||||
// no entry to map (#4800).
|
||||
#[cfg(feature = "meet")]
|
||||
assert_eq!(group_for_namespace("meet"), Some(DomainGroup::Meet));
|
||||
// Internal-only registry is grouped too (mcp_audit → Mcp).
|
||||
assert_eq!(group_for_namespace("mcp_audit"), Some(DomainGroup::Mcp));
|
||||
}
|
||||
|
||||
/// All three Meet namespaces register when the `meet` feature is on (#4800).
|
||||
///
|
||||
/// Paired with `meet_controllers_absent_when_feature_off` below: together they
|
||||
/// pin *both* directions of the compile-time gate. The negative half is the one
|
||||
/// that actually proves the gate does something — a gate that never removes
|
||||
/// anything would still pass this positive test.
|
||||
#[cfg(feature = "meet")]
|
||||
#[test]
|
||||
fn meet_controllers_registered_when_feature_on() {
|
||||
for ns in ["meet", "agent_meetings", "meet_agent"] {
|
||||
assert_eq!(
|
||||
group_for_namespace(ns),
|
||||
Some(DomainGroup::Meet),
|
||||
"`{ns}` must register under DomainGroup::Meet when the `meet` feature is on"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// No Meet namespace registers when the `meet` feature is off (#4800).
|
||||
///
|
||||
/// This is the half that proves the gate: with `meet` compiled out the three
|
||||
/// domains must leave zero trace in either the public or the internal registry.
|
||||
#[cfg(not(feature = "meet"))]
|
||||
#[test]
|
||||
fn meet_controllers_absent_when_feature_off() {
|
||||
for ns in ["meet", "agent_meetings", "meet_agent"] {
|
||||
assert_eq!(
|
||||
group_for_namespace(ns),
|
||||
None,
|
||||
"`{ns}` must not register when the `meet` feature is off"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,18 +13,43 @@
|
||||
//! - [`schemas`] — controller schema + registered handler wrappers
|
||||
//! - [`store`] — SQLite persistence for meeting sessions
|
||||
//! - [`in_call`] — Phase 2 in-call agency: wake-phrase command → orchestrator → `bot:speak`
|
||||
//!
|
||||
//! ## Compile-time gating (`meet` feature, #4800)
|
||||
//!
|
||||
//! All submodules are `#[cfg(feature = "meet")]`. Three always-compiled call
|
||||
//! sites reach in here for non-registration symbols — the heartbeat planner
|
||||
//! (`calendar::handle_calendar_meeting_candidate`) plus two subscriber
|
||||
//! registrations (`core::jsonrpc`, `channels::runtime::startup`) — so a
|
||||
//! `#[cfg(not(feature = "meet"))]` [`stub`] supplies no-op equivalents and
|
||||
//! those callers need no cfg of their own.
|
||||
|
||||
#[cfg(feature = "meet")]
|
||||
pub mod bus;
|
||||
#[cfg(feature = "meet")]
|
||||
pub mod calendar;
|
||||
#[cfg(feature = "meet")]
|
||||
pub mod in_call;
|
||||
#[cfg(feature = "meet")]
|
||||
pub mod ops;
|
||||
#[cfg(feature = "meet")]
|
||||
pub mod recent_calls;
|
||||
#[cfg(feature = "meet")]
|
||||
pub mod schemas;
|
||||
#[cfg(feature = "meet")]
|
||||
pub mod store;
|
||||
#[cfg(feature = "meet")]
|
||||
pub mod summary;
|
||||
#[cfg(feature = "meet")]
|
||||
pub mod types;
|
||||
#[cfg(feature = "meet")]
|
||||
pub mod upcoming;
|
||||
|
||||
#[cfg(not(feature = "meet"))]
|
||||
mod stub;
|
||||
#[cfg(not(feature = "meet"))]
|
||||
pub use stub::*;
|
||||
|
||||
#[cfg(feature = "meet")]
|
||||
pub use schemas::{
|
||||
all_controller_schemas as all_agent_meetings_controller_schemas,
|
||||
all_registered_controllers as all_agent_meetings_registered_controllers,
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
//! No-op stand-ins for the `agent_meetings` symbols that always-compiled code
|
||||
//! reaches for, used when the `meet` feature is off (#4800).
|
||||
//!
|
||||
//! Only the three symbols with non-registration call sites outside the Meet
|
||||
//! domain live here. Everything else in `agent_meetings` is reachable only via
|
||||
//! the controller registry, which is itself `#[cfg(feature = "meet")]` at
|
||||
//! `core::all` — so it needs no stub.
|
||||
//!
|
||||
//! These are genuine no-ops, not "register a controller that then errors": a
|
||||
//! `register_*` that registers nothing is exactly the intended disabled-build
|
||||
//! behaviour — the subscriber simply never exists, so the events never route.
|
||||
|
||||
/// Stub of [`super::calendar`](../calendar/index.html) for `--no-default-features` builds.
|
||||
pub mod calendar {
|
||||
/// No-op stand-in for `calendar::register_meet_calendar_subscriber`.
|
||||
///
|
||||
/// With `meet` compiled out there is no calendar-triggered meeting flow to
|
||||
/// drive, so registering nothing is the correct behaviour: the event bus
|
||||
/// keeps routing, it just has no Meet subscriber attached.
|
||||
pub fn register_meet_calendar_subscriber() {}
|
||||
|
||||
/// No-op stand-in for `calendar::handle_calendar_meeting_candidate`.
|
||||
///
|
||||
/// The `bool` means "Meet published its own actionable card, so the caller
|
||||
/// should skip its generic one". With `meet` compiled out no card was ever
|
||||
/// published, so `false` is semantically exact — the heartbeat planner
|
||||
/// correctly falls through to its plain "meeting starting" reminder.
|
||||
pub async fn handle_calendar_meeting_candidate(
|
||||
_meet_url: String,
|
||||
_event_title: String,
|
||||
_owner_display_name: Option<String>,
|
||||
_calendar_event_id: Option<String>,
|
||||
) -> bool {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/// Stub of [`super::bus`](../bus/index.html) for `--no-default-features` builds.
|
||||
pub mod bus {
|
||||
/// No-op stand-in for `bus::register_meeting_event_subscriber`.
|
||||
///
|
||||
/// Same reasoning as `calendar::register_meet_calendar_subscriber`: the
|
||||
/// `Meeting*` / `BackendMeet*` domain events still exist on the bus (they
|
||||
/// are inert plain data), they simply have no subscriber and never fire.
|
||||
pub fn register_meeting_event_subscriber() {}
|
||||
}
|
||||
@@ -33,19 +33,57 @@
|
||||
//! - `openhuman.meet_agent_push_listen_pcm` — shell pushes captured PCM frames
|
||||
//! - `openhuman.meet_agent_poll_speech` — shell pulls synthesized PCM frames
|
||||
//! - `openhuman.meet_agent_stop_session` — close session, flush pending audio
|
||||
//!
|
||||
//! ## Compile-time gating (`meet` feature, #4800)
|
||||
//!
|
||||
//! Every submodule here is `#[cfg(feature = "meet")]` — **except [`wav`]**.
|
||||
//!
|
||||
//! ### ⚠ The `wav` carve-out is load-bearing — do not "tidy" it
|
||||
//!
|
||||
//! [`wav::pack_pcm16le_mono_wav`] is called by
|
||||
//! `desktop_companion::pipeline::stt`, which is `DomainGroup::Platform` and is
|
||||
//! therefore compiled in **every** build, including `--no-default-features`.
|
||||
//! `wav` must stay ungated so that call site keeps its **real** implementation.
|
||||
//!
|
||||
//! This is safe to leave ungated at zero cost: `wav.rs` is a self-contained
|
||||
//! hand-rolled RIFF writer with no `use` statements and no dependencies. It
|
||||
//! pulls in nothing when the rest of the domain is compiled out. (It is
|
||||
//! hand-rolled precisely so Meet never needed `hound`, which the `voice` gate
|
||||
//! already owns and sheds.)
|
||||
//!
|
||||
//! **If you ever add `#[cfg(feature = "meet")]` to `pub mod wav;`**, the
|
||||
//! `--no-default-features` build will fail loudly at `desktop_companion`. That
|
||||
//! failure is *correct and useful*. Do **not** "fix" it by stubbing
|
||||
//! `pack_pcm16le_mono_wav` to return an empty/placeholder buffer: that turns a
|
||||
//! compile error into green CI while silently corrupting desktop-companion STT
|
||||
//! forever (the STT backend would receive a malformed WAV). Revert the cfg
|
||||
//! instead — or, if `wav` genuinely must move, relocate it to a always-compiled
|
||||
//! home rather than stubbing it.
|
||||
|
||||
#[cfg(feature = "meet")]
|
||||
pub mod brain;
|
||||
#[cfg(feature = "meet")]
|
||||
pub mod ops;
|
||||
#[cfg(feature = "meet")]
|
||||
pub mod rpc;
|
||||
#[cfg(feature = "meet")]
|
||||
pub mod schemas;
|
||||
#[cfg(feature = "meet")]
|
||||
pub mod session;
|
||||
#[cfg(feature = "meet")]
|
||||
pub mod store;
|
||||
#[cfg(feature = "meet")]
|
||||
pub mod types;
|
||||
// NOT gated — see the carve-out note above. `desktop_companion` (always-on)
|
||||
// depends on the real implementation.
|
||||
pub mod wav;
|
||||
|
||||
#[cfg(feature = "meet")]
|
||||
pub use schemas::{
|
||||
all_controller_schemas as all_meet_agent_controller_schemas,
|
||||
all_registered_controllers as all_meet_agent_registered_controllers,
|
||||
};
|
||||
#[cfg(feature = "meet")]
|
||||
pub use session::{MeetAgentSession, MeetAgentSessionRegistry, SESSION_REGISTRY};
|
||||
#[cfg(feature = "meet")]
|
||||
pub use types::*;
|
||||
|
||||
@@ -71,6 +71,7 @@ pub mod mcp_registry;
|
||||
pub mod mcp_server;
|
||||
#[cfg(feature = "media")]
|
||||
pub mod media_generation;
|
||||
#[cfg(feature = "meet")]
|
||||
pub mod meet;
|
||||
pub mod meet_agent;
|
||||
pub mod memory;
|
||||
|
||||
Reference in New Issue
Block a user