Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
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: 3 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -233,12 +233,15 @@ GGML_NATIVE=OFF cargo check --manifest-path Cargo.toml \
| Feature | Default | Gates | Drops deps |
| ------- | ------- | ----- | ---------- |
| `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) |

**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.

**Scope note:** the `voice` gate does **not** drop `whisper-rs` / `llama` / `cpal`. Those live in the inference domain (`src/openhuman/inference/local/service/whisper_engine.rs`; `cpal` is shared with accessibility) and await a separate future `inference` gate. The issue-level DoD line claiming whisper is dropped is superseded by this scope correction.

**`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.

### Event bus (`src/core/event_bus/`)
Expand Down
20 changes: 17 additions & 3 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -250,7 +250,7 @@ ethers-signers = { version = "2.0.14", default-features = false }
# - ed25519-dalek: Solana transaction signing.
# - bs58: Solana base58 addresses + Tron base58check addresses.
# - ripemd: RIPEMD160 for BTC HASH160 (P2WPKH) and Tron address hash.
bitcoin = { version = "0.32", default-features = false, features = ["std", "secp-recovery", "rand-std"] }
bitcoin = { version = "0.32", default-features = false, features = ["std", "secp-recovery", "rand-std"], optional = true }
ed25519-dalek = { version = "2", default-features = false, features = ["std", "rand_core"] }
bs58 = { version = "0.5", default-features = false, features = ["std", "check"] }
ripemd = "0.1"
Expand All @@ -260,7 +260,7 @@ ripemd = "0.1"
# off the recovery phrase without going through the EVM signer wrapper.
coins-bip39 = "0.8"
# Solana off-curve check for ATA derivation (find_program_address).
curve25519-dalek = { version = "4", default-features = false, features = ["alloc"] }
curve25519-dalek = { version = "4", default-features = false, features = ["alloc"], optional = true }

fantoccini = { version = "0.22.0", optional = true, default-features = false, features = ["rustls-tls"] }
pdf-extract = "0.10"
Expand Down Expand Up @@ -341,7 +341,7 @@ tokio = { version = "1", features = ["test-util"] }
proptest = "1"

[features]
default = ["tokenjuice-treesitter", "voice", "media"]
default = ["tokenjuice-treesitter", "voice", "web3", "media"]
# 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 All @@ -358,6 +358,20 @@ tokenjuice-treesitter = [
# inference domain (shared with accessibility for cpal) and await a separate
# `inference` gate.
voice = ["dep:hound", "dep:lettre"]
# Web3 domains: openhuman::wallet + openhuman::web3 + openhuman::x402 — the
# crypto wallet (multi-chain sign/broadcast), the high-level swap/bridge/dapp
# surface, and the x402 machine-payment protocol. Default-ON — the desktop app
# always ships with the wallet. Slim / headless builds opt out via
# `--no-default-features --features "<explicit list without web3>"`, which also
# drops the exclusive `bitcoin` (P2WPKH PSBT) + `curve25519-dalek` (Solana
# off-curve ATA) dependencies. Composes with the runtime `DomainSet::Web3` flag
# (#4796): the feature narrows the compile-time surface, `DomainSet` gates it at
# runtime. NOTE: this gate does NOT drop ethers-core / ethers-signers /
# coins-bip39 / bs58 / ed25519-dalek / ripemd — those are shared with the
# Polymarket tools + tinyplace on-chain payments + orchestration and stay
# always-on. When off, tinyplace payments + Polymarket writes degrade to
# graceful "wallet disabled" errors via the wallet/web3/x402 stub facades.
web3 = ["dep:bitcoin", "dep:curve25519-dalek"]
# Media-generation + image domains: the `media_generate_*` agent tools
# (image/video via GMI through the backend) and the `openhuman::image` tool
# contracts scaffold. Default-ON. Slim builds opt out via
Expand Down
3 changes: 3 additions & 0 deletions app/src-tauri/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -150,12 +150,15 @@ cef = { version = "=146.4.1", default-features = false }
# - `media` — re-registers the `media_generate_*` agent tools that #4804 moved
# behind `#[cfg(feature = "media")]`; it sheds no deps, so this only restores
# the pre-gate desktop tool surface.
# - `web3` — keeps the wallet/web3/x402 domains and their agent tools in the
# desktop build while allowing slim builds to omit the crypto-only deps.
#
# `tokenjuice-treesitter` is the remaining un-forwarded default — tracked in
# #4918, deliberately not bundled here because dropping it may be intentional.
openhuman_core = { path = "../..", package = "openhuman", default-features = false, features = [
"media",
"voice",
"web3",
] }
tinyjuice = { version = "0.2.1", default-features = false }

