vtcode_core/tools/registry/
policy_facade.rs1use 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 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 pub async fn has_policy_manager(&self) -> bool {
163 self.policy_gateway.has_policy_manager().await
164 }
165
166 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 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 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 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}