fix: support keyless Ollama and harden exec working_dir checks

This commit is contained in:
Kevin Perez 2026-02-18 15:31:32 +04:00
parent 80c8b57533
commit 9f6b5a73a5
No known key found for this signature in database
3 changed files with 65 additions and 13 deletions

View file

@ -25,14 +25,15 @@ const (
) )
type providerSelection struct { type providerSelection struct {
providerType providerType providerType providerType
apiKey string apiKey string
apiBase string apiBase string
proxy string proxy string
model string model string
workspace string workspace string
connectMode string connectMode string
enableWebSearch bool enableWebSearch bool
allowEmptyAPIKey bool
} }
func resolveProviderSelection(cfg *config.Config) (providerSelection, error) { func resolveProviderSelection(cfg *config.Config) (providerSelection, error) {
@ -172,6 +173,14 @@ func resolveProviderSelection(cfg *config.Config) (providerSelection, error) {
sel.model = "deepseek-chat" sel.model = "deepseek-chat"
} }
} }
case "ollama":
sel.apiKey = cfg.Providers.Ollama.APIKey
sel.apiBase = cfg.Providers.Ollama.APIBase
sel.proxy = cfg.Providers.Ollama.Proxy
if sel.apiBase == "" {
sel.apiBase = "http://localhost:11434/v1"
}
sel.allowEmptyAPIKey = true
case "github_copilot", "copilot": case "github_copilot", "copilot":
sel.providerType = providerTypeGitHubCopilot sel.providerType = providerTypeGitHubCopilot
if cfg.Providers.GitHubCopilot.APIBase != "" { if cfg.Providers.GitHubCopilot.APIBase != "" {
@ -268,13 +277,14 @@ func resolveProviderSelection(cfg *config.Config) (providerSelection, error) {
if sel.apiBase == "" { if sel.apiBase == "" {
sel.apiBase = "https://integrate.api.nvidia.com/v1" sel.apiBase = "https://integrate.api.nvidia.com/v1"
} }
case (strings.Contains(lowerModel, "ollama") || strings.HasPrefix(model, "ollama/")) && cfg.Providers.Ollama.APIKey != "": case (strings.Contains(lowerModel, "ollama") || strings.HasPrefix(model, "ollama/")) && (cfg.Providers.Ollama.APIBase != "" || cfg.Providers.Ollama.APIKey != ""):
sel.apiKey = cfg.Providers.Ollama.APIKey sel.apiKey = cfg.Providers.Ollama.APIKey
sel.apiBase = cfg.Providers.Ollama.APIBase sel.apiBase = cfg.Providers.Ollama.APIBase
sel.proxy = cfg.Providers.Ollama.Proxy sel.proxy = cfg.Providers.Ollama.Proxy
if sel.apiBase == "" { if sel.apiBase == "" {
sel.apiBase = "http://localhost:11434/v1" sel.apiBase = "http://localhost:11434/v1"
} }
sel.allowEmptyAPIKey = true
case cfg.Providers.VLLM.APIBase != "": case cfg.Providers.VLLM.APIBase != "":
sel.apiKey = cfg.Providers.VLLM.APIKey sel.apiKey = cfg.Providers.VLLM.APIKey
sel.apiBase = cfg.Providers.VLLM.APIBase sel.apiBase = cfg.Providers.VLLM.APIBase
@ -295,7 +305,7 @@ func resolveProviderSelection(cfg *config.Config) (providerSelection, error) {
} }
if sel.providerType == providerTypeHTTPCompat { if sel.providerType == providerTypeHTTPCompat {
if sel.apiKey == "" && !strings.HasPrefix(model, "bedrock/") { if sel.apiKey == "" && !strings.HasPrefix(model, "bedrock/") && !sel.allowEmptyAPIKey {
return providerSelection{}, fmt.Errorf("no API key configured for provider (model: %s)", model) return providerSelection{}, fmt.Errorf("no API key configured for provider (model: %s)", model)
} }
if sel.apiBase == "" { if sel.apiBase == "" {

View file

@ -135,6 +135,24 @@ func TestResolveProviderSelection(t *testing.T) {
wantType: providerTypeHTTPCompat, wantType: providerTypeHTTPCompat,
wantAPIBase: "http://localhost:11434/v1", wantAPIBase: "http://localhost:11434/v1",
}, },
{
name: "explicit ollama provider allows empty key and uses default base",
setup: func(cfg *config.Config) {
cfg.Agents.Defaults.Provider = "ollama"
cfg.Agents.Defaults.Model = "qwen2.5:14b"
},
wantType: providerTypeHTTPCompat,
wantAPIBase: "http://localhost:11434/v1",
},
{
name: "ollama model allows api base without key",
setup: func(cfg *config.Config) {
cfg.Agents.Defaults.Model = "ollama/qwen2.5:14b"
cfg.Providers.Ollama.APIBase = "http://localhost:11434/v1"
},
wantType: providerTypeHTTPCompat,
wantAPIBase: "http://localhost:11434/v1",
},
{ {
name: "moonshot model keeps proxy and default base", name: "moonshot model keeps proxy and default base",
setup: func(cfg *config.Config) { setup: func(cfg *config.Config) {

View file

@ -142,7 +142,8 @@ func (t *ExecTool) Execute(ctx context.Context, args map[string]any) *ToolResult
return ErrorResult("command is required") return ErrorResult("command is required")
} }
cwd := t.workingDir workspaceRoot := t.workingDir
cwd := workspaceRoot
if wd, ok := args["working_dir"].(string); ok && wd != "" { if wd, ok := args["working_dir"].(string); ok && wd != "" {
if t.restrictToWorkspace && t.workingDir != "" { if t.restrictToWorkspace && t.workingDir != "" {
resolvedWD, err := validatePath(wd, t.workingDir, true) resolvedWD, err := validatePath(wd, t.workingDir, true)
@ -150,6 +151,10 @@ func (t *ExecTool) Execute(ctx context.Context, args map[string]any) *ToolResult
return ErrorResult("Command blocked by safety guard (" + err.Error() + ")") return ErrorResult("Command blocked by safety guard (" + err.Error() + ")")
} }
cwd = resolvedWD cwd = resolvedWD
} else if filepath.IsAbs(wd) {
cwd = wd
} else if workspaceRoot != "" {
cwd = filepath.Join(workspaceRoot, wd)
} else { } else {
cwd = wd cwd = wd
} }
@ -162,6 +167,20 @@ func (t *ExecTool) Execute(ctx context.Context, args map[string]any) *ToolResult
} }
} }
if absCwd, err := filepath.Abs(cwd); err == nil {
cwd = absCwd
}
if t.restrictToWorkspace && workspaceRoot != "" {
absWorkspace, err := filepath.Abs(workspaceRoot)
if err == nil {
rel, err := filepath.Rel(absWorkspace, cwd)
if err != nil || rel == ".." || strings.HasPrefix(rel, ".."+string(filepath.Separator)) {
return ErrorResult("working_dir must be within workspace")
}
}
}
if guardError := t.guardCommand(command, cwd); guardError != "" { if guardError := t.guardCommand(command, cwd); guardError != "" {
return ErrorResult(guardError) return ErrorResult(guardError)
} }
@ -285,7 +304,12 @@ func (t *ExecTool) guardCommand(command, cwd string) string {
return "Command blocked by safety guard (path traversal detected)" return "Command blocked by safety guard (path traversal detected)"
} }
cwdPath, err := filepath.Abs(cwd) basePath := cwd
if t.workingDir != "" {
basePath = t.workingDir
}
basePath, err := filepath.Abs(basePath)
if err != nil { if err != nil {
return "" return ""
} }
@ -299,7 +323,7 @@ func (t *ExecTool) guardCommand(command, cwd string) string {
continue continue
} }
rel, err := filepath.Rel(cwdPath, p) rel, err := filepath.Rel(basePath, p)
if err != nil { if err != nil {
continue continue
} }