105 lines
2.9 KiB
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
|
|
}
|