Skip to content

Commit a35ba10

Browse files
feat(channels): expose WhatsApp Web data to agent via structured RPC API (tinyhumansai#1308)
Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent f83aa94 commit a35ba10

13 files changed

Lines changed: 2763 additions & 124 deletions

File tree

app/src-tauri/src/whatsapp_scanner/mod.rs

Lines changed: 850 additions & 123 deletions
Large diffs are not rendered by default.

src/core/all.rs

Lines changed: 49 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,11 @@ impl RegisteredController {
4949
/// The global static registry of all controllers, initialized once on first access.
5050
static REGISTRY: OnceLock<Vec<RegisteredController>> = OnceLock::new();
5151

52+
/// Internal-only controllers: registered for RPC dispatch but NOT in the agent-facing
53+
/// schema catalog. These handlers are callable by trusted callers (e.g. the Tauri scanner)
54+
/// but should not be advertised to agents via tool listings or schema discovery.
55+
static INTERNAL_REGISTRY: OnceLock<Vec<RegisteredController>> = OnceLock::new();
56+
5257
/// The global static registry of standalone CLI adapters.
5358
static CLI_ADAPTERS: OnceLock<Vec<RegisteredCliAdapter>> = OnceLock::new();
5459

@@ -69,6 +74,16 @@ fn registry() -> &'static [RegisteredController] {
6974
.as_slice()
7075
}
7176

77+
/// Returns a reference to the internal-only controller registry.
78+
///
79+
/// These controllers are callable over RPC but are NOT included in agent tool listings
80+
/// or schema discovery endpoints.
81+
fn internal_registry() -> &'static [RegisteredController] {
82+
INTERNAL_REGISTRY
83+
.get_or_init(build_internal_only_controllers)
84+
.as_slice()
85+
}
86+
7287
/// Returns a reference to the global CLI adapter registry.
7388
fn cli_adapters() -> &'static [RegisteredCliAdapter] {
7489
CLI_ADAPTERS.get_or_init(|| {
@@ -192,6 +207,21 @@ fn build_registered_controllers() -> Vec<RegisteredController> {
192207
);
193208
// Integration notification ingest, triage, and per-provider settings
194209
controllers.extend(crate::openhuman::notifications::all_notifications_registered_controllers());
210+
// Structured WhatsApp Web data — agent-facing read-only controllers (list/search).
211+
// The write-path ingest controller is registered separately in build_internal_only_controllers.
212+
controllers.extend(crate::openhuman::whatsapp_data::all_whatsapp_data_registered_controllers());
213+
controllers
214+
}
215+
216+
/// Aggregates controllers that are registered for RPC routing but NOT exposed to agents.
217+
///
218+
/// These are write-path or internal-only handlers callable by trusted callers
219+
/// (e.g. the Tauri scanner ingest path) that should not appear in agent tool listings.
220+
fn build_internal_only_controllers() -> Vec<RegisteredController> {
221+
let mut controllers = Vec::new();
222+
// whatsapp_data ingest: scanner-side write path. Callable over RPC by the
223+
// Tauri scanner but excluded from agent-facing schema discovery.
224+
controllers.extend(crate::openhuman::whatsapp_data::all_whatsapp_data_internal_controllers());
195225
controllers
196226
}
197227

@@ -255,6 +285,8 @@ fn build_declared_controller_schemas() -> Vec<ControllerSchema> {
255285
);
256286
// Integration notification ingest, triage, and per-provider settings
257287
schemas.extend(crate::openhuman::notifications::all_notifications_controller_schemas());
288+
// Structured WhatsApp Web data — local SQLite store, agent-queryable
289+
schemas.extend(crate::openhuman::whatsapp_data::all_whatsapp_data_controller_schemas());
258290
schemas
259291
}
260292

