Merge pull request #2514 from lc6464/fix/issue-2488-host-binding

feat(launcher): add host overrides for launcher and gateway
This commit is contained in:
美電球
2026-04-14 23:48:24 +08:00
committed by GitHub
29 changed files with 2420 additions and 99 deletions
+6
View File
@@ -1136,6 +1136,8 @@ func LoadConfig(path string) (*Config, error) {
applyLegacyBindingsMigration(data, cfg)
gatewayHostBeforeEnv := cfg.Gateway.Host
if err = env.Parse(cfg); err != nil {
return nil, err
}
@@ -1144,6 +1146,10 @@ func LoadConfig(path string) (*Config, error) {
if err = InitChannelList(cfg.Channels); err != nil {
return nil, err
}
cfg.Gateway.Host, err = resolveGatewayHostFromEnv(gatewayHostBeforeEnv)
if err != nil {
return nil, fmt.Errorf("invalid gateway host: %w", err)
}
// Expand multi-key configs into separate entries for key-level failover
cfg.ModelList = expandMultiKeyModels(cfg.ModelList)
+2 -2
View File
@@ -503,7 +503,7 @@ func TestDefaultConfig_Temperature(t *testing.T) {
func TestDefaultConfig_Gateway(t *testing.T) {
cfg := DefaultConfig()
if cfg.Gateway.Host != "127.0.0.1" {
if cfg.Gateway.Host != "localhost" {
t.Error("Gateway host should have default value")
}
if cfg.Gateway.Port == 0 {
@@ -739,7 +739,7 @@ func TestConfig_Complete(t *testing.T) {
if cfg.Agents.Defaults.MaxToolIterations == 0 {
t.Error("MaxToolIterations should not be zero")
}
if cfg.Gateway.Host != "127.0.0.1" {
if cfg.Gateway.Host != "localhost" {
t.Error("Gateway host should have default value")
}
if cfg.Gateway.Port == 0 {
+1 -1
View File
@@ -259,7 +259,7 @@ func DefaultConfig() *Config {
},
},
Gateway: GatewayConfig{
Host: "127.0.0.1",
Host: "localhost",
Port: 18790,
HotReload: false,
LogLevel: DefaultGatewayLogLevel,
+1 -1
View File
@@ -39,7 +39,7 @@ const (
EnvBinary = "PICOCLAW_BINARY"
// EnvGatewayHost overrides the host address for the gateway server.
// Default: "127.0.0.1"
// Default: "localhost"
EnvGatewayHost = "PICOCLAW_GATEWAY_HOST"
)
+27
View File
@@ -3,8 +3,10 @@ package config
import (
"encoding/json"
"os"
"strings"
"github.com/sipeed/picoclaw/pkg/logger"
"github.com/sipeed/picoclaw/pkg/netbind"
)
const DefaultGatewayLogLevel = "warn"
@@ -49,6 +51,31 @@ func EffectiveGatewayLogLevel(cfg *Config) string {
return normalizeGatewayLogLevel(cfg.Gateway.LogLevel)
}
func resolveGatewayHostFromEnv(baseHost string) (string, error) {
envHost, ok := os.LookupEnv(EnvGatewayHost)
if !ok {
return normalizeGatewayHostInput(baseHost)
}
envHost = strings.TrimSpace(envHost)
if envHost == "" {
return normalizeGatewayHostInput(baseHost)
}
return normalizeGatewayHostInput(envHost)
}
func normalizeGatewayHostInput(host string) (string, error) {
host = strings.TrimSpace(host)
if host == "" {
host = strings.TrimSpace(DefaultConfig().Gateway.Host)
}
if host == "" {
host = "localhost"
}
return netbind.NormalizeHostInput(host)
}
// ResolveGatewayLogLevel reads the configured gateway log level without triggering
// the full config loader, so startup code can apply logging before config load logs run.
// The PICOCLAW_LOG_LEVEL environment variable overrides the file value.
+98
View File
@@ -0,0 +1,98 @@
package config
import (
"fmt"
"os"
"path/filepath"
"testing"
)
func writeGatewayHostTestConfig(t *testing.T, host string) string {
t.Helper()
configPath := filepath.Join(t.TempDir(), "config.json")
raw := fmt.Sprintf(`{"version":2,"gateway":{"host":%q,"port":18790}}`, host)
if err := os.WriteFile(configPath, []byte(raw), 0o600); err != nil {
t.Fatalf("WriteFile(configPath): %v", err)
}
return configPath
}
func TestLoadConfig_GatewayHostEnvTrimmed(t *testing.T) {
configPath := writeGatewayHostTestConfig(t, "127.0.0.1")
t.Setenv(EnvGatewayHost, " ::1 ")
cfg, err := LoadConfig(configPath)
if err != nil {
t.Fatalf("LoadConfig() error: %v", err)
}
if cfg.Gateway.Host != "::1" {
t.Fatalf("cfg.Gateway.Host = %q, want %q", cfg.Gateway.Host, "::1")
}
}
func TestLoadConfig_GatewayHostBlankEnvFallsBackToConfigHost(t *testing.T) {
configPath := writeGatewayHostTestConfig(t, " localhost ")
t.Setenv(EnvGatewayHost, " ")
cfg, err := LoadConfig(configPath)
if err != nil {
t.Fatalf("LoadConfig() error: %v", err)
}
want, err := normalizeGatewayHostInput("localhost")
if err != nil {
t.Fatalf("normalizeGatewayHostInput() error: %v", err)
}
if cfg.Gateway.Host != want {
t.Fatalf("cfg.Gateway.Host = %q, want %q", cfg.Gateway.Host, want)
}
}
func TestLoadConfig_GatewayHostBlankEnvAndConfigFallsBackToDefault(t *testing.T) {
configPath := writeGatewayHostTestConfig(t, " ")
t.Setenv(EnvGatewayHost, " ")
cfg, err := LoadConfig(configPath)
if err != nil {
t.Fatalf("LoadConfig() error: %v", err)
}
defaultHost, err := normalizeGatewayHostInput(DefaultConfig().Gateway.Host)
if err != nil {
t.Fatalf("normalizeGatewayHostInput() error: %v", err)
}
if cfg.Gateway.Host != defaultHost {
t.Fatalf("cfg.Gateway.Host = %q, want %q", cfg.Gateway.Host, defaultHost)
}
}
func TestLoadConfig_GatewayHostEnvPreservesExplicitWildcardHost(t *testing.T) {
configPath := writeGatewayHostTestConfig(t, "localhost")
t.Setenv(EnvGatewayHost, " 0.0.0.0 ")
cfg, err := LoadConfig(configPath)
if err != nil {
t.Fatalf("LoadConfig() error: %v", err)
}
want, err := normalizeGatewayHostInput("0.0.0.0")
if err != nil {
t.Fatalf("normalizeGatewayHostInput() error: %v", err)
}
if cfg.Gateway.Host != want {
t.Fatalf("cfg.Gateway.Host = %q, want %q", cfg.Gateway.Host, want)
}
}
func TestLoadConfig_GatewayHostEnvNormalizesMultiHostInput(t *testing.T) {
configPath := writeGatewayHostTestConfig(t, "localhost")
t.Setenv(EnvGatewayHost, " [::1] , 127.0.0.1 , ::1 ")
cfg, err := LoadConfig(configPath)
if err != nil {
t.Fatalf("LoadConfig() error: %v", err)
}
if cfg.Gateway.Host != "::1,127.0.0.1" {
t.Fatalf("cfg.Gateway.Host = %q, want %q", cfg.Gateway.Host, "::1,127.0.0.1")
}
}