Skip to main content

vtcode_core/tools/
registry_adapters.rs

1//! Workspace-aware adapters that bridge `vtcode-core` with the
2//! `vtcode-commons` traits.
3//!
4//! These helpers allow downstream consumers to construct a `ToolRegistry`
5//! using their own path, telemetry, and error-reporting implementations
6//! without relying on VT Code's built-in `.vtcode` directory layout.
7
8use std::path::PathBuf;
9
10use vtcode_commons::PathScope;
11
12#[cfg(feature = "policies")]
13use crate::config::PtyConfig;
14#[cfg(feature = "policies")]
15use crate::tool_policy::ToolPolicyManager;
16#[cfg(feature = "policies")]
17use crate::tools::registry::ToolRegistry;
18#[cfg(feature = "policies")]
19use anyhow::{Context, Error, Result};
20#[cfg(feature = "policies")]
21use vtcode_commons::{ErrorFormatter, ErrorReporter, PathResolver, TelemetrySink, WorkspacePaths};
22
23/// Telemetry events emitted by the registry builder when it resolves policy
24/// storage or encounters recoverable failures.
25#[derive(Debug, Clone, PartialEq, Eq)]
26pub enum RegistryEvent {
27    /// Records the resolved policy configuration file and the scope it belongs to.
28    PolicyPathResolved { scope: PathScope, config_path: PathBuf },
29    /// Raised when telemetry recording itself fails so callers can surface the
30    /// fallback behaviour.
31    TelemetryFailure { message: String },
32    /// Emitted when the builder reports a validation or initialization error.
33    AdapterError { message: String },
34}
35
36/// Builder that wires a [`ToolRegistry`] using [`WorkspacePaths`], telemetry,
37/// and error-reporting hooks supplied by the caller.
38#[cfg(feature = "policies")]
39pub struct RegistryBuilder<'a, Paths, Telemetry, Reporter, Formatter>
40where
41    Paths: WorkspacePaths + ?Sized,
42    Telemetry: TelemetrySink<RegistryEvent> + ?Sized,
43    Reporter: ErrorReporter + ?Sized,
44    Formatter: ErrorFormatter + ?Sized,
45{
46    workspace_paths: &'a Paths,
47    telemetry: &'a Telemetry,
48    error_reporter: &'a Reporter,
49    error_formatter: &'a Formatter,
50    policy_manager: Option<ToolPolicyManager>,
51    policy_path: Option<PathBuf>,
52    pty_config: PtyConfig,
53}
54
55#[cfg(feature = "policies")]
56impl<'a, Paths, Telemetry, Reporter, Formatter> RegistryBuilder<'a, Paths, Telemetry, Reporter, Formatter>
57where
58    Paths: WorkspacePaths + ?Sized,
59    Telemetry: TelemetrySink<RegistryEvent> + ?Sized,
60    Reporter: ErrorReporter + ?Sized,
61    Formatter: ErrorFormatter + ?Sized,
62{
63    /// Creates a new [`RegistryBuilder`] that records events via the provided
64    /// telemetry sink and reports errors using the supplied reporter.
65    pub fn new(
66        workspace_paths: &'a Paths,
67        telemetry: &'a Telemetry,
68        error_reporter: &'a Reporter,
69        error_formatter: &'a Formatter,
70    ) -> Self {
71        Self {
72            workspace_paths,
73            telemetry,
74            error_reporter,
75            error_formatter,
76            policy_manager: None,
77            policy_path: None,
78            pty_config: PtyConfig::default(),
79        }
80    }
81
82    /// Overrides the PTY configuration applied to the registry.
83    pub fn with_pty_config(mut self, config: PtyConfig) -> Self {
84        self.pty_config = config;
85        self
86    }
87
88    /// Supplies a pre-built [`ToolPolicyManager`], bypassing policy path resolution.
89    pub fn with_policy_manager(mut self, manager: ToolPolicyManager) -> Self {
90        self.policy_manager = Some(manager);
91        self
92    }
93
94    /// Overrides the policy configuration path resolved from [`WorkspacePaths`].
95    pub fn with_policy_path<P>(mut self, path: P) -> Self
96    where
97        P: Into<PathBuf>,
98    {
99        self.policy_path = Some(path.into());
100        self
101    }
102
103    /// Builds a [`ToolRegistry`] using the configured hooks.
104    pub async fn build(mut self) -> Result<ToolRegistry> {
105        let workspace_root = self.workspace_paths.workspace_root().to_path_buf();
106        let policy_manager = match self.policy_manager.take() {
107            Some(manager) => manager,
108            None => {
109                let config_path = self
110                    .policy_path
111                    .clone()
112                    .unwrap_or_else(|| self.workspace_paths.resolve_config("tool-policy.json"));
113                self.record_event(RegistryEvent::PolicyPathResolved {
114                    scope: self.workspace_paths.scope_for_path(&config_path),
115                    config_path: config_path.clone(),
116                });
117
118                match ToolPolicyManager::new_with_config_path(&config_path)
119                    .await
120                    .with_context(|| format!("failed to initialize tool policy manager at {}", config_path.display()))
121                {
122                    Ok(manager) => manager,
123                    Err(err) => {
124                        self.report_error(&err);
125                        return Err(err);
126                    }
127                }
128            }
129        };
130
131        Ok(ToolRegistry::new_with_custom_policy_and_config(workspace_root, self.pty_config, policy_manager).await)
132    }
133
134    fn record_event(&self, event: RegistryEvent) {
135        if let Err(err) = self.telemetry.record(&event) {
136            self.handle_error(err.context("failed to record registry adapter telemetry event"));
137        }
138    }
139
140    fn report_error(&self, error: &Error) {
141        let message = self.error_formatter.format_error(error).into_owned();
142        let _ = self.error_reporter.capture(error);
143        let _ = self.telemetry.record(&RegistryEvent::AdapterError { message });
144    }
145
146    fn handle_error(&self, error: Error) {
147        let message = self.error_formatter.format_error(&error).into_owned();
148        let _ = self.error_reporter.capture(&error);
149        let _ = self.telemetry.record(&RegistryEvent::TelemetryFailure { message });
150    }
151}
152
153#[cfg(all(test, feature = "policies"))]
154mod tests {
155    use super::*;
156    use assert_fs::TempDir;
157    use vtcode_commons::{DisplayErrorFormatter, MemoryErrorReporter, MemoryTelemetry, StaticWorkspacePaths};
158
159    #[tokio::test]
160    async fn builds_registry_with_workspace_paths() {
161        let temp = TempDir::new().expect("tempdir");
162        let workspace_root = temp.path().join("workspace");
163        let config_dir = temp.path().join("config");
164        std::fs::create_dir_all(&workspace_root).expect("workspace");
165        std::fs::create_dir_all(&config_dir).expect("config");
166
167        let paths = StaticWorkspacePaths::new(workspace_root.clone(), config_dir.clone());
168        let telemetry = MemoryTelemetry::new();
169        let reporter = MemoryErrorReporter::new();
170        let formatter = DisplayErrorFormatter;
171
172        let builder = RegistryBuilder::new(&paths, &telemetry, &reporter, &formatter);
173        let registry = builder.build().await.expect("registry");
174
175        assert!(registry.has_tool(crate::config::constants::tools::CODE_SEARCH).await);
176
177        let events = telemetry.take();
178        assert!(matches!(events.as_slice(), [RegistryEvent::PolicyPathResolved { scope: PathScope::Config, .. }]));
179
180        let policy_file = config_dir.join("tool-policy.json");
181        assert!(policy_file.exists(), "policy file should be created");
182        assert!(reporter.take().is_empty(), "no errors expected");
183    }
184}