package tools import ( "context" "fmt" "os" "path/filepath" "strings" ) // FileTool provides sandboxed file system operations for the LLM. // All paths are restricted to a DATA_DIR to prevent directory traversal attacks. type FileTool struct { dataDir string } // NewFileTool creates a file operation tool with the given data directory. func NewFileTool(dataDir string) *FileTool { if dataDir == "" { dataDir = "/tmp/cyrene_data" } return &FileTool{dataDir: dataDir} } // Definition returns the tool definition for LLM function calling. func (t *FileTool) Definition() ToolDefinition { return ToolDefinition{ Name: "file_ops", Description: "文件操作工具。在服务端安全沙盒内读写文件、列出目录、检查文件是否存在、删除文件。所有操作限制在数据目录内,无法访问系统文件。", Parameters: map[string]interface{}{ "type": "object", "properties": map[string]interface{}{ "action": map[string]interface{}{ "type": "string", "enum": []string{"read", "write", "list", "exists", "delete"}, "description": "操作类型。read: 读取文件;write: 写入文件(覆盖或创建);list: 列出目录内容;exists: 检查路径是否存在;delete: 删除文件", }, "path": map[string]interface{}{ "type": "string", "description": "文件或目录路径(相对于数据目录),如 \"notes/todo.txt\"", }, "content": map[string]interface{}{ "type": "string", "description": "写入内容(write 操作时必需)", }, }, "required": []string{"action", "path"}, }, } } // Execute performs file operations. func (t *FileTool) Execute(ctx context.Context, arguments map[string]interface{}) (*ToolResult, error) { action, ok := arguments["action"].(string) if !ok || action == "" { return &ToolResult{ ToolName: "file_ops", Success: false, Error: "缺少 action 参数", }, nil } relPath, ok := arguments["path"].(string) if !ok || relPath == "" { return &ToolResult{ ToolName: "file_ops", Success: false, Error: "缺少 path 参数", }, nil } safePath, err := t.resolveSafePath(relPath) if err != nil { return &ToolResult{ ToolName: "file_ops", Success: false, Error: err.Error(), }, nil } switch action { case "read": return t.handleRead(safePath, relPath) case "write": content, _ := arguments["content"].(string) return t.handleWrite(safePath, relPath, content) case "list": return t.handleList(safePath, relPath) case "exists": return t.handleExists(safePath, relPath) case "delete": return t.handleDelete(safePath, relPath) default: return &ToolResult{ ToolName: "file_ops", Success: false, Error: fmt.Sprintf("未知操作: %s,支持: read, write, list, exists, delete", action), }, nil } } // resolveSafePath resolves a relative path and ensures it stays within dataDir. func (t *FileTool) resolveSafePath(relPath string) (string, error) { // Clean the path first clean := filepath.Clean(relPath) // Ensure data directory exists if err := os.MkdirAll(t.dataDir, 0755); err != nil { return "", fmt.Errorf("创建数据目录失败: %v", err) } abs := filepath.Join(t.dataDir, clean) // Prevent directory traversal realPath, err := filepath.EvalSymlinks(abs) if err != nil { // If the path doesn't exist yet, we can still check the prefix if os.IsNotExist(err) { // Ensure the resolved path (without symlinks) is within dataDir if !strings.HasPrefix(filepath.Clean(abs), filepath.Clean(t.dataDir)+string(filepath.Separator)) && filepath.Clean(abs) != filepath.Clean(t.dataDir) { return "", fmt.Errorf("路径穿越检测: %s 不在允许的数据目录内", relPath) } return abs, nil } return "", fmt.Errorf("路径解析失败: %v", err) } if !strings.HasPrefix(realPath, filepath.Clean(t.dataDir)+string(filepath.Separator)) && realPath != filepath.Clean(t.dataDir) { return "", fmt.Errorf("路径穿越检测: %s 不在允许的数据目录内", relPath) } return realPath, nil } // handleRead reads a file, limited to 100KB. func (t *FileTool) handleRead(absPath, relPath string) (*ToolResult, error) { const maxSize = 100 * 1024 // 100KB info, err := os.Stat(absPath) if err != nil { if os.IsNotExist(err) { return &ToolResult{ ToolName: "file_ops", Success: false, Error: fmt.Sprintf("文件不存在: %s", relPath), }, nil } return &ToolResult{ ToolName: "file_ops", Success: false, Error: fmt.Sprintf("读取文件失败: %v", err), }, nil } if info.IsDir() { return &ToolResult{ ToolName: "file_ops", Success: false, Error: fmt.Sprintf("路径是目录,不能用 read 操作: %s", relPath), }, nil } if info.Size() > maxSize { return &ToolResult{ ToolName: "file_ops", Success: false, Error: fmt.Sprintf("文件过大 (%d bytes),超过限制 (%d bytes)", info.Size(), maxSize), }, nil } data, err := os.ReadFile(absPath) if err != nil { return &ToolResult{ ToolName: "file_ops", Success: false, Error: fmt.Sprintf("读取文件失败: %v", err), }, nil } return &ToolResult{ ToolName: "file_ops", Success: true, Data: fmt.Sprintf("文件: %s\n大小: %d bytes\n---\n%s", relPath, len(data), string(data)), }, nil } // handleWrite writes content to a file. func (t *FileTool) handleWrite(absPath, relPath, content string) (*ToolResult, error) { // Ensure parent directory exists dir := filepath.Dir(absPath) if err := os.MkdirAll(dir, 0755); err != nil { return &ToolResult{ ToolName: "file_ops", Success: false, Error: fmt.Sprintf("创建目录失败: %v", err), }, nil } if err := os.WriteFile(absPath, []byte(content), 0644); err != nil { return &ToolResult{ ToolName: "file_ops", Success: false, Error: fmt.Sprintf("写入文件失败: %v", err), }, nil } return &ToolResult{ ToolName: "file_ops", Success: true, Data: fmt.Sprintf("已写入文件: %s (%d bytes)", relPath, len(content)), }, nil } // handleList lists directory contents. func (t *FileTool) handleList(absPath, relPath string) (*ToolResult, error) { entries, err := os.ReadDir(absPath) if err != nil { if os.IsNotExist(err) { return &ToolResult{ ToolName: "file_ops", Success: false, Error: fmt.Sprintf("目录不存在: %s", relPath), }, nil } return &ToolResult{ ToolName: "file_ops", Success: false, Error: fmt.Sprintf("读取目录失败: %v", err), }, nil } if len(entries) == 0 { return &ToolResult{ ToolName: "file_ops", Success: true, Data: fmt.Sprintf("目录: %s\n(空目录)", relPath), }, nil } var result strings.Builder result.WriteString(fmt.Sprintf("目录: %s\n共 %d 项:\n", relPath, len(entries))) for _, entry := range entries { icon := "📄" if entry.IsDir() { icon = "📁" } info, _ := entry.Info() size := "" if info != nil && !entry.IsDir() { size = fmt.Sprintf(" (%d bytes)", info.Size()) } result.WriteString(fmt.Sprintf(" %s %s%s\n", icon, entry.Name(), size)) } return &ToolResult{ ToolName: "file_ops", Success: true, Data: result.String(), }, nil } // handleExists checks whether a path exists. func (t *FileTool) handleExists(absPath, relPath string) (*ToolResult, error) { info, err := os.Stat(absPath) if err != nil { if os.IsNotExist(err) { return &ToolResult{ ToolName: "file_ops", Success: true, Data: fmt.Sprintf("路径不存在: %s", relPath), }, nil } return &ToolResult{ ToolName: "file_ops", Success: false, Error: fmt.Sprintf("检查路径失败: %v", err), }, nil } kind := "文件" if info.IsDir() { kind = "目录" } return &ToolResult{ ToolName: "file_ops", Success: true, Data: fmt.Sprintf("路径存在: %s (%s, %d bytes)", relPath, kind, info.Size()), }, nil } // handleDelete deletes a file. func (t *FileTool) handleDelete(absPath, relPath string) (*ToolResult, error) { info, err := os.Stat(absPath) if err != nil { if os.IsNotExist(err) { return &ToolResult{ ToolName: "file_ops", Success: false, Error: fmt.Sprintf("文件不存在: %s", relPath), }, nil } return &ToolResult{ ToolName: "file_ops", Success: false, Error: fmt.Sprintf("删除文件失败: %v", err), }, nil } if info.IsDir() { return &ToolResult{ ToolName: "file_ops", Success: false, Error: fmt.Sprintf("不能删除目录(安全限制): %s", relPath), }, nil } if err := os.Remove(absPath); err != nil { return &ToolResult{ ToolName: "file_ops", Success: false, Error: fmt.Sprintf("删除文件失败: %v", err), }, nil } return &ToolResult{ ToolName: "file_ops", Success: true, Data: fmt.Sprintf("已删除文件: %s", relPath), }, nil }