Skip to main content

sac/tools/
write.rs

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}