Expand Down
47 changes: 47 additions & 0 deletions src/core/all_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -234,6 +234,52 @@ fn voice_and_audio_controllers_absent_when_feature_off() {
);
}

/// With the `web3` feature on (the default), the wallet + web3 + x402
/// controllers are compiled in and registered, and the high-level web3 agent
/// tools (swap/bridge/dapp) are present — the desktop build is byte-identical.
#[test]
#[cfg(feature = "web3")]
fn wallet_web3_x402_controllers_registered_when_feature_on() {
let schemas = all_controller_schemas();
assert!(
schemas.iter().any(|s| s.namespace == "wallet"),
"wallet controllers must be registered when the `web3` feature is on"
);
assert!(
schemas.iter().any(|s| s.namespace.starts_with("web3_")),
"web3 (swap/bridge/dapp) controllers must be registered when the `web3` feature is on"
);
assert!(
schemas.iter().any(|s| s.namespace == "x402"),
"x402 controllers must be registered when the `web3` feature is on"
);
assert!(
!crate::openhuman::web3::all_web3_agent_tools().is_empty(),
"web3 agent tools must be present when the `web3` feature is on"
);
}

/// With the `web3` feature off, all three domains are compiled out: their
/// controllers never enter the registry (wallet/web3/x402 RPC methods are
/// unknown-method and absent from `/schema`) and the web3 agent tools are
/// gone. This is the compile-time stub-facade correctness gate (see
/// `openhuman::{wallet,web3,x402}::stub`).
#[test]
#[cfg(not(feature = "web3"))]
fn wallet_web3_x402_controllers_absent_when_feature_off() {
let schemas = all_controller_schemas();
assert!(
!schemas.iter().any(|s| s.namespace == "wallet"
|| s.namespace.starts_with("web3_")
|| s.namespace == "x402"),
"wallet/web3/x402 controllers must be compiled out when the `web3` feature is off"
);
assert!(
crate::openhuman::web3::all_web3_agent_tools().is_empty(),
"web3 agent tools must be gone when the `web3` feature is off"
);
}

