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 {
|
func approvalSessionKey(req ApprovalRequest) string {
|
||||||
switch req.ToolName {
|
if IsShellToolName(req.ToolName) {
|
||||||
case "bash":
|
|
||||||
if command, ok := stringApprovalArg(req.Args, "command"); ok {
|
if command, ok := stringApprovalArg(req.Args, "command"); ok {
|
||||||
return "bash:" + command
|
return req.ToolName + ":" + command
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
switch req.ToolName {
|
||||||
case "edit":
|
case "edit":
|
||||||
if path, ok := stringApprovalArg(req.Args, "path"); ok {
|
if path, ok := stringApprovalArg(req.Args, "path"); ok {
|
||||||
return "edit:" + path
|
return "edit:" + path
|
||||||
@@ -261,8 +262,8 @@ func (DefaultApprovalPolicy) EvaluateApproval(_ context.Context, req ApprovalReq
|
|||||||
return evaluateWebApproval(req)
|
return evaluateWebApproval(req)
|
||||||
case "edit":
|
case "edit":
|
||||||
return evaluateEditApproval(req)
|
return evaluateEditApproval(req)
|
||||||
case "bash":
|
case "bash", "powershell":
|
||||||
return evaluateBashApproval(req)
|
return evaluateShellApproval(req)
|
||||||
default:
|
default:
|
||||||
return ApprovalEvaluation{
|
return ApprovalEvaluation{
|
||||||
RequirePrompt: true,
|
RequirePrompt: true,
|
||||||
@@ -351,10 +352,10 @@ func sanitizeApprovalDisplay(value string) string {
|
|||||||
return value
|
return value
|
||||||
}
|
}
|
||||||
|
|
||||||
func evaluateBashApproval(req ApprovalRequest) ApprovalEvaluation {
|
func evaluateShellApproval(req ApprovalRequest) ApprovalEvaluation {
|
||||||
command, ok := stringApprovalArg(req.Args, "command")
|
command, ok := stringApprovalArg(req.Args, "command")
|
||||||
if !ok || strings.TrimSpace(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)
|
risk, reasons := classifyBashCommand(command)
|
||||||
@@ -364,9 +365,9 @@ func evaluateBashApproval(req ApprovalRequest) ApprovalEvaluation {
|
|||||||
return ApprovalEvaluation{
|
return ApprovalEvaluation{
|
||||||
RequirePrompt: true,
|
RequirePrompt: true,
|
||||||
Risk: risk,
|
Risk: risk,
|
||||||
Summary: "Bash wants to run a command",
|
Summary: fmt.Sprintf("%s wants to run a command", ToolDisplayName(req.ToolName)),
|
||||||
Reasons: reasons,
|
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.
|
// Globs, aliases, environment, and runtime shell state are intentionally out of scope.
|
||||||
func classifyBashCommand(command string) (ApprovalRisk, []string) {
|
func classifyBashCommand(command string) (ApprovalRisk, []string) {
|
||||||
classifier := bashClassifier{risk: ApprovalRiskMedium}
|
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) {
|
func TestBashApprovalClassifiesDynamicShellEvasions(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
|
|||||||
@@ -18,6 +18,8 @@ func ToolDisplayName(name string) string {
|
|||||||
return "Web Fetch"
|
return "Web Fetch"
|
||||||
case "bash":
|
case "bash":
|
||||||
return "Bash"
|
return "Bash"
|
||||||
|
case "powershell":
|
||||||
|
return "PowerShell"
|
||||||
case "read":
|
case "read":
|
||||||
return "Read"
|
return "Read"
|
||||||
case "list":
|
case "list":
|
||||||
@@ -39,7 +41,7 @@ func ToolInvocationLabel(name string, args map[string]any) string {
|
|||||||
displayName := ToolDisplayName(name)
|
displayName := ToolDisplayName(name)
|
||||||
for _, key := range []string{"query", "url", "command", "path", "name"} {
|
for _, key := range []string{"query", "url", "command", "path", "name"} {
|
||||||
if value, ok := displayStringArg(args, key); ok {
|
if value, ok := displayStringArg(args, key); ok {
|
||||||
if name == "bash" && key == "command" {
|
if IsShellToolName(name) && key == "command" {
|
||||||
value = truncateDisplayRunes(value, maxToolInvocationCommandRunes)
|
value = truncateDisplayRunes(value, maxToolInvocationCommandRunes)
|
||||||
}
|
}
|
||||||
return fmt.Sprintf("%s(%s)", displayName, strconv.Quote(value))
|
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))
|
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) {
|
func displayStringArg(args map[string]any, key string) (string, bool) {
|
||||||
value, ok := args[key].(string)
|
value, ok := args[key].(string)
|
||||||
if !ok || strings.TrimSpace(value) == "" {
|
if !ok || strings.TrimSpace(value) == "" {
|
||||||
|
|||||||
@@ -22,3 +22,12 @@ func TestToolInvocationLabelCountsRunes(t *testing.T) {
|
|||||||
t.Fatalf("label = %q, want %q", label, want)
|
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 {
|
func (b *Bash) Name() string {
|
||||||
return "bash"
|
return shellToolName()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *Bash) Description() string {
|
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 {
|
func (b *Bash) Schema() api.ToolFunction {
|
||||||
props := api.NewToolPropertiesMap()
|
props := api.NewToolPropertiesMap()
|
||||||
props.Set("command", api.ToolProperty{
|
props.Set("command", api.ToolProperty{
|
||||||
Type: api.PropertyType{"string"},
|
Type: api.PropertyType{"string"},
|
||||||
Description: "The shell command to execute.",
|
Description: shellCommandDescription(),
|
||||||
})
|
})
|
||||||
return api.ToolFunction{
|
return api.ToolFunction{
|
||||||
Name: b.Name(),
|
Name: b.Name(),
|
||||||
|
|||||||
@@ -8,6 +8,18 @@ import (
|
|||||||
"syscall"
|
"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 {
|
func newBashCommand(ctx context.Context, command, cwdPath string) *exec.Cmd {
|
||||||
script := command + "\n__ollama_status=$?\npwd -P > " + shellQuote(cwdPath) + "\nexit $__ollama_status"
|
script := command + "\n__ollama_status=$?\npwd -P > " + shellQuote(cwdPath) + "\nexit $__ollama_status"
|
||||||
cmd := exec.CommandContext(ctx, "bash", "-c", script)
|
cmd := exec.CommandContext(ctx, "bash", "-c", script)
|
||||||
|
|||||||
@@ -14,6 +14,18 @@ import (
|
|||||||
|
|
||||||
var bashJobHandles sync.Map
|
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 {
|
func newBashCommand(ctx context.Context, command, cwdPath string) *exec.Cmd {
|
||||||
return exec.CommandContext(
|
return exec.CommandContext(
|
||||||
ctx,
|
ctx,
|
||||||
|
|||||||
@@ -768,7 +768,7 @@ func agentToolsRegistry(ctx context.Context, client *api.Client, modelName strin
|
|||||||
}
|
}
|
||||||
|
|
||||||
registry := coreagent.NewRegistry()
|
registry := coreagent.NewRegistry()
|
||||||
if os.Getenv("OLLAMA_AGENT_DISABLE_BASH") == "" {
|
if os.Getenv("OLLAMA_AGENT_DISABLE_SHELL") == "" {
|
||||||
registry.Register(agenttools.NewBash())
|
registry.Register(agenttools.NewBash())
|
||||||
}
|
}
|
||||||
registry.Register(agenttools.NewRead())
|
registry.Register(agenttools.NewRead())
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import (
|
|||||||
|
|
||||||
coreagent "github.com/ollama/ollama/agent"
|
coreagent "github.com/ollama/ollama/agent"
|
||||||
"github.com/ollama/ollama/agent/skills"
|
"github.com/ollama/ollama/agent/skills"
|
||||||
|
agenttools "github.com/ollama/ollama/agent/tools"
|
||||||
"github.com/ollama/ollama/api"
|
"github.com/ollama/ollama/api"
|
||||||
agentchat "github.com/ollama/ollama/cmd/tui/chat"
|
agentchat "github.com/ollama/ollama/cmd/tui/chat"
|
||||||
"github.com/ollama/ollama/envconfig"
|
"github.com/ollama/ollama/envconfig"
|
||||||
@@ -208,7 +209,7 @@ func captureStderr(t *testing.T, fn func()) string {
|
|||||||
|
|
||||||
func TestAgentToolsRegistryNoCloudDisablesWebTools(t *testing.T) {
|
func TestAgentToolsRegistryNoCloudDisablesWebTools(t *testing.T) {
|
||||||
t.Setenv("OLLAMA_NO_CLOUD", "1")
|
t.Setenv("OLLAMA_NO_CLOUD", "1")
|
||||||
t.Setenv("OLLAMA_AGENT_DISABLE_BASH", "")
|
t.Setenv("OLLAMA_AGENT_DISABLE_SHELL", "")
|
||||||
t.Setenv("OLLAMA_AGENT_DISABLE_WEBSEARCH", "")
|
t.Setenv("OLLAMA_AGENT_DISABLE_WEBSEARCH", "")
|
||||||
|
|
||||||
statusCalls := 0
|
statusCalls := 0
|
||||||
@@ -235,7 +236,7 @@ func TestAgentToolsRegistryNoCloudDisablesWebTools(t *testing.T) {
|
|||||||
if registry == nil {
|
if registry == nil {
|
||||||
t.Fatal("registry = nil, want local tools")
|
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())
|
t.Fatalf("local tools missing: %v", registry.Names())
|
||||||
}
|
}
|
||||||
if registry.Has("list") {
|
if registry.Has("list") {
|
||||||
|
|||||||
@@ -222,13 +222,14 @@ func (m chatModel) renderApprovalPromptLines(width int) []string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func approvalRequestDetail(request coreagent.ApprovalRequest, width int) string {
|
func approvalRequestDetail(request coreagent.ApprovalRequest, width int) string {
|
||||||
switch request.ToolName {
|
if coreagent.IsShellToolName(request.ToolName) {
|
||||||
case "bash":
|
|
||||||
command, ok := rawStringArg(request.Args, "command")
|
command, ok := rawStringArg(request.Args, "command")
|
||||||
if !ok {
|
if !ok {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
return strings.Join(wrapChatText("$ "+command, width), "\n")
|
return strings.Join(wrapChatText(shellPromptPrefix(request.ToolName)+command, width), "\n")
|
||||||
|
}
|
||||||
|
switch request.ToolName {
|
||||||
case "edit":
|
case "edit":
|
||||||
path, ok := rawStringArg(request.Args, "path")
|
path, ok := rawStringArg(request.Args, "path")
|
||||||
if !ok {
|
if !ok {
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"hash/fnv"
|
"hash/fnv"
|
||||||
|
"runtime"
|
||||||
"slices"
|
"slices"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
@@ -236,7 +237,11 @@ func Run(ctx context.Context, opts Options) (*Result, error) {
|
|||||||
m.preloadingModel = strings.TrimSpace(opts.Model)
|
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()
|
finalModel, err := p.Run()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -429,6 +434,9 @@ func (m *chatModel) enterFullScreen() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (m chatModel) updateMouse(msg tea.MouseMsg) (tea.Model, tea.Cmd) {
|
func (m chatModel) updateMouse(msg tea.MouseMsg) (tea.Model, tea.Cmd) {
|
||||||
|
if runtime.GOOS == "windows" {
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
if m.modelPicker != nil {
|
if m.modelPicker != nil {
|
||||||
switch msg.Type {
|
switch msg.Type {
|
||||||
case tea.MouseWheelUp:
|
case tea.MouseWheelUp:
|
||||||
|
|||||||
@@ -1076,11 +1076,12 @@ func renderToolCallDetailLines(entry chatEntry, width int) []string {
|
|||||||
if len(entry.args) == 0 {
|
if len(entry.args) == 0 {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
switch entry.detail {
|
if coreagent.IsShellToolName(entry.detail) {
|
||||||
case "bash":
|
|
||||||
if command, ok := rawStringArg(entry.args, "command"); ok {
|
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":
|
case "web_search":
|
||||||
if query, ok := rawStringArg(entry.args, "query"); ok {
|
if query, ok := rawStringArg(entry.args, "query"); ok {
|
||||||
return renderToolCallArgLine("query: "+query, width)
|
return renderToolCallArgLine("query: "+query, width)
|
||||||
@@ -1093,6 +1094,13 @@ func renderToolCallDetailLines(entry chatEntry, width int) []string {
|
|||||||
return renderToolCallArgs(entry.args, width)
|
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 {
|
func renderToolCallArgs(args map[string]any, width int) []string {
|
||||||
keys := make([]string, 0, len(args))
|
keys := make([]string, 0, len(args))
|
||||||
for key := range args {
|
for key := range args {
|
||||||
|
|||||||
Reference in New Issue
Block a user