mirror of
https://github.com/ollama/ollama.git
synced 2026-07-23 09:10:53 -05:00
agent: use platform shell tool names
This commit is contained in:
@@ -225,11 +225,12 @@ func approvalRequestWithEvaluation(req ApprovalRequest, evaluation ApprovalEvalu
|
||||
}
|
||||
|
||||
func approvalSessionKey(req ApprovalRequest) string {
|
||||
switch req.ToolName {
|
||||
case "bash":
|
||||
if IsShellToolName(req.ToolName) {
|
||||
if command, ok := stringApprovalArg(req.Args, "command"); ok {
|
||||
return "bash:" + command
|
||||
return req.ToolName + ":" + command
|
||||
}
|
||||
}
|
||||
switch req.ToolName {
|
||||
case "edit":
|
||||
if path, ok := stringApprovalArg(req.Args, "path"); ok {
|
||||
return "edit:" + path
|
||||
@@ -261,8 +262,8 @@ func (DefaultApprovalPolicy) EvaluateApproval(_ context.Context, req ApprovalReq
|
||||
return evaluateWebApproval(req)
|
||||
case "edit":
|
||||
return evaluateEditApproval(req)
|
||||
case "bash":
|
||||
return evaluateBashApproval(req)
|
||||
case "bash", "powershell":
|
||||
return evaluateShellApproval(req)
|
||||
default:
|
||||
return ApprovalEvaluation{
|
||||
RequirePrompt: true,
|
||||
@@ -351,10 +352,10 @@ func sanitizeApprovalDisplay(value string) string {
|
||||
return value
|
||||
}
|
||||
|
||||
func evaluateBashApproval(req ApprovalRequest) ApprovalEvaluation {
|
||||
func evaluateShellApproval(req ApprovalRequest) ApprovalEvaluation {
|
||||
command, ok := stringApprovalArg(req.Args, "command")
|
||||
if !ok || strings.TrimSpace(command) == "" {
|
||||
return denyApproval("bash", ApprovalRiskHigh, "missing command argument")
|
||||
return denyApproval(req.ToolName, ApprovalRiskHigh, "missing command argument")
|
||||
}
|
||||
|
||||
risk, reasons := classifyBashCommand(command)
|
||||
@@ -364,9 +365,9 @@ func evaluateBashApproval(req ApprovalRequest) ApprovalEvaluation {
|
||||
return ApprovalEvaluation{
|
||||
RequirePrompt: true,
|
||||
Risk: risk,
|
||||
Summary: "Bash wants to run a command",
|
||||
Summary: fmt.Sprintf("%s wants to run a command", ToolDisplayName(req.ToolName)),
|
||||
Reasons: reasons,
|
||||
SessionKey: "bash:" + command,
|
||||
SessionKey: req.ToolName + ":" + command,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -380,7 +381,7 @@ func denyApproval(toolName string, risk ApprovalRisk, reason string) ApprovalEva
|
||||
}
|
||||
}
|
||||
|
||||
// Bash approval is static analysis of model-generated commands, not a sandbox.
|
||||
// Shell approval is static analysis of model-generated commands, not a sandbox.
|
||||
// Globs, aliases, environment, and runtime shell state are intentionally out of scope.
|
||||
func classifyBashCommand(command string) (ApprovalRisk, []string) {
|
||||
classifier := bashClassifier{risk: ApprovalRiskMedium}
|
||||
|
||||
@@ -223,6 +223,23 @@ func TestBashApprovalClassifiesHighRiskShell(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestPowerShellApprovalUsesShellPolicy(t *testing.T) {
|
||||
evaluation := DefaultApprovalPolicy{}.EvaluateApproval(context.Background(), ApprovalRequest{
|
||||
ToolName: "powershell",
|
||||
Args: map[string]any{"command": "Remove-Item -Recurse tmp"},
|
||||
})
|
||||
|
||||
if !evaluation.RequirePrompt {
|
||||
t.Fatal("powershell should require prompt")
|
||||
}
|
||||
if evaluation.Summary != "PowerShell wants to run a command" {
|
||||
t.Fatalf("summary = %q", evaluation.Summary)
|
||||
}
|
||||
if evaluation.SessionKey != "powershell:Remove-Item -Recurse tmp" {
|
||||
t.Fatalf("session key = %q", evaluation.SessionKey)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBashApprovalClassifiesDynamicShellEvasions(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
|
||||
@@ -18,6 +18,8 @@ func ToolDisplayName(name string) string {
|
||||
return "Web Fetch"
|
||||
case "bash":
|
||||
return "Bash"
|
||||
case "powershell":
|
||||
return "PowerShell"
|
||||
case "read":
|
||||
return "Read"
|
||||
case "list":
|
||||
@@ -39,7 +41,7 @@ func ToolInvocationLabel(name string, args map[string]any) string {
|
||||
displayName := ToolDisplayName(name)
|
||||
for _, key := range []string{"query", "url", "command", "path", "name"} {
|
||||
if value, ok := displayStringArg(args, key); ok {
|
||||
if name == "bash" && key == "command" {
|
||||
if IsShellToolName(name) && key == "command" {
|
||||
value = truncateDisplayRunes(value, maxToolInvocationCommandRunes)
|
||||
}
|
||||
return fmt.Sprintf("%s(%s)", displayName, strconv.Quote(value))
|
||||
@@ -51,6 +53,11 @@ func ToolInvocationLabel(name string, args map[string]any) string {
|
||||
return fmt.Sprintf("%s(%s)", displayName, formatDisplayArgs(args))
|
||||
}
|
||||
|
||||
// IsShellToolName reports whether name identifies a platform shell tool.
|
||||
func IsShellToolName(name string) bool {
|
||||
return name == "bash" || name == "powershell"
|
||||
}
|
||||
|
||||
func displayStringArg(args map[string]any, key string) (string, bool) {
|
||||
value, ok := args[key].(string)
|
||||
if !ok || strings.TrimSpace(value) == "" {
|
||||
|
||||
@@ -22,3 +22,12 @@ func TestToolInvocationLabelCountsRunes(t *testing.T) {
|
||||
t.Fatalf("label = %q, want %q", label, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestToolInvocationLabelTruncatesLongPowerShellCommand(t *testing.T) {
|
||||
command := strings.Repeat("a", 101)
|
||||
label := ToolInvocationLabel("powershell", map[string]any{"command": command})
|
||||
want := `PowerShell("` + strings.Repeat("a", 100) + `...")`
|
||||
if label != want {
|
||||
t.Fatalf("label = %q, want %q", label, want)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,18 +26,18 @@ func NewBash() *Bash {
|
||||
}
|
||||
|
||||
func (b *Bash) Name() string {
|
||||
return "bash"
|
||||
return shellToolName()
|
||||
}
|
||||
|
||||
func (b *Bash) Description() string {
|
||||
return "Execute a shell command on the system. Use this to run shell commands, inspect files, run tests, and perform development tasks."
|
||||
return shellToolDescription()
|
||||
}
|
||||
|
||||
func (b *Bash) Schema() api.ToolFunction {
|
||||
props := api.NewToolPropertiesMap()
|
||||
props.Set("command", api.ToolProperty{
|
||||
Type: api.PropertyType{"string"},
|
||||
Description: "The shell command to execute.",
|
||||
Description: shellCommandDescription(),
|
||||
})
|
||||
return api.ToolFunction{
|
||||
Name: b.Name(),
|
||||
|
||||
@@ -8,6 +8,18 @@ import (
|
||||
"syscall"
|
||||
)
|
||||
|
||||
func shellToolName() string {
|
||||
return "bash"
|
||||
}
|
||||
|
||||
func shellToolDescription() string {
|
||||
return "Execute a bash command on the system. Use this to inspect files, run tests, and perform development tasks."
|
||||
}
|
||||
|
||||
func shellCommandDescription() string {
|
||||
return "The bash command to execute."
|
||||
}
|
||||
|
||||
func newBashCommand(ctx context.Context, command, cwdPath string) *exec.Cmd {
|
||||
script := command + "\n__ollama_status=$?\npwd -P > " + shellQuote(cwdPath) + "\nexit $__ollama_status"
|
||||
cmd := exec.CommandContext(ctx, "bash", "-c", script)
|
||||
|
||||
@@ -14,6 +14,18 @@ import (
|
||||
|
||||
var bashJobHandles sync.Map
|
||||
|
||||
func shellToolName() string {
|
||||
return "powershell"
|
||||
}
|
||||
|
||||
func shellToolDescription() string {
|
||||
return "Execute a PowerShell command on the system. Use this to inspect files, run tests, and perform development tasks."
|
||||
}
|
||||
|
||||
func shellCommandDescription() string {
|
||||
return "The PowerShell command to execute."
|
||||
}
|
||||
|
||||
func newBashCommand(ctx context.Context, command, cwdPath string) *exec.Cmd {
|
||||
return exec.CommandContext(
|
||||
ctx,
|
||||
|
||||
@@ -768,7 +768,7 @@ func agentToolsRegistry(ctx context.Context, client *api.Client, modelName strin
|
||||
}
|
||||
|
||||
registry := coreagent.NewRegistry()
|
||||
if os.Getenv("OLLAMA_AGENT_DISABLE_BASH") == "" {
|
||||
if os.Getenv("OLLAMA_AGENT_DISABLE_SHELL") == "" {
|
||||
registry.Register(agenttools.NewBash())
|
||||
}
|
||||
registry.Register(agenttools.NewRead())
|
||||
|
||||
@@ -16,6 +16,7 @@ import (
|
||||
|
||||
coreagent "github.com/ollama/ollama/agent"
|
||||
"github.com/ollama/ollama/agent/skills"
|
||||
agenttools "github.com/ollama/ollama/agent/tools"
|
||||
"github.com/ollama/ollama/api"
|
||||
agentchat "github.com/ollama/ollama/cmd/tui/chat"
|
||||
"github.com/ollama/ollama/envconfig"
|
||||
@@ -208,7 +209,7 @@ func captureStderr(t *testing.T, fn func()) string {
|
||||
|
||||
func TestAgentToolsRegistryNoCloudDisablesWebTools(t *testing.T) {
|
||||
t.Setenv("OLLAMA_NO_CLOUD", "1")
|
||||
t.Setenv("OLLAMA_AGENT_DISABLE_BASH", "")
|
||||
t.Setenv("OLLAMA_AGENT_DISABLE_SHELL", "")
|
||||
t.Setenv("OLLAMA_AGENT_DISABLE_WEBSEARCH", "")
|
||||
|
||||
statusCalls := 0
|
||||
@@ -235,7 +236,7 @@ func TestAgentToolsRegistryNoCloudDisablesWebTools(t *testing.T) {
|
||||
if registry == nil {
|
||||
t.Fatal("registry = nil, want local tools")
|
||||
}
|
||||
if !registry.Has("bash") || !registry.Has("read") || !registry.Has("edit") {
|
||||
if !registry.Has(agenttools.NewBash().Name()) || !registry.Has("read") || !registry.Has("edit") {
|
||||
t.Fatalf("local tools missing: %v", registry.Names())
|
||||
}
|
||||
if registry.Has("list") {
|
||||
|
||||
@@ -222,13 +222,14 @@ func (m chatModel) renderApprovalPromptLines(width int) []string {
|
||||
}
|
||||
|
||||
func approvalRequestDetail(request coreagent.ApprovalRequest, width int) string {
|
||||
switch request.ToolName {
|
||||
case "bash":
|
||||
if coreagent.IsShellToolName(request.ToolName) {
|
||||
command, ok := rawStringArg(request.Args, "command")
|
||||
if !ok {
|
||||
return ""
|
||||
}
|
||||
return strings.Join(wrapChatText("$ "+command, width), "\n")
|
||||
return strings.Join(wrapChatText(shellPromptPrefix(request.ToolName)+command, width), "\n")
|
||||
}
|
||||
switch request.ToolName {
|
||||
case "edit":
|
||||
path, ok := rawStringArg(request.Args, "path")
|
||||
if !ok {
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"hash/fnv"
|
||||
"runtime"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
@@ -236,7 +237,11 @@ func Run(ctx context.Context, opts Options) (*Result, error) {
|
||||
m.preloadingModel = strings.TrimSpace(opts.Model)
|
||||
}
|
||||
|
||||
p := tea.NewProgram(m, tea.WithReportFocus(), tea.WithMouseCellMotion())
|
||||
programOptions := []tea.ProgramOption{tea.WithReportFocus()}
|
||||
if runtime.GOOS != "windows" {
|
||||
programOptions = append(programOptions, tea.WithMouseCellMotion())
|
||||
}
|
||||
p := tea.NewProgram(m, programOptions...)
|
||||
finalModel, err := p.Run()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -429,6 +434,9 @@ func (m *chatModel) enterFullScreen() {
|
||||
}
|
||||
|
||||
func (m chatModel) updateMouse(msg tea.MouseMsg) (tea.Model, tea.Cmd) {
|
||||
if runtime.GOOS == "windows" {
|
||||
return m, nil
|
||||
}
|
||||
if m.modelPicker != nil {
|
||||
switch msg.Type {
|
||||
case tea.MouseWheelUp:
|
||||
|
||||
@@ -1076,11 +1076,12 @@ func renderToolCallDetailLines(entry chatEntry, width int) []string {
|
||||
if len(entry.args) == 0 {
|
||||
return nil
|
||||
}
|
||||
switch entry.detail {
|
||||
case "bash":
|
||||
if coreagent.IsShellToolName(entry.detail) {
|
||||
if command, ok := rawStringArg(entry.args, "command"); ok {
|
||||
return renderToolCallArgLine("$ "+command, width)
|
||||
return renderToolCallArgLine(shellPromptPrefix(entry.detail)+command, width)
|
||||
}
|
||||
}
|
||||
switch entry.detail {
|
||||
case "web_search":
|
||||
if query, ok := rawStringArg(entry.args, "query"); ok {
|
||||
return renderToolCallArgLine("query: "+query, width)
|
||||
@@ -1093,6 +1094,13 @@ func renderToolCallDetailLines(entry chatEntry, width int) []string {
|
||||
return renderToolCallArgs(entry.args, width)
|
||||
}
|
||||
|
||||
func shellPromptPrefix(toolName string) string {
|
||||
if toolName == "powershell" {
|
||||
return "PS> "
|
||||
}
|
||||
return "$ "
|
||||
}
|
||||
|
||||
func renderToolCallArgs(args map[string]any, width int) []string {
|
||||
keys := make([]string, 0, len(args))
|
||||
for key := range args {
|
||||
|
||||
Reference in New Issue
Block a user