Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions pkg/providers/factory.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,10 @@ func resolveProviderSelection(cfg *config.Config) (providerSelection, error) {
providerName := strings.ToLower(cfg.Agents.Defaults.Provider)
lowerModel := strings.ToLower(model)

if providerName == "" && model == "" {
return providerSelection{}, fmt.Errorf("no model configured: agents.defaults.model is empty")
}

sel := providerSelection{
providerType: providerTypeHTTPCompat,
model: model,
Expand Down
32 changes: 26 additions & 6 deletions web/backend/api/gateway.go
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,12 @@ func (h *Handler) startGatewayLocked() (int, error) {
execPath := findPicoclawBinary()

cmd := exec.Command(execPath, "gateway")
// Forward the launcher's config path via the environment variable that
// GetConfigPath() already reads, so the gateway sub-process uses the same
// config file without requiring a --config flag on the gateway subcommand.
if h.configPath != "" {
cmd.Env = append(os.Environ(), "PICOCLAW_CONFIG="+h.configPath)
}

stdoutPipe, err := cmd.StdoutPipe()
if err != nil {
Expand Down Expand Up @@ -530,18 +536,32 @@ func (h *Handler) currentGatewayStatus() string {
}

// findPicoclawBinary locates the picoclaw executable.
// Tries the same directory as the current executable first, then falls back to $PATH.
// Search order:
// 1. PICOCLAW_BINARY environment variable (explicit override)
// 2. Same directory as the current executable
// 3. Falls back to "picoclaw" and relies on $PATH
func findPicoclawBinary() string {
if exe, err := os.Executable(); err == nil {
dir := filepath.Dir(exe)
candidate := filepath.Join(dir, "picoclaw")
if runtime.GOOS == "windows" {
candidate += ".exe"
binaryName := "picoclaw"
if runtime.GOOS == "windows" {
binaryName = "picoclaw.exe"
}

// 1. Explicit override via environment variable
if p := os.Getenv("PICOCLAW_BINARY"); p != "" {
if info, _ := os.Stat(p); info != nil && !info.IsDir() {
return p
}
}

// 2. Same directory as the launcher executable
if exe, err := os.Executable(); err == nil {
candidate := filepath.Join(filepath.Dir(exe), binaryName)
if info, err := os.Stat(candidate); err == nil && !info.IsDir() {
return candidate
}
}

// 3. Fall back to PATH lookup
return "picoclaw"
}

Expand Down
28 changes: 28 additions & 0 deletions web/backend/api/gateway_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"encoding/json"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"strings"
"testing"
Expand Down Expand Up @@ -120,3 +121,30 @@ func TestGatewayStatusIncludesStartConditionWhenNotReady(t *testing.T) {
t.Fatalf("gateway_start_reason missing or not string: %#v", body["gateway_start_reason"])
}
}

func TestFindPicoclawBinary_EnvOverride(t *testing.T) {
// Create a temporary file to act as the mock binary
tmpDir := t.TempDir()
mockBinary := filepath.Join(tmpDir, "picoclaw-mock")
if err := os.WriteFile(mockBinary, []byte("mock"), 0o755); err != nil {
t.Fatalf("WriteFile() error = %v", err)
}

t.Setenv("PICOCLAW_BINARY", mockBinary)

got := findPicoclawBinary()
if got != mockBinary {
t.Errorf("findPicoclawBinary() = %q, want %q", got, mockBinary)
}
}

func TestFindPicoclawBinary_EnvOverride_InvalidPath(t *testing.T) {
// When PICOCLAW_BINARY points to a non-existent path, fall through to next strategy
t.Setenv("PICOCLAW_BINARY", "/nonexistent/picoclaw-binary")

got := findPicoclawBinary()
// Should not return the invalid path; falls back to "picoclaw" or another found path
if got == "/nonexistent/picoclaw-binary" {
t.Errorf("findPicoclawBinary() returned invalid env path %q, expected fallback", got)
}
}
Loading