mirror of
https://github.com/tinyhumansai/openhuman.git
synced 2026-07-30 23:14:37 +00:00
Unify core JSON-RPC dispatch behind controller registry (#1238)
Co-authored-by: Jwalin Shah <jshah1331@gmail.com>
This commit is contained in:
co-authored by
Jwalin Shah
parent
377a326a18
commit
259c49e90f
@@ -108,6 +108,8 @@ fn build_registered_controllers() -> Vec<RegisteredController> {
|
||||
controllers.extend(crate::openhuman::doctor::all_doctor_registered_controllers());
|
||||
// Secret storage and encryption
|
||||
controllers.extend(crate::openhuman::encryption::all_encryption_registered_controllers());
|
||||
// Security policy metadata
|
||||
controllers.extend(crate::openhuman::security::all_security_registered_controllers());
|
||||
// Background heartbeat loop controls
|
||||
controllers.extend(crate::openhuman::heartbeat::all_heartbeat_registered_controllers());
|
||||
// Token usage and billing cost tracking
|
||||
@@ -204,6 +206,7 @@ fn build_declared_controller_schemas() -> Vec<ControllerSchema> {
|
||||
schemas.extend(crate::openhuman::health::all_health_controller_schemas());
|
||||
schemas.extend(crate::openhuman::doctor::all_doctor_controller_schemas());
|
||||
schemas.extend(crate::openhuman::encryption::all_encryption_controller_schemas());
|
||||
schemas.extend(crate::openhuman::security::all_security_controller_schemas());
|
||||
schemas.extend(crate::openhuman::heartbeat::all_heartbeat_controller_schemas());
|
||||
schemas.extend(crate::openhuman::cost::all_cost_controller_schemas());
|
||||
schemas.extend(crate::openhuman::autocomplete::all_autocomplete_controller_schemas());
|
||||
@@ -488,6 +491,60 @@ fn validate_registry(
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct HttpMethodSchemaDefinition {
|
||||
pub method: String,
|
||||
pub namespace: &'static str,
|
||||
pub function: &'static str,
|
||||
pub description: &'static str,
|
||||
pub inputs: Vec<crate::core::FieldSchema>,
|
||||
pub outputs: Vec<crate::core::FieldSchema>,
|
||||
}
|
||||
|
||||
pub fn all_http_method_schemas() -> Vec<HttpMethodSchemaDefinition> {
|
||||
let mut methods = vec![
|
||||
HttpMethodSchemaDefinition {
|
||||
method: "core.ping".to_string(),
|
||||
namespace: "core",
|
||||
function: "ping",
|
||||
description: "Liveness probe for the core JSON-RPC server.",
|
||||
inputs: vec![],
|
||||
outputs: vec![crate::core::FieldSchema {
|
||||
name: "ok",
|
||||
ty: crate::core::TypeSchema::Bool,
|
||||
comment: "Always true when the server is reachable.",
|
||||
required: true,
|
||||
}],
|
||||
},
|
||||
HttpMethodSchemaDefinition {
|
||||
method: "core.version".to_string(),
|
||||
namespace: "core",
|
||||
function: "version",
|
||||
description: "Returns the core binary version.",
|
||||
inputs: vec![],
|
||||
outputs: vec![crate::core::FieldSchema {
|
||||
name: "version",
|
||||
ty: crate::core::TypeSchema::String,
|
||||
comment: "Semantic version string for the running core binary.",
|
||||
required: true,
|
||||
}],
|
||||
},
|
||||
];
|
||||
methods.extend(
|
||||
all_controller_schemas()
|
||||
.into_iter()
|
||||
.map(|schema| HttpMethodSchemaDefinition {
|
||||
method: rpc_method_name(&schema),
|
||||
namespace: schema.namespace,
|
||||
function: schema.function,
|
||||
description: schema.description,
|
||||
inputs: schema.inputs,
|
||||
outputs: schema.outputs,
|
||||
}),
|
||||
);
|
||||
methods
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "all_tests.rs"]
|
||||
mod tests;
|
||||
|
||||
+48
-3
@@ -5,7 +5,7 @@
|
||||
|
||||
use crate::core::rpc_log;
|
||||
use crate::core::types::{AppState, InvocationResult};
|
||||
use serde_json::json;
|
||||
use serde_json::{json, Map, Value};
|
||||
|
||||
/// Dispatches an RPC method call to the appropriate subsystem.
|
||||
///
|
||||
@@ -44,8 +44,16 @@ pub async fn dispatch(
|
||||
return result.map(crate::core::types::invocation_to_rpc_json);
|
||||
}
|
||||
|
||||
// Tier 2: Domain-specific dispatcher.
|
||||
// This routes to controllers registered in src/core/all.rs and src/rpc/mod.rs.
|
||||
// Tier 2: Registered domain controllers.
|
||||
if let Some(result) = try_registry_dispatch(method, params.clone()).await {
|
||||
log::debug!(
|
||||
"[rpc:dispatch] routed method={} subsystem=controller_registry",
|
||||
method
|
||||
);
|
||||
return result;
|
||||
}
|
||||
|
||||
// Tier 3: Legacy domain-specific dispatcher.
|
||||
if let Some(result) = crate::rpc::try_dispatch(method, params).await {
|
||||
log::debug!(
|
||||
"[rpc:dispatch] routed method={} subsystem=openhuman",
|
||||
@@ -79,6 +87,43 @@ fn try_core_dispatch(
|
||||
}
|
||||
}
|
||||
|
||||
async fn try_registry_dispatch(
|
||||
method: &str,
|
||||
params: Value,
|
||||
) -> Option<Result<serde_json::Value, String>> {
|
||||
let schema = crate::core::all::schema_for_rpc_method(method)?;
|
||||
let params_obj = match params_to_object(params) {
|
||||
Ok(params_obj) => params_obj,
|
||||
Err(err) => return Some(Err(err)),
|
||||
};
|
||||
if let Err(err) = crate::core::all::validate_params(&schema, ¶ms_obj) {
|
||||
return Some(Err(err));
|
||||
}
|
||||
crate::core::all::try_invoke_registered_rpc(method, params_obj).await
|
||||
}
|
||||
|
||||
fn params_to_object(params: Value) -> Result<Map<String, Value>, String> {
|
||||
match params {
|
||||
Value::Object(map) => Ok(map),
|
||||
Value::Null => Ok(Map::new()),
|
||||
other => Err(format!(
|
||||
"invalid params: expected object or null, got {}",
|
||||
type_name(&other)
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
fn type_name(value: &Value) -> &'static str {
|
||||
match value {
|
||||
Value::Null => "null",
|
||||
Value::Bool(_) => "bool",
|
||||
Value::Number(_) => "number",
|
||||
Value::String(_) => "string",
|
||||
Value::Array(_) => "array",
|
||||
Value::Object(_) => "object",
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
+16
-43
@@ -125,13 +125,16 @@ async fn invoke_method_inner(
|
||||
) -> Result<Value, String> {
|
||||
// Phase 1: Check static controller registry.
|
||||
if let Some(schema) = all::schema_for_rpc_method(method) {
|
||||
let params_obj = params_to_object(params)?;
|
||||
let params_obj = params_to_object(params.clone())?;
|
||||
// Validate inputs against the schema before calling the handler.
|
||||
all::validate_params(&schema, ¶ms_obj)?;
|
||||
if let Some(result) = all::try_invoke_registered_rpc(method, params_obj).await {
|
||||
return result;
|
||||
}
|
||||
return Err(format!("registered schema has no handler: {method}"));
|
||||
log::debug!(
|
||||
"[jsonrpc] schema matched without registered handler; falling back method={}",
|
||||
method
|
||||
);
|
||||
}
|
||||
|
||||
// Phase 2: Fall back to dynamic dispatch (internal core methods or legacy paths).
|
||||
@@ -1080,47 +1083,17 @@ struct HttpMethodSchema {
|
||||
///
|
||||
/// Also includes built-in core methods like `core.ping` and `core.version`.
|
||||
fn build_http_schema_dump() -> HttpSchemaDump {
|
||||
let mut methods = vec![
|
||||
HttpMethodSchema {
|
||||
method: "core.ping".to_string(),
|
||||
namespace: "core".to_string(),
|
||||
function: "ping".to_string(),
|
||||
description: "Liveness probe for the core JSON-RPC server.".to_string(),
|
||||
inputs: vec![],
|
||||
outputs: vec![crate::core::FieldSchema {
|
||||
name: "ok",
|
||||
ty: crate::core::TypeSchema::Bool,
|
||||
comment: "Always true when the server is reachable.",
|
||||
required: true,
|
||||
}],
|
||||
},
|
||||
HttpMethodSchema {
|
||||
method: "core.version".to_string(),
|
||||
namespace: "core".to_string(),
|
||||
function: "version".to_string(),
|
||||
description: "Returns the core binary version.".to_string(),
|
||||
inputs: vec![],
|
||||
outputs: vec![crate::core::FieldSchema {
|
||||
name: "version",
|
||||
ty: crate::core::TypeSchema::String,
|
||||
comment: "Semantic version string for the running core binary.",
|
||||
required: true,
|
||||
}],
|
||||
},
|
||||
];
|
||||
|
||||
methods.extend(
|
||||
all::all_controller_schemas()
|
||||
.into_iter()
|
||||
.map(|schema| HttpMethodSchema {
|
||||
method: all::rpc_method_name(&schema),
|
||||
namespace: schema.namespace.to_string(),
|
||||
function: schema.function.to_string(),
|
||||
description: schema.description.to_string(),
|
||||
inputs: schema.inputs,
|
||||
outputs: schema.outputs,
|
||||
}),
|
||||
);
|
||||
let mut methods: Vec<HttpMethodSchema> = all::all_http_method_schemas()
|
||||
.into_iter()
|
||||
.map(|method| HttpMethodSchema {
|
||||
method: method.method,
|
||||
namespace: method.namespace.to_string(),
|
||||
function: method.function.to_string(),
|
||||
description: method.description.to_string(),
|
||||
inputs: method.inputs,
|
||||
outputs: method.outputs,
|
||||
})
|
||||
.collect();
|
||||
|
||||
// Sort methods alphabetically for consistent output.
|
||||
methods.sort_by(|a, b| a.method.cmp(&b.method));
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
mod core;
|
||||
pub mod ops;
|
||||
mod schemas;
|
||||
|
||||
pub mod audit;
|
||||
pub mod bubblewrap;
|
||||
@@ -28,3 +29,8 @@ pub use policy::SecurityPolicy;
|
||||
pub use secrets::SecretStore;
|
||||
#[allow(unused_imports)]
|
||||
pub use traits::{NoopSandbox, Sandbox};
|
||||
|
||||
pub use schemas::{
|
||||
all_controller_schemas as all_security_controller_schemas,
|
||||
all_registered_controllers as all_security_registered_controllers,
|
||||
};
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
use serde_json::{Map, Value};
|
||||
|
||||
use crate::core::all::{ControllerFuture, RegisteredController};
|
||||
use crate::core::{ControllerSchema, FieldSchema, TypeSchema};
|
||||
use crate::rpc::RpcOutcome;
|
||||
|
||||
pub fn all_controller_schemas() -> Vec<ControllerSchema> {
|
||||
vec![schemas("policy_info")]
|
||||
}
|
||||
|
||||
pub fn all_registered_controllers() -> Vec<RegisteredController> {
|
||||
vec![RegisteredController {
|
||||
schema: schemas("policy_info"),
|
||||
handler: handle_policy_info,
|
||||
}]
|
||||
}
|
||||
|
||||
pub fn schemas(function: &str) -> ControllerSchema {
|
||||
match function {
|
||||
"policy_info" => ControllerSchema {
|
||||
namespace: "security",
|
||||
function: "policy_info",
|
||||
description: "Return the active security/autonomy policy used by the core runtime.",
|
||||
inputs: vec![],
|
||||
outputs: vec![FieldSchema {
|
||||
name: "policy",
|
||||
ty: TypeSchema::Json,
|
||||
comment: "Security policy metadata and feature flags.",
|
||||
required: true,
|
||||
}],
|
||||
},
|
||||
_ => ControllerSchema {
|
||||
namespace: "security",
|
||||
function: "unknown",
|
||||
description: "Unknown security controller function.",
|
||||
inputs: vec![],
|
||||
outputs: vec![],
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_policy_info(_params: Map<String, Value>) -> ControllerFuture {
|
||||
Box::pin(async { to_json(crate::openhuman::security::rpc::security_policy_info()) })
|
||||
}
|
||||
|
||||
fn to_json<T: serde::Serialize>(outcome: RpcOutcome<T>) -> Result<Value, String> {
|
||||
outcome.into_cli_compatible_json()
|
||||
}
|
||||
+13
-52
@@ -1,44 +1,17 @@
|
||||
//! Main dispatcher for domain-specific RPC methods.
|
||||
//! Legacy compatibility shim for domain-specific RPC dispatch.
|
||||
//!
|
||||
//! This module routes RPC calls to their respective domain handlers (e.g.,
|
||||
//! security, memory, local AI). It serves as an extension point for
|
||||
//! domain-level functionality in the OpenHuman platform.
|
||||
//! Domain routing now lives in the controller registry (`src/core/all.rs`).
|
||||
//! This module is intentionally minimal so callers can fall through to
|
||||
//! unknown-method handling while older call sites remain compile-compatible.
|
||||
|
||||
use serde::Serialize;
|
||||
|
||||
use crate::rpc::RpcOutcome;
|
||||
|
||||
/// Helper to convert an [`RpcOutcome`] into a JSON value compatible with the CLI.
|
||||
fn rpc_json<T: Serialize>(outcome: RpcOutcome<T>) -> Result<serde_json::Value, String> {
|
||||
outcome.into_cli_compatible_json()
|
||||
}
|
||||
|
||||
/// Dispatches an RPC method to its domain-specific handler.
|
||||
/// Dispatches an RPC method to legacy handlers.
|
||||
///
|
||||
/// If the method is recognized, it executes the handler and returns the
|
||||
/// result wrapped in a `Some(Result)`. If not recognized, it returns `None`.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `method` - The name of the RPC method to invoke.
|
||||
/// * `params` - The parameters for the call as a JSON value.
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// `Some(Ok(Value))` if successful, `Some(Err(String))` if the handler failed,
|
||||
/// or `None` if the method was not found in this dispatcher.
|
||||
/// Returns `None` for all methods; controller-registry dispatch is authoritative.
|
||||
pub async fn try_dispatch(
|
||||
method: &str,
|
||||
_method: &str,
|
||||
_params: serde_json::Value,
|
||||
) -> Option<Result<serde_json::Value, String>> {
|
||||
match method {
|
||||
// Core security policy information.
|
||||
"openhuman.security_policy_info" => Some(rpc_json(
|
||||
crate::openhuman::security::rpc::security_policy_info(),
|
||||
)),
|
||||
|
||||
_ => None,
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -47,7 +20,6 @@ mod tests {
|
||||
|
||||
use super::try_dispatch;
|
||||
|
||||
/// Unknown methods must return `None` so callers can fall through.
|
||||
#[tokio::test]
|
||||
async fn dispatch_returns_none_for_unknown_method() {
|
||||
let result = try_dispatch("nonexistent.method", json!({})).await;
|
||||
@@ -55,22 +27,11 @@ mod tests {
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn dispatch_security_policy_info_returns_some() {
|
||||
async fn dispatch_security_method_now_falls_through() {
|
||||
let result = try_dispatch("openhuman.security_policy_info", json!({})).await;
|
||||
assert!(result.is_some(), "security_policy_info should be handled");
|
||||
let inner = result.unwrap();
|
||||
assert!(inner.is_ok(), "security_policy_info should succeed");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn dispatch_empty_method_returns_none() {
|
||||
let result = try_dispatch("", json!({})).await;
|
||||
assert!(result.is_none());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn dispatch_close_but_wrong_method_returns_none() {
|
||||
let result = try_dispatch("openhuman.security_policy", json!({})).await;
|
||||
assert!(result.is_none());
|
||||
assert!(
|
||||
result.is_none(),
|
||||
"security method should be handled by registry path"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user