// Package config defines CLI settings and validates values before they are // submitted to the daemon. The daemon owns persistence and credentials. package config import ( "errors" "net/url" "os" "path/filepath" "regexp" "strings" "unicode" ) var providerNameRegex = regexp.MustCompile(`^[a-zA-Z0-9][a-zA-Z0-9._-]{0,63}$`) // HomeDir returns the albedo configuration directory. func HomeDir() string { if env := os.Getenv("ALBEDO_HOME"); env != "" { abs, err := filepath.Abs(env) if err == nil { return abs } return env } home, err := os.UserHomeDir() if err != nil { return ".albedo" } return filepath.Join(home, ".albedo") } // Settings represents a provider configuration. Its api key lives in the // daemon's credential store. APIKey carries a newly entered draft value; // HasKey is presence metadata returned by the daemon. type Settings struct { ProfileName string `json:"-"` Effort *string `json:"effort,omitempty"` ImageEdge *int `json:"image_edge,omitempty"` AccountID *string `json:"account_id,omitempty"` Project *string `json:"project,omitempty"` Location *string `json:"location,omitempty"` Extension string `json:"extension,omitempty"` BaseURL string `json:"baseUrl,omitempty"` APIKey string `json:"apiKey,omitempty"` Model string `json:"model"` Protocol string `json:"protocol"` HasKey bool `json:"hasKey,omitempty"` } func validateModel(value string) (string, error) { trimmed := strings.TrimSpace(value) if trimmed == "" || len(trimmed) > 512 || strings.ContainsFunc(trimmed, func(r rune) bool { return r < 0x20 || r == 0x7f }) { return "", errors.New("model ID must be 1–512 characters and contain no control characters") } return trimmed, nil } // Validate checks whether the Settings instance is valid. func (s Settings) Validate() (Settings, error) { if s.Protocol != "responses" && s.Protocol != "chat_completions" { return s, errors.New("protocol must be responses or chat_completions") } model, err := validateModel(s.Model) if err != nil { return s, err } ext := s.Extension if ext == "" { ext = "openai" } var endpoint string if s.BaseURL != "" { endpoint, err = ValidateEndpoint(s.BaseURL) if err != nil { return s, err } } if ext == "openai" && endpoint == "" { return s, errors.New("OpenAI provider needs an API base URL") } if strings.ContainsFunc(s.APIKey, func(r rune) bool { return unicode.IsSpace(r) || r < 0x20 || r == 0x7f }) { return s, errors.New("API key must not contain spaces or control characters") } return Settings{ ProfileName: s.ProfileName, Effort: s.Effort, ImageEdge: s.ImageEdge, AccountID: s.AccountID, Project: s.Project, Location: s.Location, Extension: ext, BaseURL: endpoint, APIKey: s.APIKey, HasKey: s.HasKey, Model: model, Protocol: s.Protocol, }, nil } // ValidateProviderName validates a profile name. func ValidateProviderName(value string) (string, error) { name := strings.TrimSpace(value) if !providerNameRegex.MatchString(name) { return "", errors.New("provider name must start with a letter or number and use only letters, numbers, dots, underscores, or hyphens (1–64 characters)") } return name, nil } // ValidateEndpoint parses and validates an HTTP/HTTPS API endpoint without query, credentials, or fragment. func ValidateEndpoint(value string) (string, error) { u, err := url.Parse(strings.TrimSpace(value)) if err != nil || u.Scheme == "" || u.Host == "" { return "", errors.New("enter an HTTP or HTTPS API base URL") } if u.Scheme != "http" && u.Scheme != "https" { return "", errors.New("enter an HTTP or HTTPS API base URL") } if u.User != nil || u.RawQuery != "" || u.Fragment != "" { return "", errors.New("use an HTTP or HTTPS API base URL without credentials, a query, or a fragment") } return strings.TrimRight(u.String(), "/"), nil } // Profiles represents the stored provider configuration. type Profiles struct { ETag string `json:"-"` Providers map[string]Settings `json:"providers"` Active string `json:"active,omitempty"` }