strip execute_code to bash-only; remove language detection; run promoted tools directly
This commit is contained in:
parent
6bd77276ee
commit
1b05433a5b
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Reference in New Issue