Skip to main content

vtcode_core/tools/registry/
policy_facade.rs

1//! Tool policy evaluation helpers attached to ToolRegistry.
2
3use anyhow::Result;
4use hashbrown::HashSet;
5use indexmap::IndexMap;
6
7use super::{ToolPermissionDecision, ToolRegistry};
8use crate::config::ToolsConfig;
9use crate::tool_policy::{ToolPolicy, ToolPolicyManager};
10use crate::tools::mcp::{is_legacy_mcp_tool_name, legacy_mcp_tool_name, parse_canonical_mcp_tool_name};
11use crate::tools::names::canonical_tool_name;
12
13fn more_restrictive_policy(left: ToolPolicy, right: ToolPolicy) -> ToolPolicy {
14    match (left, right) {
15        (ToolPolicy::Deny, _) | (_, ToolPolicy::Deny) => ToolPolicy::Deny,
16        (ToolPolicy::Prompt, _) | (_, ToolPolicy::Prompt) => ToolPolicy::Prompt,
17        _ => ToolPolicy::Allow,
18    }
19}
20
21impl ToolRegistry {
22    pub(super) async fn visible_policy_names(
23        &self,
24        session_tools_config: crate::tools::handlers::SessionToolsConfig,
25    ) -> Vec<String> {
26        self.model_tools(session_tools_config)
27            .await
28            .iter()
29            .map(|tool| self.resolve_runtime_policy_name(tool.function_name()))
30            .collect()
31    }
32
33    fn resolve_runtime_policy_name(&self, name: &str) -> String {
34        if is_legacy_mcp_tool_name(name) || parse_canonical_mcp_tool_name(name).is_some() {
35            return name.to_string();
36        }
37
38        if let Ok(resolved) = self.resolve_public_tool(name) {
39            return resolved.registration_name().to_string();
40        }
41
42        canonical_tool_name(name).to_owned()
43    }
44
45    fn normalize_tools_config_policies(&self, tools_config: &ToolsConfig) -> ToolsConfig {
46        let mut normalized = tools_config.clone();
47        let mut explicit_canonical_names: HashSet<String> = HashSet::default();
48
49        for name in tools_config.policies.keys() {
50            let canonical = self.resolve_runtime_policy_name(name);
51            if canonical == *name {
52                explicit_canonical_names.insert(canonical);
53            }
54        }
55
56        let mut policies = IndexMap::new();
57        for (name, policy) in &tools_config.policies {
58            let canonical = self.resolve_runtime_policy_name(name);
59            if canonical != *name && explicit_canonical_names.contains(&canonical) {
60                continue;
61            }
62            let merged = policies
63                .get(&canonical)
64                .cloned()
65                .map(|existing| more_restrictive_policy(existing, policy.clone()))
66                .unwrap_or(policy.clone());
67            policies.insert(canonical, merged);
68        }
69
70        normalized.policies = policies;
71        normalized
72    }
73
74    pub async fn enable_full_auto_permission(&self, allowed_tools: &[String]) {
75        self.enable_full_auto_permission_for_session(
76            allowed_tools,
77            crate::tools::handlers::SessionToolsConfig::full_public(
78                crate::tools::handlers::SessionSurface::Interactive,
79                crate::config::types::CapabilityLevel::CodeSearch,
80                crate::config::ToolDocumentationMode::Full,
81                crate::tools::handlers::ToolModelCapabilities::default(),
82            ),
83        )
84        .await;
85    }
86
87    /// Enable full-auto mode against the tools visible in a specific session.
88    pub async fn enable_full_auto_permission_for_session(
89        &self,
90        allowed_tools: &[String],
91        session_tools_config: crate::tools::handlers::SessionToolsConfig,
92    ) {
93        #[cfg(test)]
94        let test_hooks = {
95            let policy_gateway = self.policy_gateway.clone();
96            policy_gateway.full_auto_catalogue_test_hooks()
97        };
98        #[cfg(test)]
99        test_hooks.pause_before_enable_lifecycle().await;
100        let lifecycle = {
101            let policy_gateway = self.policy_gateway.clone();
102            policy_gateway.full_auto_catalogue_lifecycle()
103        };
104        let _lifecycle_guard = lifecycle.lock().await;
105        let normalized_allowed_tools: Vec<String> = allowed_tools
106            .iter()
107            .map(|tool| self.resolve_runtime_policy_name(tool))
108            .collect();
109        let visible_policy_names = self.visible_policy_names(session_tools_config.clone()).await;
110        #[cfg(test)]
111        test_hooks.pause_after_enable_snapshot().await;
112        self.policy_gateway
113            .enable_full_auto_permission(&normalized_allowed_tools, &visible_policy_names, session_tools_config)
114            .await;
115    }
116
117    pub async fn disable_full_auto_permission(&self) {
118        #[cfg(test)]
119        let test_hooks = {
120            let policy_gateway = self.policy_gateway.clone();
121            policy_gateway.full_auto_catalogue_test_hooks()
122        };
123        #[cfg(test)]
124        test_hooks.pause_before_disable_lifecycle().await;
125        let lifecycle = {
126            let policy_gateway = self.policy_gateway.clone();
127            policy_gateway.full_auto_catalogue_lifecycle()
128        };
129        let _lifecycle_guard = lifecycle.lock().await;
130        self.policy_gateway.disable_full_auto_permission().await;
131    }
132
133    pub async fn set_enforce_safe_mode_prompts(&self, enabled: bool) {
134        self.policy_gateway.set_enforce_safe_mode_prompts(enabled);
135    }
136
137    pub async fn current_full_auto_allowlist(&self) -> Option<Vec<String>> {
138        self.policy_gateway.current_full_auto_allowlist().await
139    }
140
141    pub async fn is_allowed_in_full_auto(&self, tool_name: &str) -> bool {
142        self.policy_gateway
143            .is_allowed_in_full_auto(&self.resolve_runtime_policy_name(tool_name))
144            .await
145    }
146
147    pub async fn is_denied_in_full_auto(&self, tool_name: &str) -> bool {
148        self.policy_gateway
149            .is_denied_in_full_auto(&self.resolve_runtime_policy_name(tool_name))
150            .await
151    }
152
153    pub async fn set_policy_manager(&self, manager: ToolPolicyManager) {
154        self.policy_gateway.set_policy_manager(manager).await;
155        self.sync_policy_catalog().await;
156    }
157
158    /// Whether the registry owns a workspace policy manager.
159    ///
160    /// First-paint registries skip policy file I/O; hydration attaches the
161    /// manager via [`Self::ensure_workspace_policy_manager`] before any tool runs.
162    pub async fn has_policy_manager(&self) -> bool {
163        self.policy_gateway.has_policy_manager().await
164    }
165
166    /// Attach the workspace policy manager unless one is already present.
167    ///
168    /// Reads (and may create) the workspace policy file, so it belongs in
169    /// hydration, not on the paint path. Fail-open: without a manager,
170    /// evaluation falls back to tool metadata defaults.
171    pub async fn ensure_workspace_policy_manager(&self, workspace_root: &std::path::Path) {
172        if self.policy_gateway.has_policy_manager().await {
173            return;
174        }
175        match ToolPolicyManager::new_with_workspace(workspace_root).await {
176            Ok(manager) => self.set_policy_manager(manager).await,
177            Err(err) => {
178                tracing::warn!(%err, "Failed to initialize tool policy manager during hydration");
179            }
180        }
181    }
182
183    pub async fn set_tool_policy(&self, tool_name: &str, policy: ToolPolicy) -> Result<()> {
184        let normalized_name = self.resolve_runtime_policy_name(tool_name);
185        self.policy_gateway.set_tool_policy(&normalized_name, policy).await
186    }
187
188    pub async fn persist_approval_cache_key(&self, approval_key: &str) -> Result<()> {
189        self.policy_gateway.add_approval_cache_key(approval_key).await
190    }
191
192    pub async fn persist_approval_cache_prefix(&self, prefix_entry: &str) -> Result<()> {
193        self.policy_gateway.add_approval_cache_prefix(prefix_entry).await
194    }
195
196    pub async fn has_persisted_approval(&self, approval_key: &str) -> bool {
197        self.policy_gateway.has_approval_cache_key(approval_key).await
198    }
199
200    pub async fn find_persisted_shell_approval_prefix(
201        &self,
202        command_words: &[String],
203        scope_signature: &str,
204    ) -> Option<String> {
205        self.policy_gateway
206            .matching_shell_approval_prefix(command_words, scope_signature)
207            .await
208    }
209
210    pub async fn get_tool_policy(&self, tool_name: &str) -> ToolPolicy {
211        self.policy_gateway
212            .get_tool_policy(&self.resolve_runtime_policy_name(tool_name))
213            .await
214    }
215
216    pub async fn reset_tool_policies(&self) -> Result<()> {
217        let manager = self.policy_gateway.policy_manager().await;
218        let mut manager = manager.ok_or_else(|| anyhow::anyhow!("Tool policy manager not available"))?;
219        manager.reset_all_to_prompt().await?;
220        self.policy_gateway.set_policy_manager(manager).await;
221        Ok(())
222    }
223
224    pub async fn allow_all_tools(&self) -> Result<()> {
225        let manager = self.policy_gateway.policy_manager().await;
226        let mut manager = manager.ok_or_else(|| anyhow::anyhow!("Tool policy manager not available"))?;
227        let all_tools: Vec<String> = self
228            .inventory
229            .registrations_snapshot()
230            .into_iter()
231            .map(|registration| registration.name().to_string())
232            .collect();
233        manager.allow_all_tools_for_tools(&all_tools).await?;
234        self.policy_gateway.set_policy_manager(manager).await;
235        Ok(())
236    }
237
238    pub async fn deny_all_tools(&self) -> Result<()> {
239        let manager = self.policy_gateway.policy_manager().await;
240        let mut manager = manager.ok_or_else(|| anyhow::anyhow!("Tool policy manager not available"))?;
241        manager.deny_all_tools().await?;
242        self.policy_gateway.set_policy_manager(manager).await;
243        Ok(())
244    }
245
246    pub async fn print_policy_status(&self) {
247        self.policy_gateway.print_policy_status().await;
248    }
249
250    pub async fn apply_config_policies(&self, tools_config: &ToolsConfig) -> Result<()> {
251        let normalized_tools_config = self.normalize_tools_config_policies(tools_config);
252        {
253            let mut active_tool_profile = self
254                .active_tool_profile
255                .write()
256                .unwrap_or_else(std::sync::PoisonError::into_inner);
257            *active_tool_profile = normalized_tools_config.profile;
258        }
259        *self.cached_available_tools.write() = None;
260        self.sync_policy_catalog().await;
261
262        let manager = {
263            let guard = self.policy_gateway.clone();
264            guard.policy_manager().await
265        };
266        let mut manager = manager.ok_or_else(|| anyhow::anyhow!("Tool policy manager not available"))?;
267        manager.apply_tools_config(&normalized_tools_config).await?;
268        self.policy_gateway.set_policy_manager(manager).await;
269
270        let detect_window = super::DEFAULT_LOOP_DETECT_WINDOW
271            .max(normalized_tools_config.max_repeated_tool_calls.saturating_mul(2))
272            .max(1);
273        self.execution_history
274            .set_loop_detection_limits(detect_window, normalized_tools_config.max_repeated_tool_calls);
275        self.execution_history
276            .set_rate_limit_per_minute(crate::tools::rate_limit_config::tool_calls_per_minute_from_env());
277
278        Ok(())
279    }
280
281    /// Prompt for permission before starting long-running tool executions to avoid spinner conflicts
282    pub async fn preflight_tool_permission(&self, name: &str) -> Result<bool> {
283        match self.evaluate_tool_policy(name).await? {
284            ToolPermissionDecision::Allow => Ok(true),
285            ToolPermissionDecision::Deny => Ok(false),
286            ToolPermissionDecision::Prompt => Ok(true),
287        }
288    }
289
290    pub async fn evaluate_tool_policy(&self, name: &str) -> Result<ToolPermissionDecision> {
291        if let Some(tool_name) = legacy_mcp_tool_name(name) {
292            return self.evaluate_mcp_tool_policy(name, tool_name).await;
293        }
294
295        if let Some((_, tool_name)) = parse_canonical_mcp_tool_name(name) {
296            return self.evaluate_mcp_tool_policy(name, tool_name).await;
297        }
298
299        let resolved_name = self.resolve_runtime_policy_name(name);
300        let resolved_public_tool = self.resolve_public_tool(name).ok();
301
302        if let Some(resolution) = &resolved_public_tool
303            && let Some((_, tool_name)) = parse_canonical_mcp_tool_name(resolution.registration_name())
304        {
305            return self.evaluate_mcp_tool_policy(resolution.registration_name(), tool_name).await;
306        }
307
308        let (default_permission, safe_mode_prompt) = self
309            .inventory
310            .get_registration(&resolved_name)
311            .map(|registration| {
312                (
313                    registration.metadata().default_permission().unwrap_or(ToolPolicy::Prompt),
314                    registration
315                        .metadata()
316                        .behavior()
317                        .map(|behavior| behavior.safe_mode_prompt)
318                        .unwrap_or(false),
319                )
320            })
321            .or_else(|| {
322                resolved_public_tool
323                    .as_ref()
324                    .map(|resolution| (resolution.default_permission().clone(), false))
325            })
326            .unwrap_or((ToolPolicy::Prompt, false));
327
328        let has_policy_manager = {
329            let gateway = self.policy_gateway.clone();
330            gateway.has_policy_manager().await
331        };
332
333        if !has_policy_manager {
334            return Ok(match default_permission {
335                ToolPolicy::Allow => ToolPermissionDecision::Allow,
336                ToolPolicy::Deny => ToolPermissionDecision::Deny,
337                ToolPolicy::Prompt => ToolPermissionDecision::Prompt,
338            });
339        }
340
341        self.policy_gateway
342            .evaluate_tool_policy(&resolved_name, safe_mode_prompt, default_permission)
343            .await
344    }
345
346    async fn evaluate_mcp_tool_policy(&self, full_name: &str, tool_name: &str) -> Result<ToolPermissionDecision> {
347        let provider = match self.find_mcp_provider(tool_name).await {
348            Some(provider) => provider,
349            None => {
350                // Unknown provider for this tool; default to prompt for safety
351                return Ok(ToolPermissionDecision::Prompt);
352            }
353        };
354
355        {
356            let gateway = self.policy_gateway.clone();
357            if gateway.is_denied_in_full_auto(full_name).await {
358                return Ok(ToolPermissionDecision::Deny);
359            }
360        }
361
362        let mcp_decision = {
363            let gateway = self.policy_gateway.clone();
364            let policy_arc = gateway.tool_policy_arc();
365            drop(gateway);
366            let policy_guard = policy_arc.lock().await;
367            policy_guard
368                .as_ref()
369                .map(|m| m.get_mcp_tool_policy(&provider, tool_name))
370                .unwrap_or(ToolPolicy::Prompt)
371        };
372        match mcp_decision {
373            ToolPolicy::Allow => {
374                self.policy_gateway.preapprove(full_name).await;
375                Ok(ToolPermissionDecision::Allow)
376            }
377            ToolPolicy::Deny => Ok(ToolPermissionDecision::Deny),
378            ToolPolicy::Prompt => Ok(ToolPermissionDecision::Prompt),
379        }
380    }
381
382    /// Mark a tool as pre-approved for a single execution after the permission
383    /// flow already granted it.
384    pub async fn mark_tool_preapproved(&self, name: &str) {
385        let normalized_name = self.resolve_runtime_policy_name(name);
386        self.policy_gateway.preapprove(&normalized_name).await;
387        tracing::trace!(tool = %normalized_name, "Preapproved tool after explicit approval");
388    }
389
390    pub async fn persist_mcp_tool_policy(&self, name: &str, policy: ToolPolicy) -> Result<()> {
391        let (provider, tool_name) = if is_legacy_mcp_tool_name(name) {
392            let Some(tool_name) = legacy_mcp_tool_name(name) else {
393                return Ok(());
394            };
395            let Some(provider) = self.find_mcp_provider(tool_name).await else {
396                return Ok(());
397            };
398            (provider, tool_name.to_string())
399        } else if let Some((provider, tool_name)) = parse_canonical_mcp_tool_name(name) {
400            (provider.to_string(), tool_name.to_string())
401        } else if let Ok(resolution) = self.resolve_public_tool(name) {
402            let Some((provider, tool_name)) = parse_canonical_mcp_tool_name(resolution.registration_name()) else {
403                return Ok(());
404            };
405            (provider.to_string(), tool_name.to_string())
406        } else {
407            return Ok(());
408        };
409
410        self.policy_gateway.persist_mcp_tool_policy(&provider, &tool_name, policy).await
411    }
412}