Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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,
};
119 changes: 117 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,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> {
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)
|| tool_id.starts_with("security.")
|| tool_id.starts_with("approval.")
}

fn title_from_function(function: &str) -> String {
function
.split('_')
Expand Down Expand Up @@ -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();
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