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); // Make encrypted credentials readable for the runtime. Plain text from // older configurations stays unchanged and is taken over when saving. Security.ConfigSecrets.Unprotect(config); Validate(config, filePath); return config; } /// /// Lifts old configurations to the current schema. /// /// "maxTokens" used to be understood as a context limit, but counted cumulatively /// across all steps — normal runs therefore aborted prematurely. The value is /// not taken over, but replaced by the generous default for the cost budget; /// context control is taken over by maxContextTokens. /// public static void Migrate(InstanceConfig config) { foreach (var agent in config.Agents) Migrate(agent); } /// /// Migration for an individually loaded agent (AgentSettings.json). /// public static void Migrate(AgentConfig agent) { var guard = agent.LoopGuard; if (guard.LegacyMaxTokens is not { } legacy) return; // Only take over if deliberately configured more generously than the 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}"); } } }