fix: support keyless Ollama and harden exec working_dir checks
This commit is contained in:
parent
80c8b57533
commit
9f6b5a73a5
3 changed files with 65 additions and 13 deletions
|
|
@ -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 == "" {
|
||||||
|
|
|
||||||
|
|
@ -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) {
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue