From 6d220a7b2abd48f4b387799c95473c896b08fa53 Mon Sep 17 00:00:00 2001 From: YOMXXX Date: Mon, 22 Jun 2026 04:27:29 +0800 Subject: [PATCH] fix(rpc): handle legacy namespaced method calls (#3875) --- app/src/services/__tests__/rpcMethods.test.ts | 19 ++++++- app/src/services/rpcMethods.ts | 4 ++ src/core/dispatch.rs | 56 +++++++++++++------ src/core/legacy_aliases.rs | 14 +++++ 4 files changed, 76 insertions(+), 17 deletions(-) diff --git a/app/src/services/__tests__/rpcMethods.test.ts b/app/src/services/__tests__/rpcMethods.test.ts index 10ee46b39..51604eefa 100644 --- a/app/src/services/__tests__/rpcMethods.test.ts +++ b/app/src/services/__tests__/rpcMethods.test.ts @@ -113,6 +113,17 @@ describe('rpcMethods catalog', () => { }); }); + describe('channels legacy alias resolution (Sentry OPENHUMAN-CORE-1Y / OPENHUMAN-CORE-1Z)', () => { + test('dotted channel list aliases resolve to channels_list', () => { + expect(normalizeRpcMethod('channels.list')).toBe(CORE_RPC_METHODS.channelsList); + expect(normalizeRpcMethod('openhuman.channels.list')).toBe(CORE_RPC_METHODS.channelsList); + }); + + test('canonical channels_list passes through unchanged', () => { + expect(normalizeRpcMethod('openhuman.channels_list')).toBe('openhuman.channels_list'); + }); + }); + test('catalog canonical methods exist in core schema registry (drift guard)', () => { const schemaSources = [ fs.readFileSync( @@ -147,6 +158,10 @@ describe('rpcMethods catalog', () => { path.resolve(__dirname, '../../../../src/openhuman/health/schemas.rs'), 'utf8' ), + fs.readFileSync( + path.resolve(__dirname, '../../../../src/openhuman/channels/controllers/schemas.rs'), + 'utf8' + ), ].join('\n'); for (const method of Object.values(CORE_RPC_METHODS)) { @@ -165,7 +180,9 @@ describe('rpcMethods catalog', () => { ? 'mcp_clients' : methodRoot.startsWith('health_') ? 'health' - : 'config'; + : methodRoot.startsWith('channels_') + ? 'channels' + : 'config'; const fnName = methodRoot.slice(`${namespace}_`.length); expect(schemaSources).toContain(`namespace: "${namespace}"`); expect(schemaSources).toContain(`function: "${fnName}"`); diff --git a/app/src/services/rpcMethods.ts b/app/src/services/rpcMethods.ts index f149952b7..44f2ab8ba 100644 --- a/app/src/services/rpcMethods.ts +++ b/app/src/services/rpcMethods.ts @@ -57,6 +57,7 @@ export const CORE_RPC_METHODS = { embeddingsClearApiKey: 'openhuman.embeddings_clear_api_key', embeddingsEmbed: 'openhuman.embeddings_embed', embeddingsTestConnection: 'openhuman.embeddings_test_connection', + channelsList: 'openhuman.channels_list', mcpClientsInstalledList: 'openhuman.mcp_clients_installed_list', mcpClientsToolCall: 'openhuman.mcp_clients_tool_call', healthSnapshot: 'openhuman.health_snapshot', @@ -66,9 +67,12 @@ export const CORE_RPC_METHODS = { export type CoreRpcMethod = (typeof CORE_RPC_METHODS)[keyof typeof CORE_RPC_METHODS]; export const LEGACY_METHOD_ALIASES: Record = { + // #3565: old desktop clients used dotted namespace/function channel calls. + 'channels.list': CORE_RPC_METHODS.channelsList, // MCP clients — old method names that appeared in Sentry (CORE-RUST-DR/DS/DT/DV/DW). // See src/core/legacy_aliases.rs for the Rust-side mirror of this table. 'mcp_clients.list': CORE_RPC_METHODS.mcpClientsInstalledList, + 'openhuman.channels.list': CORE_RPC_METHODS.channelsList, 'openhuman.mcp_clients_list': CORE_RPC_METHODS.mcpClientsInstalledList, 'openhuman.mcp_list': CORE_RPC_METHODS.mcpClientsInstalledList, 'openhuman.mcp_servers_list': CORE_RPC_METHODS.mcpClientsInstalledList, diff --git a/src/core/dispatch.rs b/src/core/dispatch.rs index 8392d9408..728d9778a 100644 --- a/src/core/dispatch.rs +++ b/src/core/dispatch.rs @@ -85,14 +85,15 @@ pub async fn dispatch( // Tier 4: unrecognised method. The JSON-RPC response is unchanged — the // caller still receives a method-not-found error. Only the *severity* of // how the transport layer records it differs by class (see - // `jsonrpc::rpc_handler`): known external probes (`is_known_probe_method`) - // are debug-only and never reach Sentry (#3567), while any other unknown - // method is downgraded to a warn-level capture (recorded for triage, no - // page) instead of an error event. Log here at debug with the method name - // so the path stays diagnosable without re-creating the Sentry noise. + // `jsonrpc::rpc_handler`): known non-actionable misses + // (`is_known_probe_method`) are debug-only and never reach Sentry + // (#3567, #3565), while any other unknown method is downgraded to a + // warn-level capture (recorded for triage, no page) instead of an error + // event. Log here at debug with the method name so the path stays + // diagnosable without re-creating the Sentry noise. if is_known_probe_method(method) { log::debug!( - "[rpc] unknown_method method={} class=known_probe (debug-only; not reported to Sentry)", + "[rpc] unknown_method method={} class=debug_only_known_miss (not reported to Sentry)", method ); } else { @@ -109,23 +110,28 @@ pub async fn dispatch( /// classifier ([`unknown_method_name`]) cannot drift apart. pub const UNKNOWN_METHOD_PREFIX: &str = "unknown method: "; -/// Generic external probe / legacy method names that are never real RPC -/// methods and never will be (issue #3567). Infra health-checks and JSON-RPC -/// introspection clients poll these — `rpc.discover` (JSON-RPC service -/// discovery), `list_methods`, liveness `status`, `auth.status`, `config/get` -/// — and each miss previously produced a recurring Sentry ERROR event with -/// zero user impact. The transport layer keeps these debug-only (never -/// captured). The matching health-method *aliases* land separately in -/// `legacy_aliases` (#3566), which depends on this severity change. +/// Known non-actionable unknown method names that are debug-only. +/// +/// Most entries are generic external probes that are never real RPC methods +/// and never will be (issue #3567): `rpc.discover` (JSON-RPC service +/// discovery), `list_methods`, liveness `status`, `auth.status`, `config/get`. +/// This also covers retired feature calls from older clients when no safe +/// canonical handler exists (#3565: `openhuman.memory_tree_create_namespace`). +/// +/// Each miss previously produced recurring Sentry ERROR events with zero user +/// impact. The transport layer keeps these debug-only (never captured). The +/// matching health-method *aliases* land separately in `legacy_aliases` +/// (#3566), which depends on this severity change. const KNOWN_PROBE_METHODS: &[&str] = &[ "rpc.discover", "list_methods", "status", "auth.status", "config/get", + "openhuman.memory_tree_create_namespace", ]; -/// Returns `true` when `method` is a known external probe / legacy health name +/// Returns `true` when `method` is a known non-actionable unknown method name /// from [`KNOWN_PROBE_METHODS`]. Matched against the *resolved* method name /// (after legacy-alias rewrite), i.e. the name embedded in the /// [`UNKNOWN_METHOD_PREFIX`] error string. @@ -378,8 +384,12 @@ mod tests { "status", "auth.status", "config/get", + "openhuman.memory_tree_create_namespace", ] { - assert!(is_known_probe_method(m), "{m} should be a known probe"); + assert!( + is_known_probe_method(m), + "{m} should be a debug-only known miss" + ); } // Genuinely-unknown methods and near-misses are NOT allow-listed, so // they stay on the warn-for-triage path rather than being silenced. @@ -387,9 +397,23 @@ mod tests { assert!(!is_known_probe_method("core.not_a_real_method")); assert!(!is_known_probe_method("Status")); // case-sensitive assert!(!is_known_probe_method("rpc.discover.extra")); // exact match only + assert!(!is_known_probe_method("memory_tree_create_namespace")); assert!(!is_known_probe_method("")); } + #[tokio::test] + async fn dispatch_dotted_channel_list_aliases_route_to_registry() { + for method in ["channels.list", "openhuman.channels.list"] { + let out = dispatch(test_state(), method, json!({})) + .await + .unwrap_or_else(|err| panic!("{method} should route via channels_list: {err}")); + assert!( + out.is_array() || out.get("result").is_some(), + "expected {method} to return the channels_list payload, got {out}" + ); + } + } + #[test] fn unknown_method_name_extracts_from_error_string_only() { // The classifier round-trips the exact string `dispatch` emits. diff --git a/src/core/legacy_aliases.rs b/src/core/legacy_aliases.rs index 4ab927811..30fe25ad5 100644 --- a/src/core/legacy_aliases.rs +++ b/src/core/legacy_aliases.rs @@ -21,6 +21,10 @@ /// Order doesn't matter for correctness, but is kept alphabetical by legacy /// key for easier diffing against the frontend table. const LEGACY_ALIASES: &[(&str, &str)] = &[ + // #3565: old desktop clients called the channels controller with a dotted + // namespace/function spelling before the canonical + // `openhuman._` form was established. + ("channels.list", "openhuman.channels_list"), // MCP clients — old method names that appeared in Sentry (CORE-RUST-DR/DS/DT/DV/DW). // Callers used dotted namespace, bare `mcp_list`, `mcp_servers_list`, and // `mcp_clients_list` before the canonical `mcp_clients_installed_list` was @@ -28,6 +32,7 @@ const LEGACY_ALIASES: &[(&str, &str)] = &[ // `mcp_clients_tool_call` that shipped in at least one older bundle. // `mcp_clients.list` sorts before all `openhuman.*` entries (m < o). ("mcp_clients.list", "openhuman.mcp_clients_installed_list"), + ("openhuman.channels.list", "openhuman.channels_list"), ( "openhuman.get_analytics_settings", "openhuman.config_get_analytics_settings", @@ -563,6 +568,15 @@ mod tests { assert_eq!(out, "core.ping"); } + #[test] + fn resolve_legacy_rewrites_dotted_channel_list_aliases() { + assert_eq!(resolve_legacy("channels.list"), "openhuman.channels_list"); + assert_eq!( + resolve_legacy("openhuman.channels.list"), + "openhuman.channels_list" + ); + } + #[test] fn frontend_core_rpc_methods_exist_in_core_schema_registry() { let source = read_frontend_rpc_catalog();