using System.Text.Json; namespace ClawdDotNet.Core.Config; public static class ConfigLoader { private static readonly JsonSerializerOptions JsonOptions = new() { ReadCommentHandling = JsonCommentHandling.Skip, AllowTrailingCommas = true, PropertyNameCaseInsensitive = true }; public static async Task LoadAsync(string filePath, CancellationToken ct = default) { if (!File.Exists(filePath)) throw new FileNotFoundException($"Config file not found: {filePath}"); await using var stream = File.OpenRead(filePath); var config = await JsonSerializer.DeserializeAsync(stream, JsonOptions, ct) ?? throw new InvalidOperationException($"Config file is empty or invalid: {filePath}"); Migrate(config); // Verschlüsselte Zugangsdaten für die Laufzeit lesbar machen. Klartext aus // älteren Konfigurationen bleibt unverändert und wird beim Speichern übernommen. Security.ConfigSecrets.Unprotect(config); Validate(config, filePath); return config; } /// /// Hebt alte Konfigurationen auf das aktuelle Schema. /// /// "maxTokens" wurde früher als Kontextgrenze verstanden, zählte aber kumulativ /// über alle Schritte — dadurch brachen normale Läufe vorzeitig ab. Der Wert wird /// nicht übernommen, sondern durch den großzügigen Default für das Kostenbudget /// ersetzt; die Kontextsteuerung übernimmt maxContextTokens. /// public static void Migrate(InstanceConfig config) { foreach (var agent in config.Agents) Migrate(agent); } /// /// Migration für einen einzeln geladenen Agenten (AgentSettings.json). /// public static void Migrate(AgentConfig agent) { var guard = agent.LoopGuard; if (guard.LegacyMaxTokens is not { } legacy) return; // Nur übernehmen, wenn bewusst großzügiger konfiguriert als der Default. if (legacy > guard.MaxCumulativeTokens) guard.MaxCumulativeTokens = legacy; guard.LegacyMaxTokens = null; } private static void Validate(InstanceConfig config, string filePath) { if (string.IsNullOrWhiteSpace(config.OpenRouterApiKey)) throw new InvalidOperationException($"'openRouterApiKey' is required in {filePath}"); var agentIds = new HashSet(); foreach (var agent in config.Agents) { if (string.IsNullOrWhiteSpace(agent.AgentId)) throw new InvalidOperationException($"Every agent must have an 'agentId' in {filePath}"); if (!agentIds.Add(agent.AgentId)) throw new InvalidOperationException($"Duplicate agentId '{agent.AgentId}' in {filePath}"); } } }