feat(mcp): enforce spec-compliant tool registry
- Reject dotted tool identifiers during registration and remove alias-backed lookups. - Drop web.search compatibility, normalize all code/tests around the canonical web_search name, and update consent/session logic. - Harden CLI toggles to manage the spec-compliant identifier and ensure MCP configs shed non-compliant entries automatically. Acceptance Criteria: - Tool registry denies invalid identifiers by default and no alias codepaths remain. Test Notes: - cargo check -p owlen-core (tests unavailable in sandbox).
This commit is contained in:
@@ -19,6 +19,7 @@ use crate::model::{DetailedModelInfo, ModelManager};
|
||||
use crate::oauth::{DeviceAuthorization, DevicePollState, OAuthClient};
|
||||
use crate::providers::OllamaProvider;
|
||||
use crate::storage::{SessionMeta, StorageManager};
|
||||
use crate::tools::{WEB_SEARCH_TOOL_NAME, canonical_tool_name, tool_name_matches};
|
||||
use crate::types::{
|
||||
ChatParameters, ChatRequest, ChatResponse, Conversation, Message, ModelInfo, ToolCall,
|
||||
};
|
||||
@@ -407,7 +408,7 @@ async fn build_tools(
|
||||
.security
|
||||
.allowed_tools
|
||||
.iter()
|
||||
.any(|tool| tool == "web_search")
|
||||
.any(|tool| tool_name_matches(tool, WEB_SEARCH_TOOL_NAME))
|
||||
&& config_guard.tools.web_search.enabled
|
||||
&& config_guard.privacy.enable_remote_search
|
||||
{
|
||||
@@ -424,7 +425,7 @@ async fn build_tools(
|
||||
|
||||
if let Some(settings) = web_search_settings {
|
||||
let tool = WebSearchTool::new(consent_manager.clone(), settings);
|
||||
registry.register(tool);
|
||||
registry.register(tool)?;
|
||||
}
|
||||
|
||||
// Register web_scrape tool if allowed.
|
||||
@@ -432,12 +433,12 @@ async fn build_tools(
|
||||
.security
|
||||
.allowed_tools
|
||||
.iter()
|
||||
.any(|tool| tool == "web_scrape")
|
||||
.any(|tool| tool_name_matches(tool, "web_scrape"))
|
||||
&& config_guard.tools.web_search.enabled // reuse web_search toggle for simplicity
|
||||
&& config_guard.privacy.enable_remote_search
|
||||
{
|
||||
let tool = WebScrapeTool::new();
|
||||
registry.register(tool);
|
||||
registry.register(tool)?;
|
||||
}
|
||||
|
||||
if enable_code_tools
|
||||
@@ -449,11 +450,11 @@ async fn build_tools(
|
||||
&& config_guard.tools.code_exec.enabled
|
||||
{
|
||||
let tool = CodeExecTool::new(config_guard.tools.code_exec.allowed_languages.clone());
|
||||
registry.register(tool);
|
||||
registry.register(tool)?;
|
||||
}
|
||||
|
||||
registry.register(ResourcesListTool);
|
||||
registry.register(ResourcesGetTool);
|
||||
registry.register(ResourcesListTool)?;
|
||||
registry.register(ResourcesGetTool)?;
|
||||
|
||||
if config_guard
|
||||
.security
|
||||
@@ -461,7 +462,7 @@ async fn build_tools(
|
||||
.iter()
|
||||
.any(|t| t == "file_write")
|
||||
{
|
||||
registry.register(ResourcesWriteTool);
|
||||
registry.register(ResourcesWriteTool)?;
|
||||
}
|
||||
if config_guard
|
||||
.security
|
||||
@@ -469,7 +470,7 @@ async fn build_tools(
|
||||
.iter()
|
||||
.any(|t| t == "file_delete")
|
||||
{
|
||||
registry.register(ResourcesDeleteTool);
|
||||
registry.register(ResourcesDeleteTool)?;
|
||||
}
|
||||
|
||||
for tool in registry.all() {
|
||||
@@ -1023,13 +1024,14 @@ impl SessionController {
|
||||
let mut seen_tools = std::collections::HashSet::new();
|
||||
|
||||
for tool_call in tool_calls {
|
||||
if seen_tools.contains(&tool_call.name) {
|
||||
let canonical = canonical_tool_name(tool_call.name.as_str()).to_string();
|
||||
if seen_tools.contains(&canonical) {
|
||||
continue;
|
||||
}
|
||||
seen_tools.insert(tool_call.name.clone());
|
||||
seen_tools.insert(canonical.clone());
|
||||
|
||||
let (data_types, endpoints) = match tool_call.name.as_str() {
|
||||
"web_search" => (
|
||||
let (data_types, endpoints) = match canonical.as_str() {
|
||||
WEB_SEARCH_TOOL_NAME => (
|
||||
vec!["search query".to_string()],
|
||||
vec!["cloud provider".to_string()],
|
||||
),
|
||||
@@ -1097,13 +1099,14 @@ impl SessionController {
|
||||
pub async fn set_tool_enabled(&mut self, tool: &str, enabled: bool) -> Result<()> {
|
||||
{
|
||||
let mut config = self.config.lock().await;
|
||||
match tool {
|
||||
"web_search" => {
|
||||
let canonical = canonical_tool_name(tool);
|
||||
match canonical {
|
||||
WEB_SEARCH_TOOL_NAME => {
|
||||
config.tools.web_search.enabled = enabled;
|
||||
config.privacy.enable_remote_search = enabled;
|
||||
}
|
||||
"code_exec" => config.tools.code_exec.enabled = enabled,
|
||||
other => return Err(Error::InvalidInput(format!("Unknown tool: {other}"))),
|
||||
_ => return Err(Error::InvalidInput(format!("Unknown tool: {tool}"))),
|
||||
}
|
||||
}
|
||||
self.rebuild_tools().await
|
||||
@@ -1897,12 +1900,12 @@ mod tests {
|
||||
#[test]
|
||||
fn streaming_state_detects_tool_call_changes() {
|
||||
let mut state = StreamingMessageState::new();
|
||||
let tool = make_tool_call("call-1", "web.search");
|
||||
let tool = make_tool_call("call-1", "web_search");
|
||||
|
||||
let diff = state.ingest(&make_response("", Some(vec![tool.clone()]), false));
|
||||
let calls = diff.tool_calls.expect("initial tool call");
|
||||
assert_eq!(calls.len(), 1);
|
||||
assert_eq!(calls[0].name, "web.search");
|
||||
assert_eq!(calls[0].name, "web_search");
|
||||
|
||||
let diff = state.ingest(&make_response("", Some(vec![tool.clone()]), false));
|
||||
assert!(
|
||||
|
||||
Reference in New Issue
Block a user