ollie/cmd/toolsrv/internal/quirks/shell.go

105 lines
2.9 KiB
Go

// Package quirks contains workarounds for stupid model behaviors.
package quirks
import "strings"
// ShellInvokesNativeTool checks if a shell command attempts to execute a native
// tool. Returns the tool name if found, empty string otherwise.
//
// Only matches when the tool name appears in command position:
// - First word of the command
// - After a command separator (; | && ||)
// - As a path ending with the tool name (./tool, /usr/bin/tool)
//
// Does NOT match tool names that merely appear in arguments (e.g., commit messages).
func ShellInvokesNativeTool(cmd string, tools []string) string {
// Reject ollie-9p in command position.
if isInCommandPosition(cmd, "ollie-9p") {
return "client_9p"
}
for _, name := range tools {
if name == "shell" {
continue
}
if isInCommandPosition(cmd, name) {
return name
}
}
return ""
}
// isInCommandPosition returns true if name appears as an executable in cmd.
// It checks whether name is the first token of a (sub-)command or appears as a
// path ending with /name.
func isInCommandPosition(cmd, name string) bool {
// Check each sub-command (split on shell separators).
for _, sub := range splitCommands(cmd) {
sub = strings.TrimSpace(sub)
if sub == "" {
continue
}
// Get the first token (the command being executed).
first := firstToken(sub)
// Exact match: "tool_name args..."
if first == name {
return true
}
// Path match: "./tool_name", "../tool_name", "/usr/bin/tool_name"
if strings.HasSuffix(first, "/"+name) {
return true
}
}
return false
}
// splitCommands splits a shell command line on command separators: ;, |, &&, ||
// This is a rough heuristic — it doesn't handle quoting, but it's good enough
// to identify command positions in typical AI-generated shell commands.
func splitCommands(cmd string) []string {
var parts []string
var current strings.Builder
i := 0
for i < len(cmd) {
switch {
case cmd[i] == ';' || cmd[i] == '|':
parts = append(parts, current.String())
current.Reset()
if cmd[i] == '|' && i+1 < len(cmd) && cmd[i+1] == '|' {
i++ // skip ||
}
i++
case cmd[i] == '&' && i+1 < len(cmd) && cmd[i+1] == '&':
parts = append(parts, current.String())
current.Reset()
i += 2
case cmd[i] == '$' && i+1 < len(cmd) && cmd[i+1] == '(':
// Command substitution — the content after $( is a new command
parts = append(parts, current.String())
current.Reset()
i += 2
case cmd[i] == '`':
// Backtick substitution
parts = append(parts, current.String())
current.Reset()
i++
default:
current.WriteByte(cmd[i])
i++
}
}
if current.Len() > 0 {
parts = append(parts, current.String())
}
return parts
}
// firstToken returns the first whitespace-delimited token from s.
func firstToken(s string) string {
s = strings.TrimSpace(s)
if i := strings.IndexAny(s, " \t"); i >= 0 {
return s[:i]
}
return s
}