1use std::borrow::Cow;
9use std::path::PathBuf;
10use std::sync::Arc;
11
12use super::ToolRegistry;
13use super::registration::{ToolExecutorFn, ToolHandler, ToolRegistration};
14use crate::components::{
15 wrap_native_tool_ci, wrap_native_tool_interactive, wrap_tool_ci, wrap_tool_interactive,
16};
17use crate::tool_policy::ToolPolicy;
18use crate::tools::result::ToolResult as SplitToolResult;
19use crate::tools::traits::Tool;
20use anyhow::Result;
21use async_trait::async_trait;
22use serde_json::Value;
23
24fn leak_pattern_str(value: impl Into<String>) -> &'static str {
25 Box::leak(value.into().into_boxed_str())
26}
27
28fn leak_patterns(patterns: &[String]) -> Option<&'static [&'static str]> {
29 if patterns.is_empty() {
30 return None;
31 }
32
33 let leaked_patterns = patterns
34 .iter()
35 .cloned()
36 .map(leak_pattern_str)
37 .collect::<Vec<_>>()
38 .into_boxed_slice();
39 Some(Box::leak(leaked_patterns))
40}
41
42#[derive(Clone)]
43struct RegistrationMetadataSnapshot {
44 name: Arc<str>,
45 description: Arc<str>,
46 parameter_schema: Option<Value>,
47 config_schema: Option<Value>,
48 state_schema: Option<Value>,
49 prompt_path: Option<String>,
50 default_permission: ToolPolicy,
51 allow_patterns: Option<&'static [&'static str]>,
52 deny_patterns: Option<&'static [&'static str]>,
53}
54
55impl RegistrationMetadataSnapshot {
56 fn from_registration(registration: &ToolRegistration) -> Self {
57 Self {
58 name: Arc::<str>::from(registration.name()),
59 description: Arc::<str>::from(
60 registration.metadata().description().unwrap_or_default(),
61 ),
62 parameter_schema: registration.parameter_schema().cloned(),
63 config_schema: registration.config_schema().cloned(),
64 state_schema: registration.state_schema().cloned(),
65 prompt_path: registration.prompt_path().map(str::to_string),
66 default_permission: registration
67 .default_permission()
68 .unwrap_or(ToolPolicy::Prompt),
69 allow_patterns: leak_patterns(registration.metadata().allowlist()),
70 deny_patterns: leak_patterns(registration.metadata().denylist()),
71 }
72 }
73
74 fn from_registration_with_tool<T>(registration: &ToolRegistration, tool: &T) -> Self
75 where
76 T: Tool + ?Sized,
77 {
78 Self {
79 name: Arc::<str>::from(registration.name()),
80 description: registration
81 .metadata()
82 .description()
83 .map(Arc::<str>::from)
84 .unwrap_or_else(|| Arc::<str>::from(tool.description())),
85 parameter_schema: registration
86 .parameter_schema()
87 .cloned()
88 .or_else(|| tool.parameter_schema()),
89 config_schema: registration
90 .config_schema()
91 .cloned()
92 .or_else(|| tool.config_schema()),
93 state_schema: registration
94 .state_schema()
95 .cloned()
96 .or_else(|| tool.state_schema()),
97 prompt_path: registration
98 .prompt_path()
99 .map(str::to_string)
100 .or_else(|| tool.prompt_path().map(Cow::into_owned)),
101 default_permission: registration
102 .default_permission()
103 .unwrap_or_else(|| tool.default_permission()),
104 allow_patterns: leak_patterns(registration.metadata().allowlist())
105 .or_else(|| tool.allow_patterns()),
106 deny_patterns: leak_patterns(registration.metadata().denylist())
107 .or_else(|| tool.deny_patterns()),
108 }
109 }
110}
111
112struct RegistryFnTool {
113 registry: ToolRegistry,
114 executor: ToolExecutorFn,
115 metadata: RegistrationMetadataSnapshot,
116}
117
118impl RegistryFnTool {
119 fn from_registration(registry: ToolRegistry, registration: &ToolRegistration) -> Option<Self> {
120 let executor = match registration.handler() {
121 ToolHandler::RegistryFn(executor) => executor,
122 ToolHandler::TraitObject(_) => return None,
123 };
124
125 Some(Self {
126 registry,
127 executor,
128 metadata: RegistrationMetadataSnapshot::from_registration(registration),
129 })
130 }
131}
132
133#[async_trait]
134impl Tool for RegistryFnTool {
135 async fn execute(&self, args: Value) -> Result<Value> {
136 (self.executor)(&self.registry, args).await
137 }
138
139 fn name(&self) -> &str {
140 self.metadata.name.as_ref()
141 }
142
143 fn description(&self) -> &str {
144 self.metadata.description.as_ref()
145 }
146
147 fn parameter_schema(&self) -> Option<Value> {
148 self.metadata.parameter_schema.clone()
149 }
150
151 fn config_schema(&self) -> Option<Value> {
152 self.metadata.config_schema.clone()
153 }
154
155 fn state_schema(&self) -> Option<Value> {
156 self.metadata.state_schema.clone()
157 }
158
159 fn prompt_path(&self) -> Option<Cow<'static, str>> {
160 self.metadata.prompt_path.clone().map(Cow::Owned)
161 }
162
163 fn default_permission(&self) -> ToolPolicy {
164 self.metadata.default_permission.clone()
165 }
166
167 fn allow_patterns(&self) -> Option<&'static [&'static str]> {
168 self.metadata.allow_patterns
169 }
170
171 fn deny_patterns(&self) -> Option<&'static [&'static str]> {
172 self.metadata.deny_patterns
173 }
174}
175
176struct RegistrationBackedTool<T> {
177 inner: T,
178 metadata: RegistrationMetadataSnapshot,
179}
180
181impl<T> RegistrationBackedTool<T>
182where
183 T: Tool + Send + Sync,
184{
185 fn from_registration(inner: T, registration: &ToolRegistration) -> Self {
186 let metadata =
187 RegistrationMetadataSnapshot::from_registration_with_tool(registration, &inner);
188 Self { inner, metadata }
189 }
190}
191
192#[async_trait]
193impl<T> Tool for RegistrationBackedTool<T>
194where
195 T: Tool + Send + Sync,
196{
197 async fn execute(&self, args: Value) -> Result<Value> {
198 self.inner.execute(args).await
199 }
200
201 async fn execute_dual(&self, args: Value) -> Result<SplitToolResult> {
202 let mut result = self.inner.execute_dual(args).await?;
203 result.tool_name = self.name().to_string();
204 Ok(result)
205 }
206
207 fn name(&self) -> &str {
208 self.metadata.name.as_ref()
209 }
210
211 fn description(&self) -> &str {
212 self.metadata.description.as_ref()
213 }
214
215 fn validate_args(&self, args: &Value) -> Result<()> {
216 self.inner.validate_args(args)
217 }
218
219 fn parameter_schema(&self) -> Option<Value> {
220 self.metadata.parameter_schema.clone()
221 }
222
223 fn config_schema(&self) -> Option<Value> {
224 self.metadata.config_schema.clone()
225 }
226
227 fn state_schema(&self) -> Option<Value> {
228 self.metadata.state_schema.clone()
229 }
230
231 fn prompt_path(&self) -> Option<Cow<'static, str>> {
232 self.metadata.prompt_path.clone().map(Cow::Owned)
233 }
234
235 fn default_permission(&self) -> ToolPolicy {
236 self.metadata.default_permission.clone()
237 }
238
239 fn allow_patterns(&self) -> Option<&'static [&'static str]> {
240 self.metadata.allow_patterns
241 }
242
243 fn deny_patterns(&self) -> Option<&'static [&'static str]> {
244 self.metadata.deny_patterns
245 }
246
247 fn is_mutating(&self) -> bool {
248 self.inner.is_mutating()
249 }
250
251 fn is_parallel_safe(&self) -> bool {
252 self.inner.is_parallel_safe()
253 }
254
255 fn kind(&self) -> &'static str {
256 self.inner.kind()
257 }
258
259 fn resource_hints(&self, args: &Value) -> Vec<String> {
260 self.inner.resource_hints(args)
261 }
262
263 fn execution_cost(&self) -> u8 {
264 self.inner.execution_cost()
265 }
266}
267
268struct RegistrationBackedDynTool {
269 inner: Arc<dyn Tool>,
270 metadata: RegistrationMetadataSnapshot,
271}
272
273impl RegistrationBackedDynTool {
274 fn from_registration(inner: Arc<dyn Tool>, registration: &ToolRegistration) -> Self {
275 let metadata =
276 RegistrationMetadataSnapshot::from_registration_with_tool(registration, inner.as_ref());
277 Self { inner, metadata }
278 }
279}
280
281#[async_trait]
282impl Tool for RegistrationBackedDynTool {
283 async fn execute(&self, args: Value) -> Result<Value> {
284 self.inner.execute(args).await
285 }
286
287 async fn execute_dual(&self, args: Value) -> Result<SplitToolResult> {
288 let mut result = self.inner.execute_dual(args).await?;
289 result.tool_name = self.name().to_string();
290 Ok(result)
291 }
292
293 fn name(&self) -> &str {
294 self.metadata.name.as_ref()
295 }
296
297 fn description(&self) -> &str {
298 self.metadata.description.as_ref()
299 }
300
301 fn validate_args(&self, args: &Value) -> Result<()> {
302 self.inner.validate_args(args)
303 }
304
305 fn parameter_schema(&self) -> Option<Value> {
306 self.metadata.parameter_schema.clone()
307 }
308
309 fn config_schema(&self) -> Option<Value> {
310 self.metadata.config_schema.clone()
311 }
312
313 fn state_schema(&self) -> Option<Value> {
314 self.metadata.state_schema.clone()
315 }
316
317 fn prompt_path(&self) -> Option<Cow<'static, str>> {
318 self.metadata.prompt_path.clone().map(Cow::Owned)
319 }
320
321 fn default_permission(&self) -> ToolPolicy {
322 self.metadata.default_permission.clone()
323 }
324
325 fn allow_patterns(&self) -> Option<&'static [&'static str]> {
326 self.metadata.allow_patterns
327 }
328
329 fn deny_patterns(&self) -> Option<&'static [&'static str]> {
330 self.metadata.deny_patterns
331 }
332
333 fn is_mutating(&self) -> bool {
334 self.inner.is_mutating()
335 }
336
337 fn is_parallel_safe(&self) -> bool {
338 self.inner.is_parallel_safe()
339 }
340
341 fn kind(&self) -> &'static str {
342 self.inner.kind()
343 }
344
345 fn resource_hints(&self, args: &Value) -> Vec<String> {
346 self.inner.resource_hints(args)
347 }
348
349 fn execution_cost(&self) -> u8 {
350 self.inner.execution_cost()
351 }
352}
353
354fn wrap_registered_trait_object_tool(
360 registration: &ToolRegistration,
361 tool: Arc<dyn Tool>,
362 workspace_root: PathBuf,
363 mode: CgpRuntimeMode,
364) -> Arc<dyn Tool> {
365 let tool: Arc<dyn Tool> = Arc::new(RegistrationBackedDynTool::from_registration(
366 tool,
367 registration,
368 ));
369 match mode {
370 CgpRuntimeMode::Interactive => Arc::new(wrap_tool_interactive(tool, workspace_root)),
371 CgpRuntimeMode::Ci => Arc::new(wrap_tool_ci(tool, workspace_root)),
372 }
373}
374
375pub fn wrap_registered_native_tool<T>(
376 registration: &ToolRegistration,
377 tool: T,
378 workspace_root: PathBuf,
379 mode: CgpRuntimeMode,
380) -> Arc<dyn Tool>
381where
382 T: Tool + Send + Sync + 'static,
383{
384 let tool = RegistrationBackedTool::from_registration(tool, registration);
385 match mode {
386 CgpRuntimeMode::Interactive => Arc::new(wrap_native_tool_interactive(tool, workspace_root)),
387 CgpRuntimeMode::Ci => Arc::new(wrap_native_tool_ci(tool, workspace_root)),
388 }
389}
390
391pub fn native_cgp_tool_factory<T, F>(build_tool: F) -> super::registration::NativeCgpToolFactory
392where
393 T: Tool + Send + Sync + 'static,
394 F: Fn() -> T + Send + Sync + 'static,
395{
396 Arc::new(move |registration, workspace_root, mode| {
397 wrap_registered_native_tool(registration, build_tool(), workspace_root, mode)
398 })
399}
400
401#[derive(Debug, Clone, Copy, PartialEq, Eq)]
403pub enum CgpRuntimeMode {
404 Interactive,
406 Ci,
408}
409
410impl ToolRegistry {
411 pub(crate) fn current_cgp_mode(&self) -> Option<CgpRuntimeMode> {
412 self.cgp_runtime_mode.read().ok().and_then(|mode| *mode)
413 }
414
415 fn set_cgp_runtime_mode(&self, mode: CgpRuntimeMode) {
416 if let Ok(mut current_mode) = self.cgp_runtime_mode.write() {
417 *current_mode = Some(mode);
418 }
419 }
420
421 pub(crate) fn cgp_handler_for_registration(
422 &self,
423 registration: &ToolRegistration,
424 mode: CgpRuntimeMode,
425 ) -> Option<ToolHandler> {
426 let workspace = self.workspace_root_owned();
427 if let Some(factory) = registration.native_cgp_factory() {
428 return Some(ToolHandler::TraitObject(factory(
429 registration,
430 workspace,
431 mode,
432 )));
433 }
434
435 match registration.handler() {
436 ToolHandler::TraitObject(tool) => Some(ToolHandler::TraitObject(
437 wrap_registered_trait_object_tool(registration, tool, workspace, mode),
438 )),
439 ToolHandler::RegistryFn(_) => {
440 let tool = RegistryFnTool::from_registration(self.clone(), registration)?;
441 Some(ToolHandler::TraitObject(match mode {
442 CgpRuntimeMode::Interactive => {
443 Arc::new(wrap_native_tool_interactive(tool, workspace))
444 }
445 CgpRuntimeMode::Ci => Arc::new(wrap_native_tool_ci(tool, workspace)),
446 }))
447 }
448 }
449 }
450
451 pub async fn enable_cgp_pipeline(&self, mode: CgpRuntimeMode) {
459 self.set_cgp_runtime_mode(mode);
460 let snapshot = self.inventory.registrations_snapshot();
461 let mut wrapped_count = 0u32;
462
463 for reg in &snapshot {
464 if reg.is_cgp_wrapped() {
465 continue;
466 }
467
468 let Some(handler) = self.cgp_handler_for_registration(reg, mode) else {
469 continue;
470 };
471
472 if let Err(err) = self.inventory.replace_tool_handler(reg.name(), handler) {
473 tracing::warn!(
474 tool = %reg.name(),
475 %err,
476 "Failed to wrap tool with CGP pipeline"
477 );
478 } else {
479 wrapped_count += 1;
480 }
481 }
482
483 if wrapped_count > 0 {
484 self.rebuild_tool_assembly().await;
485 self.tool_catalog_state
486 .note_explicit_refresh("cgp_pipeline_enable");
487 self.invalidate_hot_cache();
488 tracing::info!(
489 count = wrapped_count,
490 mode = ?mode,
491 "CGP pipeline enabled for registered tools"
492 );
493 }
494 }
495
496 pub async fn register_cgp_tool(
501 &self,
502 tool: Arc<dyn Tool>,
503 capability: crate::config::types::CapabilityLevel,
504 mode: CgpRuntimeMode,
505 ) -> Result<()> {
506 let workspace = self.workspace_root_owned();
507 let tool_name = Arc::<str>::from(tool.name());
508 let registration = match mode {
509 CgpRuntimeMode::Interactive => ToolRegistration::from_cgp_tool(
510 tool_name,
511 capability,
512 wrap_tool_interactive(tool, workspace),
513 ),
514 CgpRuntimeMode::Ci => ToolRegistration::from_cgp_tool(
515 tool_name,
516 capability,
517 wrap_tool_ci(tool, workspace),
518 ),
519 };
520 self.register_tool(registration).await
521 }
522
523 fn invalidate_hot_cache(&self) {
525 self.hot_tool_cache.write().clear();
526 if let Ok(mut cache) = self.cached_available_tools.write() {
527 *cache = None;
528 }
529 }
530}
531
532#[cfg(test)]
533mod tests {
534 use super::*;
535 use crate::tools::traits::Tool;
536 use futures::future::BoxFuture;
537 use std::path::PathBuf;
538
539 struct DummyTool;
540
541 #[async_trait]
542 impl Tool for DummyTool {
543 async fn execute(&self, args: Value) -> Result<Value> {
544 Ok(serde_json::json!({
545 "tool_name": "dummy",
546 "echoed": args,
547 }))
548 }
549
550 fn name(&self) -> &str {
551 "dummy_cgp_test"
552 }
553
554 fn description(&self) -> &str {
555 "A dummy tool for CGP facade tests"
556 }
557 }
558
559 #[tokio::test]
560 async fn enable_cgp_pipeline_wraps_tools() {
561 let registry = ToolRegistry::new(PathBuf::from("/tmp/test")).await;
562 let tool: Arc<dyn Tool> = Arc::new(DummyTool);
563 let reg = ToolRegistration::from_tool(
564 "dummy_cgp_test",
565 crate::config::types::CapabilityLevel::Basic,
566 tool,
567 );
568 registry.register_tool(reg).await.expect("should register");
569
570 registry
571 .enable_cgp_pipeline(CgpRuntimeMode::Interactive)
572 .await;
573
574 let wrapped = registry.get_tool("dummy_cgp_test");
575 assert!(wrapped.is_some(), "tool should still be accessible");
576
577 let result = wrapped
578 .unwrap()
579 .execute(serde_json::json!({"test": true}))
580 .await
581 .expect("should execute");
582 assert_eq!(
583 result.get("echoed").and_then(|v| v.get("test")),
584 Some(&serde_json::json!(true))
585 );
586 }
587
588 #[tokio::test]
589 async fn enable_cgp_pipeline_preserves_registration_metadata_for_trait_object_tools() {
590 struct BridgeTool;
591
592 #[async_trait]
593 impl Tool for BridgeTool {
594 async fn execute(&self, _args: Value) -> Result<Value> {
595 Ok(serde_json::json!({ "path": "bridge" }))
596 }
597
598 async fn execute_dual(&self, _args: Value) -> Result<SplitToolResult> {
599 Ok(SplitToolResult::simple(self.name(), "dual bridge"))
600 }
601
602 fn name(&self) -> &str {
603 "bridge_trait_object"
604 }
605
606 fn description(&self) -> &str {
607 "bridge fallback tool"
608 }
609 }
610
611 let registry = ToolRegistry::new(PathBuf::from("/tmp/test")).await;
612 let registration = ToolRegistration::from_tool_with_metadata(
613 "registered_trait_object_cgp_test",
614 crate::config::types::CapabilityLevel::Basic,
615 Arc::new(BridgeTool),
616 crate::tools::registry::ToolMetadata::default()
617 .with_description("registered trait-object tool")
618 .with_parameter_schema(serde_json::json!({
619 "type": "object",
620 "properties": {
621 "query": { "type": "string" }
622 }
623 }))
624 .with_prompt_path("tools/registered_trait_object.md")
625 .with_permission(ToolPolicy::Allow)
626 .with_allowlist(["tool://allowed"])
627 .with_denylist(["tool://blocked"]),
628 );
629 registry
630 .register_tool(registration)
631 .await
632 .expect("should register");
633
634 registry
635 .enable_cgp_pipeline(CgpRuntimeMode::Interactive)
636 .await;
637
638 let tool = registry
639 .get_tool("registered_trait_object_cgp_test")
640 .expect("tool should exist");
641 assert_eq!(tool.name(), "registered_trait_object_cgp_test");
642 assert_eq!(tool.description(), "registered trait-object tool");
643 assert_eq!(
644 tool.prompt_path().as_deref(),
645 Some("tools/registered_trait_object.md")
646 );
647 assert_eq!(tool.default_permission(), ToolPolicy::Allow);
648 assert_eq!(
649 tool.parameter_schema(),
650 Some(serde_json::json!({
651 "type": "object",
652 "properties": {
653 "query": { "type": "string" }
654 }
655 }))
656 );
657 assert_eq!(tool.allow_patterns(), Some(&["tool://allowed"][..]));
658 assert_eq!(tool.deny_patterns(), Some(&["tool://blocked"][..]));
659
660 let dual = tool
661 .execute_dual(serde_json::json!({ "query": "rust" }))
662 .await
663 .expect("should execute dual");
664 assert_eq!(dual.tool_name, "registered_trait_object_cgp_test");
665 }
666
667 #[tokio::test]
668 async fn enable_cgp_pipeline_ci_mode() {
669 let registry = ToolRegistry::new(PathBuf::from("/tmp/test")).await;
670 let tool: Arc<dyn Tool> = Arc::new(DummyTool);
671 let reg = ToolRegistration::from_tool(
672 "dummy_cgp_test",
673 crate::config::types::CapabilityLevel::Basic,
674 tool,
675 );
676 registry.register_tool(reg).await.expect("should register");
677
678 registry.enable_cgp_pipeline(CgpRuntimeMode::Ci).await;
679
680 let wrapped = registry.get_tool("dummy_cgp_test");
681 assert!(wrapped.is_some());
682
683 let result = wrapped
684 .unwrap()
685 .execute(serde_json::json!({"ci": "mode"}))
686 .await
687 .expect("should execute");
688 assert_eq!(
689 result
690 .get("echoed")
691 .and_then(|v| v.get("ci"))
692 .and_then(|v| v.as_str()),
693 Some("mode")
694 );
695 }
696
697 #[tokio::test]
698 async fn register_cgp_tool_directly() {
699 let registry = ToolRegistry::new(PathBuf::from("/tmp/test")).await;
700 let tool: Arc<dyn Tool> = Arc::new(DummyTool);
701
702 registry
703 .register_cgp_tool(
704 tool,
705 crate::config::types::CapabilityLevel::Basic,
706 CgpRuntimeMode::Interactive,
707 )
708 .await
709 .expect("should register");
710
711 let wrapped = registry.get_tool("dummy_cgp_test");
712 assert!(wrapped.is_some());
713 }
714
715 #[tokio::test]
716 async fn enable_cgp_pipeline_skips_already_wrapped_tools() {
717 let registry = ToolRegistry::new(PathBuf::from("/tmp/test")).await;
718 let tool: Arc<dyn Tool> = Arc::new(DummyTool);
719
720 registry
721 .register_cgp_tool(
722 tool,
723 crate::config::types::CapabilityLevel::Basic,
724 CgpRuntimeMode::Interactive,
725 )
726 .await
727 .expect("should register");
728
729 let before = registry
730 .inventory
731 .registrations_snapshot()
732 .into_iter()
733 .find(|registration| registration.name() == "dummy_cgp_test")
734 .expect("registration should exist");
735 assert!(before.is_cgp_wrapped());
736
737 let before_handler = match before.handler() {
738 ToolHandler::TraitObject(tool) => tool,
739 ToolHandler::RegistryFn(_) => panic!("expected trait object handler"),
740 };
741
742 registry
743 .enable_cgp_pipeline(CgpRuntimeMode::Interactive)
744 .await;
745
746 let after = registry
747 .inventory
748 .registrations_snapshot()
749 .into_iter()
750 .find(|registration| registration.name() == "dummy_cgp_test")
751 .expect("registration should exist");
752 let after_handler = match after.handler() {
753 ToolHandler::TraitObject(tool) => tool,
754 ToolHandler::RegistryFn(_) => panic!("expected trait object handler"),
755 };
756
757 assert!(Arc::ptr_eq(&before_handler, &after_handler));
758 }
759
760 #[tokio::test]
761 async fn enable_cgp_pipeline_prefers_native_cgp_factory() {
762 struct BridgeTool;
763
764 #[async_trait]
765 impl Tool for BridgeTool {
766 async fn execute(&self, _args: Value) -> Result<Value> {
767 Ok(serde_json::json!({ "path": "bridge" }))
768 }
769
770 fn name(&self) -> &str {
771 "native_cgp_factory_test"
772 }
773
774 fn description(&self) -> &str {
775 "bridge fallback tool"
776 }
777 }
778
779 struct NativeTool;
780
781 #[async_trait]
782 impl Tool for NativeTool {
783 async fn execute(&self, _args: Value) -> Result<Value> {
784 Ok(serde_json::json!({ "path": "native" }))
785 }
786
787 fn name(&self) -> &str {
788 "native_cgp_factory_test"
789 }
790
791 fn description(&self) -> &str {
792 "native factory tool"
793 }
794 }
795
796 let registry = ToolRegistry::new(PathBuf::from("/tmp/test")).await;
797 let reg = ToolRegistration::from_tool(
798 "native_cgp_factory_test",
799 crate::config::types::CapabilityLevel::Basic,
800 Arc::new(BridgeTool),
801 )
802 .with_description("registered native factory tool")
803 .with_native_cgp_factory(Arc::new(|registration, workspace_root, mode| {
804 wrap_registered_native_tool(registration, NativeTool, workspace_root, mode)
805 }));
806 registry.register_tool(reg).await.expect("should register");
807
808 registry
809 .enable_cgp_pipeline(CgpRuntimeMode::Interactive)
810 .await;
811
812 let tool = registry
813 .get_tool("native_cgp_factory_test")
814 .expect("tool should exist");
815 assert_eq!(tool.name(), "native_cgp_factory_test");
816 assert_eq!(tool.description(), "registered native factory tool");
817
818 let result = tool
819 .execute(serde_json::json!({}))
820 .await
821 .expect("should execute");
822
823 assert_eq!(result.get("path").and_then(|v| v.as_str()), Some("native"));
824 }
825
826 #[tokio::test]
827 async fn register_tool_after_enabling_cgp_pipeline_wraps_new_tools() {
828 struct LateTool;
829
830 #[async_trait]
831 impl Tool for LateTool {
832 async fn execute(&self, args: Value) -> Result<Value> {
833 Ok(serde_json::json!({
834 "path": "late-bridge",
835 "args": args,
836 }))
837 }
838
839 fn name(&self) -> &str {
840 "late_cgp_test"
841 }
842
843 fn description(&self) -> &str {
844 "late registration test"
845 }
846 }
847
848 let registry = ToolRegistry::new(PathBuf::from("/tmp/test")).await;
849 registry
850 .enable_cgp_pipeline(CgpRuntimeMode::Interactive)
851 .await;
852
853 registry
854 .register_tool(ToolRegistration::from_tool(
855 "late_cgp_test",
856 crate::config::types::CapabilityLevel::Basic,
857 Arc::new(LateTool),
858 ))
859 .await
860 .expect("should register");
861
862 let registration = registry
863 .inventory
864 .registrations_snapshot()
865 .into_iter()
866 .find(|registration| registration.name() == "late_cgp_test")
867 .expect("registration should exist");
868 assert!(registration.is_cgp_wrapped());
869
870 let result = registry
871 .get_tool("late_cgp_test")
872 .expect("tool should exist")
873 .execute(serde_json::json!({"late": true}))
874 .await
875 .expect("should execute");
876 assert_eq!(
877 result.get("path").and_then(|v| v.as_str()),
878 Some("late-bridge")
879 );
880 }
881
882 #[tokio::test]
883 async fn register_tool_after_enabling_cgp_pipeline_prefers_native_factory() {
884 struct BridgeTool;
885
886 #[async_trait]
887 impl Tool for BridgeTool {
888 async fn execute(&self, _args: Value) -> Result<Value> {
889 Ok(serde_json::json!({ "path": "bridge" }))
890 }
891
892 fn name(&self) -> &str {
893 "late_native_cgp_test"
894 }
895
896 fn description(&self) -> &str {
897 "bridge fallback tool"
898 }
899 }
900
901 struct NativeTool;
902
903 #[async_trait]
904 impl Tool for NativeTool {
905 async fn execute(&self, _args: Value) -> Result<Value> {
906 Ok(serde_json::json!({ "path": "late-native" }))
907 }
908
909 fn name(&self) -> &str {
910 "late_native_cgp_test"
911 }
912
913 fn description(&self) -> &str {
914 "native late tool"
915 }
916 }
917
918 let registry = ToolRegistry::new(PathBuf::from("/tmp/test")).await;
919 registry
920 .enable_cgp_pipeline(CgpRuntimeMode::Interactive)
921 .await;
922
923 let registration = ToolRegistration::from_tool(
924 "late_native_cgp_test",
925 crate::config::types::CapabilityLevel::Basic,
926 Arc::new(BridgeTool),
927 )
928 .with_native_cgp_factory(Arc::new(|registration, workspace_root, mode| {
929 wrap_registered_native_tool(registration, NativeTool, workspace_root, mode)
930 }));
931 registry
932 .register_tool(registration)
933 .await
934 .expect("should register");
935
936 let result = registry
937 .get_tool("late_native_cgp_test")
938 .expect("tool should exist")
939 .execute(serde_json::json!({}))
940 .await
941 .expect("should execute");
942
943 assert_eq!(
944 result.get("path").and_then(|v| v.as_str()),
945 Some("late-native")
946 );
947 }
948
949 fn registry_fn_test_executor<'a>(
950 _registry: &'a ToolRegistry,
951 args: Value,
952 ) -> BoxFuture<'a, Result<Value>> {
953 Box::pin(async move {
954 Ok(serde_json::json!({
955 "tool_name": "registry_fn_cgp_test",
956 "echoed": args,
957 }))
958 })
959 }
960
961 #[tokio::test]
962 async fn enable_cgp_pipeline_wraps_registry_fn_tools() {
963 let registry = ToolRegistry::new(PathBuf::from("/tmp/test")).await;
964 let registration = ToolRegistration::new(
965 "registry_fn_cgp_test",
966 crate::config::types::CapabilityLevel::Basic,
967 false,
968 registry_fn_test_executor,
969 )
970 .with_description("Registry function CGP test tool")
971 .with_parameter_schema(serde_json::json!({
972 "type": "object",
973 "properties": {
974 "flag": { "type": "boolean" }
975 }
976 }))
977 .with_permission(ToolPolicy::Allow);
978 registry
979 .register_tool(registration)
980 .await
981 .expect("should register");
982
983 registry
984 .enable_cgp_pipeline(CgpRuntimeMode::Interactive)
985 .await;
986
987 let wrapped = registry
988 .get_tool("registry_fn_cgp_test")
989 .expect("tool exists");
990 assert_eq!(wrapped.name(), "registry_fn_cgp_test");
991 assert_eq!(wrapped.description(), "Registry function CGP test tool");
992 assert!(wrapped.parameter_schema().is_some());
993 assert_eq!(wrapped.default_permission(), ToolPolicy::Allow);
994
995 let result = wrapped
996 .execute(serde_json::json!({"flag": true}))
997 .await
998 .expect("should execute");
999 assert_eq!(
1000 result
1001 .get("echoed")
1002 .and_then(|value| value.get("flag"))
1003 .and_then(|value| value.as_bool()),
1004 Some(true)
1005 );
1006 }
1007
1008 #[tokio::test]
1009 async fn register_registry_fn_after_enabling_cgp_pipeline_wraps_new_tools() {
1010 let registry = ToolRegistry::new(PathBuf::from("/tmp/test")).await;
1011 registry
1012 .enable_cgp_pipeline(CgpRuntimeMode::Interactive)
1013 .await;
1014
1015 registry
1016 .register_tool(ToolRegistration::new(
1017 "late_registry_fn_cgp_test",
1018 crate::config::types::CapabilityLevel::Basic,
1019 false,
1020 registry_fn_test_executor,
1021 ))
1022 .await
1023 .expect("should register");
1024
1025 let registration = registry
1026 .inventory
1027 .registrations_snapshot()
1028 .into_iter()
1029 .find(|registration| registration.name() == "late_registry_fn_cgp_test")
1030 .expect("registration should exist");
1031 assert!(registration.is_cgp_wrapped());
1032
1033 let result = registry
1034 .get_tool("late_registry_fn_cgp_test")
1035 .expect("tool exists")
1036 .execute(serde_json::json!({"late": true}))
1037 .await
1038 .expect("should execute");
1039 assert_eq!(
1040 result
1041 .get("echoed")
1042 .and_then(|value| value.get("late"))
1043 .and_then(|value| value.as_bool()),
1044 Some(true)
1045 );
1046 }
1047}