#[test]
fn schema_for_rpc_method_finds_known_method() {
let schema = schema_for_rpc_method("openhuman.health_snapshot");
Expand Down Expand Up @@ -861,6 +907,7 @@ fn group_mapping_smoke() {
assert_eq!(group_for_namespace("flows"), Some(DomainGroup::Flows));
assert_eq!(group_for_namespace("skills"), Some(DomainGroup::Skills));
assert_eq!(group_for_namespace("voice"), Some(DomainGroup::Voice));
#[cfg(feature = "web3")]
assert_eq!(group_for_namespace("wallet"), Some(DomainGroup::Web3));
assert_eq!(group_for_namespace("meet"), Some(DomainGroup::Meet));
// Internal-only registry is grouped too (mcp_audit → Mcp).
Expand Down
11 changes: 10 additions & 1 deletion src/openhuman/tools/impl/network/http_request.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ use crate::openhuman::config::HttpRequestConfig;
use crate::openhuman::security::{CommandClass, GateDecision, SecurityPolicy};
use crate::openhuman::tools::traits::{PermissionLevel, Tool, ToolResult};
use async_trait::async_trait;
// Only used by the `web3`-gated x402 402-retry path below.
#[cfg(feature = "web3")]
use base64::engine::Engine as _;
use serde_json::json;
use std::sync::Arc;
Expand Down Expand Up @@ -161,6 +163,11 @@ impl HttpRequestTool {
Ok(request.send().await?)
}

// x402 machine-payment retry — gated with the `web3` feature (the x402
// domain, its ledger, and the `SettlementResponse`/`PaymentRecord` types
// are compiled out when web3 is disabled). With the feature off, a 402 is
// returned to the caller unpaid (see the call site).
#[cfg(feature = "web3")]
async fn handle_x402_payment(
&self,
_initial_response: reqwest::Response,
Expand Down Expand Up @@ -433,7 +440,9 @@ impl Tool for HttpRequestTool {
};

// x402: if the server returns 402 with a PAYMENT-REQUIRED header,
// attempt to pay using the wallet's Solana key and retry.
// attempt to pay using the wallet's Solana key and retry. Compiled out
// when the `web3` feature is off — a 402 then passes through unpaid.
#[cfg(feature = "web3")]
let response = if response.status() == reqwest::StatusCode::PAYMENT_REQUIRED
&& (response.headers().get("PAYMENT-REQUIRED").is_some()
|| response.headers().get("X-PAYMENT-REQUIRED").is_some())
Expand Down
52 changes: 51 additions & 1 deletion src/openhuman/tools/impl/network/polymarket.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1419,6 +1419,56 @@ fn parse_nonce_value(value: &Value) -> Option<u64> {
}
}

#[cfg(test)]
// These tests seed a real wallet (`wallet::setup` + `WalletAccount` with
// `derivation_path`) to exercise wallet-signed Polymarket CLOB writes, so they
// require the `web3` feature. With web3 off the wallet is compiled out and the
// Polymarket write path degrades to a "wallet disabled" error (via the wallet
// stub), so there is nothing here to test. The tool itself still compiles in
// both configs against the stub — only these signing tests are gated.
#[cfg(all(test, feature = "web3"))]
#[path = "polymarket_tests.rs"]
mod tests;

// Feature-off counterpart: with `web3` compiled out the wallet stub's
// `secret_material` returns the disabled error, so any wallet-signed Polymarket
// write must surface that instead of silently succeeding. The signing-heavy
// happy-path tests above cannot run (no wallet), so this narrow test locks in
// the degraded contract for the slim build.
#[cfg(all(test, not(feature = "web3")))]
mod tests_web3_disabled {
use super::*;
use crate::openhuman::config::PolymarketConfig;
use crate::openhuman::security::{AutonomyLevel, SecurityPolicy};
use std::sync::Arc;

fn write_tool_with_eoa(user: &str) -> PolymarketTool {
let config = PolymarketConfig {
enabled: true,
eoa_address: Some(user.to_string()),
..PolymarketConfig::default()
};
let security = Arc::new(SecurityPolicy {
autonomy: AutonomyLevel::Supervised,
..SecurityPolicy::default()
});
PolymarketTool::new(&config, security)
}

#[tokio::test]
async fn signer_resolution_reports_wallet_disabled() {
// A configured EOA lets user-address resolution short-circuit, so the
// failure comes from `secret_material` (the wallet stub), proving the
// write path degrades to the disabled error rather than panicking or
// succeeding without a signer.
let tool = write_tool_with_eoa("0x000000000000000000000000000000000000dEaD");
let err = tool
.resolve_signer_and_user(None)
.await
.expect_err("wallet-signed writes must fail when web3 is compiled out");
let msg = format!("{err:#}");
assert!(
msg.contains("web3/wallet feature disabled at compile time"),
"expected the stable compile-time-disabled marker, got: {msg}"
);
}
}
1 change: 1 addition & 0 deletions src/openhuman/tools/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ pub use crate::openhuman::team::tools::*;
pub use crate::openhuman::threads::tools::*;
pub use crate::openhuman::tinyplace::tools::*;
pub use crate::openhuman::todos::tools::*;
#[cfg(feature = "web3")]
pub use crate::openhuman::wallet::tools::*;
pub use crate::openhuman::whatsapp_data::tools::*;
pub use crate::openhuman::workspace::tools::*;
Expand Down
12 changes: 11 additions & 1 deletion src/openhuman/tools/ops.rs
Original file line number Diff line number Diff line change
Expand Up @@ -359,11 +359,19 @@ pub fn all_tools_with_runtime(
Box::new(SuggestWorkflowsTool::new(config.clone())),
// Wallet tools — expose wallet operations to the agent tool-call pipeline
// so the crypto sub-agent can prepare transfers, check status, etc.
// Gated with the `web3` feature (the wallet domain is compiled out when
// web3 is disabled; the concrete tool types live under `wallet::tools`).
#[cfg(feature = "web3")]
Box::new(WalletStatusTool::new()),
#[cfg(feature = "web3")]
Box::new(WalletChainStatusTool::new()),
#[cfg(feature = "web3")]
Box::new(WalletPrepareTransferTool::new()),
#[cfg(feature = "web3")]
Box::new(WalletTxStatusTool::new()),
#[cfg(feature = "web3")]
Box::new(WalletTxReceiptTool::new()),
#[cfg(feature = "web3")]
Box::new(WalletLookupTxTool::new()),
Box::new(MemoryStoreTool::new(memory.clone(), security.clone())),
Box::new(MemoryRecallTool::new(memory.clone())),
Expand Down Expand Up @@ -787,7 +795,9 @@ pub fn all_tools_with_runtime(

// x402 — dedicated tool for making paid HTTP requests to x402-enabled
// APIs (Base USDC / Solana USDC). Handles the 402 challenge, EIP-3009
// or SPL payment signing, and ledger recording.
// or SPL payment signing, and ledger recording. Gated with the `web3`
// feature (the x402 domain is compiled out when web3 is disabled).
#[cfg(feature = "web3")]
tools.push(Box::new(
crate::openhuman::x402::tools::X402RequestTool::new(),
));
Expand Down
Loading
Loading