Files
zeekay 92257f4428 fix: apply cloud config env overrides in server startup
The main.go loadConfig function used viper but never called
Cloud.Defaults() or Cloud.ApplyEnvOverrides(), so env vars like
HANZO_AGENTS_CLOUD_ENABLED were ignored and cloud routes never registered.
2026-02-13 15:44:30 -08:00

498 lines
16 KiB
Go

package main
import (
"fmt"
"log"
"os"
"os/exec"
"path/filepath"
"runtime"
"strconv"
"strings"
"time"
"github.com/hanzoai/agents/control-plane/internal/cli"
"github.com/hanzoai/agents/control-plane/internal/config"
"github.com/hanzoai/agents/control-plane/internal/server"
"github.com/hanzoai/agents/control-plane/internal/utils"
"github.com/hanzoai/agents/control-plane/web/client"
"github.com/spf13/cobra" // Import cobra
"github.com/spf13/viper"
)
// Build-time version information (set via ldflags during build)
var (
version = "dev"
commit = "none"
date = "unknown"
)
// Test injection points
var (
loadConfigFunc = loadConfig
newHanzoAgentsServerFunc = server.NewHanzoAgentsServer
buildUIFunc = buildUI
openBrowserFunc = openBrowser
sleepFunc = time.Sleep
waitForShutdownFunc = func() { select {} }
commandRunner = defaultCommandRunner
browserLauncher = defaultBrowserLauncher
startHanzoAgentsServerFunc = defaultStartHanzoAgentsServer
)
// main function now acts as the entry point for the Cobra CLI.
func main() {
// Create version info to pass to CLI
versionInfo := cli.VersionInfo{
Version: version,
Commit: commit,
Date: date,
}
rootCmd := cli.NewRootCommand(runServer, versionInfo) // Initialize RootCmd and add subcommands
// Execute the root command
if err := rootCmd.Execute(); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
}
// runServer contains the original server startup logic.
// This function will be called by the Cobra command's Run field.
func runServer(cmd *cobra.Command, args []string) {
fmt.Println("HanzoAgents server starting...")
// Load configuration
cfgFilePath, _ := cmd.Flags().GetString("config")
cfg, err := loadConfigFunc(cfgFilePath)
if err != nil {
log.Fatalf("Failed to load configuration: %v", err)
}
// Override port from flag if provided
if cmd.Flags().Lookup("port").Changed {
port, _ := cmd.Flags().GetInt("port")
cfg.HanzoAgents.Port = port
}
// Override from environment variables
if envPort := os.Getenv("HANZO_AGENTS_PORT"); envPort != "" {
if port, err := strconv.Atoi(envPort); err == nil {
cfg.HanzoAgents.Port = port
}
}
storageModeExplicit := false
if flag := cmd.Flags().Lookup("storage-mode"); flag != nil && flag.Changed {
if mode, err := cmd.Flags().GetString("storage-mode"); err == nil && mode != "" {
cfg.Storage.Mode = mode
storageModeExplicit = true
}
}
if !storageModeExplicit {
if envMode := os.Getenv("HANZO_AGENTS_STORAGE_MODE"); envMode != "" {
cfg.Storage.Mode = envMode
}
}
var postgresURL string
if flag := cmd.Flags().Lookup("postgres-url"); flag != nil && flag.Changed {
postgresURL, _ = cmd.Flags().GetString("postgres-url")
}
if postgresURL == "" {
if env := os.Getenv("HANZO_AGENTS_POSTGRES_URL"); env != "" {
postgresURL = env
} else if env := os.Getenv("HANZO_AGENTS_STORAGE_POSTGRES_URL"); env != "" {
postgresURL = env
}
}
if postgresURL != "" {
cfg.Storage.Postgres.DSN = postgresURL
cfg.Storage.Postgres.URL = postgresURL
if !storageModeExplicit {
cfg.Storage.Mode = "postgres"
}
}
if env := os.Getenv("HANZO_AGENTS_STORAGE_POSTGRES_HOST"); env != "" {
cfg.Storage.Postgres.Host = env
}
if env := os.Getenv("HANZO_AGENTS_STORAGE_POSTGRES_PORT"); env != "" {
if port, err := strconv.Atoi(env); err == nil {
cfg.Storage.Postgres.Port = port
}
}
if env := os.Getenv("HANZO_AGENTS_STORAGE_POSTGRES_DATABASE"); env != "" {
cfg.Storage.Postgres.Database = env
}
if env := os.Getenv("HANZO_AGENTS_STORAGE_POSTGRES_USER"); env != "" {
cfg.Storage.Postgres.User = env
}
if env := os.Getenv("HANZO_AGENTS_STORAGE_POSTGRES_PASSWORD"); env != "" {
cfg.Storage.Postgres.Password = env
}
if env := os.Getenv("HANZO_AGENTS_STORAGE_POSTGRES_SSLMODE"); env != "" {
cfg.Storage.Postgres.SSLMode = env
}
if cfg.Storage.Mode == "" {
cfg.Storage.Mode = "local"
}
// Adjust config based on flags
backendOnly, _ := cmd.Flags().GetBool("backend-only")
if backendOnly {
cfg.UI.Enabled = false
}
uiDev, _ := cmd.Flags().GetBool("ui-dev")
if uiDev {
cfg.UI.Mode = "dev" // Assuming "dev" mode implies UI is enabled
cfg.UI.Enabled = true
}
// Disable execution VC generation if flag is set
noVCFlag := false
if flag := cmd.Flags().Lookup("no-vc-execution"); flag != nil {
if noVC, err := cmd.Flags().GetBool("no-vc-execution"); err == nil && noVC {
noVCFlag = true
cfg.Features.DID.VCRequirements.RequireVCForExecution = false
cfg.Features.DID.VCRequirements.PersistExecutionVC = false
fmt.Println("⚠️ Execution VC generation disabled via --no-vc-execution flag")
}
}
// Explicitly enable VC generation if requested (unless explicitly disabled)
if flag := cmd.Flags().Lookup("vc-execution"); flag != nil && flag.Changed {
if vcOn, err := cmd.Flags().GetBool("vc-execution"); err == nil && vcOn {
if noVCFlag {
fmt.Println("⚠️ Ignoring --vc-execution because --no-vc-execution is also set")
} else {
cfg.Features.DID.Enabled = true
cfg.Features.DID.VCRequirements.RequireVCForExecution = true
cfg.Features.DID.VCRequirements.PersistExecutionVC = true
fmt.Println("✅ Execution VC generation enabled via --vc-execution flag")
}
}
}
// Build UI if in embedded mode and not in ui-dev mode and UI is not already embedded
if cfg.UI.Enabled && cfg.UI.Mode == "embedded" && !uiDev && !client.IsUIEmbedded() {
fmt.Println("Building UI for embedded mode...")
if err := buildUIFunc(cfg); err != nil {
log.Printf("Warning: Failed to build UI, UI might not be available: %v", err)
} else {
fmt.Println("UI build successful.")
}
} else if cfg.UI.Enabled && cfg.UI.Mode == "embedded" && client.IsUIEmbedded() {
fmt.Println("UI is already embedded in binary, skipping build.")
}
// Create HanzoAgents server instance
hanzoAgentsServer, err := newHanzoAgentsServerFunc(cfg)
if err != nil {
log.Fatalf("Failed to create HanzoAgents server: %v", err)
}
// Start the server in a goroutine so we can open the browser
go func() {
fmt.Printf("HanzoAgents server attempting to start on port %d...\n", cfg.HanzoAgents.Port)
if err := startHanzoAgentsServerFunc(hanzoAgentsServer); err != nil {
log.Fatalf("Failed to start HanzoAgents server: %v", err)
}
}()
// Wait a moment for the server to start before opening browser
sleepFunc(1 * time.Second)
openBrowserFlag, _ := cmd.Flags().GetBool("open")
if cfg.UI.Enabled && openBrowserFlag && !backendOnly {
uiTargetURL := fmt.Sprintf("http://localhost:%d", cfg.HanzoAgents.Port)
if cfg.UI.Mode == "dev" {
// Use configured dev port or environment variable
devPort := cfg.UI.DevPort
if envDevPort := os.Getenv("VITE_DEV_PORT"); envDevPort != "" {
if port, err := strconv.Atoi(envDevPort); err == nil {
devPort = port
}
}
if devPort == 0 {
devPort = 5173 // Default Vite port
}
uiTargetURL = fmt.Sprintf("http://localhost:%d", devPort)
}
fmt.Printf("Opening browser to UI at %s...\n", uiTargetURL)
openBrowserFunc(uiTargetURL)
}
fmt.Printf("HanzoAgents server running. Press Ctrl+C to exit.\n")
// Keep main goroutine alive
waitForShutdownFunc()
// TODO: Implement graceful shutdown
}
// loadConfig loads configuration from file and environment variables.
func loadConfig(configFile string) (*config.Config, error) {
// Set environment variable prefixes
viper.SetEnvPrefix("HANZO_AGENTS")
viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
viper.AutomaticEnv() // read in environment variables that match
// Explicitly bind environment variables for API auth config
// This is needed because Viper's AutomaticEnv only works for keys that exist in config
_ = viper.BindEnv("api.auth.api_key", "HANZO_AGENTS_API_KEY")
_ = viper.BindEnv("api.auth.api_key", "HANZO_AGENTS_API_AUTH_API_KEY")
// Skip config file reading if explicitly set to /dev/null or empty
if configFile != "/dev/null" && configFile != "" {
viper.SetConfigFile(configFile)
if err := viper.ReadInConfig(); err != nil {
return nil, fmt.Errorf("failed to read config file: %w", err)
}
} else if configFile == "" {
// Check for config file path from environment
if envConfigFile := os.Getenv("HANZO_AGENTS_CONFIG_FILE"); envConfigFile != "" {
viper.SetConfigFile(envConfigFile)
} else {
viper.SetConfigName("hanzo-agents") // name of config file (without extension)
viper.SetConfigType("yaml") // type of the config file
// Look for config in user's home directory first, then local paths
if homeDir, err := os.UserHomeDir(); err == nil {
viper.AddConfigPath(filepath.Join(homeDir, ".hanzo-agents"))
}
viper.AddConfigPath("./config") // path to look for the config file in
viper.AddConfigPath(".") // optionally look for config in the working directory
}
if err := viper.ReadInConfig(); err != nil {
if _, ok := err.(viper.ConfigFileNotFoundError); ok {
fmt.Println("No config file found, using environment variables and defaults.")
} else {
return nil, fmt.Errorf("failed to read config file: %w", err)
}
}
} else {
fmt.Println("Skipping config file, using environment variables and defaults.")
}
var cfg config.Config
if err := viper.Unmarshal(&cfg); err != nil {
return nil, fmt.Errorf("failed to unmarshal config: %w", err)
}
// Apply defaults if not set
if cfg.HanzoAgents.Port == 0 {
cfg.HanzoAgents.Port = 8080 // Default port
}
if cfg.Storage.Mode == "" {
cfg.Storage.Mode = "local" // Default storage mode
}
// Enable UI by default
if !viper.IsSet("ui.enabled") {
cfg.UI.Enabled = true // Default UI enabled
}
if cfg.UI.Mode == "" {
cfg.UI.Mode = "embedded" // Default UI mode
}
// Enable DID by default - only disable if explicitly configured
if !viper.IsSet("features.did.enabled") {
cfg.Features.DID.Enabled = true // DEFAULT TO TRUE
}
// Apply DID sub-config defaults if DID is enabled
if cfg.Features.DID.Enabled {
if cfg.Features.DID.Method == "" {
cfg.Features.DID.Method = "did:key"
}
if cfg.Features.DID.KeyAlgorithm == "" {
cfg.Features.DID.KeyAlgorithm = "Ed25519"
}
if cfg.Features.DID.DerivationMethod == "" {
cfg.Features.DID.DerivationMethod = "BIP32"
}
if cfg.Features.DID.KeyRotationDays == 0 {
cfg.Features.DID.KeyRotationDays = 90
}
if cfg.Features.DID.Keystore.Type == "" {
cfg.Features.DID.Keystore.Type = "local"
}
if cfg.Features.DID.Keystore.Path == "" {
cfg.Features.DID.Keystore.Path = "./data/keys"
}
if cfg.Features.DID.Keystore.Encryption == "" {
cfg.Features.DID.Keystore.Encryption = "AES-256-GCM"
}
if !viper.IsSet("features.did.keystore.backup_enabled") {
cfg.Features.DID.Keystore.BackupEnabled = true
}
if cfg.Features.DID.Keystore.BackupInterval == "" {
cfg.Features.DID.Keystore.BackupInterval = "24h"
}
// Apply VC requirements defaults
if !viper.IsSet("features.did.vc_requirements.require_vc_registration") {
cfg.Features.DID.VCRequirements.RequireVCForRegistration = true
}
if !viper.IsSet("features.did.vc_requirements.require_vc_execution") {
cfg.Features.DID.VCRequirements.RequireVCForExecution = true
}
if !viper.IsSet("features.did.vc_requirements.require_vc_cross_agent") {
cfg.Features.DID.VCRequirements.RequireVCForCrossAgent = true
}
if !viper.IsSet("features.did.vc_requirements.store_input_output") {
cfg.Features.DID.VCRequirements.StoreInputOutput = false
}
if !viper.IsSet("features.did.vc_requirements.hash_sensitive_data") {
cfg.Features.DID.VCRequirements.HashSensitiveData = true
}
if !viper.IsSet("features.did.vc_requirements.persist_execution_vc") {
cfg.Features.DID.VCRequirements.PersistExecutionVC = true
}
if cfg.Features.DID.VCRequirements.StorageMode == "" {
cfg.Features.DID.VCRequirements.StorageMode = "inline"
}
}
if cfg.UI.Enabled && cfg.UI.DevPort == 0 {
cfg.UI.DevPort = 5173 // Default Vite dev port
}
if cfg.UI.SourcePath == "" {
// Get the executable path and find UI relative to it
execPath, err := os.Executable()
if err != nil {
cfg.UI.SourcePath = filepath.Join("apps", "platform", "hanzo-agents", "web", "client")
if _, statErr := os.Stat(cfg.UI.SourcePath); os.IsNotExist(statErr) {
cfg.UI.SourcePath = filepath.Join("web", "client")
}
} else {
execDir := filepath.Dir(execPath)
// Look for web/client relative to the executable directory
// This assumes the binary is built in the hanzo-agents/ directory
cfg.UI.SourcePath = filepath.Join(execDir, "web", "client")
// If that doesn't exist, try going up one level (if binary is in hanzo-agents/)
if _, err := os.Stat(cfg.UI.SourcePath); os.IsNotExist(err) {
cfg.UI.SourcePath = filepath.Join(filepath.Dir(execDir), "apps", "platform", "hanzo-agents", "web", "client")
}
// Final fallback to current working directory
if _, err := os.Stat(cfg.UI.SourcePath); os.IsNotExist(err) {
altPath := filepath.Join("apps", "platform", "hanzo-agents", "web", "client")
if _, altErr := os.Stat(altPath); altErr == nil {
cfg.UI.SourcePath = altPath
} else {
cfg.UI.SourcePath = filepath.Join("web", "client")
}
}
}
}
if cfg.UI.DistPath == "" {
cfg.UI.DistPath = filepath.Join(cfg.UI.SourcePath, "dist") // Default UI dist path relative to source
}
// Set default storage paths for local mode using universal path management
if cfg.Storage.Mode == "local" {
// Use the universal path management system
if cfg.Storage.Local.DatabasePath == "" {
dbPath, err := utils.GetDatabasePath()
if err != nil {
return nil, fmt.Errorf("failed to get database path: %w", err)
}
cfg.Storage.Local.DatabasePath = dbPath
}
if cfg.Storage.Local.KVStorePath == "" {
kvPath, err := utils.GetKVStorePath()
if err != nil {
return nil, fmt.Errorf("failed to get KV store path: %w", err)
}
cfg.Storage.Local.KVStorePath = kvPath
}
// Ensure all HanzoAgents data directories exist
if _, err := utils.EnsureDataDirectories(); err != nil {
return nil, fmt.Errorf("failed to create HanzoAgents data directories: %w", err)
}
}
// Apply cloud config defaults and env overrides
cfg.Cloud.Defaults()
cfg.Cloud.ApplyEnvOverrides()
fmt.Printf("Loaded config - Storage mode: %s, HanzoAgents Port: %d, UI Mode: %s, UI Enabled: %t, DID Enabled: %t, Cloud Enabled: %t\n",
cfg.Storage.Mode, cfg.HanzoAgents.Port, cfg.UI.Mode, cfg.UI.Enabled, cfg.Features.DID.Enabled, cfg.Cloud.Enabled)
return &cfg, nil
}
func buildUI(cfg *config.Config) error {
uiDir := cfg.UI.SourcePath
if uiDir == "" {
uiDir = "./web/client" // Default path
}
// Check if package.json exists
if _, err := os.Stat(filepath.Join(uiDir, "package.json")); os.IsNotExist(err) {
log.Printf("UI source path (%s) or package.json not found. Skipping UI build.", uiDir)
return nil // Not an error if UI is optional or pre-built
}
fmt.Printf("Building UI in %s...\n", uiDir)
// Set environment variables for the build process
buildEnv := os.Environ()
// Add Vite environment variables based on config
if cfg.UI.DistPath != "" {
buildEnv = append(buildEnv, fmt.Sprintf("VITE_BUILD_OUT_DIR=%s", filepath.Base(cfg.UI.DistPath)))
}
// Set API proxy target for development builds
buildEnv = append(buildEnv, fmt.Sprintf("VITE_API_PROXY_TARGET=http://localhost:%d", cfg.HanzoAgents.Port))
// Install dependencies
if err := commandRunner(uiDir, buildEnv, "npm", "install", "--force"); err != nil {
return fmt.Errorf("failed to install UI dependencies: %w", err)
}
// Build for production
if err := commandRunner(uiDir, buildEnv, "npm", "run", "build"); err != nil {
return fmt.Errorf("failed to build UI: %w", err)
}
return nil
}
func defaultCommandRunner(dir string, env []string, name string, args ...string) error {
cmd := exec.Command(name, args...)
cmd.Dir = dir
if len(env) > 0 {
cmd.Env = env
}
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
return cmd.Run()
}
func defaultBrowserLauncher(name string, args ...string) error {
return exec.Command(name, args...).Start()
}
func defaultStartHanzoAgentsServer(s *server.HanzoAgentsServer) error {
return s.Start()
}
func openBrowser(url string) {
var err error
switch runtime.GOOS {
case "linux":
err = browserLauncher("xdg-open", url)
case "windows":
err = browserLauncher("rundll32", "url.dll,FileProtocolHandler", url)
case "darwin":
err = browserLauncher("open", url)
default:
err = fmt.Errorf("unsupported platform")
}
if err != nil {
log.Printf("Failed to open browser: %v. Please open manually: %s", err, url)
}
}