Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion src/openhuman/tool_registry/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
};
100 changes: 98 additions & 2 deletions src/openhuman/tool_registry/ops.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use std::collections::BTreeMap;
use std::collections::{BTreeMap, BTreeSet};

use serde_json::{json, Map, Value};

Expand All @@ -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> {
Expand All @@ -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();
Expand Down Expand Up @@ -287,6 +328,41 @@ 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}_"))
})
}

fn policy_surface_ids() -> Vec<String> {
Comment thread
vaddisrinivas marked this conversation as resolved.
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()
Comment thread
vaddisrinivas marked this conversation as resolved.
}

fn is_policy_surface(tool_id: &str) -> bool {
POLICY_SURFACES.contains(&tool_id)
}

fn title_from_function(function: &str) -> String {
function
.split('_')
Expand Down Expand Up @@ -345,6 +421,26 @@ 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 insert_registry_entry_skips_duplicate_tool_id() {
let mut entries = BTreeMap::new();
Expand Down
63 changes: 60 additions & 3 deletions src/openhuman/tool_registry/schemas.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -20,6 +20,10 @@ pub fn all_registered_controllers() -> Vec<RegisteredController> {
schema: schemas("get"),
handler: handle_get,
},
RegisteredController {
schema: schemas("diagnostics"),
handler: handle_diagnostics,
},
]
}

Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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
})
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

fn required_tool_id(params: &Map<String, Value>) -> Result<&str, String> {
params
.get("tool_id")
Expand All @@ -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]
Expand All @@ -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();
Expand Down Expand Up @@ -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 changes: 11 additions & 0 deletions src/openhuman/tool_registry/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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>,
}
Loading