From dd2390f8d5a7d587a6fb792d08349b2a51cc5abb Mon Sep 17 00:00:00 2001 From: Kohei Date: Sat, 28 Feb 2026 12:06:00 +0900 Subject: [PATCH] fix: separate gateway setup mode to prevent crash when config.json missing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When config.json doesn't exist, gateway now starts in minimal setup mode (HTTP + WebSocket only) instead of crashing on CreateProvider with empty LLM.Model. Step1 (init) no longer triggers restart — only Step5 (complete) does. handleSetupComplete now reads config from disk instead of in-memory to preserve gateway settings written by handleSetupInit. Co-Authored-By: Claude Opus 4.6 --- cmd/clawdroid/main.go | 91 +++++++++++++++++++++++++++++++++++++++---- pkg/gateway/setup.go | 15 ++----- 2 files changed, 87 insertions(+), 19 deletions(-) diff --git a/cmd/clawdroid/main.go b/cmd/clawdroid/main.go index 6dc7ca97a..d894c0169 100644 --- a/cmd/clawdroid/main.go +++ b/cmd/clawdroid/main.go @@ -451,6 +451,12 @@ func gatewayCmd() { os.Exit(1) } + // If config.json does not exist, start in setup mode (minimal gateway) + if _, err := os.Stat(configPath); os.IsNotExist(err) { + gatewaySetupMode(cfg, configPath) + return + } + provider, err := providers.CreateProvider(cfg) if err != nil { fmt.Printf("Error creating provider: %v\n", err) @@ -585,15 +591,84 @@ func gatewayCmd() { fmt.Println("✓ Gateway stopped") if restart { - exe, err := os.Executable() - if err != nil { - fmt.Printf("Error finding executable: %v\n", err) - os.Exit(1) - } - if err := syscall.Exec(exe, os.Args, os.Environ()); err != nil { - fmt.Printf("Error restarting: %v\n", err) - os.Exit(1) + execRestart() + } +} + +func execRestart() { + exe, err := os.Executable() + if err != nil { + fmt.Printf("Error finding executable: %v\n", err) + os.Exit(1) + } + if err := syscall.Exec(exe, os.Args, os.Environ()); err != nil { + fmt.Printf("Error restarting: %v\n", err) + os.Exit(1) + } +} + +// gatewaySetupMode starts a minimal gateway with only HTTP + WebSocket. +// Provider, AgentLoop, CronService, and HeartbeatService are not started. +func gatewaySetupMode(cfg *config.Config, configPath string) { + fmt.Println("\n⚙ Starting in setup mode (config.json not found)") + + restartCh := make(chan struct{}, 1) + + gwServer := gateway.NewServer(cfg, configPath, func() { + select { + case restartCh <- struct{}{}: + default: } + }) + if err := gwServer.Start(); err != nil { + fmt.Printf("Error starting gateway HTTP server: %v\n", err) + os.Exit(1) + } + fmt.Printf("✓ Config API started on 127.0.0.1:%d\n", cfg.Gateway.Port) + + msgBus := bus.NewMessageBus() + channelManager, err := channels.NewManager(cfg, msgBus, configPath) + if err != nil { + fmt.Printf("Error creating channel manager: %v\n", err) + os.Exit(1) + } + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + if err := channelManager.StartAll(ctx); err != nil { + fmt.Printf("Error starting channels: %v\n", err) + } + + enabledChannels := channelManager.GetEnabledChannels() + if len(enabledChannels) > 0 { + fmt.Printf("✓ Channels enabled: %s\n", enabledChannels) + } + + fmt.Println("✓ Setup mode ready — waiting for setup wizard") + fmt.Println("Press Ctrl+C to stop") + + sigChan := make(chan os.Signal, 1) + signal.Notify(sigChan, os.Interrupt) + + restart := false + select { + case <-sigChan: + case <-restartCh: + restart = true + fmt.Println("\nRestarting after setup complete...") + } + + fmt.Println("\nShutting down...") + cancel() + shutdownCtx, shutdownCancel := context.WithTimeout(context.Background(), 5*time.Second) + defer shutdownCancel() + gwServer.Stop(shutdownCtx) + channelManager.StopAll(shutdownCtx) + fmt.Println("✓ Gateway stopped") + + if restart { + execRestart() } } diff --git a/pkg/gateway/setup.go b/pkg/gateway/setup.go index 42575c06d..7fa354c05 100644 --- a/pkg/gateway/setup.go +++ b/pkg/gateway/setup.go @@ -51,23 +51,16 @@ func (s *Server) handleSetupInit(w http.ResponseWriter, r *http.Request) { logger.InfoC("gateway", "Initial config created via setup wizard") writeJSON(w, http.StatusOK, map[string]string{"status": "ok"}) - - if s.onRestart != nil { - go func() { - time.Sleep(100 * time.Millisecond) - s.onRestart() - }() - } } // handleSetupComplete merges additional settings into an existing config.json. // PUT /api/setup/complete — authentication required. func (s *Server) handleSetupComplete(w http.ResponseWriter, r *http.Request) { - s.cfg.RLock() - currentData, err := json.Marshal(s.cfg) - s.cfg.RUnlock() + // Read current config from disk (includes init step's gateway settings) + // rather than s.cfg which may still be DefaultConfig in setup mode. + currentData, err := os.ReadFile(s.configPath) if err != nil { - writeJSONError(w, http.StatusInternalServerError, "failed to read current config") + writeJSONError(w, http.StatusInternalServerError, "failed to read config file") return }