1use std::path::PathBuf;
2
3use serde_json::Value;
4
5use crate::tools::{require_str, ToolResult, ToolRuntime};
6
7const SANDBOX_WRITE_SCRIPT: &str = r#"
8from pathlib import Path
9import sys
10
11orig = sys.argv[1]
12path = Path(sys.argv[2])
13
14try:
15 path.parent.mkdir(parents=True, exist_ok=True)
16 path.write_bytes(sys.stdin.buffer.read())
17 print("ok")
18except Exception as exc:
19 print(f"Error writing {orig}: {exc}")
20 sys.exit(2)
21"#;
22
23pub async fn execute(args: Value, runtime: &ToolRuntime) -> ToolResult {
24 let path = match require_str(&args, "path") {
25 Ok(value) => value,
26 Err(error) => return error,
27 };
28 let content = match require_str(&args, "content") {
29 Ok(value) => value,
30 Err(error) => return error,
31 };
32
33 if let Some(sandbox) = &runtime.sandbox {
34 let guest_path = match sandbox.resolve_path(&path) {
35 Ok(path) => path,
36 Err(error) => {
37 return ToolResult {
38 content: error.to_string(),
39 is_error: true,
40 }
41 }
42 };
43 let args = vec![
44 "-c".to_string(),
45 SANDBOX_WRITE_SCRIPT.to_string(),
46 path.clone(),
47 guest_path.display().to_string(),
48 ];
49 return match sandbox
50 .exec("python3", &args, Some(content.into_bytes()))
51 .await
52 {
53 Ok(output) if output.status.success() => ToolResult {
54 content: "ok".to_string(),
55 is_error: false,
56 },
57 Ok(output) => ToolResult {
58 content: String::from_utf8_lossy(&output.stdout).trim().to_string(),
59 is_error: true,
60 },
61 Err(error) => ToolResult {
62 content: format!("Error writing {} in sandbox: {}", path, error),
63 is_error: true,
64 },
65 };
66 }
67
68 let path = PathBuf::from(path);
69 if let Some(parent) = path.parent() {
70 if let Err(e) = tokio::fs::create_dir_all(parent).await {
71 return ToolResult {
72 content: format!("Error creating directories: {}", e),
73 is_error: true,
74 };
75 }
76 }
77
78 let _guard = crate::tools::acquire_write_lock().await;
79
80 match tokio::fs::write(&path, content.as_bytes()).await {
81 Ok(_) => ToolResult {
82 content: "ok".to_string(),
83 is_error: false,
84 },
85 Err(e) => ToolResult {
86 content: format!("Error writing {}: {}", path.display(), e),
87 is_error: true,
88 },
89 }
90}
91
92#[cfg(test)]
93mod tests {
94 use serde_json::json;
95 use std::collections::HashSet;
96 use std::sync::Arc;
97
98 use super::*;
99 use crate::events::EventSink;
100 use tokio::sync::Mutex;
101
102 fn local_runtime() -> ToolRuntime {
103 ToolRuntime {
104 store_path: PathBuf::new(),
105 session_id: None,
106 worker_executable: None,
107 active_threads: Arc::new(Mutex::new(HashSet::new())),
108 event_sink: EventSink::none(),
109 sandbox: None,
110 mcp: None,
111 skills: None,
112 activated_skills: Arc::new(Mutex::new(HashSet::new())),
113 terminal_manager: crate::terminal::TerminalManager::new(),
114 thread_timeout_secs: crate::tools::thread::DEFAULT_THREAD_TIMEOUT_SECS,
115 }
116 }
117
118 #[tokio::test]
119 async fn test_write_creates_dirs() {
120 let unique = std::time::SystemTime::now()
121 .duration_since(std::time::UNIX_EPOCH)
122 .expect("time went backwards")
123 .as_nanos();
124 let dir = std::env::temp_dir().join(format!("agent_test_write_dirs_{}", unique));
125 let file_path = dir.join("deep").join("nested").join("test.txt");
126 let path_str = file_path.to_string_lossy().to_string();
127
128 let result = execute(
129 json!({ "path": path_str, "content": "hello from test" }),
130 &local_runtime(),
131 )
132 .await;
133 assert!(!result.is_error, "Write failed: {}", result.content);
134 assert_eq!(result.content, "ok");
135
136 let written = std::fs::read_to_string(&file_path).expect("failed to read written file");
137 assert_eq!(written, "hello from test");
138
139 let _ = std::fs::remove_dir_all(&dir);
140 }
141}