Add tool registry policy diagnostics (#2336)

This commit is contained in:
Srinivas Vaddi
2026-05-22 15:59:35 +05:30
committed by GitHub
parent b08aa3ea8d
commit 607a6a1db5
4 changed files with 192 additions and 6 deletions
+4 -1
View File
@@ -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,
};
+117 -2
View File
@@ -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();
+60 -3
View File
@@ -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());
}
}
+11
View File
@@ -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>,
}