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

@ -33,6 +33,7 @@ type providerSelection struct {
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
} }