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::{
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_mode(&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_mode(&normalized_allowed_tools, &available);
87 }
88
89 pub async fn disable_full_auto_mode(&self) {
90 self.policy_gateway.lock().await.disable_full_auto_mode();
91 }
92
93 pub async fn set_enforce_safe_mode_prompts(&self, enabled: bool) {
94 self.policy_gateway
95 .lock()
96 .await
97 .set_enforce_safe_mode_prompts(enabled);
98 }
99
100 pub async fn current_full_auto_allowlist(&self) -> Option<Vec<String>> {
101 self.policy_gateway
102 .lock()
103 .await
104 .current_full_auto_allowlist()
105 }
106
107 pub async fn is_allowed_in_full_auto(&self, tool_name: &str) -> bool {
108 self.policy_gateway
109 .lock()
110 .await
111 .is_allowed_in_full_auto(&self.resolve_runtime_policy_name(tool_name))
112 }
113
114 pub async fn set_policy_manager(&self, manager: ToolPolicyManager) {
115 self.policy_gateway.lock().await.set_policy_manager(manager);
116 self.sync_policy_catalog().await;
117 }
118
119 pub async fn set_tool_policy(&self, tool_name: &str, policy: ToolPolicy) -> Result<()> {
120 let normalized_name = self.resolve_runtime_policy_name(tool_name);
121 self.policy_gateway
122 .lock()
123 .await
124 .set_tool_policy(&normalized_name, policy)
125 .await
126 }
127
128 pub async fn persist_approval_cache_key(&self, approval_key: &str) -> Result<()> {
129 self.policy_gateway
130 .lock()
131 .await
132 .add_approval_cache_key(approval_key)
133 .await
134 }
135
136 pub async fn persist_approval_cache_prefix(&self, prefix_entry: &str) -> Result<()> {
137 self.policy_gateway
138 .lock()
139 .await
140 .add_approval_cache_prefix(prefix_entry)
141 .await
142 }
143
144 pub async fn has_persisted_approval(&self, approval_key: &str) -> bool {
145 self.policy_gateway
146 .lock()
147 .await
148 .has_approval_cache_key(approval_key)
149 }
150
151 pub async fn find_persisted_shell_approval_prefix(
152 &self,
153 command_words: &[String],
154 scope_signature: &str,
155 ) -> Option<String> {
156 self.policy_gateway
157 .lock()
158 .await
159 .matching_shell_approval_prefix(command_words, scope_signature)
160 }
161
162 pub async fn get_tool_policy(&self, tool_name: &str) -> ToolPolicy {
163 self.policy_gateway
164 .lock()
165 .await
166 .get_tool_policy(&self.resolve_runtime_policy_name(tool_name))
167 }
168
169 pub async fn reset_tool_policies(&self) -> Result<()> {
170 self.policy_gateway.lock().await.reset_tool_policies().await
171 }
172
173 pub async fn allow_all_tools(&self) -> Result<()> {
174 self.policy_gateway.lock().await.allow_all_tools().await
175 }
176
177 pub async fn deny_all_tools(&self) -> Result<()> {
178 self.policy_gateway.lock().await.deny_all_tools().await
179 }
180
181 pub async fn print_policy_status(&self) {
182 self.policy_gateway.lock().await.print_policy_status();
183 }
184
185 pub async fn apply_config_policies(&self, tools_config: &ToolsConfig) -> Result<()> {
186 let normalized_tools_config = self.normalize_tools_config_policies(tools_config);
187 let mut policy_gateway = self.policy_gateway.lock().await;
188 if let Ok(policy_manager) = policy_gateway.policy_manager_mut() {
189 policy_manager
190 .apply_tools_config(&normalized_tools_config)
191 .await?;
192 }
193
194 let detect_window = super::DEFAULT_LOOP_DETECT_WINDOW
195 .max(
196 normalized_tools_config
197 .max_repeated_tool_calls
198 .saturating_mul(2),
199 )
200 .max(1);
201 self.execution_history.set_loop_detection_limits(
202 detect_window,
203 normalized_tools_config.max_repeated_tool_calls,
204 );
205 self.execution_history.set_rate_limit_per_minute(
206 crate::tools::rate_limit_config::tool_calls_per_minute_from_env(),
207 );
208
209 Ok(())
210 }
211
212 pub async fn preflight_tool_permission(&self, name: &str) -> Result<bool> {
214 match self.evaluate_tool_policy(name).await? {
215 ToolPermissionDecision::Allow => Ok(true),
216 ToolPermissionDecision::Deny => Ok(false),
217 ToolPermissionDecision::Prompt => Ok(true),
218 }
219 }
220
221 pub async fn evaluate_tool_policy(&self, name: &str) -> Result<ToolPermissionDecision> {
222 if let Some(tool_name) = legacy_mcp_tool_name(name) {
223 return self.evaluate_mcp_tool_policy(name, tool_name).await;
224 }
225
226 if let Some((_, tool_name)) = parse_canonical_mcp_tool_name(name) {
227 return self.evaluate_mcp_tool_policy(name, tool_name).await;
228 }
229
230 let resolved_name = self.resolve_runtime_policy_name(name);
231 let resolved_public_tool = self.resolve_public_tool(name).ok();
232
233 if let Some(resolution) = &resolved_public_tool
234 && let Some((_, tool_name)) =
235 parse_canonical_mcp_tool_name(resolution.registration_name())
236 {
237 return self
238 .evaluate_mcp_tool_policy(resolution.registration_name(), tool_name)
239 .await;
240 }
241
242 let (default_permission, safe_mode_prompt) = self
243 .inventory
244 .get_registration(&resolved_name)
245 .map(|registration| {
246 (
247 registration
248 .metadata()
249 .default_permission()
250 .unwrap_or(ToolPolicy::Prompt),
251 registration
252 .metadata()
253 .behavior()
254 .map(|behavior| behavior.safe_mode_prompt)
255 .unwrap_or(false),
256 )
257 })
258 .or_else(|| {
259 resolved_public_tool
260 .as_ref()
261 .map(|resolution| (resolution.default_permission().clone(), false))
262 })
263 .unwrap_or((ToolPolicy::Prompt, false));
264
265 {
266 let gateway = self.policy_gateway.lock().await;
267 if !gateway.has_policy_manager() {
268 return Ok(match default_permission {
269 ToolPolicy::Allow => ToolPermissionDecision::Allow,
270 ToolPolicy::Deny => ToolPermissionDecision::Deny,
271 ToolPolicy::Prompt => ToolPermissionDecision::Prompt,
272 });
273 }
274 }
275
276 self.policy_gateway
277 .lock()
278 .await
279 .evaluate_tool_policy(&resolved_name, safe_mode_prompt, default_permission)
280 .await
281 }
282
283 async fn evaluate_mcp_tool_policy(
284 &self,
285 full_name: &str,
286 tool_name: &str,
287 ) -> Result<ToolPermissionDecision> {
288 let provider = match self.find_mcp_provider(tool_name).await {
289 Some(provider) => provider,
290 None => {
291 return Ok(ToolPermissionDecision::Prompt);
293 }
294 };
295
296 {
297 let gateway = self.policy_gateway.lock().await;
298 if gateway.has_full_auto_allowlist() && !gateway.is_allowed_in_full_auto(full_name) {
300 return Ok(ToolPermissionDecision::Deny);
301 }
302 }
303
304 let mut gateway = self.policy_gateway.lock().await;
305 if let Ok(policy_manager) = gateway.policy_manager_mut() {
306 match policy_manager.get_mcp_tool_policy(&provider, tool_name) {
307 ToolPolicy::Allow => {
308 gateway.preapprove(full_name);
309 Ok(ToolPermissionDecision::Allow)
310 }
311 ToolPolicy::Deny => Ok(ToolPermissionDecision::Deny),
312 ToolPolicy::Prompt => Ok(ToolPermissionDecision::Prompt),
313 }
314 } else {
315 Ok(ToolPermissionDecision::Prompt)
316 }
317 }
318
319 pub async fn mark_tool_preapproved(&self, name: &str) {
322 let normalized_name = self.resolve_runtime_policy_name(name);
323 let mut gateway = self.policy_gateway.lock().await;
324 gateway.preapprove(&normalized_name);
325 tracing::trace!(tool = %normalized_name, "Preapproved tool after explicit approval");
326 }
327
328 pub async fn persist_mcp_tool_policy(&self, name: &str, policy: ToolPolicy) -> Result<()> {
329 let (provider, tool_name) = if is_legacy_mcp_tool_name(name) {
330 let Some(tool_name) = legacy_mcp_tool_name(name) else {
331 return Ok(());
332 };
333 let Some(provider) = self.find_mcp_provider(tool_name).await else {
334 return Ok(());
335 };
336 (provider, tool_name.to_string())
337 } else if let Some((provider, tool_name)) = parse_canonical_mcp_tool_name(name) {
338 (provider.to_string(), tool_name.to_string())
339 } else if let Ok(resolution) = self.resolve_public_tool(name) {
340 let Some((provider, tool_name)) =
341 parse_canonical_mcp_tool_name(resolution.registration_name())
342 else {
343 return Ok(());
344 };
345 (provider.to_string(), tool_name.to_string())
346 } else {
347 return Ok(());
348 };
349
350 self.policy_gateway
351 .lock()
352 .await
353 .persist_mcp_tool_policy(&provider, &tool_name, policy)
354 .await
355 }
356}