use super::error::{Result, ToolError, validate_path_safety};
use super::r#trait::{Tool, ToolCapability, ToolExecutionContext, ToolResult};
use async_trait::async_trait;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use tokio::fs;
pub struct WriteTool;
#[derive(Debug, Deserialize, Serialize)]
struct WriteInput {
path: String,
content: String,
#[serde(default)]
create_dirs: bool,
}
#[async_trait]
impl Tool for WriteTool {
fn name(&self) -> &str {
"write_file"
}
fn description(&self) -> &str {
"Write content to a file on the filesystem. Creates the file if it doesn't exist, overwrites if it does."
}
fn input_schema(&self) -> Value {
serde_json::json!({
"type": "object",
"properties": {
"path": {
"type": "string",
"description": "Path to the file to write (absolute or relative to working directory)"
},
"content": {
"type": "string",
"description": "Content to write to the file"
},
"create_dirs": {
"type": "boolean",
"description": "Whether to create parent directories if they don't exist (default: false)",
"default": false
}
},
"required": ["path", "content"]
})
}
fn capabilities(&self) -> Vec<ToolCapability> {
vec![
ToolCapability::WriteFiles,
ToolCapability::SystemModification,
]
}
fn requires_approval(&self) -> bool {
true }
fn validate_input(&self, input: &Value) -> Result<()> {
let _: WriteInput = serde_json::from_value(input.clone())
.map_err(|e| ToolError::InvalidInput(format!("Invalid input: {}", e)))?;
Ok(())
}
async fn execute(&self, input: Value, context: &ToolExecutionContext) -> Result<ToolResult> {
let input: WriteInput = serde_json::from_value(input)?;
let path = super::error::resolve_tool_path(&input.path, &context.working_dir());
if super::brain_file_safety::is_protected_path(&path) {
return Ok(ToolResult::error(format!(
"Refusing to write protected brain file '{}' with generic write_file. \
Use the `write_opencrabs_file` tool instead. It enforces append-only \
writes, dedup-aware shrinking, and saves a `.bak` snapshot before every \
change.",
path.display()
)));
}
if input.create_dirs
&& let Some(parent) = path.parent()
{
let canonical_wd = context.working_dir().canonicalize().map_err(|e| {
ToolError::Internal(format!("Failed to canonicalize working directory: {}", e))
})?;
if parent.exists() {
let canonical_parent = parent.canonicalize().map_err(|e| {
ToolError::InvalidInput(format!("Failed to resolve parent path: {}", e))
})?;
if !canonical_parent.starts_with(&canonical_wd) {
return Ok(ToolResult::error(format!(
"Access denied: Path '{}' is outside the working directory",
input.path
)));
}
}
fs::create_dir_all(parent).await.map_err(ToolError::Io)?;
}
let path = match validate_path_safety(&input.path, &context.working_dir()) {
Ok(p) => p,
Err(ToolError::InvalidInput(msg))
if msg.contains("Parent directory does not exist") =>
{
let resolved = std::path::PathBuf::from(&input.path);
if let Some(parent) = resolved.parent() {
return Ok(ToolResult::error(format!(
"Parent directory does not exist: {}. Use create_dirs: true to create it.",
parent.display()
)));
}
return Ok(ToolResult::error(msg));
}
Err(ToolError::InvalidInput(msg)) => {
return Ok(ToolResult::error(format!("Invalid path: {}", msg)));
}
Err(e) => return Err(e),
};
if let Some(parent) = path.parent()
&& !parent.exists()
{
return Ok(ToolResult::error(format!(
"Parent directory does not exist: {}. Use create_dirs: true to create it.",
parent.display()
)));
}
fs::write(&path, &input.content)
.await
.map_err(ToolError::Io)?;
if let Some(ref sc) = context.service_context {
let fs = crate::services::FileService::new(sc.clone());
let _ = fs
.get_or_create_file(context.session_id, path.clone(), None)
.await;
}
let message = format!(
"Successfully wrote {} bytes to {}",
input.content.len(),
path.display()
);
Ok(ToolResult::success(message)
.with_metadata("path".to_string(), path.display().to_string())
.with_metadata("bytes".to_string(), input.content.len().to_string()))
}
}