mirror of
https://github.com/tinyhumansai/openhuman.git
synced 2026-07-27 21:08:00 +00:00
Add tool registry policy diagnostics (#2336)
This commit is contained in:
@@ -9,4 +9,7 @@ pub use schemas::{
|
||||
all_controller_schemas as all_tool_registry_controller_schemas,
|
||||
all_registered_controllers as all_tool_registry_registered_controllers,
|
||||
};
|
||||
pub use types::{ToolRegistryEntry, ToolRegistryHealth, ToolRegistryList, ToolRegistryTransport};
|
||||
pub use types::{
|
||||
ToolPolicyDiagnostics, ToolRegistryEntry, ToolRegistryHealth, ToolRegistryList,
|
||||
ToolRegistryTransport,
|
||||
};
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use std::collections::BTreeMap;
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
|
||||
use serde_json::{json, Map, Value};
|
||||
|
||||
@@ -8,10 +8,20 @@ use crate::openhuman::mcp_server::McpToolSpec;
|
||||
use crate::rpc::RpcOutcome;
|
||||
|
||||
use super::types::{
|
||||
ToolRegistryEntry, ToolRegistryHealth, ToolRegistryList, ToolRegistryTransport,
|
||||
ToolPolicyDiagnostics, ToolRegistryEntry, ToolRegistryHealth, ToolRegistryList,
|
||||
ToolRegistryTransport,
|
||||
};
|
||||
|
||||
const REGISTRY_ENTRY_VERSION: &str = env!("CARGO_PKG_VERSION");
|
||||
const POLICY_SURFACES: &[&str] = &[
|
||||
"security.policy_info",
|
||||
"approval.list_pending",
|
||||
"approval.list_recent_decisions",
|
||||
"approval.decide",
|
||||
"tool_registry.list",
|
||||
"tool_registry.get",
|
||||
"tool_registry.diagnostics",
|
||||
];
|
||||
|
||||
/// Return the current read-only tool registry snapshot.
|
||||
pub fn list_tools() -> RpcOutcome<ToolRegistryList> {
|
||||
@@ -23,6 +33,37 @@ pub fn list_tools() -> RpcOutcome<ToolRegistryList> {
|
||||
RpcOutcome::new(ToolRegistryList { tools }, vec![])
|
||||
}
|
||||
|
||||
/// Return redacted diagnostics for policy/tool visibility reviews.
|
||||
pub fn diagnostics() -> RpcOutcome<ToolPolicyDiagnostics> {
|
||||
let tools = registry_entries();
|
||||
let total_tools = tools.len();
|
||||
let enabled_tools = tools.iter().filter(|entry| entry.enabled).count();
|
||||
let mcp_stdio_tools = tools
|
||||
.iter()
|
||||
.filter(|entry| entry.transport == ToolRegistryTransport::McpStdio)
|
||||
.count();
|
||||
let json_rpc_tools = tools
|
||||
.iter()
|
||||
.filter(|entry| entry.transport == ToolRegistryTransport::JsonRpc)
|
||||
.count();
|
||||
let possible_write_surfaces = tools
|
||||
.iter()
|
||||
.filter(|entry| looks_write_capable(&entry.tool_id))
|
||||
.map(|entry| entry.tool_id.clone())
|
||||
.collect::<Vec<_>>();
|
||||
let policy_surfaces = policy_surface_ids();
|
||||
|
||||
let diagnostics = ToolPolicyDiagnostics {
|
||||
total_tools,
|
||||
enabled_tools,
|
||||
mcp_stdio_tools,
|
||||
json_rpc_tools,
|
||||
possible_write_surfaces,
|
||||
policy_surfaces,
|
||||
};
|
||||
RpcOutcome::new(diagnostics, vec![])
|
||||
}
|
||||
|
||||
/// Look up one registry entry by stable `tool_id`.
|
||||
pub fn get_tool(tool_id: &str) -> Result<RpcOutcome<ToolRegistryEntry>, String> {
|
||||
let normalized = tool_id.trim();
|
||||
@@ -287,6 +328,44 @@ fn push_unique(tags: &mut Vec<String>, tag: &str) {
|
||||
}
|
||||
}
|
||||
|
||||
fn looks_write_capable(tool_id: &str) -> bool {
|
||||
const MARKERS: &[&str] = &[
|
||||
"add", "apply", "create", "decide", "delete", "email", "execute", "forget", "ingest",
|
||||
"post", "put", "remove", "run", "send", "store", "update", "write",
|
||||
];
|
||||
let lower = tool_id.to_ascii_lowercase();
|
||||
MARKERS.iter().any(|marker| {
|
||||
lower == *marker
|
||||
|| lower.contains(&format!(".{marker}"))
|
||||
|| lower.contains(&format!("_{marker}"))
|
||||
|| lower.contains(&format!("{marker}."))
|
||||
|| lower.contains(&format!("{marker}_"))
|
||||
})
|
||||
}
|
||||
|
||||
fn policy_surface_ids() -> Vec<String> {
|
||||
let mut ids = POLICY_SURFACES
|
||||
.iter()
|
||||
.copied()
|
||||
.map(String::from)
|
||||
.collect::<BTreeSet<_>>();
|
||||
|
||||
ids.extend(
|
||||
all::all_controller_schemas()
|
||||
.into_iter()
|
||||
.map(|schema| schema.method_name())
|
||||
.filter(|tool_id| is_policy_surface(tool_id)),
|
||||
);
|
||||
|
||||
ids.into_iter().collect()
|
||||
}
|
||||
|
||||
fn is_policy_surface(tool_id: &str) -> bool {
|
||||
POLICY_SURFACES.contains(&tool_id)
|
||||
|| tool_id.starts_with("security.")
|
||||
|| tool_id.starts_with("approval.")
|
||||
}
|
||||
|
||||
fn title_from_function(function: &str) -> String {
|
||||
function
|
||||
.split('_')
|
||||
@@ -345,6 +424,42 @@ mod tests {
|
||||
assert_eq!(ids, sorted);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn diagnostics_reports_inventory_and_policy_surfaces() {
|
||||
let outcome = diagnostics();
|
||||
|
||||
assert!(outcome.value.total_tools > 0);
|
||||
assert_eq!(outcome.value.total_tools, outcome.value.enabled_tools);
|
||||
assert!(outcome.value.mcp_stdio_tools > 0);
|
||||
assert!(outcome.value.json_rpc_tools > 0);
|
||||
assert!(outcome
|
||||
.value
|
||||
.policy_surfaces
|
||||
.iter()
|
||||
.any(|tool_id| tool_id == "security.policy_info"));
|
||||
assert!(outcome
|
||||
.value
|
||||
.possible_write_surfaces
|
||||
.iter()
|
||||
.any(|tool_id| tool_id == "tools.composio_execute"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn looks_write_capable_detects_action_prefixes_and_suffixes() {
|
||||
assert!(looks_write_capable("user.create"));
|
||||
assert!(looks_write_capable("create.user"));
|
||||
assert!(looks_write_capable("tools.composio_execute"));
|
||||
assert!(!looks_write_capable("tools.search"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn is_policy_surface_includes_policy_namespaces() {
|
||||
assert!(is_policy_surface("security.audit_status"));
|
||||
assert!(is_policy_surface("approval.request"));
|
||||
assert!(is_policy_surface("tool_registry.diagnostics"));
|
||||
assert!(!is_policy_surface("tools.web_search"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn insert_registry_entry_skips_duplicate_tool_id() {
|
||||
let mut entries = BTreeMap::new();
|
||||
|
||||
@@ -6,7 +6,7 @@ use crate::rpc::RpcOutcome;
|
||||
|
||||
/// Declared controller schemas for the `tool_registry` namespace.
|
||||
pub fn all_controller_schemas() -> Vec<ControllerSchema> {
|
||||
vec![schemas("list"), schemas("get")]
|
||||
vec![schemas("list"), schemas("get"), schemas("diagnostics")]
|
||||
}
|
||||
|
||||
/// Registered controller handlers for the `tool_registry` namespace.
|
||||
@@ -20,6 +20,10 @@ pub fn all_registered_controllers() -> Vec<RegisteredController> {
|
||||
schema: schemas("get"),
|
||||
handler: handle_get,
|
||||
},
|
||||
RegisteredController {
|
||||
schema: schemas("diagnostics"),
|
||||
handler: handle_diagnostics,
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
@@ -55,6 +59,18 @@ pub fn schemas(function: &str) -> ControllerSchema {
|
||||
required: true,
|
||||
}],
|
||||
},
|
||||
"diagnostics" => ControllerSchema {
|
||||
namespace: "tool_registry",
|
||||
function: "diagnostics",
|
||||
description: "Return redacted tool inventory and policy visibility diagnostics.",
|
||||
inputs: vec![],
|
||||
outputs: vec![FieldSchema {
|
||||
name: "diagnostics",
|
||||
ty: TypeSchema::Json,
|
||||
comment: "Counts and redacted tool ids useful for policy/conformance checks.",
|
||||
required: true,
|
||||
}],
|
||||
},
|
||||
_ => ControllerSchema {
|
||||
namespace: "tool_registry",
|
||||
function: "unknown",
|
||||
@@ -88,6 +104,21 @@ fn handle_get(params: Map<String, Value>) -> ControllerFuture {
|
||||
})
|
||||
}
|
||||
|
||||
fn handle_diagnostics(params: Map<String, Value>) -> ControllerFuture {
|
||||
Box::pin(async move {
|
||||
log::debug!(
|
||||
"[tool_registry] rpc diagnostics requested param_count={}",
|
||||
params.len()
|
||||
);
|
||||
let result = to_json(crate::openhuman::tool_registry::ops::diagnostics());
|
||||
log::debug!(
|
||||
"[tool_registry] rpc diagnostics completed success={}",
|
||||
result.is_ok()
|
||||
);
|
||||
result
|
||||
})
|
||||
}
|
||||
|
||||
fn required_tool_id(params: &Map<String, Value>) -> Result<&str, String> {
|
||||
params
|
||||
.get("tool_id")
|
||||
@@ -111,10 +142,11 @@ mod tests {
|
||||
let schemas = all_controller_schemas();
|
||||
let controllers = all_registered_controllers();
|
||||
|
||||
assert_eq!(schemas.len(), 2);
|
||||
assert_eq!(controllers.len(), 2);
|
||||
assert_eq!(schemas.len(), 3);
|
||||
assert_eq!(controllers.len(), 3);
|
||||
assert_eq!(schemas[0].function, controllers[0].schema.function);
|
||||
assert_eq!(schemas[1].function, controllers[1].schema.function);
|
||||
assert_eq!(schemas[2].function, controllers[2].schema.function);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -133,6 +165,15 @@ mod tests {
|
||||
assert!(schema.inputs[0].required);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn diagnostics_schema_has_no_inputs() {
|
||||
let schema = schemas("diagnostics");
|
||||
assert_eq!(schema.namespace, "tool_registry");
|
||||
assert_eq!(schema.function, "diagnostics");
|
||||
assert!(schema.inputs.is_empty());
|
||||
assert_eq!(schema.outputs[0].name, "diagnostics");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn required_tool_id_rejects_wrong_type() {
|
||||
let mut params = Map::new();
|
||||
@@ -165,4 +206,20 @@ mod tests {
|
||||
Some("tools.web_search")
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn handle_diagnostics_returns_counts() {
|
||||
let value = handle_diagnostics(Map::new())
|
||||
.await
|
||||
.expect("diagnostics json");
|
||||
let diagnostics = value.get("diagnostics").unwrap_or(&value);
|
||||
assert!(diagnostics
|
||||
.get("total_tools")
|
||||
.and_then(Value::as_u64)
|
||||
.is_some_and(|count| count > 0));
|
||||
assert!(diagnostics
|
||||
.get("policy_surfaces")
|
||||
.and_then(Value::as_array)
|
||||
.is_some());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -58,3 +58,14 @@ pub struct ToolRegistryList {
|
||||
/// Sorted registry entries.
|
||||
pub tools: Vec<ToolRegistryEntry>,
|
||||
}
|
||||
|
||||
/// Redacted diagnostics for policy/tool visibility reviews.
|
||||
#[derive(Debug, Clone, PartialEq, Serialize)]
|
||||
pub struct ToolPolicyDiagnostics {
|
||||
pub total_tools: usize,
|
||||
pub enabled_tools: usize,
|
||||
pub mcp_stdio_tools: usize,
|
||||
pub json_rpc_tools: usize,
|
||||
pub possible_write_surfaces: Vec<String>,
|
||||
pub policy_surfaces: Vec<String>,
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user