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:
@@ -7,6 +7,7 @@ use chrono::{DateTime, Utc};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::encryption::VaultHandle;
|
||||
use crate::tools::canonical_tool_name;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct ConsentRequest {
|
||||
@@ -94,10 +95,12 @@ impl ConsentManager {
|
||||
data_types: Vec<String>,
|
||||
endpoints: Vec<String>,
|
||||
) -> Result<ConsentScope> {
|
||||
let canonical = canonical_tool_name(tool_name);
|
||||
|
||||
// Check if already granted permanently
|
||||
if self
|
||||
.permanent_records
|
||||
.get(tool_name)
|
||||
.get(canonical)
|
||||
.is_some_and(|existing| existing.scope == ConsentScope::Permanent)
|
||||
{
|
||||
return Ok(ConsentScope::Permanent);
|
||||
@@ -106,31 +109,31 @@ impl ConsentManager {
|
||||
// Check if granted for session
|
||||
if self
|
||||
.session_records
|
||||
.get(tool_name)
|
||||
.get(canonical)
|
||||
.is_some_and(|existing| existing.scope == ConsentScope::Session)
|
||||
{
|
||||
return Ok(ConsentScope::Session);
|
||||
}
|
||||
|
||||
// Check if request is already pending (prevent duplicate prompts)
|
||||
if self.pending_requests.contains_key(tool_name) {
|
||||
if self.pending_requests.contains_key(canonical) {
|
||||
// Wait for the other prompt to complete by returning denied temporarily
|
||||
// The caller should retry after a short delay
|
||||
return Ok(ConsentScope::Denied);
|
||||
}
|
||||
|
||||
// Mark as pending
|
||||
self.pending_requests.insert(tool_name.to_string(), ());
|
||||
self.pending_requests.insert(canonical.to_string(), ());
|
||||
|
||||
// Show consent dialog and get scope
|
||||
let scope = self.show_consent_dialog(tool_name, &data_types, &endpoints)?;
|
||||
|
||||
// Remove from pending
|
||||
self.pending_requests.remove(tool_name);
|
||||
self.pending_requests.remove(canonical);
|
||||
|
||||
// Create record based on scope
|
||||
let record = ConsentRecord {
|
||||
tool_name: tool_name.to_string(),
|
||||
tool_name: canonical.to_string(),
|
||||
scope: scope.clone(),
|
||||
timestamp: Utc::now(),
|
||||
data_types,
|
||||
@@ -140,10 +143,10 @@ impl ConsentManager {
|
||||
// Store in appropriate location
|
||||
match scope {
|
||||
ConsentScope::Permanent => {
|
||||
self.permanent_records.insert(tool_name.to_string(), record);
|
||||
self.permanent_records.insert(canonical.to_string(), record);
|
||||
}
|
||||
ConsentScope::Session => {
|
||||
self.session_records.insert(tool_name.to_string(), record);
|
||||
self.session_records.insert(canonical.to_string(), record);
|
||||
}
|
||||
ConsentScope::Once | ConsentScope::Denied => {
|
||||
// Don't store, just return the decision
|
||||
@@ -171,8 +174,9 @@ impl ConsentManager {
|
||||
endpoints: Vec<String>,
|
||||
scope: ConsentScope,
|
||||
) {
|
||||
let canonical = canonical_tool_name(tool_name);
|
||||
let record = ConsentRecord {
|
||||
tool_name: tool_name.to_string(),
|
||||
tool_name: canonical.to_string(),
|
||||
scope: scope.clone(),
|
||||
timestamp: Utc::now(),
|
||||
data_types,
|
||||
@@ -181,13 +185,13 @@ impl ConsentManager {
|
||||
|
||||
match scope {
|
||||
ConsentScope::Permanent => {
|
||||
self.permanent_records.insert(tool_name.to_string(), record);
|
||||
self.permanent_records.insert(canonical.to_string(), record);
|
||||
}
|
||||
ConsentScope::Session => {
|
||||
self.session_records.insert(tool_name.to_string(), record);
|
||||
self.session_records.insert(canonical.to_string(), record);
|
||||
}
|
||||
ConsentScope::Once => {
|
||||
self.once_records.insert(tool_name.to_string(), record);
|
||||
self.once_records.insert(canonical.to_string(), record);
|
||||
}
|
||||
ConsentScope::Denied => {} // Denied is not stored
|
||||
}
|
||||
@@ -195,28 +199,30 @@ impl ConsentManager {
|
||||
|
||||
/// Check if consent is needed (returns None if already granted, Some(info) if needed)
|
||||
pub fn check_consent_needed(&self, tool_name: &str) -> Option<ConsentRequest> {
|
||||
if self.has_consent(tool_name) {
|
||||
let canonical = canonical_tool_name(tool_name);
|
||||
if self.has_consent(canonical) {
|
||||
None
|
||||
} else {
|
||||
Some(ConsentRequest {
|
||||
tool_name: tool_name.to_string(),
|
||||
tool_name: canonical.to_string(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub fn has_consent(&self, tool_name: &str) -> bool {
|
||||
let canonical = canonical_tool_name(tool_name);
|
||||
// Check permanent first, then session, then once
|
||||
self.permanent_records
|
||||
.get(tool_name)
|
||||
.get(canonical)
|
||||
.map(|r| r.scope == ConsentScope::Permanent)
|
||||
.or_else(|| {
|
||||
self.session_records
|
||||
.get(tool_name)
|
||||
.get(canonical)
|
||||
.map(|r| r.scope == ConsentScope::Session)
|
||||
})
|
||||
.or_else(|| {
|
||||
self.once_records
|
||||
.get(tool_name)
|
||||
.get(canonical)
|
||||
.map(|r| r.scope == ConsentScope::Once)
|
||||
})
|
||||
.unwrap_or(false)
|
||||
@@ -224,13 +230,15 @@ impl ConsentManager {
|
||||
|
||||
/// Consume "once" consent for a tool (clears it after first use)
|
||||
pub fn consume_once_consent(&mut self, tool_name: &str) {
|
||||
self.once_records.remove(tool_name);
|
||||
let canonical = canonical_tool_name(tool_name);
|
||||
self.once_records.remove(canonical);
|
||||
}
|
||||
|
||||
pub fn revoke_consent(&mut self, tool_name: &str) {
|
||||
self.permanent_records.remove(tool_name);
|
||||
self.session_records.remove(tool_name);
|
||||
self.once_records.remove(tool_name);
|
||||
let canonical = canonical_tool_name(tool_name);
|
||||
self.permanent_records.remove(canonical);
|
||||
self.session_records.remove(canonical);
|
||||
self.once_records.remove(canonical);
|
||||
}
|
||||
|
||||
pub fn clear_all_consent(&mut self) {
|
||||
@@ -253,10 +261,11 @@ impl ConsentManager {
|
||||
data_types: Vec<String>,
|
||||
endpoints: Vec<String>,
|
||||
) -> Option<(String, Vec<String>, Vec<String>)> {
|
||||
if self.has_consent(tool_name) {
|
||||
let canonical = canonical_tool_name(tool_name);
|
||||
if self.has_consent(canonical) {
|
||||
return None;
|
||||
}
|
||||
Some((tool_name.to_string(), data_types, endpoints))
|
||||
Some((canonical.to_string(), data_types, endpoints))
|
||||
}
|
||||
|
||||
fn show_consent_dialog(
|
||||
|
||||
Reference in New Issue
Block a user