Files
go-sip/internal/config/config.go
T

234 lines
10 KiB
Go

package config
import (
"errors"
"fmt"
"os"
"strconv"
"strings"
"time"
)
type Config struct {
Mode string
DBPath string
SpoolRoot string
RabbitURL string
Exchange string
OutboxBatch int
AgentID string
DispatcherID string
Version string
ControlListen string
ControlToken string
CellID string
GRPCListen string
DispatcherGRPCListen string
DispatcherGRPCEndpoint string
DispatcherGRPCServerName string
DispatcherGRPCAllowedAgentIDs string
MTLSCAFile string
MTLSCertFile string
MTLSKeyFile string
MTLSServerName string
MTLSPeerCertificateFingerprints string
OSSRegion string
OSSEndpoint string
OSSBucket string
OSSAccessKeyID string
OSSAccessKeySecret string
OSSKeyPrefix string
OSSGrantTTL time.Duration
OSSMaxAssetBytes int64
StaticArtifactPath string
AgentEndpointsFile string
CallLogPath string
CallLogPhoneKey string
ARIURL string
ARIWebsocketURL string
ARIApplication string
ARIUsername string
ARIPassword string
CallTarget string
CallTrunkID string
CallCallerID string
CallMediaBind string
CallMediaPort int
CallRecordingDirectory string
CallAISnapshotPath string
CallTenantID string
CallTenantKey string
CallTaskID string
CallTaskItemID string
CallExecutionID string
}
func FromEnv() Config {
return Config{
Mode: envOr("SIP_GO_AGENT_MODE", "mock"),
DBPath: envOr("DISPATCHER_DB", "dispatcher.db"),
SpoolRoot: envOr("AGENT_SPOOL", "./spool"),
RabbitURL: os.Getenv("RABBITMQ_URL"),
Exchange: envOr("RABBITMQ_EXCHANGE", "agent-call.commands.v1"),
OutboxBatch: 50,
AgentID: envOr("AGENT_ID", "agent-local"),
DispatcherID: envOr("DISPATCHER_ID", "dispatcher-local"),
Version: envOr("AGENT_VERSION", "dev"),
ControlListen: os.Getenv("DISPATCHER_CONTROL_LISTEN"),
ControlToken: os.Getenv("DISPATCHER_CONTROL_TOKEN"),
CellID: envOr("CELL_ID", "cell-local"),
GRPCListen: os.Getenv("AGENT_GRPC_LISTEN"),
DispatcherGRPCListen: os.Getenv("DISPATCHER_GRPC_LISTEN"),
DispatcherGRPCEndpoint: os.Getenv("DISPATCHER_GRPC_ENDPOINT"),
DispatcherGRPCServerName: envOr("DISPATCHER_GRPC_SERVER_NAME", os.Getenv("MTLS_SERVER_NAME")),
DispatcherGRPCAllowedAgentIDs: os.Getenv("DISPATCHER_ALLOWED_AGENT_IDS"),
MTLSCAFile: os.Getenv("MTLS_CA_FILE"),
MTLSCertFile: os.Getenv("MTLS_CERT_FILE"),
MTLSKeyFile: os.Getenv("MTLS_KEY_FILE"),
MTLSServerName: os.Getenv("MTLS_SERVER_NAME"),
MTLSPeerCertificateFingerprints: os.Getenv("MTLS_PEER_CERT_FINGERPRINTS"),
OSSRegion: os.Getenv("DISPATCHER_OSS_REGION"),
OSSEndpoint: os.Getenv("DISPATCHER_OSS_ENDPOINT"),
OSSBucket: os.Getenv("DISPATCHER_OSS_BUCKET"),
OSSAccessKeyID: envOrSecret("DISPATCHER_OSS_ACCESS_KEY_ID", "DISPATCHER_OSS_ACCESS_KEY_ID_FILE"),
OSSAccessKeySecret: envOrSecret("DISPATCHER_OSS_ACCESS_KEY_SECRET", "DISPATCHER_OSS_ACCESS_KEY_SECRET_FILE"),
OSSKeyPrefix: envOr("DISPATCHER_OSS_KEY_PREFIX", "agent-call/recordings"),
OSSGrantTTL: time.Duration(envInt("DISPATCHER_OSS_GRANT_TTL_SECONDS", 900)) * time.Second,
OSSMaxAssetBytes: int64(envInt("DISPATCHER_OSS_MAX_ASSET_BYTES", 64<<20)),
StaticArtifactPath: os.Getenv("AGENT_STATIC_ARTIFACT"),
AgentEndpointsFile: os.Getenv("DISPATCHER_AGENT_ENDPOINTS_FILE"),
CallLogPath: os.Getenv("AGENT_CALL_BUSINESS_LOG"),
CallLogPhoneKey: os.Getenv("AGENT_CALL_PHONE_LOG_KEY"),
ARIURL: envOr("ARI_URL", "http://127.0.0.1:8088/ari"),
ARIWebsocketURL: envOr("ARI_WS_URL", "ws://127.0.0.1:8088/ari/events"),
ARIApplication: envOr("ARI_APPLICATION", "agent-call"),
ARIUsername: os.Getenv("ARI_USERNAME"),
ARIPassword: os.Getenv("ARI_PASSWORD"),
CallTarget: os.Getenv("AGENT_CALL_TARGET"),
CallTrunkID: os.Getenv("AGENT_CALL_TRUNK_ID"),
CallCallerID: os.Getenv("AGENT_CALL_CALLER_ID"),
CallMediaBind: envOr("AGENT_CALL_MEDIA_BIND", "127.0.0.1"),
CallMediaPort: envInt("AGENT_CALL_MEDIA_PORT", 12000),
CallRecordingDirectory: envOr("AGENT_CALL_RECORDING_DIR", "./recordings"),
CallAISnapshotPath: os.Getenv("AGENT_CALL_AI_SNAPSHOT"),
CallTenantID: os.Getenv("AGENT_CALL_TENANT_ID"),
CallTenantKey: os.Getenv("AGENT_CALL_TENANT_KEY"),
CallTaskID: os.Getenv("AGENT_CALL_TASK_ID"),
CallTaskItemID: os.Getenv("AGENT_CALL_TASK_ITEM_ID"),
CallExecutionID: os.Getenv("AGENT_CALL_EXECUTION_ID"),
}
}
func (c Config) Validate(role string) error {
if c.Mode != "mock" && c.Mode != "mixed" && c.Mode != "real" {
return fmt.Errorf("unsupported mode %q", c.Mode)
}
if role == "dispatcher" && strings.TrimSpace(c.DBPath) == "" {
return errors.New("dispatcher DB path is required")
}
if role == "agent" && strings.TrimSpace(c.SpoolRoot) == "" {
return errors.New("agent spool root is required")
}
if role == "agent" && strings.TrimSpace(c.CallLogPath) != "" && strings.TrimSpace(c.CallLogPhoneKey) == "" {
return errors.New("AGENT_CALL_PHONE_LOG_KEY is required when AGENT_CALL_BUSINESS_LOG is set")
}
if role == "agent" && strings.TrimSpace(c.CallLogPhoneKey) != "" && len([]byte(c.CallLogPhoneKey)) < 16 {
return errors.New("AGENT_CALL_PHONE_LOG_KEY must contain at least 16 bytes")
}
if role == "agent" && strings.TrimSpace(c.GRPCListen) != "" {
for name, value := range map[string]string{"MTLS_CA_FILE": c.MTLSCAFile, "MTLS_CERT_FILE": c.MTLSCertFile, "MTLS_KEY_FILE": c.MTLSKeyFile} {
if strings.TrimSpace(value) == "" {
return fmt.Errorf("%s is required when AGENT_GRPC_LISTEN is enabled", name)
}
}
}
if role == "agent" && c.Mode == "real" && strings.TrimSpace(c.GRPCListen) == "" {
return errors.New("real agent mode requires AGENT_GRPC_LISTEN")
}
if role == "agent" && c.Mode == "real" && strings.TrimSpace(c.StaticArtifactPath) == "" {
return errors.New("real agent mode requires AGENT_STATIC_ARTIFACT")
}
if role == "agent" && strings.TrimSpace(c.MTLSPeerCertificateFingerprints) != "" {
if _, err := ParseCertificateFingerprints(c.MTLSPeerCertificateFingerprints); err != nil {
return fmt.Errorf("invalid MTLS_PEER_CERT_FINGERPRINTS: %w", err)
}
}
if role == "dispatcher" && strings.TrimSpace(c.ControlListen) != "" && strings.TrimSpace(c.ControlToken) == "" {
return errors.New("control HTTP requires DISPATCHER_CONTROL_TOKEN")
}
if role == "agent" && strings.TrimSpace(c.DispatcherGRPCEndpoint) != "" {
for name, value := range map[string]string{"MTLS_CA_FILE": c.MTLSCAFile, "MTLS_CERT_FILE": c.MTLSCertFile, "MTLS_KEY_FILE": c.MTLSKeyFile} {
if strings.TrimSpace(value) == "" {
return fmt.Errorf("%s is required when DISPATCHER_GRPC_ENDPOINT is enabled", name)
}
}
}
if role == "dispatcher" && strings.TrimSpace(c.DispatcherGRPCListen) != "" {
for name, value := range map[string]string{"MTLS_CA_FILE": c.MTLSCAFile, "MTLS_CERT_FILE": c.MTLSCertFile, "MTLS_KEY_FILE": c.MTLSKeyFile} {
if strings.TrimSpace(value) == "" {
return fmt.Errorf("%s is required when DISPATCHER_GRPC_LISTEN is enabled", name)
}
}
for name, value := range map[string]string{"DISPATCHER_OSS_REGION": c.OSSRegion, "DISPATCHER_OSS_ENDPOINT": c.OSSEndpoint, "DISPATCHER_OSS_BUCKET": c.OSSBucket, "DISPATCHER_OSS_ACCESS_KEY_ID": c.OSSAccessKeyID, "DISPATCHER_OSS_ACCESS_KEY_SECRET": c.OSSAccessKeySecret} {
if strings.TrimSpace(value) == "" {
return fmt.Errorf("%s is required when Dispatcher gRPC is enabled", name)
}
}
if strings.TrimSpace(c.DispatcherGRPCAllowedAgentIDs) == "" {
return errors.New("DISPATCHER_ALLOWED_AGENT_IDS is required when Dispatcher gRPC is enabled")
}
fingerprints, err := ParseCertificateFingerprints(c.MTLSPeerCertificateFingerprints)
if err != nil {
return fmt.Errorf("invalid MTLS_PEER_CERT_FINGERPRINTS: %w", err)
}
if len(fingerprints) == 0 {
return errors.New("MTLS_PEER_CERT_FINGERPRINTS is required when Dispatcher gRPC is enabled")
}
}
if role == "dispatcher" && strings.TrimSpace(c.AgentEndpointsFile) != "" {
for name, value := range map[string]string{"MTLS_CA_FILE": c.MTLSCAFile, "MTLS_CERT_FILE": c.MTLSCertFile, "MTLS_KEY_FILE": c.MTLSKeyFile} {
if strings.TrimSpace(value) == "" {
return fmt.Errorf("%s is required when DISPATCHER_AGENT_ENDPOINTS_FILE is enabled", name)
}
}
}
if c.Mode == "real" && strings.TrimSpace(c.RabbitURL) == "" && role == "dispatcher" {
return errors.New("real dispatcher mode requires RABBITMQ_URL")
}
return nil
}
func envOr(key, fallback string) string {
if value := strings.TrimSpace(os.Getenv(key)); value != "" {
return value
}
return fallback
}
func envOrSecret(valueKey, fileKey string) string {
if value := strings.TrimSpace(os.Getenv(valueKey)); value != "" {
return value
}
path := strings.TrimSpace(os.Getenv(fileKey))
if path == "" {
return ""
}
data, err := os.ReadFile(path)
if err != nil {
return ""
}
return strings.TrimSpace(string(data))
}
func envInt(key string, fallback int) int {
value := strings.TrimSpace(os.Getenv(key))
if value == "" {
return fallback
}
parsed, err := strconv.Atoi(value)
if err != nil {
return fallback
}
return parsed
}