@@ -340,6 +372,9 @@ pub fn namespace_description(namespace: &str) -> Option<&'static str> {
340372
"Integration notification ingest, triage scoring, listing, read-state, \
341373
and per-provider routing settings.",
342374
),
375+
"whatsapp_data" => Some(
376+
"Structured WhatsApp conversation and message store — list chats, read messages, and search across WhatsApp Web data.",
377+
),
343378
_ => None,
344379
}
345380
}
@@ -361,9 +396,13 @@ pub fn rpc_method_from_parts(namespace: &str, function: &str) -> Option<String>
361396
}
362397

363398
/// Retrieves the schema for a specific RPC method.
399+
///
400+
/// Checks both the agent-facing registry and the internal registry so that
401+
/// parameter validation still applies to internal-only methods (e.g. ingest).
364402
pub fn schema_for_rpc_method(method: &str) -> Option<ControllerSchema> {
365403
registry()
366404
.iter()
405+
.chain(internal_registry().iter())
367406
.find(|r| r.rpc_method_name() == method)
368407
.map(|r| r.schema.clone())
369408
}
@@ -400,7 +439,11 @@ pub fn validate_params(
400439

401440
/// Attempts to invoke a registered RPC method by name.
402441
///
403-
/// Returns `None` if the method is not found in the registry.
442+
/// Checks both the agent-facing controller registry and the internal-only registry,
443+
/// so scanner-side write paths (e.g. `openhuman.whatsapp_data_ingest`) are routable
444+
/// even though they are not included in agent tool listings.
445+
///
446+
/// Returns `None` if the method is not found in either registry.
404447
pub async fn try_invoke_registered_rpc(
405448
method: &str,
406449
params: Map<String, Value>,
@@ -410,6 +453,11 @@ pub async fn try_invoke_registered_rpc(
410453
return Some((controller.handler)(params).await);
411454
}
412455
}
456+
for controller in internal_registry() {
457+
if controller.rpc_method_name() == method {
458+
return Some((controller.handler)(params).await);
459+
}
460+
}
413461
None
414462
}
415463

src/core/jsonrpc.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -674,6 +674,15 @@ async fn run_server_inner(
674674
),
675675
Err(e) => log::warn!("[boot] memory::global init failed: {e}"),
676676
}
677+
// Initialize the WhatsApp data store so scanner ingest calls
678+
// can write data without requiring a lazy-init fallback.
679+
match crate::openhuman::whatsapp_data::global::init(cfg.workspace_dir.clone()) {
680+
Ok(_) => log::info!(
681+
"[boot] whatsapp_data::global initialized (workspace={})",
682+
cfg.workspace_dir.display()
683+
),
684+
Err(e) => log::warn!("[boot] whatsapp_data::global init failed: {e}"),
685+
}
677686
}
678687

