diff --git a/pkg/tools/builtin.go b/pkg/tools/builtin.go index edc416d..4c36f22 100644 --- a/pkg/tools/builtin.go +++ b/pkg/tools/builtin.go @@ -22,7 +22,7 @@ func ExecuteDefs(toolsPath string) []ToolInfo { }, { Name: "execute_tool", - Description: "Run a named tool script from " + toolsPath + " in a sandboxed environment. Use this only for named scripts, not for inline shell commands.", + Description: "Run a named tool script from " + toolsPath + " in a sandboxed environment. Supported languages: bash, python3 (detected from shebang). Use this only for named scripts, not for inline shell commands.", InputSchema: json.RawMessage(`{ "type": "object", "required": ["tool"], diff --git a/pkg/tools/execute/dispatch.go b/pkg/tools/execute/dispatch.go index 00150bf..5c5eb6b 100644 --- a/pkg/tools/execute/dispatch.go +++ b/pkg/tools/execute/dispatch.go @@ -81,13 +81,10 @@ func dispatchExecuteTool(ctx context.Context, e *Server, args json.RawMessage) ( if err != nil { return "", err } + language := detectLanguage(toolCode) code := toolCode if len(a.Args) > 0 { - var escaped []string - for _, arg := range a.Args { - escaped = append(escaped, "'"+strings.ReplaceAll(arg, "'", "'\\''")+"'") - } - code = fmt.Sprintf("set -- %s\n%s", strings.Join(escaped, " "), code) + code = injectArgs(language, a.Tool, a.Args, toolCode) } timeout := a.Timeout if timeout <= 0 { @@ -97,7 +94,7 @@ func dispatchExecuteTool(ctx context.Context, e *Server, args json.RawMessage) ( if sandbox == "" { sandbox = "default" } - return e.Execute(ctx, code, "bash", timeout, sandbox, true) + return e.Execute(ctx, code, language, timeout, sandbox, true) } func dispatchExecutePipe(ctx context.Context, e *Server, args json.RawMessage) (string, error) { diff --git a/pkg/tools/execute/executor.go b/pkg/tools/execute/executor.go index a8b445a..6dc5c34 100644 --- a/pkg/tools/execute/executor.go +++ b/pkg/tools/execute/executor.go @@ -181,20 +181,24 @@ func (e *Server) Execute(ctx context.Context, code, language string, timeout int defer cancel() var cmd *exec.Cmd + var interpreter []string switch language { case "bash", "": - wrapped := sandbox.WrapCommand(cfg, []string{"bash", "-c", code}, workDir) - cmd = exec.CommandContext(ctx, wrapped[0], wrapped[1:]...) - cmd.Dir = workDir - cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true} - cmd.Cancel = func() error { - if cmd.Process != nil { - syscall.Kill(-cmd.Process.Pid, syscall.SIGKILL) - } - return nil - } + interpreter = []string{"bash", "-c", code} + case "python3", "python": + interpreter = []string{"python3", "-c", code} default: - return "", fmt.Errorf("unsupported language: %s (supported: bash)", language) + return "", fmt.Errorf("unsupported language: %s (supported: bash, python3)", language) + } + wrapped := sandbox.WrapCommand(cfg, interpreter, workDir) + cmd = exec.CommandContext(ctx, wrapped[0], wrapped[1:]...) + cmd.Dir = workDir + cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true} + cmd.Cancel = func() error { + if cmd.Process != nil { + syscall.Kill(-cmd.Process.Pid, syscall.SIGKILL) + } + return nil } var outputBuf bytes.Buffer diff --git a/pkg/tools/execute/pipe.go b/pkg/tools/execute/pipe.go index 519619d..b991bb0 100644 --- a/pkg/tools/execute/pipe.go +++ b/pkg/tools/execute/pipe.go @@ -38,15 +38,20 @@ func BuildPipeline(steps []PipeStep) (string, bool, error) { } else { return "", false, fmt.Errorf("each pipe step requires either 'tool' or 'code'") } + language := "bash" + if step.Tool != "" { + language = detectLanguage(code) + } if len(step.Args) > 0 { - var escaped []string - for _, arg := range step.Args { - escaped = append(escaped, "'"+strings.ReplaceAll(arg, "'", "'\\''")+"'") - } - parts = append(parts, fmt.Sprintf("( set -- %s\n%s )", strings.Join(escaped, " "), code)) - } else { + code = injectArgs(language, step.Tool, step.Args, code) + } + switch language { + case "python3": + parts = append(parts, fmt.Sprintf("( python3 -c $'%s' )", ansiCEscape(code))) + default: parts = append(parts, fmt.Sprintf("(\n%s\n)", code)) } } return strings.Join(parts, " |\n"), true, nil + } diff --git a/pkg/tools/execute/tool.go b/pkg/tools/execute/tool.go index 7bf02bd..d98027b 100644 --- a/pkg/tools/execute/tool.go +++ b/pkg/tools/execute/tool.go @@ -17,6 +17,57 @@ func ToolsPath() string { return filepath.Join(home, ".local", "share", "ollie", "tools") } +// detectLanguage infers the script language from the shebang line. +// Returns "python3" for Python scripts, "bash" for everything else. +func detectLanguage(code string) string { + line, _, _ := strings.Cut(code, "\n") + line = strings.TrimSpace(line) + if strings.HasPrefix(line, "#!") && strings.Contains(line, "python") { + return "python3" + } + return "bash" +} + +// injectArgs prepends language-appropriate argument binding to code. +func injectArgs(language, name string, args []string, code string) string { + switch language { + case "python3": + quoted := make([]string, len(args)) + for i, a := range args { + quoted[i] = fmt.Sprintf("%q", a) + } + return fmt.Sprintf("import sys\nsys.argv = [%q, %s]\n%s", name, strings.Join(quoted, ", "), code) + default: // bash + escaped := make([]string, len(args)) + for i, a := range args { + escaped[i] = "'" + strings.ReplaceAll(a, "'", "'\\''") + "'" + } + return fmt.Sprintf("set -- %s\n%s", strings.Join(escaped, " "), code) + } +} + +// ansiCEscape escapes a string for embedding in a bash $'...' literal. +func ansiCEscape(s string) string { + var b strings.Builder + for _, r := range s { + switch r { + case '\\': + b.WriteString(`\\`) + case '\'': + b.WriteString(`\'`) + case '\n': + b.WriteString(`\n`) + case '\r': + b.WriteString(`\r`) + case '\t': + b.WriteString(`\t`) + default: + b.WriteRune(r) + } + } + return b.String() +} + // ReadTool reads a named tool script from the tools directory. func ReadTool(name string) (string, error) { if strings.Contains(name, "/") || strings.Contains(name, "..") { diff --git a/prompts/SYSTEM_PROMPT.md b/prompts/SYSTEM_PROMPT.md index be79e44..fe5eee2 100644 --- a/prompts/SYSTEM_PROMPT.md +++ b/prompts/SYSTEM_PROMPT.md @@ -17,6 +17,18 @@ Be truthful. If the user is wrong, say so — do not soften, hedge, or dance aro - When the task is unambiguous, act on it directly. Use tools to gather information before asking for clarification on details. - Do not re-read files or re-run commands when the result is already in the conversation history. +# Tools + +Tool scripts live at `${OLLIE_9MOUNT:-$HOME/mnt/ollie}/t/`. Run them with `execute_tool` or as steps in an `execute_pipe` pipeline. + +```sh +ls ${OLLIE_9MOUNT:-$HOME/mnt/ollie}/t/ # list available tools +``` + +Supported languages are detected from the script's shebang line: +- `#!/usr/bin/env bash` (or any bash shebang) — runs with `bash -c` +- `#!/usr/bin/env python3` (or any python shebang) — runs with `python3 -c`; `sys.argv` is set automatically when args are provided + # Skills Skills are domain-specific knowledge files. Discover and load them before starting non-trivial tasks.