Files
hanzoai_agent/control-plane/cmd/ha/main.go
zeekay 63bde13037 fix: replace invalid hyphenated Go identifiers with camelCase
The bulk rename from agentfield to hanzo-agents introduced hyphens into
Go variable names (e.g. hanzo-agentsHome), which are not valid Go
identifiers. Fixed 361 occurrences across 36 files to use camelCase
(e.g. hanzoAgentsHome). String literals like ".hanzo-agents" are
preserved as-is.
2026-02-13 12:43:38 -08:00

422 lines
13 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/logger"
"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"
"github.com/spf13/viper"
)
// Build-time version information (set via ldflags during build)
var (
version = "dev"
commit = "none"
date = "unknown"
)
func main() {
// Create version info to pass to CLI
versionInfo := cli.VersionInfo{
Version: version,
Commit: commit,
Date: date,
}
// Create and execute the root command with actual server functionality
rootCmd := cli.NewRootCommand(runServer, versionInfo)
if err := rootCmd.Execute(); err != nil {
logger.Logger.Error().Err(err).Msg("Error executing root command")
os.Exit(1)
}
}
// runServer contains the server startup logic for unified CLI
func runServer(cmd *cobra.Command, args []string) {
logger.Logger.Debug().Msg("HanzoAgents server starting...")
// Load configuration with better defaults
cfgFilePath, _ := cmd.Flags().GetString("config")
cfg, err := loadConfig(cfgFilePath)
if err != nil {
logger.Logger.Fatal().Err(err).Msg("Failed to load configuration")
}
// 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"
cfg.UI.Enabled = true
}
// Disable execution VC generation if flag is set
if noVC, err := cmd.Flags().GetBool("no-vc-execution"); err == nil && noVC {
cfg.Features.DID.VCRequirements.RequireVCForExecution = false
cfg.Features.DID.VCRequirements.PersistExecutionVC = false
logger.Logger.Warn().Msg("Execution VC generation disabled via --no-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 := buildUI(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 := server.NewHanzoAgentsServer(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 := hanzoAgentsServer.Start(); err != nil {
log.Fatalf("Failed to start HanzoAgents server: %v", err)
}
}()
// Wait a moment for the server to start before opening browser
time.Sleep(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)
openBrowser(uiTargetURL)
}
fmt.Printf("HanzoAgents server running on http://localhost:%d\n", cfg.HanzoAgents.Port)
fmt.Printf("Press Ctrl+C to exit.\n")
// Keep main goroutine alive
select {}
}
// loadConfig loads configuration with sensible defaults for user experience
func loadConfig(configFile string) (*config.Config, error) {
// Set environment variable prefixes
viper.SetEnvPrefix("HANZO_AGENTS")
viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
viper.AutomaticEnv()
// 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")
// Get the directory where the binary is located for UI paths
execPath, err := os.Executable()
if err != nil {
log.Printf("Warning: Could not determine executable path: %v", err)
execPath = "."
}
execDir := filepath.Dir(execPath)
if configFile != "" {
viper.SetConfigFile(configFile)
} else {
// Check for config file path from environment
if envConfigFile := os.Getenv("HANZO_AGENTS_CONFIG_FILE"); envConfigFile != "" {
viper.SetConfigFile(envConfigFile)
} else {
// Look for config in user's home directory first, then relative to exec dir, then local
homeDir, _ := os.UserHomeDir()
viper.AddConfigPath(filepath.Join(homeDir, ".hanzo-agents"))
viper.AddConfigPath(filepath.Join(execDir, "config"))
viper.AddConfigPath("./config")
viper.AddConfigPath(".")
viper.SetConfigName("hanzo-agents")
viper.SetConfigType("yaml")
}
}
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)
}
}
var cfg config.Config
if err := viper.Unmarshal(&cfg); err != nil {
return nil, fmt.Errorf("failed to unmarshal config: %w", err)
}
// Apply sensible defaults for user experience
if cfg.HanzoAgents.Port == 0 {
cfg.HanzoAgents.Port = 8080
}
// Enable UI by default unless explicitly disabled
if cfg.UI.Mode == "" {
cfg.UI.Mode = "embedded"
}
// Always enable UI by default unless explicitly disabled
if !viper.IsSet("ui.enabled") {
cfg.UI.Enabled = true
}
if cfg.UI.DevPort == 0 {
cfg.UI.DevPort = 5173
}
if cfg.UI.SourcePath == "" {
candidateSourcePaths := []string{
filepath.Join(execDir, "web", "client"),
filepath.Join(filepath.Dir(execDir), "apps", "platform", "hanzo-agents", "web", "client"),
filepath.Join("apps", "platform", "hanzo-agents", "web", "client"),
filepath.Join("web", "client"),
}
for _, candidate := range candidateSourcePaths {
if _, err := os.Stat(candidate); err == nil {
cfg.UI.SourcePath = candidate
break
}
}
if cfg.UI.SourcePath == "" {
cfg.UI.SourcePath = filepath.Join("web", "client")
}
}
if cfg.UI.DistPath == "" {
candidateDistPaths := []string{
filepath.Join(cfg.UI.SourcePath, "dist"),
filepath.Join(execDir, "web", "client", "dist"),
filepath.Join(filepath.Dir(execDir), "apps", "platform", "hanzo-agents", "web", "client", "dist"),
filepath.Join("apps", "platform", "hanzo-agents", "web", "client", "dist"),
filepath.Join("web", "client", "dist"),
}
for _, candidate := range candidateDistPaths {
if _, err := os.Stat(candidate); err == nil {
cfg.UI.DistPath = candidate
break
}
}
if cfg.UI.DistPath == "" {
cfg.UI.DistPath = filepath.Join("web", "client", "dist")
}
}
// Ensure VC generation/persistence defaults remain enabled unless explicitly disabled
if cfg.Features.DID.Enabled {
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"
}
}
// Set default storage mode to local if not specified
if cfg.Storage.Mode == "" {
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)
}
}
fmt.Printf("Loaded config - Storage mode: %s, HanzoAgents Port: %d, UI Mode: %s, UI Enabled: %t\n",
cfg.Storage.Mode, cfg.HanzoAgents.Port, cfg.UI.Mode, cfg.UI.Enabled)
return &cfg, nil
}
func buildUI(cfg *config.Config) error {
uiDir := cfg.UI.SourcePath
if uiDir == "" {
uiDir = "./web/client"
}
// 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
}
fmt.Printf("Building UI in %s...\n", uiDir)
// Set environment variables for the build process
buildEnv := os.Environ()
if cfg.UI.DistPath != "" {
buildEnv = append(buildEnv, fmt.Sprintf("VITE_BUILD_OUT_DIR=%s", filepath.Base(cfg.UI.DistPath)))
}
buildEnv = append(buildEnv, fmt.Sprintf("VITE_API_PROXY_TARGET=http://localhost:%d", cfg.HanzoAgents.Port))
// Install dependencies
cmdInstall := exec.Command("npm", "install", "--force")
cmdInstall.Dir = uiDir
cmdInstall.Env = buildEnv
cmdInstall.Stdout = os.Stdout
cmdInstall.Stderr = os.Stderr
if err := cmdInstall.Run(); err != nil {
return fmt.Errorf("failed to install UI dependencies: %w", err)
}
// Build for production
cmdBuild := exec.Command("npm", "run", "build")
cmdBuild.Dir = uiDir
cmdBuild.Env = buildEnv
cmdBuild.Stdout = os.Stdout
cmdBuild.Stderr = os.Stderr
if err := cmdBuild.Run(); err != nil {
return fmt.Errorf("failed to build UI: %w", err)
}
return nil
}
func openBrowser(url string) {
var err error
switch runtime.GOOS {
case "linux":
err = exec.Command("xdg-open", url).Start()
case "windows":
err = exec.Command("rundll32", "url.dll,FileProtocolHandler", url).Start()
case "darwin":
err = exec.Command("open", url).Start()
default:
err = fmt.Errorf("unsupported platform")
}
if err != nil {
log.Printf("Failed to open browser: %v. Please open manually: %s", err, url)
}
}