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::{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 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
398fn 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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
444pub enum CgpRuntimeMode {
445 Interactive,
447 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 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 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 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 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(®istration);
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, ®istration);
742 let backed_dyn = RegistrationBackedDynTool::from_registration(Arc::new(MetaTool), ®istration);
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, ®istration).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 ®istration,
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}