package config import ( "bufio" "bytes" "encoding/json" "errors" "fmt" "os" "path/filepath" "strconv" "strings" ) var volcAccountNames = strings.Fields(`VOLC_ACCESS_KEY_ID VOLC_SECRET_ACCESS_KEY VOLC_SESSION_TOKEN VOLC_RTC_BASE_URL VOLC_RTC_REGION VOLC_RTC_API_VERSION RTC_APP_ID RTC_APP_KEY RTC_TOKEN_ISSUER VOLC_HARDWARE_INSTANCE_ID VOLC_HARDWARE_PRODUCT_KEY VOLC_HARDWARE_PRODUCT_SECRET VOLC_HARDWARE_AGENT_BOT_ID VOLC_HARDWARE_PROVISIONING_ENABLED VOLC_HARDWARE_AGENT_TRANSPORT VOLC_HARDWARE_AGENT_MINIMUM_SDK_VERSION VOLC_HARDWARE_REGISTER_URL VOLC_HARDWARE_DEVICE_NAME_PREFIX MIGRATION_HARDWARE_RTC_APP_ID MIGRATION_HARDWARE_LICENSE_READY S2S_APP_ID S2S_ACCESS_TOKEN S2S_MODEL S2S_SPEAKER VIKING_MEMORY_RESOURCE_ID VIKING_MEMORY_API_KEY VIKING_MEMORY_COLLECTION_NAME VIKING_MEMORY_EVENT_TYPE VIKING_MEMORY_PROFILE_TYPE TOS_RECORDING_ACCOUNT_ID TOS_RECORDING_BUCKET TOS_RECORDING_REGION TOS_RECORDING_PLAYBACK_ENDPOINT ARK_API_KEY ARK_BASE_URL ARK_CONTENT_MODEL ARK_EMBEDDING_MODEL CONTENT_GENERATOR EMBEDDING_PROVIDER VOLC_SUBTITLE_CALLBACK_URL VOLC_SUBTITLE_PRIMARY MIGRATION_NOTES`) // ReadVolcAccountFile reads a literal worksheet, never shell commands or expansion. // Errors identify the field/line only; credentials must never enter logs. func ReadVolcAccountFile(path string) (map[string]string, error) { if !filepath.IsAbs(path) { return nil, errors.New("VOLC_ACCOUNT_CONFIG_FILE must be absolute") } raw, err := os.ReadFile(path) if err != nil { return nil, errors.New("Volc account configuration unavailable") } if len(raw) > 65536 { return nil, errors.New("Volc account configuration too large") } allowed := map[string]bool{} for _, name := range volcAccountNames { allowed[name] = true } values := map[string]string{} s := bufio.NewScanner(bytes.NewReader(bytes.TrimPrefix(raw, []byte{0xef, 0xbb, 0xbf}))) line := 0 for s.Scan() { line++ text := strings.TrimSpace(s.Text()) if text == "" || strings.HasPrefix(text, "#") { continue } name, value, ok := strings.Cut(text, "=") name = strings.TrimSpace(name) if !ok || !allowed[name] { return nil, fmt.Errorf("Volc account configuration invalid field at line %d", line) } if _, found := values[name]; found { return nil, fmt.Errorf("Volc account configuration duplicate %s", name) } value = strings.TrimSpace(value) if len(value) >= 2 && ((value[0] == '\'' && value[len(value)-1] == '\'') || (value[0] == '"' && value[len(value)-1] == '"')) { value = value[1 : len(value)-1] } if strings.ContainsAny(value, "\x00\r\n") { return nil, fmt.Errorf("Volc account configuration invalid %s", name) } values[name] = value } if s.Err() != nil { return nil, errors.New("Volc account configuration unreadable") } for _, name := range volcAccountNames { if strings.HasPrefix(name, "MIGRATION_") { continue } if _, found := values[name]; !found { return nil, fmt.Errorf("Volc account configuration missing %s", name) } } return values, nil } func loadVolcAccount() (map[string]string, error) { path := strings.TrimSpace(os.Getenv("VOLC_ACCOUNT_CONFIG_FILE")) if path == "" { return nil, nil } return ReadVolcAccountFile(path) } func loadSecretValuesWithAccount(account map[string]string, names ...string) (map[string]string, error) { values := make(map[string]string, len(names)) for _, name := range names { if value, found := account[name]; found { values[name] = value continue } value, err := secretEnv(name) if err != nil { return nil, err } values[name] = value } return values, nil } func (c *Config) applyVolcAccount(values map[string]string) error { c.TOSRecordingAccountID = strings.TrimSpace(os.Getenv("TOS_RECORDING_ACCOUNT_ID")) c.TOSRecordingBucket = strings.TrimSpace(os.Getenv("TOS_RECORDING_BUCKET")) c.TOSRecordingRegion = strings.TrimSpace(os.Getenv("TOS_RECORDING_REGION")) c.TOSRecordingPlaybackEndpoint = strings.TrimSpace(os.Getenv("TOS_RECORDING_PLAYBACK_ENDPOINT")) if values == nil { return nil } c.volcAccount = values fields := map[string]*string{ "VOLC_ACCESS_KEY_ID": &c.VolcAccessKeyID, "VOLC_SECRET_ACCESS_KEY": &c.VolcSecretAccessKey, "VOLC_SESSION_TOKEN": &c.VolcSessionToken, "RTC_APP_ID": &c.RTCAppID, "RTC_APP_KEY": &c.RTCAppKey, "RTC_TOKEN_ISSUER": &c.RTCTokenIssuer, "VOLC_RTC_BASE_URL": &c.VolcRTCBaseURL, "VOLC_RTC_REGION": &c.VolcRTCRegion, "VOLC_RTC_API_VERSION": &c.VolcRTCAPIVersion, "VOLC_HARDWARE_INSTANCE_ID": &c.VolcHardwareInstanceID, "VOLC_HARDWARE_PRODUCT_KEY": &c.VolcHardwareProductKey, "VOLC_HARDWARE_PRODUCT_SECRET": &c.VolcHardwareProductSecret, "VOLC_HARDWARE_AGENT_BOT_ID": &c.VolcHardwareAgentBotID, "VOLC_HARDWARE_AGENT_TRANSPORT": &c.VolcHardwareTransport, "VOLC_HARDWARE_AGENT_MINIMUM_SDK_VERSION": &c.VolcHardwareMinimumSDKVersion, "VOLC_HARDWARE_REGISTER_URL": &c.VolcHardwareRegisterURL, "VOLC_HARDWARE_DEVICE_NAME_PREFIX": &c.VolcHardwareDeviceNamePrefix, "VIKING_MEMORY_RESOURCE_ID": &c.VikingResourceID, "VIKING_MEMORY_API_KEY": &c.VikingAPIKey, "VIKING_MEMORY_COLLECTION_NAME": &c.VikingCollectionName, "VIKING_MEMORY_EVENT_TYPE": &c.VikingEventType, "VIKING_MEMORY_PROFILE_TYPE": &c.VikingProfileType, "TOS_RECORDING_ACCOUNT_ID": &c.TOSRecordingAccountID, "TOS_RECORDING_BUCKET": &c.TOSRecordingBucket, "TOS_RECORDING_REGION": &c.TOSRecordingRegion, "TOS_RECORDING_PLAYBACK_ENDPOINT": &c.TOSRecordingPlaybackEndpoint, "ARK_API_KEY": &c.ArkAPIKey, "ARK_BASE_URL": &c.ArkBaseURL, "ARK_CONTENT_MODEL": &c.ArkContentModel, "ARK_EMBEDDING_MODEL": &c.ArkEmbeddingModel, "CONTENT_GENERATOR": &c.ContentGenerator, "EMBEDDING_PROVIDER": &c.EmbeddingProvider, "VOLC_SUBTITLE_CALLBACK_URL": &c.SubtitleCallbackURL, } for name, field := range fields { *field = values[name] } for name, field := range map[string]*bool{"VOLC_HARDWARE_PROVISIONING_ENABLED": &c.VolcHardwareProvisioningEnabled, "VOLC_SUBTITLE_PRIMARY": &c.SubtitlePrimary} { parsed, err := strconv.ParseBool(values[name]) if err != nil { return fmt.Errorf("Volc account configuration invalid boolean %s", name) } *field = parsed } for _, name := range []string{"VOLC_ACCESS_KEY_ID", "VOLC_SECRET_ACCESS_KEY", "RTC_APP_ID", "RTC_APP_KEY", "S2S_APP_ID", "S2S_ACCESS_TOKEN", "S2S_MODEL", "S2S_SPEAKER"} { if values[name] == "" { return fmt.Errorf("Volc account configuration requires %s", name) } } return nil } // ApplyVolcVoiceTemplate supplies credentials from the account file at startup. // Templates retain prompts and behaviour but are never an alternative key source. func (c Config) ApplyVolcVoiceTemplate(raw []byte) ([]byte, error) { if c.volcAccount == nil { return raw, nil } var root map[string]any if json.Unmarshal(raw, &root) != nil { return nil, errors.New("invalid Volc voice template") } items := []map[string]any{root} if bundle, ok := root["templates"].(map[string]any); ok { items = nil for _, entry := range bundle { item, ok := entry.(map[string]any) if !ok { return nil, errors.New("invalid Volc voice template entry") } items = append(items, item) } } updated := 0 for _, item := range items { conf, _ := item["Config"].(map[string]any) s2s, _ := conf["S2SConfig"].(map[string]any) if s2s == nil { continue } pp, _ := s2s["ProviderParams"].(map[string]any) if pp == nil { pp = map[string]any{} s2s["ProviderParams"] = pp } app, _ := pp["app"].(map[string]any) if app == nil { app = map[string]any{} pp["app"] = app } app["appid"] = c.volcAccount["S2S_APP_ID"] app["token"] = c.volcAccount["S2S_ACCESS_TOKEN"] dialog, _ := pp["dialog"].(map[string]any) if dialog == nil { dialog = map[string]any{} pp["dialog"] = dialog } extra, _ := dialog["extra"].(map[string]any) if extra == nil { extra = map[string]any{} dialog["extra"] = extra } extra["model"] = c.volcAccount["S2S_MODEL"] tts, _ := pp["tts"].(map[string]any) if tts == nil { tts = map[string]any{} pp["tts"] = tts } tts["speaker"] = c.volcAccount["S2S_SPEAKER"] conf["MemoryConfig"] = map[string]any{"Enable": false} updated++ } if updated == 0 { return nil, errors.New("Volc account requires an S2S voice template") } return json.Marshal(root) }