file_write.go
1 package tools 2 3 import ( 4 "context" 5 "encoding/json" 6 "errors" 7 "fmt" 8 "os" 9 "path/filepath" 10 11 "github.com/Kocoro-lab/ShanClaw/internal/agent" 12 "github.com/Kocoro-lab/ShanClaw/internal/cwdctx" 13 ) 14 15 type FileWriteTool struct{} 16 17 type fileWriteArgs struct { 18 Path string `json:"path"` 19 Content string `json:"content"` 20 } 21 22 func (t *FileWriteTool) Info() agent.ToolInfo { 23 return agent.ToolInfo{ 24 Name: "file_write", 25 Description: "Write complete content to a file (overwrites entirely). Use for creating new files or as fallback when file_edit fails due to non-unique text. Always file_read first if the file already exists.", 26 Parameters: map[string]any{ 27 "type": "object", 28 "properties": map[string]any{ 29 "path": map[string]any{"type": "string", "description": "File path to write"}, 30 "content": map[string]any{"type": "string", "description": "Content to write"}, 31 }, 32 }, 33 Required: []string{"path", "content"}, 34 } 35 } 36 37 func (t *FileWriteTool) Run(ctx context.Context, argsJSON string) (agent.ToolResult, error) { 38 var args fileWriteArgs 39 if err := json.Unmarshal([]byte(argsJSON), &args); err != nil { 40 return agent.ToolResult{Content: fmt.Sprintf("invalid arguments: %v", err), IsError: true}, nil 41 } 42 resolved, resolveErr := cwdctx.ResolveFilesystemPath(ctx, args.Path) 43 if resolveErr != nil { 44 if errors.Is(resolveErr, cwdctx.ErrNoSessionCWD) { 45 return agent.ValidationError( 46 "file_write: no session working directory is set. Pass an absolute path.", 47 ), nil 48 } 49 return agent.ValidationError(fmt.Sprintf("file_write: %v", resolveErr)), nil 50 } 51 args.Path = resolved 52 53 // Block file_write on the agent's MEMORY.md — always use memory_append. 54 // Check unconditionally (not just for existing files) so first-write 55 // scenarios also go through the flock-protected bounded-append path. 56 if agent.IsMemoryFile(ctx, args.Path) { 57 return agent.ToolResult{ 58 Content: "Cannot write MEMORY.md with file_write — use the memory_append tool instead.", 59 IsError: true, 60 }, nil 61 } 62 63 // Enforce read-before-write for existing files (new files are fine) 64 if _, err := os.Stat(args.Path); err == nil { 65 if err := agent.CheckReadBeforeWrite(ctx, args.Path); err != nil { 66 return agent.ToolResult{Content: err.Error(), IsError: true}, nil 67 } 68 } 69 70 if err := os.MkdirAll(filepath.Dir(args.Path), 0755); err != nil { 71 if os.IsPermission(err) { 72 return agent.PermissionError(fmt.Sprintf("cannot create directory %s: permission denied", filepath.Dir(args.Path))), nil 73 } 74 return agent.ToolResult{Content: fmt.Sprintf("error creating directory: %v", err), IsError: true}, nil 75 } 76 77 if err := os.WriteFile(args.Path, []byte(args.Content), 0644); err != nil { 78 if os.IsPermission(err) { 79 return agent.PermissionError(fmt.Sprintf("cannot write %s: permission denied", args.Path)), nil 80 } 81 return agent.ToolResult{Content: fmt.Sprintf("error writing file: %v", err), IsError: true}, nil 82 } 83 84 return agent.ToolResult{Content: fmt.Sprintf("wrote %d bytes to %s", len(args.Content), args.Path)}, nil 85 } 86 87 func (t *FileWriteTool) RequiresApproval() bool { return true } 88 89 func (t *FileWriteTool) IsReadOnlyCall(string) bool { return false }