From 81fe4184117c1c36cf7c145b7c918407e68054b4 Mon Sep 17 00:00:00 2001 From: oxoxDev Date: Tue, 14 Jul 2026 15:35:51 +0530 Subject: [PATCH 01/14] feat(web3): add default-on web3 Cargo feature gating wallet/web3/x402 (#4802) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Introduces the compile-time `web3` feature (default-ON, so the desktop build is byte-identical). Makes the exclusive `bitcoin` (BTC P2WPKH PSBT) and `curve25519-dalek` (Solana off-curve ATA) crates optional, dropped when the gate is off. Shared crypto crates (ethers-*, coins-bip39, bs58, ed25519-dalek, ripemd) stay always-on — Polymarket + tinyplace depend on them. --- Cargo.toml | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 84f0f7c527..9b85b21acb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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" @@ -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" @@ -334,7 +334,7 @@ tokio = { version = "1", features = ["test-util"] } proptest = "1" [features] -default = ["tokenjuice-treesitter", "voice"] +default = ["tokenjuice-treesitter", "voice", "web3"] # 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 = [ @@ -351,6 +351,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 ""`, 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"] sandbox-landlock = ["dep:landlock"] sandbox-bubblewrap = [] peripheral-rpi = ["dep:rppal"] From e69154fa6a5676abbf940433ca566396c3f53847 Mon Sep 17 00:00:00 2001 From: oxoxDev Date: Tue, 14 Jul 2026 15:35:57 +0530 Subject: [PATCH 02/14] feat(wallet): facade+stub for web3 gate (#4802) Keeps `pub mod wallet` always-compiled as a facade; real submodules move behind `#[cfg(feature = "web3")]`. A `stub.rs` mirrors the always-on caller surface (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, rpc::{redact_rpc_url, with_tinyplace_solana_endpoints}, prepared_quotes_for_test, controller-registration entry points) with disabled-error / empty bodies so tinyplace payments + Polymarket writes degrade gracefully instead of failing to compile. --- src/openhuman/wallet/mod.rs | 47 ++++++- src/openhuman/wallet/stub.rs | 255 +++++++++++++++++++++++++++++++++++ 2 files changed, 301 insertions(+), 1 deletion(-) create mode 100644 src/openhuman/wallet/stub.rs diff --git a/src/openhuman/wallet/mod.rs b/src/openhuman/wallet/mod.rs index e144ac831c..0cbb25c699 100644 --- a/src/openhuman/wallet/mod.rs +++ b/src/openhuman/wallet/mod.rs @@ -2,29 +2,60 @@ //! the agent-facing execution surface (balances, transfers, swaps, //! contract calls). See [`execution`] for the prepare/confirm/execute flow //! and [`chains`] for the per-chain signing/broadcast implementations. +//! +//! ## Compile-time gate (`web3` feature) +//! +//! `pub mod wallet;` is ALWAYS compiled — it is a facade. The real +//! implementation (the submodules below and their re-exports) is gated behind +//! the default-ON `web3` Cargo feature (shared with `openhuman::web3` + +//! `openhuman::x402`). When the feature is off, [`stub`] takes its place and +//! exposes the same public surface that always-on / other-gated callers depend +//! on (`WALLET_NOT_CONFIGURED_MESSAGE`, `status`, `secret_material`, +//! `WalletChain`, `prepare_transfer`, `execute_prepared`, the prepare/execute +//! param + result types, `solana_cluster` / `SolanaCluster` / +//! `tinyplace_solana_rpc_endpoints`, `tinyplace_signer_seed`, the `rpc` +//! submodule, `prepared_quotes_for_test`, and the controller-registration +//! entry points) with no-op / disabled-error bodies — so tinyplace on-chain +//! payments + the Polymarket tools degrade to graceful "wallet disabled" +//! errors rather than failing to compile. Signatures MUST match the real ones; +//! the disabled build +//! (`cargo check --no-default-features --features tokenjuice-treesitter`) is +//! the only thing that catches drift. +#[cfg(feature = "web3")] mod abi; +#[cfg(feature = "web3")] mod chains; +#[cfg(feature = "web3")] mod defaults; +#[cfg(feature = "web3")] mod execution; +#[cfg(feature = "web3")] mod ops; +#[cfg(feature = "web3")] pub(crate) mod rpc; +#[cfg(feature = "web3")] mod schemas; +#[cfg(feature = "web3")] pub mod tools; -#[cfg(test)] +#[cfg(all(test, feature = "web3"))] pub(crate) mod test_support; +#[cfg(feature = "web3")] pub use abi::encode_erc20_transfer; /// 32-byte Ed25519 seed for the tiny.place LocalSigner. Derived from the user's /// primary Solana wallet key via SLIP-0010; consumed in-process and never exposed. +#[cfg(feature = "web3")] pub(crate) use chains::solana::tinyplace_signer_seed; +#[cfg(feature = "web3")] pub use defaults::{ asset_catalog, default_rpc_url, env_var_for_chain, evm_asset_catalog, explorer_tx_url, find_asset, find_asset_for_network, network_defaults, rpc_source_for_chain, rpc_url_for_chain, rpc_url_for_evm_network, solana_cluster, tinyplace_solana_rpc_endpoints, EvmNetwork, RpcSource, SolanaCluster, WalletAssetDefinition, WalletNetworkDefaults, }; +#[cfg(feature = "web3")] pub use execution::{ balances, chain_status, execute_prepared, lookup_tx, network_defaults as wallet_network_defaults, prepare_transfer, prepared_quotes_for_test, @@ -34,16 +65,30 @@ pub use execution::{ }; /// Crate-internal signing primitives the `web3` layer builds on. Not part of /// the agent / RPC surface. +#[cfg(feature = "web3")] pub(crate) use execution::{sign_and_broadcast_evm, sign_and_broadcast_solana}; +#[cfg(feature = "web3")] pub(crate) use ops::secret_material; +#[cfg(feature = "web3")] pub use ops::{ reveal_recovery_phrase, setup, status, RevealRecoveryPhraseResult, WalletAccount, WalletChain, WalletSetupParams, WalletSetupSource, WalletStatus, WALLET_NOT_CONFIGURED_MESSAGE, }; /// Reduce an RPC URL to `scheme://host` for logging so private provider tokens /// embedded in the path/query never reach the logs. +#[cfg(feature = "web3")] pub(crate) use rpc::redact_rpc_url; +#[cfg(feature = "web3")] pub use schemas::{ all_controller_schemas, all_registered_controllers, all_wallet_controller_schemas, all_wallet_registered_controllers, schemas, wallet_schemas, }; + +// --------------------------------------------------------------------------- +// Disabled facade — compiled only when the `web3` feature is OFF. +// --------------------------------------------------------------------------- + +#[cfg(not(feature = "web3"))] +mod stub; +#[cfg(not(feature = "web3"))] +pub use stub::*; diff --git a/src/openhuman/wallet/stub.rs b/src/openhuman/wallet/stub.rs new file mode 100644 index 0000000000..23031ea85e --- /dev/null +++ b/src/openhuman/wallet/stub.rs @@ -0,0 +1,255 @@ +//! Disabled-wallet facade. +//! +//! Compiled only when the `web3` Cargo feature is OFF (see the gate in +//! [`super`]). It mirrors the subset of the real `wallet` public surface that +//! always-on / other-gated callers depend on, with no-op / `None` / +//! disabled-error bodies so the crate still compiles, boots, and serves `/rpc` +//! without the wallet + web3 + x402 domains. +//! +//! The signatures here MUST match the real ones exactly (return types +//! included). The disabled build +//! (`cargo check --no-default-features --features tokenjuice-treesitter`) is +//! the only thing that catches drift — if a real signature changes, update the +//! mirror below until that build is green again. +//! +//! Consumers covered here (all outside `wallet`, so all must keep compiling): +//! - `core/jsonrpc.rs` — `WALLET_NOT_CONFIGURED_MESSAGE` +//! - `tools/impl/network/polymarket.rs` — `secret_material`, `status`, +//! `WalletChain` +//! - `tinyplace/payment.rs` — `prepare_transfer`, `execute_prepared`, the +//! param/result types, `SolanaCluster`, `solana_cluster`, +//! `tinyplace_solana_rpc_endpoints`, `rpc::with_tinyplace_solana_endpoints` +//! - `tinyplace/manifest.rs` — `solana_cluster`, `tinyplace_solana_rpc_endpoints`, +//! `redact_rpc_url`, `SolanaCluster::usdc_mint` +//! - `tinyplace/signal_store.rs`, `tinyplace/state.rs` — `tinyplace_signer_seed` +//! - `test_support/introspect.rs` — `prepared_quotes_for_test`, +//! `PreparedTransaction` +//! - `core/all.rs` — `all_wallet_registered_controllers` + +use serde::Serialize; + +use crate::core::all::RegisteredController; +use crate::core::ControllerSchema; +use crate::rpc::RpcOutcome; + +/// Error text returned by every disabled-path operation that must yield a +/// `Result`. Shared so callers/log-greps see one stable string. +const DISABLED_MSG: &str = "web3/wallet feature disabled at compile time"; + +/// Mirrors the real `ops::WALLET_NOT_CONFIGURED_MESSAGE` verbatim. `jsonrpc.rs` +/// compares Sentry-noise errors against this exact string, so it must not drift. +pub const WALLET_NOT_CONFIGURED_MESSAGE: &str = "wallet is not configured; run wallet setup first"; + +// --------------------------------------------------------------------------- +// Chain / status surface (mirrors `ops::{WalletChain, WalletAccount, +// WalletStatus, status, secret_material}`) +// --------------------------------------------------------------------------- + +/// The four wallet chains. Mirrors [`super::ops::WalletChain`] (real build). +/// Callers pattern-match on `Evm` / `Solana`; the full set is kept for parity. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)] +#[serde(rename_all = "snake_case")] +pub enum WalletChain { + Evm, + Btc, + Solana, + Tron, +} + +/// A derived per-chain account. Mirrors the fields Polymarket reads +/// (`chain`, `address`). +#[derive(Debug, Clone, Serialize)] +pub struct WalletAccount { + pub chain: WalletChain, + pub address: String, +} + +/// Wallet status snapshot. Only `accounts` is read by out-of-module callers +/// (Polymarket EOA resolution); with the wallet disabled it is always empty. +#[derive(Debug, Clone, Default, Serialize)] +pub struct WalletStatus { + pub accounts: Vec, +} + +/// Decrypted secret handle. Polymarket reads `encrypted_mnemonic` + +/// `derivation_path` — but `secret_material` never returns `Ok` here, so these +/// are never actually produced. Kept nameable for the return type. +pub(crate) struct WalletSecretMaterial { + pub encrypted_mnemonic: String, + pub derivation_path: String, +} + +/// Disabled: no wallet is configured, so the status carries no accounts. Kept +/// `Ok` (not `Err`) so Polymarket degrades to the clean "run wallet setup" +/// message instead of a decrypt-context error. +pub async fn status() -> Result, String> { + log::debug!("[wallet-stub] status requested (web3 disabled) — no accounts"); + Ok(RpcOutcome::new( + WalletStatus::default(), + vec!["wallet disabled at compile time".to_string()], + )) +} + +/// Always errors: secret material cannot be produced with the wallet compiled +/// out. Callers `?`-propagate (Polymarket writes surface the disabled error). +pub(crate) async fn secret_material(_chain: WalletChain) -> Result { + Err(DISABLED_MSG.to_string()) +} + +// --------------------------------------------------------------------------- +// Prepare / execute surface (mirrors `execution::{prepare_transfer, +// execute_prepared, PrepareTransferParams, ExecutePreparedParams, +// PreparedTransaction, ExecutionResult, prepared_quotes_for_test}`) +// --------------------------------------------------------------------------- + +/// Inputs to `prepare_transfer`. Mirrors the fields `tinyplace/payment.rs` +/// sets. `evm_network` is `Option<()>` (the real `Option` cannot be +/// named with `defaults` compiled out); the only external constructor passes +/// `None`, so the placeholder is behaviourally identical. +#[derive(Debug, Clone)] +pub struct PrepareTransferParams { + pub chain: WalletChain, + pub to_address: String, + pub amount_raw: String, + pub asset_symbol: Option, + pub evm_network: Option<()>, +} + +/// Inputs to `execute_prepared`. Mirrors the real type. +#[derive(Debug, Default, Clone)] +pub struct ExecutePreparedParams { + pub quote_id: String, + pub confirmed: bool, +} + +/// A prepared quote. Out-of-module callers read `quote_id` (`tinyplace`) and +/// serialize the collection (`test_support`); `Serialize` + that field are the +/// only requirements. The real type is far richer. +#[derive(Debug, Clone, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct PreparedTransaction { + pub quote_id: String, +} + +/// Result of an execute. Out-of-module callers read `transaction_hash` +/// (`tinyplace/payment.rs`); `execute_prepared` never returns `Ok`, so it is +/// never actually produced. +#[derive(Debug, Clone)] +pub struct ExecutionResult { + pub transaction_hash: String, +} + +/// Disabled: no transfer can be prepared with the wallet compiled out. +pub async fn prepare_transfer( + _params: PrepareTransferParams, +) -> Result, String> { + Err(DISABLED_MSG.to_string()) +} + +/// Disabled: no prepared transfer can be executed with the wallet compiled out. +pub async fn execute_prepared( + _params: ExecutePreparedParams, +) -> Result, String> { + Err(DISABLED_MSG.to_string()) +} + +/// Always empty: there is no quote store when the wallet is compiled out. +pub fn prepared_quotes_for_test() -> Vec { + Vec::new() +} + +/// Disabled: the tiny.place signer seed derives from the Solana wallet key, +/// which does not exist. Callers `?`-propagate → "unlock wallet" prompt. +pub(crate) async fn tinyplace_signer_seed() -> Result<[u8; 32], String> { + Err(DISABLED_MSG.to_string()) +} + +// --------------------------------------------------------------------------- +// Solana cluster metadata (mirrors `defaults::{SolanaCluster, solana_cluster, +// tinyplace_solana_rpc_endpoints}`) +// --------------------------------------------------------------------------- + +/// Public Solana clusters. Mirrors [`super::defaults::SolanaCluster`] including +/// the `usdc_mint` accessor `tinyplace/{payment,manifest}.rs` call. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum SolanaCluster { + Mainnet, + Devnet, +} + +impl SolanaCluster { + /// USDC SPL-token mint address for the cluster. Same literals as the real + /// `defaults` module so any residual log/compare paths see stable values. + pub fn usdc_mint(self) -> &'static str { + match self { + Self::Mainnet => "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v", + Self::Devnet => "4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU", + } + } +} + +/// Resolve the configured Solana cluster. With the wallet disabled nothing +/// settles on-chain, so the default (Mainnet) is returned unconditionally. +pub fn solana_cluster() -> SolanaCluster { + SolanaCluster::Mainnet +} + +/// Always empty: the wallet is compiled out, so there are no settlement +/// endpoints. `tinyplace/manifest.rs`'s balance loop therefore no-ops and the +/// balance shows as unknown — the correct degraded state. +pub fn tinyplace_solana_rpc_endpoints() -> Vec { + Vec::new() +} + +// --------------------------------------------------------------------------- +// rpc submodule (mirrors `rpc::{redact_rpc_url, with_tinyplace_solana_endpoints}`) +// --------------------------------------------------------------------------- + +pub(crate) mod rpc { + /// Redact an RPC URL for logging. With the wallet disabled we never build + /// real endpoints, but `tinyplace/manifest.rs` still calls this on any URL + /// it iterates; return a constant so no token can ever leak. + pub(crate) fn redact_rpc_url(_raw: &str) -> String { + "".to_string() + } + + /// Run `fut` unchanged — there is no tiny.place endpoint scope to install + /// when the wallet is compiled out. Matches the real generic signature so + /// `tinyplace/payment.rs` type-checks; the future itself resolves to a + /// disabled error from `prepare_transfer`. + pub(crate) async fn with_tinyplace_solana_endpoints(_endpoints: Vec, fut: F) -> T + where + F: std::future::Future, + { + fut.await + } +} + +/// Re-export mirrors the real `pub(crate) use rpc::redact_rpc_url;` so +/// `wallet::redact_rpc_url` resolves at the module root for `tinyplace`. +pub(crate) use rpc::redact_rpc_url; + +// --------------------------------------------------------------------------- +// Agent-tool facade (mirrors `pub mod tools`, re-exported via tools/mod.rs) +// --------------------------------------------------------------------------- + +/// Empty tools module. `tools/mod.rs` glob-re-exports `wallet::tools::*`; the +/// concrete wallet tool constructors it names are `#[cfg(feature = "web3")]` +/// at their registration sites, so nothing is referenced here when off. +pub mod tools {} + +// --------------------------------------------------------------------------- +// Controller registration (mirrors `schemas::{all_wallet_registered_controllers, +// all_wallet_controller_schemas}`) +// --------------------------------------------------------------------------- + +/// No wallet controllers are registered when the wallet is compiled out — the +/// `openhuman.wallet_*` RPCs become unknown-method. +pub fn all_wallet_registered_controllers() -> Vec { + Vec::new() +} + +/// No wallet controller schemas when the wallet is compiled out. +pub fn all_wallet_controller_schemas() -> Vec { + Vec::new() +} From 1172b64d2708cee1512d33cf4f975575e5ec3898 Mon Sep 17 00:00:00 2001 From: oxoxDev Date: Tue, 14 Jul 2026 15:36:03 +0530 Subject: [PATCH 03/14] feat(web3): facade+stub for the web3 swap/bridge/dapp domain (#4802) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Real submodules gated behind `web3`; stub returns empty controllers, schemas, and agent tools when off (swap/bridge/dapp RPCs become unknown-method, the web3 agent tools are absent). No per-call-site cfg needed — the aggregator entry points return empty. --- src/openhuman/web3/mod.rs | 42 +++++++++++++++++++++++++++++++++++++- src/openhuman/web3/stub.rs | 32 +++++++++++++++++++++++++++++ 2 files changed, 73 insertions(+), 1 deletion(-) create mode 100644 src/openhuman/web3/stub.rs diff --git a/src/openhuman/web3/mod.rs b/src/openhuman/web3/mod.rs index b5dc3312f0..06b82df7be 100644 --- a/src/openhuman/web3/mod.rs +++ b/src/openhuman/web3/mod.rs @@ -11,26 +11,58 @@ //! - [`bridge`] (`web3_bridge`) — cross-chain DLN bridges. //! - [`dapp`] (`web3_dapp`) — generic EVM contract calls. +//! ## Compile-time gate (`web3` feature) +//! +//! `pub mod web3;` is ALWAYS compiled — it is a facade. The real swap/bridge/ +//! dapp implementation is gated behind the default-ON `web3` Cargo feature +//! (shared with `openhuman::wallet` + `openhuman::x402`). When the feature is +//! off, [`stub`] takes its place and exposes the controller/agent-tool +//! registration entry points (`all_web3_registered_controllers`, +//! `all_web3_controller_schemas`, `all_web3_agent_tools`) returning empty +//! collections, so `core/all.rs` + `tools/ops.rs` need no per-call `#[cfg]`. + +#[cfg(feature = "web3")] pub mod bridge; +#[cfg(feature = "web3")] pub mod client; +#[cfg(feature = "web3")] pub mod dapp; +#[cfg(feature = "web3")] pub mod ops; +#[cfg(feature = "web3")] pub mod store; +#[cfg(feature = "web3")] pub mod swap; +#[cfg(feature = "web3")] pub mod types; -#[cfg(test)] +#[cfg(all(test, feature = "web3"))] #[path = "web3_tests.rs"] mod tests; +#[cfg(feature = "web3")] use serde::Serialize; +#[cfg(feature = "web3")] use crate::core::all::RegisteredController; +#[cfg(feature = "web3")] use crate::core::{ControllerSchema, FieldSchema, TypeSchema}; +#[cfg(feature = "web3")] use crate::openhuman::tools::traits::{Tool, ToolResult}; +#[cfg(feature = "web3")] use crate::rpc::RpcOutcome; +// --------------------------------------------------------------------------- +// Disabled facade — compiled only when the `web3` feature is OFF. +// --------------------------------------------------------------------------- + +#[cfg(not(feature = "web3"))] +mod stub; +#[cfg(not(feature = "web3"))] +pub use stub::*; + /// A required JSON-typed controller input. +#[cfg(feature = "web3")] pub(crate) fn req_json(name: &'static str, comment: &'static str) -> FieldSchema { FieldSchema { name, @@ -41,6 +73,7 @@ pub(crate) fn req_json(name: &'static str, comment: &'static str) -> FieldSchema } /// An optional string controller input. +#[cfg(feature = "web3")] pub(crate) fn opt_str(name: &'static str, comment: &'static str) -> FieldSchema { FieldSchema { name, @@ -51,6 +84,7 @@ pub(crate) fn opt_str(name: &'static str, comment: &'static str) -> FieldSchema } /// Standard `result` output field. +#[cfg(feature = "web3")] pub(crate) fn json_result(comment: &'static str) -> FieldSchema { FieldSchema { name: "result", @@ -61,6 +95,7 @@ pub(crate) fn json_result(comment: &'static str) -> FieldSchema { } /// Shared `quoteId` + `confirmed` inputs for the execute controllers. +#[cfg(feature = "web3")] pub(crate) fn execute_inputs() -> Vec { vec![ req_json("quoteId", "quoteId returned by a prior web3 quote/call."), @@ -72,6 +107,7 @@ pub(crate) fn execute_inputs() -> Vec { } /// Shared agent-tool JSON Schema for the execute tools. +#[cfg(feature = "web3")] pub(crate) fn execute_tool_schema() -> serde_json::Value { serde_json::json!({ "type": "object", @@ -85,6 +121,7 @@ pub(crate) fn execute_tool_schema() -> serde_json::Value { } /// Convert an op result into a `ToolResult` with pretty-printed JSON on success. +#[cfg(feature = "web3")] pub(crate) fn to_tool_result(result: Result, String>) -> ToolResult { match result { Ok(outcome) => match serde_json::to_string_pretty(&outcome.value) { @@ -96,6 +133,7 @@ pub(crate) fn to_tool_result(result: Result, String> } /// All web3 controller schemas across the swap/bridge/dapp namespaces. +#[cfg(feature = "web3")] pub fn all_web3_controller_schemas() -> Vec { let mut out = swap::schemas::schemas(); out.extend(bridge::schemas::schemas()); @@ -104,6 +142,7 @@ pub fn all_web3_controller_schemas() -> Vec { } /// All web3 registered controllers across the swap/bridge/dapp namespaces. +#[cfg(feature = "web3")] pub fn all_web3_registered_controllers() -> Vec { let mut out = swap::schemas::controllers(); out.extend(bridge::schemas::controllers()); @@ -113,6 +152,7 @@ pub fn all_web3_registered_controllers() -> Vec { /// All web3 agent tools. These call the backend per-invocation, so they error /// gracefully (rather than being hidden) when the user is not signed in. +#[cfg(feature = "web3")] pub fn all_web3_agent_tools() -> Vec> { vec![ Box::new(swap::Web3SwapQuoteTool::new()), diff --git a/src/openhuman/web3/stub.rs b/src/openhuman/web3/stub.rs new file mode 100644 index 0000000000..47ddc8417f --- /dev/null +++ b/src/openhuman/web3/stub.rs @@ -0,0 +1,32 @@ +//! Disabled-web3 facade. +//! +//! Compiled only when the `web3` Cargo feature is OFF (see the gate in +//! [`super`]). The high-level swap/bridge/dapp surface has no always-on +//! callers other than the central registration points in `core/all.rs` +//! (controllers) and `tools/ops.rs` (agent tools), so the stub only needs to +//! return empty collections from those three entry points — no per-call +//! `#[cfg]` at the call sites. +//! +//! Signatures MUST match the real ones exactly; the disabled build +//! (`cargo check --no-default-features --features tokenjuice-treesitter`) is +//! the only thing that catches drift. + +use crate::core::all::RegisteredController; +use crate::core::ControllerSchema; +use crate::openhuman::tools::traits::Tool; + +/// No web3 controller schemas when the domain is compiled out. +pub fn all_web3_controller_schemas() -> Vec { + Vec::new() +} + +/// No web3 controllers are registered when the domain is compiled out — the +/// `openhuman.web3_*` RPCs become unknown-method. +pub fn all_web3_registered_controllers() -> Vec { + Vec::new() +} + +/// No web3 agent tools (swap/bridge/dapp) when the domain is compiled out. +pub fn all_web3_agent_tools() -> Vec> { + Vec::new() +} From 5ec1fc9f147fe1ecdfbf95f103483a8285717edd Mon Sep 17 00:00:00 2001 From: oxoxDev Date: Tue, 14 Jul 2026 15:36:03 +0530 Subject: [PATCH 04/14] feat(x402): facade+stub for the x402 machine-payment domain (#4802) Real submodules gated behind `web3`; stub no-ops `init_ledger` and returns empty controller/schema lists when off. The X402RequestTool registration and the http_request 402-retry path are cfg-gated at their call sites (concrete gated types). --- src/openhuman/x402/mod.rs | 33 ++++++++++++++++++++++++++++++++- src/openhuman/x402/stub.rs | 36 ++++++++++++++++++++++++++++++++++++ 2 files changed, 68 insertions(+), 1 deletion(-) create mode 100644 src/openhuman/x402/stub.rs diff --git a/src/openhuman/x402/mod.rs b/src/openhuman/x402/mod.rs index 3735ae31a8..345a1e792e 100644 --- a/src/openhuman/x402/mod.rs +++ b/src/openhuman/x402/mod.rs @@ -8,22 +8,53 @@ //! //! Protocol spec: / coinbase/x402 (v2). +//! ## Compile-time gate (`web3` feature) +//! +//! `pub mod x402;` is ALWAYS compiled — it is a facade. The real payment +//! machinery is gated behind the default-ON `web3` Cargo feature (shared with +//! `openhuman::wallet` + `openhuman::web3`). When the feature is off, [`stub`] +//! takes its place and exposes the always-on entry points (`init_ledger`, +//! `all_x402_registered_controllers`, `all_x402_controller_schemas`) with +//! no-op / empty bodies. The `X402RequestTool` and the http_request 402-retry +//! path are `#[cfg(feature = "web3")]` at their call sites, so the rest of the +//! payment surface (`PaymentRecord`, `store`, `SettlementResponse`, …) is not +//! referenced when off and need not be stubbed. + +#[cfg(feature = "web3")] mod ops; +#[cfg(feature = "web3")] mod schemas; +#[cfg(feature = "web3")] pub(crate) mod store; +#[cfg(feature = "web3")] pub mod tools; +#[cfg(feature = "web3")] mod types; -#[cfg(test)] +#[cfg(all(test, feature = "web3"))] mod x402_tests; +#[cfg(feature = "web3")] pub use ops::{ handle_402, handle_402_and_pay, try_paid_request, X402Client, X402Error, X402PaymentResult, }; +#[cfg(feature = "web3")] pub use schemas::all_controller_schemas as all_x402_controller_schemas; +#[cfg(feature = "web3")] pub use schemas::all_registered_controllers as all_x402_registered_controllers; +#[cfg(feature = "web3")] pub use store::{init_global as init_ledger, PaymentRecord, PaymentStatus, SpendingBudget}; +#[cfg(feature = "web3")] pub use types::{ EvmAuthorization, EvmPaymentProof, PaymentChain, PaymentPayload, PaymentProof, PaymentRequired, PaymentRequirements, ResourceInfo, SettlementResponse, SolanaPaymentProof, }; + +// --------------------------------------------------------------------------- +// Disabled facade — compiled only when the `web3` feature is OFF. +// --------------------------------------------------------------------------- + +#[cfg(not(feature = "web3"))] +mod stub; +#[cfg(not(feature = "web3"))] +pub use stub::*; diff --git a/src/openhuman/x402/stub.rs b/src/openhuman/x402/stub.rs new file mode 100644 index 0000000000..88b184278c --- /dev/null +++ b/src/openhuman/x402/stub.rs @@ -0,0 +1,36 @@ +//! Disabled-x402 facade. +//! +//! Compiled only when the `web3` Cargo feature is OFF (see the gate in +//! [`super`]). Only three entry points have always-on callers: `init_ledger` +//! (`core/jsonrpc.rs` boot, itself runtime-gated on `DomainGroup::Web3`) and +//! the controller-registration pair (`core/all.rs`). The `X402RequestTool` +//! registration and the http_request 402-retry path are `#[cfg(feature = +//! "web3")]` at their call sites, so no other x402 surface is referenced when +//! off. +//! +//! Signatures MUST match the real ones exactly; the disabled build +//! (`cargo check --no-default-features --features tokenjuice-treesitter`) is +//! the only thing that catches drift. + +use std::path::Path; + +use crate::core::all::RegisteredController; +use crate::core::ControllerSchema; + +/// No-op: there is no spending ledger to initialise when x402 is compiled out. +/// Mirrors `store::init_global` (re-exported as `init_ledger`). The boot call +/// site is additionally runtime-gated on `DomainGroup::Web3`. +pub fn init_ledger(_workspace_dir: &Path, _session_id: &str) { + log::debug!("[x402-stub] init_ledger ignored (web3 disabled)"); +} + +/// No x402 controller schemas when the domain is compiled out. +pub fn all_x402_controller_schemas() -> Vec { + Vec::new() +} + +/// No x402 controllers are registered when the domain is compiled out — the +/// `openhuman.x402_*` RPCs become unknown-method. +pub fn all_x402_registered_controllers() -> Vec { + Vec::new() +} From 1ae8d9d8c8cd24e24afe85a224aed24c9df629be Mon Sep 17 00:00:00 2001 From: oxoxDev Date: Tue, 14 Jul 2026 15:36:11 +0530 Subject: [PATCH 05/14] feat(tools): gate wallet/x402 call sites on the web3 feature (#4802) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Concrete gated types can't be stubbed via return-empty, so the six Wallet*Tool + X402RequestTool registrations, the wallet::tools::* glob, and the http_request x402 402-retry path (+ its base64 import) are `#[cfg(feature = "web3")]` — mirroring the voice gate. With web3 off a 402 passes through unpaid. Polymarket's wallet-signing tests are gated too (they seed a real wallet); the tool itself compiles against the stub in both configs. --- src/openhuman/tools/impl/network/http_request.rs | 11 ++++++++++- src/openhuman/tools/impl/network/polymarket.rs | 8 +++++++- src/openhuman/tools/mod.rs | 1 + src/openhuman/tools/ops.rs | 12 +++++++++++- 4 files changed, 29 insertions(+), 3 deletions(-) diff --git a/src/openhuman/tools/impl/network/http_request.rs b/src/openhuman/tools/impl/network/http_request.rs index f133103ff6..7e03526c72 100644 --- a/src/openhuman/tools/impl/network/http_request.rs +++ b/src/openhuman/tools/impl/network/http_request.rs @@ -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; @@ -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, @@ -416,7 +423,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()) diff --git a/src/openhuman/tools/impl/network/polymarket.rs b/src/openhuman/tools/impl/network/polymarket.rs index 7a8fcfe88a..777fdb9b3e 100644 --- a/src/openhuman/tools/impl/network/polymarket.rs +++ b/src/openhuman/tools/impl/network/polymarket.rs @@ -1370,6 +1370,12 @@ fn parse_nonce_value(value: &Value) -> Option { } } -#[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; diff --git a/src/openhuman/tools/mod.rs b/src/openhuman/tools/mod.rs index 58aef6e4a6..1b18ff8b4b 100644 --- a/src/openhuman/tools/mod.rs +++ b/src/openhuman/tools/mod.rs @@ -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::*; diff --git a/src/openhuman/tools/ops.rs b/src/openhuman/tools/ops.rs index d96dac8688..11586252cb 100644 --- a/src/openhuman/tools/ops.rs +++ b/src/openhuman/tools/ops.rs @@ -328,11 +328,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())), @@ -747,7 +755,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(), )); From e8251ed23bc3e2861b58f3e657a8c7275e04d860 Mon Sep 17 00:00:00 2001 From: oxoxDev Date: Tue, 14 Jul 2026 15:36:11 +0530 Subject: [PATCH 06/14] test(web3): on/off controller-registration gate tests (#4802) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Asserts wallet/web3/x402 controllers + web3 agent tools are present in the default build and absent in the disabled build — the compile-time stub-facade correctness gate. --- src/core/all_tests.rs | 46 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/src/core/all_tests.rs b/src/core/all_tests.rs index 08412d4d26..af232c9131 100644 --- a/src/core/all_tests.rs +++ b/src/core/all_tests.rs @@ -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"); From 7a9cddc1ee2ba065b0eed49ae2d46d530a0ecd74 Mon Sep 17 00:00:00 2001 From: oxoxDev Date: Tue, 14 Jul 2026 15:36:27 +0530 Subject: [PATCH 07/14] docs(web3): document the web3 gate in AGENTS.md (#4802) Adds the web3 gate-table row (drops bitcoin + curve25519-dalek) and a facade note: graceful tinyplace/Polymarket degradation, the shared crypto deps kept always-on, and the two caller families still needing per-call cfg. --- AGENTS.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/AGENTS.md b/AGENTS.md index e4d86eb785..b32709c01a 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -231,11 +231,14 @@ 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` | **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. + ### Event bus (`src/core/event_bus/`) Typed pub/sub + native request/response. Both singletons — use module-level functions. From 9d240ca906c8bc302b915729813e5e6ac2f5ea6a Mon Sep 17 00:00:00 2001 From: oxoxDev Date: Tue, 14 Jul 2026 16:05:37 +0530 Subject: [PATCH 08/14] fix(app): forward web3 feature to the embedded core (#4802) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Tauri host consumes `openhuman_core` with `default-features = false`, so gating the wallet/web3/x402 domains behind the default-ON `web3` feature would drop them + their agent tools from the shipped desktop app. Forward `features = ["web3"]` to keep the desktop build byte-identical. Safe — web3 is default-ON; this only restores the pre-gate desktop surface. --- app/src-tauri/Cargo.toml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/app/src-tauri/Cargo.toml b/app/src-tauri/Cargo.toml index 5907d15b52..a2e85f6f79 100644 --- a/app/src-tauri/Cargo.toml +++ b/app/src-tauri/Cargo.toml @@ -132,7 +132,11 @@ cef = { version = "=146.4.1", default-features = false } # by tying the core's lifetime to the GUI process. The existing port-7788 # probe in `core_process::ensure_running` still attaches to a running # `openhuman-core run` harness when one is already listening. -openhuman_core = { path = "../..", package = "openhuman", default-features = false } +# `default-features = false` drops the core's default gates, so any domain +# moved behind a default-ON Cargo feature must be re-forwarded here or it +# silently vanishes from the shipped desktop app. `web3` (#4802) keeps the +# wallet/web3/x402 domains + their agent tools in the desktop build. +openhuman_core = { path = "../..", package = "openhuman", default-features = false, features = ["web3"] } tinyjuice = { version = "0.2.1", default-features = false } [target.'cfg(unix)'.dependencies] From 765e8d8b104ccc8e87038355a6cc87ae346af251 Mon Sep 17 00:00:00 2001 From: oxoxDev Date: Tue, 14 Jul 2026 16:26:50 +0530 Subject: [PATCH 09/14] feat(wallet): diagnostics + disabled-path unit tests for the wallet stub (#4802) Log entry on each disabled error path (secret_material, prepare_transfer, execute_prepared, tinyplace_signer_seed) so a slim build's degraded tinyplace/Polymarket flows are grep-visible, matching status(). Add a #[cfg(test)] module (compiled only when web3 is off) pinning the degraded contract: empty status, disabled errors, empty quote/endpoint lists, and empty controller registration. --- src/openhuman/wallet/stub.rs | 95 ++++++++++++++++++++++++++++++++++++ 1 file changed, 95 insertions(+) diff --git a/src/openhuman/wallet/stub.rs b/src/openhuman/wallet/stub.rs index 23031ea85e..f576002372 100644 --- a/src/openhuman/wallet/stub.rs +++ b/src/openhuman/wallet/stub.rs @@ -93,6 +93,9 @@ pub async fn status() -> Result, String> { /// Always errors: secret material cannot be produced with the wallet compiled /// out. Callers `?`-propagate (Polymarket writes surface the disabled error). pub(crate) async fn secret_material(_chain: WalletChain) -> Result { + log::debug!( + "[wallet-stub] secret_material requested (web3 disabled) — returning disabled error" + ); Err(DISABLED_MSG.to_string()) } @@ -143,6 +146,9 @@ pub struct ExecutionResult { pub async fn prepare_transfer( _params: PrepareTransferParams, ) -> Result, String> { + log::debug!( + "[wallet-stub] prepare_transfer requested (web3 disabled) — returning disabled error" + ); Err(DISABLED_MSG.to_string()) } @@ -150,6 +156,9 @@ pub async fn prepare_transfer( pub async fn execute_prepared( _params: ExecutePreparedParams, ) -> Result, String> { + log::debug!( + "[wallet-stub] execute_prepared requested (web3 disabled) — returning disabled error" + ); Err(DISABLED_MSG.to_string()) } @@ -161,6 +170,9 @@ pub fn prepared_quotes_for_test() -> Vec { /// Disabled: the tiny.place signer seed derives from the Solana wallet key, /// which does not exist. Callers `?`-propagate → "unlock wallet" prompt. pub(crate) async fn tinyplace_signer_seed() -> Result<[u8; 32], String> { + log::debug!( + "[wallet-stub] tinyplace_signer_seed requested (web3 disabled) — returning disabled error" + ); Err(DISABLED_MSG.to_string()) } @@ -253,3 +265,86 @@ pub fn all_wallet_registered_controllers() -> Vec { pub fn all_wallet_controller_schemas() -> Vec { Vec::new() } + +// This module is only compiled when the `web3` feature is OFF (see the +// `#[cfg(not(feature = "web3"))] mod stub;` gate in `super`), so a plain +// `#[cfg(test)]` here already runs only in the disabled build — it locks in the +// degraded contract that always-on callers (tinyplace payments, Polymarket +// writes) depend on. +#[cfg(test)] +mod tests { + use super::*; + + #[tokio::test] + async fn status_reports_no_accounts() { + let outcome = status().await.expect("stub status is always Ok"); + assert!( + outcome.value.accounts.is_empty(), + "disabled wallet must expose no accounts" + ); + } + + #[tokio::test] + async fn secret_material_is_disabled_error() { + // `WalletSecretMaterial` intentionally omits `Debug` (mirrors the real + // type, which never logs a mnemonic), so match rather than `expect_err`. + match secret_material(WalletChain::Evm).await { + Ok(_) => panic!("secret material must be unavailable when the wallet is compiled out"), + Err(msg) => assert_eq!(msg, DISABLED_MSG), + } + } + + #[tokio::test] + async fn prepare_transfer_is_disabled_error() { + let err = prepare_transfer(PrepareTransferParams { + chain: WalletChain::Solana, + to_address: "recipient".to_string(), + amount_raw: "1".to_string(), + asset_symbol: None, + evm_network: None, + }) + .await + .expect_err("no transfer can be prepared when the wallet is compiled out"); + assert_eq!(err, DISABLED_MSG); + } + + #[tokio::test] + async fn execute_prepared_is_disabled_error() { + let err = execute_prepared(ExecutePreparedParams { + quote_id: "quote".to_string(), + confirmed: true, + }) + .await + .expect_err("no prepared transfer can execute when the wallet is compiled out"); + assert_eq!(err, DISABLED_MSG); + } + + #[tokio::test] + async fn tinyplace_signer_seed_is_disabled_error() { + let err = tinyplace_signer_seed() + .await + .expect_err("no signer seed derives when the wallet is compiled out"); + assert_eq!(err, DISABLED_MSG); + } + + #[test] + fn prepared_quotes_and_rpc_endpoints_are_empty() { + assert!(prepared_quotes_for_test().is_empty()); + assert!(tinyplace_solana_rpc_endpoints().is_empty()); + } + + #[test] + fn solana_cluster_defaults_to_mainnet_with_stable_usdc_mint() { + assert_eq!(solana_cluster(), SolanaCluster::Mainnet); + assert_eq!( + SolanaCluster::Mainnet.usdc_mint(), + "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v" + ); + } + + #[test] + fn registration_entry_points_are_empty() { + assert!(all_wallet_registered_controllers().is_empty()); + assert!(all_wallet_controller_schemas().is_empty()); + } +} From 570983d49fdc5bbc6867f4095f37939a10814990 Mon Sep 17 00:00:00 2001 From: oxoxDev Date: Tue, 14 Jul 2026 16:27:29 +0530 Subject: [PATCH 10/14] test(web3): feature-off registration tests for the web3 + x402 stubs (#4802) Pin the disabled facades' contract: empty controller/schema registration (and empty web3 agent-tool list) plus x402 init_ledger as a callable no-op, so a slim build's empty surface is locked in against drift. --- src/openhuman/web3/stub.rs | 20 ++++++++++++++++++++ src/openhuman/x402/stub.rs | 22 ++++++++++++++++++++++ 2 files changed, 42 insertions(+) diff --git a/src/openhuman/web3/stub.rs b/src/openhuman/web3/stub.rs index 47ddc8417f..8e3201fde7 100644 --- a/src/openhuman/web3/stub.rs +++ b/src/openhuman/web3/stub.rs @@ -30,3 +30,23 @@ pub fn all_web3_registered_controllers() -> Vec { pub fn all_web3_agent_tools() -> Vec> { Vec::new() } + +// Compiled only in the disabled build (`#[cfg(not(feature = "web3"))] mod stub;` +// in `super`), so a plain `#[cfg(test)]` runs only when web3 is compiled out — +// it pins the empty controller/tool surface that `core/all.rs` + `tools/ops.rs` +// consume without per-call `#[cfg]`. +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn registration_entry_points_are_empty() { + assert!(all_web3_registered_controllers().is_empty()); + assert!(all_web3_controller_schemas().is_empty()); + } + + #[test] + fn agent_tools_are_absent() { + assert!(all_web3_agent_tools().is_empty()); + } +} diff --git a/src/openhuman/x402/stub.rs b/src/openhuman/x402/stub.rs index 88b184278c..3026c07d18 100644 --- a/src/openhuman/x402/stub.rs +++ b/src/openhuman/x402/stub.rs @@ -34,3 +34,25 @@ pub fn all_x402_controller_schemas() -> Vec { pub fn all_x402_registered_controllers() -> Vec { Vec::new() } + +// Compiled only in the disabled build (`#[cfg(not(feature = "web3"))] mod stub;` +// in `super`), so a plain `#[cfg(test)]` here runs only when x402 is compiled +// out — it pins the disabled facade's callable no-op + empty-registration +// contract that `core/jsonrpc.rs` boot and `core/all.rs` rely on. +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn init_ledger_is_callable_noop() { + // Must not panic — the boot path calls this unconditionally (itself + // runtime-gated on `DomainGroup::Web3`) even in a slim build. + init_ledger(Path::new("/tmp/openhuman-x402-stub-test"), "session-x"); + } + + #[test] + fn registration_entry_points_are_empty() { + assert!(all_x402_registered_controllers().is_empty()); + assert!(all_x402_controller_schemas().is_empty()); + } +} From 82ff383678c0397321e1b525db569733bc850ce4 Mon Sep 17 00:00:00 2001 From: oxoxDev Date: Tue, 14 Jul 2026 16:27:29 +0530 Subject: [PATCH 11/14] test(polymarket): assert writes degrade to disabled error with web3 off (#4802) The signing happy-path tests require a real wallet and stay web3-gated; add a feature-off counterpart proving wallet-signed writes surface the wallet-stub disabled error (via secret_material) instead of panicking or proceeding without a signer. --- .../tools/impl/network/polymarket.rs | 44 +++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/src/openhuman/tools/impl/network/polymarket.rs b/src/openhuman/tools/impl/network/polymarket.rs index 777fdb9b3e..4ab1279042 100644 --- a/src/openhuman/tools/impl/network/polymarket.rs +++ b/src/openhuman/tools/impl/network/polymarket.rs @@ -1379,3 +1379,47 @@ fn parse_nonce_value(value: &Value) -> Option { #[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("wallet secret material") || msg.contains("feature disabled"), + "expected a wallet-disabled error, got: {msg}" + ); + } +} From 3226ce26e555ec6b552e55158950394fe83ac1cf Mon Sep 17 00:00:00 2001 From: oxoxDev Date: Tue, 14 Jul 2026 18:45:55 +0530 Subject: [PATCH 12/14] test(polymarket): assert the exact compile-time-disabled marker (#4802) The disabled-wallet test matched `"wallet secret material"` (from the `.context()` wrapper, present regardless of failure cause) OR the loose `"feature disabled"`. Require the stable stub marker `"web3/wallet feature disabled at compile time"` exclusively so the test proves the disabled-stub path specifically. Per CodeRabbit review. --- src/openhuman/tools/impl/network/polymarket.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/openhuman/tools/impl/network/polymarket.rs b/src/openhuman/tools/impl/network/polymarket.rs index 4ab1279042..37cea06abc 100644 --- a/src/openhuman/tools/impl/network/polymarket.rs +++ b/src/openhuman/tools/impl/network/polymarket.rs @@ -1418,8 +1418,8 @@ mod tests_web3_disabled { .expect_err("wallet-signed writes must fail when web3 is compiled out"); let msg = format!("{err:#}"); assert!( - msg.contains("wallet secret material") || msg.contains("feature disabled"), - "expected a wallet-disabled error, got: {msg}" + msg.contains("web3/wallet feature disabled at compile time"), + "expected the stable compile-time-disabled marker, got: {msg}" ); } } From 35bb08dc4cf2f379e247124b14f82667be3a84da Mon Sep 17 00:00:00 2001 From: M3gA-Mind Date: Tue, 14 Jul 2026 22:49:57 +0530 Subject: [PATCH 13/14] test(agent): exercise deterministic cap checkpoint via empty wrap-up MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit max_iteration_checkpoint_uses_deterministic_fallback_and_hooks fed the wrap-up call a non-empty response (tool-call XML text) plus a streamed delta, so summarize_turn_wrapup returned that text as the checkpoint and the deterministic fallback the test is named for never ran — the (1 steps) assertion failed under the FULL instrumented suite. Make the wrap-up yield nothing (empty text, no deltas) so build_deterministic_checkpoint is the answer, and assert no iteration-2 wrap-up delta is streamed. Pre-existing drift (checkpoint-selection predates #4386); surfaced here because #4802's Cargo.toml change forces the full coverage suite. --- .../agent_session_round24_raw_coverage_e2e.rs | 27 ++++++++----------- 1 file changed, 11 insertions(+), 16 deletions(-) diff --git a/tests/raw_coverage/agent_session_round24_raw_coverage_e2e.rs b/tests/raw_coverage/agent_session_round24_raw_coverage_e2e.rs index 6865e8da0c..0e0d455b69 100644 --- a/tests/raw_coverage/agent_session_round24_raw_coverage_e2e.rs +++ b/tests/raw_coverage/agent_session_round24_raw_coverage_e2e.rs @@ -411,17 +411,13 @@ async fn max_iteration_checkpoint_uses_deterministic_fallback_and_hooks() { let calls = Arc::new(AtomicUsize::new(0)); let hook_calls = Arc::new(AtomicUsize::new(0)); let hook_contexts = Arc::new(Mutex::new(Vec::new())); + // The wrap-up model call yields nothing (empty text + no streamed deltas), so + // `summarize_turn_wrapup` returns an empty summary and the cap path falls back + // to `build_deterministic_checkpoint` — the exact path this test covers. (A + // non-empty wrap-up would instead be surfaced verbatim as the answer.) let provider = ScriptedProvider::with_stream( - vec![ - xml_tool_response("alpha"), - text_response( - "{\"name\":\"round24_echo\",\"arguments\":{\"value\":\"again\"}}", - None, - ), - ], - vec![ProviderDelta::TextDelta { - delta: "checkpoint delta".to_string(), - }], + vec![xml_tool_response("alpha"), text_response("", None)], + vec![], ); let mut agent = Agent::builder() @@ -454,7 +450,7 @@ async fn max_iteration_checkpoint_uses_deterministic_fallback_and_hooks() { let answer = agent.turn("hit the cap").await.unwrap(); assert!(answer.contains("I reached the tool-call limit for this turn (1 steps)")); - // The unified TurnEngine digest uses `- round24_echo [ok]: ...` format (no backticks). + // The deterministic checkpoint lists each executed tool, e.g. ``- `round24_echo` — ok``. assert!(answer.contains("round24_echo")); assert_eq!(calls.load(Ordering::SeqCst), 1); wait_for_hook_calls(&hook_calls, 1).await; @@ -480,12 +476,11 @@ async fn max_iteration_checkpoint_uses_deterministic_fallback_and_hooks() { while let Ok(event) = progress_rx.try_recv() { streamed.push(event); } - assert!(streamed.iter().any(|event| matches!( + // The wrap-up produced no text, so no iteration-2 wrap-up delta is streamed; + // the deterministic fallback (asserted above) becomes the answer instead. + assert!(!streamed.iter().any(|event| matches!( event, - openhuman_core::openhuman::agent::progress::AgentProgress::TextDelta { - delta, - iteration: 2 - } if delta == "checkpoint delta" + openhuman_core::openhuman::agent::progress::AgentProgress::TextDelta { iteration: 2, .. } ))); } From ae34b3d57009ae2d9cb5be98842c5d16caf22ab8 Mon Sep 17 00:00:00 2001 From: oxoxDev Date: Wed, 15 Jul 2026 15:09:01 +0530 Subject: [PATCH 14/14] test(core): gate the wallet group assertion behind the web3 feature (#4802) group_mapping_smoke asserted group_for_namespace("wallet") == Some(Web3) unconditionally. group_for_namespace resolves against the live controller registry, so with the web3 gate off the wallet controllers are unregistered and the lookup returns None, failing the test in the disabled config. The CI feature-gate-smoke lane only runs `cargo check`, which never compiles test code, so this could not surface there. Verified: `cargo test --lib --no-default-features --features tokenjuice-treesitter core::all::tests` no longer fails on the wallet assertion. The two remaining failures in that config assert the voice namespace and pre-date this branch (#4803's gate); they are out of scope here. --- src/core/all_tests.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/core/all_tests.rs b/src/core/all_tests.rs index af232c9131..b8980b2f33 100644 --- a/src/core/all_tests.rs +++ b/src/core/all_tests.rs @@ -907,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).