Skip to main content

vtcode_core/tools/registry/
cgp_facade.rs

1//! CGP integration facade for ToolRegistry.
2//!
3//! Provides `enable_cgp_pipeline()` which prefers tool-specific native CGP
4//! facades when available and otherwise wraps registered `TraitObject` tools
5//! through the CGP approval → sandbox → logging/cache/retry pipeline while
6//! preserving registration-sourced metadata.
7
8use 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
354/// Fallback bridge for registrations that already carry shared tool ownership.
355///
356/// Prefer `wrap_registered_native_tool()` when the caller still owns the
357/// concrete tool instance and does not need to preserve an existing
358/// `Arc<dyn Tool>` handle.
359fn 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/// Runtime mode for CGP pipeline selection.
402#[derive(Debug, Clone, Copy, PartialEq, Eq)]
403pub enum CgpRuntimeMode {
404    /// Interactive TUI sessions: prompt approval + workspace sandbox + tracing.
405    Interactive,
406    /// CI/automation: auto-approval + strict sandbox + no middleware.
407    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    /// Enable the CGP pipeline for all registered tools.
452    ///
453    /// This replaces each eligible tool's handler with a CGP `ToolFacade`
454    /// determined by the runtime mode. Registrations that provide a native CGP
455    /// factory use that directly; `TraitObject` handlers are wrapped with
456    /// registration-backed metadata before entering the passthrough bridge, and
457    /// `RegistryFn` handlers are projected through a concrete `RegistryFnTool`.
458    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    /// Wrap a single tool through the CGP pipeline and register it.
497    ///
498    /// This is the preferred path for new tool registrations that should
499    /// participate in the CGP approval/sandbox/logging/cache/retry pipeline.
500    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    /// Invalidate the hot tool cache after CGP wrapping.
524    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}