679688
let (resolved_port, port_source) = match port {

src/openhuman/about_app/catalog.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -686,6 +686,16 @@ const CAPABILITIES: &[Capability] = &[
686686
status: CapabilityStatus::Beta,
687687
privacy: None,
688688
},
689+
Capability {
690+
id: "channels.whatsapp_read_messages",
691+
name: "Read WhatsApp Messages",
692+
domain: "channels",
693+
category: CapabilityCategory::Channels,
694+
description: "Read and search WhatsApp Web conversations and messages after connecting WhatsApp in OpenHuman. Data is stored locally only and never transmitted.",
695+
how_to: "Connect WhatsApp Web via Channels, then ask the agent to read or summarise your messages.",
696+
status: CapabilityStatus::Beta,
697+
privacy: LOCAL_RAW,
698+
},
689699
Capability {
690700
id: "settings.configure_ai",
691701
name: "Configure AI",

src/openhuman/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,4 +70,5 @@ pub mod webhooks;
7070
pub mod webview_accounts;
7171
pub mod webview_apis;
7272
pub mod webview_notifications;
73+
pub mod whatsapp_data;
7374
pub mod workspace;
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
//! Process-global WhatsApp data store singleton.
2+
//!
3+
//! One `WhatsAppDataStore` lives for the entire core process, shared by RPC
4+
//! handlers and any other subsystem that needs it.
5+
//!
6+
//! # Usage
7+
//!
8+
//! ```ignore
9+
//! // At startup:
10+
//! whatsapp_data::global::init(workspace_dir)?;
11+
//!
12+
//! // In RPC handlers:
13+
//! let store = whatsapp_data::global::store()?;
14+
//! ```
15+
16+
use std::path::PathBuf;
17+
use std::sync::{Arc, OnceLock};
18+
19+
use crate::openhuman::whatsapp_data::store::WhatsAppDataStore;
20+
21+
/// Shared, thread-safe reference to the store.
22+
pub type WhatsAppDataStoreRef = Arc<WhatsAppDataStore>;
23+
24+
static GLOBAL_STORE: OnceLock<WhatsAppDataStoreRef> = OnceLock::new();
25+
26+
/// Initialise the global store from a workspace directory. Idempotent —
27+
/// only the first call has any effect; subsequent calls return the existing
28+
/// instance.
29+
pub fn init(workspace_dir: PathBuf) -> Result<WhatsAppDataStoreRef, String> {
30+
if let Some(existing) = GLOBAL_STORE.get() {
31+
log::debug!("[whatsapp_data:global] already initialised");
32+
return Ok(Arc::clone(existing));
33+
}
34+
log::info!(
35+
"[whatsapp_data:global] initialising store workspace={}",
36+
workspace_dir.display()
37+
);
38+
let store = Arc::new(
39+
WhatsAppDataStore::new(&workspace_dir)
40+
.map_err(|e| format!("[whatsapp_data] store init failed: {e}"))?,
41+
);
42+
let _ = GLOBAL_STORE.set(Arc::clone(&store));
43+
Ok(GLOBAL_STORE.get().cloned().unwrap_or(store))
44+
}
45+
46+
/// Return the global store. Errors if [`init`] has not been called yet.
47+
pub fn store() -> Result<WhatsAppDataStoreRef, String> {
48+
GLOBAL_STORE.get().cloned().ok_or_else(|| {
49+
"whatsapp_data global store accessed before init — call init(workspace) at startup"
50+
.to_string()
51+
})
52+
}
53+
54+
/// Return the global store if already initialised, without error.
55+
pub fn store_if_ready() -> Option<WhatsAppDataStoreRef> {
56+
GLOBAL_STORE.get().cloned()
57+
}

src/openhuman/whatsapp_data/mod.rs

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
//! Structured WhatsApp Web data — local-only SQLite persistence and agent API.
2+
//!
3+
//! This domain stores WhatsApp chats and messages scraped by the Tauri
4+
//! `whatsapp_scanner` via CDP, making them queryable by the agent through
5+
//! the JSON-RPC controller surface.
6+
//!
7+
//! **Data locality**: all data remains on-device in `whatsapp_data.db`; it is
8+
//! never transmitted to any external service.
9+
//!
10+
//! ## Agent-facing RPC methods (read-only)
11+
//! - `openhuman.whatsapp_data_list_chats`
12+
//! - `openhuman.whatsapp_data_list_messages`
13+
//! - `openhuman.whatsapp_data_search_messages`
14+
//!
15+
//! ## Internal-only RPC method (write, scanner-side)
16+
//! - `openhuman.whatsapp_data_ingest` — NOT exposed via agent tool listings
17+
18+
pub mod global;
19+
pub mod ops;
20+
pub mod rpc;
21+
mod schemas;
22+
pub mod store;
23+
pub mod types;
24+
25+
pub use schemas::{
26+
all_controller_schemas as all_whatsapp_data_controller_schemas,
27+
all_internal_controllers as all_whatsapp_data_internal_controllers,
28+
all_registered_controllers as all_whatsapp_data_registered_controllers,
29+
};

0 commit comments

Comments
 (0)