vtcode_core/tools/registry/
plan_mode_checks.rs1use serde_json::Value;
4
5use super::ToolRegistry;
6
7impl ToolRegistry {
8 pub fn is_mutating_tool(&self, name: &str) -> bool {
12 let resolved_name = self
13 .resolve_public_tool_name_sync(name)
14 .ok()
15 .unwrap_or_else(|| name.to_string());
16
17 if let Some(reg) = self.inventory.get_registration(&resolved_name) {
18 if let Some(behavior) = reg.metadata().behavior() {
19 return !matches!(
20 behavior.mutation_model,
21 crate::tools::tool_intent::ToolMutationModel::ReadOnly
22 );
23 }
24
25 if let super::ToolHandler::TraitObject(tool) = reg.handler() {
26 return tool.is_mutating();
27 }
28 }
29
30 if let Some(reg) = self.inventory.get_registration(name)
31 && let super::ToolHandler::TraitObject(tool) = reg.handler()
32 {
33 return tool.is_mutating();
34 }
35
36 true
38 }
39
40 pub fn is_plan_mode_allowed(&self, tool_name: &str, args: &Value) -> bool {
45 use crate::config::constants::tools;
46 use crate::tools::names::canonical_tool_name;
47
48 let canonical = canonical_tool_name(tool_name);
50 match canonical {
51 tools::TASK_TRACKER => return true,
52 tools::PLAN_TASK_TRACKER => return true,
53 _ => {}
54 }
55
56 let intent = crate::tools::tool_intent::classify_tool_intent(tool_name, args);
57 if !intent.mutating {
58 return true;
59 }
60
61 let allowed_plan_write = self.is_plan_file_operation(tool_name, args);
62 let allowed_unified_readonly = intent.readonly_unified_action;
63
64 allowed_plan_write || allowed_unified_readonly
65 }
66
67 pub fn is_retry_safe_call(&self, tool_name: &str, args: &Value) -> bool {
72 crate::tools::tool_intent::classify_tool_intent(tool_name, args).retry_safe
73 }
74
75 pub(super) fn is_plan_file_operation(&self, tool_name: &str, args: &Value) -> bool {
78 use crate::config::constants::tools as tool_names;
79 use crate::tools::names::canonical_tool_name;
80
81 let canonical = canonical_tool_name(tool_name);
82 let normalized = canonical;
83
84 let file_writing_tools = [
86 tool_names::WRITE_FILE,
87 tool_names::UNIFIED_FILE,
88 tool_names::CREATE_FILE,
89 tool_names::EDIT_FILE,
90 tool_names::SEARCH_REPLACE,
91 ];
92
93 if !file_writing_tools.contains(&normalized) {
94 return false;
95 }
96
97 let path_str = args
99 .get("path")
100 .or_else(|| args.get("file_path"))
101 .or_else(|| args.get("filePath"))
102 .or_else(|| args.get("destination"))
103 .or_else(|| args.get("destination_path"))
104 .and_then(|v| v.as_str());
105
106 let Some(path_str) = path_str else {
107 return false;
108 };
109 let path = std::path::Path::new(path_str);
110
111 let plans_suffix = std::path::Path::new(".vtcode").join("plans");
113
114 if path_str.contains(".vtcode/plans/") || path_str.contains(".vtcode\\plans\\") {
116 return true;
117 }
118
119 if path.starts_with(&plans_suffix) {
121 return true;
122 }
123
124 let workspace = self.inventory.workspace_root();
126 let plans_dir = workspace.join(".vtcode").join("plans");
127 if path.starts_with(&plans_dir) {
128 return true;
129 }
130
131 if path_str.contains("/tmp/vtcode-plans/") || path_str.contains("\\tmp\\vtcode-plans\\") {
133 return true;
134 }
135 let tmp_plans = std::env::temp_dir().join("vtcode-plans");
136 if path.starts_with(&tmp_plans) {
137 return true;
138 }
139
140 false
141 }
142
143 #[expect(dead_code)]
147 pub(super) fn is_readonly_unified_action(&self, tool_name: &str, args: &Value) -> bool {
148 crate::tools::tool_intent::classify_tool_intent(tool_name, args).readonly_unified_action
149 }
150}
151
152#[cfg(test)]
153mod tests {
154 use super::ToolRegistry;
155 use crate::config::constants::tools;
156 use anyhow::Result;
157 use serde_json::json;
158 use tempfile::TempDir;
159
160 #[tokio::test]
161 async fn retry_safe_for_readonly_calls() -> Result<()> {
162 let temp_dir = TempDir::new()?;
163 let registry = ToolRegistry::new(temp_dir.path().to_path_buf()).await;
164
165 assert!(registry.is_retry_safe_call(
166 tools::UNIFIED_FILE,
167 &json!({"action": "read", "path": "README.md"})
168 ));
169 assert!(registry.is_retry_safe_call(
170 tools::UNIFIED_EXEC,
171 &json!({"action": "poll", "session_id": 42})
172 ));
173 assert!(registry.is_retry_safe_call(
174 tools::UNIFIED_EXEC,
175 &json!({"action": "inspect", "spool_path": ".vtcode/context/tool_outputs/run-1.txt"})
176 ));
177 assert!(registry.is_retry_safe_call(tools::READ_FILE, &json!({"path": "README.md"})));
178
179 Ok(())
180 }
181
182 #[tokio::test]
183 async fn retry_unsafe_for_mutating_calls() -> Result<()> {
184 let temp_dir = TempDir::new()?;
185 let registry = ToolRegistry::new(temp_dir.path().to_path_buf()).await;
186
187 assert!(!registry.is_retry_safe_call(
188 tools::UNIFIED_FILE,
189 &json!({"action": "write", "path": "foo.txt", "content": "x"})
190 ));
191 assert!(!registry.is_retry_safe_call(
192 tools::UNIFIED_EXEC,
193 &json!({"action": "run", "command": "echo hi"})
194 ));
195 assert!(!registry.is_retry_safe_call(
196 tools::WRITE_FILE,
197 &json!({"path": "foo.txt", "content": "x"})
198 ));
199
200 Ok(())
201 }
202
203 #[tokio::test]
204 async fn plan_mode_allows_adaptive_task_tracker_and_plan_alias() -> Result<()> {
205 let temp_dir = TempDir::new()?;
206 let registry = ToolRegistry::new(temp_dir.path().to_path_buf()).await;
207 registry.enable_plan_mode();
208
209 assert!(registry.is_plan_mode_allowed(tools::TASK_TRACKER, &json!({"action": "list"})));
210 assert!(
211 registry.is_plan_mode_allowed(tools::PLAN_TASK_TRACKER, &json!({"action": "list"}))
212 );
213
214 Ok(())
215 }
216
217 #[tokio::test]
218 async fn plan_mode_allows_readonly_unified_exec_runs() -> Result<()> {
219 let temp_dir = TempDir::new()?;
220 let registry = ToolRegistry::new(temp_dir.path().to_path_buf()).await;
221 registry.enable_plan_mode();
222
223 assert!(registry.is_plan_mode_allowed(
224 tools::UNIFIED_EXEC,
225 &json!({"action": "run", "command": "ls -la"})
226 ));
227 assert!(registry.is_plan_mode_allowed(
228 tools::UNIFIED_EXEC,
229 &json!({"action": "run", "command": "npm install --dry-run"})
230 ));
231 assert!(!registry.is_plan_mode_allowed(
232 tools::UNIFIED_EXEC,
233 &json!({"action": "run", "command": "echo hi"})
234 ));
235
236 Ok(())
237 }
238}