Files
zeekay b2dc8d8956 feat: add agent canvas, collaboration, and IAM auth to control plane
- Add agent canvas with visual node editor, chat view, and collaboration cursors
- Add real-time collaboration infrastructure (contexts, hooks, presence)
- Add command palette, spaces, terminal panel, and side chat components
- Add IAM auth middleware and route handlers for server
- Add Hanzo branding (favicons, config, navigation updates)
- Update AuthContext/AuthGuard with enhanced auth flow
- Update Dockerfile and Makefile for control-plane builds
- Rebrand UI references and update dependencies across web client
2026-02-21 13:58:11 -08:00

276 lines
13 KiB
Go

package config
import (
"fmt" // Added for fmt.Errorf
"os" // Added for os.Stat, os.ReadFile
"path/filepath" // Added for filepath.Join
"strconv"
"time"
"gopkg.in/yaml.v3" // Added for yaml.Unmarshal
"github.com/hanzoai/agents/control-plane/internal/cloud"
"github.com/hanzoai/agents/control-plane/internal/storage"
)
// Config holds the entire configuration for the HanzoAgents server.
type Config struct {
HanzoAgents HanzoAgentsConfig `yaml:"hanzo-agents" mapstructure:"hanzo-agents"`
Features FeatureConfig `yaml:"features" mapstructure:"features"`
Storage StorageConfig `yaml:"storage" mapstructure:"storage"`
UI UIConfig `yaml:"ui" mapstructure:"ui"`
API APIConfig `yaml:"api" mapstructure:"api"`
Cloud CloudConfig `yaml:"cloud" mapstructure:"cloud"`
Billing BillingConfig `yaml:"billing" mapstructure:"billing"`
}
// BillingConfig holds prepaid credit billing configuration.
// Billing is always active — every execution is balance-gated and charged actual cost.
type BillingConfig struct {
CommerceURL string `yaml:"commerce_url" mapstructure:"commerce_url"` // BILLING_COMMERCE_URL
AdminToken string `yaml:"admin_token" mapstructure:"admin_token"` // BILLING_ADMIN_TOKEN
Currency string `yaml:"currency" mapstructure:"currency"` // default "usd"
}
// CloudConfig is an alias of the cloud package's configuration so callers can
// work with a single definition while keeping the canonical struct colocated
// with the implementation in the cloud package.
type CloudConfig = cloud.CloudConfig
// UIConfig holds configuration for the web UI.
type UIConfig struct {
Enabled bool `yaml:"enabled" mapstructure:"enabled"`
Mode string `yaml:"mode" mapstructure:"mode"` // "embedded", "dev", "separate"
SourcePath string `yaml:"source_path" mapstructure:"source_path"` // Path to UI source for building
DistPath string `yaml:"dist_path" mapstructure:"dist_path"` // Path to built UI assets for serving
DevPort int `yaml:"dev_port" mapstructure:"dev_port"` // Port for UI dev server
}
// HanzoAgentsConfig holds the core HanzoAgents server configuration.
type HanzoAgentsConfig struct {
Port int `yaml:"port"`
NodeHealth NodeHealthConfig `yaml:"node_health" mapstructure:"node_health"`
ExecutionCleanup ExecutionCleanupConfig `yaml:"execution_cleanup" mapstructure:"execution_cleanup"`
ExecutionQueue ExecutionQueueConfig `yaml:"execution_queue" mapstructure:"execution_queue"`
}
// NodeHealthConfig holds configuration for agent node health monitoring.
// Zero values are treated as "use default" — set explicitly to override.
type NodeHealthConfig struct {
CheckInterval time.Duration `yaml:"check_interval" mapstructure:"check_interval"` // How often to HTTP health check nodes (0 = default 10s)
CheckTimeout time.Duration `yaml:"check_timeout" mapstructure:"check_timeout"` // Timeout per HTTP health check (0 = default 5s)
ConsecutiveFailures int `yaml:"consecutive_failures" mapstructure:"consecutive_failures"` // Failures before marking inactive (0 = default 3; set 1 for instant)
RecoveryDebounce time.Duration `yaml:"recovery_debounce" mapstructure:"recovery_debounce"` // Wait before allowing inactive->active (0 = default 5s)
HeartbeatStaleThreshold time.Duration `yaml:"heartbeat_stale_threshold" mapstructure:"heartbeat_stale_threshold"` // Heartbeat age before marking stale (0 = default 60s)
}
// ExecutionCleanupConfig holds configuration for execution cleanup and garbage collection
type ExecutionCleanupConfig struct {
Enabled bool `yaml:"enabled" mapstructure:"enabled" default:"true"`
RetentionPeriod time.Duration `yaml:"retention_period" mapstructure:"retention_period" default:"24h"`
CleanupInterval time.Duration `yaml:"cleanup_interval" mapstructure:"cleanup_interval" default:"1h"`
BatchSize int `yaml:"batch_size" mapstructure:"batch_size" default:"100"`
PreserveRecentDuration time.Duration `yaml:"preserve_recent_duration" mapstructure:"preserve_recent_duration" default:"1h"`
StaleExecutionTimeout time.Duration `yaml:"stale_execution_timeout" mapstructure:"stale_execution_timeout" default:"30m"`
}
// ExecutionQueueConfig configures execution and webhook settings.
type ExecutionQueueConfig struct {
AgentCallTimeout time.Duration `yaml:"agent_call_timeout" mapstructure:"agent_call_timeout"`
WebhookTimeout time.Duration `yaml:"webhook_timeout" mapstructure:"webhook_timeout"`
WebhookMaxAttempts int `yaml:"webhook_max_attempts" mapstructure:"webhook_max_attempts"`
WebhookRetryBackoff time.Duration `yaml:"webhook_retry_backoff" mapstructure:"webhook_retry_backoff"`
WebhookMaxRetryBackoff time.Duration `yaml:"webhook_max_retry_backoff" mapstructure:"webhook_max_retry_backoff"`
}
// FeatureConfig holds configuration for enabling/disabling features.
type FeatureConfig struct {
DID DIDConfig `yaml:"did" mapstructure:"did"`
}
// DIDConfig holds configuration for DID identity system.
type DIDConfig struct {
Enabled bool `yaml:"enabled" mapstructure:"enabled" default:"true"`
Method string `yaml:"method" mapstructure:"method" default:"did:key"`
KeyAlgorithm string `yaml:"key_algorithm" mapstructure:"key_algorithm" default:"Ed25519"`
DerivationMethod string `yaml:"derivation_method" mapstructure:"derivation_method" default:"BIP32"`
KeyRotationDays int `yaml:"key_rotation_days" mapstructure:"key_rotation_days" default:"90"`
VCRequirements VCRequirements `yaml:"vc_requirements" mapstructure:"vc_requirements"`
Keystore KeystoreConfig `yaml:"keystore" mapstructure:"keystore"`
}
// VCRequirements holds VC generation requirements.
type VCRequirements struct {
RequireVCForRegistration bool `yaml:"require_vc_registration" mapstructure:"require_vc_registration" default:"true"`
RequireVCForExecution bool `yaml:"require_vc_execution" mapstructure:"require_vc_execution" default:"true"`
RequireVCForCrossAgent bool `yaml:"require_vc_cross_agent" mapstructure:"require_vc_cross_agent" default:"true"`
StoreInputOutput bool `yaml:"store_input_output" mapstructure:"store_input_output" default:"false"`
HashSensitiveData bool `yaml:"hash_sensitive_data" mapstructure:"hash_sensitive_data" default:"true"`
PersistExecutionVC bool `yaml:"persist_execution_vc" mapstructure:"persist_execution_vc" default:"true"`
StorageMode string `yaml:"storage_mode" mapstructure:"storage_mode" default:"inline"`
}
// KeystoreConfig holds keystore configuration.
type KeystoreConfig struct {
Type string `yaml:"type" mapstructure:"type" default:"local"`
Path string `yaml:"path" mapstructure:"path" default:"./data/keys"`
Encryption string `yaml:"encryption" mapstructure:"encryption" default:"AES-256-GCM"`
BackupEnabled bool `yaml:"backup_enabled" mapstructure:"backup_enabled" default:"true"`
BackupInterval string `yaml:"backup_interval" mapstructure:"backup_interval" default:"24h"`
}
// APIConfig holds configuration for API settings
type APIConfig struct {
CORS CORSConfig `yaml:"cors" mapstructure:"cors"`
Auth AuthConfig `yaml:"auth" mapstructure:"auth"`
}
// CORSConfig holds CORS configuration
type CORSConfig struct {
AllowedOrigins []string `yaml:"allowed_origins" mapstructure:"allowed_origins"`
AllowedMethods []string `yaml:"allowed_methods" mapstructure:"allowed_methods"`
AllowedHeaders []string `yaml:"allowed_headers" mapstructure:"allowed_headers"`
ExposedHeaders []string `yaml:"exposed_headers" mapstructure:"exposed_headers"`
AllowCredentials bool `yaml:"allow_credentials" mapstructure:"allow_credentials"`
}
// AuthConfig holds API authentication configuration.
type AuthConfig struct {
// APIKey is checked against headers or query params. Empty disables auth.
APIKey string `yaml:"api_key" mapstructure:"api_key"`
// SkipPaths allows bypassing auth for specific endpoints (e.g., health).
SkipPaths []string `yaml:"skip_paths" mapstructure:"skip_paths"`
// IAM (Casdoor) OAuth configuration
IAMEnabled bool `yaml:"iam_enabled" mapstructure:"iam_enabled"`
IAMEndpoint string `yaml:"iam_endpoint" mapstructure:"iam_endpoint"` // Internal endpoint (e.g., http://iam.hanzo.svc:8000)
IAMPublicEndpoint string `yaml:"iam_public_endpoint" mapstructure:"iam_public_endpoint"` // External endpoint (e.g., https://hanzo.id)
IAMClientID string `yaml:"iam_client_id" mapstructure:"iam_client_id"`
IAMClientSecret string `yaml:"iam_client_secret" mapstructure:"iam_client_secret"`
IAMOrganization string `yaml:"iam_organization" mapstructure:"iam_organization"`
IAMApplication string `yaml:"iam_application" mapstructure:"iam_application"`
}
// StorageConfig is an alias of the storage layer's configuration so callers can
// work with a single definition while keeping the canonical struct colocated
// with the implementation in the storage package.
type StorageConfig = storage.StorageConfig
// DefaultConfigPath is the default path for the af configuration file.
const DefaultConfigPath = "hanzo-agents.yaml" // Or "./hanzo-agents.yaml", "config/hanzo-agents.yaml" depending on convention
// LoadConfig reads the configuration from the given path or default paths.
func LoadConfig(configPath string) (*Config, error) {
if configPath == "" {
configPath = DefaultConfigPath
}
// Check if the specific path exists
if _, err := os.Stat(configPath); os.IsNotExist(err) {
// Fallback: try to find it in common locations relative to executable or CWD
// This part might need more sophisticated logic depending on project structure
// For now, let's assume configPath is either absolute or relative to CWD.
// If not found, try a common "config/" subdirectory
altPath := filepath.Join("config", "hanzo-agents.yaml")
if _, err2 := os.Stat(altPath); err2 == nil {
configPath = altPath
} else {
// If still not found, return the original error for the specified/default path
return nil, fmt.Errorf("configuration file not found at %s or default locations: %w", configPath, err)
}
}
data, err := os.ReadFile(configPath)
if err != nil {
return nil, fmt.Errorf("failed to read configuration file %s: %w", configPath, err)
}
var cfg Config
if err := yaml.Unmarshal(data, &cfg); err != nil {
return nil, fmt.Errorf("failed to parse configuration file %s: %w", configPath, err)
}
// Apply environment variable overrides
applyEnvOverrides(&cfg)
// Apply cloud config defaults and env overrides
cfg.Cloud.Defaults()
cfg.Cloud.ApplyEnvOverrides()
return &cfg, nil
}
// applyEnvOverrides applies environment variable overrides to the config.
// Environment variables take precedence over YAML config values.
func applyEnvOverrides(cfg *Config) {
// API Authentication
if apiKey := os.Getenv("HANZO_AGENTS_API_KEY"); apiKey != "" {
cfg.API.Auth.APIKey = apiKey
}
// Also support the nested path format for consistency
if apiKey := os.Getenv("HANZO_AGENTS_API_AUTH_API_KEY"); apiKey != "" {
cfg.API.Auth.APIKey = apiKey
}
// IAM Authentication overrides
if val := os.Getenv("HANZO_AGENTS_IAM_ENABLED"); val != "" {
cfg.API.Auth.IAMEnabled = val == "true" || val == "1"
}
if val := os.Getenv("HANZO_AGENTS_IAM_ENDPOINT"); val != "" {
cfg.API.Auth.IAMEndpoint = val
}
if val := os.Getenv("HANZO_AGENTS_IAM_PUBLIC_ENDPOINT"); val != "" {
cfg.API.Auth.IAMPublicEndpoint = val
}
if val := os.Getenv("HANZO_AGENTS_IAM_CLIENT_ID"); val != "" {
cfg.API.Auth.IAMClientID = val
}
if val := os.Getenv("HANZO_AGENTS_IAM_CLIENT_SECRET"); val != "" {
cfg.API.Auth.IAMClientSecret = val
}
if val := os.Getenv("HANZO_AGENTS_IAM_ORGANIZATION"); val != "" {
cfg.API.Auth.IAMOrganization = val
}
if val := os.Getenv("HANZO_AGENTS_IAM_APPLICATION"); val != "" {
cfg.API.Auth.IAMApplication = val
}
// Node health monitoring overrides
if val := os.Getenv("HANZO_AGENTS_HEALTH_CHECK_INTERVAL"); val != "" {
if d, err := time.ParseDuration(val); err == nil {
cfg.HanzoAgents.NodeHealth.CheckInterval = d
}
}
if val := os.Getenv("HANZO_AGENTS_HEALTH_CHECK_TIMEOUT"); val != "" {
if d, err := time.ParseDuration(val); err == nil {
cfg.HanzoAgents.NodeHealth.CheckTimeout = d
}
}
if val := os.Getenv("HANZO_AGENTS_HEALTH_CONSECUTIVE_FAILURES"); val != "" {
if i, err := strconv.Atoi(val); err == nil {
cfg.HanzoAgents.NodeHealth.ConsecutiveFailures = i
}
}
if val := os.Getenv("HANZO_AGENTS_HEALTH_RECOVERY_DEBOUNCE"); val != "" {
if d, err := time.ParseDuration(val); err == nil {
cfg.HanzoAgents.NodeHealth.RecoveryDebounce = d
}
}
if val := os.Getenv("HANZO_AGENTS_HEARTBEAT_STALE_THRESHOLD"); val != "" {
if d, err := time.ParseDuration(val); err == nil {
cfg.HanzoAgents.NodeHealth.HeartbeatStaleThreshold = d
}
}
// Billing overrides
if val := os.Getenv("BILLING_COMMERCE_URL"); val != "" {
cfg.Billing.CommerceURL = val
}
if val := os.Getenv("BILLING_ADMIN_TOKEN"); val != "" {
cfg.Billing.AdminToken = val
}
if val := os.Getenv("BILLING_CURRENCY"); val != "" {
cfg.Billing.Currency = val
}
}