From 1b05433a5b89e965dd74c17d509e1d231af7f314 Mon Sep 17 00:00:00 2001 From: ollie Date: Tue, 28 Jul 2026 22:59:44 +0200 Subject: [PATCH] strip execute_code to bash-only; remove language detection; run promoted tools directly --- pkg/tools/execute/code.go | 34 +--------- pkg/tools/execute/server.go | 125 +++++++++------------------------- pkg/tools/execute/tool.go | 131 ++---------------------------------- 3 files changed, 39 insertions(+), 251 deletions(-) diff --git a/pkg/tools/execute/code.go b/pkg/tools/execute/code.go index 92550ec..469ee84 100644 --- a/pkg/tools/execute/code.go +++ b/pkg/tools/execute/code.go @@ -39,14 +39,10 @@ func resolveCodeStep(s CodeStep) (code, language string, trusted bool, err error if terr != nil { return "", "", false, terr } - language = detectLanguage(toolCode) + language = "bash" code = toolCode if len(s.Args) > 0 { code = injectArgs(language, s.Tool, s.Args, toolCode) - switch language { - case "awk", "sed", "jq", "ed", "expect", "bc": - language = "bash" - } } return code, language, true, nil } @@ -79,33 +75,9 @@ var bashPatterns = []*regexp.Regexp{ regexp.MustCompile(`\beval\s+".*\$`), } -// pythonPatterns apply to python3/python. -var pythonPatterns = []*regexp.Regexp{ - regexp.MustCompile(`shutil\.rmtree\s*\(\s*['"/]`), - regexp.MustCompile(`(os\.system|subprocess\.(call|run|popen))\s*\(.*\brm\s+-[a-z]*r[a-z]*f`), - regexp.MustCompile(`os\.(remove|unlink)\s*\(\s*['"]/(etc|usr|bin|sbin|lib|boot)`), -} - -// perlPatterns apply to perl. -var perlPatterns = []*regexp.Regexp{ - regexp.MustCompile(`(system|exec)\s*\(.*\brm\s+-[a-z]*r[a-z]*f`), - regexp.MustCompile("`.+rm\\s+-[a-z]*r[a-z]*f"), // backtick execution - regexp.MustCompile(`unlink\s+glob\s*\(['"]/(etc|usr|bin|sbin|lib|boot)`), -} - -// luaPatterns apply to lua. -var luaPatterns = []*regexp.Regexp{ - regexp.MustCompile(`(os\.execute|io\.popen)\s*\(.*\brm\s+-[a-z]*r[a-z]*f`), - regexp.MustCompile(`os\.remove\s*\(\s*['"]/(etc|usr|bin|sbin|lib|boot)`), -} - var languagePatterns = map[string][]*regexp.Regexp{ - "bash": bashPatterns, - "": bashPatterns, - "python3": pythonPatterns, - "python": pythonPatterns, - "perl": perlPatterns, - "lua": luaPatterns, + "bash": bashPatterns, + "": bashPatterns, } // Dispatch routes a named execute tool call. diff --git a/pkg/tools/execute/server.go b/pkg/tools/execute/server.go index 29460e6..a76e72a 100644 --- a/pkg/tools/execute/server.go +++ b/pkg/tools/execute/server.go @@ -148,7 +148,7 @@ Each step is one of: - {detach: true, code} — start process and immediately background it; returns PID - {parallel: [{code/language}...]} — concurrent fan-out; outputs concatenated in submission order -Supported languages: bash, python3, perl, lua, awk, sed, jq, ed, expect, bc. +Only bash is supported. timeout applies to each step independently (default: 30s). A failed step aborts. Examples: @@ -270,12 +270,16 @@ func (e *Server) SetToolRegistry(r *Registry, sessionID string) { e.sessionID = sessionID } -// callPromotedTool executes a tool promoted via the registry. +// callPromotedTool executes a tool promoted via the registry by running the +// script file directly as an executable. func (e *Server) callPromotedTool(ctx context.Context, tool string, args json.RawMessage) (json.RawMessage, error) { - script, err := ReadTool(tool) - if err != nil { - return nil, fmt.Errorf("read tool %s: %w", tool, err) + // Resolve script path. + if strings.Contains(tool, "/") || strings.Contains(tool, "..") { + return nil, fmt.Errorf("invalid tool name") } + path := filepath.Join(ToolsPath(), tool) + + // Parse positional args. var positional []string var argMap map[string]interface{} if err := json.Unmarshal(args, &argMap); err == nil { @@ -285,78 +289,36 @@ func (e *Server) callPromotedTool(ctx context.Context, tool string, args json.Ra positional = append(positional, fmt.Sprintf("%v", v)) } } - } else { - positional = orderArgsBySchema(script, argMap) } } - lang := DetectLanguage(script) - code := InjectArgs(lang, tool, positional, script) - result, err := e.Execute(ctx, code, lang, 30, "default", true) + + var cancel context.CancelFunc + ctx, cancel = context.WithTimeout(ctx, 30*time.Second) + defer cancel() + + // Execute the script directly — shebang handles the interpreter. + e.wdMu.RLock() + workDir := e.cwd + e.wdMu.RUnlock() + if workDir == "" { + workDir, _ = os.Getwd() + } + cmd := exec.CommandContext(ctx, path, positional...) + cmd.Dir = workDir + + output, err := cmd.CombinedOutput() if err != nil { return json.Marshal(map[string]interface{}{ "isError": true, - "content": []map[string]string{{"type": "text", "text": err.Error()}}, + "content": []map[string]string{{"type": "text", "text": string(output) + ": " + err.Error()}}, }) } return json.Marshal(map[string]interface{}{ - "content": []map[string]string{{"type": "text", "text": result}}, + "content": []map[string]string{{"type": "text", "text": string(output)}}, }) } -// orderArgsBySchema extracts values from argMap in schema property order. -// Uses json.Decoder to preserve JSON key ordering. -func orderArgsBySchema(script string, argMap map[string]interface{}) []string { - schema := ExtractArgsSchema(script) - if schema == nil { - var out []string - for _, v := range argMap { - out = append(out, fmt.Sprintf("%v", v)) - } - return out - } - // Parse the schema to find the "properties" key - var raw map[string]json.RawMessage - if err := json.Unmarshal(schema, &raw); err != nil { - var out []string - for _, v := range argMap { - out = append(out, fmt.Sprintf("%v", v)) - } - return out - } - props, ok := raw["properties"] - if !ok || len(props) == 0 { - var out []string - for _, v := range argMap { - out = append(out, fmt.Sprintf("%v", v)) - } - return out - } - // Decode properties sub-object preserving key order using json.Decoder - dec := json.NewDecoder(bytes.NewReader(props)) - tok, err := dec.Token() - if err != nil || tok != json.Delim('{') { - var out []string - for _, v := range argMap { - out = append(out, fmt.Sprintf("%v", v)) - } - return out - } - var out []string - for dec.More() { - keyTok, err := dec.Token() - if err != nil { - break - } - key := fmt.Sprintf("%v", keyTok) - // Skip the value - var val json.RawMessage - dec.Decode(&val) - if v, ok := argMap[key]; ok { - out = append(out, fmt.Sprintf("%v", v)) - } - } - return out -} + // Close is called when the session ends. Calls OnClose hook if registered. func (e *Server) Close() { @@ -585,13 +547,7 @@ func (e *Server) executeWithStdin(ctx context.Context, code, language string, ti timeout = 30 } - // Domain-specific tools (awk, sed, jq, ed, bc, expect) skip validation — - // they are constrained by design and the sandbox handles OS-level restrictions. - // General-purpose languages get universal + language-specific pattern checks. - _, isDomainSpecific := map[string]struct{}{ - "awk": {}, "sed": {}, "jq": {}, "ed": {}, "bc": {}, "expect": {}, - }[language] - if !trusted && !isDomainSpecific { + if !trusted { if err := e.ValidateCode(code, language); err != nil { return "", err } @@ -632,30 +588,9 @@ func (e *Server) executeWithStdin(ctx context.Context, code, language string, ti switch language { case "bash", "": interpreter = []string{"bash", "-c", code} - case "python3", "python": - interpreter = []string{"python3", "-c", code} - case "perl": - interpreter = []string{"perl", "-e", code} - case "lua": - interpreter = []string{"lua", "-e", code} - case "awk": - interpreter = []string{"gawk", code} - case "sed": - interpreter = []string{"sed", "-e", code} - case "jq": - interpreter = []string{"jq", code} - case "ed": - interpreter = []string{"ed", "-s"} - codeStdin = code - case "expect": - interpreter = []string{"expect", "-"} - codeStdin = code - case "bc": - interpreter = []string{"bc", "-l"} - codeStdin = code default: cancel() - return "", fmt.Errorf("unsupported language: %s (supported: bash, python3, perl, awk, sed, ed, jq, expect, bc, lua)", language) + return "", fmt.Errorf("unsupported language: %s (only bash is supported)", language) } e.envMu.RLock() envMap := make(map[string]string, len(e.envExtra)) diff --git a/pkg/tools/execute/tool.go b/pkg/tools/execute/tool.go index 2777684..fa5f166 100644 --- a/pkg/tools/execute/tool.go +++ b/pkg/tools/execute/tool.go @@ -36,114 +36,15 @@ func PluginsPath() string { return paths.CfgDir() + "/scripts/x" } -// detectLanguage infers the script language from the shebang line. -// Returns "python3", "perl", "awk", "sed", "ed", or "bash". -func detectLanguage(code string) string { - line, _, _ := strings.Cut(code, "\n") - line = strings.TrimSpace(line) - if !strings.HasPrefix(line, "#!") { - return "bash" - } - fields := strings.Fields(strings.TrimPrefix(line, "#!")) - if len(fields) == 0 { - return "bash" - } - // When the interpreter is /usr/bin/env, the actual interpreter is the next argument. - names := fields - if filepath.Base(fields[0]) == "env" && len(fields) > 1 { - names = fields[1:] - } - switch filepath.Base(names[0]) { - case "python", "python3": - return "python3" - case "perl": - return "perl" - case "awk", "gawk": - return "awk" - case "sed", "gsed": - return "sed" - case "ed": - return "ed" - case "jq": - return "jq" - case "expect": - return "expect" - case "bc": - return "bc" - case "lua", "lua5.1", "lua5.2", "lua5.3", "lua5.4": - return "lua" - } - return "bash" -} -// DetectLanguage is the exported version of detectLanguage. -func DetectLanguage(code string) string { - return detectLanguage(code) -} -// injectArgs prepends language-appropriate argument binding to code. +// injectArgs prepends 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) - case "perl": - quoted := make([]string, len(args)) - for i, a := range args { - quoted[i] = "'" + strings.ReplaceAll(a, "'", "\\'") + "'" - } - return fmt.Sprintf("@ARGV = (%s);\n%s", strings.Join(quoted, ", "), code) - case "awk": - // awk args are input filenames; produce a bash snippet: gawk -e $'prog' -- file ... - fileArgs := make([]string, len(args)) - for i, a := range args { - fileArgs[i] = "'" + strings.ReplaceAll(a, "'", "'\\''") + "'" - } - return fmt.Sprintf("gawk -e $'%s' -- %s", ansiCEscape(code), strings.Join(fileArgs, " ")) - case "sed": - // sed args are input filenames (use -i in the script for in-place editing). - fileArgs := make([]string, len(args)) - for i, a := range args { - fileArgs[i] = "'" + strings.ReplaceAll(a, "'", "'\\''") + "'" - } - return fmt.Sprintf("sed -e $'%s' %s", ansiCEscape(code), strings.Join(fileArgs, " ")) - case "ed": - // ed reads commands from stdin; first arg is the file to edit. - file := "" - if len(args) > 0 { - file = " '" + strings.ReplaceAll(args[0], "'", "'\\''") + "'" - } - return fmt.Sprintf("printf '%%s' $'%s' | ed -s%s", ansiCEscape(code), file) - case "jq": - // jq args are JSON input files; filter is the program. - fileArgs := make([]string, len(args)) - for i, a := range args { - fileArgs[i] = "'" + strings.ReplaceAll(a, "'", "'\\''") + "'" - } - return fmt.Sprintf("jq $'%s' %s", ansiCEscape(code), strings.Join(fileArgs, " ")) - case "expect": - // expect reads script from stdin via expect -. - return fmt.Sprintf("printf '%%s' $'%s' | expect -", ansiCEscape(code)) - case "bc": - // bc reads from stdin; -ql for quiet mode + math library. - return fmt.Sprintf("printf '%%s' $'%s' | bc -ql", ansiCEscape(code)) - case "lua": - // Inject args as the arg table, matching lua's scriptfile convention. - quoted := make([]string, len(args)) - for i, a := range args { - quoted[i] = fmt.Sprintf("%q", a) - } - return fmt.Sprintf("arg={%s}\n%s", 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) + 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) } // InjectArgs is the exported version of injectArgs. @@ -151,27 +52,7 @@ func InjectArgs(language, name string, args []string, code string) string { return injectArgs(language, name, args, 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() -} + // ExtractPrompt parses the ollie:prompt ... ollie:end block from a tool // script's header comments. Returns the prompt text with comment prefixes