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::{wrap_native_tool_ci, wrap_native_tool_interactive, wrap_tool_ci, wrap_tool_interactive};
15use crate::tool_policy::ToolPolicy;
16use crate::tools::result::ToolResult as SplitToolResult;
17use crate::tools::traits::Tool;
18use anyhow::Result;
19use async_trait::async_trait;
20use serde_json::Value;
21
22fn registration_uses_live_task_tracker_metadata(registration: &ToolRegistration) -> bool {
23    registration.name() == crate::config::constants::tools::TASK_TRACKER
24}
25
26fn leak_pattern_str(value: impl Into<String>) -> &'static str {
27    Box::leak(value.into().into_boxed_str())
28}
29
30fn leak_patterns(patterns: &[String]) -> Option<&'static [&'static str]> {
31    if patterns.is_empty() {
32        return None;
33    }
34
35    let leaked_patterns = patterns
36        .iter()
37        .cloned()
38        .map(leak_pattern_str)
39        .collect::<Vec<_>>()
40        .into_boxed_slice();
41    Some(Box::leak(leaked_patterns))
42}
43
44#[derive(Clone)]
45struct RegistrationMetadataSnapshot {
46    name: Arc<str>,
47    description: Arc<str>,
48    parameter_schema: Option<Value>,
49    config_schema: Option<Value>,
50    state_schema: Option<Value>,
51    prompt_path: Option<String>,
52    default_permission: ToolPolicy,
53    allow_patterns: Option<&'static [&'static str]>,
54    deny_patterns: Option<&'static [&'static str]>,
55}
56
57impl RegistrationMetadataSnapshot {
58    fn from_registration(registration: &ToolRegistration) -> Self {
59        Self {
60            name: Arc::<str>::from(registration.name()),
61            description: Arc::<str>::from(registration.metadata().description().unwrap_or_default()),
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.default_permission().unwrap_or(ToolPolicy::Prompt),
67            allow_patterns: leak_patterns(registration.metadata().allowlist()),
68            deny_patterns: leak_patterns(registration.metadata().denylist()),
69        }
70    }
71
72    fn from_registration_with_tool<T>(registration: &ToolRegistration, tool: &T) -> Self
73    where
74        T: Tool + ?Sized,
75    {
76        let use_live_task_tracker_metadata = registration_uses_live_task_tracker_metadata(registration);
77        Self {
78            name: Arc::<str>::from(registration.name()),
79            description: if use_live_task_tracker_metadata {
80                Arc::<str>::from(tool.description())
81            } else {
82                registration
83                    .metadata()
84                    .description()
85                    .map(Arc::<str>::from)
86                    .unwrap_or_else(|| Arc::<str>::from(tool.description()))
87            },
88            parameter_schema: if use_live_task_tracker_metadata {
89                tool.parameter_schema()
90            } else {
91                registration.parameter_schema().cloned().or_else(|| tool.parameter_schema())
92            },
93            config_schema: registration.config_schema().cloned().or_else(|| tool.config_schema()),
94            state_schema: registration.state_schema().cloned().or_else(|| tool.state_schema()),
95            prompt_path: registration
96                .prompt_path()
97                .map(str::to_string)
98                .or_else(|| tool.prompt_path().map(Cow::into_owned)),
99            default_permission: registration.default_permission().unwrap_or_else(|| tool.default_permission()),
100            allow_patterns: leak_patterns(registration.metadata().allowlist()).or_else(|| tool.allow_patterns()),
101            deny_patterns: leak_patterns(registration.metadata().denylist()).or_else(|| tool.deny_patterns()),
102        }
103    }
104
105    /// Shared metadata accessors for the three wrapper tools below.
106    ///
107    /// These bodies are intentionally identical across `RegistryFnTool`,
108    /// `RegistrationBackedTool`, and `RegistrationBackedDynTool`: keep them
109    /// here so a future per-wrapper edit cannot silently diverge one copy.
110    /// `description()` / `parameter_schema()` stay per-impl: they carry the
111    /// task_tracker live-metadata branch, which `RegistryFnTool` cannot
112    /// share (it holds no inner tool to delegate to).
113    fn name(&self) -> &str {
114        self.name.as_ref()
115    }
116
117    fn config_schema(&self) -> Option<Value> {
118        self.config_schema.clone()
119    }
120
121    fn state_schema(&self) -> Option<Value> {
122        self.state_schema.clone()
123    }
124
125    fn prompt_path(&self) -> Option<Cow<'static, str>> {
126        self.prompt_path.clone().map(Cow::Owned)
127    }
128
129    fn default_permission(&self) -> ToolPolicy {
130        self.default_permission.clone()
131    }
132
133    fn allow_patterns(&self) -> Option<&'static [&'static str]> {
134        self.allow_patterns
135    }
136
137    fn deny_patterns(&self) -> Option<&'static [&'static str]> {
138        self.deny_patterns
139    }
140}
141
142struct RegistryFnTool {
143    registry: ToolRegistry,
144    executor: ToolExecutorFn,
145    metadata: RegistrationMetadataSnapshot,
146}
147
148impl RegistryFnTool {
149    fn from_registration(registry: ToolRegistry, registration: &ToolRegistration) -> Option<Self> {
150        let executor = match registration.handler() {
151            ToolHandler::RegistryFn(executor) => executor,
152            ToolHandler::TraitObject(_) => return None,
153        };
154
155        Some(Self {
156            registry,
157            executor,
158            metadata: RegistrationMetadataSnapshot::from_registration(registration),
159        })
160    }
161}
162
163#[async_trait]
164impl Tool for RegistryFnTool {
165    async fn execute(&self, args: Value) -> Result<Value> {
166        (self.executor)(&self.registry, args).await
167    }
168
169    fn name(&self) -> &str {
170        self.metadata.name()
171    }
172
173    fn description(&self) -> &str {
174        self.metadata.description.as_ref()
175    }
176
177    fn parameter_schema(&self) -> Option<Value> {
178        self.metadata.parameter_schema.clone()
179    }
180
181    fn config_schema(&self) -> Option<Value> {
182        self.metadata.config_schema()
183    }
184
185    fn state_schema(&self) -> Option<Value> {
186        self.metadata.state_schema()
187    }
188
189    fn prompt_path(&self) -> Option<Cow<'static, str>> {
190        self.metadata.prompt_path()
191    }
192
193    fn default_permission(&self) -> ToolPolicy {
194        self.metadata.default_permission()
195    }
196
197    fn allow_patterns(&self) -> Option<&'static [&'static str]> {
198        self.metadata.allow_patterns()
199    }
200
201    fn deny_patterns(&self) -> Option<&'static [&'static str]> {
202        self.metadata.deny_patterns()
203    }
204}
205
206struct RegistrationBackedTool<T> {
207    inner: T,
208    metadata: RegistrationMetadataSnapshot,
209}
210
211impl<T> RegistrationBackedTool<T>
212where
213    T: Tool + Send + Sync,
214{
215    fn from_registration(inner: T, registration: &ToolRegistration) -> Self {
216        let metadata = RegistrationMetadataSnapshot::from_registration_with_tool(registration, &inner);
217        Self { inner, metadata }
218    }
219}
220
221#[async_trait]
222impl<T> Tool for RegistrationBackedTool<T>
223where
224    T: Tool + Send + Sync,
225{
226    async fn execute(&self, args: Value) -> Result<Value> {
227        self.inner.execute(args).await
228    }
229
230    async fn execute_dual(&self, args: Value) -> Result<SplitToolResult> {
231        let mut result = self.inner.execute_dual(args).await?;
232        result.tool_name = self.name().to_string();
233        Ok(result)
234    }
235
236    fn name(&self) -> &str {
237        self.metadata.name()
238    }
239
240    fn description(&self) -> &str {
241        if self.metadata.name.as_ref() == crate::config::constants::tools::TASK_TRACKER {
242            self.inner.description()
243        } else {
244            self.metadata.description.as_ref()
245        }
246    }
247
248    fn validate_args(&self, args: &Value) -> Result<()> {
249        self.inner.validate_args(args)
250    }
251
252    fn parameter_schema(&self) -> Option<Value> {
253        if self.metadata.name.as_ref() == crate::config::constants::tools::TASK_TRACKER {
254            self.inner.parameter_schema()
255        } else {
256            self.metadata.parameter_schema.clone()
257        }
258    }
259
260    fn config_schema(&self) -> Option<Value> {
261        self.metadata.config_schema()
262    }
263
264    fn state_schema(&self) -> Option<Value> {
265        self.metadata.state_schema()
266    }
267
268    fn prompt_path(&self) -> Option<Cow<'static, str>> {
269        self.metadata.prompt_path()
270    }
271
272    fn default_permission(&self) -> ToolPolicy {
273        self.metadata.default_permission()
274    }
275
276    fn allow_patterns(&self) -> Option<&'static [&'static str]> {
277        self.metadata.allow_patterns()
278    }
279
280    fn deny_patterns(&self) -> Option<&'static [&'static str]> {
281        self.metadata.deny_patterns()
282    }
283
284    fn is_mutating(&self) -> bool {
285        self.inner.is_mutating()
286    }
287
288    fn is_parallel_safe(&self) -> bool {
289        self.inner.is_parallel_safe()
290    }
291
292    fn kind(&self) -> &'static str {
293        self.inner.kind()
294    }
295
296    fn resource_hints(&self, args: &Value) -> Vec<String> {
297        self.inner.resource_hints(args)
298    }
299
300    fn execution_cost(&self) -> u8 {
301        self.inner.execution_cost()
302    }
303}
304
305struct RegistrationBackedDynTool {
306    inner: Arc<dyn Tool>,
307    metadata: RegistrationMetadataSnapshot,
308}
309
310impl RegistrationBackedDynTool {
311    fn from_registration(inner: Arc<dyn Tool>, registration: &ToolRegistration) -> Self {
312        let metadata = RegistrationMetadataSnapshot::from_registration_with_tool(registration, inner.as_ref());
313        Self { inner, metadata }
314    }
315}
316
317#[async_trait]
318impl Tool for RegistrationBackedDynTool {
319    async fn execute(&self, args: Value) -> Result<Value> {
320        self.inner.execute(args).await
321    }
322
323    async fn execute_dual(&self, args: Value) -> Result<SplitToolResult> {
324        let mut result = self.inner.execute_dual(args).await?;
325        result.tool_name = self.name().to_string();
326        Ok(result)
327    }
328
329    fn name(&self) -> &str {
330        self.metadata.name()
331    }
332
333    fn description(&self) -> &str {
334        if self.metadata.name.as_ref() == crate::config::constants::tools::TASK_TRACKER {
335            self.inner.description()
336        } else {
337            self.metadata.description.as_ref()
338        }
339    }
340
341    fn validate_args(&self, args: &Value) -> Result<()> {
342        self.inner.validate_args(args)
343    }
344
345    fn parameter_schema(&self) -> Option<Value> {
346        if self.metadata.name.as_ref() == crate::config::constants::tools::TASK_TRACKER {
347            self.inner.parameter_schema()
348        } else {
349            self.metadata.parameter_schema.clone()
350        }
351    }
352
353    fn config_schema(&self) -> Option<Value> {
354        self.metadata.config_schema()
355    }
356
357    fn state_schema(&self) -> Option<Value> {
358        self.metadata.state_schema()
359    }
360
361    fn prompt_path(&self) -> Option<Cow<'static, str>> {
362        self.metadata.prompt_path()
363    }
364
365    fn default_permission(&self) -> ToolPolicy {
366        self.metadata.default_permission()
367    }
368
369    fn allow_patterns(&self) -> Option<&'static [&'static str]> {
370        self.metadata.allow_patterns()
371    }
372
373    fn deny_patterns(&self) -> Option<&'static [&'static str]> {
374        self.metadata.deny_patterns()
375    }
376
377    fn is_mutating(&self) -> bool {
378        self.inner.is_mutating()
379    }
380
381    fn is_parallel_safe(&self) -> bool {
382        self.inner.is_parallel_safe()
383    }
384
385    fn kind(&self) -> &'static str {
386        self.inner.kind()
387    }
388
389    fn resource_hints(&self, args: &Value) -> Vec<String> {
390        self.inner.resource_hints(args)
391    }
392
393    fn execution_cost(&self) -> u8 {
394        self.inner.execution_cost()
395    }
396}
397
398/// Fallback bridge for registrations that already carry shared tool ownership.
399///
400/// Prefer `wrap_registered_native_tool()` when the caller still owns the
401/// concrete tool instance and does not need to preserve an existing
402/// `Arc<dyn Tool>` handle.
403fn wrap_registered_trait_object_tool(
404    registration: &ToolRegistration,
405    tool: Arc<dyn Tool>,
406    workspace_root: PathBuf,
407    mode: CgpRuntimeMode,
408) -> Arc<dyn Tool> {
409    let tool: Arc<dyn Tool> = Arc::new(RegistrationBackedDynTool::from_registration(tool, registration));
410    match mode {
411        CgpRuntimeMode::Interactive => Arc::new(wrap_tool_interactive(tool, workspace_root)),
412        CgpRuntimeMode::Ci => Arc::new(wrap_tool_ci(tool, workspace_root)),
413    }
414}
415
416pub fn wrap_registered_native_tool<T>(
417    registration: &ToolRegistration,
418    tool: T,
419    workspace_root: PathBuf,
420    mode: CgpRuntimeMode,
421) -> Arc<dyn Tool>
422where
423    T: Tool + Send + Sync + 'static,
424{
425    let tool = RegistrationBackedTool::from_registration(tool, registration);
426    match mode {
427        CgpRuntimeMode::Interactive => Arc::new(wrap_native_tool_interactive(tool, workspace_root)),
428        CgpRuntimeMode::Ci => Arc::new(wrap_native_tool_ci(tool, workspace_root)),
429    }
430}
431
432pub fn native_cgp_tool_factory<T, F>(build_tool: F) -> super::registration::NativeCgpToolFactory
433where
434    T: Tool + Send + Sync + 'static,
435    F: Fn() -> T + Send + Sync + 'static,
436{
437    Arc::new(move |registration, workspace_root, mode| {
438        wrap_registered_native_tool(registration, build_tool(), workspace_root, mode)
439    })
440}
441
442/// Runtime mode for CGP pipeline selection.
443#[derive(Debug, Clone, Copy, PartialEq, Eq)]
444pub enum CgpRuntimeMode {
445    /// Interactive TUI sessions: prompt approval + workspace sandbox + tracing.
446    Interactive,
447    /// CI/automation: auto-approval + strict sandbox + no middleware.
448    Ci,
449}
450
451impl ToolRegistry {
452    pub(crate) fn current_cgp_mode(&self) -> Option<CgpRuntimeMode> {
453        *self.cgp_runtime_mode.read().unwrap_or_else(|e| e.into_inner())
454    }
455
456    fn set_cgp_runtime_mode(&self, mode: CgpRuntimeMode) {
457        if let Ok(mut current_mode) = self.cgp_runtime_mode.write() {
458            *current_mode = Some(mode);
459        }
460    }
461
462    pub(crate) fn cgp_handler_for_registration(
463        &self,
464        registration: &ToolRegistration,
465        mode: CgpRuntimeMode,
466    ) -> Option<ToolHandler> {
467        let workspace = self.workspace_root_owned();
468        if let Some(factory) = registration.native_cgp_factory() {
469            return Some(ToolHandler::TraitObject(factory(registration, workspace, mode)));
470        }
471
472        match registration.handler() {
473            ToolHandler::TraitObject(tool) => {
474                Some(ToolHandler::TraitObject(wrap_registered_trait_object_tool(registration, tool, workspace, mode)))
475            }
476            ToolHandler::RegistryFn(_) => {
477                let tool = RegistryFnTool::from_registration(self.clone(), registration)?;
478                Some(ToolHandler::TraitObject(match mode {
479                    CgpRuntimeMode::Interactive => Arc::new(wrap_native_tool_interactive(tool, workspace)),
480                    CgpRuntimeMode::Ci => Arc::new(wrap_native_tool_ci(tool, workspace)),
481                }))
482            }
483        }
484    }
485
486    /// Enable the CGP pipeline for all registered tools.
487    ///
488    /// This replaces each eligible tool's handler with a CGP `ToolFacade`
489    /// determined by the runtime mode. Registrations that provide a native CGP
490    /// factory use that directly; `TraitObject` handlers are wrapped with
491    /// registration-backed metadata before entering the passthrough bridge, and
492    /// `RegistryFn` handlers are projected through a concrete `RegistryFnTool`.
493    pub async fn enable_cgp_pipeline(&self, mode: CgpRuntimeMode) {
494        self.set_cgp_runtime_mode(mode);
495        let snapshot = self.inventory.registrations_snapshot();
496        let mut wrapped_count = 0u32;
497
498        for reg in &snapshot {
499            if reg.is_cgp_wrapped() {
500                continue;
501            }
502
503            let Some(handler) = self.cgp_handler_for_registration(reg, mode) else {
504                continue;
505            };
506
507            if let Err(err) = self.inventory.replace_tool_handler(reg.name(), handler) {
508                tracing::warn!(
509                    tool = %reg.name(),
510                    %err,
511                    "Failed to wrap tool with CGP pipeline"
512                );
513            } else {
514                wrapped_count += 1;
515            }
516        }
517
518        if wrapped_count > 0 {
519            self.rebuild_tool_assembly().await;
520            self.tool_catalog_state.note_explicit_refresh("cgp_pipeline_enable");
521            self.invalidate_hot_cache();
522            tracing::info!(
523                count = wrapped_count,
524                mode = ?mode,
525                "CGP pipeline enabled for registered tools"
526            );
527        }
528    }
529
530    /// Wrap a single tool through the CGP pipeline and register it.
531    ///
532    /// This is the preferred path for new tool registrations that should
533    /// participate in the CGP approval/sandbox/logging/cache/retry pipeline.
534    pub async fn register_cgp_tool(
535        &self,
536        tool: Arc<dyn Tool>,
537        capability: crate::config::types::CapabilityLevel,
538        mode: CgpRuntimeMode,
539    ) -> Result<()> {
540        let workspace = self.workspace_root_owned();
541        let tool_name = Arc::<str>::from(tool.name());
542        let registration = match mode {
543            CgpRuntimeMode::Interactive => {
544                ToolRegistration::from_cgp_tool(tool_name, capability, wrap_tool_interactive(tool, workspace))
545            }
546            CgpRuntimeMode::Ci => ToolRegistration::from_cgp_tool(tool_name, capability, wrap_tool_ci(tool, workspace)),
547        };
548        self.register_tool(registration).await
549    }
550
551    /// Invalidate the hot tool cache after CGP wrapping.
552    fn invalidate_hot_cache(&self) {
553        self.hot_tool_cache.write().clear();
554        *self.cached_available_tools.write() = None;
555    }
556}
557
558#[cfg(test)]
559mod tests {
560    use super::*;
561    use crate::config::constants::tools;
562    use crate::tools::handlers::{PlanningWorkflowState, TaskTrackerTool};
563    use crate::tools::traits::Tool;
564    use futures::future::BoxFuture;
565    use std::path::PathBuf;
566    use tempfile::TempDir;
567
568    struct DummyTool;
569
570    #[async_trait]
571    impl Tool for DummyTool {
572        async fn execute(&self, args: Value) -> Result<Value> {
573            Ok(serde_json::json!({
574                "tool_name": "dummy",
575                "echoed": args,
576            }))
577        }
578
579        fn name(&self) -> &str {
580            "dummy_cgp_test"
581        }
582
583        fn description(&self) -> &str {
584            "A dummy tool for CGP facade tests"
585        }
586    }
587
588    #[tokio::test]
589    async fn enable_cgp_pipeline_wraps_tools() {
590        let registry = ToolRegistry::new(PathBuf::from("/tmp/test")).await;
591        let tool: Arc<dyn Tool> = Arc::new(DummyTool);
592        let reg = ToolRegistration::from_tool("dummy_cgp_test", crate::config::types::CapabilityLevel::Basic, tool);
593        registry.register_tool(reg).await.expect("should register");
594
595        registry.enable_cgp_pipeline(CgpRuntimeMode::Interactive).await;
596
597        let wrapped = registry.get_tool("dummy_cgp_test");
598        assert!(wrapped.is_some(), "tool should still be accessible");
599
600        let result = wrapped
601            .unwrap()
602            .execute(serde_json::json!({"test": true}))
603            .await
604            .expect("should execute");
605        assert_eq!(result.get("echoed").and_then(|v| v.get("test")), Some(&serde_json::json!(true)));
606    }
607
608    #[tokio::test]
609    async fn enable_cgp_pipeline_preserves_registration_metadata_for_trait_object_tools() {
610        struct BridgeTool;
611
612        #[async_trait]
613        impl Tool for BridgeTool {
614            async fn execute(&self, _args: Value) -> Result<Value> {
615                Ok(serde_json::json!({ "path": "bridge" }))
616            }
617
618            async fn execute_dual(&self, _args: Value) -> Result<SplitToolResult> {
619                Ok(SplitToolResult::simple(self.name(), "dual bridge"))
620            }
621
622            fn name(&self) -> &str {
623                "bridge_trait_object"
624            }
625
626            fn description(&self) -> &str {
627                "bridge fallback tool"
628            }
629        }
630
631        let registry = ToolRegistry::new(PathBuf::from("/tmp/test")).await;
632        let registration = ToolRegistration::from_tool_with_metadata(
633            "registered_trait_object_cgp_test",
634            crate::config::types::CapabilityLevel::Basic,
635            Arc::new(BridgeTool),
636            crate::tools::registry::ToolMetadata::default()
637                .with_description("registered trait-object tool")
638                .with_parameter_schema(serde_json::json!({
639                    "type": "object",
640                    "properties": {
641                        "query": { "type": "string" }
642                    }
643                }))
644                .with_prompt_path("tools/registered_trait_object.md")
645                .with_permission(ToolPolicy::Allow)
646                .with_allowlist(["tool://allowed"])
647                .with_denylist(["tool://blocked"]),
648        );
649        registry.register_tool(registration).await.expect("should register");
650
651        registry.enable_cgp_pipeline(CgpRuntimeMode::Interactive).await;
652
653        let tool = registry
654            .get_tool("registered_trait_object_cgp_test")
655            .expect("tool should exist");
656        assert_eq!(tool.name(), "registered_trait_object_cgp_test");
657        assert_eq!(tool.description(), "registered trait-object tool");
658        assert_eq!(tool.prompt_path().as_deref(), Some("tools/registered_trait_object.md"));
659        assert_eq!(tool.default_permission(), ToolPolicy::Allow);
660        assert_eq!(
661            tool.parameter_schema(),
662            Some(serde_json::json!({
663                "type": "object",
664                "properties": {
665                    "query": { "type": "string" }
666                }
667            }))
668        );
669        assert_eq!(tool.allow_patterns(), Some(&["tool://allowed"][..]));
670        assert_eq!(tool.deny_patterns(), Some(&["tool://blocked"][..]));
671
672        let dual = tool
673            .execute_dual(serde_json::json!({ "query": "rust" }))
674            .await
675            .expect("should execute dual");
676        assert_eq!(dual.tool_name, "registered_trait_object_cgp_test");
677    }
678
679    #[tokio::test]
680    async fn registration_wrappers_share_snapshot_metadata_accessors() {
681        // The seven snapshot-sourced accessors are byte-identical across all
682        // three wrapper types; only description()/parameter_schema() carry a
683        // task_tracker live-metadata branch. Wrap one non-task_tracker tool
684        // all three ways and require identical output, so a future
685        // per-wrapper edit cannot silently diverge one copy. Asymmetric
686        // controls: the inner tool disagrees with its registration on every
687        // accessor (proving the snapshot wins), and a TraitObject
688        // registration is rejected by RegistryFnTool (proving why it has no
689        // live branch to share).
690        struct MetaTool;
691
692        #[async_trait]
693        impl Tool for MetaTool {
694            async fn execute(&self, _args: Value) -> Result<Value> {
695                Ok(Value::Null)
696            }
697
698            fn name(&self) -> &str {
699                "meta_probe_inner"
700            }
701
702            fn description(&self) -> &str {
703                "inner description"
704            }
705        }
706
707        fn echo_executor<'a>(_registry: &'a ToolRegistry, args: Value) -> BoxFuture<'a, Result<Value>> {
708            Box::pin(async move { Ok(args) })
709        }
710
711        let parameter_schema = serde_json::json!({
712            "type": "object",
713            "properties": { "query": { "type": "string" } },
714        });
715        let config_schema = serde_json::json!({ "type": "object" });
716        let state_schema = serde_json::json!({ "type": "object" });
717        let registration = ToolRegistration::from_tool_with_metadata(
718            "meta_probe",
719            crate::config::types::CapabilityLevel::Basic,
720            Arc::new(MetaTool),
721            crate::tools::registry::ToolMetadata::default()
722                .with_description("registered description")
723                .with_parameter_schema(parameter_schema.clone())
724                .with_config_schema(config_schema.clone())
725                .with_state_schema(state_schema.clone())
726                .with_prompt_path("tools/meta_probe.md")
727                .with_permission(ToolPolicy::Allow)
728                .with_allowlist(["tool://allowed"])
729                .with_denylist(["tool://blocked"]),
730        );
731
732        let snapshot = RegistrationMetadataSnapshot::from_registration(&registration);
733        assert_eq!(snapshot.name(), "meta_probe");
734        assert_eq!(snapshot.config_schema(), Some(config_schema.clone()));
735        assert_eq!(snapshot.state_schema(), Some(state_schema.clone()));
736        assert_eq!(snapshot.prompt_path().as_deref(), Some("tools/meta_probe.md"));
737        assert_eq!(snapshot.default_permission(), ToolPolicy::Allow);
738        assert_eq!(snapshot.allow_patterns(), Some(&["tool://allowed"][..]));
739        assert_eq!(snapshot.deny_patterns(), Some(&["tool://blocked"][..]));
740
741        let backed = RegistrationBackedTool::from_registration(MetaTool, &registration);
742        let backed_dyn = RegistrationBackedDynTool::from_registration(Arc::new(MetaTool), &registration);
743        for (label, tool) in [
744            ("backed", &backed as &dyn Tool),
745            ("backed_dyn", &backed_dyn as &dyn Tool),
746        ] {
747            assert_eq!(tool.name(), snapshot.name(), "{label} name");
748            assert_eq!(tool.description(), "registered description", "{label} keeps snapshot over inner");
749            assert_eq!(tool.parameter_schema(), Some(parameter_schema.clone()), "{label} parameter schema");
750            assert_eq!(tool.config_schema(), snapshot.config_schema(), "{label} config schema");
751            assert_eq!(tool.state_schema(), snapshot.state_schema(), "{label} state schema");
752            assert_eq!(tool.prompt_path().as_deref(), Some("tools/meta_probe.md"), "{label} prompt path");
753            assert_eq!(tool.default_permission(), ToolPolicy::Allow, "{label} permission");
754            assert_eq!(tool.allow_patterns(), Some(&["tool://allowed"][..]), "{label} allowlist");
755            assert_eq!(tool.deny_patterns(), Some(&["tool://blocked"][..]), "{label} denylist");
756        }
757
758        let temp_dir = TempDir::new().expect("temp dir");
759        let registry = ToolRegistry::new(temp_dir.path().to_path_buf()).await;
760        let fn_registration =
761            ToolRegistration::new("meta_probe_fn", crate::config::types::CapabilityLevel::Basic, false, echo_executor);
762        let func = RegistryFnTool::from_registration(registry, &fn_registration)
763            .expect("RegistryFn handler registration must wrap");
764        let fn_snapshot = RegistrationMetadataSnapshot::from_registration(&fn_registration);
765        assert_eq!(func.name(), fn_snapshot.name());
766        assert_eq!(func.description(), "");
767        assert_eq!(func.parameter_schema(), None);
768        assert_eq!(func.config_schema(), fn_snapshot.config_schema());
769        assert_eq!(func.state_schema(), fn_snapshot.state_schema());
770        assert_eq!(func.prompt_path().as_deref(), None);
771        assert_eq!(func.default_permission(), ToolPolicy::Prompt);
772        assert_eq!(func.allow_patterns(), None);
773        assert_eq!(func.deny_patterns(), None);
774
775        let registry = ToolRegistry::new(temp_dir.path().to_path_buf()).await;
776        assert!(
777            RegistryFnTool::from_registration(registry, &registration).is_none(),
778            "TraitObject registrations must not wrap as RegistryFnTool"
779        );
780    }
781
782    #[tokio::test]
783    async fn enable_cgp_pipeline_ci_mode() {
784        let registry = ToolRegistry::new(PathBuf::from("/tmp/test")).await;
785        let tool: Arc<dyn Tool> = Arc::new(DummyTool);
786        let reg = ToolRegistration::from_tool("dummy_cgp_test", crate::config::types::CapabilityLevel::Basic, tool);
787        registry.register_tool(reg).await.expect("should register");
788
789        registry.enable_cgp_pipeline(CgpRuntimeMode::Ci).await;
790
791        let wrapped = registry.get_tool("dummy_cgp_test");
792        assert!(wrapped.is_some());
793
794        let result = wrapped
795            .unwrap()
796            .execute(serde_json::json!({"ci": "mode"}))
797            .await
798            .expect("should execute");
799        assert_eq!(result.get("echoed").and_then(|v| v.get("ci")).and_then(|v| v.as_str()), Some("mode"));
800    }
801
802    #[tokio::test]
803    async fn register_cgp_tool_directly() {
804        let registry = ToolRegistry::new(PathBuf::from("/tmp/test")).await;
805        let tool: Arc<dyn Tool> = Arc::new(DummyTool);
806
807        registry
808            .register_cgp_tool(tool, crate::config::types::CapabilityLevel::Basic, CgpRuntimeMode::Interactive)
809            .await
810            .expect("should register");
811
812        let wrapped = registry.get_tool("dummy_cgp_test");
813        assert!(wrapped.is_some());
814    }
815
816    #[tokio::test]
817    async fn enable_cgp_pipeline_skips_already_wrapped_tools() {
818        let registry = ToolRegistry::new(PathBuf::from("/tmp/test")).await;
819        let tool: Arc<dyn Tool> = Arc::new(DummyTool);
820
821        registry
822            .register_cgp_tool(tool, crate::config::types::CapabilityLevel::Basic, CgpRuntimeMode::Interactive)
823            .await
824            .expect("should register");
825
826        let before = registry
827            .inventory
828            .registrations_snapshot()
829            .into_iter()
830            .find(|registration| registration.name() == "dummy_cgp_test")
831            .expect("registration should exist");
832        assert!(before.is_cgp_wrapped());
833
834        let before_handler = match before.handler() {
835            ToolHandler::TraitObject(tool) => tool,
836            ToolHandler::RegistryFn(_) => panic!("expected trait object handler"),
837        };
838
839        registry.enable_cgp_pipeline(CgpRuntimeMode::Interactive).await;
840
841        let after = registry
842            .inventory
843            .registrations_snapshot()
844            .into_iter()
845            .find(|registration| registration.name() == "dummy_cgp_test")
846            .expect("registration should exist");
847        let after_handler = match after.handler() {
848            ToolHandler::TraitObject(tool) => tool,
849            ToolHandler::RegistryFn(_) => panic!("expected trait object handler"),
850        };
851
852        assert!(Arc::ptr_eq(&before_handler, &after_handler));
853    }
854
855    #[tokio::test]
856    async fn enable_cgp_pipeline_prefers_native_cgp_factory() {
857        struct BridgeTool;
858
859        #[async_trait]
860        impl Tool for BridgeTool {
861            async fn execute(&self, _args: Value) -> Result<Value> {
862                Ok(serde_json::json!({ "path": "bridge" }))
863            }
864
865            fn name(&self) -> &str {
866                "native_cgp_factory_test"
867            }
868
869            fn description(&self) -> &str {
870                "bridge fallback tool"
871            }
872        }
873
874        struct NativeTool;
875
876        #[async_trait]
877        impl Tool for NativeTool {
878            async fn execute(&self, _args: Value) -> Result<Value> {
879                Ok(serde_json::json!({ "path": "native" }))
880            }
881
882            fn name(&self) -> &str {
883                "native_cgp_factory_test"
884            }
885
886            fn description(&self) -> &str {
887                "native factory tool"
888            }
889        }
890
891        let registry = ToolRegistry::new(PathBuf::from("/tmp/test")).await;
892        let reg = ToolRegistration::from_tool(
893            "native_cgp_factory_test",
894            crate::config::types::CapabilityLevel::Basic,
895            Arc::new(BridgeTool),
896        )
897        .with_description("registered native factory tool")
898        .with_native_cgp_factory(Arc::new(|registration, workspace_root, mode| {
899            wrap_registered_native_tool(registration, NativeTool, workspace_root, mode)
900        }));
901        registry.register_tool(reg).await.expect("should register");
902
903        registry.enable_cgp_pipeline(CgpRuntimeMode::Interactive).await;
904
905        let tool = registry.get_tool("native_cgp_factory_test").expect("tool should exist");
906        assert_eq!(tool.name(), "native_cgp_factory_test");
907        assert_eq!(tool.description(), "registered native factory tool");
908
909        let result = tool.execute(serde_json::json!({})).await.expect("should execute");
910
911        assert_eq!(result.get("path").and_then(|v| v.as_str()), Some("native"));
912    }
913
914    #[tokio::test]
915    async fn enable_cgp_pipeline_uses_live_task_tracker_metadata_for_native_factory_tools() {
916        let temp_dir = TempDir::new().expect("temp dir");
917        let registry = ToolRegistry::new(temp_dir.path().to_path_buf()).await;
918        let plan_state = registry.planning_workflow_state();
919        let registration = registry
920            .inventory
921            .get_registration(tools::TASK_TRACKER)
922            .expect("task_tracker registration exists");
923
924        registry.enable_cgp_pipeline(CgpRuntimeMode::Interactive).await;
925
926        let tool = registry.get_tool(tools::TASK_TRACKER).expect("wrapped task_tracker tool");
927        assert_eq!(
928            tool.description(),
929            crate::tools::handlers::task_tracker::task_tracker_description_for_workflow(false)
930        );
931        assert_eq!(tool.parameter_schema().expect("standard schema")["properties"]["index"]["minimum"], 0);
932
933        plan_state.enable();
934
935        let tool = registry.get_tool(tools::TASK_TRACKER).expect("wrapped task_tracker tool");
936        assert_eq!(
937            tool.description(),
938            crate::tools::handlers::task_tracker::task_tracker_description_for_workflow(true)
939        );
940        assert_eq!(tool.parameter_schema().expect("planning schema")["properties"]["index"]["minimum"], 1);
941
942        let direct_native = wrap_registered_native_tool(
943            &registration,
944            TaskTrackerTool::new(temp_dir.path().to_path_buf(), plan_state.clone()),
945            temp_dir.path().to_path_buf(),
946            CgpRuntimeMode::Interactive,
947        );
948        assert_eq!(
949            direct_native.description(),
950            crate::tools::handlers::task_tracker::task_tracker_description_for_workflow(true)
951        );
952        assert_eq!(
953            direct_native.parameter_schema().expect("native planning schema")["properties"]["index"]["minimum"],
954            1
955        );
956    }
957
958    #[tokio::test]
959    async fn enable_cgp_pipeline_uses_live_task_tracker_metadata_for_trait_object_tools() {
960        let temp_dir = TempDir::new().expect("temp dir");
961        let registry = ToolRegistry::new(temp_dir.path().to_path_buf()).await;
962        let state = PlanningWorkflowState::new(temp_dir.path().to_path_buf());
963        let registration = ToolRegistration::from_tool(
964            tools::TASK_TRACKER,
965            crate::config::types::CapabilityLevel::Basic,
966            Arc::new(TaskTrackerTool::new(temp_dir.path().to_path_buf(), state.clone())),
967        )
968        .with_description("stale registered task tracker metadata");
969
970        registry.register_tool(registration).await.expect("should register override");
971        registry.enable_cgp_pipeline(CgpRuntimeMode::Interactive).await;
972
973        let tool = registry.get_tool(tools::TASK_TRACKER).expect("wrapped task_tracker tool");
974        assert_eq!(
975            tool.description(),
976            crate::tools::handlers::task_tracker::task_tracker_description_for_workflow(false)
977        );
978
979        state.enable();
980
981        let tool = registry.get_tool(tools::TASK_TRACKER).expect("wrapped task_tracker tool");
982        assert_eq!(
983            tool.description(),
984            crate::tools::handlers::task_tracker::task_tracker_description_for_workflow(true)
985        );
986        assert_eq!(tool.parameter_schema().expect("planning schema")["properties"]["index"]["minimum"], 1);
987    }
988
989    #[tokio::test]
990    async fn register_tool_after_enabling_cgp_pipeline_wraps_new_tools() {
991        struct LateTool;
992
993        #[async_trait]
994        impl Tool for LateTool {
995            async fn execute(&self, args: Value) -> Result<Value> {
996                Ok(serde_json::json!({
997                    "path": "late-bridge",
998                    "args": args,
999                }))
1000            }
1001
1002            fn name(&self) -> &str {
1003                "late_cgp_test"
1004            }
1005
1006            fn description(&self) -> &str {
1007                "late registration test"
1008            }
1009        }
1010
1011        let registry = ToolRegistry::new(PathBuf::from("/tmp/test")).await;
1012        registry.enable_cgp_pipeline(CgpRuntimeMode::Interactive).await;
1013
1014        registry
1015            .register_tool(ToolRegistration::from_tool(
1016                "late_cgp_test",
1017                crate::config::types::CapabilityLevel::Basic,
1018                Arc::new(LateTool),
1019            ))
1020            .await
1021            .expect("should register");
1022
1023        let registration = registry
1024            .inventory
1025            .registrations_snapshot()
1026            .into_iter()
1027            .find(|registration| registration.name() == "late_cgp_test")
1028            .expect("registration should exist");
1029        assert!(registration.is_cgp_wrapped());
1030
1031        let result = registry
1032            .get_tool("late_cgp_test")
1033            .expect("tool should exist")
1034            .execute(serde_json::json!({"late": true}))
1035            .await
1036            .expect("should execute");
1037        assert_eq!(result.get("path").and_then(|v| v.as_str()), Some("late-bridge"));
1038    }
1039
1040    #[tokio::test]
1041    async fn register_tool_after_enabling_cgp_pipeline_prefers_native_factory() {
1042        struct BridgeTool;
1043
1044        #[async_trait]
1045        impl Tool for BridgeTool {
1046            async fn execute(&self, _args: Value) -> Result<Value> {
1047                Ok(serde_json::json!({ "path": "bridge" }))
1048            }
1049
1050            fn name(&self) -> &str {
1051                "late_native_cgp_test"
1052            }
1053
1054            fn description(&self) -> &str {
1055                "bridge fallback tool"
1056            }
1057        }
1058
1059        struct NativeTool;
1060
1061        #[async_trait]
1062        impl Tool for NativeTool {
1063            async fn execute(&self, _args: Value) -> Result<Value> {
1064                Ok(serde_json::json!({ "path": "late-native" }))
1065            }
1066
1067            fn name(&self) -> &str {
1068                "late_native_cgp_test"
1069            }
1070
1071            fn description(&self) -> &str {
1072                "native late tool"
1073            }
1074        }
1075
1076        let registry = ToolRegistry::new(PathBuf::from("/tmp/test")).await;
1077        registry.enable_cgp_pipeline(CgpRuntimeMode::Interactive).await;
1078
1079        let registration = ToolRegistration::from_tool(
1080            "late_native_cgp_test",
1081            crate::config::types::CapabilityLevel::Basic,
1082            Arc::new(BridgeTool),
1083        )
1084        .with_native_cgp_factory(Arc::new(|registration, workspace_root, mode| {
1085            wrap_registered_native_tool(registration, NativeTool, workspace_root, mode)
1086        }));
1087        registry.register_tool(registration).await.expect("should register");
1088
1089        let result = registry
1090            .get_tool("late_native_cgp_test")
1091            .expect("tool should exist")
1092            .execute(serde_json::json!({}))
1093            .await
1094            .expect("should execute");
1095
1096        assert_eq!(result.get("path").and_then(|v| v.as_str()), Some("late-native"));
1097    }
1098
1099    fn registry_fn_test_executor<'a>(_registry: &'a ToolRegistry, args: Value) -> BoxFuture<'a, Result<Value>> {
1100        Box::pin(async move {
1101            Ok(serde_json::json!({
1102                "tool_name": "registry_fn_cgp_test",
1103                "echoed": args,
1104            }))
1105        })
1106    }
1107
1108    #[tokio::test]
1109    async fn enable_cgp_pipeline_wraps_registry_fn_tools() {
1110        let registry = ToolRegistry::new(PathBuf::from("/tmp/test")).await;
1111        let registration = ToolRegistration::new(
1112            "registry_fn_cgp_test",
1113            crate::config::types::CapabilityLevel::Basic,
1114            false,
1115            registry_fn_test_executor,
1116        )
1117        .with_description("Registry function CGP test tool")
1118        .with_parameter_schema(serde_json::json!({
1119            "type": "object",
1120            "properties": {
1121                "flag": { "type": "boolean" }
1122            }
1123        }))
1124        .with_permission(ToolPolicy::Allow);
1125        registry.register_tool(registration).await.expect("should register");
1126
1127        registry.enable_cgp_pipeline(CgpRuntimeMode::Interactive).await;
1128
1129        let wrapped = registry.get_tool("registry_fn_cgp_test").expect("tool exists");
1130        assert_eq!(wrapped.name(), "registry_fn_cgp_test");
1131        assert_eq!(wrapped.description(), "Registry function CGP test tool");
1132        assert!(wrapped.parameter_schema().is_some());
1133        assert_eq!(wrapped.default_permission(), ToolPolicy::Allow);
1134
1135        let result = wrapped
1136            .execute(serde_json::json!({"flag": true}))
1137            .await
1138            .expect("should execute");
1139        assert_eq!(
1140            result
1141                .get("echoed")
1142                .and_then(|value| value.get("flag"))
1143                .and_then(|value| value.as_bool()),
1144            Some(true)
1145        );
1146    }
1147
1148    #[tokio::test]
1149    async fn register_registry_fn_after_enabling_cgp_pipeline_wraps_new_tools() {
1150        let registry = ToolRegistry::new(PathBuf::from("/tmp/test")).await;
1151        registry.enable_cgp_pipeline(CgpRuntimeMode::Interactive).await;
1152
1153        registry
1154            .register_tool(ToolRegistration::new(
1155                "late_registry_fn_cgp_test",
1156                crate::config::types::CapabilityLevel::Basic,
1157                false,
1158                registry_fn_test_executor,
1159            ))
1160            .await
1161            .expect("should register");
1162
1163        let registration = registry
1164            .inventory
1165            .registrations_snapshot()
1166            .into_iter()
1167            .find(|registration| registration.name() == "late_registry_fn_cgp_test")
1168            .expect("registration should exist");
1169        assert!(registration.is_cgp_wrapped());
1170
1171        let result = registry
1172            .get_tool("late_registry_fn_cgp_test")
1173            .expect("tool exists")
1174            .execute(serde_json::json!({"late": true}))
1175            .await
1176            .expect("should execute");
1177        assert_eq!(
1178            result
1179                .get("echoed")
1180                .and_then(|value| value.get("late"))
1181                .and_then(|value| value.as_bool()),
1182            Some(true)
1183        );
1184    }
1185}