diff --git a/src/core/all.rs b/src/core/all.rs index 17870da07..583edb710 100644 --- a/src/core/all.rs +++ b/src/core/all.rs @@ -108,6 +108,8 @@ fn build_registered_controllers() -> Vec { 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 { 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, + pub outputs: Vec, +} + +pub fn all_http_method_schemas() -> Vec { + 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; diff --git a/src/core/dispatch.rs b/src/core/dispatch.rs index ef1f85559..4d0b5b2ee 100644 --- a/src/core/dispatch.rs +++ b/src/core/dispatch.rs @@ -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> { + 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, 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::*; diff --git a/src/core/jsonrpc.rs b/src/core/jsonrpc.rs index b567a925a..1c903bcf5 100644 --- a/src/core/jsonrpc.rs +++ b/src/core/jsonrpc.rs @@ -125,13 +125,16 @@ async fn invoke_method_inner( ) -> Result { // 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 = 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)); diff --git a/src/openhuman/security/mod.rs b/src/openhuman/security/mod.rs index e2dd8460e..ba3c10a3b 100644 --- a/src/openhuman/security/mod.rs +++ b/src/openhuman/security/mod.rs @@ -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, +}; diff --git a/src/openhuman/security/schemas.rs b/src/openhuman/security/schemas.rs new file mode 100644 index 000000000..446ff74ac --- /dev/null +++ b/src/openhuman/security/schemas.rs @@ -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 { + vec![schemas("policy_info")] +} + +pub fn all_registered_controllers() -> Vec { + 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) -> ControllerFuture { + Box::pin(async { to_json(crate::openhuman::security::rpc::security_policy_info()) }) +} + +fn to_json(outcome: RpcOutcome) -> Result { + outcome.into_cli_compatible_json() +} diff --git a/src/rpc/dispatch.rs b/src/rpc/dispatch.rs index 6fdd335e5..1b1f7f78e 100644 --- a/src/rpc/dispatch.rs +++ b/src/rpc/dispatch.rs @@ -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(outcome: RpcOutcome) -> Result { - 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> { - 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" + ); } }