From d80c33046a2dab0c883621f9a61126caa90fef5a Mon Sep 17 00:00:00 2001 From: "cyrus@tinyhumans.ai" Date: Fri, 29 May 2026 03:19:42 +0530 Subject: [PATCH] fix(rpc): register openhuman.system_info handler and add legacy alias (Sentry CORE-RUST-G0, closes #2871) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Adds `openhuman.health_system_info` controller to the `health` domain, returning app version, OS, architecture, and PID via `std::env::consts` and `CARGO_PKG_VERSION`. - Adds legacy alias `openhuman.system_info` → `openhuman.health_system_info` to both `src/core/legacy_aliases.rs` and `app/src/services/rpcMethods.ts` so older clients resolve without error. - Adds `healthSystemInfo: 'openhuman.health_system_info'` to `CORE_RPC_METHODS`. - Unit tests: `handle_system_info_returns_json_object` in `health/schemas.rs`; `system_info_returns_non_empty_version/os/pid` in `health/ops.rs`; `resolve_legacy_rewrites_system_info` in `legacy_aliases.rs`; targeted alias test in `rpcMethods.test.ts`. --- app/src/services/__tests__/rpcMethods.test.ts | 18 ++++ app/src/services/rpcMethods.ts | 5 ++ src/core/legacy_aliases.rs | 16 ++++ src/openhuman/health/ops.rs | 82 +++++++++++++++++ src/openhuman/health/schemas.rs | 88 ++++++++++++++++--- 5 files changed, 199 insertions(+), 10 deletions(-) diff --git a/app/src/services/__tests__/rpcMethods.test.ts b/app/src/services/__tests__/rpcMethods.test.ts index 54d266f3b2..3ea39ea61d 100644 --- a/app/src/services/__tests__/rpcMethods.test.ts +++ b/app/src/services/__tests__/rpcMethods.test.ts @@ -95,6 +95,24 @@ describe('rpcMethods catalog', () => { }); }); + describe('health legacy alias resolution (Sentry CORE-RUST-FG / CORE-RUST-G0)', () => { + test('health_snapshot resolves to openhuman.health_snapshot', () => { + expect(normalizeRpcMethod('health_snapshot')).toBe(CORE_RPC_METHODS.healthSnapshot); + }); + + test('openhuman.system_info resolves to openhuman.health_system_info (Sentry CORE-RUST-G0)', () => { + // Older clients called openhuman.system_info before the method was + // namespaced under health as openhuman.health_system_info. + expect(normalizeRpcMethod('openhuman.system_info')).toBe(CORE_RPC_METHODS.healthSystemInfo); + }); + + test('canonical health_system_info passes through unchanged', () => { + expect(normalizeRpcMethod('openhuman.health_system_info')).toBe( + 'openhuman.health_system_info' + ); + }); + }); + test('catalog canonical methods exist in core schema registry (drift guard)', () => { const schemaSources = [ fs.readFileSync( diff --git a/app/src/services/rpcMethods.ts b/app/src/services/rpcMethods.ts index 0086744b9e..1f0a592afd 100644 --- a/app/src/services/rpcMethods.ts +++ b/app/src/services/rpcMethods.ts @@ -38,6 +38,7 @@ export const CORE_RPC_METHODS = { mcpClientsInstalledList: 'openhuman.mcp_clients_installed_list', mcpClientsToolCall: 'openhuman.mcp_clients_tool_call', healthSnapshot: 'openhuman.health_snapshot', + healthSystemInfo: 'openhuman.health_system_info', } as const; export type CoreRpcMethod = (typeof CORE_RPC_METHODS)[keyof typeof CORE_RPC_METHODS]; @@ -77,6 +78,10 @@ export const LEGACY_METHOD_ALIASES: Record = { 'openhuman.providers_list_models': CORE_RPC_METHODS.inferenceListModels, 'openhuman.inference_embed': CORE_RPC_METHODS.embeddingsEmbed, health_snapshot: CORE_RPC_METHODS.healthSnapshot, + // `openhuman.system_info` was used by older clients / SDK callers before the + // method was namespaced as `openhuman.health_system_info`. + // Sentry CORE-RUST-G0 — https://sentry.tinyhumans.ai/organizations/tinyhumans/issues/6340/ + 'openhuman.system_info': CORE_RPC_METHODS.healthSystemInfo, }; export function normalizeRpcMethod(method: string): string { diff --git a/src/core/legacy_aliases.rs b/src/core/legacy_aliases.rs index fcc605b59b..832bb77ac5 100644 --- a/src/core/legacy_aliases.rs +++ b/src/core/legacy_aliases.rs @@ -118,6 +118,10 @@ const LEGACY_ALIASES: &[(&str, &str)] = &[ // bare `health_snapshot` (no namespace prefix) was used by older clients // before the canonical `openhuman.health_snapshot` form was established. ("health_snapshot", "openhuman.health_snapshot"), + // `openhuman.system_info` was used by older clients / SDK callers before + // the method was namespaced under `health` as `openhuman.health_system_info`. + // Sentry CORE-RUST-G0 — https://sentry.tinyhumans.ai/organizations/tinyhumans/issues/6340/ + ("openhuman.system_info", "openhuman.health_system_info"), ("openhuman.inference_embed", "openhuman.embeddings_embed"), ("openhuman.local_ai_presets", "openhuman.inference_presets"), ( @@ -422,6 +426,18 @@ mod tests { ); } + #[test] + fn resolve_legacy_rewrites_system_info() { + // Sentry CORE-RUST-G0: older clients called `openhuman.system_info` + // before the method was namespaced under `health` as + // `openhuman.health_system_info`. The alias table must rewrite it so + // the call resolves against the registered controller. + assert_eq!( + resolve_legacy("openhuman.system_info"), + "openhuman.health_system_info", + ); + } + #[test] fn resolve_legacy_passes_through_unknown_methods() { assert_eq!( diff --git a/src/openhuman/health/ops.rs b/src/openhuman/health/ops.rs index 2e6177a931..0624cb3acd 100644 --- a/src/openhuman/health/ops.rs +++ b/src/openhuman/health/ops.rs @@ -1,8 +1,90 @@ //! JSON-RPC / CLI controller surface for the process health registry. +use serde::Serialize; + use crate::openhuman::health; use crate::rpc::RpcOutcome; pub fn health_snapshot() -> RpcOutcome { RpcOutcome::single_log(health::snapshot_json(), "health_snapshot requested") } + +/// Static system information returned by `openhuman.health_system_info`. +#[derive(Debug, Serialize)] +pub struct SystemInfo { + /// Cargo package version of the running core binary. + pub version: &'static str, + /// Target operating system name (`linux`, `macos`, `windows`, …). + pub os: &'static str, + /// Target CPU architecture (`x86_64`, `aarch64`, …). + pub arch: &'static str, + /// Current process ID. + pub pid: u32, +} + +/// Returns static system information: version, OS, architecture, and PID. +/// +/// This is the handler backing the `openhuman.health_system_info` RPC method +/// (legacy callers may send `openhuman.system_info`, which the alias table +/// rewrites before dispatch). +pub fn system_info() -> RpcOutcome { + let info = SystemInfo { + version: env!("CARGO_PKG_VERSION"), + os: std::env::consts::OS, + arch: std::env::consts::ARCH, + pid: std::process::id(), + }; + tracing::debug!( + version = info.version, + os = info.os, + arch = info.arch, + pid = info.pid, + "[health] system_info requested" + ); + RpcOutcome::single_log(info, "system_info requested") +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn system_info_returns_non_empty_version() { + let outcome = system_info(); + let json = outcome + .into_cli_compatible_json() + .expect("serialization ok"); + let version = json["version"].as_str().expect("version is a string"); + assert!(!version.is_empty(), "version must be non-empty"); + } + + #[test] + fn system_info_returns_known_os() { + let outcome = system_info(); + let json = outcome + .into_cli_compatible_json() + .expect("serialization ok"); + let os = json["os"].as_str().expect("os is a string"); + // std::env::consts::OS is always one of the compile-time Rust target OS names. + assert!(!os.is_empty(), "os must be non-empty"); + } + + #[test] + fn system_info_returns_non_zero_pid() { + let outcome = system_info(); + let json = outcome + .into_cli_compatible_json() + .expect("serialization ok"); + let pid = json["pid"].as_u64().expect("pid is a u64"); + assert!(pid > 0, "pid must be greater than zero"); + } + + #[test] + fn health_snapshot_returns_serializable_value() { + let outcome = health_snapshot(); + let json = outcome + .into_cli_compatible_json() + .expect("serialization ok"); + assert!(json.is_object(), "snapshot must be a JSON object"); + } +} diff --git a/src/openhuman/health/schemas.rs b/src/openhuman/health/schemas.rs index 4ad769b654..435b31f7d8 100644 --- a/src/openhuman/health/schemas.rs +++ b/src/openhuman/health/schemas.rs @@ -5,14 +5,20 @@ use crate::core::{ControllerSchema, FieldSchema, TypeSchema}; use crate::rpc::RpcOutcome; pub fn all_controller_schemas() -> Vec { - vec![schemas("snapshot")] + vec![schemas("snapshot"), schemas("system_info")] } pub fn all_registered_controllers() -> Vec { - vec![RegisteredController { - schema: schemas("snapshot"), - handler: handle_snapshot, - }] + vec![ + RegisteredController { + schema: schemas("snapshot"), + handler: handle_snapshot, + }, + RegisteredController { + schema: schemas("system_info"), + handler: handle_system_info, + }, + ] } pub fn schemas(function: &str) -> ControllerSchema { @@ -29,6 +35,39 @@ pub fn schemas(function: &str) -> ControllerSchema { required: true, }], }, + "system_info" => ControllerSchema { + namespace: "health", + function: "system_info", + description: + "Return static system information: app version, OS, architecture, and PID.", + inputs: vec![], + outputs: vec![ + FieldSchema { + name: "version", + ty: TypeSchema::String, + comment: "Running core binary version (CARGO_PKG_VERSION).", + required: true, + }, + FieldSchema { + name: "os", + ty: TypeSchema::String, + comment: "Host operating system name (linux, macos, windows, …).", + required: true, + }, + FieldSchema { + name: "arch", + ty: TypeSchema::String, + comment: "CPU architecture (x86_64, aarch64, …).", + required: true, + }, + FieldSchema { + name: "pid", + ty: TypeSchema::String, + comment: "Current process ID.", + required: true, + }, + ], + }, _ => ControllerSchema { namespace: "health", function: "unknown", @@ -48,6 +87,10 @@ fn handle_snapshot(_params: Map) -> ControllerFuture { Box::pin(async { to_json(crate::openhuman::health::rpc::health_snapshot()) }) } +fn handle_system_info(_params: Map) -> ControllerFuture { + Box::pin(async { to_json(crate::openhuman::health::rpc::system_info()) }) +} + fn to_json(outcome: RpcOutcome) -> Result { outcome.into_cli_compatible_json() } @@ -57,13 +100,13 @@ mod tests { use super::*; #[test] - fn all_schemas_returns_one() { - assert_eq!(all_controller_schemas().len(), 1); + fn all_schemas_returns_two() { + assert_eq!(all_controller_schemas().len(), 2); } #[test] - fn all_controllers_returns_one() { - assert_eq!(all_registered_controllers().len(), 1); + fn all_controllers_returns_two() { + assert_eq!(all_registered_controllers().len(), 2); } #[test] @@ -75,6 +118,16 @@ mod tests { assert!(!s.outputs.is_empty()); } + #[test] + fn system_info_schema() { + let s = schemas("system_info"); + assert_eq!(s.namespace, "health"); + assert_eq!(s.function, "system_info"); + assert!(s.inputs.is_empty()); + // version, os, arch, pid + assert_eq!(s.outputs.len(), 4); + } + #[test] fn unknown_function_returns_unknown() { let s = schemas("bad"); @@ -86,7 +139,10 @@ mod tests { fn schemas_and_controllers_match() { let s = all_controller_schemas(); let c = all_registered_controllers(); - assert_eq!(s[0].function, c[0].schema.function); + assert_eq!(s.len(), c.len()); + for (schema, controller) in s.iter().zip(c.iter()) { + assert_eq!(schema.function, controller.schema.function); + } } #[tokio::test] @@ -96,6 +152,18 @@ mod tests { assert!(result.unwrap().is_object()); } + #[tokio::test] + async fn handle_system_info_returns_json_object() { + let result = handle_system_info(Map::new()).await; + assert!(result.is_ok()); + let json = result.unwrap(); + assert!(json.is_object()); + assert!(json["version"].as_str().is_some()); + assert!(json["os"].as_str().is_some()); + assert!(json["arch"].as_str().is_some()); + assert!(json["pid"].as_u64().is_some()); + } + #[test] fn to_json_helper() { let outcome = RpcOutcome::single_log(serde_json::json!({"ok": true}), "log");