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:
2025-10-25 04:48:17 +02:00
parent 6a94373c4f
commit c3a92a092b
13 changed files with 284 additions and 105 deletions

View File

@@ -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!(