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::{
11    is_legacy_mcp_tool_name, legacy_mcp_tool_name, parse_canonical_mcp_tool_name,
12};
13use crate::tools::names::canonical_tool_name;
14
15fn more_restrictive_policy(
16    left: vtcode_config::ToolPolicy,
17    right: vtcode_config::ToolPolicy,
18) -> vtcode_config::ToolPolicy {
19    match (left, right) {
20        (vtcode_config::ToolPolicy::Deny, _) | (_, vtcode_config::ToolPolicy::Deny) => {
21            vtcode_config::ToolPolicy::Deny
22        }
23        (vtcode_config::ToolPolicy::Prompt, _) | (_, vtcode_config::ToolPolicy::Prompt) => {
24            vtcode_config::ToolPolicy::Prompt
25        }
26        _ => vtcode_config::ToolPolicy::Allow,
27    }
28}
29
30impl ToolRegistry {
31    fn resolve_runtime_policy_name(&self, name: &str) -> String {
32        if is_legacy_mcp_tool_name(name) || parse_canonical_mcp_tool_name(name).is_some() {
33            return name.to_string();
34        }
35
36        if let Ok(resolved) = self.resolve_public_tool(name) {
37            return resolved.registration_name().to_string();
38        }
39
40        match name {
41            "list_dir" | "list_directory" => {
42                crate::config::constants::tools::UNIFIED_SEARCH.to_string()
43            }
44            _ => canonical_tool_name(name).to_owned(),
45        }
46    }
47
48    fn normalize_tools_config_policies(&self, tools_config: &ToolsConfig) -> ToolsConfig {
49        let mut normalized = tools_config.clone();
50        let mut explicit_canonical_names: HashSet<String> = HashSet::default();
51
52        for name in tools_config.policies.keys() {
53            let canonical = self.resolve_runtime_policy_name(name);
54            if canonical == *name {
55                explicit_canonical_names.insert(canonical);
56            }
57        }
58
59        let mut policies = IndexMap::new();
60        for (name, policy) in &tools_config.policies {
61            let canonical = self.resolve_runtime_policy_name(name);
62            if canonical != *name && explicit_canonical_names.contains(&canonical) {
63                continue;
64            }
65            let merged = policies
66                .get(&canonical)
67                .cloned()
68                .map(|existing| more_restrictive_policy(existing, *policy))
69                .unwrap_or(*policy);
70            policies.insert(canonical, merged);
71        }
72
73        normalized.policies = policies;
74        normalized
75    }
76
77    pub async fn enable_full_auto_permission(&self, allowed_tools: &[String]) {
78        let normalized_allowed_tools: Vec<String> = allowed_tools
79            .iter()
80            .map(|tool| self.resolve_runtime_policy_name(tool))
81            .collect();
82        let available = self.available_tools().await;
83        self.policy_gateway
84            .lock()
85            .await
86            .enable_full_auto_permission(&normalized_allowed_tools, &available);
87    }
88
89    pub async fn disable_full_auto_permission(&self) {
90        self.policy_gateway
91            .lock()
92            .await
93            .disable_full_auto_permission();
94    }
95
96    pub async fn set_enforce_safe_mode_prompts(&self, enabled: bool) {
97        self.policy_gateway
98            .lock()
99            .await
100            .set_enforce_safe_mode_prompts(enabled);
101    }
102
103    pub async fn current_full_auto_allowlist(&self) -> Option<Vec<String>> {
104        self.policy_gateway
105            .lock()
106            .await
107            .current_full_auto_allowlist()
108    }
109
110    pub async fn is_allowed_in_full_auto(&self, tool_name: &str) -> bool {
111        self.policy_gateway
112            .lock()
113            .await
114            .is_allowed_in_full_auto(&self.resolve_runtime_policy_name(tool_name))
115    }
116
117    pub async fn set_policy_manager(&self, manager: ToolPolicyManager) {
118        self.policy_gateway.lock().await.set_policy_manager(manager);
119        self.sync_policy_catalog().await;
120    }
121
122    pub async fn set_tool_policy(&self, tool_name: &str, policy: ToolPolicy) -> Result<()> {
123        let normalized_name = self.resolve_runtime_policy_name(tool_name);
124        self.policy_gateway
125            .lock()
126            .await
127            .set_tool_policy(&normalized_name, policy)
128            .await
129    }
130
131    pub async fn persist_approval_cache_key(&self, approval_key: &str) -> Result<()> {
132        self.policy_gateway
133            .lock()
134            .await
135            .add_approval_cache_key(approval_key)
136            .await
137    }
138
139    pub async fn persist_approval_cache_prefix(&self, prefix_entry: &str) -> Result<()> {
140        self.policy_gateway
141            .lock()
142            .await
143            .add_approval_cache_prefix(prefix_entry)
144            .await
145    }
146
147    pub async fn has_persisted_approval(&self, approval_key: &str) -> bool {
148        self.policy_gateway
149            .lock()
150            .await
151            .has_approval_cache_key(approval_key)
152    }
153
154    pub async fn find_persisted_shell_approval_prefix(
155        &self,
156        command_words: &[String],
157        scope_signature: &str,
158    ) -> Option<String> {
159        self.policy_gateway
160            .lock()
161            .await
162            .matching_shell_approval_prefix(command_words, scope_signature)
163    }
164
165    pub async fn get_tool_policy(&self, tool_name: &str) -> ToolPolicy {
166        self.policy_gateway
167            .lock()
168            .await
169            .get_tool_policy(&self.resolve_runtime_policy_name(tool_name))
170    }
171
172    pub async fn reset_tool_policies(&self) -> Result<()> {
173        self.policy_gateway.lock().await.reset_tool_policies().await
174    }
175
176    pub async fn allow_all_tools(&self) -> Result<()> {
177        self.policy_gateway.lock().await.allow_all_tools().await
178    }
179
180    pub async fn deny_all_tools(&self) -> Result<()> {
181        self.policy_gateway.lock().await.deny_all_tools().await
182    }
183
184    pub async fn print_policy_status(&self) {
185        self.policy_gateway.lock().await.print_policy_status();
186    }
187
188    pub async fn apply_config_policies(&self, tools_config: &ToolsConfig) -> Result<()> {
189        let normalized_tools_config = self.normalize_tools_config_policies(tools_config);
190        let mut policy_gateway = self.policy_gateway.lock().await;
191        if let Ok(policy_manager) = policy_gateway.policy_manager_mut() {
192            policy_manager
193                .apply_tools_config(&normalized_tools_config)
194                .await?;
195        }
196
197        let detect_window = super::DEFAULT_LOOP_DETECT_WINDOW
198            .max(
199                normalized_tools_config
200                    .max_repeated_tool_calls
201                    .saturating_mul(2),
202            )
203            .max(1);
204        self.execution_history.set_loop_detection_limits(
205            detect_window,
206            normalized_tools_config.max_repeated_tool_calls,
207        );
208        self.execution_history.set_rate_limit_per_minute(
209            crate::tools::rate_limit_config::tool_calls_per_minute_from_env(),
210        );
211
212        Ok(())
213    }
214
215    /// Prompt for permission before starting long-running tool executions to avoid spinner conflicts
216    pub async fn preflight_tool_permission(&self, name: &str) -> Result<bool> {
217        match self.evaluate_tool_policy(name).await? {
218            ToolPermissionDecision::Allow => Ok(true),
219            ToolPermissionDecision::Deny => Ok(false),
220            ToolPermissionDecision::Prompt => Ok(true),
221        }
222    }
223
224    pub async fn evaluate_tool_policy(&self, name: &str) -> Result<ToolPermissionDecision> {
225        if let Some(tool_name) = legacy_mcp_tool_name(name) {
226            return self.evaluate_mcp_tool_policy(name, tool_name).await;
227        }
228
229        if let Some((_, tool_name)) = parse_canonical_mcp_tool_name(name) {
230            return self.evaluate_mcp_tool_policy(name, tool_name).await;
231        }
232
233        let resolved_name = self.resolve_runtime_policy_name(name);
234        let resolved_public_tool = self.resolve_public_tool(name).ok();
235
236        if let Some(resolution) = &resolved_public_tool
237            && let Some((_, tool_name)) =
238                parse_canonical_mcp_tool_name(resolution.registration_name())
239        {
240            return self
241                .evaluate_mcp_tool_policy(resolution.registration_name(), tool_name)
242                .await;
243        }
244
245        let (default_permission, safe_mode_prompt) = self
246            .inventory
247            .get_registration(&resolved_name)
248            .map(|registration| {
249                (
250                    registration
251                        .metadata()
252                        .default_permission()
253                        .unwrap_or(ToolPolicy::Prompt),
254                    registration
255                        .metadata()
256                        .behavior()
257                        .map(|behavior| behavior.safe_mode_prompt)
258                        .unwrap_or(false),
259                )
260            })
261            .or_else(|| {
262                resolved_public_tool
263                    .as_ref()
264                    .map(|resolution| (resolution.default_permission().clone(), false))
265            })
266            .unwrap_or((ToolPolicy::Prompt, false));
267
268        {
269            let gateway = self.policy_gateway.lock().await;
270            if !gateway.has_policy_manager() {
271                return Ok(match default_permission {
272                    ToolPolicy::Allow => ToolPermissionDecision::Allow,
273                    ToolPolicy::Deny => ToolPermissionDecision::Deny,
274                    ToolPolicy::Prompt => ToolPermissionDecision::Prompt,
275                });
276            }
277        }
278
279        self.policy_gateway
280            .lock()
281            .await
282            .evaluate_tool_policy(&resolved_name, safe_mode_prompt, default_permission)
283            .await
284    }
285
286    async fn evaluate_mcp_tool_policy(
287        &self,
288        full_name: &str,
289        tool_name: &str,
290    ) -> Result<ToolPermissionDecision> {
291        let provider = match self.find_mcp_provider(tool_name).await {
292            Some(provider) => provider,
293            None => {
294                // Unknown provider for this tool; default to prompt for safety
295                return Ok(ToolPermissionDecision::Prompt);
296            }
297        };
298
299        {
300            let gateway = self.policy_gateway.lock().await;
301            // Check full-auto allowlist first (aligned with policy_gateway behavior)
302            if gateway.has_full_auto_allowlist() && !gateway.is_allowed_in_full_auto(full_name) {
303                return Ok(ToolPermissionDecision::Deny);
304            }
305        }
306
307        let mut gateway = self.policy_gateway.lock().await;
308        if let Ok(policy_manager) = gateway.policy_manager_mut() {
309            match policy_manager.get_mcp_tool_policy(&provider, tool_name) {
310                ToolPolicy::Allow => {
311                    gateway.preapprove(full_name);
312                    Ok(ToolPermissionDecision::Allow)
313                }
314                ToolPolicy::Deny => Ok(ToolPermissionDecision::Deny),
315                ToolPolicy::Prompt => Ok(ToolPermissionDecision::Prompt),
316            }
317        } else {
318            Ok(ToolPermissionDecision::Prompt)
319        }
320    }
321
322    /// Mark a tool as pre-approved for a single execution after the permission
323    /// flow already granted it.
324    pub async fn mark_tool_preapproved(&self, name: &str) {
325        let normalized_name = self.resolve_runtime_policy_name(name);
326        let mut gateway = self.policy_gateway.lock().await;
327        gateway.preapprove(&normalized_name);
328        tracing::trace!(tool = %normalized_name, "Preapproved tool after explicit approval");
329    }
330
331    pub async fn persist_mcp_tool_policy(&self, name: &str, policy: ToolPolicy) -> Result<()> {
332        let (provider, tool_name) = if is_legacy_mcp_tool_name(name) {
333            let Some(tool_name) = legacy_mcp_tool_name(name) else {
334                return Ok(());
335            };
336            let Some(provider) = self.find_mcp_provider(tool_name).await else {
337                return Ok(());
338            };
339            (provider, tool_name.to_string())
340        } else if let Some((provider, tool_name)) = parse_canonical_mcp_tool_name(name) {
341            (provider.to_string(), tool_name.to_string())
342        } else if let Ok(resolution) = self.resolve_public_tool(name) {
343            let Some((provider, tool_name)) =
344                parse_canonical_mcp_tool_name(resolution.registration_name())
345            else {
346                return Ok(());
347            };
348            (provider.to_string(), tool_name.to_string())
349        } else {
350            return Ok(());
351        };
352
353        self.policy_gateway
354            .lock()
355            .await
356            .persist_mcp_tool_policy(&provider, &tool_name, policy)
357            .await
358    }
359}