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_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 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 return Ok(ToolPermissionDecision::Prompt);
296 }
297 };
298
299 {
300 let gateway = self.policy_gateway.lock().await;
301 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 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}