fix(web): auto-configure Pico channel on launcher startup
Export EnsurePicoChannel and reuse it during launcher and gateway startup so the Pico channel is initialized earlier with a generated token when needed. Also extend backend tests to cover startup-time Pico setup behavior and keep the setup path idempotent.
This commit is contained in:
parent
ffbcbea4dc
commit
dea99da7d9
4 changed files with 56 additions and 26 deletions
|
|
@ -407,7 +407,7 @@ func (h *Handler) startGatewayLocked(initialStatus string, existingPid int) (int
|
||||||
gateway.logs.Reset()
|
gateway.logs.Reset()
|
||||||
|
|
||||||
// Ensure Pico Channel is configured before starting gateway
|
// Ensure Pico Channel is configured before starting gateway
|
||||||
if _, err := h.ensurePicoChannel(""); err != nil {
|
if _, err := h.EnsurePicoChannel(""); err != nil {
|
||||||
logger.ErrorC("gateway", fmt.Sprintf("Warning: failed to ensure pico channel: %v", err))
|
logger.ErrorC("gateway", fmt.Sprintf("Warning: failed to ensure pico channel: %v", err))
|
||||||
// Non-fatal: gateway can still start without pico channel
|
// Non-fatal: gateway can still start without pico channel
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -90,14 +90,14 @@ func (h *Handler) handleRegenPicoToken(w http.ResponseWriter, r *http.Request) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// ensurePicoChannel enables the Pico channel with sane defaults if it isn't
|
// EnsurePicoChannel enables the Pico channel with sane defaults if it isn't
|
||||||
// already configured. Returns true when the config was modified.
|
// already configured. Returns true when the config was modified.
|
||||||
//
|
//
|
||||||
// callerOrigin is the Origin header from the setup request. If non-empty and
|
// callerOrigin is the Origin header from the setup request. If non-empty and
|
||||||
// no origins are configured yet, it's written as the allowed origin so the
|
// no origins are configured yet, it's written as the allowed origin so the
|
||||||
// WebSocket handshake works for whatever host the caller is on (LAN, custom
|
// WebSocket handshake works for whatever host the caller is on (LAN, custom
|
||||||
// port, etc.). Pass "" when there's no request context.
|
// port, etc.). Pass "" when there's no request context.
|
||||||
func (h *Handler) ensurePicoChannel(callerOrigin string) (bool, error) {
|
func (h *Handler) EnsurePicoChannel(callerOrigin string) (bool, error) {
|
||||||
cfg, err := config.LoadConfig(h.configPath)
|
cfg, err := config.LoadConfig(h.configPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, fmt.Errorf("failed to load config: %w", err)
|
return false, fmt.Errorf("failed to load config: %w", err)
|
||||||
|
|
@ -134,7 +134,7 @@ func (h *Handler) ensurePicoChannel(callerOrigin string) (bool, error) {
|
||||||
//
|
//
|
||||||
// POST /api/pico/setup
|
// POST /api/pico/setup
|
||||||
func (h *Handler) handlePicoSetup(w http.ResponseWriter, r *http.Request) {
|
func (h *Handler) handlePicoSetup(w http.ResponseWriter, r *http.Request) {
|
||||||
changed, err := h.ensurePicoChannel(r.Header.Get("Origin"))
|
changed, err := h.EnsurePicoChannel(r.Header.Get("Origin"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
|
|
|
||||||
|
|
@ -18,12 +18,12 @@ func TestEnsurePicoChannel_FreshConfig(t *testing.T) {
|
||||||
configPath := filepath.Join(t.TempDir(), "config.json")
|
configPath := filepath.Join(t.TempDir(), "config.json")
|
||||||
h := NewHandler(configPath)
|
h := NewHandler(configPath)
|
||||||
|
|
||||||
changed, err := h.ensurePicoChannel("")
|
changed, err := h.EnsurePicoChannel("")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("ensurePicoChannel() error = %v", err)
|
t.Fatalf("EnsurePicoChannel() error = %v", err)
|
||||||
}
|
}
|
||||||
if !changed {
|
if !changed {
|
||||||
t.Fatal("ensurePicoChannel() should report changed on a fresh config")
|
t.Fatal("EnsurePicoChannel() should report changed on a fresh config")
|
||||||
}
|
}
|
||||||
|
|
||||||
cfg, err := config.LoadConfig(configPath)
|
cfg, err := config.LoadConfig(configPath)
|
||||||
|
|
@ -43,8 +43,8 @@ func TestEnsurePicoChannel_DoesNotEnableTokenQuery(t *testing.T) {
|
||||||
configPath := filepath.Join(t.TempDir(), "config.json")
|
configPath := filepath.Join(t.TempDir(), "config.json")
|
||||||
h := NewHandler(configPath)
|
h := NewHandler(configPath)
|
||||||
|
|
||||||
if _, err := h.ensurePicoChannel(""); err != nil {
|
if _, err := h.EnsurePicoChannel(""); err != nil {
|
||||||
t.Fatalf("ensurePicoChannel() error = %v", err)
|
t.Fatalf("EnsurePicoChannel() error = %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
cfg, err := config.LoadConfig(configPath)
|
cfg, err := config.LoadConfig(configPath)
|
||||||
|
|
@ -61,8 +61,8 @@ func TestEnsurePicoChannel_DoesNotSetWildcardOrigins(t *testing.T) {
|
||||||
configPath := filepath.Join(t.TempDir(), "config.json")
|
configPath := filepath.Join(t.TempDir(), "config.json")
|
||||||
h := NewHandler(configPath)
|
h := NewHandler(configPath)
|
||||||
|
|
||||||
if _, err := h.ensurePicoChannel("http://localhost:18800"); err != nil {
|
if _, err := h.EnsurePicoChannel("http://localhost:18800"); err != nil {
|
||||||
t.Fatalf("ensurePicoChannel() error = %v", err)
|
t.Fatalf("EnsurePicoChannel() error = %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
cfg, err := config.LoadConfig(configPath)
|
cfg, err := config.LoadConfig(configPath)
|
||||||
|
|
@ -81,8 +81,8 @@ func TestEnsurePicoChannel_NoOriginWithoutCaller(t *testing.T) {
|
||||||
configPath := filepath.Join(t.TempDir(), "config.json")
|
configPath := filepath.Join(t.TempDir(), "config.json")
|
||||||
h := NewHandler(configPath)
|
h := NewHandler(configPath)
|
||||||
|
|
||||||
if _, err := h.ensurePicoChannel(""); err != nil {
|
if _, err := h.EnsurePicoChannel(""); err != nil {
|
||||||
t.Fatalf("ensurePicoChannel() error = %v", err)
|
t.Fatalf("EnsurePicoChannel() error = %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
cfg, err := config.LoadConfig(configPath)
|
cfg, err := config.LoadConfig(configPath)
|
||||||
|
|
@ -102,8 +102,8 @@ func TestEnsurePicoChannel_SetsCallerOrigin(t *testing.T) {
|
||||||
h := NewHandler(configPath)
|
h := NewHandler(configPath)
|
||||||
|
|
||||||
lanOrigin := "http://192.168.1.9:18800"
|
lanOrigin := "http://192.168.1.9:18800"
|
||||||
if _, err := h.ensurePicoChannel(lanOrigin); err != nil {
|
if _, err := h.EnsurePicoChannel(lanOrigin); err != nil {
|
||||||
t.Fatalf("ensurePicoChannel() error = %v", err)
|
t.Fatalf("EnsurePicoChannel() error = %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
cfg, err := config.LoadConfig(configPath)
|
cfg, err := config.LoadConfig(configPath)
|
||||||
|
|
@ -131,12 +131,12 @@ func TestEnsurePicoChannel_PreservesUserSettings(t *testing.T) {
|
||||||
|
|
||||||
h := NewHandler(configPath)
|
h := NewHandler(configPath)
|
||||||
|
|
||||||
changed, err := h.ensurePicoChannel("")
|
changed, err := h.EnsurePicoChannel("")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("ensurePicoChannel() error = %v", err)
|
t.Fatalf("EnsurePicoChannel() error = %v", err)
|
||||||
}
|
}
|
||||||
if changed {
|
if changed {
|
||||||
t.Error("ensurePicoChannel() should not change a fully configured config")
|
t.Error("EnsurePicoChannel() should not change a fully configured config")
|
||||||
}
|
}
|
||||||
|
|
||||||
cfg, err = config.LoadConfig(configPath)
|
cfg, err = config.LoadConfig(configPath)
|
||||||
|
|
@ -169,12 +169,12 @@ func TestEnsurePicoChannel_ExistingConfigWithoutSecurityFile(t *testing.T) {
|
||||||
|
|
||||||
h := NewHandler(configPath)
|
h := NewHandler(configPath)
|
||||||
|
|
||||||
changed, err := h.ensurePicoChannel("")
|
changed, err := h.EnsurePicoChannel("")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("ensurePicoChannel() error = %v", err)
|
t.Fatalf("EnsurePicoChannel() error = %v", err)
|
||||||
}
|
}
|
||||||
if !changed {
|
if !changed {
|
||||||
t.Fatal("ensurePicoChannel() should report changed when pico is missing")
|
t.Fatal("EnsurePicoChannel() should report changed when pico is missing")
|
||||||
}
|
}
|
||||||
|
|
||||||
cfg, err = config.LoadConfig(configPath)
|
cfg, err = config.LoadConfig(configPath)
|
||||||
|
|
@ -193,6 +193,33 @@ func TestEnsurePicoChannel_ExistingConfigWithoutSecurityFile(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestEnsurePicoChannel_ConfiguresPicoWithoutGateway(t *testing.T) {
|
||||||
|
configPath := filepath.Join(t.TempDir(), "config.json")
|
||||||
|
|
||||||
|
cfg := config.DefaultConfig()
|
||||||
|
cfg.Agents.Defaults.ModelName = ""
|
||||||
|
if err := config.SaveConfig(configPath, cfg); err != nil {
|
||||||
|
t.Fatalf("SaveConfig() error = %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
h := NewHandler(configPath)
|
||||||
|
if _, err := h.EnsurePicoChannel(""); err != nil {
|
||||||
|
t.Fatalf("EnsurePicoChannel() error = %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg, err := config.LoadConfig(configPath)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("LoadConfig() error = %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !cfg.Channels.Pico.Enabled {
|
||||||
|
t.Error("expected Pico to be enabled after launcher startup setup")
|
||||||
|
}
|
||||||
|
if cfg.Channels.Pico.Token() == "" {
|
||||||
|
t.Error("expected a non-empty token after launcher startup setup")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestEnsurePicoChannel_Idempotent(t *testing.T) {
|
func TestEnsurePicoChannel_Idempotent(t *testing.T) {
|
||||||
configPath := filepath.Join(t.TempDir(), "config.json")
|
configPath := filepath.Join(t.TempDir(), "config.json")
|
||||||
h := NewHandler(configPath)
|
h := NewHandler(configPath)
|
||||||
|
|
@ -200,20 +227,20 @@ func TestEnsurePicoChannel_Idempotent(t *testing.T) {
|
||||||
origin := "http://localhost:18800"
|
origin := "http://localhost:18800"
|
||||||
|
|
||||||
// First call sets things up
|
// First call sets things up
|
||||||
if _, err := h.ensurePicoChannel(origin); err != nil {
|
if _, err := h.EnsurePicoChannel(origin); err != nil {
|
||||||
t.Fatalf("first ensurePicoChannel() error = %v", err)
|
t.Fatalf("first EnsurePicoChannel() error = %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
cfg1, _ := config.LoadConfig(configPath)
|
cfg1, _ := config.LoadConfig(configPath)
|
||||||
token1 := cfg1.Channels.Pico.Token()
|
token1 := cfg1.Channels.Pico.Token()
|
||||||
|
|
||||||
// Second call should be a no-op
|
// Second call should be a no-op
|
||||||
changed, err := h.ensurePicoChannel(origin)
|
changed, err := h.EnsurePicoChannel(origin)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("second ensurePicoChannel() error = %v", err)
|
t.Fatalf("second EnsurePicoChannel() error = %v", err)
|
||||||
}
|
}
|
||||||
if changed {
|
if changed {
|
||||||
t.Error("second ensurePicoChannel() should not report changed")
|
t.Error("second EnsurePicoChannel() should not report changed")
|
||||||
}
|
}
|
||||||
|
|
||||||
cfg2, _ := config.LoadConfig(configPath)
|
cfg2, _ := config.LoadConfig(configPath)
|
||||||
|
|
|
||||||
|
|
@ -169,6 +169,9 @@ func main() {
|
||||||
|
|
||||||
// API Routes (e.g. /api/status)
|
// API Routes (e.g. /api/status)
|
||||||
apiHandler = api.NewHandler(absPath)
|
apiHandler = api.NewHandler(absPath)
|
||||||
|
if _, err = apiHandler.EnsurePicoChannel(""); err != nil {
|
||||||
|
logger.ErrorC("web", fmt.Sprintf("Warning: failed to ensure pico channel on startup: %v", err))
|
||||||
|
}
|
||||||
apiHandler.SetServerOptions(portNum, effectivePublic, explicitPublic, launcherCfg.AllowedCIDRs)
|
apiHandler.SetServerOptions(portNum, effectivePublic, explicitPublic, launcherCfg.AllowedCIDRs)
|
||||||
apiHandler.RegisterRoutes(mux)
|
apiHandler.RegisterRoutes(mux)
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue