Unify core JSON-RPC dispatch behind controller registry (#1238)

Co-authored-by: Jwalin Shah <jshah1331@gmail.com>
This commit is contained in:
Jwalin Shah
2026-05-05 11:10:32 -07:00
committed by GitHub
co-authored by Jwalin Shah
parent 377a326a18
commit 259c49e90f
6 changed files with 188 additions and 98 deletions
+57
View File
@@ -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
View File
@@ -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, &params_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
View File
@@ -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, &params_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));
+6
View File
@@ -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,
};
+48
View File
@@ -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
View File
@@ -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"
);
}
}