package tool import ( "context" "encoding/json" "fmt" "os" "path/filepath" "strings" "time" "github.com/orca/orca/pkg/sandbox" ) // --------------------------------------------------------------------------- // exec — Execute a shell command via the sandbox // --------------------------------------------------------------------------- // execTool runs shell commands through the ProcessSandbox. type execTool struct { sandbox sandbox.Sandbox } // NewExecTool creates a new exec tool backed by the given sandbox. func NewExecTool(sb sandbox.Sandbox) Tool { if sb == nil { sb = sandbox.NewProcessSandbox() } return &execTool{sandbox: sb} } func (t *execTool) Name() string { return "exec" } func (t *execTool) Description() string { return "Execute a shell command and return its output. Use this for running scripts, " + "installing packages, compiling code, or any command-line operation." } func (t *execTool) Parameters() map[string]ParameterSchema { return map[string]ParameterSchema{ "command": { Type: "string", Description: "The shell command to execute (e.g., 'ls -la' or 'python script.py')", Required: true, }, "timeout": { Type: "number", Description: "Timeout in seconds for the command execution (default: 120)", Required: false, Default: float64(120), }, "workdir": { Type: "string", Description: "Working directory for the command (default: sandbox default)", Required: false, }, } } func (t *execTool) Execute(ctx context.Context, args map[string]interface{}) (*Result, error) { cmdStr, ok := args["command"].(string) if !ok || cmdStr == "" { return ErrorResult("'command' argument is required and must be a string"), nil } // Use a timeout if specified in args execCtx := ctx if timeoutVal, ok := args["timeout"]; ok { if timeout, err := toFloat64(timeoutVal); err == nil && timeout > 0 { var cancel context.CancelFunc execCtx, cancel = context.WithTimeout(ctx, time.Duration(timeout*float64(time.Second))) defer cancel() } } // Set working directory if specified sb := t.sandbox if wd, ok := args["workdir"].(string); ok && wd != "" { if ps, ok := sb.(*sandbox.ProcessSandbox); ok { ps.WorkingDir = wd } } // Execute the command via shell result, err := sb.Execute(execCtx, "sh", "-c", cmdStr) if err != nil { return nil, fmt.Errorf("exec tool: %w", err) } return &Result{ Success: result.ExitCode == 0, Data: map[string]interface{}{ "stdout": result.Stdout, "stderr": result.Stderr, "exit_code": result.ExitCode, }, }, nil } // --------------------------------------------------------------------------- // read_file — Read the contents of a file // --------------------------------------------------------------------------- type readFileTool struct{} func NewReadFileTool() Tool { return &readFileTool{} } func (t *readFileTool) Name() string { return "read_file" } func (t *readFileTool) Description() string { return "Read the contents of a file from the local filesystem. Returns the file content as a string." } func (t *readFileTool) Parameters() map[string]ParameterSchema { return map[string]ParameterSchema{ "path": { Type: "string", Description: "Absolute path to the file to read", Required: true, }, } } func (t *readFileTool) Execute(ctx context.Context, args map[string]interface{}) (*Result, error) { path, ok := args["path"].(string) if !ok || path == "" { return ErrorResult("'path' argument is required and must be a string"), nil } // Prevent directory traversal / read of non-regular files info, err := os.Stat(path) if err != nil { return ErrorResult(fmt.Sprintf("cannot access %q: %v", path, err)), nil } if info.IsDir() { return ErrorResult(fmt.Sprintf("%q is a directory, not a file", path)), nil } data, err := os.ReadFile(path) if err != nil { return ErrorResult(fmt.Sprintf("failed to read %q: %v", path, err)), nil } return SuccessResult(map[string]interface{}{ "path": path, "content": string(data), "size": len(data), }), nil } // --------------------------------------------------------------------------- // write_file — Write content to a file // --------------------------------------------------------------------------- type writeFileTool struct{} func NewWriteFileTool() Tool { return &writeFileTool{} } func (t *writeFileTool) Name() string { return "write_file" } func (t *writeFileTool) Description() string { return "Write content to a file on the local filesystem. Creates parent directories if needed." } func (t *writeFileTool) Parameters() map[string]ParameterSchema { return map[string]ParameterSchema{ "path": { Type: "string", Description: "Absolute path where the file should be written", Required: true, }, "content": { Type: "string", Description: "The content to write to the file", Required: true, }, } } func (t *writeFileTool) Execute(ctx context.Context, args map[string]interface{}) (*Result, error) { path, ok := args["path"].(string) if !ok || path == "" { return ErrorResult("'path' argument is required and must be a string"), nil } content, ok := args["content"].(string) if !ok { return ErrorResult("'content' argument is required and must be a string"), nil } // Create parent directories dir := filepath.Dir(path) if err := os.MkdirAll(dir, 0755); err != nil { return ErrorResult(fmt.Sprintf("failed to create directories for %q: %v", path, err)), nil } if err := os.WriteFile(path, []byte(content), 0644); err != nil { return ErrorResult(fmt.Sprintf("failed to write %q: %v", path, err)), nil } return SuccessResult(map[string]interface{}{ "path": path, "size": len(content), }), nil } // --------------------------------------------------------------------------- // list_dir — List the contents of a directory // --------------------------------------------------------------------------- type listDirTool struct{} func NewListDirTool() Tool { return &listDirTool{} } func (t *listDirTool) Name() string { return "list_dir" } func (t *listDirTool) Description() string { return "List files and directories in a given path. Returns names, sizes, and modification times." } func (t *listDirTool) Parameters() map[string]ParameterSchema { return map[string]ParameterSchema{ "path": { Type: "string", Description: "Absolute path to the directory to list", Required: true, }, "recursive": { Type: "boolean", Description: "Whether to list recursively (default: false)", Required: false, Default: false, }, } } func (t *listDirTool) Execute(ctx context.Context, args map[string]interface{}) (*Result, error) { path, ok := args["path"].(string) if !ok || path == "" { return ErrorResult("'path' argument is required and must be a string"), nil } recursive, _ := args["recursive"].(bool) info, err := os.Stat(path) if err != nil { return ErrorResult(fmt.Sprintf("cannot access %q: %v", path, err)), nil } if !info.IsDir() { return ErrorResult(fmt.Sprintf("%q is not a directory", path)), nil } var entries []map[string]interface{} if recursive { err = filepath.Walk(path, func(p string, fi os.FileInfo, err error) error { if err != nil { return err } rel, _ := filepath.Rel(path, p) if rel == "." { return nil } entries = append(entries, entryToMap(p, rel, fi)) return nil }) } else { files, err := os.ReadDir(path) if err != nil { return ErrorResult(fmt.Sprintf("failed to list %q: %v", path, err)), nil } for _, f := range files { fi, err := f.Info() if err != nil { continue } fullPath := filepath.Join(path, f.Name()) entries = append(entries, entryToMap(fullPath, f.Name(), fi)) } } if err != nil { return ErrorResult(fmt.Sprintf("failed to list %q: %v", path, err)), nil } return SuccessResult(map[string]interface{}{ "path": path, "entries": entries, "count": len(entries), }), nil } func entryToMap(fullPath, name string, fi os.FileInfo) map[string]interface{} { return map[string]interface{}{ "name": name, "path": fullPath, "size": fi.Size(), "is_dir": fi.IsDir(), "mode": fi.Mode().String(), "modtime": fi.ModTime().Format("2006-01-02T15:04:05Z07:00"), } } // --------------------------------------------------------------------------- // search_files — Search for content in files // --------------------------------------------------------------------------- type searchFilesTool struct{} func NewSearchFilesTool() Tool { return &searchFilesTool{} } func (t *searchFilesTool) Name() string { return "search_files" } func (t *searchFilesTool) Description() string { return "Search for a pattern in files within a directory. Supports simple substring matching." } func (t *searchFilesTool) Parameters() map[string]ParameterSchema { return map[string]ParameterSchema{ "pattern": { Type: "string", Description: "The text pattern to search for (substring match)", Required: true, }, "path": { Type: "string", Description: "Directory to search in (default: current directory)", Required: false, Default: ".", }, "include": { Type: "string", Description: "File glob pattern to include (e.g., '*.go', '*.{ts,tsx}')", Required: false, }, } } func (t *searchFilesTool) Execute(ctx context.Context, args map[string]interface{}) (*Result, error) { pattern, ok := args["pattern"].(string) if !ok || pattern == "" { return ErrorResult("'pattern' argument is required and must be a string"), nil } searchPath := "." if p, ok := args["path"].(string); ok && p != "" { searchPath = p } include, _ := args["include"].(string) // Verify search path exists info, err := os.Stat(searchPath) if err != nil { return ErrorResult(fmt.Sprintf("cannot access search path %q: %v", searchPath, err)), nil } if !info.IsDir() { return ErrorResult(fmt.Sprintf("%q is not a directory", searchPath)), nil } var matches []map[string]interface{} err = filepath.Walk(searchPath, func(p string, fi os.FileInfo, err error) error { if err != nil { return nil // skip files we can't access } if fi.IsDir() { return nil } // Apply include filter if include != "" { matched, err := filepath.Match(include, fi.Name()) if err != nil || !matched { return nil } } // Read file and search data, err := os.ReadFile(p) if err != nil { return nil // skip unreadable files } content := string(data) if strings.Contains(content, pattern) { matches = append(matches, map[string]interface{}{ "path": p, "size": len(data), }) } return nil }) if err != nil { return ErrorResult(fmt.Sprintf("search failed: %v", err)), nil } return SuccessResult(map[string]interface{}{ "pattern": pattern, "path": searchPath, "matches": matches, "count": len(matches), }), nil } // --------------------------------------------------------------------------- // Helpers // --------------------------------------------------------------------------- // toFloat64 converts an interface{} value to float64. // Supports float64, int, int64, and json.Number types. func toFloat64(v interface{}) (float64, error) { switch val := v.(type) { case float64: return val, nil case int: return float64(val), nil case int64: return float64(val), nil case json.Number: return val.Float64() default: return 0, fmt.Errorf("cannot convert %T to float64", v) } } // Compile-time interface checks. var _ Tool = (*execTool)(nil) var _ Tool = (*readFileTool)(nil) var _ Tool = (*writeFileTool)(nil) var _ Tool = (*listDirTool)(nil) var _ Tool = (*searchFilesTool)(nil)