fix: separate gateway setup mode to prevent crash when config.json missing

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 <noreply@anthropic.com>
This commit is contained in:
Kohei 2026-02-28 12:06:00 +09:00
parent 4a899b92c2
commit dd2390f8d5
2 changed files with 87 additions and 19 deletions

View file

@ -451,6 +451,12 @@ func gatewayCmd() {
os.Exit(1) 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) provider, err := providers.CreateProvider(cfg)
if err != nil { if err != nil {
fmt.Printf("Error creating provider: %v\n", err) fmt.Printf("Error creating provider: %v\n", err)
@ -585,6 +591,11 @@ func gatewayCmd() {
fmt.Println("✓ Gateway stopped") fmt.Println("✓ Gateway stopped")
if restart { if restart {
execRestart()
}
}
func execRestart() {
exe, err := os.Executable() exe, err := os.Executable()
if err != nil { if err != nil {
fmt.Printf("Error finding executable: %v\n", err) fmt.Printf("Error finding executable: %v\n", err)
@ -594,6 +605,70 @@ func gatewayCmd() {
fmt.Printf("Error restarting: %v\n", err) fmt.Printf("Error restarting: %v\n", err)
os.Exit(1) 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()
} }
} }

View file

@ -51,23 +51,16 @@ func (s *Server) handleSetupInit(w http.ResponseWriter, r *http.Request) {
logger.InfoC("gateway", "Initial config created via setup wizard") logger.InfoC("gateway", "Initial config created via setup wizard")
writeJSON(w, http.StatusOK, map[string]string{"status": "ok"}) 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. // handleSetupComplete merges additional settings into an existing config.json.
// PUT /api/setup/complete — authentication required. // PUT /api/setup/complete — authentication required.
func (s *Server) handleSetupComplete(w http.ResponseWriter, r *http.Request) { func (s *Server) handleSetupComplete(w http.ResponseWriter, r *http.Request) {
s.cfg.RLock() // Read current config from disk (includes init step's gateway settings)
currentData, err := json.Marshal(s.cfg) // rather than s.cfg which may still be DefaultConfig in setup mode.
s.cfg.RUnlock() currentData, err := os.ReadFile(s.configPath)
if err != nil { if err != nil {
writeJSONError(w, http.StatusInternalServerError, "failed to read current config") writeJSONError(w, http.StatusInternalServerError, "failed to read config file")
return return
} }