1#![allow(clippy::items_after_test_module)]
2
3use std::collections::{BTreeMap, BTreeSet};
4use std::sync::{Arc, RwLock};
5
6use serde::{Deserialize, Serialize};
7
8use crate::{
9 PreparedToolCall, ProgressSender, ToolCall, ToolContext, ToolContract, ToolManifest,
10 ToolPrepareCall, ToolProvider, ToolResult,
11};
12
13const PLUGIN_SOURCE_ID: &str = "plugins";
14
15#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
16#[serde(transparent)]
17pub struct ToolSourceHandle {
18 id: String,
19}
20
21impl ToolSourceHandle {
22 pub(crate) fn new(id: impl Into<String>) -> Self {
23 Self { id: id.into() }
24 }
25
26 pub(crate) fn as_str(&self) -> &str {
27 &self.id
28 }
29}
30
31#[derive(Clone, Debug, Serialize, Deserialize)]
32pub struct ToolStateEntry {
33 manifest: ToolManifest,
34 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
39 orphaned: bool,
40}
41
42impl ToolStateEntry {
43 #[cfg(test)]
44 pub(crate) fn new(manifest: ToolManifest) -> Self {
45 Self {
46 manifest,
47 orphaned: false,
48 }
49 }
50
51 pub fn manifest(&self) -> ToolManifest {
55 let mut manifest = self.manifest.clone();
56 if self.orphaned {
57 manifest.availability_override = Some(crate::ToolAvailability::Off);
58 }
59 manifest
60 }
61
62 fn stored_manifest(&self) -> &ToolManifest {
63 &self.manifest
64 }
65
66 pub fn is_orphaned(&self) -> bool {
67 self.orphaned
68 }
69}
70
71#[derive(Clone, Debug, Default)]
72pub struct ToolState {
73 generation: u64,
74 tools: Arc<BTreeMap<String, ToolStateEntry>>,
75}
76
77impl ToolState {
78 pub(crate) fn new(generation: u64, tools: BTreeMap<String, ToolStateEntry>) -> Self {
79 Self {
80 generation,
81 tools: Arc::new(tools),
82 }
83 }
84
85 pub fn generation(&self) -> u64 {
86 self.generation
87 }
88
89 pub fn with_generation(mut self, generation: u64) -> Self {
90 self.generation = generation;
91 self
92 }
93
94 pub fn tool_manifests(&self) -> Vec<ToolManifest> {
95 self.tools.values().map(ToolStateEntry::manifest).collect()
96 }
97
98 pub fn get(&self, name: &str) -> Option<&ToolStateEntry> {
99 self.tools.get(name)
100 }
101
102 pub fn manifest_mut(&mut self, name: &str) -> Option<&mut ToolManifest> {
103 Arc::make_mut(&mut self.tools)
104 .get_mut(name)
105 .map(|entry| &mut entry.manifest)
106 }
107
108 pub fn contains(&self, name: &str) -> bool {
109 self.tools.contains_key(name)
110 }
111
112 pub fn is_empty(&self) -> bool {
113 self.tools.is_empty()
114 }
115
116 pub fn len(&self) -> usize {
117 self.tools.len()
118 }
119
120 pub fn iter(&self) -> impl Iterator<Item = (&str, &ToolStateEntry)> {
121 self.tools
122 .iter()
123 .map(|(name, entry)| (name.as_str(), entry))
124 }
125
126 pub fn set_availability(
127 &mut self,
128 name: &str,
129 availability: Option<crate::ToolAvailability>,
130 ) -> Result<(), ReconfigureError> {
131 let Some(entry) = Arc::make_mut(&mut self.tools).get_mut(name) else {
132 return Err(ReconfigureError::Validation(format!(
133 "unknown tool `{name}`"
134 )));
135 };
136 entry.manifest.availability_override = availability;
137 Ok(())
138 }
139
140 pub fn retain(&mut self, mut keep: impl FnMut(&str, &ToolStateEntry) -> bool) {
141 Arc::make_mut(&mut self.tools).retain(|name, entry| keep(name, entry));
142 }
143
144 pub fn remove(&mut self, name: &str) -> Option<ToolStateEntry> {
145 Arc::make_mut(&mut self.tools).remove(name)
146 }
147
148 pub(crate) fn entries(&self) -> &BTreeMap<String, ToolStateEntry> {
149 self.tools.as_ref()
150 }
151}
152
153impl Serialize for ToolState {
154 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
155 where
156 S: serde::Serializer,
157 {
158 #[derive(Serialize)]
159 struct ToolStateRef<'a> {
160 generation: u64,
161 tools: &'a BTreeMap<String, ToolStateEntry>,
162 }
163
164 ToolStateRef {
165 generation: self.generation,
166 tools: self.tools.as_ref(),
167 }
168 .serialize(serializer)
169 }
170}
171
172impl<'de> Deserialize<'de> for ToolState {
173 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
174 where
175 D: serde::Deserializer<'de>,
176 {
177 #[derive(Deserialize)]
178 struct ToolStateOwned {
179 generation: u64,
180 tools: BTreeMap<String, ToolStateEntry>,
181 }
182
183 let owned = ToolStateOwned::deserialize(deserializer)?;
184 Ok(Self {
185 generation: owned.generation,
186 tools: Arc::new(owned.tools),
187 })
188 }
189}
190
191#[async_trait::async_trait]
192pub(crate) trait ToolSourceExecutor: Send + Sync + 'static {
193 fn id(&self) -> &str;
194 fn advertised_tools(&self) -> Vec<ToolManifest>;
195 fn resolve_manifest(&self, name: &str) -> Option<ToolManifest> {
196 self.advertised_tools()
197 .into_iter()
198 .find(|manifest| manifest.name == name)
199 }
200 fn resolve_contract(&self, name: &str) -> Option<Arc<ToolContract>>;
201 async fn prepare_tool_call(
202 &self,
203 call: ToolPrepareCall<'_>,
204 ) -> Result<PreparedToolCall, ToolResult> {
205 Ok(PreparedToolCall::identity(call.pending))
206 }
207 async fn execute(
208 &self,
209 tool: &str,
210 args: &serde_json::Value,
211 context: &ToolContext<'_>,
212 progress: Option<&ProgressSender>,
213 ) -> ToolResult;
214}
215
216struct ToolProviderSource {
217 id: String,
218 provider: Arc<dyn ToolProvider>,
219}
220
221impl ToolProviderSource {
222 fn new(id: impl Into<String>, provider: Arc<dyn ToolProvider>) -> Self {
223 Self {
224 id: id.into(),
225 provider,
226 }
227 }
228}
229
230struct ToolProviderGroupSource {
231 id: String,
232 tools: RwLock<BTreeMap<String, (ToolManifest, usize)>>,
233 providers: Vec<Arc<dyn ToolProvider>>,
234}
235
236impl ToolProviderGroupSource {
237 fn new(id: impl Into<String>, providers: Vec<Arc<dyn ToolProvider>>) -> Self {
238 let mut tools = BTreeMap::new();
239 for (provider_idx, provider) in providers.iter().enumerate() {
240 for manifest in provider.tool_manifests() {
241 tools.insert(manifest.name.clone(), (manifest, provider_idx));
242 }
243 }
244 Self {
245 id: id.into(),
246 tools: RwLock::new(tools),
247 providers,
248 }
249 }
250
251 fn read_advertised_tools(&self) -> Vec<ToolManifest> {
252 let mut tools = BTreeMap::new();
253 for (provider_idx, provider) in self.providers.iter().enumerate() {
254 for manifest in provider.tool_manifests() {
255 tools.insert(manifest.name.clone(), (manifest, provider_idx));
256 }
257 }
258 let manifests = tools
259 .values()
260 .map(|(manifest, _)| manifest.clone())
261 .collect::<Vec<_>>();
262 *self
263 .tools
264 .write()
265 .expect("tool provider group lock poisoned") = tools;
266 manifests
267 }
268
269 fn provider_index_for(&self, name: &str) -> Option<usize> {
270 self.resolve_manifest(name).and_then(|_| {
271 self.tools
272 .read()
273 .expect("tool provider group lock poisoned")
274 .get(name)
275 .map(|(_, provider_idx)| *provider_idx)
276 })
277 }
278}
279
280#[async_trait::async_trait]
281impl ToolSourceExecutor for ToolProviderGroupSource {
282 fn id(&self) -> &str {
283 &self.id
284 }
285
286 fn advertised_tools(&self) -> Vec<ToolManifest> {
287 self.read_advertised_tools()
288 }
289
290 fn resolve_manifest(&self, name: &str) -> Option<ToolManifest> {
291 if let Some((manifest, _)) = self
292 .tools
293 .read()
294 .expect("tool provider group lock poisoned")
295 .get(name)
296 {
297 return Some(manifest.clone());
298 }
299 for (provider_idx, provider) in self.providers.iter().enumerate() {
300 if let Some(manifest) = provider.resolve_manifest(name) {
301 self.tools
302 .write()
303 .expect("tool provider group lock poisoned")
304 .insert(name.to_string(), (manifest.clone(), provider_idx));
305 return Some(manifest);
306 }
307 }
308 None
309 }
310
311 fn resolve_contract(&self, name: &str) -> Option<Arc<ToolContract>> {
312 let provider_idx = self.provider_index_for(name)?;
313 self.providers[provider_idx].resolve_contract(name)
314 }
315
316 async fn prepare_tool_call(
317 &self,
318 call: ToolPrepareCall<'_>,
319 ) -> Result<PreparedToolCall, ToolResult> {
320 let name = call.pending.tool_name.clone();
321 let Some(provider_idx) = self.provider_index_for(&name) else {
322 return Err(ToolResult::err_fmt(format_args!("Unknown tool: {name}")));
323 };
324 self.providers[provider_idx].prepare_tool_call(call).await
325 }
326
327 async fn execute(
328 &self,
329 tool: &str,
330 args: &serde_json::Value,
331 context: &ToolContext<'_>,
332 progress: Option<&ProgressSender>,
333 ) -> ToolResult {
334 let Some(provider_idx) = self.provider_index_for(tool) else {
335 return ToolResult::err_fmt(format_args!("Unknown tool: {tool}"));
336 };
337 self.providers[provider_idx]
338 .execute(ToolCall {
339 name: tool,
340 args,
341 context,
342 progress,
343 })
344 .await
345 }
346}
347
348#[async_trait::async_trait]
349impl ToolSourceExecutor for ToolProviderSource {
350 fn id(&self) -> &str {
351 &self.id
352 }
353
354 fn advertised_tools(&self) -> Vec<ToolManifest> {
355 self.provider.tool_manifests()
356 }
357
358 fn resolve_manifest(&self, name: &str) -> Option<ToolManifest> {
359 self.provider.resolve_manifest(name)
360 }
361
362 fn resolve_contract(&self, name: &str) -> Option<Arc<ToolContract>> {
363 self.provider.resolve_contract(name)
364 }
365
366 async fn prepare_tool_call(
367 &self,
368 call: ToolPrepareCall<'_>,
369 ) -> Result<PreparedToolCall, ToolResult> {
370 self.provider.prepare_tool_call(call).await
371 }
372
373 async fn execute(
374 &self,
375 tool: &str,
376 args: &serde_json::Value,
377 context: &ToolContext<'_>,
378 progress: Option<&ProgressSender>,
379 ) -> ToolResult {
380 self.provider
381 .execute(ToolCall {
382 name: tool,
383 args,
384 context,
385 progress,
386 })
387 .await
388 }
389}
390
391#[derive(Clone, Debug, PartialEq, Eq)]
393enum ToolBinding {
394 Bound(String),
396 Orphaned,
400}
401
402impl ToolBinding {
403 fn source_id(&self) -> Option<&str> {
404 match self {
405 Self::Bound(id) => Some(id),
406 Self::Orphaned => None,
407 }
408 }
409}
410
411#[derive(Clone)]
412struct ToolRegistryEntry {
413 manifest: ToolManifest,
414 binding: ToolBinding,
415}
416
417impl ToolRegistryEntry {
418 fn new(manifest: ToolManifest, source_id: impl Into<String>) -> Self {
419 Self {
420 manifest,
421 binding: ToolBinding::Bound(source_id.into()),
422 }
423 }
424
425 fn orphaned(manifest: ToolManifest) -> Self {
426 Self {
427 manifest,
428 binding: ToolBinding::Orphaned,
429 }
430 }
431
432 fn is_orphaned(&self) -> bool {
433 self.binding == ToolBinding::Orphaned
434 }
435
436 fn view_manifest(&self) -> ToolManifest {
441 let mut manifest = self.manifest.clone();
442 if self.is_orphaned() {
443 manifest.availability_override = Some(crate::ToolAvailability::Off);
444 }
445 manifest
446 }
447
448 fn export(&self) -> ToolStateEntry {
449 ToolStateEntry {
450 manifest: self.manifest.clone(),
451 orphaned: self.is_orphaned(),
452 }
453 }
454}
455
456#[derive(Clone)]
457struct ToolRegistryState {
458 generation: u64,
459 tools: BTreeMap<String, ToolRegistryEntry>,
460 next_live_source_id: u64,
461}
462
463#[derive(Clone, Debug, Default)]
468pub struct ToolRestoreReport {
469 pub generation: u64,
470 pub orphaned: Vec<String>,
471}
472
473#[derive(Debug, thiserror::Error)]
474pub enum ReconfigureError {
475 #[error("validation error: {0}")]
476 Validation(String),
477 #[error("unknown tool source: {0}")]
478 UnknownSource(String),
479 #[error("generation mismatch: expected {expected}, actual {actual}")]
480 GenerationMismatch { expected: u64, actual: u64 },
481}
482
483#[derive(Clone)]
484pub struct ToolRegistry {
485 sources: Arc<RwLock<BTreeMap<String, Arc<dyn ToolSourceExecutor>>>>,
486 state: Arc<RwLock<ToolRegistryState>>,
487}
488
489#[derive(Clone, Copy, Debug, PartialEq, Eq)]
490enum SourceReconcilePolicy {
491 RejectExternalConflicts,
492 OverlayReplacingConflicts,
493}
494
495impl ToolRegistry {
496 pub fn from_tool_provider(provider: Arc<dyn ToolProvider>) -> Result<Self, ReconfigureError> {
497 let registry = Self::empty();
498 registry.upsert_source(Arc::new(ToolProviderSource::new(
499 PLUGIN_SOURCE_ID,
500 provider,
501 )))?;
502 Ok(registry)
503 }
504
505 pub(crate) fn from_tool_providers(
506 providers: Vec<Arc<dyn ToolProvider>>,
507 ) -> Result<Self, ReconfigureError> {
508 let registry = Self::empty();
509 registry.upsert_source(Arc::new(ToolProviderGroupSource::new(
510 PLUGIN_SOURCE_ID,
511 providers,
512 )))?;
513 Ok(registry)
514 }
515
516 pub(crate) fn empty() -> Self {
517 Self {
518 sources: Arc::new(RwLock::new(BTreeMap::new())),
519 state: Arc::new(RwLock::new(ToolRegistryState {
520 generation: 0,
521 tools: BTreeMap::new(),
522 next_live_source_id: 0,
523 })),
524 }
525 }
526
527 pub fn generation(&self) -> u64 {
528 self.state
529 .read()
530 .expect("tool registry state lock poisoned")
531 .generation
532 }
533
534 pub fn export_state(&self) -> ToolState {
535 let state = self
536 .state
537 .read()
538 .expect("tool registry state lock poisoned");
539 ToolState::new(state.generation, export_tool_state_entries(&state.tools))
540 }
541
542 pub fn apply_state(&self, next: ToolState) -> Result<u64, ReconfigureError> {
543 let current_generation = self.generation();
544 if next.generation != current_generation {
545 return Err(ReconfigureError::GenerationMismatch {
546 expected: next.generation,
547 actual: current_generation,
548 });
549 }
550
551 validate_unique_manifest_entries(next.entries().values())?;
552 let rebound = {
553 let sources = self.sources.read().expect("tool source lock poisoned");
554 rebind_tool_state_entries(next.entries(), &sources, RebindMode::RejectUnresolved)?
555 };
556
557 let mut state = self
558 .state
559 .write()
560 .expect("tool registry state lock poisoned");
561 if state.generation != next.generation {
562 return Err(ReconfigureError::GenerationMismatch {
563 expected: next.generation,
564 actual: state.generation,
565 });
566 }
567 state.tools = rebound.tools;
568 state.generation += 1;
569 Ok(state.generation)
570 }
571
572 pub fn restore_state(
592 &self,
593 snapshot: ToolState,
594 ) -> Result<ToolRestoreReport, ReconfigureError> {
595 validate_unique_manifest_entries(snapshot.entries().values())?;
596 let rebound = {
597 let sources = self.sources.read().expect("tool source lock poisoned");
598 rebind_tool_state_entries(snapshot.entries(), &sources, RebindMode::OrphanUnresolved)?
599 };
600
601 let mut state = self
602 .state
603 .write()
604 .expect("tool registry state lock poisoned");
605 state.tools = rebound.tools;
606 state.generation = snapshot.generation();
607 Ok(ToolRestoreReport {
608 generation: state.generation,
609 orphaned: rebound.orphaned,
610 })
611 }
612
613 pub fn add_tool_provider(
614 &self,
615 provider: Arc<dyn ToolProvider>,
616 ) -> Result<ToolSourceHandle, ReconfigureError> {
617 let source_id = {
618 let mut state = self
619 .state
620 .write()
621 .expect("tool registry state lock poisoned");
622 state.next_live_source_id += 1;
623 format!("live:{}", state.next_live_source_id)
624 };
625 self.upsert_source(Arc::new(ToolProviderSource::new(
626 source_id.clone(),
627 provider,
628 )))?;
629 Ok(ToolSourceHandle::new(source_id))
630 }
631
632 pub(crate) fn compose_session_surface(
633 &self,
634 include_base_tools: bool,
635 context_providers: Vec<Arc<dyn ToolProvider>>,
636 ) -> Result<Self, ReconfigureError> {
637 let registry = if include_base_tools {
638 self.fork_with_state(self.export_state())?
639 } else {
640 Self::empty()
641 };
642 registry.upsert_overlay_source(Arc::new(ToolProviderGroupSource::new(
643 "context",
644 context_providers,
645 )))?;
646 Ok(registry)
647 }
648
649 pub(crate) fn upsert_source(
650 &self,
651 source: Arc<dyn ToolSourceExecutor>,
652 ) -> Result<u64, ReconfigureError> {
653 self.reconcile_source(source, SourceReconcilePolicy::RejectExternalConflicts)
654 }
655
656 fn upsert_overlay_source(
657 &self,
658 source: Arc<dyn ToolSourceExecutor>,
659 ) -> Result<u64, ReconfigureError> {
660 self.reconcile_source(source, SourceReconcilePolicy::OverlayReplacingConflicts)
661 }
662
663 fn reconcile_source(
664 &self,
665 source: Arc<dyn ToolSourceExecutor>,
666 policy: SourceReconcilePolicy,
667 ) -> Result<u64, ReconfigureError> {
668 let source_id = source.id().to_string();
669 let advertised_tools = source
670 .advertised_tools()
671 .into_iter()
672 .map(|manifest| manifest_with_compact_contract(source.as_ref(), manifest))
673 .collect::<Vec<_>>();
674 validate_unique_manifests(&advertised_tools)?;
675
676 let advertised_names = advertised_tools
677 .iter()
678 .map(|manifest| manifest.name.clone())
679 .collect::<BTreeSet<_>>();
680 let advertised_ids = advertised_tools
681 .iter()
682 .map(|manifest| manifest.id.clone())
683 .collect::<BTreeSet<_>>();
684 let mut state = self
685 .state
686 .write()
687 .expect("tool registry state lock poisoned");
688 let previous_overrides = state
689 .tools
690 .iter()
691 .map(|(name, entry)| (name.clone(), entry.manifest.availability_override))
692 .collect::<BTreeMap<_, _>>();
693 match policy {
694 SourceReconcilePolicy::RejectExternalConflicts => {
695 for manifest in &advertised_tools {
700 if let Some(existing) = state.tools.get(&manifest.name)
701 && let Some(existing_source) = existing.binding.source_id()
702 && existing_source != source_id
703 {
704 return Err(ReconfigureError::Validation(format!(
705 "duplicate tool name `{}` from source `{}` conflicts with source `{}`",
706 manifest.name, source_id, existing_source
707 )));
708 }
709 if let Some((existing_name, existing_source)) =
710 state.tools.iter().find_map(|(name, entry)| {
711 let existing_source = entry.binding.source_id()?;
712 (existing_source != source_id && entry.manifest.id == manifest.id)
713 .then(|| (name.clone(), existing_source.to_string()))
714 })
715 {
716 return Err(ReconfigureError::Validation(format!(
717 "duplicate tool id `{}` from source `{}` conflicts with tool `{}` from source `{}`",
718 manifest.id, source_id, existing_name, existing_source
719 )));
720 }
721 }
722 state.tools.retain(|name, entry| match &entry.binding {
723 ToolBinding::Bound(bound) => bound != &source_id,
726 ToolBinding::Orphaned => {
728 !advertised_names.contains(name)
729 && !advertised_ids.contains(&entry.manifest.id)
730 }
731 });
732 }
733 SourceReconcilePolicy::OverlayReplacingConflicts => {
734 state.tools.retain(|name, entry| {
735 entry.binding.source_id() != Some(source_id.as_str())
736 && !advertised_names.contains(name)
737 && !advertised_ids.contains(&entry.manifest.id)
738 });
739 }
740 }
741 for mut manifest in advertised_tools {
742 let name = manifest.name.clone();
743 manifest.availability_override = previous_overrides
744 .get(&name)
745 .copied()
746 .flatten()
747 .or(manifest.availability_override);
748 state
749 .tools
750 .insert(name, ToolRegistryEntry::new(manifest, source_id.clone()));
751 }
752 self.sources
753 .write()
754 .expect("tool source lock poisoned")
755 .insert(source_id, source);
756 state.generation += 1;
757 Ok(state.generation)
758 }
759
760 pub fn remove_source(&self, handle: &ToolSourceHandle) -> Result<u64, ReconfigureError> {
761 self.remove_source_id(handle.as_str())
762 }
763
764 pub fn refresh_sources(&self) -> Result<u64, ReconfigureError> {
765 let sources = self
766 .sources
767 .read()
768 .expect("tool source lock poisoned")
769 .values()
770 .cloned()
771 .collect::<Vec<_>>();
772 let mut generation = self.generation();
773 for source in sources {
774 generation = self.upsert_source(source)?;
775 }
776 Ok(generation)
777 }
778
779 pub(crate) fn remove_source_id(&self, source_id: &str) -> Result<u64, ReconfigureError> {
780 {
781 let mut sources = self.sources.write().expect("tool source lock poisoned");
782 if sources.remove(source_id).is_none() {
783 return Err(ReconfigureError::UnknownSource(source_id.to_string()));
784 }
785 }
786 let mut state = self
787 .state
788 .write()
789 .expect("tool registry state lock poisoned");
790 state
791 .tools
792 .retain(|_, entry| entry.binding.source_id() != Some(source_id));
793 state.generation += 1;
794 Ok(state.generation)
795 }
796
797 pub(crate) fn fork_with_state(&self, snapshot: ToolState) -> Result<Self, ReconfigureError> {
798 let sources = self
799 .sources
800 .read()
801 .expect("tool source lock poisoned")
802 .iter()
803 .map(|(k, v)| (k.clone(), Arc::clone(v)))
804 .collect::<BTreeMap<_, _>>();
805 validate_unique_manifest_entries(snapshot.entries().values())?;
806 let rebound =
809 rebind_tool_state_entries(snapshot.entries(), &sources, RebindMode::OrphanUnresolved)?;
810 let generation = snapshot.generation.max(1);
811 Ok(Self {
812 sources: Arc::new(RwLock::new(sources)),
813 state: Arc::new(RwLock::new(ToolRegistryState {
814 generation,
815 tools: rebound.tools,
816 next_live_source_id: 0,
817 })),
818 })
819 }
820}
821
822impl ToolRegistry {
823 fn try_rebind_orphan(&self, name: &str, orphan_id: &crate::ToolId) -> Option<ToolManifest> {
829 let sources = self
830 .sources
831 .read()
832 .expect("tool source lock poisoned")
833 .iter()
834 .map(|(source_id, source)| (source_id.clone(), Arc::clone(source)))
835 .collect::<Vec<_>>();
836 for (source_id, source) in sources {
837 let Some(manifest) = source.resolve_manifest(name) else {
838 continue;
839 };
840 if manifest.id != *orphan_id {
841 continue;
842 }
843 let mut manifest = manifest_with_compact_contract(source.as_ref(), manifest);
844 let mut state = self
845 .state
846 .write()
847 .expect("tool registry state lock poisoned");
848 let existing = state.tools.get(name)?;
849 if !existing.is_orphaned() {
850 return Some(existing.view_manifest());
851 }
852 manifest.availability_override = existing
853 .manifest
854 .availability_override
855 .or(manifest.availability_override);
856 state.tools.insert(
857 name.to_string(),
858 ToolRegistryEntry::new(manifest.clone(), source_id),
859 );
860 state.generation += 1;
861 return Some(manifest);
862 }
863 None
864 }
865
866 fn resolve_execution_source(
869 &self,
870 name: &str,
871 ) -> Result<Arc<dyn ToolSourceExecutor>, ToolResult> {
872 if self.resolve_manifest(name).is_none() {
873 return Err(ToolResult::err_fmt(format_args!("Unknown tool: {name}")));
874 }
875 let binding = {
876 let state = self
877 .state
878 .read()
879 .expect("tool registry state lock poisoned");
880 state.tools.get(name).map(|entry| entry.binding.clone())
881 };
882 let source_id = match binding {
883 Some(ToolBinding::Bound(source_id)) => source_id,
884 Some(ToolBinding::Orphaned) => {
885 return Err(ToolResult::err_fmt(format_args!(
886 "Tool `{name}` is unavailable: it was restored from a persisted session \
887 but its source is not currently registered"
888 )));
889 }
890 None => return Err(ToolResult::err_fmt(format_args!("Unknown tool: {name}"))),
891 };
892 let source = {
893 self.sources
894 .read()
895 .expect("tool source lock poisoned")
896 .get(&source_id)
897 .cloned()
898 };
899 source.ok_or_else(|| {
900 ToolResult::err_fmt(format_args!("Tool source missing for tool `{name}`"))
901 })
902 }
903}
904
905#[async_trait::async_trait]
906impl ToolProvider for ToolRegistry {
907 fn tool_manifests(&self) -> Vec<ToolManifest> {
908 let state = self
909 .state
910 .read()
911 .expect("tool registry state lock poisoned");
912 state
913 .tools
914 .values()
915 .map(ToolRegistryEntry::view_manifest)
916 .collect()
917 }
918
919 fn resolve_manifest(&self, name: &str) -> Option<ToolManifest> {
920 enum Known {
921 Bound(ToolManifest),
922 Orphaned(ToolManifest, crate::ToolId),
923 }
924 let known = {
925 let state = self
926 .state
927 .read()
928 .expect("tool registry state lock poisoned");
929 state.tools.get(name).map(|entry| {
930 if entry.is_orphaned() {
931 Known::Orphaned(entry.view_manifest(), entry.manifest.id.clone())
932 } else {
933 Known::Bound(entry.manifest.clone())
934 }
935 })
936 };
937 match known {
938 Some(Known::Bound(manifest)) => return Some(manifest),
939 Some(Known::Orphaned(off_manifest, orphan_id)) => {
940 return Some(
943 self.try_rebind_orphan(name, &orphan_id)
944 .unwrap_or(off_manifest),
945 );
946 }
947 None => {}
948 }
949
950 let sources = self
951 .sources
952 .read()
953 .expect("tool source lock poisoned")
954 .iter()
955 .map(|(source_id, source)| (source_id.clone(), Arc::clone(source)))
956 .collect::<Vec<_>>();
957 for (source_id, source) in sources {
958 let Some(manifest) = source.resolve_manifest(name) else {
959 continue;
960 };
961 let mut manifest = manifest_with_compact_contract(source.as_ref(), manifest);
962 let previous_override = {
963 let state = self
964 .state
965 .read()
966 .expect("tool registry state lock poisoned");
967 state
968 .tools
969 .get(&manifest.name)
970 .and_then(|entry| entry.manifest.availability_override)
971 };
972 manifest.availability_override = previous_override.or(manifest.availability_override);
973 let mut state = self
974 .state
975 .write()
976 .expect("tool registry state lock poisoned");
977 if let Some(existing) = state.tools.get(&manifest.name) {
978 return (existing.binding.source_id() == Some(source_id.as_str()))
979 .then(|| existing.view_manifest());
980 }
981 if let Some((_, existing)) = state
982 .tools
983 .iter()
984 .find(|(_, entry)| entry.manifest.id == manifest.id)
985 {
986 return (existing.binding.source_id() == Some(source_id.as_str()))
987 .then(|| existing.view_manifest());
988 }
989 state.tools.insert(
990 manifest.name.clone(),
991 ToolRegistryEntry::new(manifest.clone(), source_id),
992 );
993 state.generation += 1;
994 return Some(manifest);
995 }
996 None
997 }
998
999 fn resolve_contract(&self, name: &str) -> Option<Arc<ToolContract>> {
1000 let source_id = self.resolve_manifest(name).and_then(|_| {
1001 let state = self
1002 .state
1003 .read()
1004 .expect("tool registry state lock poisoned");
1005 state
1006 .tools
1007 .get(name)
1008 .and_then(|entry| entry.binding.source_id().map(str::to_string))
1009 })?;
1010 self.sources
1011 .read()
1012 .expect("tool source lock poisoned")
1013 .get(&source_id)?
1014 .resolve_contract(name)
1015 }
1016
1017 async fn prepare_tool_call(
1018 &self,
1019 call: ToolPrepareCall<'_>,
1020 ) -> Result<PreparedToolCall, ToolResult> {
1021 let name = call.pending.tool_name.clone();
1022 let source = self.resolve_execution_source(&name)?;
1023 source.prepare_tool_call(call).await
1024 }
1025
1026 async fn execute(&self, call: ToolCall<'_>) -> ToolResult {
1027 let name = call.name;
1028 let source = match self.resolve_execution_source(name) {
1029 Ok(source) => source,
1030 Err(result) => return result,
1031 };
1032 source
1033 .execute(name, call.args, call.context, call.progress)
1034 .await
1035 }
1036}
1037
1038fn validate_unique_manifests(manifests: &[ToolManifest]) -> Result<(), ReconfigureError> {
1039 let mut names = BTreeSet::new();
1040 let mut ids = BTreeSet::new();
1041 for manifest in manifests {
1042 if manifest.id.as_str().trim().is_empty() {
1043 return Err(ReconfigureError::Validation(
1044 "tool id cannot be empty".to_string(),
1045 ));
1046 }
1047 if !ids.insert(manifest.id.clone()) {
1048 return Err(ReconfigureError::Validation(format!(
1049 "duplicate tool id `{}` in source",
1050 manifest.id
1051 )));
1052 }
1053 if manifest.name.trim().is_empty() {
1054 return Err(ReconfigureError::Validation(
1055 "tool name cannot be empty".to_string(),
1056 ));
1057 }
1058 if !names.insert(manifest.name.clone()) {
1059 return Err(ReconfigureError::Validation(format!(
1060 "duplicate tool name `{}` in source",
1061 manifest.name
1062 )));
1063 }
1064 }
1065 Ok(())
1066}
1067
1068fn manifest_with_compact_contract(
1069 source: &dyn ToolSourceExecutor,
1070 mut manifest: ToolManifest,
1071) -> ToolManifest {
1072 if manifest.compact_contract.is_none()
1073 && let Some(contract) = source.resolve_contract(&manifest.name)
1074 {
1075 manifest.compact_contract = Some(contract.compact_contract(&manifest));
1076 }
1077 manifest
1078}
1079
1080fn export_tool_state_entries(
1081 entries: &BTreeMap<String, ToolRegistryEntry>,
1082) -> BTreeMap<String, ToolStateEntry> {
1083 entries
1084 .iter()
1085 .map(|(name, entry)| (name.clone(), entry.export()))
1086 .collect()
1087}
1088
1089#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1092enum RebindMode {
1093 OrphanUnresolved,
1096 RejectUnresolved,
1100}
1101
1102struct ReboundTools {
1103 tools: BTreeMap<String, ToolRegistryEntry>,
1104 orphaned: Vec<String>,
1105}
1106
1107fn rebind_tool_state_entries(
1108 entries: &BTreeMap<String, ToolStateEntry>,
1109 sources: &BTreeMap<String, Arc<dyn ToolSourceExecutor>>,
1110 mode: RebindMode,
1111) -> Result<ReboundTools, ReconfigureError> {
1112 let mut rebound = BTreeMap::new();
1113 let mut orphaned = Vec::new();
1114 for (name, entry) in entries {
1115 if name != &entry.manifest.name {
1116 return Err(ReconfigureError::Validation(format!(
1117 "tool state key `{}` does not match manifest name `{}`",
1118 name, entry.manifest.name
1119 )));
1120 }
1121
1122 let mut name_matches = Vec::new();
1123 for (source_id, source) in sources {
1124 let Some(manifest) = source.resolve_manifest(name) else {
1125 continue;
1126 };
1127 name_matches.push((
1128 source_id.clone(),
1129 manifest_with_compact_contract(source.as_ref(), manifest),
1130 ));
1131 }
1132
1133 if name_matches.is_empty() {
1134 if mode == RebindMode::RejectUnresolved && !entry.orphaned {
1135 return Err(ReconfigureError::Validation(format!(
1136 "no registered tool source resolves tool `{name}`"
1137 )));
1138 }
1139 orphaned.push(name.clone());
1140 rebound.insert(
1141 name.clone(),
1142 ToolRegistryEntry::orphaned(entry.manifest.clone()),
1143 );
1144 continue;
1145 }
1146
1147 let matching_id = name_matches
1148 .iter()
1149 .filter(|(_, manifest)| manifest.id == entry.manifest.id)
1150 .collect::<Vec<_>>();
1151
1152 if matching_id.len() == 1 {
1153 let source_id = matching_id[0].0.clone();
1154 rebound.insert(
1155 name.clone(),
1156 ToolRegistryEntry::new(entry.manifest.clone(), source_id),
1157 );
1158 } else if matching_id.is_empty() {
1159 let resolved_ids = name_matches
1160 .iter()
1161 .map(|(_, manifest)| manifest.id.as_str())
1162 .collect::<Vec<_>>()
1163 .join(", ");
1164 return Err(ReconfigureError::Validation(format!(
1165 "tool `{name}` resolved with id(s) `{resolved_ids}`, expected `{}`",
1166 entry.manifest.id
1167 )));
1168 } else {
1169 return Err(ReconfigureError::Validation(format!(
1170 "tool `{name}` with id `{}` is resolved by multiple registered sources",
1171 entry.manifest.id
1172 )));
1173 }
1174 }
1175 Ok(ReboundTools {
1176 tools: rebound,
1177 orphaned,
1178 })
1179}
1180
1181fn validate_unique_manifest_entries<'a>(
1182 entries: impl IntoIterator<Item = &'a ToolStateEntry>,
1183) -> Result<(), ReconfigureError> {
1184 let manifests = entries
1185 .into_iter()
1186 .map(|entry| entry.stored_manifest().clone())
1187 .collect::<Vec<_>>();
1188 validate_unique_manifests(&manifests)
1189}
1190
1191#[cfg(test)]
1192mod tests {
1193 use super::*;
1194 use crate::ToolDefinition;
1195 use serde_json::json;
1196 use std::sync::atomic::{AtomicUsize, Ordering};
1197
1198 struct MockTool;
1199 struct MixedEnabledTool;
1200 struct ExternalMockSource;
1201 struct ExactResolvingSource {
1202 manifest_resolutions: Arc<AtomicUsize>,
1203 contract_resolutions: Arc<AtomicUsize>,
1204 executions: Arc<AtomicUsize>,
1205 }
1206 struct NamedExactSource {
1207 id: &'static str,
1208 }
1209 struct DynamicToolProvider {
1210 names: Arc<std::sync::Mutex<Vec<String>>>,
1211 }
1212
1213 fn test_tool(
1214 name: &str,
1215 description: &str,
1216 availability: crate::ToolAvailabilityConfig,
1217 ) -> ToolDefinition {
1218 ToolDefinition::raw_with_id(
1219 format!("tool:{name}"),
1220 name,
1221 description,
1222 ToolDefinition::default_input_schema(),
1223 json!({ "type": "string" }),
1224 )
1225 .with_availability(availability)
1226 }
1227
1228 fn manifests(definitions: Vec<ToolDefinition>) -> Vec<ToolManifest> {
1229 definitions
1230 .into_iter()
1231 .map(|tool| tool.manifest())
1232 .collect()
1233 }
1234
1235 fn contract_from(definitions: Vec<ToolDefinition>, name: &str) -> Option<Arc<ToolContract>> {
1236 definitions
1237 .into_iter()
1238 .find(|tool| tool.name() == name)
1239 .map(|tool| Arc::new(tool.contract()))
1240 }
1241
1242 fn dynamic_definition(name: &str) -> ToolDefinition {
1243 test_tool(name, "dynamic", crate::ToolAvailabilityConfig::callable())
1244 }
1245
1246 fn test_tool_context() -> crate::ToolContext<'static> {
1247 crate::ToolContext::builder(
1248 "registry-test".to_string(),
1249 Arc::new(crate::testing::MockSessionManager::default()),
1250 Arc::new(crate::testing::MockSessionManager::default()),
1251 Arc::new(crate::testing::MockSessionManager::default()),
1252 Arc::new(crate::UnavailableProcessService),
1253 Arc::new(crate::DefaultProcessCancelAbility),
1254 crate::runtime::RuntimeEffectControllerHandle::shared(Arc::new(
1255 crate::InlineRuntimeEffectController,
1256 )),
1257 Arc::new(crate::InMemoryAttachmentStore::new()),
1258 crate::DirectCompletionClient::unavailable(
1259 "direct completions are unavailable in this test context",
1260 ),
1261 )
1262 .build()
1263 }
1264
1265 #[async_trait::async_trait]
1266 impl ToolProvider for MockTool {
1267 fn tool_manifests(&self) -> Vec<ToolManifest> {
1268 manifests(vec![test_tool(
1269 "mock_tool",
1270 "mock",
1271 crate::ToolAvailabilityConfig::callable(),
1272 )])
1273 }
1274
1275 fn resolve_contract(&self, name: &str) -> Option<Arc<ToolContract>> {
1276 contract_from(
1277 vec![test_tool(
1278 "mock_tool",
1279 "mock",
1280 crate::ToolAvailabilityConfig::callable(),
1281 )],
1282 name,
1283 )
1284 }
1285
1286 async fn execute(&self, _call: ToolCall<'_>) -> ToolResult {
1287 ToolResult::ok(serde_json::json!("ok"))
1288 }
1289 }
1290
1291 #[async_trait::async_trait]
1292 impl ToolProvider for MixedEnabledTool {
1293 fn tool_manifests(&self) -> Vec<ToolManifest> {
1294 manifests(vec![
1295 test_tool(
1296 "enabled_tool",
1297 "enabled",
1298 crate::ToolAvailabilityConfig::callable(),
1299 ),
1300 test_tool(
1301 "disabled_tool",
1302 "disabled",
1303 crate::ToolAvailabilityConfig::off(),
1304 ),
1305 ])
1306 }
1307
1308 fn resolve_contract(&self, name: &str) -> Option<Arc<ToolContract>> {
1309 contract_from(
1310 vec![
1311 test_tool(
1312 "enabled_tool",
1313 "enabled",
1314 crate::ToolAvailabilityConfig::callable(),
1315 ),
1316 test_tool(
1317 "disabled_tool",
1318 "disabled",
1319 crate::ToolAvailabilityConfig::off(),
1320 ),
1321 ],
1322 name,
1323 )
1324 }
1325
1326 async fn execute(&self, _call: ToolCall<'_>) -> ToolResult {
1327 ToolResult::ok(serde_json::json!("ok"))
1328 }
1329 }
1330
1331 #[async_trait::async_trait]
1332 impl ToolSourceExecutor for ExternalMockSource {
1333 fn id(&self) -> &str {
1334 "external"
1335 }
1336
1337 fn advertised_tools(&self) -> Vec<ToolManifest> {
1338 manifests(vec![ToolDefinition::raw_with_id(
1339 "tool:mcp__demo__search",
1340 "mcp__demo__search",
1341 "search",
1342 json!({
1343 "type": "object",
1344 "properties": {
1345 "query": { "type": "string" }
1346 },
1347 "required": ["query"],
1348 "additionalProperties": false
1349 }),
1350 json!({ "type": "object", "additionalProperties": true }),
1351 )])
1352 }
1353
1354 fn resolve_contract(&self, name: &str) -> Option<Arc<ToolContract>> {
1355 contract_from(
1356 vec![ToolDefinition::raw_with_id(
1357 "tool:mcp__demo__search",
1358 "mcp__demo__search",
1359 "search",
1360 json!({
1361 "type": "object",
1362 "properties": {
1363 "query": { "type": "string" }
1364 },
1365 "required": ["query"],
1366 "additionalProperties": false
1367 }),
1368 json!({ "type": "object", "additionalProperties": true }),
1369 )],
1370 name,
1371 )
1372 }
1373
1374 async fn execute(
1375 &self,
1376 tool: &str,
1377 args: &serde_json::Value,
1378 _context: &ToolContext<'_>,
1379 _progress: Option<&ProgressSender>,
1380 ) -> ToolResult {
1381 ToolResult::ok(json!({
1382 "tool": tool,
1383 "args": args
1384 }))
1385 }
1386 }
1387
1388 #[async_trait::async_trait]
1389 impl ToolSourceExecutor for ExactResolvingSource {
1390 fn id(&self) -> &str {
1391 "exact"
1392 }
1393
1394 fn advertised_tools(&self) -> Vec<ToolManifest> {
1395 Vec::new()
1396 }
1397
1398 fn resolve_manifest(&self, name: &str) -> Option<ToolManifest> {
1399 self.manifest_resolutions.fetch_add(1, Ordering::SeqCst);
1400 (name == "host_only").then(|| {
1401 test_tool(
1402 "host_only",
1403 "host-only",
1404 crate::ToolAvailabilityConfig::callable(),
1405 )
1406 .manifest()
1407 })
1408 }
1409
1410 fn resolve_contract(&self, name: &str) -> Option<Arc<ToolContract>> {
1411 self.contract_resolutions.fetch_add(1, Ordering::SeqCst);
1412 contract_from(
1413 vec![test_tool(
1414 "host_only",
1415 "host-only",
1416 crate::ToolAvailabilityConfig::callable(),
1417 )],
1418 name,
1419 )
1420 }
1421
1422 async fn execute(
1423 &self,
1424 tool: &str,
1425 _args: &serde_json::Value,
1426 _context: &ToolContext<'_>,
1427 _progress: Option<&ProgressSender>,
1428 ) -> ToolResult {
1429 self.executions.fetch_add(1, Ordering::SeqCst);
1430 ToolResult::ok(json!(tool))
1431 }
1432 }
1433
1434 #[async_trait::async_trait]
1435 impl ToolSourceExecutor for NamedExactSource {
1436 fn id(&self) -> &str {
1437 self.id
1438 }
1439
1440 fn advertised_tools(&self) -> Vec<ToolManifest> {
1441 Vec::new()
1442 }
1443
1444 fn resolve_manifest(&self, name: &str) -> Option<ToolManifest> {
1445 (name == "host_only").then(|| {
1446 test_tool(
1447 "host_only",
1448 "host-only",
1449 crate::ToolAvailabilityConfig::callable(),
1450 )
1451 .manifest()
1452 })
1453 }
1454
1455 fn resolve_contract(&self, _name: &str) -> Option<Arc<ToolContract>> {
1456 None
1457 }
1458
1459 async fn execute(
1460 &self,
1461 tool: &str,
1462 _args: &serde_json::Value,
1463 _context: &ToolContext<'_>,
1464 _progress: Option<&ProgressSender>,
1465 ) -> ToolResult {
1466 ToolResult::ok(json!(tool))
1467 }
1468 }
1469
1470 #[async_trait::async_trait]
1471 impl ToolProvider for DynamicToolProvider {
1472 fn tool_manifests(&self) -> Vec<ToolManifest> {
1473 self.names
1474 .lock()
1475 .expect("dynamic tool names lock")
1476 .iter()
1477 .map(|name| dynamic_definition(name).manifest())
1478 .collect()
1479 }
1480
1481 fn resolve_contract(&self, name: &str) -> Option<Arc<ToolContract>> {
1482 self.names
1483 .lock()
1484 .expect("dynamic tool names lock")
1485 .iter()
1486 .any(|tool_name| tool_name == name)
1487 .then(|| Arc::new(dynamic_definition(name).contract()))
1488 }
1489
1490 async fn execute(&self, call: ToolCall<'_>) -> ToolResult {
1491 ToolResult::ok(json!(call.name))
1492 }
1493 }
1494
1495 #[test]
1496 fn registry_preserves_initial_availability_state() {
1497 let registry =
1498 ToolRegistry::from_tool_provider(Arc::new(MixedEnabledTool)).expect("registry");
1499 let snapshot = registry.export_state();
1500 assert_eq!(
1501 snapshot
1502 .get("enabled_tool")
1503 .unwrap()
1504 .manifest()
1505 .effective_availability(),
1506 crate::ToolAvailability::Callable
1507 );
1508 assert_eq!(
1509 snapshot
1510 .get("disabled_tool")
1511 .unwrap()
1512 .manifest()
1513 .effective_availability(),
1514 crate::ToolAvailability::Off
1515 );
1516 }
1517
1518 #[test]
1519 fn exported_tool_state_is_source_free() {
1520 let registry = ToolRegistry::from_tool_provider(Arc::new(MockTool)).expect("registry");
1521 registry
1522 .add_tool_provider(Arc::new(MixedEnabledTool))
1523 .expect("live provider registered");
1524
1525 let value = serde_json::to_value(registry.export_state()).expect("serialized tool state");
1526 let serialized = value.to_string();
1527
1528 assert!(!serialized.contains("source_id"));
1529 assert!(!serialized.contains(PLUGIN_SOURCE_ID));
1530 assert!(!serialized.contains("live:"));
1531 }
1532
1533 #[test]
1534 fn apply_state_rebinds_source_free_snapshot_to_current_sources() {
1535 let source_registry =
1536 ToolRegistry::from_tool_provider(Arc::new(MixedEnabledTool)).expect("source registry");
1537 let snapshot = source_registry.export_state();
1538
1539 let target_registry =
1540 ToolRegistry::from_tool_provider(Arc::new(MixedEnabledTool)).expect("target registry");
1541 let next_generation = target_registry
1542 .apply_state(snapshot.with_generation(target_registry.generation()))
1543 .expect("state rebound");
1544
1545 assert_eq!(next_generation, target_registry.generation());
1546 assert!(target_registry.resolve_contract("enabled_tool").is_some());
1547 }
1548
1549 #[test]
1550 fn apply_state_rejects_tools_not_advertised_by_source() {
1551 let registry = ToolRegistry::from_tool_provider(Arc::new(MockTool)).expect("registry");
1552 let snapshot = registry.export_state();
1553 let generation = snapshot.generation();
1554 let mut tools = snapshot.entries().clone();
1555 tools.insert(
1556 "missing".to_string(),
1557 ToolStateEntry::new(
1558 test_tool(
1559 "missing",
1560 "missing",
1561 crate::ToolAvailabilityConfig::callable(),
1562 )
1563 .manifest(),
1564 ),
1565 );
1566 let snapshot = ToolState::new(generation, tools);
1567 assert!(matches!(
1568 registry.apply_state(snapshot),
1569 Err(ReconfigureError::Validation(_))
1570 ));
1571 }
1572
1573 #[test]
1574 fn apply_state_rejects_snapshot_when_provider_is_absent() {
1575 let source_registry =
1576 ToolRegistry::from_tool_provider(Arc::new(MockTool)).expect("source registry");
1577 source_registry
1578 .upsert_source(Arc::new(ExternalMockSource))
1579 .expect("source registered");
1580 let snapshot = source_registry.export_state();
1581
1582 let target_registry =
1583 ToolRegistry::from_tool_provider(Arc::new(MockTool)).expect("target registry");
1584 let err = target_registry
1585 .apply_state(snapshot.with_generation(target_registry.generation()))
1586 .expect_err("missing provider should fail");
1587
1588 assert!(matches!(err, ReconfigureError::Validation(_)));
1589 }
1590
1591 #[test]
1592 fn apply_state_rejects_ambiguous_current_source_binding() {
1593 let registry = ToolRegistry::empty();
1594 registry
1595 .upsert_source(Arc::new(NamedExactSource { id: "exact-a" }))
1596 .expect("source a registered");
1597 registry
1598 .upsert_source(Arc::new(NamedExactSource { id: "exact-b" }))
1599 .expect("source b registered");
1600
1601 let mut tools = BTreeMap::new();
1602 tools.insert(
1603 "host_only".to_string(),
1604 ToolStateEntry::new(
1605 test_tool(
1606 "host_only",
1607 "host-only",
1608 crate::ToolAvailabilityConfig::callable(),
1609 )
1610 .manifest(),
1611 ),
1612 );
1613
1614 let err = registry
1615 .apply_state(ToolState::new(registry.generation(), tools))
1616 .expect_err("ambiguous source binding should fail");
1617
1618 assert!(matches!(err, ReconfigureError::Validation(_)));
1619 }
1620
1621 #[test]
1622 fn advertised_manifest_resolves_without_exact_host_lookup() {
1623 let manifest_resolutions = Arc::new(AtomicUsize::new(0));
1624 let registry = ToolRegistry::from_tool_provider(Arc::new(MockTool)).expect("registry");
1625 registry
1626 .upsert_source(Arc::new(ExactResolvingSource {
1627 manifest_resolutions: Arc::clone(&manifest_resolutions),
1628 contract_resolutions: Arc::new(AtomicUsize::new(0)),
1629 executions: Arc::new(AtomicUsize::new(0)),
1630 }))
1631 .expect("source registered");
1632
1633 assert_eq!(
1634 registry
1635 .resolve_manifest("mock_tool")
1636 .map(|manifest| manifest.name),
1637 Some("mock_tool".to_string())
1638 );
1639 assert_eq!(manifest_resolutions.load(Ordering::SeqCst), 0);
1640 }
1641
1642 #[test]
1643 fn refresh_sources_re_reads_group_provider_manifests() {
1644 let names = Arc::new(std::sync::Mutex::new(vec!["dynamic_one".to_string()]));
1645 let provider: Arc<dyn ToolProvider> = Arc::new(DynamicToolProvider {
1646 names: Arc::clone(&names),
1647 });
1648 let registry = ToolRegistry::from_tool_providers(vec![provider]).expect("registry");
1649
1650 let tool_names = || {
1651 registry
1652 .tool_manifests()
1653 .into_iter()
1654 .map(|manifest| manifest.name)
1655 .collect::<BTreeSet<_>>()
1656 };
1657
1658 assert!(tool_names().contains("dynamic_one"));
1659 assert!(!tool_names().contains("dynamic_two"));
1660
1661 names
1662 .lock()
1663 .expect("dynamic tool names lock")
1664 .push("dynamic_two".to_string());
1665 registry.refresh_sources().expect("refresh sources");
1666 let refreshed = tool_names();
1667 assert!(refreshed.contains("dynamic_one"));
1668 assert!(refreshed.contains("dynamic_two"));
1669
1670 names
1671 .lock()
1672 .expect("dynamic tool names lock")
1673 .retain(|name| name != "dynamic_one");
1674 registry.refresh_sources().expect("refresh sources");
1675 let refreshed = tool_names();
1676 assert!(!refreshed.contains("dynamic_one"));
1677 assert!(refreshed.contains("dynamic_two"));
1678 }
1679
1680 #[tokio::test]
1681 async fn unknown_manifest_exact_resolves_and_routes_to_owner() {
1682 let manifest_resolutions = Arc::new(AtomicUsize::new(0));
1683 let contract_resolutions = Arc::new(AtomicUsize::new(0));
1684 let executions = Arc::new(AtomicUsize::new(0));
1685 let registry = ToolRegistry::from_tool_provider(Arc::new(MockTool)).expect("registry");
1686 registry
1687 .upsert_source(Arc::new(ExactResolvingSource {
1688 manifest_resolutions: Arc::clone(&manifest_resolutions),
1689 contract_resolutions: Arc::clone(&contract_resolutions),
1690 executions: Arc::clone(&executions),
1691 }))
1692 .expect("source registered");
1693
1694 assert_eq!(
1695 registry
1696 .resolve_manifest("host_only")
1697 .map(|manifest| manifest.name),
1698 Some("host_only".to_string())
1699 );
1700 assert_eq!(manifest_resolutions.load(Ordering::SeqCst), 1);
1701
1702 let contract = registry.resolve_contract("host_only");
1703 assert!(contract.is_some());
1704 assert_eq!(manifest_resolutions.load(Ordering::SeqCst), 1);
1705 assert_eq!(contract_resolutions.load(Ordering::SeqCst), 1);
1706
1707 let context = test_tool_context();
1708 let args = json!({});
1709 let result = registry
1710 .execute(crate::ToolCall {
1711 name: "host_only",
1712 args: &args,
1713 context: &context,
1714 progress: None,
1715 })
1716 .await;
1717 assert!(result.is_success());
1718 assert_eq!(result.value_for_projection(), json!("host_only"));
1719 assert_eq!(executions.load(Ordering::SeqCst), 1);
1720 }
1721
1722 #[test]
1723 fn unknown_manifest_without_host_resolver_is_unavailable() {
1724 let registry = ToolRegistry::from_tool_provider(Arc::new(MockTool)).expect("registry");
1725
1726 assert!(registry.resolve_manifest("missing").is_none());
1727 assert!(registry.resolve_contract("missing").is_none());
1728 }
1729
1730 #[tokio::test]
1731 async fn upsert_source_registers_and_executes_external_tools() {
1732 let registry = ToolRegistry::from_tool_provider(Arc::new(MockTool)).expect("registry");
1733 registry
1734 .upsert_source(Arc::new(ExternalMockSource))
1735 .expect("source registered");
1736
1737 let defs = registry.tool_manifests();
1738 assert!(defs.iter().any(|def| def.name == "mcp__demo__search"));
1739
1740 let context = test_tool_context();
1741 let args = json!({ "query": "hello" });
1742 let result = registry
1743 .execute(crate::ToolCall {
1744 name: "mcp__demo__search",
1745 args: &args,
1746 context: &context,
1747 progress: None,
1748 })
1749 .await;
1750 assert!(result.is_success());
1751 assert_eq!(
1752 result.value_for_projection()["tool"],
1753 json!("mcp__demo__search")
1754 );
1755 assert_eq!(
1756 result.value_for_projection()["args"]["query"],
1757 json!("hello")
1758 );
1759 }
1760
1761 #[test]
1762 fn upsert_source_preserves_availability_override_on_refresh() {
1763 let registry = ToolRegistry::from_tool_provider(Arc::new(MockTool)).expect("registry");
1764 registry
1765 .upsert_source(Arc::new(ExternalMockSource))
1766 .expect("source registered");
1767 let mut snapshot = registry.export_state();
1768 snapshot
1769 .set_availability("mcp__demo__search", Some(crate::ToolAvailability::Off))
1770 .unwrap();
1771 registry.apply_state(snapshot).unwrap();
1772 registry
1773 .upsert_source(Arc::new(ExternalMockSource))
1774 .expect("source refreshed");
1775 let snapshot = registry.export_state();
1776 assert_eq!(
1777 snapshot
1778 .get("mcp__demo__search")
1779 .unwrap()
1780 .manifest()
1781 .effective_availability(),
1782 crate::ToolAvailability::Off
1783 );
1784 }
1785
1786 #[test]
1787 fn restore_state_adopts_generation_at_or_above_three() {
1788 let source = ToolRegistry::from_tool_provider(Arc::new(MockTool)).expect("source registry");
1794 let snapshot = source.export_state().with_generation(3);
1795
1796 let target = ToolRegistry::from_tool_provider(Arc::new(MockTool)).expect("target registry");
1797 assert_eq!(
1798 target.generation(),
1799 1,
1800 "a fresh registry starts at generation 1"
1801 );
1802 let restored = target
1803 .restore_state(snapshot.clone())
1804 .expect("restore adopts the snapshot generation");
1805 assert_eq!(
1806 restored.generation, 3,
1807 "restore returns the adopted generation"
1808 );
1809 assert!(
1810 restored.orphaned.is_empty(),
1811 "all tools resolve, so nothing orphans"
1812 );
1813 assert_eq!(
1814 target.generation(),
1815 3,
1816 "restore adopts gen 3 onto a base-1 registry without bumping"
1817 );
1818 assert_eq!(target.export_state().generation(), 3);
1820
1821 let fresh = ToolRegistry::from_tool_provider(Arc::new(MockTool)).expect("fresh registry");
1824 assert!(
1825 matches!(
1826 fresh.apply_state(snapshot),
1827 Err(ReconfigureError::GenerationMismatch {
1828 expected: 3,
1829 actual: 1
1830 })
1831 ),
1832 "apply_state must reject a gen-3 snapshot on a base-1 registry"
1833 );
1834 }
1835
1836 fn snapshot_with_external_tool() -> ToolState {
1839 let source = ToolRegistry::from_tool_provider(Arc::new(MockTool)).expect("source registry");
1840 source
1841 .upsert_source(Arc::new(ExternalMockSource))
1842 .expect("source registered");
1843 source.export_state()
1844 }
1845
1846 #[tokio::test]
1847 async fn restore_orphans_unresolved_tools_instead_of_failing() {
1848 let mut snapshot = snapshot_with_external_tool();
1849 snapshot
1850 .set_availability(
1851 "mcp__demo__search",
1852 Some(crate::ToolAvailability::Showcased),
1853 )
1854 .expect("override set");
1855
1856 let target = ToolRegistry::from_tool_provider(Arc::new(MockTool)).expect("target");
1857 let report = target
1858 .restore_state(snapshot)
1859 .expect("restore tolerates the missing source");
1860 assert_eq!(report.orphaned, vec!["mcp__demo__search".to_string()]);
1861
1862 let view = target
1864 .tool_manifests()
1865 .into_iter()
1866 .find(|manifest| manifest.name == "mcp__demo__search")
1867 .expect("orphan stays in the surface listing");
1868 assert_eq!(
1869 view.effective_availability(),
1870 crate::ToolAvailability::Off,
1871 "orphans are forced Off in the view"
1872 );
1873 let exported = target.export_state();
1874 let exported_view = exported
1875 .tool_manifests()
1876 .into_iter()
1877 .find(|manifest| manifest.name == "mcp__demo__search")
1878 .expect("orphan is visible in exported tool state");
1879 assert_eq!(
1880 exported_view.effective_availability(),
1881 crate::ToolAvailability::Off,
1882 "exported ToolState exposes the same forced-Off orphan view"
1883 );
1884 let entry = exported.get("mcp__demo__search").expect("orphan exported");
1885 assert!(entry.is_orphaned());
1886 assert_eq!(
1887 entry.manifest().effective_availability(),
1888 crate::ToolAvailability::Off,
1889 "entry manifest is also the public forced-Off view"
1890 );
1891 assert_eq!(
1892 entry.stored_manifest().availability_override,
1893 Some(crate::ToolAvailability::Showcased),
1894 "the persisted override survives orphaning"
1895 );
1896
1897 let context = test_tool_context();
1899 let args = json!({ "query": "hello" });
1900 let result = target
1901 .execute(crate::ToolCall {
1902 name: "mcp__demo__search",
1903 args: &args,
1904 context: &context,
1905 progress: None,
1906 })
1907 .await;
1908 assert!(!result.is_success());
1909 assert!(
1910 format!("{result:?}").contains("unavailable"),
1911 "orphan execution error names the condition: {result:?}"
1912 );
1913
1914 assert!(target.resolve_contract("mock_tool").is_some());
1916 }
1917
1918 #[tokio::test]
1919 async fn orphan_rebinds_when_source_is_upserted_again() {
1920 let mut snapshot = snapshot_with_external_tool();
1921 snapshot
1922 .set_availability(
1923 "mcp__demo__search",
1924 Some(crate::ToolAvailability::Showcased),
1925 )
1926 .expect("override set");
1927 let target = ToolRegistry::from_tool_provider(Arc::new(MockTool)).expect("target");
1928 target.restore_state(snapshot).expect("restore");
1929 let orphaned_generation = target.generation();
1930
1931 target
1932 .upsert_source(Arc::new(ExternalMockSource))
1933 .expect("the returning source must not conflict with its own orphan");
1934 assert!(
1935 target.generation() > orphaned_generation,
1936 "rebinding bumps the generation"
1937 );
1938
1939 let exported = target.export_state();
1940 let entry = exported.get("mcp__demo__search").expect("entry kept");
1941 assert!(
1942 !entry.is_orphaned(),
1943 "the orphan rebound to the live source"
1944 );
1945 assert_eq!(
1946 entry.manifest().availability_override,
1947 Some(crate::ToolAvailability::Showcased),
1948 "rebinding preserves the persisted override"
1949 );
1950
1951 let context = test_tool_context();
1952 let args = json!({ "query": "hello" });
1953 let result = target
1954 .execute(crate::ToolCall {
1955 name: "mcp__demo__search",
1956 args: &args,
1957 context: &context,
1958 progress: None,
1959 })
1960 .await;
1961 assert!(result.is_success(), "rebound tool executes: {result:?}");
1962 }
1963
1964 #[tokio::test]
1965 async fn orphan_rebinds_lazily_via_resolve_manifest() {
1966 let source_registry = ToolRegistry::empty();
1969 source_registry
1970 .upsert_source(Arc::new(NamedExactSource { id: "exact-a" }))
1971 .expect("source registered");
1972 assert!(source_registry.resolve_manifest("host_only").is_some());
1973 let snapshot = source_registry.export_state();
1974
1975 let target = ToolRegistry::from_tool_provider(Arc::new(MockTool)).expect("target");
1976 let report = target.restore_state(snapshot).expect("restore");
1977 assert_eq!(report.orphaned, vec!["host_only".to_string()]);
1978
1979 target
1980 .upsert_source(Arc::new(NamedExactSource { id: "exact-a" }))
1981 .expect("source returns");
1982 let manifest = target
1983 .resolve_manifest("host_only")
1984 .expect("resolves after the source returned");
1985 assert_eq!(
1986 manifest.effective_availability(),
1987 crate::ToolAvailability::Callable,
1988 "lazy rebind drops the forced-Off orphan view"
1989 );
1990 assert!(
1991 !target
1992 .export_state()
1993 .get("host_only")
1994 .expect("entry kept")
1995 .is_orphaned()
1996 );
1997 }
1998
1999 #[test]
2000 fn restore_still_fails_when_name_resolves_with_different_id() {
2001 struct ReplacedSearchTool;
2002 #[async_trait::async_trait]
2003 impl ToolProvider for ReplacedSearchTool {
2004 fn tool_manifests(&self) -> Vec<ToolManifest> {
2005 manifests(vec![ToolDefinition::raw_with_id(
2006 "tool:replaced",
2007 "mcp__demo__search",
2008 "a different implementation under the same name",
2009 ToolDefinition::default_input_schema(),
2010 json!({}),
2011 )])
2012 }
2013 fn resolve_contract(&self, _name: &str) -> Option<Arc<ToolContract>> {
2014 None
2015 }
2016 async fn execute(&self, _call: ToolCall<'_>) -> ToolResult {
2017 ToolResult::ok(json!("ok"))
2018 }
2019 }
2020
2021 let snapshot = snapshot_with_external_tool();
2022 let target = ToolRegistry::from_tool_provider(Arc::new(MockTool)).expect("target");
2023 target
2024 .add_tool_provider(Arc::new(ReplacedSearchTool))
2025 .expect("replacement registered");
2026 let err = target
2027 .restore_state(snapshot)
2028 .expect_err("same name with a different id is a real conflict");
2029 assert!(matches!(err, ReconfigureError::Validation(_)));
2030 }
2031
2032 #[test]
2033 fn apply_state_round_trips_while_orphans_exist() {
2034 let target = ToolRegistry::from_tool_provider(Arc::new(MockTool)).expect("target");
2037 target
2038 .restore_state(snapshot_with_external_tool())
2039 .expect("restore");
2040
2041 let mut edited = target.export_state();
2042 edited
2043 .set_availability("mock_tool", Some(crate::ToolAvailability::Searchable))
2044 .expect("edit bound tool");
2045 target
2046 .apply_state(edited)
2047 .expect("apply accepts the snapshot it exported");
2048 let exported = target.export_state();
2049 assert!(exported.get("mcp__demo__search").unwrap().is_orphaned());
2050 assert_eq!(
2051 exported
2052 .get("mock_tool")
2053 .unwrap()
2054 .manifest()
2055 .effective_availability(),
2056 crate::ToolAvailability::Searchable
2057 );
2058
2059 let strict = snapshot_with_external_tool().with_generation(target.generation());
2062 assert!(matches!(
2063 target.apply_state(strict),
2064 Err(ReconfigureError::Validation(_))
2065 ));
2066 }
2067
2068 #[test]
2069 fn orphan_flag_serializes_and_legacy_snapshots_deserialize_as_bound() {
2070 let target = ToolRegistry::from_tool_provider(Arc::new(MockTool)).expect("target");
2071 target
2072 .restore_state(snapshot_with_external_tool())
2073 .expect("restore");
2074 let value = serde_json::to_value(target.export_state()).expect("serializes");
2075 assert_eq!(value["tools"]["mcp__demo__search"]["orphaned"], json!(true));
2076 assert!(
2077 value["tools"]["mock_tool"].get("orphaned").is_none(),
2078 "bound entries omit the flag, keeping old and new snapshots byte-compatible"
2079 );
2080
2081 let legacy: ToolStateEntry = serde_json::from_value(json!({
2082 "manifest": value["tools"]["mock_tool"]["manifest"]
2083 }))
2084 .expect("legacy entry without the flag deserializes");
2085 assert!(!legacy.is_orphaned());
2086 }
2087
2088 #[test]
2089 fn remove_source_removes_all_source_tools() {
2090 let registry = ToolRegistry::from_tool_provider(Arc::new(MockTool)).expect("registry");
2091 registry
2092 .upsert_source(Arc::new(ExternalMockSource))
2093 .expect("source registered");
2094 registry
2095 .remove_source_id("external")
2096 .expect("source removed");
2097 let defs = registry.tool_manifests();
2098 assert!(!defs.iter().any(|def| def.name == "mcp__demo__search"));
2099 }
2100
2101 #[test]
2102 fn project_tool_catalog_keeps_searchable_tools_with_surface_metadata() {
2103 fn dummy_tool(name: &str) -> crate::ToolDefinition {
2104 let tool = crate::ToolDefinition::raw_with_id(
2105 format!("tool:{name}"),
2106 name,
2107 format!("desc for {name}"),
2108 crate::ToolDefinition::default_input_schema(),
2109 serde_json::json!({}),
2110 );
2111 match name {
2112 "read_file" => {
2113 tool.with_agent_surface(crate::ToolAgentSurface::new(["files"], "read"))
2114 }
2115 "search_tools" => {
2116 tool.with_agent_surface(crate::ToolAgentSurface::new(["tools"], "search"))
2117 }
2118 _ => tool,
2119 }
2120 }
2121 let catalog = project_tool_catalog([
2122 crate::ToolSurfaceEntry {
2123 manifest: dummy_tool("read_file").manifest(),
2124 availability: crate::ToolAvailability::Showcased,
2125 },
2126 crate::ToolSurfaceEntry {
2127 manifest: dummy_tool("search_tools").manifest(),
2128 availability: crate::ToolAvailability::Callable,
2129 },
2130 ]);
2131 assert_eq!(catalog.len(), 2);
2132 assert_eq!(catalog[0]["name"], serde_json::json!("read_file"));
2133 assert_eq!(
2134 catalog[0]["contract"]["signature"],
2135 serde_json::json!("await files.read({})?")
2136 );
2137 assert_eq!(catalog[0]["showcased"], serde_json::json!(true));
2138 assert_eq!(catalog[1]["callable"], serde_json::json!(true));
2139 }
2140
2141 #[test]
2142 fn project_tool_catalog_preserves_dynamic_output_contracts() {
2143 fn dummy_tool(name: &str) -> crate::ToolDefinition {
2144 crate::ToolDefinition::raw_with_id(
2145 format!("tool:{name}"),
2146 name,
2147 format!("desc for {name}"),
2148 crate::ToolDefinition::default_input_schema(),
2149 serde_json::json!({}),
2150 )
2151 .with_agent_surface(crate::ToolAgentSurface::new(["llm"], "query"))
2152 }
2153 let catalog = project_tool_catalog([crate::ToolSurfaceEntry {
2154 manifest: dummy_tool("llm_query")
2155 .with_output_from_input_schema(
2156 "output",
2157 Some(serde_json::json!({ "type": "string" })),
2158 )
2159 .manifest(),
2160 availability: crate::ToolAvailability::Searchable,
2161 }]);
2162
2163 assert_eq!(
2164 catalog[0]["contract"]["signature"],
2165 serde_json::json!("await llm.query<T = str>({})?")
2166 );
2167 assert_eq!(catalog[0]["contract"]["returns"], serde_json::json!("T"));
2168 }
2169}
2170
2171pub(crate) fn project_tool_catalog<I>(entries: I) -> Vec<serde_json::Value>
2172where
2173 I: IntoIterator<Item = crate::ToolSurfaceEntry>,
2174{
2175 entries
2176 .into_iter()
2177 .filter(|entry| entry.availability.is_searchable())
2178 .map(|entry| {
2179 let manifest = entry.manifest;
2180 let availability = entry.availability;
2181 let agent_surface = manifest.agent_surface.executable_for(&manifest.name);
2182 let call = agent_surface.call_path();
2183 let mut projected = serde_json::json!({
2184 "id": manifest.id,
2185 "name": manifest.name,
2186 "module_path": agent_surface.module_path,
2187 "operation": agent_surface.operation,
2188 "authority_type": agent_surface.authority_type,
2189 "call": call,
2190 "description": manifest.description,
2191 "aliases": agent_surface.aliases,
2192 "availability": availability,
2193 "callable": availability.is_callable(),
2194 "showcased": availability.is_showcased(),
2195 "searchable": availability.is_searchable(),
2196 "activation": manifest.activation,
2197 });
2198 if let Some(contract) = manifest.compact_contract {
2199 projected
2200 .as_object_mut()
2201 .expect("projected tool catalog entry is an object")
2202 .insert("contract".to_string(), serde_json::json!(contract));
2203 }
2204 projected
2205 })
2206 .collect()
2207}