Skip to content
12 changes: 12 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand All @@ -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/`)

Expand Down
16 changes: 15 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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 = [
Expand Down Expand Up @@ -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"]
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 @@ -159,6 +159,7 @@ openhuman_core = { path = "../..", package = "openhuman", default-features = fal
"media",
"voice",
"web3",
"meet",
] }
tinyjuice = { version = "0.2.1", default-features = false }

Expand Down
10 changes: 8 additions & 2 deletions src/core/all.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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")]
Comment thread
M3gA-Mind marked this conversation as resolved.
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")]
Comment thread
M3gA-Mind marked this conversation as resolved.
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,
Expand Down
37 changes: 37 additions & 0 deletions src/core/all_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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"
);
}
}
25 changes: 25 additions & 0 deletions src/openhuman/agent_meetings/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
46 changes: 46 additions & 0 deletions src/openhuman/agent_meetings/stub.rs
Original file line number Diff line number Diff line change
@@ -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() {}
}
38 changes: 38 additions & 0 deletions src/openhuman/meet_agent/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::*;
1 change: 1 addition & 0 deletions src/openhuman/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Loading