opendev_tools_impl/
file_write.rs1use std::collections::HashMap;
4use std::path::Path;
5
6use opendev_tools_core::{BaseTool, ToolContext, ToolResult};
7
8use crate::diagnostics_helper;
9use crate::formatter;
10use crate::path_utils::{is_sensitive_file, resolve_file_path, validate_path_access};
11
12#[derive(Debug)]
14pub struct FileWriteTool;
15
16#[async_trait::async_trait]
17impl BaseTool for FileWriteTool {
18 fn name(&self) -> &str {
19 "write_file"
20 }
21
22 fn description(&self) -> &str {
23 "Write content to a file. Creates parent directories if needed. Uses atomic writes."
24 }
25
26 fn parameter_schema(&self) -> serde_json::Value {
27 serde_json::json!({
28 "type": "object",
29 "properties": {
30 "file_path": {
31 "type": "string",
32 "description": "Absolute path to the file to write"
33 },
34 "content": {
35 "type": "string",
36 "description": "Content to write to the file"
37 },
38 "create_dirs": {
39 "type": "boolean",
40 "description": "Create parent directories if they don't exist (default: true)"
41 }
42 },
43 "required": ["file_path", "content"]
44 })
45 }
46
47 async fn execute(
48 &self,
49 args: HashMap<String, serde_json::Value>,
50 ctx: &ToolContext,
51 ) -> ToolResult {
52 let file_path = match args.get("file_path").and_then(|v| v.as_str()) {
53 Some(p) => p,
54 None => return ToolResult::fail("file_path is required"),
55 };
56
57 let content = match args.get("content").and_then(|v| v.as_str()) {
58 Some(c) => c,
59 None => return ToolResult::fail("content is required"),
60 };
61
62 let create_dirs = args
63 .get("create_dirs")
64 .and_then(|v| v.as_bool())
65 .unwrap_or(true);
66
67 let path = resolve_file_path(file_path, &ctx.working_dir);
68
69 if let Err(msg) = validate_path_access(&path, &ctx.working_dir) {
70 return ToolResult::fail(msg);
71 }
72
73 if let Some(reason) = is_sensitive_file(&path) {
75 return ToolResult::fail(format!(
76 "Refusing to write to {}: {} — this file likely contains secrets. \
77 If you need to modify it, ask the user to do so manually.",
78 file_path, reason
79 ));
80 }
81
82 if create_dirs {
84 if let Some(parent) = path.parent()
85 && !parent.exists()
86 && let Err(e) = std::fs::create_dir_all(parent)
87 {
88 return ToolResult::fail(format!("Failed to create directories: {e}"));
89 }
90 } else if let Some(parent) = path.parent()
91 && !parent.exists()
92 {
93 return ToolResult::fail(format!(
94 "Parent directory does not exist: {}",
95 parent.display()
96 ));
97 }
98
99 let dir = path.parent().unwrap_or(Path::new("."));
101 let tmp_path = dir.join(format!(".{}.tmp", uuid::Uuid::new_v4()));
102
103 if let Err(e) = std::fs::write(&tmp_path, content) {
104 return ToolResult::fail(format!("Failed to write temp file: {e}"));
105 }
106
107 if let Err(e) = std::fs::rename(&tmp_path, &path) {
108 let _ = std::fs::remove_file(&tmp_path);
110 return ToolResult::fail(format!("Failed to rename temp file: {e}"));
111 }
112
113 let formatted =
115 formatter::format_file(path.to_str().unwrap_or(file_path), &ctx.working_dir);
116
117 let lines = content.lines().count();
118 let bytes = content.len();
119
120 let mut metadata = HashMap::new();
121 metadata.insert("lines".into(), serde_json::json!(lines));
122 metadata.insert("bytes".into(), serde_json::json!(bytes));
123 if formatted {
124 metadata.insert("formatted".into(), serde_json::json!(true));
125 }
126
127 let fmt_note = if formatted { " (formatted)" } else { "" };
128 let mut output = format!("Wrote {bytes} bytes ({lines} lines) to {file_path}{fmt_note}");
129
130 if let Some(diag_output) =
132 diagnostics_helper::collect_post_edit_diagnostics(ctx, &path).await
133 {
134 output.push_str(&diag_output);
135 }
136
137 ToolResult::ok_with_metadata(output, metadata)
138 }
139}
140
141#[cfg(test)]
142#[path = "file_write_tests.rs"]
143mod tests;